././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768 mcomix-3.1.0/0000755000175000017500000000000014553265237012515 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0 mcomix-3.1.0/COPYING0000644000175000017500000004350214476523373013556 0ustar00moritzmoritzMComix is licensed under the terms of the GNU General Public License, which can be found below, or at http://www.gnu.org/licenses/gpl-2.0.html. --- GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1705863733.0 mcomix-3.1.0/ChangeLog.md0000644000175000017500000023316214553265065014674 0ustar00moritzmoritz# MComix 3.1.0 ## Release date: 2024-01-21 ### Features - Image colors can be negated in the Image Enhancement dialog. Furthermore, enhancements now apply to most UI elements, such as sidebar thumbnails, magnified lens image and library covers. - "Fit to size" has been generalised. Bounds can now be set independently for width and height, and different bounds can be applied to wide pages and other pages. ### Bug Fixes - Fixed regression from 2.0.0 in Library collection list. The bug prevented drag-and-dropping books from the main book area into a collection. - Added official MIME types for CBZ/CBR files to list of supported archive formats. - Fixed a bug that caused the "Recent" pseudo-collection in the Library to be displayed with the wrong localized name in some cases. - Fixed error when re-opening an archive in double-page mode, thanks to a patch by Spencer Berger. - Improved robustness to invalid UTF-8. - Fixed copy/paste error in application metadata XML file (thanks to Emfox Zhou) - The library search field now considers not only the book name, but also its full path, as the tooltip already stated. # MComix 3.0.0 ## Release date: 2023-09-16 ### Breaking Changes - MComix no longer uses the now obsolete setup.py-based installation and packaging. Instead, it is based on pyproject.toml with setuptools as build backend (since MComix already had a dependency on it anyway). - Due to this change, application meta files (such as mcomix.desktop) that usually go into /usr/share are no longer copied automatically by the installation process, since the Python ecosystem is moving away from packaging files outside of the Python package itself. - Source archives will be distributed in .tar.gz format only, since this is what the Python packaging specifications mandate. - When the package is installed, simply run "mcomix". `mcomixstarter.py` is no longer needed. - Refer to the installation documentation at https://sourceforge.net/p/mcomix/wiki/Installation/ for more information. - The Windows all-in-one ZIP package is now called simply mcomix-win64-\.zip. - Former `README` and `ChangeLog` have been converted to Markdown format and are now called `README.md` and `ChangeLog.md`. - On Win32, the MComix user folder, formerly in `%HOMEPATH%/MComix`, has been moved to `%APPDATA%/MComix` to be more in line with regular Windows directory conventions. The old directory will be automatically migrated on startup. ### Bug fixes - Fix regression from version 2.0.0 that triggered the wrong hotkey functionality when the SHIFT modifier was involved on Windows (for example, SHIFT+Space only triggered Space) - MComix should now be properly selectable as default application on Windows in the "Open with..." shell dialog. - Fixed broken thumbnail generation using the Python PDF extraction manager on Windows. - Fixed bug in Python PDF extraction that prevented rotated PDF images being displayed without rotation. - Fixed regression from version 2.2.0 that broke archive password handling. - MComix previously didn't remember to restore the "maximized" window state when restarting. - Fixed window not being restored at correct position after restarting when runnig on Windows. ### Features - MComix automatically switches to a dark theme if Windows color settings are set to "Dark". - MComix now has a Windows MSI installer. Users no longer have to manually extract the ZIP archive and move around files. # MComix 2.3.0 ## Release date: 2023-08-26 ### Bug fixes - Updated the bundled UnRAR64.dll in the Win32 All-In-One package to the latest version. Old versions may or may not be affected by a remote code execution vulnerability recently fixed in WinRAR 6.23. - Restored Python 3.7 compatibility in native PDF extractor. - Added missing pdf_native submodule to source distribution. ### Features - MComix now offers a high-resolution application icon. # MComix 2.2.1 ## Release date: 2023-07-12 ### Bug fixes - Added missing vendor package to source distribution. # MComix 2.2.0 ## Release date: 2023-07-11 ### Bug fixes - Fixed incorrect PDF transformations on systems using a recent version of MuPDF. - Fixed incompatibility with Pillow 10.0.0 due to bug in version check. ### Features - MComix can now use the PyMuPDF Python package to provide PDF reading capabilities, with improved extraction/decoding speed. - Added support for MobiPocket (AZW3) format books. Archives with DRM are not supported. - The OSD now shows the current page and the total number of pages. - The tabs of the Preferences dialog are scrollable so all dialog tabs can be properly used on smaller screens. - Updated the simplified Chinese translation. # MComix 2.1.1 ## Release date: 2023-05-15 ### Bug fixes - 7z.dll is again bundled with Windows all-in-one package. - Improved quality and speed of the magnifying lens. - Added new de facto IEC prefixes. - Window size should be remembered correctly again when restarting the application. - Replaced usage of deprecated GTK threading/timer functionality. - Fixed GLib application name. This improves integration with Gnome. # MComix 2.1.0 ## Release date: 2022-12-17 ### Bug fixes - Fixed byte/unicode error in library search text field. - Fixed DPI detection for PDFs which cannot be decoded using UTF-8. - Fixed magnifying lens errors when image was rotated. - Fixed another byte/unicode error in file chooser dialog. - 7z archives with encrypted header could not be extracted on Windows, as MComix did not properly parse that the archive needed password input. ### Features - Added option to customize space between pages in double-page mode. - Added options to open first file/archive when going backwards to previous archive/directory. - The "Fit to same size" option now results in more aggressively distorted images to make them fit, if necessary. The old, more conservative behaviour is available via the "Prefer same size" option. # MComix 2.0.2 ## Release date: 2022-05-20 ### Bug fixes - Fixed bytes/unicode error in library 'Add collection' dialog. - Fixed missing localization and image resources after calling "setup.py install" - Fixed bytes/unicode error in unrar executable extraction handler. - Fixed one more instance of incorrect color conversion from preferences. # MComix 2.0.1 ## Release date: 2022-03-09 ### Bug fixes - Fixed conversion of unexpected color values from stored preferences. - Fixed error trying to display page file size within archives where members have not been extracted yet. - Fixed bytes/unicode error in the library 'Add Book' dialog. - Fixed endless password popup when library fails to genrate thumbnail for password-protected archive. - Handle error when thumbnail metadata Thumb::MTime is a floating point number. - Fixed alphanumeric sort for names with mismatched text/number patterns. - Fixed unicode/bytes error in "Save as" dialog, as well as errors when saving double-page images. - Reduced declared minimum PyGObject version to 3.36.0 as well as PyCairo to 1.16.0. - Fixed minimum Pillow requirement in code not matching setup.py. # MComix 2.0.0 ## Release date: 2022-01-29 ### Breaking changes - MComix now requires Python 3.7 or newer, as well as GTK+ 3, PyGObject, and PyCairo. Minimum Pillow version has been increased to 6.0.0. - MComix no longer depends on the setuptools module. Optional dependencies on czipfile and subprocess32 have been removed, since they are no longer supported or necessary for Python 3. ### Known issues - The MComix window will not remember its last position on Windows. Code that works on Linux causes the window to fly off the screen for some reason. ### Features - Animated image formats are now supported. Previously, MComix would only display the first frame. - Supported image formats are now a combination of formats supported by GDK/Pixbuf and Pillow formats. ### Translation - Added Lithuanian translation - Updated Swedish and Korean translation # MComix 1.2.1 ## Release date: 2016-02-12 ### Environment/Locale/Translation: - Fixed a bug that made it impossible to open a book with MComix directly if the path contains spaces (Windows only) - Updated libraries for the Windows distribution: UnRAR DLL # MComix 1.2 ## Release date: 30.01.2016 ### Gui/Main - If metadata-based rotation is enabled, PNG files will be automatically rotated as well. - Double page mode respects Exif rotation now. - Some transformation issues have been fixed. In double page mode, all transformations are applied to the union of both pages. Also, reflection is performed first, followed by rotation. - Some OSD issues have been fixed. - When flipping pages, the content of the viewport does not appear somewhere else first anymore. - The default scaling quality is now "Bilinear". ### Gui/Thumbnailer - The thumbnailer now displays page numbers in a reasonable color appropriate for the respective background color. - The thumbnailer uses the same size for all thumbnails now. If thumbnails need to be rescaled, it is done using linear interpolation. - When using the keyboard, the thumbnailer now tries to keep the currently selected page in the upper half of its area. - The size of the thumbnailer is calculated more reasonably now. - Fixed a bug that could lead to crashes if the thumbnailer uses a dynamic background color. - The "missing image" icon appears in its original size in the thumbnailer. - Fix race condition that could lead to thumbnails being rendered with different sizes. - Added a workaround for a bug in gdk-pixbuf that could prevent thumbnails of animated GIF images from being rendered properly. For details, see https://bugzilla.gnome.org/show_bug.cgi?id=735422 - Re-enabled double buffering for the thumbnailer. ### Gui/Library - Some encoding issues with the library have been fixed. - The cover display in the library has been fixed. - The book area uses a tighter layout. - Various other issues with the library have been fixed. ### Gui/EditArchive - Applying changes in the "Edit archive" dialog could raise an exception under certain circumstances. This has been fixed. - Fixed a bug that prevented MComix from shutting down properly if an archive with no images in it has been opened or the "Edit archive" dialog has been used. - Some issues with displaying thumbnails in the "Edit archive" dialog have been fixed. ### Gui/WM - Fullscreen handling has been improved. - The Preferences dialog is not modal anymore. - When clicking on the thumbnailer while the main window is unfocused, the window should be focused only without switching to another page. This has been fixed so it works properly now. - Modal dialogs do not immediately hide the mouse cursor in the main area anymore. - Fix various minor window manager interaction issues. - The default window width is now 640 pixels. ### Gui/Misc - You can select the text in the Properties dialog now. - The "Continue reading" dialog defaults to "Yes" now. - Dialogs refresh their respective contents whenever you switch to another page or book. - The password dialog now displays the path of the archive. - Overall widget handling has been fixed and improved. This also eliminates some GTK warnings. - Fixed a lot of issues with empty directories and empty archives. - File name filters and supported formats handling have been improved. - Various other issues with the Preferences dialog and the Properties dialog have been fixed. - Recently opened PDF files are now listed in the "Recent Files" menu. ### Environment/Locale/Translations - The list of supported image formats is now determined dynamically, depending on the underlying libraries. This might implicitly add support for image formats such as WebP. - Due to a bug, PIL (or Pillow) was preferred over GdkPixbuf on Windows in earlier versions. Now, GdkPixbuf will be preferred on Windows as well. - MComix uses czipfile when available to speed up extraction of encrypted zip files. - Zombie processes will be removed if possible. - Some issues related to child processes have been fixed. Unnecessary console windows should not appear anymore. - File descriptors will be properly closed when possible. This fixes an issue especially on Windows where files used to stay "locked". - Searching for external tools (e.g. MuPDF) is performed more properly now. - Temporary directories will be created only when necessary and will be deleted as soon as the corresponding book has been closed. - The shebangs now ask for python2 instead of just python so we do not accidentally run Python 3. - comicthumb has been rewritten to make it consistent with MComix. - Some locale issues have been fixed. - The French translation has been updated. - The Russian translation has been updated (by Ulyanich Michael). - The Korean translation has been updated (by Gyeongmin Bak). - Fixed PDF support with newer versions of MuPDF (1.7 and 1.8). - Better support for using the 7z executable: encrypted files are now supported (including encrypted header support, and for all supported formats: 7z, RAR and ZIP). - Fixed an issue with unrar.dll that could lead to crashes if 7z is also present. - Improved detection of available RAR extractors. (unrar-free is currently incompatible with MComix and will be ignored.) - Fixed support for LHA archives (they were always marked as empty). - Fixed support for tar.xz archives (they were always marked as empty). - Updated libraries for the Windows distribution: Pillow 3.1.0 and UnRAR 5.30 - On Windows, MComix normally appears to be frozen on startup while fontconfig is updating the font cache. As a workaround, a window will be displayed. - Fix MComix not starting when 'auto load last file' is on and the last attempt at opening a file was an invalid path - The Windows icon file mcomix.ico has been updated. ### Misc - The MIME database has been updated. - The Py2Exe workaround has been removed. - A Wine-based helper script allows building Windows versions of MComix in Wine. - Huge code refactoring, cleanups and documentation updates - Various minor bug fixes and improvements - New code and examples for testing, improved logging - New version numbering scheme in compliance with PEP440 - ChangeLog updated for MComix 1.01 # MComix 1.01 ## Release date: 31.01.2015 - Keyboard shortcuts can now be edited from MComix' preference dialog in a new tab "Shortcuts". (by Valentin Gologuzov) Please not that the arrow keys, Backspace and Escape cannot be bound to actions right now, unless you're manually editing the config file. - During database upgrade, MComix did not consider that books in the "Last read" database might no longer exist, leading to program crash. This has been fixed. - Adding a collection with a numeric name to the library made the library unusable. This has been fixed. - Fixed win32 builds missing the 'calendar' module. - Fixed bookmarks not being displayed in the Ubuntu Unity global menu. - Fixed 'Continue reading' not working when files are opened from the command line (by Boris Bogar). - Improved page extraction and caching algorithm, leading to much better responsiveness, especially for viewing large archives. (by Benoit Pierre) - MComix will now always hide the mouse cursor after a period of inactivity, even when not in fullscreen mode. (by Benoit Pierre) - The ALT+Left and ALT+Right keys will now either advance one page, or go back one page, depending on the user being in manga mode. - CTRL plus mouse wheel will now zoom in/out one level. - Manual zooming will now use a logarithmic scale instead of a linear spline. - The library will now use natural sorting for "Sort by name" and "Sort by path" instead of alphanumeric sorting, bringing it in line with most other sorting done by MComix. - Adding a book to a collection with the same book already existing in another collection did not immediately show the book in the library main view when the new collection was already selected. - MComix can now use the '7z' executable to read .tar.xz and .tar.lzma archives. - ZIP archives using BZIP2 compression will now fall back to external unzip/7z instead of failing (by Awad Mackie). - MComix can now read PDF files using tools provided by mupdf, namely mutool and mudraw. (by Benoit Pierre) - Double page mode will not implicitly resize images anymore. - The smart scrolling algorithm has been improved. - Some issues with the magnifying glass have been fixed. - Some new variables have been introduced that you can use when running external commands. See the documentation for details: https://sourceforge.net/p/mcomix/wiki/External_Commands - MComix will now use the current GTK theme's icons for Next/Previous buttons. - Added AppData meta information for software repositories. - Updated traditional Chinese translation (by Wayne Su). # MComix 1.00 ## Release date: 26.04.2013 - When "Store information about recently opened files" is enabled in the preferences dialog, all opened books will automatically be added to the library and moved into the collection "Recent". In addition, the last read page will be stored and recalled the next time the book is opened again. - Fixed several malfunctions that could occur if no SQLite library was installed. - Fixed a bug that prevented MComix from showing the first page of an archive nested in other archives. - If both scrollbars were shown, it was impossible to scroll all the way down using the scrolling keys. This has been fixed. - When a directory was opened using the File->Open dialog, MComix did not sort files within the directory, ignoring the user's preferences. - "File->Refresh" did not restore the currently viewed page in archives. - Deleting a file in the library without closing the same file in the main window before no longer causes an exception. - The two images in double-page mode will now scale separately again. (by Valentin Gologuzov) - "Fit to size" mode no longer scales up small images unless "Stretch small images" is enabled as well. - If "Store information about recently opened files" is disabled, MComix will no longer remember the last browsed directory in the File->Open dialog. - Deleting a large amout of books from the library should be much faster now. - MComix now starts in RTL mode when a RTL language has been manually selected in the preference dialog. - Added an option to run arbitrary external commands on the currently opened file or archive. Commands can be edited from the "File->Open with" menu entry. The first item in this list can be accessed with the '1' key, the second using '2', and so on, up to '9' for command nine. The first argument to each command must be the absolute path to an executable, or an executable found in PATH, or an executable found in the specified working directory. - Added an option to automatically rotate images if their height exceeds their width (or width exceeds height), located in the menu bar under Tools -> Transform image -> Auto-rotate image. - Added a new preference option to control sorting of files within archives. Natural sort order is the default ordering, which sorts numbers in file names based on their natural order (e.g. 1, 2, .., 10), while literal order will use standard C sorting (e.g. 1, 23, 4). - "Reset zoom" is now bound to CTRL-0 and KeyPad0 by default. Previously, CTRL-0 and CTRL-KeyPad0 were used. - Using the Shift key with one of the next page / previous page keybindings will advance or go back by 10 pages instead of only one. - Added thumbnailer file for Gnome3 integration. Please note that comicthumb is unmaintained and not installed by MComix' setup routine by default. - MComix will no longer complain that the PIL library is missing when a user has Pillow (a PIL fork) installed. - Updated traditional Chinese translation (by Wayne Su). - Updated Hebrew translation (by Isratine Citizen). - Updated Japanese translation (by Toshiharu Kudoh). - Updated Spanish translation (by Carlos Feliu). - Updated simplified Chinese translation (by Zach Cheung). - Updated French translation (by Frédéric Chateaux). - Updated Italian translation (by Giovanni Scafora). # MComix 0.99 ## Release date: 14.07.2012 - Fixed "Go to page" dialog's thumbnail not scaling depending on dialog size. - Using the mouse wheel to scroll left now correctly advances to the next page in manga mode, instead of going back one page. Scrolling right has also been fixed. - Multiple open instances of MComix no longer overwrite each others' bookmarks when closed. - Fixed exception when trying to pack an archive using the archive editor. - Fixed a bug that prevented using the "Next archive"/"Previous archive" buttons when an empty archive was loaded (by Gabriel Falcone). - "Smart scrolling" now also works with the mouse wheel. In smart scrolling mode, MComix tries to follow the natural reading flow of a comic book by not only scrolling up or down, but also sideways. Please not that smart scrolling does not work in "Fit to width" or "Best fit mode", as there is no need to scroll sideways in these modes. - Zoom is now enabled in all fit modes (best fit, fit to width, fit to height). - Added new zoom mode 'Fit to size'. This mode always stretches an image to a given height or width. By default, a height of 1800px is set. This can be changed in the preferences dialog. - Most confirmation dialogs can now be permanently disabled by activating the "Do not ask again" checkbox in each dialog. This action can be undone by clicking on "Clear dialog choices" in the preferences dialog. - Added new preference option for switching between different resizing algorithms (higher quality usually means longer page loading times). - Added "Scan now" button to library watch list dialog to trigger immediate update. Also added an option to scan directories recursively. Automatically scanning for new books every time the library is opened can now be disabled in the watch list dialog. - The watch list feature no longer tries to add archive formats that aren't currently supported, i.e. no .7z archives when 7z isn't installed or found. - Added an option to quit the program when the ESC key is pressed. When disabled, ESC only exits fullscreen mode. ESC now also closes the library. - Added a new menu item to minimize the MComix window, bound to "N" by default. - Updated traditional Chinese translation (by Wayne Su). - Updated Italian translation (by Giovanni Scafora). - Added Hebrew translation (by Isratine Citizen). # MComix 0.98 ## Release date: 09.04.2012 - Fixed a bug that occasionally caused MComix to display wrong images after deleting an image from a directory. - Fixed a bug that caused MComix to jump back up after scrolling down when an archive was still being loaded. - Fixed NumLock being enabled breaking other keybindings containing Shift, Alt or Ctrl (e.g. smart scrolling with space). - The last-read-page module now falls back to pysqlite2 if sqlite3 isn't available. - The library can now scan directories for new files every time it is started, and automatically add new books. Watched directories can be edited with the "Watch list" button in the library main window. - Added "Date added" to library sort criteriae. This might be slightly inaccurate for older library entries, as only the day, not the time of the moment a book was added used to be stored in the library database. - Improved performance for library book area and thumbnail side bar by only loading thumbnails when they become visible, e.g. triggered by the user scrolling around. The "Delay thumbnail generation" option has thus been removed. - Greatly improved performance for browsing directories with many images. - Updated Japanese translation (by Toshiharu Kudoh). - Updated French translation (by Frédéric Chateaux). # MComix 0.97.1 ## Release date: 18.02.2012 - Corrected libunrar regression. (thanks to Giovanni Scafora for pointing this out) # MComix 0.97 ## Release date: 17.02.2012 - Fixed segmentation fault on x64 platforms when trying to extract RAR archives with libunrar. - The lens now uses the original pixbuf when preparing the magnified image instead of the already scaled pixbuf that is shown in MComix' display area. In addition, fixed zero division error when trying to use the lens on images with width greatly exceeding height. - If 'Auto load last opened file' was enabled in the preferences, MComix would try to load an invalid path if no file was opened when MComix was last closed. - Menu item hotkeys can now be changed by hovering over a menu item with the mouse and pressing the desired key, or key combination. (by Juha Sahakangas and Alan Horkan) - All other hotkeys (such as keys for scrolling or zooming) can now be customized by editing keybindings.conf in MComix' configuration directory, i.e. ~/.config/mcomix on Linux or %HOMEPATH%/MComix on Windows. MComix must not be running while editing the file, or changes will be overwritten once the program exits. - Removed error nag box that would pop up after program shutdown on Windows occasionally. - The order in which files are loaded and displayed can now be customized in the "Advanced" tab of the preferences dialog. Files can be sorted either by name, file size, or by last-modified date. This change does not affect ordering of files inside archives. (by C Nelson) - MComix can now automatically remember the last read page in archive files. When an archive is opened, the last read page will be loaded if "Store information about recently opened files" is set to "File names and last read page" (see "Behaviour" tab of the preferences window). - Updated Italian translation (by Giovanni Scafora). # MComix 0.96 ## Release date: 24.12.2011 - Opening a RAR archive with 7z would destroy the archive, leaving only a 0-byte file. This has been fixed. - Fixed MComix opening files in other directories after scrolling past the first page, even when "Automatically open next directory" was disabled. - Fixed a bug that would hang MComix when trying to open a password-protected RAR archive. - MComix no longer restores the last opened file when it was terminated abnormally. - Files opened outside of archives are now naturally sorted (e.g. 1.jpg, 2.jpg, 10.jpg instead of 1.jpg, 10.jpg, 2.jpg). Before, only images within archives were naturally sorted. - The preference option "Show only one page where appropriate" has been split up to allow controlling whether certain pages should be displayed as single page in double page mode (title pages/wide pages/none). - "Delete" is now bound to "DEL" instead of "F8" for consistency with most other desktop applications. - Updated traditional Chinese translation (by Wayne Su). # MComix 0.95 ## Release date: 05.11.2011 - mcomix/mcomixstarter.py has been moved out of the mcomix package into the root directory of the mcomix distribution. Note for packagers: Please do not directly symlink a file in /usr/bin to mcomix/run.py! Use the wrapper generated by 'setup.py install' instead, or a script similar to mcomixstarter.py. - Fixed library freezing up when displaying large amounts of books. In addition, changes to cover size and sort order weren't kept across program restarts. - Fixed "Copy to clipboard" doing nothing on Win32. - Fixed freezing on password-protected 7zip archives. Please not that such files currently aren't supported and will always appear empty in MComix. - The All-in-one package on Win32 should now use the native Windows theme. - Fix MComix crashing on startup when opening a file in a directory that contains names Python cannot directly convert to Unicode strings. (by Joseph Seaton) - Selecting "Japanese" from the language dropdown box in the preferences dialog reverted the language to English. - Added support for reading archives in archives. (by David Pineau) - Reduced minumum slideshow scrolling delay. With small values here and in scrolling distance (e.g. 0.05s, 1px), MComix can simulate "smooth" scrolling. - The "Dynamic background color" option now uses a color that should be closer to a page's actual edge color. - Removed preference options for 'Use double page mode by default' and 'Use manga mode by default'. The last used settings will be remembered instead. - The OSD is now used more frequently for displaying error messages that would only appear in the status bar or in the console before. In addition, the OSD can now be triggered with mouse button 4, as well as with the TAB key. - Updated French translation. (by Frédéric Chateaux) # MComix 0.94 ## Release date: 27.09.2011 - Fixed MComix opening archives in sibling directories even when "Automatically open next archive" was disabled. - Fixed recursively adding directories to the library not working consistently on Win32. - Fixed the first command line argument to MComix being ignored on Win32, breaking "Open with..." functionality. - The library window has been slighly reorganized. All collection-related functionality can now be accessed via the right-click popup on the collection panel to the left. Similiarily, "Add books" is now on the main book panel popup. Additionally, CTRL-SHIFT-A has been set as shortcut for this action. - Library covers will now be cached after being loaded. This will avoid frequent reloading when switching between collections, or when filtering books. - The magnifying lens can no longer become partially invisible when moving around near window edges, and should no longer flicker. - MComix automatically switching to next/previous directories can now be controlled with a new preference option. - Updated French translation. (by Frédéric Chateaux) # MComix 0.93 ## Release date: 27.08.2011 - Removing a book from the library while its thumbnail wasn't loaded yet would result in a segmentation fault. This issue has been fixed. - Fixed sorting in the bookmark edit dialog not working as expected. The buttons "Sort ascending" and "Sort descending" have been removed, as they did the same as clicking on the "Name" header of the bookmarks table. Double-clicking a bookmark will open it. - Fixed a bug that made it impossible to show toolbar/menu controls in fullscreen mode if "Automatically hide all toolbars in fullscreen" was enabled. - Fixed exception related to calculation of dynamic background colors. (by Nephiel) - Library collection names did not accept non-ASCII characters. This has been fixed. - Added support for LHA/LZH archives, using either 'lha' or '7z' as extractors. Please not that the '7z' executable on Windows does not support printing Unicode characters at all, so extracting an archive with non-ASCII filenames will always fail. - By selecting a folder instead of a file in the library's "Add book" dialog, all archives within the selected directory will be added to the library recursively. - Doing the same in the normal "Open" dialog will open all files within the directory. - MComix will now ask for confirmation when creating a new bookmark in an archive that was already bookmarked before. This allows the user to either create a new bookmark, or replace the old one with the current page. - ALT+Left mouse button and ALT+Right now advance one page, while ALT+Right mouse button/ALT+Left go back one page. - When on the last page, advancing to the next page will load files from the next sibling directory - holding CTRL is no longer necessary. - Added a new option to invert the smart scrolling direction. Instead of going left/right, then top/bottom, MComix will scroll top/bottom, then left/right. - Settings in the Enhance dialog can now be remembered using the "Save" button. - The option "Stretch small images" now increases an image's base size when using manual zoom mode. - Information shown in the status bar can now be enabled/disabled separately by right-clicking on the status bar and toggling the respective check box. - By pressing TAB, an OSD-like panel will be displayed, showing the current page and file. - MComix can now use Chardet (http://chardet.feedparser.org) for guessing filename encodings in archives, if installed. If file names are too short, the detection will still be hit-and-miss. # MComix 0.92 ## Release date: 27.05.2011 - Fixed a bug that made MComix save preview thumbnails to disk even if this behaviour was disabled in the preferences window. - Fixed a bug in the the archive editor that prevented it from actually saving the modified archive on Win32. - Added limited support for password-protected ZIP and RAR archives. For ZIP archives, Python >= 2.6 is required. For RAR archives, only extraction with libunrar/unrar.dll is supported. - Added a combobox to the library dialog to enable sorting of books based on file name, full path, or file size. - If a library collection has sub-collections, the books from these sub-collections will be shown as well when the collection is opened. - The "Bookmarks" menu can be accessed via the normal menu bar again. "Clear bookmarks" has been removed in favour of using the "Edit bookmarks" dialog. (by Alan Horkan) - Several usability improvements were done to the Enhance, Edit and Library dialogs. (by Alan Horkan) - If applications for extracting RAR or 7Z archives aren't found on start-up, MComix will no longer allow selecting the corresponding file types in the "Open" dialog. (suggested by Alan Horkan) - The "Copy" menu item will now copy the current file name to the clipboard, in addition to the currently opened page as bitmap. - The currently opened file or archive can now be deleted using File -> Delete, or by pressing F8. - Added an option to use the first page of an archive as application icon instead of the standard MComix icon. (inspired by Alexandr Domrachev) - Added an option to manually change the user interface language used by MComix. Changes to the language require an application restart to take effect. - Added the following new command line switches: -m Manga mode -s Slideshow -d Double-page mode -b, -w, -h Fit best/width/height, respectively. (suggested by Anonymous on the Comix tracker, adapted by Alan Horkan) -W\[all|warn|error\] Set log level (default is 'warn') - The following preference items have been removed: "Automated crash recovery": No longer necessary. "Show page numbers": Enabled by default. "Avoid unintentional page flips": Enabled by default. "Stretch small images": Now in Menu->View->Stretch small images. "Default zoom mode": Last setting is remembered instead. - Updated Japanese translation. (by Keita Haga) - Updated French translation. (by Joseph M. Sleiman) # MComix 0.91 ## Release date: 24.04.2011 - Fixed excessive memory consumption due to cached pixmaps not being properly evicted. - Fixed certain wait conditions that prevented MComix from exiting on Win32. - Fixed "Remove from the library" deleting the actual book instead of its library thumbnail. - The "Go to page" dialog now shows thumbnails when they are available, not only after all thumbnails have been loaded. Additionally, some usability improvements have been done to the dialog, such as instantly updating the thumbnail when editing the page box, and setting focus to the page box when the dialog is opened. - When passing more than one file to MComix at startup, only those files will be opened. This differs from the traditional behavior, where MComix would only consider the first file and open all remaining files in the same directory. If the passed files are archives, MComix will only open these archives when "Automatically open next archive" is enabled. If only a single file is passed, MComix will keep opening all files in that directory. - When the first/last file of a directory is open, pressing CTRL and advancing to the previous/next page (e.g. by pressing CTRL+Space), files in the previous/next sibling directory will be opened. Note that this feature is disabled by intent when MComix has been opened with a list of more than one file. This feature is also available via CTRL+N/CTRL+P, or the menu bar. - Speed up thumbnail generation by parallelizing load tasks. (inspired by David Zaragoza, who originally suggested to use processes instead of threads) - Library cover generation is now parallelized as well. - New option to delay loading of thumbnails. This way, thumbnails will only be generated when they are actually needed, i.e. the thumbnail sidebar is open or "Go to page" is used. - Minor options have been moved into a new tab in the preferences dialog. - MComix' configuration files are now stored in ~/.config/mcomix instead of in ~/.local/share/mcomix, as originally intended. - Added the toolbar show/hide menu to the right-click popup. Previously, if the menu bar had been disabled using the normal menu, there was no way to get it back. - The menu bar can now also be shown/hidden using CTRL+M. (by Alan Horkan) - MComix could not switch back to windowed mode when started in fullscreen mode on Win32. - The MComix window will no longer close instantly after starting up when reporting an error due to unsatisfied dependencies on Win32. - Fixed the settings dialog window no longer opening when it has been closed with the X icon on the dialog before. - Fix "Automatically open next archive" with empty archives. - Fixed magnifiying lens being broken when the page was rotated in any way. - Fixed setup.py failing when no X session was started. - Required Python version is now 2.5 or newer. - Updated Swedish translation. (by Martin Karlsson) - Updated Russian translation. (by Евгений Лежнин) # MComix 0.90.3 ## Release date: 13.03.2011 MComix now uses a slightly different directory structure than before. The 'src' folder is now 'mcomix' to provide a correct package name. 'mcomix.py' is now 'mcomixstarter.py' to avoid confusing Python by having a module with the same name as the package around. Translations and images required by the GUI are now sub-packages of 'mcomix'. A setuptools-based setup.py replaces `install.py`. This should help for uniform installs across different operating systems. - Several strings have been reworked to ease localization. - Added ability to apply current changes in the edit archive window. - Various usability fixes on Win32, including Unicode filenames, loading speed, recently opened files not being displayed, temporary directories not being deleted, crashing due to missing icons, MIME type file filters in the "Open" dialog not working, thumbnails being regenerated unnecessarily, and others. - Magnifying lens is now hotkeyed to 'L', while 'G' is Go to page. (by Nephiel) - Right-click menu is now more suitable for fullscreen reading, adding several menu items previously only available via normal menu. (by Nephiel) - Additional RAR handler using libunrar.so/unrar.dll. Added archive handler using Rarlab's libunrar library for extracting files. Apart from being faster for sequential extractions than calling unrar for each single file, this library supports Unicode filenames natively and thus allows Windows users to read most RAR files. Libunrar can be obtained from http://www.rarlab.com/rar_add.htm and can be placed either in usual system directories such as /usr/lib or C:\Windows\system32, or directly in MComix' root directory. - Fixed rar/unrar failing regularly on Win32 when the archive contains files not matching the current locale. - Go to Page is now enabled even when the archive is still loading. (by Nephiel) - Added support for the 7zip archive format. As with rar/unrar, this requires the "7z" executable being installed and on PATH. - When pressing CTRL while being in double page mode, stepping forwards and backwards will now always only advance/go back one image instead of possibly two. - Graceful shutdown on SIGTERM. (by Marco Nicolini) - Switching pages while in slideshow mode now resets the slideshow timer. (by Anonymous) - When opening an archive in double page mode, the first page (i.e. the cover) is displayed as single page. - The currently opened file can now be extracted from archives using the 'Save as...' menu item. - Fixed thumbnail size preference not being respected, and scaling of book covers in the library dialog being broken. - Files in the library can now be opened without closing the library window using the right-click popup menu. - Reordered various menu items. - A possible deadlock that could occur when opening archive files has been fixed. - Updated German translation. # MComix 0.90 Initial Release ## Release date: 15.08.2010 - Changed the mechanism of page flipping. - Added preferences to allow changing scrolling amount with arrow keys and mouse scroll button. - Added auto scrolling functionality. - Changed automatic background color selection algorithm to random sampling instead of only edge sampling. - Fixed non-recognition of pbm, pgm, and ppm images in archives. - Added save and quit functionality. - Added crash recovery. - Added bookmark sorting. - Added changed focus page protection option. - Added refresh button and capability. - Added color preference and selection for thumbnail bar background color. - Fixed lens not magnifying the enhanced image. - Added file deletion to the Library right-click option window. - Added recursive book adding in the Library (if you select more than one folder in the book selection window. - Split each file to only contain one class per file (except labels.py). - Fixed file name ampersand encoding error. - Added page selector with page preview. - Added preference regarding the number of keys pressed needed to flip the page. - Added next archive and previous archive buttons. - Added copy (CTRL+C) functionality which allows copying of the current image. - Added thumbnail cacheing and threading. - Added threaded page cacheing and cacheing preferences. - Added the preference to turn on/off page number display. # Comix is forked and becomes MComix # Comix 4.0.5 - Added a Ukrainian translation by Олександр Заяц. - Added a Galician translation by Roxerio Roxo Carrillo. - The German translation updated for Comix 4 by Chris Leick. - Added support for BMP images in archives. Thanks to Nathaniel Moseley. - The status bar now displays the filename of the viewed image files also in archives. - Fixed a bug that caused the wrong background colour to be used with the dynamic background colour preference on some systems. Thanks to Nathaniel Moseley. - Fixed a bug that could cause the thumbnail maintenance dialog to crash. - Fixed a bug that caused the zoom scale in manual zoom mode to be wrong when using double page mode. # Comix 4.0.4 - Applied a workaround for a bug that caused the "Open" dialog to crash when trying to open a file when the file type filter had been reset to blank. This bug seems to only appear on some systems, probably depending on the installed GTK+ version. - Fixed a bug that caused the error message for unfulfilled dependencies to not be printed properly. - The rar/unrar program is now invoked in such a way as to keep broken or incomplete files extracted from RAR archives, since Comix might be able to display parts of these files anyway. # Comix 4.0.3 - Hungarian translation updated by Ernő Drabik. - French translation updated by Benoît H. - Added a feature to automatically rotate images according to their EXIF tags. - Fixed a bug that caused drag-n-drop actions from KDE applications to not work properly. - Fixed some bugs that caused problems with non-UTF-8 filename encodings. - Fixed a bug that caused the manual zoom mode to not work as expected when set as the default mode. - Comix now accepts directories as command-line arguments. - Added command-line arguments to start Comix in fullscreen mode and to display the library on startup. - Comix preferences and data now reside in the $XDG_CONFIG_HOME and $XDG_DATA_HOME directories instead of in ~/.comix/. - Some minor interface enhancements. # Comix 4.0.2 - Brazilian Portuguese translation updated by Marcelo Góes. - Traditional Chinese translation updated by Wayne Su. - Catalan translation updated by Carles Escrig Royo. - Internal filenames in archives created by the archive editing dialog no longer contain temporary filename cruft. # Comix 4.0.1 - Croatian translation updated by Adrian C. - Polish translation updated by Darek Jakoniuk. - Russian translation updated by Артем Смирнов. - Simplified Chinese translation updated by Xie Yanbo. - Re-added the "flip pages when scrolling off the page" preference from previous Comix versions. Thanks to Mamoru Tasaka. - Added a portability module for handling home directories in a more portable way. Thanks to Oddegamra. # Comix 4.0.0 - Comix has been completely rewritten from scratch. On the surface things look quite a bit like they used to, but the internal workings are entirely new. There are too many changes for them all to be mentioned here, but a couple of highlights are a much more functional library and a new archive editing dialog. The work on this new version of Comix has been going on in rather sporadic phases for almost two years, and during that time I have received help from lots of different people. Now, I must admit, I can no longer remember them all. So instead of trying to list as many as I can here, I will instead simply say thank you to everyone who have contributed fixes, patches, suggestions or encouraging words. Thanks! # Comix 3.6.5 - Applied security fix patches to handle unsecure tempfile creation and character escaping in filenames. Thanks to Mamoru Tasaka and others for the patches. - Added a Korean translation by 김민기. - Added a Persian translation by Maryam Sanaat. - Added a Indonesian translation by Andhika Padmawan. - Added a Czech translation by Jan Nekvasil. # Comix 3.6.4 - Added a Russian translation by Artyom Smirnov. - Added a Croatian translation by Adrian C. - Fixed a bug in the thumbnailer, comicthumb, failing to create thumbnails for Zip and tar archives. - Some minor changes. # Comix 3.6.3 - Added a Hungarian translation by Ernő Drabik. - Added a patch by Abdullah Hamed that fixes so that the arrow keys can be used to flip pages also when not in fit-to-screen mode when the corresponding preference is set. Just like was possible only with the scroll wheel before. - Fixed a bug with opening certain Zip files. Thanks to Steve Juranich for the fix. - Fixed a bug concerning %'s in filenames. # Comix 3.6.2 - Added Japanese translation by Mamoru Tasaka. # Comix 3.6.1 - Updated Brazilian Portuguese and Dutch translations. # Comix 3.6 - Added an "Adjust colour" dialog that lets you specify values for brightness, contrast, saturation and sharpness. - Improved the behaviour of the "Save window position and size" and "Default fullscreen" preferences. - Changed the "Save window position and size" preference to on by default. - Changed the menus a bit. - Improved autocontrast (slightly heavier contrast change). - Changed the UI of the properties dialog a bit to better suit low resolution screens. - Improved handling of Zip files containing files with filenames of an unknown character encoding. - Added extra error message to `install.py` that is displayed when trying to install into a non-existing directory. - Added a --no-balloon option to `install.py` that tells the Nautilus thumbnailer to not imprint balloon images on thumbnails by default. - Fixed a bug that could cause Comix to scale images to the wrong dimensions on a dual-screen setup. Thanks to Vegard Eriksen for this fix. - Fixed a bug that caused icons to not be loaded when starting Comix through a symbolic link not located in the same directory as the `comix` executable. - Fixed a bug that could cause an error message when going back to a previous archive by flipping backwards in double page mode and directly switching to single page mode afterwards. - Fixed a bug that caused Comix to treat empty files as tar archives. Thanks to Christoph Wolk for this fix. - Fixed a bug in comicthumb with thumbnailing rar archives, plus some cleanup. (Christoph Wolk) # Comix 3.5.1 - Fixed a bug that caused the mode of all images to be reported as "unknown" instead of RGB/CMYK etc. - Fixed a bug that could cause an error when trying to quit Comix under certain circumstances (i.e. when there is no ~/.comix/menu_thumbnails/ directory present). # Comix 3.5 - Added a bunch of new icons, including a new "logo". - The magnifying lens code has been polished a bit. It is now substantially faster so the lens should appear less choppy. - Added horizontal and vertical lossless JPEG flip commands. - Added a JPEG desaturation command. - Added support for SVG, PCX, PNM, PBM, PGM, PPM, Targa and Sun raster image files. - Rearranged the toolbar a bit and added tooltips to it. - When a directory is given as a command line parameter, Comix now recursively searches for cbr, cbz and cbt files as well as image files. - Changed `install.py` so that it aborts installation if the required dependencies are not found. - Fixed a bug that caused the space key to not scroll down when in double page mode and manga mode and the window is wider than the pages. - Applied a workaround for a bug(?) in WindowMaker that caused problems when using the "fullscreen as default" preference. - Fixed a memory leak in the magnifying lens code. - Some internal and some minor changes. # Comix 3.4 - Added more image data to the properties dialog. - Added a "delete image" command that can remove single images from Comix. It is currently not possible to remove image files within archives. - Added lossless JPEG rotation commands. It is currently not possible to rotate image files within archives. The `jpegtran` program (part of the jpeg library) must be present for this to work. Comix can still run as normal without `jpegtran`, but then without the new JPEG rotation capabilities. - Changed the buttons in the toolbar. - Improved the space key smart scrolling mode so that it automatically performs all the sideways scrolling as well. - Added a preference to set the magnitude of the space key scroll in percentages of either the window size or the page size. - Added a Traditional Chinese translation by Hsin-Lin Cheng. - Comments are now displayed using a monospaced font. - Comments can now be dragged around with the mouse just like an image. - Directories can now be given as command line parameters as well as files. If a directory is given it will be recursed into and the first image file found will be loaded. - Improved cover guessing of comicthumb and the library a bit. - Handling of files that have filenames encoded with the wrong character encoding is now more sturdy. - Fixed a bug that could cause outdated thumbnails to be left in the ~/.comix/menu_thumbnails/ directory when running multiple instances of Comix at the same time. - Applied a workaround for a bug(?) in WindowMaker that caused the "Open dialog" to be invisible while in fullscreen mode when using WindowMaker. The same problem applies to the library window, but there is no workaround for that in place currently. - Fixed a bug that could cause no images to be displayed when turning double page mode off, then on again and flipping to the next couple of pages in that order. - Some minor changes. # Comix 3.3 - Added a slideshow feature. - Added RGB colour histogram to the properties dialog. More data will be added in future versions. - Added a Catalan translation by Carles Escrig. - Rewrote `install.py` from scratch. - Fixed a bug that caused compressed tar archives to be presented as plain tar archives. - Fixed a bug that could cause invalid page numbers in bookmarks when re-adding an already present bookmark. - Some minor changes. # Comix 3.2.1 - Added support for the `rar` program in addition to `unrar` to handle RAR (.cbr) files. - Updated Polish translation by Kamil Leduchowski. - Some minor changes. # Comix 3.2 - Changed PyGTK requirement to version 2.8 or higher. - Added a "Fit width mode" and a "Fit height mode" that automatically scales images to fit the width or height of the window. - Default filenames for extracted images are changed to _. from .. - Moved the manga mode setting from the preferences dialog to the menus. - Redesigned the library interface a bit. The background colour is now fixed and does not change with the background colour of the main window. Default thumbnail size is now 128x128 px, and thumbnails have a border to make them more clearly separated. - Added Greek translation by Paul Chatzidimitriou. - Xie Yanbo updated the Simplified Chinese translation. - Changed the menu icons for "Open library..." and "Add to library". The icons are taken from the Silk Icons set at www.famfamfam.com. - Broken images are now correctly handled by the thumbnail sidebar. - The workaround against a problem with unrar applied in version 3.1.3 has been removed again. It created some new problems with archives that have multiple files with the same filename in different subdirectories. - Fixed a bug so that translations and extra icons are always available when running Comix from the source directory, no matter what directory is the current working directory. - Fixed a bug in the "Go to page dialog" that caused the page to not be changed when manually typing in a new page number and pressing Enter. - Some minor changes. # Comix 3.1.3 - Added Polish translation by Kamil Leduchowski. - Updated French translation by Achraf Cherti. # Comix 3.1.2 - Fixed a bug which caused ALL files to be added to the library when adding in recursive mode instead of just archives. Also, only files with cbz, cbr or cbt as filename extension will now be added in recursive mode to avoid adding cruft files with the same magic numbers as the archives. # Comix 3.1.1 - Added automatic dependency checking to `install.py`. - Added error messages and graceful exit from Comix in the case of missing dependencies. - Applied a workaround for a bug(?) in unrar that caused problems with some RAR archives containing directories with invalid filename encodings. Thanks to François Ingelrest. - Updated French translation by Achraf Cherti. - Changed the "Use stored thumbnails for images in archives" preference to off by default. - Changed the "Go to the next archive in directory after last page" preference to on by default. # Comix 3.1 - Created a new convert dialog that is built from the standard GTK+ save dialog. It now supports saving in different directories etc. - Added an "Extract image" menu item that lets you extract individual images from the archive. - Added support for recursive adding of archives to the library. - Added a Frech translation by Achraf Cherti. - Fixed a bug which rendered the magnifying lens and the ability to drag images around with the mouse useless in some situations, and with some certain versions (7.0?) of X.org. - Pressing enter in the "Go to page" dialog entry now has the same effect as pressing OK. - Applied a workaround for a bug(?) in certain builds of PIL that made Comix crash when it tried to draw page numbers on thumbnails. Now Comix simply ignores the page numbers if this problem occurs and imforms the user that a different version of PIL is required, instead of crashing. # Comix 3.0.1 - Added a Dutch translation by Arthur Nieuwland. # Comix 3.0 - Major cleanup of the entire code base. - Completely redesigned the properties dialog. - Comix now stores a list of the 10 last viewed files. It also updates the ~/.recently-used file as is proposed by the freedesktop.org standard. Thanks to Jose M. daLuz. - Added an "Add to library" menu item. - Redesigned the library window slightly. - Added an Italian translation by Raimondo Giammanco. - The Nautilus thumbnailer, comicthumb, has been updated by Christoph Wolk to support subarchives among other things. - Added a preference to set the size of the magnifying lens. Thanks to Jose M. daLuz. - Added a scalable svg icon. - Improved handling of files without read permission. - Fixed a bug which caused the recommended name for a converted directory of images to be the same as one of the image files plus filename extension instead of the name of the directory plus filename extension. Thanks to Manuel Quiñones. - Fixed a bug with the magnifying lens which could appear when using it in double page mode and manga mode, possibly showing the images as if not in manga mode. - Fixed a bug which caused unnecessary reloading of files from disk when resizing images that is already in memory in double page mode. - Fixed a bug which could cause the wrong image to be displayed when continuously flipping forward really fast in cache mode. - Some minor fixes. # Comix 2.9 - Added a comic book library feature to Comix. Comic book archives can be added to the library through a dialog or by drag and drop. The comic books appear as covers in the library window where they can be browsed or opened. They can be easily filtered by typing in regular expressions. - When dropping multiple files on the Comix main window, the first file gets opened now instead of none. - The convert dialog now saves the last used archive type. - Fixed a memory leak when creating new thumbnails from files. - Some minor fixes. # Comix 2.8 - MIME types for cbz, cbr and cbt archives are now registered by default. Use the --no-mime flag for `install.py` to skip it. - Added a thumbnailer (by Christoph Wolk) that lets file managers create thumbnails for cbz, cbr and cbt archives. Currently it is only supported by Nautilus and does not affect other file managers. It is installed if the --no-mime flag is not given to `install.py`. Nautilus has to be restarted before the thumbnailer is activated. - Added a "Hide all" menu item which hides menubar, toolbar, statusbar, scrollbars and thumbnails at once. - Added an option to only display a single image in double page mode if that image consists of two pages. An image is assumed to consist of two pages if it's width is greater than it's height. - Filename is now displayed as well as directory name when viewing images in a directory in single page mode. - Changed max zoom to 1000% to prevent X server resource drains. - F11 can now be used to toggle fullscreen mode. - Fixed a bug which caused the cursor to be invisible when dragging around an image in fullscreen mode. - Fixed a bug which removed the drag and drop functionality. - Fixed a bug which could cause the scroll wheel to stop working when displaying the magnifying lens by pressing the middle mouse button, holding down the right mouse button and letting go the middle mouse button again. - Fixed a bug which could cause the chess board pattern background for small transparent images to be zoomed in or out when changing size. - Fixed a bug which could cause changes of saturation in small images to not be updated when changing it and changing it back again. - Fixed a bug which could cause the images to be scaled incorrectly when the "Use smart scaling in double page mode" preference was set. - Fixed a bug which caused the thumb selection not to be updated when moving from page two to page one in double page mode. - Some minor fixes. # Comix 2.7 - Improved image quality through dithering in 16 bits per pixel. Thanks to John Ellis, the author of GQview, for helping me with this. - Added previews of files in the "Open" dialog. Stored thumbnails are used when they exist, otherwise previews are created directly from the files. Previews of archives are also available, but only when thumbnails have already been stored for that archive. - The cursor is now only being hidden when it has been idle for two seconds instead of always. - Changed "Hide cursor in fullscreen mode" to on by default. - Added chess board pattern as background for transparent images. - Added file filters to the "Open" dialog. - Saturation adjustment now affects the magnifying lens also. - Added Brazilian Portuguese translation by Marcelo Góes. - Added German translation by Christoph Wolk. - Added a "comix.xml" file which can be installed to register cbz, cbr and cbt mime types. Because of inconsistency on some systems, mime types are not registered by default. Follow the instructions in the mime README file to install. Thanks to Cristoph Wolk for this contribution. - Changed permissions of temporary folders to octal 700 to preserve the users privacy. - Added page number information to thumbnails generated for archives. This information will displayed for previews of archives if available. - Fixed a bug which caused the image width and height information embedded in thumbnails to be put in the wrong namespace of the PNG tEXt chunks. - Fixed possible wrong permissions of thumbnail folders on some systems. Permissions should now always be 700. - Fixed a bug which caused the magnifying lens to display the wrong page in manga mode (thanks Christoph Wolk). - Fixed a bug which could cause Comix to crash when trying to view a very small image scaled down so that it contained no pixels at all. - Some minor fixes. # Comix 2.6 - Comix now conforms to the thumbnail managing standard as proposed by freedesktop.org. Thumbnails for plain image files can be read and written to ~/.thumbnails where they are shared with other applications conforming to the same standard. Thumbnails for images in archives can also be stored, but due to limitations in the standard they are stored in ~/.comix for private use by Comix only. - Improved handling of corrupt and missing files. Comix now simply displays a "file missing" image instead of terminating. - Added a menu entry for the lens toggle action. - Some minor fixes. # Comix 2.5 - Added a mouse controlled "magnification lens" that can be used to zoom in on parts of the images. It will appear while holding the middle mouse button. Pressing 'z' can also toggle it on or off. - Added the ability to set the size of the thumbnails. - Improved the look of the bookmark handling features. Small thumbnails will now be displayed for the bookmarks. - Fixed a bug which caused one of the two images in double page mode to remain displayed even after using the "Close" command. - Fixed a bug which caused the "Use smart scaling in double page mode" preference to misbehave with rotated images. - Fixed a bug which caused the convert dialog to not automatically fill in a new filename for files with non-UTF8 filenames. - Some minor fixes. # Comix 2.4.1 - Improved cache handling slightly. - FIxed a bug which could cause Comix to crash when starting with the "Save window position and size for future sessions" option turned on. - Fixed a bug which caused only one thumbnail to remain selected when clicking on the first of the two selected thumbnails in double page mode. - Fixed a bug which caused Comix to always go to the first page when reloading thumbnails in double page mode due to switching the "Show page numbers on thumbnails" on or off. # Comix 2.4 - Added full support for internationalization (i18n). Apart from English, Comix is now translated to Swedish, Simplified Chinese and Spanish. - Added an option to let Comix automatically adjust the contrast of the images so that the darkest pixel is completely black and the lightest pixel is completely white. - Added an option to space key "smart scrolling" feature as if in double page mode even when in single page mode. - Added thin borders around the thumbnails so that they should be more easily distinguished. - Added accelerators for more menu items. - Fixed a bug which caused the wrong image to be displayed when viewing the third last and the second last pages in double page mode, exiting double page mode and flipping forward one page. - Fixed a bug which caused Comix to display no warning when trying to create a file in a directory where it does not have permission to do so. # Comix 2.3 - Comix now depends on the Python Imaging Library (http://www.pythonware.com/products/pil/) to handle some image manipulations. - Added the ability to rotate and mirror images. - Added an option to set the contrast of the images. - Added an option to show the page numbers in the upper left corner of each thumbnail. - Cleaned up the preferences dialog a bit. - The arrow keys now flips pages in fit-to-screen mode. - Added an option to go to the same directory as last time when opening the "Open" dialog, instead of always going to a preset directory. - Fixed a bug which caused comments to not be displayed until after all thumbnails had been loaded when using the "Always view comments when opening a new file" option. - Fixed a bug which caused comment files in a directory, had their extensions been added to the comment extensions list while the directory was loaded, to not be opened correctly until the directory had been reloaded. - Fixed a bug which caused the scrollbars to not align themselves automatically when switching between viewing comments and viewing images. - Fixed a bug which caused the "Fit width", "Fit height" and "Best fit" commands to scale incorrectly when viewing thumbnails. - Fixed a bug which could cause the wrong image to be displayed if switching from one page to another and back again really quickly. - Some minor fixes. # Comix 2.2.1 - Fixed a bug which caused underscores in bookmark names to be shown as underlines for the next character. - Fixed a bug which caused Comix to crash when trying to close the program while loading thumbnails for an archive/directory that was opened while loading thumbnails for the same archive/directory. - Fixed a bug which caused some events (e.g. switching fullscreen on/off), invoked while loading thumbnails, to be delayed until all the thumbnails had been loaded if they were invoked more than once during this loading. There are still some issues with events that in themself initiate the loading of thumbnails (e.g. open new file) being delayed if invoked multiple times during the loading. This should, however, not be a problem in most cases. - Fixed a bug which caused some thumbnails to not be displayed in whole when using certain GTK themes. - Fixed a bug which caused the selected thumbnails to not be updated properly when switching between single page and double page mode. - Fixed a bug which caused the thumbnail scrollbar to not update it's length when resizing the main window. - Made the automatic scrolling of the thumbnail pane more consequent in double page mode. # Comix 2.2 - Added a thumbnail browser. - Added a "smart scrolling" option for the space key. - The number pad on the keyboard can now be used to align the image(s). '1' takes you to the lower left corner, '2' takes you to the middle of the bottom, '3' takes you to the lower right corner and so on for all nine digits. - Added an option to automatically open the last viewed page when Comix is started. - The zoom in and zoom out commands now zoom straight on, preserving the alignment of the viewed image(s). - Fixed a bug which caused images to be scaled slighty wrong during specific window size/image size combinations. - Fixed a bug which caused the wrong image to be displayed when jumping two pages forward with the cache option turned on in single page mode. # Comix 2.1 - Made the dialogs more GNOME HIG compliant. - Added an option to let the scroll wheel scroll horizontally when at the top or bottom of the page. - Added an option to let the scroll wheel flip pages when scrolling "off the page". - Added an option to use smart scaling when in double page mode and fit-to-screen mode. The smart scaling feature makes the images scale independently so that no space is wasted. The largest of the images is scaled as before, but the smallest in now scaled up to fill any extra space. - Added an option to set the toolbar to show either icons, text or both. - The image can now be dragged around with the middle mouse button as well. - If adding a bookmark for an archive/directory that is already bookmarked, Comix now updates the page number of that bookmark instead of adding a new one. - Added tooltips for the preferences dialog. - Removed the zoom entry in the preferences dialog. - Made the cleanup of files in /tmp a bit more sturdy. - Fixed a bug which caused the scrollbars to not align themselves automatically when opening a new archive/folder. - Fixed a bug which caused changes to fullscreen, fit-to-screen mode etc. that were made while the preferences dialog was open to be reverted when the dialog was closed. - Fixed a bug which caused some images to appear twice if they were packed in an archive containing multiple subfolders. - Fixed a bug which caused some strange behaviour when (un)hiding the menubar after a bookmark had been added/removed. - Some minor fixes. # Comix 2.0 - The text in the comments is now selectable. - Some minor (mainly cosmetic) changes. # Comix 2.0b - Comix 2.0b has a major speed advantage over previous versions. I rewrote some of the code, mainly the parts concerning the caching of images. Page flipping with the cache option turned off is now about twice as fast as before (not counting the time it takes to scale the image(s) since this is highly dependant on the image scaling quality being used). Forward page flipping with the cache option turned on now seems almost instantaneous (again not counting the possible delay to scale the image(s)). Backwards flipping with the cache option turned on is no longer suffering any speed penalty, it is just as fast as normal flipping without any cache would be. The only occasion when things will be slower with the cache option turned on is when doing irregular flipping, as from page 1 to page 7 to page 3 etc., and even then things will seem to be as fast as normal though some extra work is done "under the hood". Using cache is strongly recommended from now on. - Added a bookmarks manager that lets you add or remove single bookmarks. - Added support for image/x-icon, image/x-xpixmap and image/x-xbitmap image formats. - Changed the text entry field for the default path to a button which brings up a nice standard folder selection dialog. - The "Flip position of pages in double page mode" option, now called "Manga mode", automatically aligns the scrollbars to the upper-right corner of the window when flipping to a new page. - The "Hide menubar etc. in fullscreen" option now also affects the scrollbars. - Fixed a bug which caused temporary files to be overwritten when viewing two archives in two different Comix sessions at the same time. - Fixed a bug which caused the separators in the menus to disappear after a while. - Fixed a bug that caused the wrong page to be displayed when flipping backwards one step in double page mode, switching to one page mode, switching back to double page mode again and then flipping forward one step, all with the cache option set. - Lots of minor fixes. # Comix 1.6 - The next page can now be flipped to by pressing the left mouse button on the main window. Images can still be moved around by clicking and dragging before you release the button again. - The "Hide cursor" option now only affect fullscreen. - Comix now sorts files by the standard set up by the LC_COLLATE environmental variable. - Added a new dialog to display an error message if the convert utility is used in some way to alter a file which the user does not have appropriate permissions to. - Fixed a bug with the wrong filename sometimes being shown in the properties dialog when viewing plain image files. - Fixed a bug with strange things happening when opening the preferences window in windowed mode and closing it in fullscreen mode and vice versa. - Fixed a bug with the "Hide menubar etc. in fullscreen" option not always working as expected. - Some minor fixes. # Comix 1.5 - Added support for forward/back buttons on mice that have them. Thanks to Stephen Jones for this feature. - New option to set which filename extensions should be treated as comment files. - New option to automatically hide menubar etc. when entering fullscreen mode. - Improved the look of the properties dialog. - Fixed problems with parsing certain comment files. - Some minor fixes. # Comix 1.4 - Added support for embedded archive comments in the form of .txt or .nfo files. - The convert utility now creates exact copies of the directory structure in the source archive/directory, including correct filenames. - The zoom in and zoom out commands now zoom 15% relative to the current size instead of 20% relative to the original size. - Changed default image scaling quality to "Tiles" from "Hyper". - Some minor fixes. # Comix 1.3.1 - Fixed a bug with old cached images sometimes being shown when a new archive/directory had been opened. # Comix 1.3 - The viewed page(s) can now be dragged around by pressing the mouse button and moving the cursor. - New option to cache the next page for faster forward page flipping. Continuous backwards flipping will be slower with this option. Slightly more RAM will be used as well. - Comix now handles manipulation of already loaded images (resizing, changing scaling technique, etc.) more efficiently due to less disk IO. - Fixed a bug that caused saved options to not be loaded when the program is restarted. # Comix 1.2 - Drag and drop support. - Changed the `install.py` script slightly. - Added new error messages for the statusbar when trying to open non-valid files etc. # Comix 1.1.2 - Applied a workaround for a bug(?) in newer versions of PyGTK, which made the OK button in the file selection dialog return the wrong value, rendering it useless. Thanks to neota for this solution. # Comix 1.1.1 - The "file" dependancy is no longer needed. All mime type checks are now done internally. - Fixed an error in the .desktop file. - The `install.py` script now uses "update-desktop-database" to update the menus properly. - Some minor fixes. # Comix 1.1 - Support for nestled archives, i.e. archives in archives. - The `install.py` script now includes a "--installdir" option to let the user choose the install directory, e.g. /usr instead of /usr/local. - Comix now includes a manpage. - Some minor fixes. # Comix 1.0.2 - Added support for filenames encoded with various international character encodings. - Some minor fixes. # Comix 1.0.1 - Fixed a bug with the background colour option not working as expected on certain systems. # Comix 1.0 - Added an archive convert feature. Archives and directories can be converted to Zip, tar, tar.gz or tar.bz2. - More changes toward a hopefully more user-friendly GUI. # Comix 0.9 - A number of efforts has been made to make Comix more GNOME HIG compliant. - Added optional menubar, toolbar and statusbar. - Added an option to hide the mouse cursor. - The space key now works in fit to screen mode as well. - Fixed a bug with certain hotkeys not working before the right-click menu had been shown the first time. - Fixed a bug with archive names sometimes being displayed incorrectly in the bookmarks menu. - Some minor fixes. # Comix 0.8 - Comix now handles zip, tar, tar.gz and tar.bz2 archives internally. The unzip and tar programs are no longer needed. - New option to let you go to the next archive when flipping past the last page in the current archive, and vice versa for the first page. - Fixed a bug with small images sometimes being stretched to fit the screen although the corresponding option was not set. - Fixed a bug with exiting fullscreen mode with the escape key. - Supported image formats are now "only" JPEG, PNG, TIFF, GIF and BMP. There were problems with other files being reported as image files though they were not. - Some minor fixes. # Comix 0.7 - Added a new "bookmark" feature. - Now reads any image format supported by gtk.gdk.Pixbuf (that means most formats). - Fixed a bug with capital letters not working as hotkeys. - Some minor fixes. # Comix 0.6 - Added a new option to hide the scrollbars even when not in fit to screen mode. - Pressing space now scrolls the page to show the next part of it, if pressed at the bottom of the page it flips to the next page. - Dialogs should now look better and use buttons with stock icons. - Scrolling could be a bit faulty due to different dimensions of the scrollbars in different themes. That problem is now fixed. - `Fit width', `Fit height' and `Fit to screen' commands should now be exact. # Comix 0.5.2 - The makefile in 0.5.1 could in some cases change the permissions of the folders Comix were installed in to 0755. To solve this vulnerability Comix 0.5.2 uses a python script `install.py` to install the program instead. Usage of the 0.5.1 makefile is not recommended! # Comix 0.5.1 - Fixed a problem with the makefile. Destination directories will now be created if they do not already exist. # Comix 0.5 - Fixed a bug with the background colour option not working. - Added a colour picking dialog instead of the old entry box for the background colour. - Added a new more user-friendly file chooser dialog. - Added `Fit width', `Fit height' and `Fit to screen' commands to the right-click menu. - Added new option to save zooming values to future sessions. - The right-click menu and the preferences window now looks better and should hopefully be more user friendly. - Changed some hotkeys and their effect. - PyGTK requirement is now 2.6+. # Comix 0.4 - Comix now supports image zooming and window scrolling as well as the old fit-to-screen-mode. - Comix now includes a makefile, a .desktop file and an icon for an easy installation and desktop integration. # Comix 0.3 - New option to save window position and size to future sessions. - New option to set a default path to open when selecting a new file with the `Open file' command. - Comix now supports page scrolling with the mouse wheel. - Fixed a bug with trying to view `File info' when viewing the last page in double page mode. - Fixed a bug with the number of pages always reported as zero in the go-to-page-window. - Some minor fixes. # Comix 0.2.1 - Fixed a bug with opening archives with 3+ files with the same name. # Comix 0.2 - Now uses the shutil module to move and delete files instead of calling the `rm` and `mv` programs. - Now uses gtk.gdk.pixbuf_get_file_info() to check image file types and the `file' program to check filetypes for ZIP/RAR archives. Filename extensions should no longer matter for Comix. - Now supports tar, tar.gz and tar.bz2 archives. - New menu option `File info' brings up a window and displays various information about the file being viewed. - New option to flip the pages viewed in double page mode. - New option to reverse the reading order (start with the last page). - New option to set the saturation of the images displayed. - Some minor fixes. - New dependencies are `file` and `tar` (should be installed on most systems as default). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694888268.0 mcomix-3.1.0/MANIFEST.in0000644000175000017500000000014714501370514014241 0ustar00moritzmoritzinclude ChangeLog.md recursive-include mcomix/messages *.mo graft mcomix/images graft share prune test ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768 mcomix-3.1.0/PKG-INFO0000644000175000017500000005725314553265237013626 0ustar00moritzmoritzMetadata-Version: 2.1 Name: mcomix Version: 3.1.0 Summary: GTK comic book viewer Author: Pontus Ekberg Maintainer: The MComix Team License: MComix is licensed under the terms of the GNU General Public License, which can be found below, or at http://www.gnu.org/licenses/gpl-2.0.html. --- GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. Project-URL: Homepage, https://mcomix.sourceforge.io Project-URL: Documentation, https://sourceforge.net/projects/mcomix/Wiki/Home/ Project-URL: Repository, https://sourceforge.net/p/mcomix/git/ci/master/tree/ Project-URL: Changelog, https://sourceforge.net/p/mcomix/news/ Keywords: comix,comics,manga,images,reader,image viewer,cbr,cbz Classifier: Development Status :: 6 - Mature Classifier: Environment :: X11 Applications :: GTK Classifier: Intended Audience :: End Users/Desktop Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2) Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: POSIX :: BSD Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Multimedia :: Graphics :: Viewers Requires-Python: >=3.7 Description-Content-Type: text/markdown License-File: COPYING Requires-Dist: PyGObject>=3.36.0 Requires-Dist: pycairo>=1.16.0 Requires-Dist: Pillow>=6.0.0 Provides-Extra: fileformats Requires-Dist: chardet; extra == "fileformats" Requires-Dist: PyMuPDF>=1.19.2; extra == "fileformats" Provides-Extra: dev Requires-Dist: pyinstaller; os_name == "nt" and extra == "dev" Requires-Dist: build; extra == "dev" Requires-Dist: pip-review; extra == "dev" Requires-Dist: python-lsp-server[flake8]; extra == "dev" Requires-Dist: pylsp-mypy; extra == "dev" Requires-Dist: pyls-isort; extra == "dev" Requires-Dist: python-lsp-black; extra == "dev" Requires-Dist: types-Pillow; extra == "dev" Requires-Dist: pygobject-stubs; extra == "dev" # MComix README ## About MComix is a user-friendly, customizable image viewer. It is specifically designed to handle comic books (both Western comics and manga) and supports a variety of container formats. MComix is a fork of Comix. It is written in Python and uses GTK 3 through the PyGObject bindings. ## Installation Please follow the [installation instructions](https://sourceforge.net/p/mcomix/wiki/Installation/) on the Wiki. Most users will find it convenient to use the package provided by their operating system package manager. ## Dependencies For a list of packages and libraries needed to run MComix, please refer to [our documentation](https://sourceforge.net/p/mcomix/wiki/Home/#Dependencies). ## Credits Thanks to everyone who have contributed translations, suggestions, bug reports, fixes and donations! Icons with a filename starting with "gimp" are taken from The GIMP, and icons with a filename starting with "tango" are taken from the Tango Desktop Project. Most other icons are made by Victor Castillejo, creator of the GNOME-Colors icon theme. The directory mcomix/_vendor/packaging/ contains portions of 'packaging' version 21.0, (c) Donald Stufft and individual contributors. The packaging code is made available under the terms of either the Apache 2.0 license or BSD 2-clause license (user's choice). See mcomix/_vendor/packaging-21.0.dist-info/LICENSE for details. ## Contact Please use the [issue tracker](https://sourceforge.net/p/mcomix/_list/tickets) to get in touch with the MComix developers. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694848649.0 mcomix-3.1.0/README.md0000644000175000017500000000300414501253211013747 0ustar00moritzmoritz# MComix README ## About MComix is a user-friendly, customizable image viewer. It is specifically designed to handle comic books (both Western comics and manga) and supports a variety of container formats. MComix is a fork of Comix. It is written in Python and uses GTK 3 through the PyGObject bindings. ## Installation Please follow the [installation instructions](https://sourceforge.net/p/mcomix/wiki/Installation/) on the Wiki. Most users will find it convenient to use the package provided by their operating system package manager. ## Dependencies For a list of packages and libraries needed to run MComix, please refer to [our documentation](https://sourceforge.net/p/mcomix/wiki/Home/#Dependencies). ## Credits Thanks to everyone who have contributed translations, suggestions, bug reports, fixes and donations! Icons with a filename starting with "gimp" are taken from The GIMP, and icons with a filename starting with "tango" are taken from the Tango Desktop Project. Most other icons are made by Victor Castillejo, creator of the GNOME-Colors icon theme. The directory mcomix/_vendor/packaging/ contains portions of 'packaging' version 21.0, (c) Donald Stufft and individual contributors. The packaging code is made available under the terms of either the Apache 2.0 license or BSD 2-clause license (user's choice). See mcomix/_vendor/packaging-21.0.dist-info/LICENSE for details. ## Contact Please use the [issue tracker](https://sourceforge.net/p/mcomix/_list/tickets) to get in touch with the MComix developers. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9774768 mcomix-3.1.0/mcomix/0000755000175000017500000000000014553265237014011 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0 mcomix-3.1.0/mcomix/__init__.py0000644000175000017500000000000014476523373016112 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694519035.0 mcomix-3.1.0/mcomix/__main__.py0000644000175000017500000000174314500047373016077 0ustar00moritzmoritz"""MComix - GTK Comic Book Viewer """ # ------------------------------------------------------------------------- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # ------------------------------------------------------------------------- import multiprocessing as mp from .run import run def main() -> None: mp.freeze_support() mp.set_start_method('spawn') run() if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9574769 mcomix-3.1.0/mcomix/_vendor/0000755000175000017500000000000014553265237015445 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9774768 mcomix-3.1.0/mcomix/_vendor/packaging/0000755000175000017500000000000014553265237017371 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0 mcomix-3.1.0/mcomix/_vendor/packaging/__about__.py0000644000175000017500000000122514476523373021653 0ustar00moritzmoritz# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. __all__ = [ "__title__", "__summary__", "__uri__", "__version__", "__author__", "__email__", "__license__", "__copyright__", ] __title__ = "packaging" __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" __version__ = "21.0" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" __license__ = "BSD-2-Clause or Apache-2.0" __copyright__ = "2014-2019 %s" % __author__ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0 mcomix-3.1.0/mcomix/_vendor/packaging/__init__.py0000644000175000017500000000076114476523373021510 0ustar00moritzmoritz# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. from .__about__ import ( __author__, __copyright__, __email__, __license__, __summary__, __title__, __uri__, __version__, ) __all__ = [ "__title__", "__summary__", "__uri__", "__version__", "__author__", "__email__", "__license__", "__copyright__", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0 mcomix-3.1.0/mcomix/_vendor/packaging/_structures.py0000644000175000017500000000313514476523373022331 0ustar00moritzmoritz# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. class InfinityType: def __repr__(self) -> str: return "Infinity" def __hash__(self) -> int: return hash(repr(self)) def __lt__(self, other: object) -> bool: return False def __le__(self, other: object) -> bool: return False def __eq__(self, other: object) -> bool: return isinstance(other, self.__class__) def __ne__(self, other: object) -> bool: return not isinstance(other, self.__class__) def __gt__(self, other: object) -> bool: return True def __ge__(self, other: object) -> bool: return True def __neg__(self: object) -> "NegativeInfinityType": return NegativeInfinity Infinity = InfinityType() class NegativeInfinityType: def __repr__(self) -> str: return "-Infinity" def __hash__(self) -> int: return hash(repr(self)) def __lt__(self, other: object) -> bool: return True def __le__(self, other: object) -> bool: return True def __eq__(self, other: object) -> bool: return isinstance(other, self.__class__) def __ne__(self, other: object) -> bool: return not isinstance(other, self.__class__) def __gt__(self, other: object) -> bool: return False def __ge__(self, other: object) -> bool: return False def __neg__(self: object) -> InfinityType: return Infinity NegativeInfinity = NegativeInfinityType() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0 mcomix-3.1.0/mcomix/_vendor/packaging/py.typed0000644000175000017500000000000014476523373021060 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0 mcomix-3.1.0/mcomix/_vendor/packaging/version.py0000644000175000017500000003421614476523373021440 0ustar00moritzmoritz# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. import collections import itertools import re import warnings from typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType __all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"] InfiniteTypes = Union[InfinityType, NegativeInfinityType] PrePostDevType = Union[InfiniteTypes, Tuple[str, int]] SubLocalType = Union[InfiniteTypes, int, str] LocalType = Union[ NegativeInfinityType, Tuple[ Union[ SubLocalType, Tuple[SubLocalType, str], Tuple[NegativeInfinityType, SubLocalType], ], ..., ], ] CmpKey = Tuple[ int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType ] LegacyCmpKey = Tuple[int, Tuple[str, ...]] VersionComparisonMethod = Callable[ [Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool ] _Version = collections.namedtuple( "_Version", ["epoch", "release", "dev", "pre", "post", "local"] ) def parse(version: str) -> Union["LegacyVersion", "Version"]: """ Parse the given version string and return either a :class:`Version` object or a :class:`LegacyVersion` object depending on if the given version is a valid PEP 440 version or a legacy version. """ try: return Version(version) except InvalidVersion: return LegacyVersion(version) class InvalidVersion(ValueError): """ An invalid version was found, users should refer to PEP 440. """ class _BaseVersion: _key: Union[CmpKey, LegacyCmpKey] def __hash__(self) -> int: return hash(self._key) # Please keep the duplicated `isinstance` check # in the six comparisons hereunder # unless you find a way to avoid adding overhead function calls. def __lt__(self, other: "_BaseVersion") -> bool: if not isinstance(other, _BaseVersion): return NotImplemented return self._key < other._key def __le__(self, other: "_BaseVersion") -> bool: if not isinstance(other, _BaseVersion): return NotImplemented return self._key <= other._key def __eq__(self, other: object) -> bool: if not isinstance(other, _BaseVersion): return NotImplemented return self._key == other._key def __ge__(self, other: "_BaseVersion") -> bool: if not isinstance(other, _BaseVersion): return NotImplemented return self._key >= other._key def __gt__(self, other: "_BaseVersion") -> bool: if not isinstance(other, _BaseVersion): return NotImplemented return self._key > other._key def __ne__(self, other: object) -> bool: if not isinstance(other, _BaseVersion): return NotImplemented return self._key != other._key class LegacyVersion(_BaseVersion): def __init__(self, version: str) -> None: self._version = str(version) self._key = _legacy_cmpkey(self._version) def __str__(self) -> str: return self._version def __repr__(self) -> str: return f"" @property def public(self) -> str: return self._version @property def base_version(self) -> str: return self._version @property def epoch(self) -> int: return -1 @property def release(self) -> None: return None @property def pre(self) -> None: return None @property def post(self) -> None: return None @property def dev(self) -> None: return None @property def local(self) -> None: return None @property def is_prerelease(self) -> bool: return False @property def is_postrelease(self) -> bool: return False @property def is_devrelease(self) -> bool: return False _legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE) _legacy_version_replacement_map = { "pre": "c", "preview": "c", "-": "final-", "rc": "c", "dev": "@", } def _parse_version_parts(s: str) -> Iterator[str]: for part in _legacy_version_component_re.split(s): part = _legacy_version_replacement_map.get(part, part) if not part or part == ".": continue if part[:1] in "0123456789": # pad for numeric comparison yield part.zfill(8) else: yield "*" + part # ensure that alpha/beta/candidate are before final yield "*final" def _legacy_cmpkey(version: str) -> LegacyCmpKey: # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch # greater than or equal to 0. This will effectively put the LegacyVersion, # which uses the defacto standard originally implemented by setuptools, # as before all PEP 440 versions. epoch = -1 # This scheme is taken from pkg_resources.parse_version setuptools prior to # it's adoption of the packaging library. parts: List[str] = [] for part in _parse_version_parts(version.lower()): if part.startswith("*"): # remove "-" before a prerelease tag if part < "*final": while parts and parts[-1] == "*final-": parts.pop() # remove trailing zeros from each series of numeric parts while parts and parts[-1] == "00000000": parts.pop() parts.append(part) return epoch, tuple(parts) # Deliberately not anchored to the start and end of the string, to make it # easier for 3rd party code to reuse VERSION_PATTERN = r""" v? (?: (?:(?P[0-9]+)!)? # epoch (?P[0-9]+(?:\.[0-9]+)*) # release segment (?P
                                          # pre-release
            [-_\.]?
            (?P(a|b|c|rc|alpha|beta|pre|preview))
            [-_\.]?
            (?P[0-9]+)?
        )?
        (?P                                         # post release
            (?:-(?P[0-9]+))
            |
            (?:
                [-_\.]?
                (?Ppost|rev|r)
                [-_\.]?
                (?P[0-9]+)?
            )
        )?
        (?P                                          # dev release
            [-_\.]?
            (?Pdev)
            [-_\.]?
            (?P[0-9]+)?
        )?
    )
    (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
"""


class Version(_BaseVersion):

    _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)

    def __init__(self, version: str) -> None:

        # Validate the version and parse it into pieces
        match = self._regex.search(version)
        if not match:
            raise InvalidVersion(f"Invalid version: '{version}'")

        # Store the parsed out pieces of the version
        self._version = _Version(
            epoch=int(match.group("epoch")) if match.group("epoch") else 0,
            release=tuple(int(i) for i in match.group("release").split(".")),
            pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
            post=_parse_letter_version(
                match.group("post_l"), match.group("post_n1") or match.group("post_n2")
            ),
            dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
            local=_parse_local_version(match.group("local")),
        )

        # Generate a key which will be used for sorting
        self._key = _cmpkey(
            self._version.epoch,
            self._version.release,
            self._version.pre,
            self._version.post,
            self._version.dev,
            self._version.local,
        )

    def __repr__(self) -> str:
        return f""

    def __str__(self) -> str:
        parts = []

        # Epoch
        if self.epoch != 0:
            parts.append(f"{self.epoch}!")

        # Release segment
        parts.append(".".join(str(x) for x in self.release))

        # Pre-release
        if self.pre is not None:
            parts.append("".join(str(x) for x in self.pre))

        # Post-release
        if self.post is not None:
            parts.append(f".post{self.post}")

        # Development release
        if self.dev is not None:
            parts.append(f".dev{self.dev}")

        # Local version segment
        if self.local is not None:
            parts.append(f"+{self.local}")

        return "".join(parts)

    @property
    def epoch(self) -> int:
        _epoch: int = self._version.epoch
        return _epoch

    @property
    def release(self) -> Tuple[int, ...]:
        _release: Tuple[int, ...] = self._version.release
        return _release

    @property
    def pre(self) -> Optional[Tuple[str, int]]:
        _pre: Optional[Tuple[str, int]] = self._version.pre
        return _pre

    @property
    def post(self) -> Optional[int]:
        return self._version.post[1] if self._version.post else None

    @property
    def dev(self) -> Optional[int]:
        return self._version.dev[1] if self._version.dev else None

    @property
    def local(self) -> Optional[str]:
        if self._version.local:
            return ".".join(str(x) for x in self._version.local)
        else:
            return None

    @property
    def public(self) -> str:
        return str(self).split("+", 1)[0]

    @property
    def base_version(self) -> str:
        parts = []

        # Epoch
        if self.epoch != 0:
            parts.append(f"{self.epoch}!")

        # Release segment
        parts.append(".".join(str(x) for x in self.release))

        return "".join(parts)

    @property
    def is_prerelease(self) -> bool:
        return self.dev is not None or self.pre is not None

    @property
    def is_postrelease(self) -> bool:
        return self.post is not None

    @property
    def is_devrelease(self) -> bool:
        return self.dev is not None

    @property
    def major(self) -> int:
        return self.release[0] if len(self.release) >= 1 else 0

    @property
    def minor(self) -> int:
        return self.release[1] if len(self.release) >= 2 else 0

    @property
    def micro(self) -> int:
        return self.release[2] if len(self.release) >= 3 else 0


def _parse_letter_version(
    letter: str, number: Union[str, bytes, SupportsInt]
) -> Optional[Tuple[str, int]]:

    if letter:
        # We consider there to be an implicit 0 in a pre-release if there is
        # not a numeral associated with it.
        if number is None:
            number = 0

        # We normalize any letters to their lower case form
        letter = letter.lower()

        # We consider some words to be alternate spellings of other words and
        # in those cases we want to normalize the spellings to our preferred
        # spelling.
        if letter == "alpha":
            letter = "a"
        elif letter == "beta":
            letter = "b"
        elif letter in ["c", "pre", "preview"]:
            letter = "rc"
        elif letter in ["rev", "r"]:
            letter = "post"

        return letter, int(number)
    if not letter and number:
        # We assume if we are given a number, but we are not given a letter
        # then this is using the implicit post release syntax (e.g. 1.0-1)
        letter = "post"

        return letter, int(number)

    return None


_local_version_separators = re.compile(r"[\._-]")


def _parse_local_version(local: str) -> Optional[LocalType]:
    """
    Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
    """
    if local is not None:
        return tuple(
            part.lower() if not part.isdigit() else int(part)
            for part in _local_version_separators.split(local)
        )
    return None


def _cmpkey(
    epoch: int,
    release: Tuple[int, ...],
    pre: Optional[Tuple[str, int]],
    post: Optional[Tuple[str, int]],
    dev: Optional[Tuple[str, int]],
    local: Optional[Tuple[SubLocalType]],
) -> CmpKey:

    # When we compare a release version, we want to compare it with all of the
    # trailing zeros removed. So we'll use a reverse the list, drop all the now
    # leading zeros until we come to something non zero, then take the rest
    # re-reverse it back into the correct order and make it a tuple and use
    # that for our sorting key.
    _release = tuple(
        reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
    )

    # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
    # We'll do this by abusing the pre segment, but we _only_ want to do this
    # if there is not a pre or a post segment. If we have one of those then
    # the normal sorting rules will handle this case correctly.
    if pre is None and post is None and dev is not None:
        _pre: PrePostDevType = NegativeInfinity
    # Versions without a pre-release (except as noted above) should sort after
    # those with one.
    elif pre is None:
        _pre = Infinity
    else:
        _pre = pre

    # Versions without a post segment should sort before those with one.
    if post is None:
        _post: PrePostDevType = NegativeInfinity

    else:
        _post = post

    # Versions without a development segment should sort after those with one.
    if dev is None:
        _dev: PrePostDevType = Infinity

    else:
        _dev = dev

    if local is None:
        # Versions without a local segment should sort before those with one.
        _local: LocalType = NegativeInfinity
    else:
        # Versions with a local segment need that segment parsed to implement
        # the sorting rules in PEP440.
        # - Alpha numeric segments sort before numeric segments
        # - Alpha numeric segments sort lexicographically
        # - Numeric segments sort numerically
        # - Shorter versions sort before longer versions when the prefixes
        #   match exactly
        _local = tuple(
            (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
        )

    return epoch, _release, _pre, _post, _dev, _local
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/about_dialog.py0000644000175000017500000000375314476523373017026 0ustar00moritzmoritz# -*- coding: utf-8 -*-
"""about_dialog.py - About dialog."""

from gi.repository import Gtk
import pkgutil
import webbrowser

from mcomix import constants
from mcomix import strings
from mcomix import image_tools
from mcomix.i18n import _

class _AboutDialog(Gtk.AboutDialog):

    def __init__(self, window):
        super(_AboutDialog, self).__init__(parent=window)

        self.set_name(constants.APPNAME)
        self.set_program_name(constants.APPNAME)
        self.set_version(constants.VERSION)
        self.set_website('https://sourceforge.net/p/mcomix/wiki/')
        self.set_copyright('Copyright © 2005-2022')

        icon_data = pkgutil.get_data('mcomix', 'images/mcomix.png')
        pixbuf = image_tools.load_pixbuf_data(icon_data)
        self.set_logo(pixbuf)

        comment = \
            _('%s is an image viewer specifically designed to handle comic books.') % \
            constants.APPNAME + ' ' + \
            _('It reads ZIP, RAR and tar archives, as well as plain image files.')
        self.set_comments(comment)

        license = \
            _('%s is licensed under the terms of the GNU General Public License.') % constants.APPNAME + \
            ' ' + \
            _('A copy of this license can be obtained from %s') % \
            'http://www.gnu.org/licenses/gpl-2.0.html'
        self.set_wrap_license(True)
        self.set_license(license)

        authors = [ '%s: %s' % (name, description) for name, description in strings.AUTHORS ]
        self.set_authors(authors)

        translators = [ '%s: %s' % (name, description) for name, description in strings.TRANSLATORS ]
        self.set_translator_credits("\n".join(translators))

        artists = [ '%s: %s' % (name, description) for name, description in strings.ARTISTS ]
        self.set_artists(artists)

        self.connect('activate-link', self._on_activate_link)

        self.show_all()

    def _on_activate_link(self, about_dialog, uri):
        webbrowser.open(uri)
        return True

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9774768
mcomix-3.1.0/mcomix/archive/0000755000175000017500000000000014553265237015432 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/archive/__init__.py0000644000175000017500000000000014476523373017533 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694847829.0
mcomix-3.1.0/mcomix/archive/archive_base.py0000644000175000017500000002233714501251525020412 0ustar00moritzmoritz# -*- coding: utf-8 -*-

""" Base class for unified handling of various archive formats. Used for simplifying
extraction and adding new archive formats. """

import os
import errno
import threading

from mcomix import portability
from mcomix import i18n
from mcomix import process
from mcomix import callback
from mcomix.archive import password as archive_password


class BaseArchive(object):
    """ Base archive interface. All filenames passed from and into archives
    are expected to be Unicode objects. Archive files are converted to
    Unicode with some guess-work. """

    """ True if concurrent calls to extract is supported. """
    support_concurrent_extractions = False

    def __init__(self, archive):
        assert isinstance(archive, str), "File should be an Unicode string."

        self.archive = archive
        self._password = None
        self._event = threading.Event()
        if self.support_concurrent_extractions:
            # When multiple concurrent extractions are supported,
            # we need a lock to handle concurent calls to _get_password.
            self._lock = threading.Lock()
            self._waiting_for_password = False

    def iter_contents(self):
        """ Generator for listing the archive contents.
        """
        return
        yield

    def list_contents(self):
        """ Returns a list of unicode filenames relative to the archive root.
        These names do not necessarily exist in the actual archive since they
        need to saveable on the local filesystems, so some characters might
        need to be replaced. """

        return [f for f in self.iter_contents()]

    def extract(self, filename, destination_dir):
        """ Extracts the file specified by . This filename must
        be obtained by calling list_contents(). The file is saved to
        . """

        assert isinstance(filename, str) and \
            isinstance(destination_dir, str)

    def iter_extract(self, entries, destination_dir):
        """ Generator to extract  from archive to . """
        wanted = set(entries)
        for filename in self.iter_contents():
            if not filename in wanted:
                continue
            self.extract(filename, destination_dir)
            yield filename
            wanted.remove(filename)
            if 0 == len(wanted):
                break

    def close(self):
        """ Closes the archive and releases held resources. """

        pass

    def is_solid(self):
        """ Returns True if the archive is solid and extraction should be done
        in one pass. """
        return False

    def _replace_invalid_filesystem_chars(self, filename):
        """ Replaces characters in  that cannot be saved to the disk
        with underscore and returns the cleaned-up name. """

        unsafe_chars = portability.invalid_filesystem_chars()
        translation_table = {}
        replacement_char = '_'
        for char in unsafe_chars:
            translation_table[ord(char)] = replacement_char

        new_name = filename.translate(translation_table)

        # Make sure the filename does not contain portions that might
        # traverse directories, i.e. do not allow absolute paths
        # and paths containing ../
        normalized = os.path.normpath(new_name)
        return normalized.lstrip('..' + os.sep).lstrip(os.sep)

    def _create_directory(self, directory):
        """ Recursively create a directory if it doesn't exist yet. """
        if os.path.exists(directory):
            return
        try:
            os.makedirs(directory)
        except OSError as e:
            # Can happen with concurrent calls.
            if e.errno != errno.EEXIST:
                raise e

    def _create_file(self, dst_path):
        """ Open  for writing, making sure base directory exists. """
        dst_dir = os.path.dirname(dst_path)
        # Create directory if it doesn't exist
        self._create_directory(dst_dir)
        return open(dst_path, 'wb')

    @callback.Callback
    def _password_required(self):
        """ Asks the user for a password and sets .
        If  is None, no password has been requested yet.
        If an empty string is set, assume that the user did not provide
        a password. """

        password = archive_password.ask_for_password(self.archive)
        if password is None:
            password = ""

        self._password = password
        self._event.set()

    def _get_password(self):
        ask_for_password = self._password is None
        # Don't trigger concurrent password dialogs.
        if ask_for_password and self.support_concurrent_extractions:
            with self._lock:
                if self._waiting_for_password:
                    ask_for_password = False
                else:
                    self._waiting_for_password = True
        if ask_for_password:
            self._password_required()
        self._event.wait()

class NonUnicodeArchive(BaseArchive):
    """ Base class for archives that manage a conversion of byte member names ->
    Unicode member names internally. Required for formats that do not provide
    wide character member names. """

    def __init__(self, archive):
        super(NonUnicodeArchive, self).__init__(archive)
        # Maps Unicode names to regular names as expected by the original archive format
        self.unicode_mapping = {}

    def _unicode_filename(self, filename, conversion_func=i18n.to_unicode):
        """ Instead of returning archive members directly, map each filename through
        this function first to convert them to Unicode. """

        unicode_name = conversion_func(filename)
        safe_name = self._replace_invalid_filesystem_chars(unicode_name)
        self.unicode_mapping[safe_name] = filename
        return safe_name

    def _original_filename(self, filename):
        """ Map Unicode filename back to original archive name. """
        if filename in self.unicode_mapping:
            return self.unicode_mapping[filename]
        else:
            return i18n.to_utf8(filename)

class ExternalExecutableArchive(NonUnicodeArchive):
    """ For archives that are extracted by spawning an external
    application. """

    # Since we're using an external program for extraction,
    # concurrent calls are supported.
    support_concurrent_extractions = True

    def __init__(self, archive):
        super(ExternalExecutableArchive, self).__init__(archive)
        # Flag to determine if list_contents() has been called
        # This builds the Unicode mapping and is likely required
        # for extracting filenames that have been internally mapped.
        self.filenames_initialized = False

    def _get_executable(self):
        """ Returns the executable's name or path. Return None if no executable
        was found on the system. """
        raise NotImplementedError("Subclasses must override _get_executable.")

    def _get_list_arguments(self):
        """ Returns an array of arguments required for the executable
        to produce a list of archive members. """
        raise NotImplementedError("Subclasses must override _get_list_arguments.")

    def _get_extract_arguments(self):
        """ Returns an array of arguments required for the executable
        to extract a file to STDOUT. """
        raise NotImplementedError("Subclasses must override _get_extract_arguments.")

    def _parse_list_output_line(self, line):
        """ Parses the output of the external executable's list command
        and return either a file path relative to the archive's root,
        or None if the current line doesn't contain any file references. """

        return line

    def iter_contents(self):
        if not self._get_executable():
            return

        proc = process.popen([self._get_executable()] +
                             self._get_list_arguments() +
                             [self.archive])
        try:
            for line in proc.stdout:
                filename = self._parse_list_output_line(line.rstrip(os.linesep))
                if filename is not None:
                    yield self._unicode_filename(filename)
        finally:
            proc.stdout.close()
            proc.wait()

        self.filenames_initialized = True

    def extract(self, filename, destination_dir):
        """ Extract  from the archive to . """
        assert isinstance(filename, str) and \
                isinstance(destination_dir, str)

        if not self._get_executable():
            return

        if not self.filenames_initialized:
            self.list_contents()

        output = self._create_file(os.path.join(destination_dir, filename))
        try:
            process.call([self._get_executable()] +
                         self._get_extract_arguments() +
                         [self.archive, self._original_filename(filename)],
                         stdout=output)
        finally:
            output.close()


class DisabledArchive(BaseArchive):
    """Returned to indicate that a requested archiver is unavailable."""

    def __init__(self, archive: str) -> None:
        super().__init__(archive)

    @staticmethod
    def is_available() -> bool:
        """Status of this archiver (always false)."""
        return False

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/archive/archive_recursive.py0000644000175000017500000001336714476523373021530 0ustar00moritzmoritz# -*- coding: utf-8 -*-

""" Class for transparently handling an archive containing sub-archives. """

from mcomix.archive import archive_base
from mcomix import archive_tools
from mcomix import log

import os

class RecursiveArchive(archive_base.BaseArchive):

    def __init__(self, archive, destination_dir):
        super(RecursiveArchive, self).__init__(archive.archive)
        self._main_archive = archive
        self._destination_dir = destination_dir
        self._archive_list = []
        # Map entry name to its archive+name.
        self._entry_mapping = {}
        # Map archive to its root.
        self._archive_root = {}
        self._contents_listed = False
        self._contents = []
        # Assume concurrent extractions are not supported.
        self.support_concurrent_extractions = False

    def _iter_contents(self, archive, root=None):
        self._archive_list.append(archive)
        self._archive_root[archive] = root
        sub_archive_list = []
        for f in archive.iter_contents():
            if archive_tools.is_archive_file(f):
                # We found a sub-archive, don't try to extract it now, as we
                # must finish listing the containing archive contents before
                # any extraction can be done.
                sub_archive_list.append(f)
                continue
            name = f
            if root is not None:
                name = os.path.join(root, name)
            self._entry_mapping[name] = (archive, f)
            yield name
        for f in sub_archive_list:
            # Extract sub-archive.
            destination_dir = self._destination_dir
            if root is not None:
                destination_dir = os.path.join(destination_dir, root)
            archive.extract(f, destination_dir)
            sub_archive_ext = os.path.splitext(f)[1].lower()[1:]
            sub_archive_path = os.path.join(
                self._destination_dir, 'sub-archives',
                '%04u.%s' % (len(self._archive_list), sub_archive_ext
            ))
            self._create_directory(os.path.dirname(sub_archive_path))
            os.rename(os.path.join(destination_dir, f), sub_archive_path)
            # And open it and list its contents.
            sub_archive = archive_tools.get_archive_handler(sub_archive_path)
            if sub_archive is None:
                log.warning('Non-supported archive format: %s',
                            os.path.basename(sub_archive_path))
                continue
            sub_root = f
            if root is not None:
                sub_root = os.path.join(root, sub_root)
            for name in self._iter_contents(sub_archive, sub_root):
                yield name

    def _check_concurrent_extraction_support(self):
        supported = True
        # We need all archives to support concurrent extractions.
        for archive in self._archive_list:
            if not archive.support_concurrent_extractions:
                supported = False
                break
        self.support_concurrent_extractions = supported

    def iter_contents(self):
        if self._contents_listed:
            for f in self._contents:
                yield f
            return
        self._contents = []
        for f in self._iter_contents(self._main_archive):
            self._contents.append(f)
            yield f
        self._contents_listed = True
        # We can now check if concurrent extractions are really supported.
        self._check_concurrent_extraction_support()

    def list_contents(self):
        if self._contents_listed:
            return self._contents
        return [f for f in self.iter_contents()]

    def extract(self, filename, destination_dir):
        if not self._contents_listed:
            self.list_contents()
        archive, name = self._entry_mapping[filename]
        root = self._archive_root[archive]
        if root is not None:
            destination_dir = os.path.join(destination_dir, root)
        log.debug('extracting from %s to %s: %s',
                  archive.archive, destination_dir, filename)
        archive.extract(name, destination_dir)

    def iter_extract(self, entries, destination_dir):
        if not self._contents_listed:
            self.list_contents()
        # Unfortunately we can't just rely on BaseArchive default
        # implementation if solid archives are to be correctly supported:
        # we need to call iter_extract (not extract) for each archive ourselves.
        wanted = set(entries)
        for archive in self._archive_list:
            archive_wanted = {}
            for name in wanted:
                name_archive, name_archive_name = self._entry_mapping[name]
                if name_archive == archive:
                    archive_wanted[name_archive_name] = name
            if 0 == len(archive_wanted):
                continue
            root = self._archive_root[archive]
            archive_destination_dir = destination_dir
            if root is not None:
                archive_destination_dir = os.path.join(destination_dir, root)
            log.debug('extracting from %s to %s: %s',
                      archive.archive, archive_destination_dir,
                      ' '.join(list(archive_wanted.keys())))
            for f in archive.iter_extract(list(archive_wanted.keys()), archive_destination_dir):
                yield archive_wanted[f]
            wanted -= set(archive_wanted.values())
            if 0 == len(wanted):
                break

    def is_solid(self):
        if not self._contents_listed:
            self.list_contents()
        # We're solid if at least one archive is solid.
        for archive in self._archive_list:
            if archive.is_solid():
                return True
        return False

    def close(self):
        for archive in self._archive_list:
            archive.close()

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/archive/lha_external.py0000644000175000017500000000226414476523373020460 0ustar00moritzmoritz# -*- coding: utf-8 -*-

""" LHA archive extractor. """

import re

from mcomix import process
from mcomix.archive import archive_base

# Filled on-demand by LhaArchive
_lha_executable = -1

class LhaArchive(archive_base.ExternalExecutableArchive):
    """ LHA file extractor using the lha executable. """

    def _get_executable(self):
        return LhaArchive._find_lha_executable()

    def _get_list_arguments(self):
        return ['l', '-g', '-q2']

    def _get_extract_arguments(self):
        return ['p', '-q2']

    def _parse_list_output_line(self, line):
        match = re.search(r'\[generic\]\s+\d+\s+\S+?\s+\w+\s+\d+\s+\d+\s+(.+)$', line)
        if match:
            return match.group(1)
        else:
            return None

    @staticmethod
    def _find_lha_executable():
        """ Tries to start lha, and returns either 'lha' if
        it was started successfully or None otherwise. """
        global _lha_executable
        if _lha_executable == -1:
            _lha_executable = process.find_executable(('lha',))
        return _lha_executable

    @staticmethod
    def is_available():
        return bool(LhaArchive._find_lha_executable())


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701597438.0
mcomix-3.1.0/mcomix/archive/mobi.py0000644000175000017500000000571114533050376016730 0ustar00moritzmoritz# -*- coding: utf-8 -*-

''' MobiPocket handling (extract pictures) for MComix.

    Based on code from mobiunpack by Charles M. Hannum et al.
'''

import os
import re
import struct

from gi.repository import Gio

from mcomix import image_tools
from mcomix.archive import archive_base

class UnpackException(Exception):
    pass

class Sectionizer:
    def __init__(self, f):
        self.f = f
        header = self.f.read(78)
        self.ident = header[0x3C:0x3C+8]
        self.num_sections, = struct.unpack_from('>H', header, 76)
        sections = self.f.read(self.num_sections*8)
        self.sections = struct.unpack_from('>%dL' % (self.num_sections*2), sections, 0)[::2] + (0x7fffffff, )

    def loadSection(self, section, limit=0x7fffffff):
        before, after = self.sections[section:section+2]
        self.f.seek(before)
        if limit > after - before:
            limit = after - before
        return self.f.read(limit)

class MobiArchive(archive_base.NonUnicodeArchive):
    def __init__(self, archive):
        super(MobiArchive, self).__init__(archive)
        f = open(archive, 'rb')
        try:
            self.file = f
            self.sect = Sectionizer(self.file)
            if self.sect.ident != b'BOOKMOBI':
                raise UnpackException('invalid file format')
            self.header = self.sect.loadSection(0)
            self.crypto_type, = struct.unpack_from('>H', self.header, 0xC)
            if self.crypto_type != 0:
                raise UnpackException('file is encrypted')
            self.firstimg, = struct.unpack_from('>L', self.header, 0x6C)
        except:
            self.file = None
            f.close()
            raise

    def _close(self):
        if self.file is not None:
            self.file.close()
            self.file = None

    def iter_contents(self):
        ''' List archive contents. '''
        supported_mimes={}
        for mimes,exts in image_tools.get_supported_formats().values():
            ext = next(iter(exts))
            for mime in mimes:
                supported_mimes[mime] = ext
        for i in range(self.firstimg, self.sect.num_sections):
            magic = self.sect.loadSection(i, 10)
            mime, uncertain = Gio.content_type_guess(data=magic)
            mime = mime.lower()
            if mime in supported_mimes:
                ext = supported_mimes[mime]
                yield "image%05d.%s" % (1+i-self.firstimg, ext)

    def extract(self, filename, destination_dir):
        ''' Extract  from the archive to . '''
        destination_path = os.path.join(destination_dir, filename)
        fnparts = re.split(r'^image([0-9]*)\.', filename)
        if len(fnparts) == 3:
            i = int(fnparts[1])-1+self.firstimg
            data = self.sect.loadSection(i)
            with self._create_file(destination_path) as new:
                new.write(data)
        return destination_path

    def close(self):
        ''' Close the archive handle '''
        self._close()
././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9774768
mcomix-3.1.0/mcomix/archive/native_pdf/0000755000175000017500000000000014553265237017551 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/archive/native_pdf/__init__.py0000644000175000017500000000000014476523373021652 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694785214.0
mcomix-3.1.0/mcomix/archive/native_pdf/child.py0000644000175000017500000001663214501057276021211 0ustar00moritzmoritz"""Child process code for native PDF multiprocessing."""

import io
import os
import multiprocessing as mp
from PIL import Image
from typing import Generator, Optional

import fitz

from mcomix.constants import PDF_RENDER_DPI_DEF
from mcomix.preferences import prefs


# Will delimit the page name from the xref part of a file name
XREF_DELIMITER = '_mcmxref'


class FitzWorker:
    def __init__(self, filename: Optional[str], log_level: Optional[int] = None) -> None:
        self._extension: Optional[str] = None
        self._complex_doc = False
        self.log = mp.get_logger()
        if log_level is not None:
            self.log.setLevel(log_level)
        self.doc = fitz.open(filename)

    def page_count(self) -> int:
        return self.doc.page_count

    def _image_extension(self, page_num: int) -> str:
        """Return the filename extension for the page image."""
        if self._extension is None:
            self._extension = self._check_image_type(page_num)
        return self._extension

    def _extract_as_image(self, page_num: int) -> bool:
        """Check whether the page can be extracted via image export."""
        if self._complex_doc:
            return False
        if not self._must_render_page(page_num) and self._can_extract_image(page_num):
            return True
        return False

    def _must_render_page(self, page_num: int) -> bool:
        """Determine if a page has any forced-render markers.

        Rendering to pixmap must be forced if any of these apply:
            - The page has any text content
            - The page has any drawing content
            - The page contains 0, or more than 1, embedded image
        """
        page = self.doc[page_num]
        if len(page.get_images()) != 1 or len(page.get_text()) > 0:
            self._complex_doc = True
            result = True
            self.log.debug("PDF page %d, must render page", page_num + 1)
        else:
            result = False
            self.log.debug("PDF page %d, rendering not forced", page_num + 1)
        del page
        return result

    def _can_extract_image(self, page_num: int) -> bool:
        """Determine if a page has an extractable image.

        Makes a closer examination than _must_render_page(),
        by checking whether the page contains a single, full-page
        image, then actually extracting the first such image
        encountered to determine the filetype (extension).

        (Subsequent embedded images are assumed to have the same
        type as the first full-page image encountered. This may
        be somewhat fragile, but it's a huge performance boost.)
        """
        page = self.doc[page_num]
        image_info = page.get_image_info()
        if len(image_info) != 1:
            self.log.debug(
                "PDF page %d, cannot extract. Image count = %d",
                page_num + 1, len(image_info))
            return False
        info = image_info[0]
        img_rect = fitz.Rect(info.get('bbox', (0, 0, 0, 0))).irect
        page_rect = fitz.Rect(page.mediabox).irect
        page_area = page_rect.get_area()
        area_diff = abs(page_area - img_rect.get_area())
        is_full_page: bool = area_diff < 0.05 * page_area
        if is_full_page:
            self.log.debug('PDF page %d: can extract fullpage image', page_num + 1)
        else:
            self.log.debug(
                'PDF page %d, cannot extract image: %s',
                page_num + 1, f"img_rect={img_rect}, page_rect={page_rect}")
        del page
        del image_info
        return is_full_page

    def _check_image_type(self, page_num: int) -> str:
        """Examine the page's embedded image for its file type.

        The extension is determined heuristically by probing only the
        first page of the document for an embedded image, then using
        its type.

        If the first page does have a single embedded image, it's
        assumed that _all_ pages contain an image of the same type.
        This may be a fragile assumption.

        If the probe fails, 'png' is used as a fallback, as that's the
        type rendered page pixmaps will be saved with.
        """
        extension = 'png'
        try:
            xref = self._get_image_xref(page_num)
            img = fitz.image_profile(
                self.doc.xref_stream_raw(xref))
            # If image_profile returns an empty dict, the image type is
            # "exotic" and not supported for direct extraction.
            # That doesn't mean that the images can't be extracted.
            # Document.extract_image will automatically convert to PNG,
            # when we call it to extract the xref. It'll be slower than
            # extraction without converting, but still very fast.
            if img:
                extension = img.get('ext', 'png')
                del img
        except (AttributeError, TypeError):
            pass
        finally:
            return extension

    def _get_image_xref(self, page_num: int) -> int:
        try:
            image_info = self.doc.get_page_images(page_num)
            xref = int(image_info[0][0])
            return xref
        except (TypeError, IndexError):
            return -1
        finally:
            image_info = None

    def iter_contents(self) -> Generator[str, None, None]:
        for pg in range(self.doc.page_count):
            pagenum = f"page{pg + 1:04}"
            if self._extract_as_image(pg):
                xref = self._get_image_xref(pg)
                ext = self._image_extension(pg)
                filename = f"{pagenum}{XREF_DELIMITER}{xref:04}.{ext}"
            else:
                filename = f"{pagenum}.png"
            yield filename

    def extract_xref(self, page: int, xref: int, path: str) -> None:
        """Save the embedded PDF image for a given xref. The page is indexed starting with zero."""
        img = self.doc.extract_image(xref)
        if not img:
            return
        os.makedirs(os.path.dirname(path), exist_ok=True)
        img_bytes = img.get("image", b"")
        del img

        # The extract_image method always returns the unrotated version of the image,
        # unaffected by any page modifications of rotation.
        rotation = self.doc[page].rotation
        if rotation in (90, 180, 270) and prefs['auto rotate from exif']:
            buffer = io.BytesIO(img_bytes)
            pil_img = Image.open(buffer)
            transpose = Image.Transpose.ROTATE_270
            if rotation == 180:
                transpose = Image.Transpose.ROTATE_180
            elif rotation == 270:
                transpose = Image.Transpose.ROTATE_90
            pil_img = pil_img.transpose(transpose)
            pil_img.save(path)

        else:
            with open(path, "wb") as out:
                out.write(img_bytes)
        del img_bytes

    def render_page(self, pg: int, path: str) -> None:
        """Render the page to an image file and save."""
        page = self.doc[pg]
        pixmap = page.get_pixmap(dpi=PDF_RENDER_DPI_DEF)
        pixmap.save(path)
        del pixmap
        del page

    def extract_file(self, filename: str, dest: str) -> None:
        outpath = os.path.join(dest, filename)
        if XREF_DELIMITER in filename:
            pginfo, ref = filename.split(XREF_DELIMITER)
            page = int(pginfo[-4:]) - 1
            xref = int(ref[:4])
            self.extract_xref(page, xref, outpath)
        elif filename.startswith('page'):
            pg_num = int(filename[4:8]) - 1
            self.render_page(pg_num, outpath)
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694496588.0
mcomix-3.1.0/mcomix/archive/native_pdf/manager.py0000644000175000017500000000620614477773514021550 0ustar00moritzmoritz"""Manager code for spawned processes in multiprocessing implementation
of PDF extractor."""

import sys
import threading
import multiprocessing as mp
from multiprocessing.managers import BaseManager, BaseProxy

from typing import Optional, Generator

from .child import FitzWorker


class GeneratorProxy(BaseProxy):
    """Proxy type for generator objects."""

    _exposed_ = ['__next__']

    def __iter__(self):
        return self

    def __next__(self):
        return self._callmethod('__next__')


class WorkerProxy(BaseProxy):
    """Proxy type for FitzWorker methods.

    This code will run in the child processes, when triggered by
    the registered methods of the Manager.
    """

    filename: Optional[str] = None

    @classmethod
    def _open(cls, filename):
        cls.filename = filename

    @classmethod
    def _count_pages(cls) -> int:
        w = FitzWorker(cls.filename)
        return w.page_count()

    @classmethod
    def _list_pages(cls) -> Generator[str, None, None]:
        w = FitzWorker(cls.filename)
        return w.iter_contents()

    @classmethod
    def _extract_pages(cls, entries, save_path: str) -> Generator[str, None, None]:
        w = FitzWorker(cls.filename)
        for e in entries:
            w.extract_file(e, save_path)
            yield e


class FitzManager(BaseManager):
    """Multiprocessing manager to hold proxied worker callables."""

    pass


FitzManager.register('open', WorkerProxy._open)
FitzManager.register('page_count', WorkerProxy._count_pages)
FitzManager.register('iter_contents', WorkerProxy._list_pages, proxytype=GeneratorProxy)
FitzManager.register('extract_pages', WorkerProxy._extract_pages, proxytype=GeneratorProxy)


class FitzProcessWrangler(threading.local):
    """Thread-local state object holding a FitzManager instance.

    This is necessary so that each Mcomix extractor thread has its own
    FitzManager instance (and, therefore, its own worker process).
    """

    def __init__(self, filename, log_level):
        self.mgr = FitzManager()
        self.mgr.start()
        self.mgr.open(filename)
        self.log = mp.get_logger()
        if log_level is not None:
            self.log.setLevel(log_level)

    def page_count(self) -> int:
        """Get the number of pages in the PDF."""
        return self.mgr.page_count()

    def iter_contents(self) -> Generator[str, None, None]:
        """Return an iterator over all the page filenames in the PDF."""
        return self.mgr.iter_contents()

    def extract_pages(self, page_list, destination_dir) -> Generator[str, None, None]:
        """Extract the listed pages to the given directory."""
        return self.mgr.extract_pages(page_list, destination_dir)


# Test code, this module can be called directly with one argument (a PDF
# filename), and will print a list of all pages in the PDF (produced by
# a spawned FitzWorker process)
if __name__ == "__main__":
    mp.freeze_support()
    mp.set_start_method('spawn')
    infile = sys.argv[1]
    import logging
    wrangler = FitzProcessWrangler(infile, log_level=logging.DEBUG)

    print(f"All pages in PDF {infile}:")
    for file in wrangler.iter_contents():
        print(f"  {file}")
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/archive/native_pdf/parent.py0000644000175000017500000000415214476523373021420 0ustar00moritzmoritz# -*- coding: utf-8 -*-

"""Multiprocessing PDF handler."""

import multiprocessing as mp
from mcomix.archive import archive_base
from mcomix.archive.native_pdf.manager import FitzProcessWrangler
from mcomix.log import getLevel

from typing import Generator, List


class FitzArchive(archive_base.BaseArchive):
    """PDF file reader/extractor using PyMuPDF."""

    # Concurrent calls to extract welcome!
    support_concurrent_extractions = True

    def __init__(self, archive) -> None:
        """Initialize the object, first as a BaseArchive."""
        super().__init__(archive)
        self.log = mp.get_logger()
        self.log.setLevel(getLevel())
        self.log.debug("PDF contains %d pages", self.mgr.page_count())

    @staticmethod
    def is_available() -> bool:
        """Report whether this extractor is available (always true)."""
        return True

    def _open_doc(self) -> FitzProcessWrangler:
        """Create a new FitzManager instance for processes accessing the archive."""
        self.close()
        self._mgr = FitzProcessWrangler(self.archive, log_level=self.log.level)
        return self._mgr

    def close(self) -> None:
        """Destroy the wrangler object and free resources."""
        if hasattr(self, '_mgr'):
            del self._mgr

    @property
    def mgr(self) -> FitzProcessWrangler:
        if not hasattr(self, '_mgr'):
            return self._open_doc()
        return self._mgr

    def iter_contents(self) -> Generator[str, None, None]:
        """Generate page filenames."""
        return self.mgr.iter_contents()

    def extract(self, filename, destination_dir):
        """Extract the named page file to a directory."""
        self._create_directory(destination_dir)
        output = list(self.mgr.extract_pages([filename], destination_dir))
        if len(output) > 0:
            return True
        return False

    def iter_extract(self, entries: List[str], destination_dir: str) -> Generator[str, None, None]:
        """Return a generator of extracted filepaths."""
        self._create_directory(destination_dir)
        return self.mgr.extract_pages(entries, destination_dir)
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/archive/password.py0000644000175000017500000000207014476523373017647 0ustar00moritzmoritz# -*- coding: utf-8 -*-

from gi.repository import Gtk

from mcomix import message_dialog
from mcomix.i18n import _

def ask_for_password(archive):
    """ Openes an input dialog to ask for a password. Returns either
    an Unicode string (the password), or None."""
    dialog = message_dialog.MessageDialog(None, Gtk.DialogFlags.MODAL,
            Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL)
    dialog.set_text(
        _("The archive is password-protected:"),
        archive + '\n\n' +
        ("Please enter the password to continue:"))
    dialog.set_default_response(Gtk.ResponseType.OK)
    dialog.set_auto_destroy(False)

    password_box = Gtk.Entry()
    password_box.set_visibility(False)
    password_box.set_activates_default(True)
    dialog.get_content_area().pack_end(password_box, True, True, 0)
    dialog.set_focus(password_box)

    result = dialog.run()
    password = password_box.get_text()
    dialog.destroy()

    if result == Gtk.ResponseType.OK and password:
        return password
    else:
        return None

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/archive/pdf_external.py0000644000175000017500000001133014476523373020457 0ustar00moritzmoritz# -*- coding: utf-8 -*-

""" PDF handler. """

from mcomix import log
from mcomix import process
from mcomix.version_tools import LegacyVersion
from mcomix.archive import archive_base
from mcomix.constants import PDF_RENDER_DPI_DEF, PDF_RENDER_DPI_MAX

import math
import os
import re
import subprocess

_pdf_possible = None
_mutool_exec = None
_mudraw_exec = None
_mudraw_trace_args = None

class PdfArchive(archive_base.BaseArchive):

    """ Concurrent calls to extract welcome! """
    support_concurrent_extractions = True

    _fill_image_regex = re.compile(r'^\s*[^"]+)".*\bwidth="(?P\d+)".*\bheight="(?P\d+)".*/>\s*$')

    def __init__(self, archive):
        super(PdfArchive, self).__init__(archive)

    def iter_contents(self):
        proc = subprocess.run(_mutool_exec + ['show', '--', self.archive, 'pages'], stdout=subprocess.PIPE, encoding='utf-8')
        for line in proc.stdout.splitlines():
            if line.startswith('page '):
                yield line.split()[1] + '.png'

    def extract(self, filename, destination_dir):
        self._create_directory(destination_dir)
        destination_path = os.path.join(destination_dir, filename)
        page_num = int(filename[0:-4])
        # Try to find optimal DPI.
        cmd = _mudraw_exec + _mudraw_trace_args + ['--', self.archive, str(page_num)]
        log.debug('finding optimal DPI for %s: %s', filename, ' '.join(cmd))
        proc = subprocess.run(cmd, stdout=subprocess.PIPE, encoding='utf-8', errors='replace')
        max_size = 0
        max_dpi = PDF_RENDER_DPI_DEF
        for line in proc.stdout.splitlines():
            match = self._fill_image_regex.match(line)
            if not match:
                continue
            matrix = [float(f) for f in match.group('matrix').split()]
            for size, coeff1, coeff2 in (
                (int(match.group('width')), matrix[0], matrix[1]),
                (int(match.group('height')), matrix[2], matrix[3]),
            ):
                if size < max_size:
                    continue
                render_size = math.sqrt(coeff1 * coeff1 + coeff2 * coeff2)
                dpi = int(size * 72 / render_size)
                if dpi > PDF_RENDER_DPI_MAX:
                    dpi = PDF_RENDER_DPI_MAX
                max_size = size
                max_dpi = dpi
        # Render...
        cmd = _mudraw_exec + ['-r', str(max_dpi), '-o', destination_path, '--', self.archive, str(page_num)]
        log.debug('rendering %s: %s', filename, ' '.join(cmd))
        process.call(cmd)

    @staticmethod
    def is_available():
        global _pdf_possible
        if _pdf_possible is not None:
            return _pdf_possible
        global _mutool_exec, _mudraw_exec, _mudraw_trace_args
        mutool = process.find_executable(('mutool',))
        _pdf_possible = False
        version = None
        if mutool is None:
            log.debug('mutool executable not found')
        else:
            _mutool_exec = [mutool]
            # Find MuPDF version; assume 1.6 version since
            # the '-v' switch is only supported from 1.7 onward...
            version = '1.6'
            proc = process.popen([mutool, '-v'],
                                 stdout=process.NULL,
                                 stderr=process.PIPE)
            try:
                output = proc.stderr.read()
                if output.startswith(b'mutool version '):
                    version = output[15:].rstrip().decode()
            finally:
                proc.stderr.close()
                proc.wait()
            version = LegacyVersion(version)
            if version >= LegacyVersion('1.8'):
                # Mutool executable with draw support.
                _mudraw_exec = [mutool, 'draw']
                _mudraw_trace_args = ['-F', 'trace']
                _pdf_possible = True
            else:
                # Separate mudraw executable.
                mudraw = process.find_executable(('mudraw',))
                if mudraw is None:
                    log.debug('mudraw executable not found')
                else:
                    _mudraw_exec = [mudraw]
                    if version >= LegacyVersion('1.7'):
                        _mudraw_trace_args = ['-F', 'trace']
                    else:
                        _mudraw_trace_args = ['-x']
                    _pdf_possible = True
        if _pdf_possible:
            log.info('Using MuPDF version: %s', version)
            log.debug('mutool: %s', ' '.join(_mutool_exec))
            log.debug('mudraw: %s', ' '.join(_mudraw_exec))
            log.debug('mudraw trace arguments: %s', ' '.join(_mudraw_trace_args))
        else:
            log.info('MuPDF not available.')
        return _pdf_possible

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/archive/pdf_multi.py0000644000175000017500000000417314476523373017776 0ustar00moritzmoritz# -*- coding: utf-8 -*-

"""Shim module to conditionally load the FitzArchive class."""

import os
from typing import Type

from mcomix import log
from mcomix.archive.archive_base import BaseArchive, DisabledArchive

from mcomix.version_tools import LegacyVersion

FITZ_VERSION_REQUIRED = "1.19.2"


class DisabledError(RuntimeError): pass

class UnsupportedFitzVersionError(ImportError):
    def __init__(
            self, message=None, name=None, path=None,
            found_version=None):
        self.found_version = found_version
        self.minimum_version = FITZ_VERSION_REQUIRED
        if message is None:
            message = f"PyMuPDF {self.minimum_version} or later required"
            if self.found_version is not None:
                message += f", {self.found_version} found"
        super().__init__(message, name=name, path=path)


class DisabledFitzArchive(DisabledArchive):
    """Subclass of DisabledArchive used when FitzArchive is unavailable.

    This class will masquerade as FitzArchive for purposes of upstream
    reporting, so that the correct class is logged as being unavailable."""

    __name__ = "FitzArchive"


# On import, this code tests for a compatible version of the fitz
# module (PyMuPDF), and exports as "PdfMultiArchive" either the
# FitzArchive class from native_pdf.parent, or the DisabledFitzArchive
# class (aliased to 'FitzArchive' for caller-side reporting purposes)

try:
    if os.environ.get("MCOMIX_DISABLE_PDF_MULTI") is not None:
        raise DisabledError("MCOMIX_DISABLE_PDF_MULTI set in environment")
    import fitz

    fitz_version = LegacyVersion(fitz.VersionFitz)
    required_version = LegacyVersion(FITZ_VERSION_REQUIRED)

    if fitz_version < required_version:
        raise UnsupportedFitzVersionError(found_version=fitz.VersionFitz)
    from mcomix.archive.native_pdf.parent import FitzArchive

    log.info("Native PDF handler loaded, PyMuPDF version %s", fitz.VersionFitz)
    PdfMultiArchive: Type[BaseArchive] = FitzArchive
except (DisabledError, ImportError) as ex:
    log.info("Can't enable pdf_multi: %s", str(ex))
    PdfMultiArchive = DisabledFitzArchive

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/archive/rar.py0000644000175000017500000003313014476523373016572 0ustar00moritzmoritz# -*- coding: utf-8 -*-

""" Glue around libunrar.so/unrar.dll to extract RAR files without having to
resort to calling rar/unrar manually. """

import sys, os
import ctypes, ctypes.util

from mcomix import constants
from mcomix.archive import archive_base
from mcomix import log

if sys.platform == 'win32':
    UNRARCALLBACK = ctypes.WINFUNCTYPE(ctypes.c_longlong, ctypes.c_uint,
        ctypes.c_longlong, ctypes.c_longlong, ctypes.c_longlong)
else:
    UNRARCALLBACK = ctypes.CFUNCTYPE(ctypes.c_longlong, ctypes.c_uint,
        ctypes.c_longlong, ctypes.c_longlong, ctypes.c_longlong)

class RarArchive(archive_base.BaseArchive):
    """ Wrapper class for libunrar. All string values passed to this class must be unicode objects.
    In turn, all values returned are also unicode. """

    # Nope! Not a good idea...
    support_concurrent_extractions = False

    class _OpenMode(object):
        """ Rar open mode """
        RAR_OM_LIST    = 0
        RAR_OM_EXTRACT = 1

    class _ProcessingMode(object):
        """ Rar file processing mode """
        RAR_SKIP       = 0
        RAR_EXTRACT    = 2

    class _ErrorCode(object):
        """ Rar error codes """
        ERAR_END_ARCHIVE = 10
        ERAR_NO_MEMORY = 11
        ERAR_BAD_DATA = 12
        ERAR_BAD_ARCHIVE = 13
        ERAR_UNKNOWN_FORMAT = 14
        ERAR_EOPEN = 15
        ERAR_ECREATE = 16
        ERAR_ECLOSE = 17
        ERAR_EREAD = 18
        ERAR_EWRITE = 19
        ERAR_SMALL_BUF = 20
        ERAR_UNKNOWN = 21
        ERAR_MISSING_PASSWORD = 22

    class _RAROpenArchiveDataEx(ctypes.Structure):
        """ Archive header structure. Used by DLL calls. """
        _pack_ = 1
        _fields_ = [("ArcName", ctypes.c_char_p),
                      ("ArcNameW", ctypes.c_wchar_p),
                      ("OpenMode", ctypes.c_uint),
                      ("OpenResult", ctypes.c_uint),
                      ("CmtBuf", ctypes.c_char_p),
                      ("CmtBufSize", ctypes.c_uint),
                      ("CmtSize", ctypes.c_uint),
                      ("CmtState", ctypes.c_uint),
                      ("Flags", ctypes.c_uint),
                      ("Callback", UNRARCALLBACK),
                      ("UserData", ctypes.c_long),
                      ("Reserved", ctypes.c_uint * 28)]

    class _RARHeaderDataEx(ctypes.Structure):
        """ Archive file structure. Used by DLL calls. """
        _pack_ = 1
        _fields_ = [("ArcName", ctypes.c_char * 1024),
                      ("ArcNameW", ctypes.c_wchar * 1024),
                      ("FileName", ctypes.c_char * 1024),
                      ("FileNameW", ctypes.c_wchar * 1024),
                      ("Flags", ctypes.c_uint),
                      ("PackSize", ctypes.c_uint),
                      ("PackSizeHigh", ctypes.c_uint),
                      ("UnpSize", ctypes.c_uint),
                      ("UnpSizeHigh", ctypes.c_uint),
                      ("HostOS", ctypes.c_uint),
                      ("FileCRC", ctypes.c_uint),
                      ("FileTime", ctypes.c_uint),
                      ("UnpVer", ctypes.c_uint),
                      ("Method", ctypes.c_uint),
                      ("FileAttr", ctypes.c_uint),
                      ("CmtBuf", ctypes.c_char_p),
                      ("CmtBufSize", ctypes.c_uint),
                      ("CmtSize", ctypes.c_uint),
                      ("CmtState", ctypes.c_uint),
                      ("Reserved", ctypes.c_uint * 1024)]


    @staticmethod
    def is_available():
        """ Returns True if unrar.dll can be found, False otherwise. """
        return bool(_get_unrar_dll())

    def __init__(self, archive):
        """ Initialize Unrar.dll. """
        super(RarArchive, self).__init__(archive)
        self._unrar = _get_unrar_dll()
        self._handle = None
        self._callback_function = None
        self._is_solid = False
        # Information about the current file will be stored in this structure
        self._headerdata = RarArchive._RARHeaderDataEx()
        self._current_filename = None

        # Set up function prototypes.
        # Mandatory since pointers get truncated on x64 otherwise!
        self._unrar.RAROpenArchiveEx.restype = ctypes.c_void_p
        self._unrar.RAROpenArchiveEx.argtypes = \
            [ctypes.POINTER(RarArchive._RAROpenArchiveDataEx)]
        self._unrar.RARCloseArchive.restype = ctypes.c_int
        self._unrar.RARCloseArchive.argtypes = \
            [ctypes.c_void_p]
        self._unrar.RARReadHeaderEx.restype = ctypes.c_int
        self._unrar.RARReadHeaderEx.argtypes = \
            [ctypes.c_void_p, ctypes.POINTER(RarArchive._RARHeaderDataEx)]
        self._unrar.RARProcessFileW.restype = ctypes.c_int
        self._unrar.RARProcessFileW.argtypes = \
            [ctypes.c_void_p, ctypes.c_int, ctypes.c_wchar_p, ctypes.c_wchar_p]
        self._unrar.RARSetCallback.argtypes = \
            [ctypes.c_void_p, UNRARCALLBACK, ctypes.c_long]

    def is_solid(self):
        return self._is_solid

    def iter_contents(self):
        """ List archive contents. """
        self._close()
        self._open()
        try:
            while True:
                self._read_header()
                if 0 != (0x10 & self._headerdata.Flags):
                    self._is_solid = True
                filename = self._current_filename
                yield filename
                # Skip to the next entry if we're still on the same name
                # (extract may have been called by iter_extract).
                if filename == self._current_filename:
                    self._process()
        except UnrarException as exc:
            log.error('Error while listing contents: %s', str(exc))
        except EOFError:
            # End of archive reached.
            pass
        finally:
            self._close()

    def extract(self, filename, destination_dir):
        """ Extract  from the archive to . """
        if not self._handle:
            self._open()
        looped = False
        while True:
            # Check if the current entry matches the requested file.
            if self._current_filename is not None:
                if (self._current_filename == filename):
                    # It's the entry we're looking for, extract it.
                    dest = ctypes.c_wchar_p(os.path.join(destination_dir, filename))
                    self._process(dest)
                    break
                # Not the right entry, skip it.
                self._process()
            try:
                self._read_header()
            except EOFError:
                # Archive end was reached, this might be due to out-of-order
                # extraction while the handle was still open.  Close the
                # archive and jump back to archive start and try to extract
                # file again.  Do this only once; if the file isn't found after
                # a second full pass, it probably doesn't even exist in the
                # archive.
                if looped:
                    break
                self._open()
        # After the method returns, the RAR handler is still open and pointing
        # to the next archive file. This will improve extraction speed for sequential file reads.
        # After all files have been extracted, close() should be called to free the handler resources.

    def close(self):
        """ Close the archive handle """
        self._close()

    def _open(self):
        """ Open rar handle for extraction. """
        self._callback_function = UNRARCALLBACK(self._password_callback)
        archivedata = RarArchive._RAROpenArchiveDataEx(ArcNameW=self.archive,
                                                       OpenMode=RarArchive._OpenMode.RAR_OM_EXTRACT,
                                                       Callback=self._callback_function,
                                                       UserData=0)

        handle = self._unrar.RAROpenArchiveEx(ctypes.byref(archivedata))
        if not handle:
            errormessage = UnrarException.get_error_message(archivedata.OpenResult)
            raise UnrarException("Couldn't open archive: %s" % errormessage)
        self._unrar.RARSetCallback(handle, self._callback_function, 0)
        self._handle = handle

    def _check_errorcode(self, errorcode):
        if 0 == errorcode:
            # No error.
            return
        self._close()
        if RarArchive._ErrorCode.ERAR_END_ARCHIVE == errorcode:
            # End of archive reached.
            exc = EOFError()
        else:
            errormessage = UnrarException.get_error_message(errorcode)
            exc = UnrarException(errormessage)
        raise exc

    def _read_header(self):
        self._current_filename = None
        errorcode = self._unrar.RARReadHeaderEx(self._handle, ctypes.byref(self._headerdata))
        self._check_errorcode(errorcode)
        self._current_filename = self._headerdata.FileNameW

    def _process(self, dest=None):
        """ Process current entry: extract or skip it. """
        if dest is None:
            mode = RarArchive._ProcessingMode.RAR_SKIP
        else:
            mode = RarArchive._ProcessingMode.RAR_EXTRACT
        errorcode = self._unrar.RARProcessFileW(self._handle, mode, None, dest)
        self._current_filename = None
        self._check_errorcode(errorcode)

    def _close(self):
        """ Close the rar handle previously obtained by open. """
        if self._handle is None:
            return
        errorcode = self._unrar.RARCloseArchive(self._handle)
        if errorcode != 0:
            errormessage = UnrarException.get_error_message(errorcode)
            raise UnrarException("Couldn't close archive: %s" % errormessage)
        self._handle = None

    def _password_callback(self, msg, userdata, buffer_address, buffer_size):
        """ Called by the unrar library in case of missing password. """
        if msg == 2: # UCM_NEEDPASSWORD
            self._get_password()
            if not self._password or len(self._password) == 0:
                # Abort extraction
                return -1
            password = ctypes.create_string_buffer(self._password.encode('utf-8'))
            copy_size = min(buffer_size, len(password))
            ctypes.memmove(buffer_address, password, copy_size)
            return 1
        elif msg == 4: # UCM_NEEDPASSWORDW
            self._get_password()
            if not self._password or len(self._password) == 0:
                # Abort extraction
                return -1
            password = ctypes.create_string_buffer(self._password.encode('utf-16le'))
            copy_size = min(buffer_size, len(password))
            ctypes.memmove(buffer_address, password, copy_size)
            return 1
        else:
            # Continue operation
            return 0

class UnrarException(Exception):
    """ Exception class for RarArchive. """

    _exceptions = {
        RarArchive._ErrorCode.ERAR_END_ARCHIVE: "End of archive",
        RarArchive._ErrorCode.ERAR_NO_MEMORY:" Not enough memory to initialize data structures",
        RarArchive._ErrorCode.ERAR_BAD_DATA: "Bad data, CRC mismatch",
        RarArchive._ErrorCode.ERAR_BAD_ARCHIVE: "Volume is not valid RAR archive",
        RarArchive._ErrorCode.ERAR_UNKNOWN_FORMAT: "Unknown archive format",
        RarArchive._ErrorCode.ERAR_EOPEN: "Volume open error",
        RarArchive._ErrorCode.ERAR_ECREATE: "File create error",
        RarArchive._ErrorCode.ERAR_ECLOSE: "File close error",
        RarArchive._ErrorCode.ERAR_EREAD: "Read error",
        RarArchive._ErrorCode.ERAR_EWRITE: "Write error",
        RarArchive._ErrorCode.ERAR_SMALL_BUF: "Buffer too small",
        RarArchive._ErrorCode.ERAR_UNKNOWN: "Unknown error",
        RarArchive._ErrorCode.ERAR_MISSING_PASSWORD: "Password missing"
    }

    @staticmethod
    def get_error_message(errorcode):
        if errorcode in UnrarException._exceptions:
            return UnrarException._exceptions[errorcode]
        else:
            return "Unkown error"

# Filled on-demand by _get_unrar_dll
_unrar_dll = -1

def _get_unrar_dll():
    """ Tries to load libunrar and will return a handle of it.
    Returns None if an error occured or the library couldn't be found. """
    global _unrar_dll
    if _unrar_dll != -1:
        return _unrar_dll

    # Load unrar64.dll on win32
    if sys.platform == 'win32':

        # First, search for unrar.dll in PATH
        unrar_path = ctypes.util.find_library("UnRar64.dll")
        if unrar_path:
            try:
                return ctypes.windll.LoadLibrary(unrar_path)
            except WindowsError:
                pass

        # The file wasn't found in PATH, try MComix' root directory
        try:
            return ctypes.windll.LoadLibrary(os.path.join(constants.BASE_PATH, "UnRar64.dll"))
        except WindowsError:
            pass

        # Last attempt, just use the current directory
        try:
            _unrar_dll = ctypes.windll.LoadLibrary("UnRar64.dll")
        except WindowsError:
            _unrar_dll = None

        return _unrar_dll

    # Load libunrar.so on UNIX
    else:
        # find_library on UNIX uses various mechanisms to determine the path
        # of a library, so one could assume the library is not installed
        # when find_library fails
        unrar_path = ctypes.util.find_library("unrar") or \
            '/usr/lib64/libunrar.so'

        if unrar_path:
            try:
                _unrar_dll = ctypes.cdll.LoadLibrary(unrar_path)
                return _unrar_dll
            except OSError:
                pass

        # Last attempt, try the current directory
        try:
            _unrar_dll = ctypes.cdll.LoadLibrary(os.path.join(os.getcwd(), "libunrar.so"))
        except OSError:
            _unrar_dll = None

        return _unrar_dll

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/archive/rar_external.py0000644000175000017500000001566514476523373020511 0ustar00moritzmoritz# -*- coding: utf-8 -*-

""" RAR archive extractor. """

import os
import sys
import subprocess

from mcomix import log
from mcomix import process
from mcomix.archive import archive_base

# Filled on-demand by RarArchive
_rar_executable = -1

class RarArchive(archive_base.ExternalExecutableArchive):
    """ RAR file extractor using the unrar/rar executable. """

    STATE_HEADER, STATE_LISTING = 1, 2

    class EncryptedHeader(Exception):
        pass

    def __init__(self, archive):
        super(RarArchive, self).__init__(archive)
        self._is_solid = False
        self._is_encrypted =  False
        self._contents = []

    def _get_executable(self):
        return self._find_unrar_executable()

    def _get_password_argument(self):
        if not self._is_encrypted:
            # Add a dummy password anyway, to prevent deadlock on reading for
            # input if we did not correctly detect the archive is encrypted.
            return '-p-'
        self._get_password()
        # Check for invalid empty password, see comment above.
        if not self._password:
            return '-p-'
        return '-p' + self._password

    def _get_list_arguments(self):
        args = [self._get_executable(), 'vt']
        args.append(self._get_password_argument())
        args.extend(('--', self.archive))
        return args

    def _get_extract_arguments(self):
        args = [self._get_executable(), 'p', '-inul', '-@']
        args.append(self._get_password_argument())
        args.extend(('--', self.archive))
        return args

    def _parse_list_output_line(self, line):
        if self._state == self.STATE_HEADER:
            if line.startswith('Details: '):
                flags = line[9:].split(', ')
                if 'solid' in flags:
                    self._is_solid = True
                if 'encrypted headers' in flags:
                    if not self._is_encrypted:
                        # Trigger a restart of the enclosing
                        # iter_contents loop with a password.
                        self._is_encrypted = True
                        raise self.EncryptedHeader()
                self._state = self.STATE_LISTING
                return None
        if self._state == self.STATE_LISTING:
            line = line.lstrip()
            if line.startswith('Name: '):
                self._path = line[6:]
                return self._path
            if line.startswith('Size: '):
                filesize = int(line[6:])
                if filesize > 0:
                    self._contents.append((self._path, filesize))
            if line.startswith('Flags: '):
                flags = line[7:].split()
                if 'solid' in flags:
                    self._is_solid = True
                if 'encrypted' in flags:
                    self._is_encrypted = True
        return None

    def is_solid(self):
        return self._is_solid

    def iter_contents(self):
        if not self._get_executable():
            return

        # We'll try at most 2 times:
        # - the first time without a password
        # - a second time with a password if the header is encrypted
        for retry_count in range(2):
            #: Indicates which part of the file listing has been read.
            self._state = self.STATE_HEADER
            #: Current path while listing contents.
            self._path = None
            proc = subprocess.run(
                self._get_list_arguments(), stdout=process.PIPE, stderr=process.STDOUT,
                encoding="utf-8",
                creationflags=process._get_creationflags())
            try:
                for line in proc.stdout.splitlines():
                    filename = self._parse_list_output_line(line.rstrip(os.linesep))
                    if filename is not None:
                        yield self._unicode_filename(filename)
            except self.EncryptedHeader:
                # The header is encrypted, try again
                # if it was our first attempt.
                if 0 == retry_count:
                    continue
            # Last and/or successful attempt.
            break

        self.filenames_initialized = True

    def extract(self, filename, destination_dir):
        """ Extract  from the archive to . """
        assert isinstance(filename, str) and \
                isinstance(destination_dir, str)

        if not self._get_executable():
            return

        if not self.filenames_initialized:
            self.list_contents()

        desired_filename = self._original_filename(filename)
        cmd = self._get_extract_arguments() + [desired_filename]
        output = self._create_file(os.path.join(destination_dir, filename))
        try:
            process.call(cmd, stdout=output)
        finally:
            output.close()

    def iter_extract(self, entries, destination_dir):

        if not self._get_executable():
            return

        if not self.filenames_initialized:
            self.list_contents()

        proc = process.popen(self._get_extract_arguments())
        try:
            wanted = dict([(self._original_filename(unicode_name), unicode_name)
                           for unicode_name in entries])

            for filename, filesize in self._contents:
                data = proc.stdout.read(filesize)
                if filename not in wanted:
                    continue
                unicode_name = wanted.get(filename, None)
                if unicode_name is None:
                    continue
                new = self._create_file(os.path.join(destination_dir, unicode_name))
                new.write(data)
                new.close()
                yield unicode_name
                del wanted[filename]
                if 0 == len(wanted):
                    break

        finally:
            proc.stdout.close()
            proc.wait()

    @staticmethod
    def _find_unrar_executable():
        """ Tries to start rar/unrar, and returns either 'rar' or 'unrar' if
        one of them was started successfully.
        Returns None if neither could be started. """
        global _rar_executable
        if _rar_executable == -1:
            if 'win32' == sys.platform:
                def is_not_unrar_free(exe):
                    return True
            else:
                def is_not_unrar_free(exe):
                    real_exe = exe
                    while os.path.islink(real_exe):
                        real_exe = os.readlink(real_exe)
                    if real_exe.endswith(os.path.sep + 'unrar-free'):
                        log.warning('RAR executable %s is unrar-free, ignoring', exe)
                        return False
                    return True
            _rar_executable = process.find_executable(('unrar-nonfree', 'unrar', 'rar'),
                                                      is_valid_candidate=is_not_unrar_free)
        return _rar_executable

    @staticmethod
    def is_available():
        return bool(RarArchive._find_unrar_executable())

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/archive/sevenzip_external.py0000644000175000017500000002032314476523373021553 0ustar00moritzmoritz# -*- coding: utf-8 -*-

""" 7z archive extractor. """

import re
import os
import subprocess
import tempfile

from mcomix import process
from mcomix import log
from mcomix.archive import archive_base
from mcomix.i18n import _

# Filled on-demand by SevenZipArchive
_7z_executable = -1


class SevenZipArchive(archive_base.ExternalExecutableArchive):
    """ 7z file extractor using the 7z executable. """

    STATE_HEADER, STATE_LISTING, STATE_FOOTER = 1, 2, 3

    class EncryptedHeader(Exception):
        pass

    def __init__(self, archive):
        super(SevenZipArchive, self).__init__(archive)
        self._is_solid = False
        self._is_encrypted = False
        self._contents = []

    def _get_executable(self):
        return SevenZipArchive._find_7z_executable()

    def _get_password_argument(self):
        if self._is_encrypted:
            self._get_password()
            return '-p' + self._password
        else:
            # Add an empty password anyway, to prevent deadlock on reading for
            # input if we did not correctly detect the archive is encrypted.
            return '-p'

    def _get_list_arguments(self):
        args = [self._get_executable(), 'l', '-slt', '-sccUTF-8']
        args.append(self._get_password_argument())
        args.extend(('--', self.archive))
        return args

    def _get_extract_arguments(self, list_file=None):
        args = [self._get_executable(), 'x', '-so', '-sccUTF-8']
        if list_file is not None:
            args.append('-i@' + list_file)
        args.append(self._get_password_argument())
        args.extend(('--', self.archive))
        return args

    def _parse_list_output_line(self, line):
        """ Start parsing after the first delimiter (bunch of - characters),
        and end when delimiters appear again. Format:
        Date  Time  Attr  Size  Compressed  Name"""

        if line.startswith('----------'):
            if self._state == self.STATE_HEADER:
                # First delimiter reached, start reading from next line.
                self._state = self.STATE_LISTING
            elif self._state == self.STATE_LISTING:
                # Last delimiter read, stop reading from now on.
                self._state = self.STATE_FOOTER

            return None

        if self._state == self.STATE_HEADER:
            if re.match(r'^error:.+?can\s?not open encrypted archive\. wrong password\?$', line,
                        re.IGNORECASE):
                self._is_encrypted = True
                raise self.EncryptedHeader()
            if 'Solid = +' == line:
                self._is_solid = True

        if self._state == self.STATE_LISTING:
            if line.startswith('Path = '):
                self._path = line[7:]
                return self._path
            if line.startswith('Size = '):
                filesize = int(line[7:])
                if filesize > 0:
                    self._contents.append((self._path, filesize))
            elif 'Encrypted = +' == line:
                self._is_encrypted = True

        return None

    def is_solid(self):
        return self._is_solid

    def iter_contents(self):
        if not self._get_executable():
            return

        # We'll try at most 2 times:
        # - the first time without a password
        # - a second time with a password if the header is encrypted
        for retry_count in range(2):
            #: Indicates which part of the file listing has been read.
            self._state = self.STATE_HEADER
            #: Current path while listing contents.
            self._path = None
            proc = subprocess.run(self._get_list_arguments(),
                                  stdout=subprocess.PIPE, stderr=process.STDOUT, encoding='utf-8')
            try:
                for line in proc.stdout.splitlines():
                    filename = self._parse_list_output_line(line.rstrip(os.linesep))
                    if filename is not None:
                        yield filename
            except self.EncryptedHeader:
                # The header is encrypted, try again
                # if it was our first attempt.
                if 0 == retry_count:
                    continue
            # Last and/or successful attempt.
            break

        self.filenames_initialized = True

    def extract(self, filename, destination_dir):
        """ Extract  from the archive to . """
        assert isinstance(filename, str) and \
               isinstance(destination_dir, str)

        if not self._get_executable():
            return

        if not self.filenames_initialized:
            self.list_contents()

        tmplistfile = tempfile.NamedTemporaryFile(prefix='mcomix.7z.', delete=False)
        try:
            desired_filename = self._original_filename(filename)
            if isinstance(desired_filename, str):
                desired_filename = desired_filename.encode('utf-8')

            tmplistfile.write(desired_filename + os.linesep.encode('utf-8'))
            tmplistfile.close()

            output = self._create_file(os.path.join(destination_dir, filename))
            try:
                proc = subprocess.run(
                    self._get_extract_arguments(list_file=tmplistfile.name),
                    stdout=output, stderr=subprocess.PIPE,
                    creationflags=process._get_creationflags())

                if len(proc.stderr) > 0:
                    log.error(_("Extraction of %(archivefile)s might have failed: %(error)s"),
                              {'archivefile': filename, 'error': proc.stderr.decode('utf-8')})
            finally:
                output.close()
        finally:
            os.unlink(tmplistfile.name)

    def iter_extract(self, entries, destination_dir):

        if not self._get_executable():
            return

        if not self.filenames_initialized:
            self.list_contents()

        proc = process.popen(self._get_extract_arguments())
        try:
            wanted = set(entries)
            for filename, filesize in self._contents:
                data = proc.stdout.read(filesize)
                if filename not in wanted:
                    continue
                new = self._create_file(os.path.join(destination_dir, filename))
                new.write(data)
                new.close()
                yield filename
                wanted.remove(filename)
                if 0 == len(wanted):
                    break

        finally:
            proc.stdout.close()
            proc.wait()

    @staticmethod
    def _find_7z_executable():
        """ Tries to start 7z, and returns either '7z' if
        it was started successfully or None otherwise. """
        global _7z_executable
        if _7z_executable == -1:
            _7z_executable = process.find_executable(('7z',))
        return _7z_executable

    @staticmethod
    def is_available():
        return bool(SevenZipArchive._find_7z_executable())


class TarArchive(SevenZipArchive):

    '''Special class for handling tar archives.

       Needed because for XZ archives, the technical listing
       does not contain the archive member name...
    '''

    def __init__(self, archive):
        super(TarArchive, self).__init__(archive)
        self._is_solid = True
        self._is_encrypted = False

    def _get_extract_arguments(self, list_file=None):
        # Note: we ignore the list_file argument, which
        # contains our made up archive member name.
        return super(TarArchive, self)._get_extract_arguments()

    def iter_contents(self):
        if not self._get_executable():
            return
        self._state = self.STATE_HEADER
        # We make up a name that's guaranteed to be
        # recognized as an archive by MComix.
        self._path = 'archive.tar'
        proc = process.popen(self._get_list_arguments(), stderr=process.STDOUT)
        try:
            for line in proc.stdout:
                self._parse_list_output_line(line.rstrip(os.linesep))
        finally:
            proc.stdout.close()
            proc.wait()
        if self._contents:
            # The archive should not contain more than 1 member.
            assert 1 == len(self._contents)
            yield self._unicode_filename(self._path)
        self.filenames_initialized = True

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/archive/tar.py0000644000175000017500000000354514476523373016603 0ustar00moritzmoritz# -*- coding: utf-8 -*-

""" Unicode-aware wrapper for tarfile.TarFile. """

import os
import tarfile
from . import archive_base

class TarArchive(archive_base.NonUnicodeArchive):
    def __init__(self, archive):
        super(TarArchive, self).__init__(archive)
        # Track if archive contents have been listed at least one time: this
        # must be done before attempting to extract contents.
        self._contents_listed = False
        self._contents = []
        self.tar = None

    def is_solid(self):
        return True

    def iter_contents(self):
        if self._contents_listed:
            for name in self._contents:
                yield name
            return
        # Make sure we start back at the beginning of the tar.
        self.tar = tarfile.open(self.archive, 'r')
        self._contents = []
        while True:
            info = self.tar.next()
            if info is None:
                break
            name = self._unicode_filename(info.name)
            self._contents.append(name)
            yield name
        self._contents_listed = True

    def list_contents(self):
        return [f for f in self.iter_contents()]

    def extract(self, filename, destination_dir):
        if not self._contents_listed:
            self.list_contents()
        new = self._create_file(os.path.join(destination_dir, filename))
        file_object = self.tar.extractfile(self._original_filename(filename))
        new.write(file_object.read())
        file_object.close()
        new.close()

    def iter_extract(self, entries, destination_dir):
        if not self._contents_listed:
            self.list_contents()
        for f in super(TarArchive, self).iter_extract(entries, destination_dir):
            yield f

    def close(self):
        if self.tar is not None:
            self.tar.close()
            self.tar = None

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/archive/zip.py0000644000175000017500000000454714476523373016622 0ustar00moritzmoritz# -*- coding: utf-8 -*-

""" Unicode-aware wrapper for zipfile.ZipFile. """

import os
import zipfile
from contextlib import closing

from mcomix import log
from mcomix import i18n
from mcomix.archive import archive_base
from mcomix.i18n import _


def is_py_supported_zipfile(path):
    """Check if a given zipfile has all internal files stored with Python supported compression
    """
    # Use contextlib's closing for 2.5 compatibility
    with closing(zipfile.ZipFile(path, 'r')) as zip_file:
        for file_info in zip_file.infolist():
            if file_info.compress_type not in (zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED):
                return False
    return True

class ZipArchive(archive_base.NonUnicodeArchive):
    def __init__(self, archive):
        super(ZipArchive, self).__init__(archive)
        self.zip = zipfile.ZipFile(archive, 'r')

        # Encryption is supported starting with Python 2.6
        self._encryption_supported = hasattr(self.zip, "setpassword")
        self._password = None

    def iter_contents(self):
        if self._encryption_supported and self._has_encryption():
            self._get_password()
            self.zip.setpassword(i18n.to_utf8(self._password))

        for filename in self.zip.namelist():
            yield self._unicode_filename(filename)

    def extract(self, filename, destination_dir):
        new = self._create_file(os.path.join(destination_dir, filename))
        content = self.zip.read(self._original_filename(filename))
        new.write(content)
        new.close()

        zipinfo = self.zip.getinfo(self._original_filename(filename))
        if len(content) != zipinfo.file_size:
            log.warning(_('%(filename)s\'s extracted size is %(actual_size)d bytes,'
                ' but should be %(expected_size)d bytes.'
                ' The archive might be corrupt or in an unsupported format.'),
                { 'filename' : filename, 'actual_size' : len(content),
                  'expected_size' : zipinfo.file_size })



    def close(self):
        self.zip.close()

    def _has_encryption(self):
        """ Checks all files in the archive for encryption.
        Returns True if at least one encrypted file was found. """
        for zipinfo in self.zip.infolist():
            if zipinfo.flag_bits & 0x1: # File is encrypted
                return True

        return False

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/archive/zip_external.py0000644000175000017500000000274214476523373020517 0ustar00moritzmoritz# -*- coding: utf-8 -*-

""" ZIP archive extractor via executable."""

from mcomix import i18n
from mcomix import process
from mcomix.archive import archive_base

# Filled on-demand by ZipArchive
_zip_executable = -1

class ZipArchive(archive_base.ExternalExecutableArchive):
    """ ZIP file extractor using unzip executable. """

    def _get_executable(self):
        return ZipArchive._find_unzip_executable()

    def _get_list_arguments(self):
        return ['-Z1']

    def _get_extract_arguments(self):
        return ['-p', '-P', '']

    @staticmethod
    def _find_unzip_executable():
        """ Tries to run unzip, and returns 'unzip' on success.
        Returns None on failure. """
        global _zip_executable
        if -1 == _zip_executable:
            _zip_executable = process.find_executable(('unzip',))
        return _zip_executable

    @staticmethod
    def is_available():
        return bool(ZipArchive._find_unzip_executable())

    def _unicode_filename(self, filename, conversion_func=i18n.to_unicode):
        unicode_name = conversion_func(filename)
        safe_name = self._replace_invalid_filesystem_chars(unicode_name)
        # As it turns out, unzip will try to interpret filenames as glob...
        for c in '[*?':
            filename = filename.replace(c, '[' + c + ']')
        # Won't work on Windows...
        filename = filename.replace('\\', '\\\\')
        self.unicode_mapping[safe_name] = filename
        return safe_name

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/archive_extractor.py0000644000175000017500000002134314476523373020104 0ustar00moritzmoritz"""archive_extractor.py - Archive extraction class."""


import os
import threading
import traceback

from mcomix import archive_tools
from mcomix import callback
from mcomix import log
from mcomix.preferences import prefs
from mcomix.worker_thread import WorkerThread
from mcomix.i18n import _


class Extractor(object):

    """Extractor is a threaded class for extracting different archive formats.

    The Extractor can be loaded with paths to archives and a path to a
    destination directory. Once an archive has been set and its contents
    listed, it is possible to filter out the files to be extracted and set the
    order in which they should be extracted.  The extraction can then be
    started in a new thread in which files are extracted one by one, and a
    signal is sent on a condition after each extraction, so that it is possible
    for other threads to wait on specific files to be ready.

    Note: Support for gzip/bzip2 compressed tar archives is limited, see
    set_files() for more info.
    """

    def __init__(self):
        self._setupped = False
        self._archive = None

    def setup(self, src, dst, type=None):
        """Setup the extractor with archive  and destination dir .
        Return a threading.Condition related to the is_ready() method, or
        None if the format of  isn't supported.
        """
        self._src = src
        self._dst = dst
        self._files = []
        self._extracted = set()
        self._archive = archive_tools.get_recursive_archive_handler(src, dst, type=type)
        if self._archive is None:
            msg = _('Non-supported archive format: %s') % os.path.basename(src)
            log.warning(msg)
            raise ArchiveException(msg)

        self._contents_listed = False
        self._extract_started = False
        self._condition = threading.Condition()
        self._list_thread = WorkerThread(self._list_contents, name='list')
        self._list_thread.append_order(self._archive)
        self._setupped = True

        return self._condition

    def get_files(self):
        """Return a list of names of all the files the extractor is currently
        set for extracting. After a call to setup() this is by default all
        files found in the archive. The paths in the list are relative to
        the archive root and are not absolute for the files once extracted.
        """
        with self._condition:
            if not self._contents_listed:
                return
            return self._files[:]

    def get_directory(self):
        """Returns the root extraction directory of this extractor."""
        return self._dst

    def set_files(self, files):
        """Set the files that the extractor should extract from the archive in
        the order of extraction. Normally one would get the list of all files
        in the archive using get_files(), then filter and/or permute this
        list before sending it back using set_files().

        Note: Random access on gzip or bzip2 compressed tar archives is
        no good idea. These formats are supported *only* for backwards
        compability. They are fine formats for some purposes, but should
        not be used for scanned comic books. So, we cheat and ignore the
        ordering applied with this method on such archives.
        """
        with self._condition:
            if not self._contents_listed:
                return
            self._files = [f for f in files if f not in self._extracted]
            if not self._files:
                # Nothing to do!
                return
            if self._extract_started:
                self.extract()

    def is_ready(self, name):
        """Return True if the file  in the extractor's file list
        (as set by set_files()) is fully extracted.
        """
        with self._condition:
            return name in self._extracted

    def stop(self):
        """Signal the extractor to stop extracting and kill the extracting
        thread. Blocks until the extracting thread has terminated.
        """
        if self._setupped:
            self._list_thread.stop()
            if self._extract_started:
                self._extract_thread.stop()
                self._extract_started = False
            self.setupped = False

    def extract(self):
        """Start extracting the files in the file list one by one using a
        new thread. Every time a new file is extracted a notify() will be
        signalled on the Condition that was returned by setup().
        """
        with self._condition:
            if not self._contents_listed:
                return
            if not self._extract_started:
                if self._archive.support_concurrent_extractions \
                   and not self._archive.is_solid():
                    max_threads = prefs['max extract threads']
                else:
                    max_threads = 1
                if self._archive.is_solid():
                    fn = self._extract_all_files
                else:
                    fn = self._extract_file
                self._extract_thread = WorkerThread(fn,
                                                    name='extract',
                                                    max_threads=max_threads,
                                                    unique_orders=True)
                self._extract_started = True
            else:
                self._extract_thread.clear_orders()
            if self._archive.is_solid():
                # Sort files so we don't queue the same batch multiple times.
                self._extract_thread.append_order(sorted(self._files))
            else:
                self._extract_thread.extend_orders(self._files)

    @callback.Callback
    def contents_listed(self, extractor, files):
        """ Called after the contents of the archive has been listed. """
        pass

    @callback.Callback
    def file_extracted(self, extractor, filename):
        """ Called whenever a new file is extracted and ready. """
        pass

    def close(self):
        """Close any open file objects, need only be called manually if the
        extract() method isn't called.
        """
        self.stop()
        if self._archive:
            self._archive.close()

    def _extraction_finished(self, name):
        with self._condition:
            self._files.remove(name)
            self._extracted.add(name)
            self._condition.notifyAll()
        self.file_extracted(self, name)

    def _extract_all_files(self, files):

        # With multiple extractions for each pass, some of the files might have
        # already been extracted.
        with self._condition:
            files = list(set(files) - self._extracted)
            files.sort()

        try:
            # log.debug('Extracting from "%s" to "%s": "%s"', self._src, self._dst, '", "'.join(files))
            for f in self._archive.iter_extract(files, self._dst):
                if self._extract_thread.must_stop():
                    return
                self._extraction_finished(f)

        except Exception as ex:
            # Better to ignore any failed extractions (e.g. from a corrupt
            # archive) than to crash here and leave the main thread in a
            # possible infinite block. Damaged or missing files *should* be
            # handled gracefully by the main program anyway.
            log.error(_('! Extraction error: %s'), ex)
            log.debug('Traceback:\n%s', traceback.format_exc())

    def _extract_file(self, name):
        """Extract the file named  to the destination directory,
        mark the file as "ready", then signal a notify() on the Condition
        returned by setup().
        """

        try:
            # log.debug('Extracting from "%s" to "%s": "%s"', self._src, self._dst, name)
            self._archive.extract(name, self._dst)

        except Exception as ex:
            # Better to ignore any failed extractions (e.g. from a corrupt
            # archive) than to crash here and leave the main thread in a
            # possible infinite block. Damaged or missing files *should* be
            # handled gracefully by the main program anyway.
            log.error(_('! Extraction error: %s'), ex)
            log.debug('Traceback:\n%s', traceback.format_exc())

        if self._extract_thread.must_stop():
            return
        self._extraction_finished(name)

    def _list_contents(self, archive):
        files = []
        for f in archive.iter_contents():
            if self._list_thread.must_stop():
                return
            files.append(f)
        with self._condition:
            self._files = files
            self._contents_listed = True
        self.contents_listed(self, files)

class ArchiveException(Exception):
    """ Indicate error during extraction operations. """
    pass

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/archive_packer.py0000644000175000017500000000741614476523373017343 0ustar00moritzmoritz"""archive_packer.py - Archive creation class."""

import os
import zipfile
import threading

from mcomix import log
from mcomix.i18n import _

class Packer(object):

    """Packer is a threaded class for packing files into ZIP archives.

    It would be straight-forward to add support for more archive types,
    but basically all other types are less well fitted for this particular
    task than ZIP archives are (yes, really).
    """

    def __init__(self, image_files, other_files, archive_path, base_name):
        """Setup a Packer object to create a ZIP archive at .
        All files pointed to by paths in the sequences  and
         will be included in the archive when packed.

        The files in  will be renamed on the form
        "NN - .ext", so that the lexical ordering of their
        filenames match that of their order in the list.

        The files in  will be included as they are,
        assuming their filenames does not clash with other filenames in
        the archive. All files are placed in the archive root.
        """
        self._image_files = image_files
        self._other_files = other_files
        self._archive_path = archive_path
        self._base_name = base_name
        self._pack_thread = None
        self._packing_successful = False

    def pack(self):
        """Pack all the files in the file lists into the archive."""
        self._pack_thread = threading.Thread(target=self._thread_pack)
        self._pack_thread.name += '-pack'
        self._pack_thread.setDaemon(False)
        self._pack_thread.start()

    def wait(self):
        """Block until the packer thread has finished. Return True if the
        packer finished its work successfully.
        """
        if self._pack_thread != None:
            self._pack_thread.join()

        return self._packing_successful

    def _thread_pack(self):
        try:
            zfile = zipfile.ZipFile(self._archive_path, 'w')
        except Exception:
            log.error(_('! Could not create archive at path "%s"'),
                      self._archive_path)
            return

        used_names = []
        pattern = '%%0%dd - %s%%s' % (len(str(len(self._image_files))),
            self._base_name)

        for i, path in enumerate(self._image_files):
            filename = pattern % (i + 1, os.path.splitext(path)[1])

            try:
                zfile.write(path, filename, zipfile.ZIP_STORED)
            except Exception:
                log.error(_('! Could not add file %(sourcefile)s '
                            'to archive %(archivefile)s, aborting...'),
                          { "sourcefile" : path,
                            "archivefile" : self._archive_path})

                zfile.close()

                try:
                    os.remove(self._archive_path)
                except:
                    pass

                return

            used_names.append(filename)

        for path in self._other_files:
            filename = os.path.basename(path)

            while filename in used_names:
                filename = '_%s' % filename

            try:
                zfile.write(path, filename, zipfile.ZIP_DEFLATED)
            except Exception:
                log.error(_('! Could not add file %(sourcefile)s '
                            'to archive %(archivefile)s, aborting...'),
                          { "sourcefile" : path,
                            "archivefile" : self._archive_path})

                zfile.close()

                try:
                    os.remove(self._archive_path)
                except:
                    pass

                return

            used_names.append(filename)

        zfile.close()
        self._packing_successful = True

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/archive_tools.py0000644000175000017500000001632214476523373017232 0ustar00moritzmoritz"""archive_tools.py - Archive tool functions."""

import os
import shutil
import zipfile
import tarfile
import tempfile

from mcomix import image_tools
from mcomix import constants
from mcomix import log
from mcomix.archive import (
    lha_external,
    mobi,
    pdf_multi,
    pdf_external,
    rar,
    rar_external,
    sevenzip_external,
    tar,
    zip,
    zip_external,
)
from mcomix import tools
from mcomix.i18n import _

# Handlers for each archive type.
_HANDLERS = {
    constants.ZIP: (
        zip.ZipArchive,
    ),
    # Prefer 7z over zip executable for encryption and Unicode support.
    constants.ZIP_EXTERNAL: (
        sevenzip_external.SevenZipArchive,
        zip_external.ZipArchive
    ),
    constants.TAR: (
        tar.TarArchive,
    ),
    constants.GZIP: (
        tar.TarArchive,
    ),
    constants.BZIP2: (
        tar.TarArchive,
    ),
    constants.XZ: (
        # No LZMA support in Python 2 tarfile module.
        sevenzip_external.TarArchive,
    ),
    constants.RAR: (
        rar.RarArchive,
        rar_external.RarArchive,
        # Last resort: some versions of 7z support RAR.
        sevenzip_external.SevenZipArchive,
    ),
    # Prefer 7z over lha executable for Unicode support.
    constants.LHA: (
        sevenzip_external.SevenZipArchive,
        lha_external.LhaArchive,
    ),
    constants.SEVENZIP: (
        sevenzip_external.SevenZipArchive,
    ),
    constants.PDF: (
        pdf_multi.PdfMultiArchive,
        pdf_external.PdfArchive,
    ),
    constants.MOBI: (
        mobi.MobiArchive,
    ),
}

def _get_handler(archive_type):
    """ Return best archive class for format  """

    for handler in _HANDLERS[archive_type]:
        if not hasattr(handler, 'is_available'):
            return handler
        if handler.is_available():
            return handler
        log.debug("Ignoring unavailable handler %s", handler.__name__)

def _is_available(archive_type):
    """ Return True if a handler supporting the  format is available """
    return _get_handler(archive_type) is not None

def szip_available():
    return _is_available(constants.SEVENZIP)

def rar_available():
    return _is_available(constants.RAR)

def lha_available():
    return _is_available(constants.LHA)

def pdf_available():
    return _is_available(constants.PDF)

def mobi_available():
    return _is_available(constants.MOBI)

def get_supported_formats():
    global _SUPPORTED_ARCHIVE_FORMATS
    if _SUPPORTED_ARCHIVE_FORMATS is None:
        supported_formats = {}
        for name, formats, is_available in (
            ('ZIP', constants.ZIP_FORMATS , True            ),
            ('Tar', constants.TAR_FORMATS , True            ),
            ('RAR', constants.RAR_FORMATS , rar_available() ),
            ('7z' , constants.SZIP_FORMATS, szip_available()),
            ('LHA', constants.LHA_FORMATS , lha_available() ),
            ('PDF', constants.PDF_FORMATS , pdf_available() ),
            ('MobiPocket', constants.MOBI_FORMATS , mobi_available() ),
        ):
            if is_available:
                supported_formats[name] = (set(formats[0]), set(formats[1]))
        _SUPPORTED_ARCHIVE_FORMATS = supported_formats
    return _SUPPORTED_ARCHIVE_FORMATS

_SUPPORTED_ARCHIVE_FORMATS = None
# Set supported archive extensions regexp from list of supported formats.
# Only used internally.
_SUPPORTED_ARCHIVE_REGEX = tools.formats_to_regex(get_supported_formats())
log.debug("_SUPPORTED_ARCHIVE_REGEX='%s'", _SUPPORTED_ARCHIVE_REGEX.pattern)

def is_archive_file(path):
    """Return True if the file at  is a supported archive file.
    """
    return _SUPPORTED_ARCHIVE_REGEX.search(path) is not None

def archive_mime_type(path):
    """Return the archive type of  or None for non-archives."""
    try:

        if os.path.isfile(path):

            if not os.access(path, os.R_OK):
                return None

            if zipfile.is_zipfile(path):
                if zip.is_py_supported_zipfile(path):
                    return constants.ZIP
                else:
                    return constants.ZIP_EXTERNAL

            fd = open(path, 'rb')
            magic = fd.read(5)
            fd.seek(60)
            magic2 = fd.read(8)
            fd.close()

            try:
                istarfile = tarfile.is_tarfile(path)
            except IOError:
                # Tarfile raises an error when accessing certain network shares
                istarfile = False

            if istarfile and os.path.getsize(path) > 0:
                if magic.startswith(b'BZh'):
                    return constants.BZIP2
                elif magic.startswith(b'\037\213'):
                    return constants.GZIP
                else:
                    return constants.TAR

            if magic[0:4] == b'Rar!':
                return constants.RAR

            if magic[0:4] == b'7z\xBC\xAF':
                return constants.SEVENZIP

            # Headers for TAR-XZ and TAR-LZMA that aren't supported by tarfile
            if magic[0:5] == b'\xFD7zXZ' or magic[0:5] == b']\x00\x00\x80\x00':
                return constants.XZ

            if magic[2:4] == b'-l':
                return constants.LHA

            if magic[0:4] == b'%PDF':
                return constants.PDF

            if magic2 == b'BOOKMOBI':
                return constants.MOBI

    except Exception:
        log.warning(_('! Could not read %s'), path)

    return None

def get_archive_info(path):
    """Return a tuple (mime, num_pages, size) with info about the archive
    at , or None if  doesn't point to a supported
    """
    cleanup = []
    try:
        tmpdir = tempfile.mkdtemp(prefix='mcomix_archive_info.')
        cleanup.append(lambda: shutil.rmtree(tmpdir, True))

        mime = archive_mime_type(path)
        archive = get_recursive_archive_handler(path, tmpdir, type=mime)
        if archive is None:
            return None
        cleanup.append(archive.close)

        files = archive.list_contents()
        num_pages = len(list(filter(image_tools.is_image_file, files)))
        size = os.stat(path).st_size

        return (mime, num_pages, size)
    finally:
        for fn in reversed(cleanup):
            fn()

def get_archive_handler(path, mimetype=None):
    """ Returns a fitting extractor handler for the archive passed
    in  (with optional mime type . Returns None if no matching
    extractor was found.
    """
    if mimetype is None:
        mimetype = archive_mime_type(path)
        if mimetype is None:
            return None

    handler = _get_handler(mimetype)
    if handler is None:
        return None

    log.debug('Archive handler %(handler)s for archive "%(archivename)s" was selected.',
              {'handler': handler.__name__, 'archivename': os.path.split(path)[1]})
    return handler(path)

def get_recursive_archive_handler(path, destination_dir, type=None):
    """ Same as  but the handler will transparently handle
    archives within archives.
    """
    archive = get_archive_handler(path, mimetype=type)
    if archive is None:
        return None
    # XXX: Deferred import to avoid circular dependency
    from mcomix.archive import archive_recursive
    return archive_recursive.RecursiveArchive(archive, destination_dir)
 
# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1698566559.0
mcomix-3.1.0/mcomix/bookmark_backend.py0000644000175000017500000001757214517410637017647 0ustar00moritzmoritz"""bookmark_backend.py - Bookmarks handler."""

import os
import pickle
from gi.repository import Gtk
import operator
import datetime
import time

from mcomix import constants
from mcomix import log
from mcomix import bookmark_menu_item
from mcomix import callback
from mcomix import i18n
from mcomix import message_dialog
from mcomix.i18n import _

class __BookmarksStore(object):

    """The _BookmarksStore is a backend for both the bookmarks menu and dialog.
    Changes in the _BookmarksStore are mirrored in both.
    """

    def __init__(self):
        self._initialized = False
        self._window = None
        self._file_handler = None
        self._image_handler = None

        bookmarks, mtime = self.load_bookmarks()

        #: List of bookmarks
        self._bookmarks = bookmarks
        #: Modification date of bookmarks file
        self._bookmarks_mtime = mtime

    def initialize(self, window):
        """ Initializes references to the main window and file/image handlers. """
        if not self._initialized:
            self._window = window
            self._file_handler = window.filehandler
            self._image_handler = window.imagehandler
            self._initialized = True

            # Update already loaded bookmarks with window and file handler information
            for bookmark in self._bookmarks:
                bookmark._window = window
                bookmark._file_handler = window.filehandler

    def add_bookmark_by_values(self, name, path, page, numpages, archive_type, date_added):
        """Create a bookmark and add it to the list."""
        bookmark = bookmark_menu_item._Bookmark(self._window, self._file_handler,
            i18n.to_display_string(name), path, page, numpages, archive_type, date_added)

        self.add_bookmark(bookmark)

    @callback.Callback
    def add_bookmark(self, bookmark):
        """Add the  to the list."""
        self._bookmarks.append(bookmark)
        self.write_bookmarks_file()

    @callback.Callback
    def remove_bookmark(self, bookmark):
        """Remove the  from the list."""
        self._bookmarks.remove(bookmark)
        self.write_bookmarks_file()

    def add_current_to_bookmarks(self):
        """Add the currently viewed page to the list."""
        name = self._image_handler.get_pretty_current_filename()
        path = self._image_handler.get_real_path()
        page = self._image_handler.get_current_page()
        numpages = self._image_handler.get_number_of_pages()
        archive_type = self._file_handler.archive_type
        date_added = datetime.datetime.now()

        same_file_bookmarks = []

        for bookmark in self._bookmarks:
            if bookmark.same_path(path):
                if bookmark.same_page(page):
                    # Do not create identical bookmarks
                    return
                else:
                    same_file_bookmarks.append(bookmark)

        # If the same file was already bookmarked, ask to replace
        # the existing bookmarks before deleting them.
        if len(same_file_bookmarks) > 0:
            response = self.show_replace_bookmark_dialog(same_file_bookmarks, page)

            # Delete old bookmarks
            if response == Gtk.ResponseType.YES:
                for bookmark in same_file_bookmarks:
                    self.remove_bookmark(bookmark)
            # Perform no action
            elif response not in (Gtk.ResponseType.YES, Gtk.ResponseType.NO):
                return

        self.add_bookmark_by_values(name, path, page, numpages,
            archive_type, date_added)

    def clear_bookmarks(self):
        """Remove all bookmarks from the list."""

        while not self.is_empty():
            self.remove_bookmark(self._bookmarks[-1])

    def get_bookmarks(self):
        """Return all the bookmarks in the list."""
        if not self.file_was_modified():
            return self._bookmarks
        else:
            self._bookmarks, self._bookmarks_mtime = self.load_bookmarks()
            return self._bookmarks

    def is_empty(self):
        """Return True if the bookmark list is empty."""
        return len(self._bookmarks) == 0

    def load_bookmarks(self):
        """ Loads persisted bookmarks from a local file.
        @return: Tuple of (bookmarks, file mtime)
        """

        path = constants.BOOKMARK_PICKLE_PATH
        bookmarks = []
        mtime = 0

        if os.path.isfile(path):
            fd = None
            try:
                mtime = int(os.stat(path).st_mtime)
                fd = open(path, 'rb')
                version = pickle.load(fd)
                packs = pickle.load(fd)

                for pack in packs:
                    # Handle old bookmarks without date_added attribute
                    if len(pack) == 5:
                        pack = pack + (datetime.datetime.now(),)

                    bookmark = bookmark_menu_item._Bookmark(self._window,
                            self._file_handler, *pack)
                    bookmarks.append(bookmark)

            except Exception:
                log.error(_('! Could not parse bookmarks file %s'), path)
            finally:
                try:
                    if fd:
                        fd.close()
                except IOError:
                    pass

        return bookmarks, mtime

    def file_was_modified(self):
        """ Checks the bookmark store's mtime to see if it has been modified
        since it was last read. """
        path = constants.BOOKMARK_PICKLE_PATH
        if os.path.isfile(path):
            try:
                mtime = int(os.stat(path).st_mtime)
            except IOError:
                mtime = 0

            if mtime > self._bookmarks_mtime:
                return True
            else:
                return False
        else:
            return True

    def write_bookmarks_file(self):
        """Store relevant bookmark info in the mcomix directory."""

        # Merge changes in case file was modified from within other instances
        if self.file_was_modified():
            new_bookmarks, _ = self.load_bookmarks()
            self._bookmarks = list(set(self._bookmarks + new_bookmarks))

        fd = open(constants.BOOKMARK_PICKLE_PATH, 'wb')
        pickle.dump(constants.VERSION, fd, pickle.HIGHEST_PROTOCOL)

        packs = [bookmark.pack() for bookmark in self._bookmarks]
        pickle.dump(packs, fd, pickle.HIGHEST_PROTOCOL)
        fd.close()

        self._bookmarks_mtime = int(time.time())


    def show_replace_bookmark_dialog(self, old_bookmarks, new_page):
        """ Present a confirmation dialog to replace old bookmarks.
        @return RESPONSE_YES to create replace bookmarks,
            RESPONSE_NO to create a new bookmark, RESPONSE_CANCEL to abort creating
            a new bookmark. """
        dialog = message_dialog.MessageDialog(self._window, Gtk.DialogFlags.MODAL, Gtk.MessageType.INFO)
        dialog.add_buttons(Gtk.STOCK_YES, Gtk.ResponseType.YES,
             Gtk.STOCK_NO, Gtk.ResponseType.NO,
             Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
        dialog.set_default_response(Gtk.ResponseType.YES)
        dialog.set_should_remember_choice('replace-existing-bookmark',
            (Gtk.ResponseType.YES, Gtk.ResponseType.NO))

        pages = list(map(str, sorted(map(operator.attrgetter('_page'), old_bookmarks))))
        dialog.set_text(
            i18n.get_translation().ngettext(
                'Replace existing bookmark on page %s?',
                'Replace existing bookmarks on pages %s?',
                len(pages)
            ) % ", ".join(pages),

            _('The current book already contains marked pages. '
              'Do you want to replace them with a new bookmark on page %d?') % new_page +
              '\n\n' +
            _('Selecting "No" will create a new bookmark without affecting the other bookmarks.'))

        return dialog.run()


# Singleton instance of the bookmarks store.
BookmarksStore = __BookmarksStore()

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/bookmark_dialog.py0000644000175000017500000001616014476523373017515 0ustar00moritzmoritz"""bookmark_dialog.py - Bookmarks dialog handler."""

from gi.repository import Gdk, GdkPixbuf, Gtk, GObject

from mcomix import constants
from mcomix import bookmark_menu_item
from mcomix import tools
from mcomix.i18n import _

class _BookmarksDialog(Gtk.Dialog):

    """_BookmarksDialog lets the user remove or rearrange bookmarks."""

    _SORT_TYPE, _SORT_NAME, _SORT_PAGE, _SORT_ADDED = 100, 101, 102, 103

    def __init__(self, window, bookmarks_store):
        super(_BookmarksDialog, self).__init__(_('Edit Bookmarks'), window, Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (Gtk.STOCK_REMOVE, constants.RESPONSE_REMOVE,
             Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE))

        self._bookmarks_store = bookmarks_store

        self.set_resizable(True)
        self.set_default_response(Gtk.ResponseType.CLOSE)
        # scroll area fill to the edge (TODO window should not really be a dialog)
        self.set_border_width(0)

        scrolled = Gtk.ScrolledWindow()
        scrolled.set_border_width(0)
        scrolled.set_shadow_type(Gtk.ShadowType.IN)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        self.vbox.pack_start(scrolled, True, True, 0)

        self._liststore = Gtk.ListStore(GdkPixbuf.Pixbuf, GObject.TYPE_STRING,
            GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, bookmark_menu_item._Bookmark)

        self._treeview = Gtk.TreeView(self._liststore)
        self._treeview.set_rules_hint(True)
        self._treeview.set_reorderable(True)
        # search by typing first few letters of name
        self._treeview.set_search_column(1)
        self._treeview.set_enable_search(True)
        self._treeview.set_headers_clickable(True)
        self._selection = self._treeview.get_selection()

        scrolled.add(self._treeview)

        cellrenderer_text = Gtk.CellRendererText()
        cellrenderer_pbuf = Gtk.CellRendererPixbuf()

        self._icon_col = Gtk.TreeViewColumn(_('Type'), cellrenderer_pbuf)
        self._name_col = Gtk.TreeViewColumn(_('Name'), cellrenderer_text)
        self._page_col = Gtk.TreeViewColumn(_('Page'), cellrenderer_text)
        self._path_col = Gtk.TreeViewColumn(_('Location'), cellrenderer_text)
        # TRANSLATORS: "Added" as in "Date Added"
        self._date_add_col = Gtk.TreeViewColumn(_('Added'), cellrenderer_text)

        self._treeview.append_column(self._icon_col)
        self._treeview.append_column(self._name_col)
        self._treeview.append_column(self._page_col)
        self._treeview.append_column(self._path_col)
        self._treeview.append_column(self._date_add_col)

        self._icon_col.set_attributes(cellrenderer_pbuf, pixbuf=0)
        self._name_col.set_attributes(cellrenderer_text, text=1)
        self._page_col.set_attributes(cellrenderer_text, text=2)
        self._path_col.set_attributes(cellrenderer_text, text=3)
        self._date_add_col.set_attributes(cellrenderer_text, text=4)
        self._name_col.set_expand(True)

        self._liststore.set_sort_func(_BookmarksDialog._SORT_TYPE,
            self._sort_model, ('_archive_type', '_name', '_page'))
        self._liststore.set_sort_func(_BookmarksDialog._SORT_NAME,
            self._sort_model, ('_name', '_page', '_path'))
        self._liststore.set_sort_func(_BookmarksDialog._SORT_PAGE,
            self._sort_model, ('_page', '_numpages', '_name'))
        self._liststore.set_sort_func(_BookmarksDialog._SORT_ADDED,
            self._sort_model, ('_date_added',))

        self._icon_col.set_sort_column_id(_BookmarksDialog._SORT_TYPE)
        self._name_col.set_sort_column_id(_BookmarksDialog._SORT_NAME)
        self._page_col.set_sort_column_id(_BookmarksDialog._SORT_PAGE)
        self._path_col.set_sort_column_id(3)
        self._date_add_col.set_sort_column_id(_BookmarksDialog._SORT_ADDED)

        self._icon_col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
        self._name_col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
        self._page_col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
        self._path_col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
        self._date_add_col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)

        # FIXME Hide extra columns. Needs UI controls to enable these.
        self._path_col.set_visible(False)

        self.resize(600, 450)

        self.connect('response', self._response)
        self.connect('delete_event', self._close)

        self._treeview.connect('key_press_event', self._key_press_event)
        self._treeview.connect('row_activated', self._bookmark_activated)

        for bookmark in self._bookmarks_store.get_bookmarks():
            self._add_bookmark(bookmark)

        self.show_all()

    def _add_bookmark(self, bookmark):
        """Add the  to the dialog."""
        self._liststore.prepend(bookmark.to_row())

    def _remove_selected(self):
        """Remove the currently selected bookmark from the dialog and from
        the store."""

        treeiter = self._selection.get_selected()[1]

        if treeiter is not None:

            bookmark = self._liststore.get_value(treeiter, 5)
            self._liststore.remove(treeiter)
            self._bookmarks_store.remove_bookmark(bookmark)

    def _bookmark_activated(self, treeview, path, view_column, *args):
        """ Open the activated bookmark. """

        iter = treeview.get_model().get_iter(path)
        bookmark = treeview.get_model().get_value(iter, 5)

        self._close()
        bookmark._load()

    def _sort_model(self, treemodel, iter1, iter2, user_data):
        """ Custom sort function to sort to model entries based on the
        BookmarkMenuItem's fields specified in @C{user_data}. This is a list
        of field names. """
        if iter1 == iter2:
            return 0
        if iter1 is None:
            return 1
        elif iter2 is None:
            return -1

        bookmark1 = treemodel.get_value(iter1, 5)
        bookmark2 = treemodel.get_value(iter2, 5)

        for field in user_data:
            result = tools.cmp(getattr(bookmark1, field),
                               getattr(bookmark2, field))
            if result != 0:
                return result

        # If the loop didn't return, both entries are equal.
        return 0

    def _response(self, dialog, response):

        if response == Gtk.ResponseType.CLOSE:
            self._close()

        elif response == constants.RESPONSE_REMOVE:
            self._remove_selected()

        else:
            self.destroy()

    def _key_press_event(self, dialog, event, *args):

        if event.keyval == Gdk.KEY_Delete:
            self._remove_selected()

    def _close(self, *args):
        """Close the dialog and update the _BookmarksStore with the new
        ordering."""

        ordering = []
        treeiter = self._liststore.get_iter_first()

        while treeiter is not None:
            bookmark = self._liststore.get_value(treeiter, 5)
            ordering.insert(0, bookmark)
            treeiter = self._liststore.iter_next(treeiter)

        for bookmark in ordering:
            self._bookmarks_store.remove_bookmark(bookmark)
            self._bookmarks_store.add_bookmark(bookmark)

        self.destroy()


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/bookmark_menu.py0000644000175000017500000000606114476523373017221 0ustar00moritzmoritz"""bookmark_menu.py - Bookmarks menu."""

from gi.repository import Gtk

from mcomix import bookmark_backend
from mcomix import bookmark_dialog
from mcomix.i18n import _

class BookmarksMenu(Gtk.Menu):

    """BookmarksMenu extends Gtk.Menu with convenience methods relating to
    bookmarks. It contains fixed items for adding bookmarks etc. as well
    as dynamic items corresponding to the current bookmarks.
    """

    def __init__(self, ui, window):
        super(BookmarksMenu, self).__init__()

        self._window = window
        self._bookmarks_store = bookmark_backend.BookmarksStore
        self._bookmarks_store.initialize(window)

        self._actiongroup = Gtk.ActionGroup('mcomix-bookmarks')
        self._actiongroup.add_actions([
            ('add_bookmark', 'mcomix-add-bookmark', _('Add _Bookmark'),
                'D', None, self._add_current_to_bookmarks),
            ('edit_bookmarks', None, _('_Edit Bookmarks...'),
                'B', None, self._edit_bookmarks)])
        
        action = self._actiongroup.get_action('add_bookmark')
        action.set_accel_group(ui.get_accel_group())
        self.add_button = action.create_menu_item()
        self.append(self.add_button)

        action = self._actiongroup.get_action('edit_bookmarks')
        action.set_accel_group(ui.get_accel_group())
        self.edit_button = action.create_menu_item()
        self.append(self.edit_button)

        # Re-create the bookmarks menu if one was added/removed
        self._create_bookmark_menuitems()
        self._bookmarks_store.add_bookmark += lambda bookmark: self._create_bookmark_menuitems()
        self._bookmarks_store.remove_bookmark += lambda bookmark: self._create_bookmark_menuitems()

        self.show_all()

    def _create_bookmark_menuitems(self):
        # Delete all old menu entries
        for item in self.get_children():
            if item not in (self.add_button, self.edit_button):
                self.remove(item)

        bookmarks = self._bookmarks_store.get_bookmarks()

        # Add separator
        if bookmarks:
            separator = Gtk.SeparatorMenuItem()
            separator.show()
            self.append(separator)

        # Add new bookmarks
        for bookmark in bookmarks:
            self.add_bookmark(bookmark)

    def add_bookmark(self, bookmark):
        """Add  to the menu."""
        bookmark = bookmark.clone()
        bookmark.show()
        self.insert(bookmark, 3)

    def _add_current_to_bookmarks(self, *args):
        """Add the current page to the bookmarks list."""
        self._bookmarks_store.add_current_to_bookmarks()

    def _edit_bookmarks(self, *args):
        """Open the bookmarks dialog."""
        bookmark_dialog._BookmarksDialog(self._window, self._bookmarks_store)

    def set_sensitive(self, loaded):
        """Set the sensitivities of menu items as appropriate if 
        represents whether a file is currently loaded in the main program
        or not.
        """
        self._actiongroup.get_action('add_bookmark').set_sensitive(loaded)

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1698566559.0
mcomix-3.1.0/mcomix/bookmark_menu_item.py0000644000175000017500000000634114517410637020232 0ustar00moritzmoritz"""bookmark_menu_item.py - A signle bookmark item."""

from gi.repository import Gtk
from mcomix import i18n

class _Bookmark(Gtk.ImageMenuItem):

    """_Bookmark represents one bookmark. It extends the Gtk.ImageMenuItem
    and is thus put directly in the bookmarks menu.
    """

    def __init__(self, window, file_handler, name, path, page, numpages, archive_type, date_added):

        self._name = name
        self._path = path
        self._page = page
        self._numpages = numpages
        self._window = window
        self._archive_type = archive_type
        self._file_handler = file_handler
        self._date_added = date_added

        super(_Bookmark, self).__init__(str(self), False)

        if self._archive_type is not None:
            im = Gtk.Image.new_from_stock('mcomix-archive', Gtk.IconSize.MENU)

        else:
            im = Gtk.Image.new_from_stock('mcomix-image', Gtk.IconSize.MENU)

        self.set_image(im)
        self.connect('activate', self._load)

    def __str__(self):
        return '%s, (%d / %d)' % (self._name, self._page, self._numpages)

    def _load(self, *args):
        """Open the file and page the bookmark represents."""

        if self._file_handler._base_path != self._path:
            self._file_handler.open_file(self._path, self._page)
        else:
            self._window.set_page(self._page)

            self._window.toolbar.hide()
            self._window.toolbar.show()

    def same_path(self, path):
        """Return True if the bookmark is for the file ."""
        return path == self._path

    def same_page(self, page):
        """Return True if the bookmark is for the same page."""
        return page == self._page

    def to_row(self):
        """Return a tuple corresponding to one row in the _BookmarkDialog's
        ListStore.
        """
        stock = self.get_image().get_stock()
        pixbuf = self.render_icon(*stock)
        page = '%d / %d' % (self._page, self._numpages)
        date = self._date_added.strftime("%x %X")

        return (pixbuf, self._name, page, i18n.to_display_string(self._path),
            date, self)

    def pack(self):
        """Return a tuple suitable for pickling. The bookmark can be fully
        re-created using the values in the tuple.
        """
        return (self._name, self._path, self._page, self._numpages,
            self._archive_type, self._date_added)

    def clone(self):
        """ Creates a copy of the provided Bookmark menu item. This is necessary
        since one bookmark item cannot be anchored in more than one menu. There are,
        however, at least two: The main menu and the popup menu. """
        return _Bookmark(
            self._window,
            self._file_handler,
            self._name,
            self._path,
            self._page,
            self._numpages,
            self._archive_type,
            self._date_added)

    def __eq__(self, other):
        """ Equality comparison for Bookmark items. """
        if isinstance(other, _Bookmark):
            return self._path == other._path and self._page == other._page
        else:
            return False

    def __hash__(self):
        """ Hash for this object. """
        return hash(self._path) | hash(self._page)

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/box.py0000644000175000017500000003244014476523373015160 0ustar00moritzmoritz""" Hyperrectangles. """

from mcomix import tools


class Box(object):

    def __init__(self, size, position=None):
        """ A Box is immutable and always axis-aligned.
        Each component of size should be positive (i.e. non-zero).
        Both position and size must have equal number of dimensions.
        If there is only one argument, it must be the size. In this case, the
        position is set to origin (i.e. all coordinates are 0) by definition.
        @param size: The size of this Box.
        @param position: The position of this Box."""
        if position is None:
            self.position = (0,) * len(size)
        else:
            self.position = tuple(position)
        self.size = tuple(size)
        if len(self.position) != len(self.size):
            raise ValueError("different number of dimensions: " +
                str(len(self.position)) + " != " + str(len(self.size)))


    def __str__(self):
        """ Returns a string representation of this Box. """
        return "{" + str(self.get_position()) + ":" + str(self.get_size()) + "}"


    def __eq__(self, other):
        """ Two Boxes are said to be equal if and only if the number of
        dimensions, the positions and the sizes of the two Boxes are equal,
        respectively. """
        return (self.get_position() == other.get_position()) and \
            (self.get_size() == other.get_size())


    def __len__(self):
        """ Returns the number of dimensions of this Box. """
        return len(self.position)


    def get_size(self):
        """ Returns the size of this Box.
        @return: The size of this Box. """
        return self.size


    def get_position(self):
        """ Returns the position of this Box.
        @return: The position of this Box. """
        return self.position


    def set_position(self, position):
        """ Returns a new Box that has the same size as this Box and the
        specified position.
        @return: A new Box as specified above. """
        return Box(self.get_size(), position)


    def set_size(self, size):
        """ Returns a new Box that has the same position as this Box and the
        specified size.
        @return: A new Box as specified above. """
        return Box(size, self.get_position())


    def distance_point_squared(self, point):
        """ Returns the square of the Euclidean distance between this Box and a
        point. If the point lies within the Box, this Box is said to have a
        distance of zero. Otherwise, the square of the Euclidean distance
        between point and the closest point of the Box is returned.
        @param point: The point of interest.
        @return The distance between the point and the Box as specified above.
        """
        result = 0
        for i in range(len(point)):
            p = point[i]
            bs = self.position[i]
            be = self.size[i] + bs
            if p < bs:
                r = bs - p
            elif p >= be:
                r = p - be + 1
            else:
                continue
            result += r * r
        return result


    def translate(self, delta):
        """ Returns a new Box that has the same size as this Box and a
        translated position as specified by delta.
        @param delta: The distance to the position of this Box.
        @return: A new Box as specified above. """
        return Box(self.get_size(),
            tools.vector_add(self.get_position(), delta))


    def translate_opposite(self, delta):
        """ Returns a new Box that has the same size as this Box and a
        oppositely translated position as specified by delta.
        @param delta: The distance to the position of this Box, with opposite
        direction.
        @return: A new Box as specified above. """
        return Box(self.get_size(),
            tools.vector_sub(self.get_position(), delta))


    @staticmethod
    def closest_boxes(point, boxes, orientation=None):
        """ Returns the indices of the Boxes that are closest to the specified
        point. First, the Euclidean distance between point and the closest point
        of the respective Box is used to determine which of these Boxes are the
        closest ones. If two Boxes have the same distance, the Box that is
        closer to the origin as defined by orientation is said to have a shorter
        distance.
        @param point: The point of interest.
        @param boxes: A list of Boxes.
        @param orientation: The orientation which shows where "forward" points
        to. Either 1 (towards larger values in this dimension when reading) or
        -1 (towards smaller values in this dimension when reading). If
        orientation is set to None, it will be ignored.
        @return The indices of the closest Boxes as specified above. """
        result = []
        mindist = -1
        for i in range(len(boxes)):
            # 0 --> keep
            # 1 --> append
            # 2 --> replace
            keep_append_replace = 0
            b = boxes[i]
            dist = b.distance_point_squared(point)
            if (result == []) or (dist < mindist):
                keep_append_replace = 2
            elif dist == mindist:
                if orientation is not None:
                # Take orientation into account.
                # If result is small, a simple iteration shouldn't be a
                # performance issue.
                    for ri in range(len(result)):
                        c = Box._compare_distance_to_origin(b,
                            boxes[result[ri]], orientation)
                        if c < 0:
                            keep_append_replace = 2
                            break
                        if c == 0:
                            keep_append_replace = 1
                else:
                    keep_append_replace = 1

            if keep_append_replace == 1:
                result.append(i)
            if keep_append_replace == 2:
                mindist = dist
                result = [i]
        return result


    @staticmethod
    def _compare_distance_to_origin(box1, box2, orientation):
        """ Returns an integer that is less than, equal to or greater than zero
        if the distance between box1 and the origin is less than, equal to or
        greater than the distance between box2 and the origin, respectively.
        The origin is implied by orientation.
        @param box1: The first Box.
        @param box2: The second Box.
        @param orientation: The orientation which shows where "forward" points
        to. Either 1 (towards larger values in this dimension when reading) or
        -1 (towards smaller values in this dimension when reading).
        @return An integer as specified above. """
        for i in range(len(orientation)):
            o = orientation[i]
            if o == 0:
                continue
            box1edge = box1.get_position()[i]
            box2edge = box2.get_position()[i]
            if o < 0:
                box1edge = box1.get_size()[i] - box1edge
                box2edge = box2.get_size()[i] - box2edge
            d = box1edge - box2edge
            if d != 0:
                return d
        return 0


    def get_center(self, orientation):
        """ Returns the center of this Box. If the exact value is not equal to
        an integer, the integer that is closer to the origin (as implied by
        orientation) is chosen.
        @orientation: The orientation which shows where "forward" points
        to. Either 1 (towards larger values in this dimension when reading) or
        -1 (towards smaller values in this dimension when reading).
        @return The center of this Box as specified above. """
        result = [0] * len(orientation)
        bp = self.get_position()
        bs = self.get_size()
        for i in range(len(orientation)):
            result[i] = Box._box_to_center_offset_1d(bs[i] - 1,
                orientation[i]) + bp[i]
        return result


    @staticmethod
    def _box_to_center_offset_1d(box_size_delta, orientation):
        if orientation == -1:
            box_size_delta += 1
        return box_size_delta >> 1


    def current_box_index(self, orientation, boxes):
        """ Calculates the index of the Box that is closest to the center of
        this Box.
        @param orientation: The orientation to use.
        @param boxes: The Boxes to examine.
        @return: The index as specified above. """
        return Box.closest_boxes(self.get_center(orientation), boxes,
            orientation)[0]


    @staticmethod
    def align_center(boxes, axis, fix, orientation):
        """ Aligns Boxes so that the center of each Box appears on the same
        line.
        @param axis: the axis to center.
        @param fix: the index of the Box that should not move.
        @param orientation: The orientation to use.
        @return: A list of new Boxes with accordingly translated positions. """
        if len(boxes) == 0:
            return []
        center_box = boxes[fix]
        cs = center_box.get_size()[axis]
        if cs % 2 != 0:
            cs +=1
        cp = center_box.get_position()[axis]
        result = []
        for b in boxes:
            s = b.get_size()
            p = list(b.get_position())
            p[axis] = cp + Box._box_to_center_offset_1d(cs - s[axis],
                orientation)
            result.append(Box(s, p))
        return result


    @staticmethod
    def distribute(boxes, axis, fix, spacing=0):
        """ Ensures that the Boxes do not overlap. For this purpose, the Boxes
        are distributed according to the index of the respective Box.
        @param axis: the axis along which the Boxes are distributed.
        @param fix: the index of the Box that should not move.
        @param spacing: the number of additional pixels between Boxes.
        @return: A new list with new Boxes that are accordingly translated. """
        if len(boxes) == 0:
            return []
        result = [None] * len(boxes)
        initialSum = boxes[fix].get_position()[axis]
        partial_sum = initialSum
        for bi in range(fix, len(boxes)):
            b = boxes[bi]
            s = b.get_size()
            p = list(b.get_position())
            p[axis] = partial_sum
            result[bi] = Box(s, p)
            partial_sum += s[axis] + spacing
        partial_sum = initialSum
        for bi in range(fix - 1, -1, -1):
            b = boxes[bi]
            s = b.get_size()
            p = list(b.get_position())
            partial_sum -= s[axis] + spacing
            p[axis] = partial_sum
            result[bi] = Box(s, p)
        return result


    def wrapper_box(self, viewport_size, orientation):
        """ Returns a Box that covers the same area that is covered by a
        scrollable viewport showing this Box.
        @param viewport_size: The size of the viewport.
        @param orientation: The orientation to use.
        @return: A Box as specified above. """
        size = self.get_size()
        position = self.get_position()
        result_size = [0] * len(size)
        result_position = [0] * len(size)
        for i in range(len(size)):
            c = size[i]
            v = viewport_size[i]
            result_size[i] = max(c, v)
            result_position[i] = Box._box_to_center_offset_1d(c - result_size[i],
                orientation[i]) + position[i]
        return Box(result_size, result_position)


    @staticmethod
    def bounding_box(boxes):
        """ Returns the union of all specified Boxes (that is, the smallest Box
        that contains all specified Boxes).
        @param boxes: The Boxes to calculate the union from.
        @return: A Box as specified above. """
        if len(boxes) == 0:
            return Box((), ())
        mins = [None] * len(boxes[0].get_size())
        maxes = [None] * len(mins)
        for b in boxes:
            s = b.get_size()
            p = b.get_position()
            for i in range(len(mins)):
                if (mins[i] is None) or (p[i] < mins[i]):
                    mins[i] = p[i]
                ps = p[i] + s[i]
                if (maxes[i] is None) or (ps > maxes[i]):
                    maxes[i] = ps
        return Box(tools.vector_sub(maxes, mins), mins)


    @staticmethod
    def intersect(boxA, boxB):
        """ Returns the intersection of the two specified Boxes (that is, the
        largest Box that is contained by the two specified Boxes).
        @param boxA: The first Box to calculate the intersection from.
        @param boxB: The second Box to calculate the intersection from.
        @return: A Box as specified above. """
        # TODO Add tests.
        # TODO What if there is no intersection?
        # TODO What if the intersection is a single point?
        # TODO Make interfaces of intersect and bounding_box consistent with
        # each other, and then rename bounding_box to union.
        aPos = boxA.get_position()
        bPos = boxB.get_position()
        aSize = boxA.get_size()
        bSize = boxB.get_size()
        resPos = [0] * len(aPos)
        resSize = [0] * len(aSize)
        for i in range(len(aPos)):
            ax1 = aPos[i]
            bx1 = bPos[i]
            ax2 = ax1
            ax2 += aSize[i]
            bx2 = bx1
            bx2 += bSize[i]
            if ax1 < bx1:
                ax1 = bx1
            if ax2 > bx2:
                ax2 = bx2
            ax2 -= ax1
            resPos[i] = ax1
            resSize[i] = ax2
        return Box(resSize, resPos)


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/callback.py0000644000175000017500000001060714476523373016125 0ustar00moritzmoritz# -*- coding: utf-8 -*-

import traceback
import weakref
import threading
from gi.repository import GLib

from mcomix import log
from mcomix.i18n import _

class CallbackList(object):
    """ Helper class for implementing callbacks within the main thread.
    Add listeners to method calls with method += callback_function. """

    def __init__(self, obj, function):
        self.__callbacks = []
        self.__object = obj
        self.__function = function

    def __call__(self, *args, **kwargs):
        """ Runs the wrapped function. After the funtion has finished,
        callbacks are run. Code within the function and the callback is
        always executed in the main thread. """

        if threading.currentThread().name == 'MainThread':
            if self.__object:
                # Assume that the Callback object is bound to a class method.
                result = self.__function(self.__object, *args, **kwargs)
            else:
                # Otherwise, the callback should be bound to a normal function.
                result = self.__function(*args, **kwargs)

            self.__run_callbacks(*args, **kwargs)
            return result
        else:
            # Call this method again in the main thread.
            GLib.idle_add(self.__mainthread_call, (args, kwargs))

    def __iadd__(self, function):
        """ Support for 'method += callback_function' syntax. """
        obj, func = self.__get_function(function)

        if (obj, func) not in self.__callbacks:
            self.__callbacks.append((obj, func))

        return self

    def __isub__(self, function):
        """ Support for 'method -= callback_function' syntax. """
        obj, func = self.__get_function(function)

        if (obj, func) in self.__callbacks:
            self.__callbacks.remove((obj, func))

        return self

    def __mainthread_call(self, params):
        """ Helper function to execute code in the main thread.
        This will be called by GLib.idle_add, with  being a tuple
        of (args, kwargs). """

        result = self(*params[0], **params[1])

        # Remove this function from the idle queue
        return 0

    def __run_callbacks(self, *args, **kwargs):
        """ Executes callback functions. """
        for obj_ref, func in self.__callbacks:

            if obj_ref is None:
                # Callback is a normal function
                callback = func
            elif obj_ref() is not None:
                # Callback is a bound method.
                # Recreate it by binding the function to the object.
                callback = func.__get__(obj_ref())
            else:
                # Callback is a bound method, object
                # no longer exists.
                callback = None

            if callback:
                try:
                    callback(*args, **kwargs)
                except Exception as e:
                    log.error(_('! Callback %(function)r failed: %(error)s'),
                              { 'function' : callback, 'error' : e })
                    log.debug('Traceback:\n%s', traceback.format_exc())

    def __callback_deleted(self, obj_ref):
        """ Called whenever one of the callback objects is collected by gc.
        This removes all callback functions registered by the object. """
        self.__callbacks = [callback for callback in self.__callbacks if callback[0] != obj_ref]

    def __get_function(self, func):
        """ If  is a normal function, return (None, func).
        If  is a bound method, return (weakref(obj), func), with 
        being the object  is bound to. This is required since
        weak references do not work on bound methods. """

        if hasattr(func, "im_self") and getattr(func, "im_self") is not None:
            return (weakref.ref(func.__self__, self.__callback_deleted), func.__func__)
        else:
            return (None, func)

class Callback(object):
    """ Decorator class for using the CallbackList helper. """

    def __init__(self, function):
        # This is the function the Callback is decorating.
        self.__function = function

    def __get__(self, obj, cls):
        """ This method makes Callback implement the descriptor interface.
        Enables calling bound methods with the correct  reference.
        Do not ask me why or how this actually works, I simply do not know. """

        return CallbackList(obj, self.__function)

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/clipboard.py0000644000175000017500000000253114476523373016325 0ustar00moritzmoritz"""clipboard.py - Clipboard handler"""

from gi.repository import Gdk, Gtk

from mcomix import image_tools


class Clipboard(object):

    """The Clipboard takes care of all necessary copy-paste functionality
    """

    def __init__(self, window):
        self._clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False))
        self._window = window

    def copy(self, text, pixbuf):
        """ Copies C{text} and C{pixbuf} to clipboard. """
        self._clipboard.set_text(text, len(text))
        self._clipboard.set_image(pixbuf)

    def copy_page(self, *args):
        """ Copies the currently opened page and pixbuf to clipboard. """

        if self._window.filehandler.file_loaded:
            # Get pixbuf for current page
            current_page_pixbufs = self._window.imagehandler.get_pixbufs(
                2 if self._window.displayed_double() else 1) # XXX limited to at most 2 pages

            if len(current_page_pixbufs) == 1:
                pixbuf = current_page_pixbufs[ 0 ]
            else:
                pixbuf = image_tools.combine_pixbufs(
                        current_page_pixbufs[ 0 ],
                        current_page_pixbufs[ 1 ],
                        self._window.is_manga_mode )

            path = self._window.imagehandler.get_path_to_page()
            self.copy(path, pixbuf)

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/comment_dialog.py0000644000175000017500000000671514476523373017357 0ustar00moritzmoritz"""comment.py - Comments dialog."""

import os
from gi.repository import Gtk

from mcomix import i18n
from mcomix.i18n import _

class _CommentsDialog(Gtk.Dialog):

    def __init__(self, window):
        super(_CommentsDialog, self).__init__(_('Comments'), window, 0,
            (Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE))

        self.set_resizable(True)
        self.set_default_response(Gtk.ResponseType.CLOSE)
        self.set_default_size(600, 550)
        self.set_border_width(4)

        tag = Gtk.TextTag()
        tag.set_property('editable', False)
        tag.set_property('editable-set', True)
        tag.set_property('family', 'Monospace')
        tag.set_property('family-set', True)
        tag.set_property('scale', 0.9)
        tag.set_property('scale-set', True)
        tag_table = Gtk.TextTagTable()
        tag_table.add(tag)

        self._tag = tag
        self._tag_table = tag_table
        self._notebook = None
        self._window = window
        self._comments = []

        self._window.filehandler.file_available += self._on_file_available
        self._window.filehandler.file_opened += self._update_comments
        self._window.filehandler.file_closed += self._update_comments
        self._update_comments()
        self.show_all()

    def _on_file_available(self, path_list):
        for path in path_list:
            if path in self._comments:
                self._add_comment(path, self._comments[path])
        self._notebook.show_all()

    def _update_comments(self):

        if self._notebook is not None:
            self._notebook.destroy()
            self._notebook = None

        notebook = Gtk.Notebook()
        notebook.set_scrollable(True)
        notebook.set_border_width(6)
        self.vbox.pack_start(notebook, True, True, 0)
        self._notebook = notebook
        self._comments = {}

        for num in range(1, self._window.filehandler.get_number_of_comments() + 1):
            path = self._window.filehandler.get_comment_name(num)
            if self._window.filehandler.file_is_available(path):
                self._add_comment(path, num)
            else:
                # In case it's not ready yet, bump it's
                # extraction in front of the queue.
                self._window.filehandler._ask_for_files([path])
            self._comments[path] = num

        self._notebook.show_all()

    def _add_comment(self, path, num):

        name = os.path.basename(path)

        page = Gtk.VBox(False)
        page.set_border_width(8)

        scrolled = Gtk.ScrolledWindow()
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        page.pack_start(scrolled, True, True, 0)

        outbox = Gtk.EventBox()
        scrolled.add_with_viewport(outbox)

        inbox = Gtk.EventBox()
        inbox.set_border_width(6)
        outbox.add(inbox)

        text = self._window.filehandler.get_comment_text(num)
        if text is None:
            text = _('Could not read %s') % name

        text_buffer = Gtk.TextBuffer(tag_table=self._tag_table)
        text_buffer.set_text(i18n.to_unicode(text))
        text_buffer.apply_tag(self._tag, *text_buffer.get_bounds())
        text_view = Gtk.TextView(buffer=text_buffer)
        inbox.add(text_view)

        bg_color = text_view.get_default_attributes().pg_bg_color
        outbox.modify_bg(Gtk.StateType.NORMAL, bg_color)
        tab_label = Gtk.Label(label=i18n.to_unicode(name))
        self._notebook.insert_page(page, tab_label, -1)


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1705863745.0
mcomix-3.1.0/mcomix/constants.py0000644000175000017500000000754414553265101016377 0ustar00moritzmoritz# -*- coding: utf-8 -*-
"""constants.py - Miscellaneous constants."""

import enum
import os
import sys

from mcomix import tools

APPNAME = 'MComix'
VERSION = '3.1.0'

HOME_DIR = tools.get_home_directory()
CONFIG_DIR = tools.get_config_directory()
DATA_DIR = tools.get_data_directory()

BASE_PATH = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
THUMBNAIL_PATH = os.path.join(HOME_DIR if sys.platform != 'win32' else DATA_DIR, '.thumbnails/normal')
LIBRARY_DATABASE_PATH = os.path.join(DATA_DIR, 'library.db')
LASTPAGE_DATABASE_PATH = os.path.join(DATA_DIR, 'lastreadpage.db')
LIBRARY_COVERS_PATH = os.path.join(DATA_DIR, 'library_covers')
PREFERENCE_PATH = os.path.join(CONFIG_DIR, 'preferences.conf')
KEYBINDINGS_CONF_PATH = os.path.join(CONFIG_DIR, 'keybindings.conf')

BOOKMARK_PICKLE_PATH = os.path.join(DATA_DIR, 'bookmarks.pickle')
FILEINFO_PICKLE_PATH = os.path.join(DATA_DIR, 'file.pickle')
# Transitional - used if json preferences are (were) absent.
PREFERENCE_PICKLE_PATH = os.path.join(CONFIG_DIR, 'preferences.pickle')


class ZoomMode(enum.IntEnum):
    BEST = 0
    WIDTH = 1
    HEIGHT = 2
    MANUAL = 3
    SIZE = 4


DOUBLE_PAGE_AUTORESIZE_SCALE, DOUBLE_PAGE_AUTORESIZE_SIZE, DOUBLE_PAGE_AUTORESIZE_FIT_SIZE = list(range(3))


class PageAxis(enum.IntEnum):
    WIDTH = 0
    HEIGHT = 1


DISTRIBUTION_AXIS, ALIGNMENT_AXIS = PageAxis.WIDTH, PageAxis.HEIGHT
NORMAL_AXES = (0, 1)
SWAPPED_AXES = (1, 0)
WESTERN_ORIENTATION = (1, 1)
MANGA_ORIENTATION = (-1, 1)
SCROLL_TO_CENTER = -2
SCROLL_TO_START = -3
SCROLL_TO_END = -4
FIRST_INDEX = 0
LAST_INDEX = -1
UNION_INDEX = -2

ANIMATION_DISABLED, ANIMATION_NORMAL = list(range(2))

ZIP, RAR, TAR, GZIP, BZIP2, XZ, PDF, SEVENZIP, LHA, ZIP_EXTERNAL, MOBI = list(range(11))
NORMAL_CURSOR, GRAB_CURSOR, WAIT_CURSOR, NO_CURSOR = list(range(4))
LIBRARY_DRAG_EXTERNAL_ID, LIBRARY_DRAG_BOOK_ID, LIBRARY_DRAG_COLLECTION_ID = list(range(3))
AUTOROTATE_NEVER, AUTOROTATE_WIDTH_90, AUTOROTATE_WIDTH_270, \
    AUTOROTATE_HEIGHT_90, AUTOROTATE_HEIGHT_270 = list(range(5))

RESPONSE_REVERT_TO_DEFAULT = 3
RESPONSE_REMOVE = 4
RESPONSE_IMPORT = 5
RESPONSE_SAVE_AS = 6
RESPONSE_REPLACE = 7
RESPONSE_NEW = 8

# These are bit field values, so only use powers of two.
STATUS_PAGE, STATUS_RESOLUTION, STATUS_PATH, STATUS_FILENAME, STATUS_FILENUMBER, STATUS_FILESIZE = \
    1, 2, 4, 8, 16, 32
SHOW_DOUBLE_AS_ONE_TITLE, SHOW_DOUBLE_AS_ONE_WIDE = 1, 2

MAX_LIBRARY_COVER_SIZE = 500
SORT_NAME, SORT_PATH, SORT_SIZE, SORT_LAST_MODIFIED, SORT_NAME_LITERAL = 1, 2, 3, 4, 5
SORT_DESCENDING, SORT_ASCENDING = 1, 2
SIZE_HUGE, SIZE_LARGE, SIZE_NORMAL, SIZE_SMALL, SIZE_TINY = MAX_LIBRARY_COVER_SIZE, 300, 250, 125, 80
RENDER_SIZE_LIMIT = 100000

ACCEPTED_COMMENT_EXTENSIONS = ['txt', 'nfo', 'xml']

ZIP_FORMATS = (
        ('application/x-zip', 'application/zip', 'application/x-zip-compressed', 'application/vnd.comicbook+zip', 'application/x-cbz'),
        ('zip', 'cbz'))
RAR_FORMATS = (
        ('application/x-rar', 'application/vnd.comicbook-rar', 'application/x-cbr'),
        ('rar', 'cbr'))
TAR_FORMATS = (
        ('application/x-tar', 'application/x-gzip', 'application/x-bzip2', 'application/x-cbt'),
        ('tar', 'gz', 'bz2', 'bzip2', 'cbt'))
SZIP_FORMATS = (
        ('application/x-7z-compressed', 'application/x-cb7'),
        ('7z', 'cb7', 'xz', 'lzma'))
LHA_FORMATS = (
        ('application/x-lzh', 'application/x-lha', 'application/x-lzh-compressed'),
        ('lha', 'lzh'))
PDF_FORMATS = (
        ('application/pdf',),
        ('pdf',))
MOBI_FORMATS = (
        ('application/vnd.amazon.mobi8-ebook',),
        ('azw3',))

IMAGEIO_GDKPIXBUF, IMAGEIO_PIL = list(range(2))

# Default DPI for rendering.
PDF_RENDER_DPI_DEF = 72 * 4
# Maximum DPI for rendering.
PDF_RENDER_DPI_MAX = 72 * 10


class SystemThemeLightness(enum.Enum):
    """ Represents the system theme configuration for light/dark. """
    LIGHT = 0
    DARK = 1
    UNKNOWN = 2

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/cursor_handler.py0000644000175000017500000000503714476523373017404 0ustar00moritzmoritz"""cursor_handler.py - Cursor handler."""

from gi.repository import Gdk, GLib

from mcomix import constants

class CursorHandler(object):

    def __init__(self, window):
        self._window = window
        self._timer_id = None
        self._auto_hide = False
        self._current_cursor = constants.NORMAL_CURSOR

    def set_cursor_type(self, cursor):
        """Set the cursor to type . Supported cursor types are
        available as constants in this module. If  is not one of the
        cursor constants above, it must be a Gdk.Cursor.
        """
        if cursor == constants.NORMAL_CURSOR:
            mode = None
        elif cursor == constants.GRAB_CURSOR:
            mode = Gdk.Cursor.new(Gdk.CursorType.FLEUR)
        elif cursor == constants.WAIT_CURSOR:
            mode = Gdk.Cursor.new(Gdk.CursorType.WATCH)
        elif cursor == constants.NO_CURSOR:
            mode = self._get_hidden_cursor()
        else:
            mode = cursor

        self._window.set_cursor(mode)

        self._current_cursor = cursor

        if self._auto_hide:

            if cursor == constants.NORMAL_CURSOR:
                self._set_hide_timer()
            else:
                self._kill_timer()

    def auto_hide_on(self):
        """Signal that the cursor should auto-hide from now on (e.g. that
        we are entering fullscreen).
        """
        self._auto_hide = True

        if self._current_cursor == constants.NORMAL_CURSOR:
            self._set_hide_timer()

    def auto_hide_off(self):
        """Signal that the cursor should *not* auto-hide from now on."""
        self._auto_hide = False
        self._kill_timer()

        if self._current_cursor == constants.NORMAL_CURSOR:
            self.set_cursor_type(constants.NORMAL_CURSOR)

    def refresh(self):
        """Refresh the current cursor (i.e. display it and set a new timer in
        fullscreen). Used when we move the cursor.
        """
        if self._auto_hide:
            self.set_cursor_type(self._current_cursor)

    def _on_timeout(self):
        mode = self._get_hidden_cursor()
        self._window.set_cursor(mode)
        self._timer_id = None
        return False

    def _set_hide_timer(self):
        self._kill_timer()
        self._timer_id = GLib.timeout_add(2000, self._on_timeout)

    def _kill_timer(self):
        if self._timer_id is not None:
            GLib.source_remove(self._timer_id)
            self._timer_id = None

    def _get_hidden_cursor(self):
        return Gdk.Cursor.new(Gdk.CursorType.BLANK_CURSOR)


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/dialog_handler.py0000644000175000017500000000272714476523373017331 0ustar00moritzmoritz"""dialog_handler.py - Takes care of opening and closing and destroying of simple dialog windows.
   Dialog windows should only be taken care of here if they are windows that need to display
   information and then exit with no added functionality inbetween.
"""

from mcomix import about_dialog
from mcomix import comment_dialog
from mcomix import properties_dialog

dialog_windows = {}
dialog_windows[ 'about-dialog' ] = [None, about_dialog._AboutDialog]
dialog_windows[ 'comments-dialog' ] = [None, comment_dialog._CommentsDialog]
dialog_windows[ 'properties-dialog' ] = [None, properties_dialog._PropertiesDialog]

def open_dialog(action, data):
    """Create and display the given dialog."""

    window, name_of_dialog = data

    _dialog = dialog_windows[ name_of_dialog ]

    # if the dialog window is not created then create the window
    # and connect the _close_dialog action to the dialog window
    if _dialog[0] is None:
        dialog_windows[ name_of_dialog ][0] = _dialog[1](window)
        dialog_windows[ name_of_dialog ][0].connect('response', _close_dialog, name_of_dialog)
    else:
        # if the dialog window already exists bring it to the forefront of the screen
        _dialog[0].present()

def _close_dialog(action, exit_response, name_of_dialog):

    _dialog = dialog_windows[ name_of_dialog ]

    # if the dialog window exists then destroy it
    if _dialog[0] is not None:
        _dialog[0].destroy()
        _dialog[0] = None


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/edit_comment_area.py0000644000175000017500000000742214476523373020031 0ustar00moritzmoritz"""edit_comment_area.py - The area in the editing window that displays comments."""

import os
from gi.repository import Gdk, Gtk
from mcomix import tools
from mcomix.i18n import _

class _CommentArea(Gtk.VBox):

    """The area used for displaying and handling non-image files."""

    def __init__(self, edit_dialog):
        super(_CommentArea, self).__init__()
        self._edit_dialog = edit_dialog

        scrolled = Gtk.ScrolledWindow()
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        self.pack_start(scrolled, True, True, 0)

        info = Gtk.Label(label=_('Please note that the only files that are automatically added to this list are those files in archives that MComix recognizes as comments.'))
        info.set_alignment(0.5, 0.5)
        info.set_line_wrap(True)
        self.pack_start(info, False, False, 10)

        # The ListStore layout is (basename, size, full path).
        self._liststore = Gtk.ListStore(str, str, str)
        self._treeview = Gtk.TreeView(self._liststore)
        self._treeview.set_rules_hint(True)
        self._treeview.connect('button_press_event', self._button_press)
        self._treeview.connect('key_press_event', self._key_press)

        cellrenderer = Gtk.CellRendererText()
        column = Gtk.TreeViewColumn(_('Name'), cellrenderer, text=0)
        column.set_expand(True)
        self._treeview.append_column(column)

        column = Gtk.TreeViewColumn(_('Size'), cellrenderer, text=1)
        self._treeview.append_column(column)
        scrolled.add(self._treeview)

        self._ui_manager = Gtk.UIManager()

        ui_description = """
        
            
                
            
        
        """

        self._ui_manager.add_ui_from_string(ui_description)
        actiongroup = Gtk.ActionGroup('mcomix-edit-archive-comment-area')
        actiongroup.add_actions([
            ('remove', Gtk.STOCK_REMOVE, _('Remove from archive'), None, None,
                self._remove_file)])
        self._ui_manager.insert_action_group(actiongroup, 0)

    def fetch_comments(self):
        """Load all comments in the archive."""

        for num in range(1,
          self._edit_dialog.file_handler.get_number_of_comments() + 1):

            path = self._edit_dialog.file_handler.get_comment_name(num)
            size = tools.format_byte_size(os.stat(path).st_size)
            self._liststore.append([os.path.basename(path), size, path])

    def add_extra_file(self, path):
        """Add an extra imported file (at ) to the list."""
        size = tools.format_byte_size(os.stat(path).st_size)
        self._liststore.append([os.path.basename(path), size, path])

    def get_file_listing(self):
        """Return a list with the full paths to all the files, in order."""
        file_list = []

        for row in self._liststore:
            file_list.append(row[2])

        return file_list

    def _remove_file(self, *args):
        """Remove the currently selected file from the list."""
        iterator = self._treeview.get_selection().get_selected()[1]

        if iterator is not None:
            self._liststore.remove(iterator)

    def _button_press(self, treeview, event):
        """Handle mouse button presses on the area."""
        path = treeview.get_path_at_pos(int(event.x), int(event.y))

        if path is None:
            return

        path = path[0]

        if event.button == 3:
            self._ui_manager.get_widget('/Popup').popup(None, None, None, None,
                                                        event.button, event.time)

    def _key_press(self, iconview, event):
        """Handle key presses on the area."""
        if event.keyval == Gdk.KEY_Delete:
            self._remove_file()

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/edit_dialog.py0000644000175000017500000002022114476523373016626 0ustar00moritzmoritz"""edit_dialog.py - The dialog for the archive editing window."""

import os
import tempfile
from gi.repository import Gdk, GLib, Gtk
import re

from mcomix.preferences import prefs
from mcomix import archive_packer
from mcomix import file_chooser_simple_dialog
from mcomix import image_tools
from mcomix import edit_image_area
from mcomix import edit_comment_area
from mcomix import constants
from mcomix import message_dialog
from mcomix.i18n import _

_dialog = None

class _EditArchiveDialog(Gtk.Dialog):

    """The _EditArchiveDialog lets users edit archives (or directories) by
    reordering images and removing and adding images or comment files. The
    result can be saved as a ZIP archive.
    """

    def __init__(self, window):
        super(_EditArchiveDialog, self).__init__(_('Edit archive'), window, Gtk.DialogFlags.MODAL,
            (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL))

        self._accept_changes_button = self.add_button(Gtk.STOCK_APPLY, Gtk.ResponseType.APPLY)

        self.kill = False # Dialog is killed.
        self.file_handler = window.filehandler
        self._window = window
        self._imported_files = []

        self._save_button = self.add_button(Gtk.STOCK_SAVE_AS, constants.RESPONSE_SAVE_AS)

        self._import_button = self.add_button(_('_Import'), constants.RESPONSE_IMPORT)
        self._import_button.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_ADD,
            Gtk.IconSize.BUTTON))

        self.set_border_width(4)
        self.resize(min(Gdk.Screen.get_default().get_width() - 50, 750),
            min(Gdk.Screen.get_default().get_height() - 50, 600))

        self.connect('response', self._response)

        self._image_area = edit_image_area._ImageArea(self, window)
        self._comment_area = edit_comment_area._CommentArea(self)

        notebook = Gtk.Notebook()
        notebook.set_border_width(6)
        notebook.append_page(self._image_area, Gtk.Label(label=_('Images')))
        notebook.append_page(self._comment_area, Gtk.Label(label=_('Comment files')))
        self.vbox.pack_start(notebook, True, True, 0)

        self.show_all()

        GLib.idle_add(self._load_original_files)

    def _load_original_files(self):
        """Load the original files from the archive or directory into
        the edit dialog.
        """
        self._save_button.set_sensitive(False)
        self._import_button.set_sensitive(False)
        self._window.set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH))
        self._image_area.fetch_images()

        if self.kill: # fetch_images() allows pending events to be handled.
            return False

        self._comment_area.fetch_comments()
        self._window.set_cursor(None)
        self._save_button.set_sensitive(True)
        self._import_button.set_sensitive(True)

        return False

    def _pack_archive(self, archive_path):
        """Create a new archive with the chosen files."""
        self.set_sensitive(False)
        self._window.set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH))

        while Gtk.events_pending():
            Gtk.main_iteration_do(False)

        image_files = self._image_area.get_file_listing()
        comment_files = self._comment_area.get_file_listing()

        try:
            fd, tmp_path = tempfile.mkstemp(
                suffix='.%s' % os.path.basename(archive_path),
                prefix='tmp.', dir=os.path.dirname(archive_path))
            # Close open tempfile handle (writing is handled by the packer)
            os.close(fd)
            fail = False

        except:
            fail = True

        if not fail:
            packer = archive_packer.Packer(image_files, comment_files, tmp_path,
                os.path.splitext(os.path.basename(archive_path))[0])
            packer.pack()
            packing_success = packer.wait()

            if packing_success:
                # Preserve permissions if currently edited files come from an archive
                if (self._window.filehandler.archive_type is not None and
                    os.path.exists(self._window.filehandler.get_path_to_base())):
                    mode = os.stat(self._window.filehandler.get_path_to_base()).st_mode
                else:
                    mode = os.stat(tmp_path).st_mode

                # Remove existing file (Win32 fails on rename otherwise)
                if os.path.exists(archive_path):
                    os.unlink(archive_path)

                os.rename(tmp_path, archive_path)
                os.chmod(archive_path, mode)

                _close_dialog()
            else:
                fail = True
        
        self._window.set_cursor(None)
        if fail:
            dialog = message_dialog.MessageDialog(self._window, 0, Gtk.MessageType.ERROR,
                Gtk.ButtonsType.CLOSE)
            dialog.set_text(
                _("The new archive could not be saved!"),
                _("The original files have not been removed."))
            dialog.run()

            self.set_sensitive(True)

    def _response(self, dialog, response):

        if response == constants.RESPONSE_SAVE_AS:

            dialog = file_chooser_simple_dialog.SimpleFileChooserDialog(
                Gtk.FileChooserAction.SAVE)

            src_path = self.file_handler.get_path_to_base()

            dialog.set_current_directory(os.path.dirname(src_path))
            dialog.set_save_name('%s.cbz' % os.path.splitext(
                os.path.basename(src_path))[0])
            dialog.filechooser.set_extra_widget(Gtk.Label(label=
                _('Archives are stored as ZIP files.')))
            dialog.add_archive_filters()
            dialog.run()

            paths = dialog.get_paths()
            dialog.destroy()

            if paths:
                self._pack_archive(paths[0])

        elif response == constants.RESPONSE_IMPORT:

            dialog = file_chooser_simple_dialog.SimpleFileChooserDialog()
            dialog.add_image_filters()
            dialog.run()
            paths = dialog.get_paths()
            dialog.destroy()

            exts = '|'.join(prefs['comment extensions'])
            comment_re = re.compile(r'\.(%s)\s*$' % exts, re.I)

            for path in paths:

                if image_tools.is_image_file(path):
                    self._imported_files.append( path )
                    self._image_area.add_extra_image(path)

                elif os.path.isfile(path):

                    if comment_re.search( path ):
                        self._imported_files.append( path )
                        self._comment_area.add_extra_file(path)

        elif response == Gtk.ResponseType.APPLY:

            old_image_array = self._window.imagehandler._image_files

            treeiter = self._image_area._liststore.get_iter_first()

            new_image_array = []

            while treeiter is not None:
                path = self._image_area._liststore.get_value(treeiter, 2)
                new_image_array.append(path)
                treeiter = self._image_area._liststore.iter_next(treeiter)

            new_positions = []

            end_index = len(old_image_array) - 1

            for image_path in old_image_array:

                try:
                    new_position = new_image_array.index( image_path )
                    new_positions.append(new_position)
                except ValueError:
                    # the path was not found in the new array so that means it was deleted
                    new_positions.append(end_index)
                    end_index -= 1

            self._window.imagehandler._image_files = new_image_array
            self._window.imagehandler._raw_pixbufs = {}
            self._window.imagehandler.do_cacheing()
            self._window.thumbnailsidebar.clear()
            self._window.set_page(1)
            self._window.thumbnailsidebar.load_thumbnails()

        else:
            _close_dialog()
            self.kill = True

    def destroy(self):
        self._image_area.cleanup()
        Gtk.Dialog.destroy(self)

def open_dialog(action, window):
    global _dialog

    if _dialog is None:
        _dialog = _EditArchiveDialog(window)
    else:
        _dialog.present()


def _close_dialog(*args):
    global _dialog

    if _dialog is not None:
        _dialog.destroy()
        _dialog = None

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/edit_image_area.py0000644000175000017500000001340114476523373017443 0ustar00moritzmoritz"""edit_image_area.py - The area of the editing archive window that displays images."""

import os
from gi.repository import Gdk, GdkPixbuf, Gtk

from mcomix import image_tools
from mcomix import i18n
from mcomix import thumbnail_tools
from mcomix import thumbnail_view
from mcomix.preferences import prefs
from mcomix.i18n import _

class _ImageArea(Gtk.ScrolledWindow):

    """The area used for displaying and handling image files."""

    def __init__(self, edit_dialog, window):
        super(_ImageArea, self).__init__()

        self._window = window
        self._edit_dialog = edit_dialog
        self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)

        # The ListStore layout is (thumbnail, basename, full path, thumbnail status).
        # Basename is used as image tooltip.
        self._liststore = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str, bool)
        self._iconview = thumbnail_view.ThumbnailIconView(
            self._liststore,
            2, # UID
            0, # pixbuf
            3, # status
        )
        self._iconview.generate_thumbnail = self._generate_thumbnail
        self._iconview.set_tooltip_column(1)
        self._iconview.set_reorderable(True)
        self._iconview.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
        self._iconview.connect('button_press_event', self._button_press)
        self._iconview.connect('key_press_event', self._key_press)
        self._iconview.connect_after('drag_begin', self._drag_begin)
        self.add(self._iconview)

        self._thumbnail_size = 128
        self._thumbnailer = thumbnail_tools.Thumbnailer(store_on_disk=False,
                                                        size=(self._thumbnail_size,
                                                              self._thumbnail_size))

        self._filler = GdkPixbuf.Pixbuf.new(colorspace=GdkPixbuf.Colorspace.RGB,
                                            has_alpha=True, bits_per_sample=8,
                                            width=self._thumbnail_size,
                                            height=self._thumbnail_size)
        # Make the pixbuf transparent.
        self._filler.fill(0)

        self._window.imagehandler.page_available += self._on_page_available

        self._ui_manager = Gtk.UIManager()
        ui_description = """
        
            
                
            
        
        """

        self._ui_manager.add_ui_from_string(ui_description)

        actiongroup = Gtk.ActionGroup('mcomix-edit-archive-image-area')
        actiongroup.add_actions([
            ('remove', Gtk.STOCK_REMOVE, _('Remove from archive'), None, None,
                self._remove_pages)])
        self._ui_manager.insert_action_group(actiongroup, 0)

    def fetch_images(self):
        """Load all the images in the archive or directory."""
        for page in range(1, self._window.imagehandler.get_number_of_pages() + 1):
            path = self._window.imagehandler.get_path_to_page(page)
            encoded_path = i18n.to_unicode(os.path.basename(path))
            encoded_path = encoded_path.replace('&', '&')
            self._liststore.append([self._filler, encoded_path, path, False])

    def _generate_thumbnail(self, uid):
        assert isinstance(uid, str)
        path = uid
        try:
            if not self._window.filehandler.file_is_available(path):
                return None
        except KeyError:
            # Not a page from the current archive, ignore.
            pass
        pixbuf = self._thumbnailer.thumbnail(path)
        if pixbuf is None:
            pixbuf = image_tools.MISSING_IMAGE_ICON
        return pixbuf

    def add_extra_image(self, path):
        """Add an imported image (at ) to the end of the image list."""
        self._liststore.append([self._filler, os.path.basename(path), path, False])

    def get_file_listing(self):
        """Return a list with the full paths to all the images, in order."""
        return [row[2] for row in self._liststore]

    def _remove_pages(self, *args):
        """Remove the currently selected pages from the list."""
        paths = self._iconview.get_selected_items()

        for path in paths:
            iterator = self._liststore.get_iter(path)
            self._liststore.remove(iterator)

    def _button_press(self, iconview, event):
        """Handle mouse button presses on the thumbnail area."""
        path = iconview.get_path_at_pos(int(event.x), int(event.y))

        if path is None:
            return

        if event.button == 3:

            if not iconview.path_is_selected(path):
                iconview.unselect_all()
                iconview.select_path(path)

            self._ui_manager.get_widget('/Popup').popup(None, None, None, None,
                                                        event.button, event.time)

    def _key_press(self, iconview, event):
        """Handle key presses on the thumbnail area."""
        if event.keyval == Gdk.KEY_Delete:
            self._remove_pages()

    def _drag_begin(self, iconview, context):
        """We hook up on drag_begin events so that we can set the hotspot
        for the cursor at the top left corner of the thumbnail (so that we
        might actually see where we are dropping!).
        """
        path = iconview.get_cursor()[0]
        surface = treeview.create_row_drag_icon(path)
        width, height = surface.get_width(), surface.get_height()
        pixbuf = Gdk.pixbuf_get_from_surface(surface, 0, 0, width, height)
        Gtk.drag_set_icon_pixbuf(context, pixbuf, -5, -5)

    def cleanup(self):
        self._iconview.stop_update()

    def _on_page_available(self, page):
        """ Called whenever a new page is ready for display. """
        self._iconview.draw_thumbnails_on_screen()

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704114443.0
mcomix-3.1.0/mcomix/enhance_backend.py0000644000175000017500000000332214544534413017426 0ustar00moritzmoritz"""enhance_backend.py - Image enhancement handler and dialog (e.g. contrast,
brightness etc.)
"""
from gi.repository import GLib

from mcomix.preferences import prefs
from mcomix import image_tools
from mcomix.library import main_dialog

class ImageEnhancer(object):

    """The ImageEnhancer keeps track of the "enhancement" values and performs
    these enhancements on pixbufs. Changes to the ImageEnhancer's values
    can be made using an _EnhanceImageDialog.
    """

    def __init__(self, window):
        self._window = window
        self.brightness = prefs['brightness']
        self.contrast = prefs['contrast']
        self.saturation = prefs['saturation']
        self.sharpness = prefs['sharpness']
        self.autocontrast = prefs['auto contrast']
        self.invert_color = prefs['invert color']

    def enhance(self, pixbuf):
        """Return an "enhanced" version of ."""

        if (self.brightness != 1.0 or self.contrast != 1.0 or
          self.saturation != 1.0 or self.sharpness != 1.0 or
          self.autocontrast or self.invert_color):

            return image_tools.enhance(pixbuf, self.brightness, self.contrast,
                self.saturation, self.sharpness, self.autocontrast,
                self.invert_color)

        return pixbuf

    def signal_update(self):
        """Signal to the main window that a change in the enhancement
        values has been made.
        """
        self._window.draw_image()

        self._window.thumbnailsidebar.clear()
        GLib.idle_add(self._window.thumbnailsidebar.load_thumbnails)

        if main_dialog._dialog is not None:
            main_dialog._dialog.book_area.load_covers()

        self._window.update_icon(False)

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704114982.0
mcomix-3.1.0/mcomix/enhance_dialog.py0000644000175000017500000001700614544535446017311 0ustar00moritzmoritz"""enhance_dialog.py - Image enhancement dialog."""

from gi.repository import Gtk
from . import histogram

from mcomix.preferences import prefs
from mcomix import image_tools
from mcomix.i18n import _

_dialog = None

class _EnhanceImageDialog(Gtk.Dialog):

    """A Gtk.Dialog which allows modification of the values belonging to
    an ImageEnhancer.
    """

    def __init__(self, window):
        super(_EnhanceImageDialog, self).__init__(_('Enhance image'), window, 0)

        self._window = window

        reset = Gtk.Button(stock=Gtk.STOCK_REVERT_TO_SAVED)
        reset.set_tooltip_text(_('Reset to defaults.'))
        self.add_action_widget(reset, Gtk.ResponseType.REJECT)
        save = Gtk.Button(stock=Gtk.STOCK_SAVE)
        save.set_tooltip_text(_('Save the selected values as default for future files.'))
        self.add_action_widget(save, Gtk.ResponseType.APPLY)
        self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)

        self.set_resizable(False)
        self.connect('response', self._response)
        self.set_default_response(Gtk.ResponseType.OK)

        self._enhancer = window.enhancer
        self._block = False

        vbox = Gtk.VBox(False, 10)
        self.set_border_width(4)
        vbox.set_border_width(6)
        self.vbox.add(vbox)

        self._hist_image = Gtk.Image()
        self._hist_image.set_size_request(262, 170)
        vbox.pack_start(self._hist_image, True, True, 0)
        vbox.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), True, True, 0)

        hbox = Gtk.HBox(False, 4)
        vbox.pack_start(hbox, False, False, 2)
        vbox_left = Gtk.VBox(False, 4)
        vbox_right = Gtk.VBox(False, 4)
        hbox.pack_start(vbox_left, False, False, 2)
        hbox.pack_start(vbox_right, True, True, 2)

        def _create_scale(label_text):
            label = Gtk.Label(label=label_text)
            label.set_alignment(1, 0.5)
            label.set_use_underline(True)
            vbox_left.pack_start(label, True, False, 2)
            adj = Gtk.Adjustment(0.0, -1.0, 1.0, 0.01, 0.1)
            scale = Gtk.HScale.new(adj)
            scale.set_digits(2)
            scale.set_value_pos(Gtk.PositionType.RIGHT)
            scale.connect('value-changed', self._change_values)
            # FIXME
            # scale.set_update_policy(Gtk.UPDATE_DELAYED)
            label.set_mnemonic_widget(scale)
            vbox_right.pack_start(scale, True, False, 2)
            return scale

        self._brightness_scale = _create_scale(_('_Brightness:'))
        self._contrast_scale = _create_scale(_('_Contrast:'))
        self._saturation_scale = _create_scale(_('S_aturation:'))
        self._sharpness_scale = _create_scale(_('S_harpness:'))

        vbox.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), True, True, 0)

        self._autocontrast_button = \
            Gtk.CheckButton.new_with_mnemonic(_('_Automatically adjust contrast'))
        self._autocontrast_button.set_tooltip_text(
            _('Automatically adjust contrast (both lightness and darkness), separately for each colour band.'))
        vbox.pack_start(self._autocontrast_button, False, False, 2)
        self._autocontrast_button.connect('toggled', self._change_values)

        self._invert_color_button = \
            Gtk.CheckButton.new_with_mnemonic(_('_Invert image colors'))
        self._invert_color_button.set_tooltip_text(
            _('Invert (negate) image colors.'))
        vbox.pack_start(self._invert_color_button, False, False, 2)
        self._invert_color_button.connect('toggled', self._change_values)

        self._block = True
        self._brightness_scale.set_value(self._enhancer.brightness - 1)
        self._contrast_scale.set_value(self._enhancer.contrast - 1)
        self._saturation_scale.set_value(self._enhancer.saturation - 1)
        self._sharpness_scale.set_value(self._enhancer.sharpness - 1)
        self._autocontrast_button.set_active(self._enhancer.autocontrast)
        self._invert_color_button.set_active(self._enhancer.invert_color)
        self._block = False
        self._contrast_scale.set_sensitive(
            not self._autocontrast_button.get_active())

        self._window.imagehandler.page_available += self._on_page_available
        self._window.filehandler.file_closed += self._on_book_close
        self._window.page_changed += self._on_page_change
        self._on_page_change()

        self.show_all()

    def _on_book_close(self):
        self.clear_histogram()

    def _on_page_change(self):
        if not self._window.imagehandler.page_is_available():
            self.clear_histogram()
            return
        # XXX transitional(double page limitation)
        pixbuf = self._window.imagehandler.get_pixbufs(1)[0]
        self.draw_histogram(pixbuf)

    def _on_page_available(self, page_number):
        current_page_number = self._window.imagehandler.get_current_page()
        if current_page_number == page_number:
            self._on_page_change()

    def draw_histogram(self, pixbuf):
        """Draw a histogram representing  in the dialog."""
        pixbuf = image_tools.static_image(pixbuf)
        histogram_pixbuf = histogram.draw_histogram(pixbuf, text=False)
        self._hist_image.set_from_pixbuf(histogram_pixbuf)

    def clear_histogram(self):
        """Clear the histogram in the dialog."""
        self._hist_image.clear()

    def _change_values(self, *args):
        if self._block:
            return

        self._enhancer.brightness = self._brightness_scale.get_value() + 1
        self._enhancer.contrast = self._contrast_scale.get_value() + 1
        self._enhancer.saturation = self._saturation_scale.get_value() + 1
        self._enhancer.sharpness = self._sharpness_scale.get_value() + 1
        self._enhancer.autocontrast = self._autocontrast_button.get_active()
        self._contrast_scale.set_sensitive(
            not self._autocontrast_button.get_active())
        self._enhancer.invert_color = self._invert_color_button.get_active()
        self._enhancer.signal_update()

    def _response(self, dialog, response):

        if response in [Gtk.ResponseType.OK, Gtk.ResponseType.DELETE_EVENT]:
            _close_dialog()

        elif response == Gtk.ResponseType.APPLY:
            self._change_values(self)
            prefs['brightness'] = self._enhancer.brightness
            prefs['contrast'] = self._enhancer.contrast
            prefs['saturation'] = self._enhancer.saturation
            prefs['sharpness'] = self._enhancer.sharpness
            prefs['auto contrast'] = self._enhancer.autocontrast
            prefs['invert color'] = self._enhancer.invert_color

        elif response == Gtk.ResponseType.REJECT:
            self._block = True
            self._brightness_scale.set_value(prefs['brightness'] - 1.0)
            self._contrast_scale.set_value(prefs['contrast'] - 1.0)
            self._saturation_scale.set_value(prefs['saturation'] - 1.0)
            self._sharpness_scale.set_value(prefs['sharpness'] - 1.0)
            self._autocontrast_button.set_active(prefs['auto contrast'])
            self._invert_color_button.set_active(prefs['invert color'])
            self._block = False
            self._change_values(self)


def open_dialog(action, window):
    """Create and display the (singleton) image enhancement dialog."""
    global _dialog

    if _dialog is None:
        _dialog = _EnhanceImageDialog(window)
    else:
        _dialog.present()

def _close_dialog(*args):
    """Destroy the image enhancement dialog."""
    global _dialog

    if _dialog is not None:
        _dialog.destroy()
        _dialog = None


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704114443.0
mcomix-3.1.0/mcomix/event.py0000644000175000017500000007472114544534413015512 0ustar00moritzmoritz"""event.py - Event handling (keyboard, mouse, etc.) for the main window.
"""

import urllib.request, urllib.parse, urllib.error
from gi.repository import Gdk, Gtk

from mcomix.preferences import prefs
from mcomix import constants
from mcomix import portability
from mcomix import keybindings
from mcomix import openwith


class EventHandler(object):

    def __init__(self, window):
        self._window = window

        self._last_pointer_pos_x = 0
        self._last_pointer_pos_y = 0
        self._pressed_pointer_pos_x = 0
        self._pressed_pointer_pos_y = 0

        #: For scrolling "off the page".
        self._extra_scroll_events = 0
        #: If True, increment _extra_scroll_events before switchting pages
        self._scroll_protection = False

    def resize_event(self, widget, event):
        """Handle events from resizing and moving the main window."""
        size = (event.width, event.height)
        if size != self._window.previous_size:
            self._window.previous_size = size
            self._window.draw_image()

    def window_state_event(self, widget, event: Gdk.EventWindowState):
        is_fullscreen = self._window.is_fullscreen
        if self._window.was_fullscreen != is_fullscreen:
            # Fullscreen state changed.
            self._window.was_fullscreen = is_fullscreen
            # Re-enable control, now that transition is complete.
            toggleaction = self._window.actiongroup.get_action('fullscreen')
            toggleaction.set_sensitive(True)
            if is_fullscreen:
                redraw = True
            else:
                # Only redraw if we don't need to restore geometry.
                redraw = not self._window.restore_window_geometry()
            self._window._update_toggles_sensitivity()
            if redraw:
                self._window.previous_size = self._window.get_size()
                self._window.draw_image()


    def register_key_events(self):
        """ Registers keyboard events and their default binings, and hooks
        them up with their respective callback functions. """

        manager = keybindings.keybinding_manager(self._window)

        # Navigation keys
        manager.register('previous_page',
            ['Page_Up', 'KP_Page_Up', 'BackSpace'],
            self._flip_page, kwargs={'number_of_pages': -1})
        manager.register('next_page',
            ['Page_Down', 'KP_Page_Down'],
            self._flip_page, kwargs={'number_of_pages': 1})
        manager.register('previous_page_singlestep',
            ['Page_Up', 'KP_Page_Up', 'BackSpace'],
            self._flip_page, kwargs={'number_of_pages': -1, 'single_step': True})
        manager.register('next_page_singlestep',
            ['Page_Down', 'KP_Page_Down'],
            self._flip_page, kwargs={'number_of_pages': 1, 'single_step': True})
        manager.register('previous_page_dynamic',
            ['Left'],
            self._left_right_page_progress, kwargs={'number_of_pages': -1})
        manager.register('next_page_dynamic',
            ['Right'],
            self._left_right_page_progress, kwargs={'number_of_pages': 1})

        manager.register('previous_page_ff',
            ['Page_Up', 'KP_Page_Up', 'BackSpace', 'Left'],
            self._flip_page, kwargs={'number_of_pages': -10})
        manager.register('next_page_ff',
            ['Page_Down', 'KP_Page_Down', 'Right'],
            self._flip_page, kwargs={'number_of_pages': 10})


        manager.register('first_page',
            ['Home', 'KP_Home'],
            self._window.first_page)
        manager.register('last_page',
            ['End', 'KP_End'],
            self._window.last_page)
        manager.register('go_to',
            ['G'],
            self._window.page_select)

        # Numpad (without numlock) aligns the image depending on the key.
        manager.register('scroll_left_bottom',
            ['KP_1'],
            self._window.scroll_to_predefined,
            kwargs={'destination': (-1, 1), 'index': constants.UNION_INDEX})
        manager.register('scroll_middle_bottom',
            ['KP_2'],
            self._window.scroll_to_predefined,
            kwargs={'destination': (constants.SCROLL_TO_CENTER, 1),
                'index': constants.UNION_INDEX})
        manager.register('scroll_right_bottom',
            ['KP_3'],
            self._window.scroll_to_predefined,
            kwargs={'destination': (1, 1), 'index': constants.UNION_INDEX})

        manager.register('scroll_left_middle',
            ['KP_4'],
            self._window.scroll_to_predefined,
            kwargs={'destination': (-1, constants.SCROLL_TO_CENTER),
                'index': constants.UNION_INDEX})
        manager.register('scroll_middle',
            ['KP_5'],
            self._window.scroll_to_predefined,
            kwargs={'destination': (constants.SCROLL_TO_CENTER,
                constants.SCROLL_TO_CENTER), 'index': constants.UNION_INDEX})
        manager.register('scroll_right_middle',
            ['KP_6'],
            self._window.scroll_to_predefined,
            kwargs={'destination': (1, constants.SCROLL_TO_CENTER),
                'index': constants.UNION_INDEX})

        manager.register('scroll_left_top',
            ['KP_7'],
            self._window.scroll_to_predefined,
            kwargs={'destination': (-1, -1), 'index': constants.UNION_INDEX})
        manager.register('scroll_middle_top',
            ['KP_8'],
            self._window.scroll_to_predefined,
            kwargs={'destination': (constants.SCROLL_TO_CENTER, -1),
                'index': constants.UNION_INDEX})
        manager.register('scroll_right_top',
            ['KP_9'],
            self._window.scroll_to_predefined,
            kwargs={'destination': (1, -1), 'index': constants.UNION_INDEX})

        # Enter/exit fullscreen.
        manager.register('exit_fullscreen',
            ['Escape'],
            self.escape_event)

        # View modes
        manager.register('double_page',
            ['d'],
            self._window.actiongroup.get_action('double_page').activate)


        manager.register('best_fit_mode',
            ['b'],
            self._window.actiongroup.get_action('best_fit_mode').activate)

        manager.register('fit_width_mode',
            ['w'],
            self._window.actiongroup.get_action('fit_width_mode').activate)

        manager.register('fit_height_mode',
            ['h'],
            self._window.actiongroup.get_action('fit_height_mode').activate)

        manager.register('fit_size_mode',
            ['s'],
            self._window.actiongroup.get_action('fit_size_mode').activate)

        manager.register('fit_manual_mode',
            ['a'],
            self._window.actiongroup.get_action('fit_manual_mode').activate)


        manager.register('manga_mode',
            ['m'],
            self._window.actiongroup.get_action('manga_mode').activate)

        manager.register('invert_scroll',
            ['x'],
            self._window.actiongroup.get_action('invert_scroll').activate)

        manager.register('keep_transformation',
            ['k'],
            self._window.actiongroup.get_action('keep_transformation').activate)

        manager.register('lens',
            ['l'],
            self._window.actiongroup.get_action('lens').activate)

        manager.register('stretch',
            ['y'],
            self._window.actiongroup.get_action('stretch').activate)

        # Zooming commands for manual zoom mode
        manager.register('zoom_in',
            ['plus', 'KP_Add', 'equal'],
            self._window.actiongroup.get_action('zoom_in').activate)
        manager.register('zoom_out',
            ['minus', 'KP_Subtract'],
            self._window.actiongroup.get_action('zoom_out').activate)
        # Zoom out is already defined as GTK menu hotkey
        manager.register('zoom_original',
            ['0', 'KP_0'],
            self._window.actiongroup.get_action('zoom_original').activate)

        manager.register('rotate_90',
            ['r'],
            self._window.rotate_90)

        manager.register('rotate_270',
            ['r'],
            self._window.rotate_270)

        manager.register('rotate_180',
            [],
            self._window.rotate_180)

        manager.register('flip_horiz',
            [],
            self._window.flip_horizontally)

        manager.register('flip_vert',
            [],
            self._window.flip_vertically)

        manager.register('no_autorotation',
            [],
            self._window.actiongroup.get_action('no_autorotation').activate)

        manager.register('rotate_90_width',
            [],
            self._window.actiongroup.get_action('rotate_90_width').activate)
        manager.register('rotate_270_width',
            [],
            self._window.actiongroup.get_action('rotate_270_width').activate)

        manager.register('rotate_90_height',
            [],
            self._window.actiongroup.get_action('rotate_90_height').activate)

        manager.register('rotate_270_height',
            [],
            self._window.actiongroup.get_action('rotate_270_height').activate)

        # Arrow keys scroll the image
        manager.register('scroll_down',
            ['Down', 'KP_Down'],
            self._scroll_down)
        manager.register('scroll_up',
            ['Up', 'KP_Up'],
            self._scroll_up)
        manager.register('scroll_right',
            ['Right', 'KP_Right'],
            self._scroll_right)
        manager.register('scroll_left',
            ['Left', 'KP_Left'],
            self._scroll_left)

        # File operations
        manager.register('close',
            ['W'],
            self._window.filehandler.close_file)

        manager.register('quit',
            ['Q'],
            self._window.close_program)

        manager.register('save_and_quit',
            ['q'],
            self._window.save_and_terminate_program)

        manager.register('delete',
            ['Delete'],
            self._window.delete)

        manager.register('extract_page',
            ['s'],
            self._window.extract_page)

        manager.register('refresh_archive',
            ['R'],
            self._window.filehandler.refresh_file)

        manager.register('next_archive',
            ['N'],
            self._window.filehandler._open_next_archive)

        manager.register('previous_archive',
            ['P'],
            self._window.filehandler._open_previous_archive)

        manager.register('next_directory',
            ['N'],
            self._window.filehandler.open_next_directory)

        manager.register('previous_directory',
            ['P'],
            self._window.filehandler.open_previous_directory)

        manager.register('comments',
            ['c'],
            self._window.actiongroup.get_action('comments').activate)

        manager.register('properties',
            ['Return'],
            self._window.actiongroup.get_action('properties').activate)

        manager.register('preferences',
            ['F12'],
            self._window.actiongroup.get_action('preferences').activate)

        manager.register('edit_archive',
            [],
            self._window.actiongroup.get_action('edit_archive').activate)

        manager.register('open',
            ['O'],
            self._window.actiongroup.get_action('open').activate)

        manager.register('enhance_image',
            ['e'],
            self._window.actiongroup.get_action('enhance_image').activate)

        manager.register('library',
            ['L'],
            self._window.actiongroup.get_action('library').activate)

        manager.register('invert_color',
            ['I'],
            self._window.actiongroup.get_action('invert_color').activate)

        # Space key scrolls down a percentage of the window height or the
        # image height at a time. When at the bottom it flips to the next
        # page.
        #
        # It also has a "smart scrolling mode" in which we try to follow
        # the flow of the comic.
        #
        # If Shift is pressed we should backtrack instead.
        manager.register('smart_scroll_down',
            ['space'],
            self._smart_scroll_down)
        manager.register('smart_scroll_up',
            ['space'],
            self._smart_scroll_up)

        # User interface
        manager.register('osd_panel',
            ['Tab'],
            self._window.show_info_panel)

        manager.register('minimize',
            ['n'],
            self._window.minimize)

        manager.register('fullscreen',
            ['f', 'F11'],
            self._window.actiongroup.get_action('fullscreen').activate)

        manager.register('toolbar',
            [],
            self._window.actiongroup.get_action('toolbar').activate)

        manager.register('menubar',
            ['M'],
            self._window.actiongroup.get_action('menubar').activate)

        manager.register('statusbar',
            [],
            self._window.actiongroup.get_action('statusbar').activate)

        manager.register('scrollbar',
            [],
            self._window.actiongroup.get_action('scrollbar').activate)

        manager.register('thumbnails',
            ['F9'],
            self._window.actiongroup.get_action('thumbnails').activate)


        manager.register('hide_all',
            ['i'],
            self._window.actiongroup.get_action('hide_all').activate)

        manager.register('slideshow',
            ['S'],
            self._window.actiongroup.get_action('slideshow').activate)

        # Execute external command. Bind keys from 1 to 9 to commands 1 to 9.
        for i in range(1, 10):
            manager.register('execute_command_%d' % i, ['%d' % i],
                             self._execute_command, args=[i - 1])

    def key_press_event(self, widget, event, *args):
        """Handle key press events on the main window."""

        # This is set on demand by callback functions
        self._scroll_protection = False

        # Dispatch keyboard input handling
        manager = keybindings.keybinding_manager(self._window)
        # Some keys can only be pressed with certain modifiers that
        # are irrelevant to the actual hotkey. Find out and ignore them.
        ALL_ACCELS_MASK = (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK |
                           Gdk.ModifierType.MOD1_MASK)

        keymap = Gdk.Keymap.get_default()
        code = keymap.translate_keyboard_state(
                event.hardware_keycode, event.get_state(), event.group)

        if code[0]:
            keyval = code[1]
            # 'consumed' is the modifier that was necessary to type the key
            consumed = code[4]

            if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
                # If the resulting key is upper case (i.e. SHIFT + key),
                # convert it to lower case and remove SHIFT from the consumed flags
                # to match how keys are registered ( + lowercase)
                if keyval != Gdk.keyval_to_lower(keyval):
                    keyval = Gdk.keyval_to_lower(keyval)
                    consumed &= ~Gdk.ModifierType.SHIFT_MASK
                # If lower/upper case conversion with SHIFT is not applicable to the key pressed,
                # i.e. Space and other special keys, remove SHIFT from the consumed mask.
                if Gdk.keyval_to_upper(keyval) == Gdk.keyval_to_lower(keyval):
                    consumed &= ~Gdk.ModifierType.SHIFT_MASK

            manager.execute((keyval, event.get_state() & ~consumed & ALL_ACCELS_MASK))

        # ---------------------------------------------------------------
        # Register CTRL for scrolling only one page instead of two
        # pages in double page mode. This is mainly for mouse scrolling.
        # ---------------------------------------------------------------
        if event.keyval in (Gdk.KEY_Control_L, Gdk.KEY_Control_R):
            self._window.imagehandler.force_single_step = True

        # ----------------------------------------------------------------
        # We kill the signals here for the Up, Down, Space and Enter keys,
        # or they will start fiddling with the thumbnail selector (bad).
        # ----------------------------------------------------------------
        if (event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down,
          Gdk.KEY_space, Gdk.KEY_KP_Enter, Gdk.KEY_KP_Up,
          Gdk.KEY_KP_Down, Gdk.KEY_KP_Home, Gdk.KEY_KP_End,
          Gdk.KEY_KP_Page_Up, Gdk.KEY_KP_Page_Down) or
          (event.keyval == Gdk.KEY_Return and not
          'GDK_MOD1_MASK' in event.get_state().value_names)):

            self._window.emit_stop_by_name('key_press_event')
            return True

    def key_release_event(self, widget, event, *args):
        """ Handle release of keys for the main window. """

        # ---------------------------------------------------------------
        # Unregister CTRL for scrolling only one page in double page mode
        # ---------------------------------------------------------------
        if event.keyval in (Gdk.KEY_Control_L, Gdk.KEY_Control_R):
            self._window.imagehandler.force_single_step = False

    def escape_event(self):
        """ Determines the behavior of the ESC key. """
        if prefs['escape quits']:
            self._window.close_program()
        else:
            self._window.actiongroup.get_action('fullscreen').set_active(False)

    def scroll_wheel_event(self, widget, event, *args):
        """Handle scroll wheel events on the main layout area. The scroll
        wheel flips pages in best fit mode and scrolls the scrollbars
        otherwise.
        """
        if event.get_state() & Gdk.ModifierType.BUTTON2_MASK:
            return

        has_direction, direction = event.get_scroll_direction()
        if not has_direction:
            direction = None
            has_delta, delta_x, delta_y = event.get_scroll_deltas()
            if has_delta:
                if delta_y < 0:
                    direction = Gdk.ScrollDirection.UP
                elif delta_y > 0:
                    direction = Gdk.ScrollDirection.DOWN
                elif delta_x < 0:
                    direction = Gdk.ScrollDirection.LEFT
                elif delta_x > 0:
                    direction = Gdk.ScrollDirection.RIGHT

        self._scroll_protection = True

        if direction == Gdk.ScrollDirection.UP:
            if event.get_state() & Gdk.ModifierType.CONTROL_MASK:
                self._window.manual_zoom_in()
            elif prefs['smart scroll']:
                self._smart_scroll_up(prefs['number of pixels to scroll per mouse wheel event'])
            else:
                self._scroll_with_flipping(0, -prefs['number of pixels to scroll per mouse wheel event'])

        elif direction == Gdk.ScrollDirection.DOWN:
            if event.get_state() & Gdk.ModifierType.CONTROL_MASK:
                self._window.manual_zoom_out()
            elif prefs['smart scroll']:
                self._smart_scroll_down(prefs['number of pixels to scroll per mouse wheel event'])
            else:
                self._scroll_with_flipping(0, prefs['number of pixels to scroll per mouse wheel event'])

        elif direction == Gdk.ScrollDirection.RIGHT:
            if not self._window.is_manga_mode:
                self._window.flip_page(+1)
            else:
                self._previous_page_with_protection()

        elif direction == Gdk.ScrollDirection.LEFT:
            if not self._window.is_manga_mode:
                self._previous_page_with_protection()
            else:
                self._window.flip_page(+1)

    def mouse_press_event(self, widget, event):
        """Handle mouse click events on the main layout area."""

        if self._window.was_out_of_focus:
            return

        if event.button == 1:
            self._pressed_pointer_pos_x = event.x_root
            self._pressed_pointer_pos_y = event.y_root
            self._last_pointer_pos_x = event.x_root
            self._last_pointer_pos_y = event.y_root

        elif event.button == 2:
            self._window.actiongroup.get_action('lens').set_active(True)

        elif (event.button == 3 and
              not event.get_state() & Gdk.ModifierType.MOD1_MASK and
              not event.get_state() & Gdk.ModifierType.SHIFT_MASK):
            self._window.cursor_handler.set_cursor_type(constants.NORMAL_CURSOR)
            self._window.popup.popup(None, None, None, None,
                                     event.button, event.time)

        elif event.button == 4:
            self._window.show_info_panel()

    def mouse_release_event(self, widget, event):
        """Handle mouse button release events on the main layout area."""

        self._window.cursor_handler.set_cursor_type(constants.NORMAL_CURSOR)

        if (event.button == 1):

            if event.x_root == self._pressed_pointer_pos_x and \
                event.y_root == self._pressed_pointer_pos_y and \
                not self._window.was_out_of_focus:

                if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
                    self._flip_page(10)
                else:
                    self._flip_page(1)

            else:
                self._window.was_out_of_focus = False

        elif event.button == 2:
            self._window.actiongroup.get_action('lens').set_active(False)

        elif event.button == 3:
            if event.get_state() & Gdk.ModifierType.MOD1_MASK:
                self._flip_page(-1)
            elif event.get_state() & Gdk.ModifierType.SHIFT_MASK:
                self._flip_page(-10)

    def mouse_move_event(self, widget, event):
        """Handle mouse pointer movement events."""

        event = _get_latest_event_of_same_type(event)

        if 'GDK_BUTTON1_MASK' in event.get_state().value_names:
            self._window.cursor_handler.set_cursor_type(constants.GRAB_CURSOR)
            scrolled = self._window.scroll(self._last_pointer_pos_x - event.x_root,
                                           self._last_pointer_pos_y - event.y_root)

            # Cursor wrapping stuff. See:
            # https://sourceforge.net/tracker/?func=detail&aid=2988441&group_id=146377&atid=764987
            if prefs['wrap mouse scroll'] and scrolled:
                # FIXME: Problems with multi-screen setups
                screen = self._window.get_screen()
                warp_x0 = warp_y0 = 0
                warp_x1 = screen.get_width()
                warp_y1 = screen.get_height()

                new_x = _valwarp(event.x_root, warp_x1, minval=warp_x0)
                new_y = _valwarp(event.y_root, warp_y1, minval=warp_y0)
                if (new_x != event.x_root) or (new_y != event.y_root):
                    display = screen.get_display()
                    display.warp_pointer(screen, int(new_x), int(new_y))
                    ## This might be (or might not be) necessary to avoid
                    ## doing one warp multiple times.
                    event = _get_latest_event_of_same_type(event)

                self._last_pointer_pos_x = new_x
                self._last_pointer_pos_y = new_y
            else:
                self._last_pointer_pos_x = event.x_root
                self._last_pointer_pos_y = event.y_root
            self._drag_timer = event.time

    def drag_n_drop_event(self, widget, context, x, y, selection, drag_id,
      eventtime):
        """Handle drag-n-drop events on the main layout area."""
        # The drag source is inside MComix itself, so we ignore.

        if (Gtk.drag_get_source_widget(context) is not None):
            return

        uris = selection.get_uris()

        if not uris:
            return

        # Normalize URIs
        uris = [portability.normalize_uri(uri) for uri in uris]
        paths = [urllib.request.url2pathname(uri) for uri in uris]

        if len(paths) > 1:
            self._window.filehandler.open_file(paths)
        else:
            self._window.filehandler.open_file(paths[0])

    def _scroll_with_flipping(self, x, y):
        """Handle scrolling with the scroll wheel or the arrow keys, for which
        the pages might be flipped depending on the preferences.  Returns True
        if able to scroll without flipping and False if a new page was flipped
        to.
        """

        self._scroll_protection = True

        if self._window.scroll(x, y):
            self._extra_scroll_events = 0
            return True

        if y > 0 or (self._window.is_manga_mode and x < 0) or (
          not self._window.is_manga_mode and x > 0):
            page_flipped = self._next_page_with_protection()
        else:
            page_flipped = self._previous_page_with_protection()

        return not page_flipped

    def _scroll_down(self):
        """ Scrolls down. """
        self._scroll_with_flipping(0, prefs['number of pixels to scroll per key event'])

    def _scroll_up(self):
        """ Scrolls up. """
        self._scroll_with_flipping(0, -prefs['number of pixels to scroll per key event'])

    def _scroll_right(self):
        """ Scrolls right. """
        self._scroll_with_flipping(prefs['number of pixels to scroll per key event'], 0)

    def _scroll_left(self):
        """ Scrolls left. """
        self._scroll_with_flipping(-prefs['number of pixels to scroll per key event'], 0)

    def _smart_scroll_down(self, small_step=None):
        """ Smart scrolling. """
        self._smart_scrolling(small_step, False)

    def _smart_scroll_up(self, small_step=None):
        """ Reversed smart scrolling. """
        self._smart_scrolling(small_step, True)

    def _smart_scrolling(self, small_step, backwards):
        # Collect data from the environment
        viewport_size = self._window.get_visible_area_size()
        distance = prefs['smart scroll percentage']
        if small_step is None:
            max_scroll = [distance * viewport_size[0],
                distance * viewport_size[1]] # 2D only
        else:
            max_scroll = [small_step] * 2 # 2D only
        swap_axes = constants.SWAPPED_AXES if prefs['invert smart scroll'] \
            else constants.NORMAL_AXES
        self._window.update_layout_position()

        # Scroll to the new position
        new_index = self._window.layout.scroll_smartly(max_scroll, backwards, swap_axes)
        n = 2 if self._window.displayed_double() else 1 # XXX limited to at most 2 pages

        if new_index == -1:
            self._previous_page_with_protection()
        elif new_index == n:
            self._next_page_with_protection()
        else:
            # Update actual viewport
            self._window.update_viewport_position()


    def _next_page_with_protection(self):
        """ Advances to the next page. If L{_scroll_protection} is enabled,
        this method will only advance if enough scrolling attempts have been made.

        @return: True when the page was flipped."""

        if not prefs['flip with wheel']:
            self._extra_scroll_events = 0
            return False

        if (not self._scroll_protection
            or self._extra_scroll_events >= prefs['number of key presses before page turn'] - 1
            or not self._window.is_scrollable()):

            self._flip_page(1)
            return True

        elif (self._scroll_protection):
            self._extra_scroll_events = max(1, self._extra_scroll_events + 1)
            return False

        else:
            # This path should not be reached.
            assert False, "Programmer is moron, incorrect assertion."

    def _previous_page_with_protection(self):
        """ Goes back to the previous page. If L{_scroll_protection} is enabled,
        this method will only go back if enough scrolling attempts have been made.

        @return: True when the page was flipped."""

        if not prefs['flip with wheel']:
            self._extra_scroll_events = 0
            return False

        if (not self._scroll_protection
            or self._extra_scroll_events <= -prefs['number of key presses before page turn'] + 1
            or not self._window.is_scrollable()):

            self._flip_page(-1)
            return True

        elif (self._scroll_protection):
            self._extra_scroll_events = min(-1, self._extra_scroll_events - 1)
            return False

        else:
            # This path should not be reached.
            assert False, "Programmer is moron, incorrect assertion."


    def _flip_page(self, number_of_pages, single_step=False):
        """ Switches a number of pages forwards/backwards. If C{single_step} is True,
        the page count will be advanced by only one page even in double page mode. """
        self._extra_scroll_events = 0
        self._window.flip_page(number_of_pages, single_step=single_step)

    def _left_right_page_progress(self, number_of_pages=1):
        """ If number_of_pages is positive, this function advances the specified
        number of pages in manga mode and goes back the same number of pages in
        normal mode. The opposite happens for number_of_pages being negative. """
        self._flip_page(-number_of_pages if self._window.is_manga_mode else number_of_pages)

    def _execute_command(self, cmdindex):
        """ Execute an external command. cmdindex should be an integer from 0 to 9,
        representing the command that should be executed. """
        manager = openwith.OpenWithManager()
        commands = [cmd for cmd in manager.get_commands() if not cmd.is_separator()]
        if len(commands) > cmdindex:
            commands[cmdindex].execute(self._window)


def _get_latest_event_of_same_type(event):
    """Return the latest event in the event queue that is of the same type
    as , or  itself if no such events are in the queue. All
    events of that type will be removed from the event queue.
    """
    return event
    events = []

    while Gdk.events_pending():
        queued_event = Gdk.event_get()

        if queued_event is not None:

            if queued_event.type == event.type:
                event = queued_event
            else:
                events.append(queued_event)

    for queued_event in events:
        queued_event.put()

    return event


def _valwarp(cur, maxval, minval=0, tolerance=3, extra=2):
    """ Helper function for warping the cursor around the screen when it
      comes within `tolerance` to a border (and `extra` more to avoid
      jumping back and forth).  """
    if cur < minval + tolerance:
        overmove = minval + tolerance - cur
        return maxval - tolerance - overmove - extra
    if (maxval - cur) < tolerance:
        overmove = tolerance - (maxval - cur)
        return minval + tolerance + overmove + extra
    return cur


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1695133199.0
mcomix-3.1.0/mcomix/file_chooser_base_dialog.py0000644000175000017500000002730714502327017021333 0ustar00moritzmoritz"""filechooser_chooser_base_dialbg.py - Custom FileChooserDialog implementations."""

import os
import mimetypes
import fnmatch
from gi.repository import Gtk, Pango

from mcomix.preferences import prefs
from mcomix import image_tools
from mcomix import archive_tools
from mcomix import labels
from mcomix import constants
from mcomix import log
from mcomix import thumbnail_tools
from mcomix import message_dialog
from mcomix import file_provider
from mcomix import tools
from mcomix.i18n import _

mimetypes.init()

class _BaseFileChooserDialog(Gtk.Dialog):

    """We roll our own FileChooserDialog because the one in GTK seems
    buggy with the preview widget. The  argument dictates what type
    of filechooser dialog we want (i.e. it is Gtk.FileChooserAction.OPEN
    or Gtk.FileChooserAction.SAVE).

    This is a base class for the _MainFileChooserDialog, the
    _LibraryFileChooserDialog and the SimpleFileChooserDialog.

    Subclasses should implement a method files_chosen(paths) that will be
    called once the filechooser has done its job and selected some files.
    If the dialog was closed or Cancel was pressed,  is the empty list.
    """

    _last_activated_file = None

    def __init__(self, action=Gtk.FileChooserAction.OPEN):
        self._action = action
        self._destroyed = False

        if action == Gtk.FileChooserAction.OPEN:
            title = _('Open')
            buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OPEN, Gtk.ResponseType.OK)

        else:
            title = _('Save')
            buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_SAVE, Gtk.ResponseType.OK)

        super(_BaseFileChooserDialog, self).__init__(title, None, 0, buttons)
        self.set_default_response(Gtk.ResponseType.OK)

        self.filechooser = Gtk.FileChooserWidget(action=action)
        self.filechooser.set_size_request(680, 420)
        self.vbox.pack_start(self.filechooser, True, True, 0)
        self.set_border_width(4)
        self.filechooser.set_border_width(6)
        self.connect('response', self._response)
        self.filechooser.connect('file_activated', self._response,
            Gtk.ResponseType.OK)

        preview_box = Gtk.VBox(False, 10)
        preview_box.set_size_request(130, 0)
        self._preview_image = Gtk.Image()
        self._preview_image.set_size_request(130, 130)
        preview_box.pack_start(self._preview_image, False, False, 0)
        self.filechooser.set_preview_widget(preview_box)

        pango_scale_small = (1 / 1.2)

        self._namelabel = labels.FormattedLabel(weight=Pango.Weight.BOLD,
            scale=pango_scale_small)
        self._namelabel.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
        preview_box.pack_start(self._namelabel, False, False, 0)

        self._sizelabel = labels.FormattedLabel(scale=pango_scale_small)
        self._sizelabel.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
        preview_box.pack_start(self._sizelabel, False, False, 0)
        self.filechooser.set_use_preview_label(False)
        preview_box.show_all()
        self.filechooser.connect('update-preview', self._update_preview)

        self._all_files_filter = self.add_filter( _('All files'), [], ['*'])

        try:
            current_file = self._current_file()
            last_file = self.__class__._last_activated_file

            # If a file is currently open, use its path
            if current_file and os.path.exists(current_file):
                self.filechooser.set_current_folder(os.path.dirname(current_file))
            # If no file is open, use the last stored file
            elif (last_file and os.path.exists(last_file)):
                self.filechooser.set_filename(last_file)
            # If no file was stored yet, fall back to preferences
            elif os.path.isdir(prefs['path of last browsed in filechooser']):
                if prefs['store recent file info']:
                    self.filechooser.set_current_folder(
                        prefs['path of last browsed in filechooser'])
                else:
                    self.filechooser.set_current_folder(
                        constants.HOME_DIR)

        except Exception as ex: # E.g. broken prefs values.
            log.debug(ex)

        self.show_all()

    def add_filter(self, name, mimes, patterns=[]):
        """Add a filter, called , for each mime type in  and
        each pattern in  to the filechooser.
        """
        ffilter = Gtk.FileFilter()
        ffilter.add_custom(
                Gtk.FileFilterFlags.FILENAME | Gtk.FileFilterFlags.MIME_TYPE,
                self._filter, (patterns, mimes))

        ffilter.set_name(name)
        self.filechooser.add_filter(ffilter)
        return ffilter

    def add_archive_filters(self):
        """Add archive filters to the filechooser.
        """
        ffilter = Gtk.FileFilter()
        ffilter.set_name(_('All archives'))
        self.filechooser.add_filter(ffilter)
        supported_formats = archive_tools.get_supported_formats()
        for name in sorted(supported_formats):
            mime_types, extensions = supported_formats[name]
            patterns = ['*.%s' % ext for ext in extensions]
            self.add_filter(_('%s archives') % name, mime_types, patterns)
            for mime in mime_types:
                ffilter.add_mime_type(mime)
            for pat in patterns:
                ffilter.add_pattern(pat)

    def add_image_filters(self):
        """Add images filters to the filechooser.
        """
        ffilter = Gtk.FileFilter()
        ffilter.set_name(_('All images'))
        self.filechooser.add_filter(ffilter)
        supported_formats = image_tools.get_supported_formats()
        for name in sorted(supported_formats):
            mime_types, extensions = supported_formats[name]
            patterns = ['*.%s' % ext for ext in extensions]
            self.add_filter(_('%s images') % name, mime_types, patterns)
            for mime in mime_types:
                ffilter.add_mime_type(mime)
            for pat in patterns:
                ffilter.add_pattern(pat)

    def _filter(self, filter_info, data):
        """ Callback function used to determine if a file
        should be filtered or not. C{data} is a tuple containing
        (patterns, mimes) that should pass the test. Returns True
        if the file passed in C{filter_info} should be displayed. """

        match_patterns, match_mimes = data

        matches_mime = bool([match_mime for match_mime in match_mimes if match_mime == filter_info.mime_type])
        matches_pattern = bool([match_pattern for match_pattern in match_patterns if fnmatch.fnmatch(filter_info.filename, match_pattern)])

        return matches_mime or matches_pattern

    def collect_files_from_subdir(self, path, filter, recursive=False):
        """ Finds archives within C{path} that match the
        L{Gtk.FileFilter} passed in C{filter}. """

        for root, dirs, files in os.walk(path):
            for file in files:
                full_path = os.path.join(root, file)
                mimetype = mimetypes.guess_type(full_path)[0] or 'application/octet-stream'
                filter_info = Gtk.FileFilterInfo()
                filter_info.contains = Gtk.FileFilterFlags.FILENAME | Gtk.FileFilterFlags.MIME_TYPE
                filter_info.filename = full_path
                filter_info.mime_type = mimetype

                if (filter == self._all_files_filter or filter.filter(filter_info)):
                    yield full_path

            if not recursive:
                break

    def set_save_name(self, name):
        self.filechooser.set_current_name(name)

    def set_current_directory(self, path):
        self.filechooser.set_current_folder(path)

    def should_open_recursive(self):
        return False

    def _response(self, widget, response):
        """Return a list of the paths of the chosen files, or None if the
        event only changed the current directory.
        """
        if response == Gtk.ResponseType.OK:
            if not self.filechooser.get_filenames():
                return

            # Collect files, if necessary also from subdirectories
            filter = self.filechooser.get_filter()
            paths = [ ]
            for path in self.filechooser.get_filenames():
                if os.path.isdir(path):
                    subdir_files = list(self.collect_files_from_subdir(path, filter,
                        self.should_open_recursive()))
                    file_provider.FileProvider.sort_files(subdir_files)
                    paths.extend(subdir_files)
                else:
                    paths.append(path)

            # FileChooser.set_do_overwrite_confirmation() doesn't seem to
            # work on our custom dialog, so we use a simple alternative.
            first_path = self.filechooser.get_filenames()[0]
            if (self._action == Gtk.FileChooserAction.SAVE and
                not os.path.isdir(first_path) and
                os.path.exists(first_path)):

                overwrite_dialog = message_dialog.MessageDialog(None, 0,
                    Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL)
                overwrite_dialog.set_text(
                    _("A file named '%s' already exists. Do you want to replace it?") %
                        os.path.basename(first_path),
                    _('Replacing it will overwrite its contents.'))
                response = overwrite_dialog.run()

                if response != Gtk.ResponseType.OK:
                    self.emit_stop_by_name('response')
                    return

            # Do not store path if the user chose not to keep a file history
            if prefs['store recent file info']:
                prefs['path of last browsed in filechooser'] = \
                    self.filechooser.get_current_folder()
            else:
                prefs['path of last browsed in filechooser'] = \
                    constants.HOME_DIR

            self.__class__._last_activated_file = first_path
            self.files_chosen(paths)

        else:
            self.files_chosen([])

        self._destroyed = True

    def _update_preview(self, *args):
        if self.filechooser.get_preview_filename():
            path = self.filechooser.get_preview_filename()
        else:
            path = None

        if path and os.path.isfile(path):
            thumbnailer = thumbnail_tools.Thumbnailer(size=(128, 128),
                                                      archive_support=True)
            thumbnailer.thumbnail_finished += self._preview_thumbnail_finished
            thumbnailer.thumbnail(path, threaded=True)
        else:
            self._preview_image.clear()
            self._namelabel.set_text('')
            self._sizelabel.set_text('')

    def _preview_thumbnail_finished(self, filepath, pixbuf):
        """ Called when the thumbnailer has finished creating
        the thumbnail for . """

        if self._destroyed:
            return

        current_path = self.filechooser.get_preview_filename()
        if current_path and current_path == filepath:

            if pixbuf is None:
                self._preview_image.clear()
                self._namelabel.set_text('')
                self._sizelabel.set_text('')

            else:
                pixbuf = image_tools.add_border(pixbuf, 1)
                self._preview_image.set_from_pixbuf(pixbuf)
                self._namelabel.set_text(os.path.basename(filepath))
                self._sizelabel.set_text(tools.format_byte_size(
                    os.stat(filepath).st_size))

    def _current_file(self):
        # XXX: This method defers the import of main to avoid cyclic imports
        # during startup.

        from mcomix import main
        return main.main_window().filehandler.get_path_to_base()

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/file_chooser_library_dialog.py0000644000175000017500000000601114476523373022067 0ustar00moritzmoritz"""file_chooser_library_dialog.py - Custom FileChooserDialog implementations."""

from gi.repository import Gtk

from mcomix.preferences import prefs
from mcomix import file_chooser_base_dialog
from mcomix.i18n import _

_library_filechooser_dialog = None

class _LibraryFileChooserDialog(file_chooser_base_dialog._BaseFileChooserDialog):

    """The filechooser dialog used when adding books to the library."""

    def __init__(self, library):
        super(_LibraryFileChooserDialog, self).__init__()
        self.set_transient_for(library)
        self.set_title(_('Add books'))

        self._library = library

        self.filechooser.set_select_multiple(True)
        self.add_archive_filters()

        # Remove 'All files' filter from base class
        filters = self.filechooser.list_filters()
        self.filechooser.remove_filter(filters[0])
        self.filechooser.set_filter(filters[1])

        try:
            # When setting this to the first filter ("All files"), this
            # fails on some GTK+ versions and sets the filter to "blank".
            # The effect is the same though (i.e. display all files), and
            # there is no solution that I know of, so we'll have to live
            # with it. It only happens the second time a dialog is created
            # though, which is very strange.
            self.filechooser.set_filter(filters[
                prefs['last filter in library filechooser']])

        except Exception:
            self.filechooser.set_filter(filters[0])

        # Remove default buttons and add buttons that make more sense
        for widget in self.get_action_area().get_children():
            self.get_action_area().remove(widget)

        self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
        self.add_button(Gtk.STOCK_ADD, Gtk.ResponseType.OK)
        self.set_default_response(Gtk.ResponseType.OK)

    def should_open_recursive(self):
        return True

    def files_chosen(self, paths):
        if paths:
            try: # For some reason this fails sometimes (GTK+ bug?)
                filter_index = self.filechooser.list_filters().index(
                    self.filechooser.get_filter())
                prefs['last filter in library filechooser'] = filter_index

            except Exception:
                pass

            close_library_filechooser_dialog()
            self._library.add_books(paths, None)

        else:
            close_library_filechooser_dialog()

def open_library_filechooser_dialog(library):
    """Open the library filechooser dialog."""
    global _library_filechooser_dialog

    if _library_filechooser_dialog is None:
        _library_filechooser_dialog = _LibraryFileChooserDialog(library)
    else:
        _library_filechooser_dialog.present()

def close_library_filechooser_dialog(*args):
    """Close the library filechooser dialog."""
    global _library_filechooser_dialog

    if _library_filechooser_dialog is not None:
        _library_filechooser_dialog.destroy()
        _library_filechooser_dialog = None


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/file_chooser_main_dialog.py0000644000175000017500000000507214476523373021355 0ustar00moritzmoritz"""file_chooser_main_dialog.py - Custom FileChooserDialog implementations."""

from gi.repository import Gtk

from mcomix.preferences import prefs
from mcomix import file_chooser_base_dialog

_main_filechooser_dialog = None

class _MainFileChooserDialog(file_chooser_base_dialog._BaseFileChooserDialog):

    """The normal filechooser dialog used with the "Open" menu item."""

    def __init__(self, window):
        super(_MainFileChooserDialog, self).__init__()
        self._window = window
        self.set_transient_for(window)
        self.filechooser.set_select_multiple(True)
        self.add_archive_filters()
        self.add_image_filters()
        filters = self.filechooser.list_filters()
        try:
            # When setting this to the first filter ("All files"), this
            # fails on some GTK+ versions and sets the filter to "blank".
            # The effect is the same though (i.e. display all files), and
            # there is no solution that I know of, so we'll have to live
            # with it. It only happens the second time a dialog is created
            # though, which is very strange.
            self.filechooser.set_filter(filters[
                prefs['last filter in main filechooser']])
        except:
            self.filechooser.set_filter(filters[0])

    def files_chosen(self, paths):
        if paths:
            try: # For some reason this fails sometimes (GTK+ bug?)
                filter_index = self.filechooser.list_filters().index(
                    self.filechooser.get_filter())
                prefs['last filter in main filechooser'] = filter_index
            except:
                pass
            _close_main_filechooser_dialog()

            # If more than one file is selected, restrict opening
            # further files to the selection.
            if len(paths) > 1:
                files = paths
            else:
                files = paths[0]

            self._window.filehandler.open_file(files)
        else:
            _close_main_filechooser_dialog()

def open_main_filechooser_dialog(action, window):
    """Open the main filechooser dialog."""
    global _main_filechooser_dialog
    if _main_filechooser_dialog is None:
        _main_filechooser_dialog = _MainFileChooserDialog(window)
    else:
        _main_filechooser_dialog.present()


def _close_main_filechooser_dialog(*args):
    """Close the main filechooser dialog."""
    global _main_filechooser_dialog
    if _main_filechooser_dialog is not None:
        _main_filechooser_dialog.destroy()
        _main_filechooser_dialog = None

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/file_chooser_simple_dialog.py0000644000175000017500000000176514476523373021727 0ustar00moritzmoritz"""file_chooser_simple_dialog.py - Custom FileChooserDialog implementations."""

from gi.repository import Gtk

from mcomix import file_chooser_base_dialog

class SimpleFileChooserDialog(file_chooser_base_dialog._BaseFileChooserDialog):

    """A simple filechooser dialog that is designed to be used with the
    Gtk.Dialog.run() method. The  dictates what type of filechooser
    dialog we want (i.e. save or open). If the type is an open-dialog, we
    use multiple selection by default.
    """

    def __init__(self, action=Gtk.FileChooserAction.OPEN):
        super(SimpleFileChooserDialog, self).__init__(action)
        if action == Gtk.FileChooserAction.OPEN:
            self.filechooser.set_select_multiple(True)
        self._paths = None

    def get_paths(self):
        """Return the selected paths. To be called after run() has returned
        a response.
        """
        return self._paths

    def files_chosen(self, paths):
        self._paths = paths

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/file_handler.py0000644000175000017500000006372214476523373017013 0ustar00moritzmoritz"""file_handler.py - File handler that takes care of opening archives and images."""


import os
import shutil
import tempfile
import threading
import re
import pickle
from gi.repository import Gtk

from mcomix.preferences import prefs
from mcomix import archive_extractor
from mcomix import archive_tools
from mcomix import image_tools
from mcomix import tools
from mcomix import constants
from mcomix import file_provider
from mcomix import callback
from mcomix import log
from mcomix import last_read_page
from mcomix import message_dialog
from mcomix.library import backend
from mcomix.i18n import _


class FileHandler(object):

    """The FileHandler keeps track of the actual files/archives opened.

    While ImageHandler takes care of pages/images, this class provides
    the raw file names for archive members and image files, extracts
    archives, and lists directories for image files.
    """

    def __init__(self, window):
        #: Indicates if files/archives are currently loaded/loading.
        self.file_loaded = False
        self.file_loading = False
        #: None if current file is not an archive, or unrecognized format.
        self.archive_type = None

        #: Either path to the current archive, or first file in image list.
        #: This is B{not} the path to the currently open page.
        self._current_file = None
        #: Reference to L{MainWindow}.
        self._window = window
        #: Path to opened archive file, or directory containing current images.
        self._base_path = None
        #: Temporary directory used for extracting archives.
        self._tmp_dir = None
        #: If C{True}, no longer wait for files to get extracted.
        self._stop_waiting = False
        #: List of comment files inside of the currently opened archive.
        self._comment_files = []
        #: Mapping of absolute paths to archive path names.
        self._name_table = {}
        #: Archive extractor.
        self._extractor = archive_extractor.Extractor()
        self._extractor.file_extracted += self._extracted_file
        self._extractor.contents_listed += self._listed_contents
        #: Condition to wait on when extracting archives and waiting on files.
        self._condition = None
        #: Provides a list of available files/archives in the open directory.
        self._file_provider = None
        #: Keeps track of the last read page in archives
        self.last_read_page = last_read_page.LastReadPage(backend.LibraryBackend())
        #: Regexp used for determining which archive files are comment files.
        self._comment_re = None
        self.update_comment_extensions()

        self.last_read_page.set_enabled(bool(prefs['store recent file info']))

    def refresh_file(self, *args, **kwargs):
        """ Closes the current file(s)/archive and reloads them. """
        if self.file_loaded:
            current_file = os.path.abspath(self._window.imagehandler.get_real_path())
            if self.archive_type is not None:
                start_page = self._window.imagehandler.get_current_page()
            else:
                start_page = 0
            self.open_file(current_file, start_page, keep_fileprovider=True)

    def open_file(self, path, start_page=0, keep_fileprovider=False):
        """Open the file pointed to by .

        If  is not set we set the current
        page to 1 (first page), if it is set we set the current page to the
        value of . If  is non-positive it means the
        last image.

        Return True if the file is successfully loaded.
        """

        self._close()

        try:
            path = self._initialize_fileprovider(path, keep_fileprovider)
        except ValueError as ex:
            self._window.statusbar.set_message(str(ex))
            self._window.osd.show(str(ex))
            return False

        error_message = self._check_access(path)
        if error_message:
            self._window.statusbar.set_message(error_message)
            self._window.osd.show(error_message)
            self.file_opened()
            return False

        self.filelist = self._file_provider.list_files()
        self.archive_type = archive_tools.archive_mime_type(path)
        self._start_page = start_page
        self._current_file = os.path.abspath(path)
        self._stop_waiting = False

        image_files = []
        current_image_index = 0

        # Actually open the file(s)/archive passed in path.
        if self.archive_type is not None:
            try:
                self._open_archive(self._current_file)
            except Exception as ex:
                self._window.statusbar.set_message(str(ex))
                self._window.osd.show(str(ex))
                self.file_opened()
                return False
            self.file_loading = True
        else:
            image_files, current_image_index = \
                self._open_image_files(self.filelist, self._current_file)
            self._archive_opened(image_files)

        return True

    def _archive_opened(self, image_files):
        """ Called once the archive has been opened and its contents listed.
        """

        self._window.imagehandler._base_path = self._base_path
        self._window.imagehandler._image_files = image_files
        self.file_opened()

        if not image_files:
            msg = _("No images in '%s'") % os.path.basename(self._current_file)
            self._window.statusbar.set_message(msg)
            self._window.osd.show(msg)

        else:
            if self.archive_type is None:
                # If no extraction is required, mark all files as available.
                self.file_available(self.filelist)
                # Set current page to current file.
                if self._current_file in self.filelist:
                    current_image_index = self.filelist.index(self._current_file)
                else:
                    current_image_index = 0
            else:
                last_image_index = self._get_index_for_page(self._start_page,
                                                            len(image_files),
                                                            self._current_file)
                if self._start_page or \
                   prefs['stored dialog choices'].get('resume-from-last-read-page', False):
                    current_image_index = last_image_index
                else:
                    # Don't switch to last page yet; since we have not asked
                    # the user for confirmation yet.
                    current_image_index = 0
                if last_image_index != current_image_index:
                    # Bump last page closer to the front of the extractor queue.
                    self._window.set_page(last_image_index + 1)

            self._window.set_page(current_image_index + 1)

            if self.archive_type is not None:
                self._extractor.extract()
                if last_image_index != current_image_index and \
                   self._ask_goto_last_read_page(self._current_file, last_image_index + 1):
                    self._window.set_page(last_image_index + 1)

            self.write_fileinfo_file()

        self._window.uimanager.recent.add(self._current_file)

    @callback.Callback
    def file_opened(self):
        """ Called when a new set of files has successfully been opened. """
        self.file_loaded = True

    @callback.Callback
    def file_closed(self):
        """ Called when the current file has been closed. """
        pass

    def close_file(self):
        """Close the currently opened file and its provider. """
        self._close(close_provider=True)

    def _close(self, close_provider=False):
        """Run tasks for "closing" the currently opened file(s)."""
        if self.file_loaded or self.file_loading:
            if close_provider:
                self._file_provider = None
            self.update_last_read_page()
            if self.archive_type is not None:
                self._extractor.close()
            self._window.imagehandler.cleanup()
            self.file_loaded = False
            self.file_loading = False
            self.archive_type = None
            self._current_file = None
            self._base_path = None
            self._stop_waiting = True
            self._comment_files = []
            self._name_table.clear()
            self.file_closed()
        # Catch up on UI events, so we don't leave idle callbacks.
        while Gtk.events_pending():
            Gtk.main_iteration_do(False)
        tools.garbage_collect()
        if self._tmp_dir is not None:
            self.thread_delete(self._tmp_dir)
            self._tmp_dir = None

    def _initialize_fileprovider(self, path, keep_fileprovider):
        """ Creates the L{file_provider.FileProvider} for C{path}.

        If C{path} is a list, assumes that only the files in the list
        should be available. If C{path} is a string, assume that it is
        either a directory or an image file, and all files in that directory
        should be opened.

        @param path: List of file names, or single file/directory as string.
        @param keep_fileprovider: If C{True}, no new provider is constructed.
        @return: If C{path} was a list, returns the first list element.
            Otherwise, C{path} is not modified."""

        if isinstance(path, list) and len(path) == 0:
            # This is a programming error and does not need translation.
            assert False, "Tried to open an empty list of files."

        elif isinstance(path, list) and len(path) > 0:
            # A list of files was passed - open only these files.
            if self._file_provider is None or not keep_fileprovider:
                self._file_provider = file_provider.get_file_provider(path)

            return path[0]
        else:
            # A single file was passed - use Comix' classic open mode
            # and open all files in its directory.
            if self._file_provider is None or not keep_fileprovider:
                self._file_provider = file_provider.get_file_provider([ path ])

            return path

    def _check_access(self, path):
        """ Checks for various error that could occur when opening C{path}.

        @param path: Path to file that should be opened.
        @return: An appropriate error string, or C{None} if no error was found.
        """
        if not os.path.exists(path):
            return _('Could not open %s: No such file.') % path

        elif not os.access(path, os.R_OK):
            return _('Could not open %s: Permission denied.') % path

        else:
            return None

    def _open_archive(self, path):
        """ Opens the archive passed in C{path}.

        Creates an L{archive_extractor.Extractor} and extracts all images
        found within the archive.

        @return: A tuple containing C{(image_files, image_index)}. """

        self._tmp_dir = tempfile.mkdtemp(prefix='mcomix.', suffix=os.sep)
        self._base_path = path
        try:
            self._condition = self._extractor.setup(self._base_path,
                                                self._tmp_dir,
                                                self.archive_type)
        except Exception:
            self._condition = None
            raise

    def _listed_contents(self, archive, files):

        if not self.file_loading:
            return
        self.file_loading = False

        files = self._extractor.get_files()
        archive_images = [image for image in files
            if image_tools.is_image_file(image)
            # Remove MacOS meta files from image list
            and not '__MACOSX' in os.path.normpath(image).split(os.sep)]

        self._sort_archive_images(archive_images)
        image_files = [ os.path.join(self._tmp_dir, f)
                        for f in archive_images ]

        comment_files = list(filter(self._comment_re.search, files))
        tools.alphanumeric_sort(comment_files)
        self._comment_files = [ os.path.join(self._tmp_dir, f)
                                for f in comment_files ]

        self._name_table = dict(list(zip(image_files, archive_images)))
        self._name_table.update(list(zip(self._comment_files, comment_files)))

        self._extractor.set_files(archive_images + comment_files)

        self._archive_opened(image_files)

    def _sort_archive_images(self, filelist):
        """ Sorts the image list passed in C{filelist} based on the sorting
        preference option. """

        if prefs['sort archive by'] == constants.SORT_NAME:
            tools.alphanumeric_sort(filelist)
        elif prefs['sort archive by'] == constants.SORT_NAME_LITERAL:
            filelist.sort()
        else:
            # No sorting
            pass

        if prefs['sort archive order'] == constants.SORT_DESCENDING:
            filelist.reverse()

    def _get_index_for_page(self, start_page, num_of_pages, path):
        """ Returns the page that should be displayed for an archive.
        @param start_page: If -1, show last page. If 0, show either first page
                           or last read page. If > 0, show C{start_page}.
        @param num_of_pages: Page count.
        @param path: Archive path.
        """
        if start_page < 0 and prefs['default double page']:
            current_image_index = num_of_pages - 2
        elif start_page < 0 and not prefs['default double page']:
            current_image_index = num_of_pages - 1
        elif start_page == 0:
            current_image_index = (self.last_read_page.get_page(path) or 1) - 1
        else:
            current_image_index = start_page - 1

        return min(max(0, current_image_index), num_of_pages - 1)

    def _ask_goto_last_read_page(self, path, last_read_page):
        """ If the user read an archive previously, ask to continue from
        that time, or from page 1. This method returns a page index, that is,
        index + 1. """

        read_date = self.last_read_page.get_date(path)

        dialog = message_dialog.MessageDialog(self._window, Gtk.DialogFlags.MODAL, Gtk.MessageType.INFO,
            Gtk.ButtonsType.YES_NO)
        dialog.set_default_response(Gtk.ResponseType.YES)
        dialog.set_should_remember_choice('resume-from-last-read-page',
            (Gtk.ResponseType.YES, Gtk.ResponseType.NO))
        dialog.set_text(
            (_('Continue reading from page %d?') % last_read_page),
            _('You stopped reading here on %(date)s, %(time)s. '
            'If you choose "Yes", reading will resume on page %(page)d. Otherwise, '
            'the first page will be loaded.') % {'date': read_date.date().strftime("%x"),
                'time': read_date.time().strftime("%X"), 'page': last_read_page})
        result = dialog.run()

        return result == Gtk.ResponseType.YES

    def _open_image_files(self, filelist, image_path):
        """ Opens all files passed in C{filelist}.

        If C{image_path} is found in C{filelist}, the current page will be set
        to its index within C{filelist}.

        @return: Tuple of C{(image_files, image_index)}
        """

        self._base_path = self._file_provider.get_directory()

        if image_path in filelist:
            current_image_index = filelist.index(image_path)
        else:
            current_image_index = 0

        return filelist, current_image_index

    def get_file_number(self):
        if self.archive_type is None:
            # No file numbers for images.
            return 0, 0
        file_list = self._file_provider.list_files(file_provider.FileProvider.ARCHIVES)
        if self._current_file in file_list:
            current_index = file_list.index(self._current_file)
        else:
            current_index = 0
        return current_index + 1, len(file_list)

    def get_number_of_comments(self):
        """Return the number of comments in the current archive."""
        return len(self._comment_files)

    def get_comment_text(self, num):
        """Return the text in comment  or None if comment  is not
        readable.
        """
        self._wait_on_comment(num)
        try:
            fd = open(self._comment_files[num - 1], 'r')
            text = fd.read()
            fd.close()
        except Exception:
            text = None
        return text

    def get_comment_name(self, num):
        """Return the filename of comment ."""
        return self._comment_files[num - 1]

    def update_comment_extensions(self):
        """Update the regular expression used to filter out comments in
        archives by their filename.
        """
        exts = '|'.join(prefs['comment extensions'])
        self._comment_re = re.compile(r'\.(%s)\s*$' % exts, re.I)

    def get_path_to_base(self):
        """Return the full path to the current base (path to archive or
        image directory.)
        """
        if self.archive_type is not None:
            return self._base_path
        elif self._window.imagehandler._image_files:
            img_index = self._window.imagehandler._current_image_index
            filename = self._window.imagehandler._image_files[img_index]
            return os.path.dirname(filename)
        else:
            return None

    def get_base_filename(self):
        """Return the filename of the current base (archive filename or
        directory name).
        """
        return os.path.basename(self.get_path_to_base())

    def get_pretty_current_filename(self):
        """Return a string with the name of the currently viewed file that is
        suitable for printing.
        """

        return self._window.imagehandler.get_pretty_current_filename()

    def _open_next_archive(self, *args):
        """Open the archive that comes directly after the currently loaded
        archive in that archive's directory listing, sorted alphabetically.
        Returns True if a new archive was opened, False otherwise.
        """
        if self.archive_type is not None:

            files = self._file_provider.list_files(file_provider.FileProvider.ARCHIVES)
            absolute_path = os.path.abspath(self._base_path)
            if absolute_path not in files: return
            current_index = files.index(absolute_path)

            for path in files[current_index + 1:]:
                if archive_tools.archive_mime_type(path) is not None:
                    self._close()
                    self.open_file(path, keep_fileprovider=True)
                    return True

        return False

    def _open_previous_archive(self, *args):
        """Open the archive that comes directly before the currently loaded
        archive in that archive's directory listing, sorted alphabetically.
        Returns True if a new archive was opened, False otherwise.
        """
        if self.archive_type is not None:

            files = self._file_provider.list_files(file_provider.FileProvider.ARCHIVES)
            absolute_path = os.path.abspath(self._base_path)
            if absolute_path not in files: return
            current_index = files.index(absolute_path)

            for path in reversed(files[:current_index]):
                if archive_tools.archive_mime_type(path) is not None:
                    self._close()
                    self.open_file(path, prefs['open first file in prev archive']-1,
                                   keep_fileprovider=True)
                    return True

        return False

    def open_next_directory(self, *args):
        """ Opens the next sibling directory of the current file, as specified by
        file provider. Returns True if a new directory was opened and files found. """

        if self._file_provider is None:
            return

        if self.archive_type is not None:
            listmode = file_provider.FileProvider.ARCHIVES
        else:
            listmode = file_provider.FileProvider.IMAGES

        current_dir = self._file_provider.get_directory()
        if not self._file_provider.next_directory():
            # Restore current directory if no files were found
            self._file_provider.set_directory(current_dir)
            return False

        files = self._file_provider.list_files(listmode)
        self._close()
        if len(files) > 0:
            path = files[0]
        else:
            path = self._file_provider.get_directory()
        self.open_file(path, keep_fileprovider=True)
        return True

    def open_previous_directory(self, *args):
        """ Opens the previous sibling directory of the current file, as specified by
        file provider. Returns True if a new directory was opened and files found. """

        if self._file_provider is None:
            return

        if self.archive_type is not None:
            listmode = file_provider.FileProvider.ARCHIVES
        else:
            listmode = file_provider.FileProvider.IMAGES

        current_dir = self._file_provider.get_directory()
        if not self._file_provider.previous_directory():
            # Restore current directory if no files were found
            self._file_provider.set_directory(current_dir)
            return False

        files = self._file_provider.list_files(listmode)
        self._close()
        if len(files) > 0:
            path = files[prefs['open first file in prev directory']-1]
        else:
            path = self._file_provider.get_directory()

        self.open_file(path, (
            prefs['open first file in prev archive'] or \
            prefs['open first file in prev directory'])-1,
                       keep_fileprovider=True)
        return True

    def file_is_available(self, filepath):
        """ Returns True if the file specified by "filepath" is available
        for reading, i.e. extracted to harddisk. """

        if self.archive_type is not None:
            with self._condition:
                return self._extractor.is_ready(self._name_table[filepath])

        elif filepath is None:
            return False

        elif os.path.isfile(filepath):
            return True

        else:
            return False

    @callback.Callback
    def file_available(self, filepaths):
        """ Called every time a new file from the Filehandler's opened
        files becomes available. C{filepaths} is a list of now available files.
        """
        pass

    def _extracted_file(self, extractor, name):
        """ Called when the extractor finishes extracting the file at
        . This name is relative to the temporary directory
        the files were extracted to. """
        if not self.file_loaded:
            return
        filepath = os.path.join(extractor.get_directory(), name)
        self.file_available([filepath])

    def _wait_on_comment(self, num):
        """Block the running (main) thread until the file corresponding to
        comment  has been fully extracted.
        """
        path = self._comment_files[num - 1]
        self._wait_on_file(path)

    def _wait_on_file(self, path):
        """Block the running (main) thread if the file  is from an
        archive and has not yet been extracted. Return when the file is
        ready.
        """
        if self.archive_type == None or path == None:
            return

        try:
            name = self._name_table[path]
            with self._condition:
                while not self._extractor.is_ready(name) and not self._stop_waiting:
                    self._condition.wait()
        except Exception as ex:
            log.error('Waiting on extraction of "%s" failed: %s', path, ex)
            return

    def _ask_for_files(self, files):
        """Ask for  to be given priority for extraction.
        """
        if self.archive_type == None:
            return

        with self._condition:
            extractor_files = self._extractor.get_files()
            for path in reversed(files):
                name = self._name_table[path]
                if not self._extractor.is_ready(name):
                    extractor_files.remove(name)
                    extractor_files.insert(0, name)
            self._extractor.set_files(extractor_files)

    def thread_delete(self, path):
        """Start a threaded removal of the directory tree rooted at .
        This is to avoid long blockings when removing large temporary dirs.
        """
        del_thread = threading.Thread(target=shutil.rmtree, args=(path, True))
        del_thread.name += '-delete'
        del_thread.setDaemon(False)
        del_thread.start()

    def write_fileinfo_file(self):
        """Write current open file information."""

        if self.file_loaded:
            config = open(constants.FILEINFO_PICKLE_PATH, 'wb')

            path = self._window.imagehandler.get_real_path()
            page_index = self._window.imagehandler.get_current_page() - 1
            current_file_info = [ path, page_index ]

            pickle.dump(current_file_info, config, pickle.HIGHEST_PROTOCOL)
            config.close()

    def read_fileinfo_file(self):
        """Read last loaded file info from disk."""

        fileinfo = None

        if os.path.isfile(constants.FILEINFO_PICKLE_PATH):
            config = None
            try:
                config = open(constants.FILEINFO_PICKLE_PATH, 'rb')

                fileinfo = pickle.load(config)

                config.close()

            except Exception as ex:
                log.error(_('! Corrupt preferences file "%s", deleting...'),
                        constants.FILEINFO_PICKLE_PATH )
                log.info('Error was: %s', ex)
                if config is not None:
                    config.close()
                os.remove(constants.FILEINFO_PICKLE_PATH)

        return fileinfo

    def update_last_read_page(self):
        """ Stores the currently viewed page. """
        if self.archive_type is None or not self.file_loaded:
            return

        archive_path = self.get_path_to_base()
        page = self._window.imagehandler.get_current_page()
        # Do not store first page (first page is default
        # behaviour and would waste space unnecessarily)
        try:
            if page == 1:
                self.last_read_page.clear_page(archive_path)
            else:
                self.last_read_page.set_page(archive_path, page)
        except ValueError:
            # The book no longer exists in the library and has been deleted
            pass


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/file_provider.py0000644000175000017500000001736314476523373017230 0ustar00moritzmoritz# -*- coding: utf-8 -*-
""" file_provider.py - Handles listing files for the current directory and
    switching to the next/previous directory. """

import os
import re

from mcomix import image_tools
from mcomix import archive_tools
from mcomix import tools
from mcomix import constants
from mcomix import preferences
from mcomix import i18n
from mcomix import log
from mcomix.i18n import _

def get_file_provider(filelist):
    """ Initialize a FileProvider with the files in .
    If len(filelist) is 1, a OrderedFileProvider will be constructed, which
    will simply open all files in the passed directory.
    If len(filelist) is greater 1, a PreDefinedFileProvider will be created,
    which will only ever list the files that were passed into it.
    If len(filelist) is zero, FileProvider will look at the last file opened,
    if "Auto Open last file" is set. Otherwise, no provider is constructed. """

    if len(filelist) > 0:
        if len(filelist) == 1:
            if os.path.exists(filelist[0]):
                provider = OrderedFileProvider(filelist[0])
            else:
                provider = None
        else:
            provider = PreDefinedFileProvider(filelist)


    elif (preferences.prefs['auto load last file']
        and os.path.isfile(preferences.prefs['path to last file'])):
        provider = OrderedFileProvider(preferences.prefs['path to last file'])

    else:
        provider = None

    return provider

class FileProvider(object):
    """ Base class for various file listing strategies. """

    # Constants for determining which files to list.
    IMAGES, ARCHIVES = 1, 2

    def set_directory(self, file_or_directory):
        pass

    def get_directory(self):
        return os.path.abspath(os.getcwd())

    def list_files(self, mode=IMAGES):
        return []

    def next_directory(self):
        return False

    def previous_directory(self):
        return False

    @staticmethod
    def sort_files(files):
        """ Sorts a list of C{files} depending on the current preferences.
        The list is sorted in-place. """
        if preferences.prefs['sort by'] == constants.SORT_NAME:
            tools.alphanumeric_sort(files)
        elif preferences.prefs['sort by'] == constants.SORT_LAST_MODIFIED:
            # Most recently modified file first
            files.sort(key=lambda filename: os.path.getmtime(filename)*-1)
        elif preferences.prefs['sort by'] == constants.SORT_SIZE:
            # Smallest file first
            files.sort(key=lambda filename: os.stat(filename).st_size)
        # else: don't sort at all: use OS ordering.

        # Default is ascending.
        if preferences.prefs['sort order'] == constants.SORT_DESCENDING:
            files.reverse()


class OrderedFileProvider(FileProvider):
    """ This provider will list all files in the same directory as the
        one passed to the constructor. """

    def __init__(self, file_or_directory):
        """ Initializes the file listing. If  is a file,
            directory will be used as base path. If it is a directory, that
            will be used as base file. """

        self.set_directory(file_or_directory)

    def set_directory(self, file_or_directory):
        """ Sets the base directory. """

        if os.path.isdir(file_or_directory):
            dir = file_or_directory
        elif os.path.isfile(file_or_directory):
            dir = os.path.dirname(file_or_directory)
        else:
            # Passed file doesn't exist
            raise ValueError(_("Invalid path: '%s'") % file_or_directory)

        self.base_dir = os.path.abspath(dir)

    def get_directory(self):
        return self.base_dir

    def list_files(self, mode=FileProvider.IMAGES):
        """ Lists all files in the current directory.
            Returns a list of absolute paths, already sorted. """

        if mode == FileProvider.IMAGES:
            should_accept = image_tools.is_image_file
        elif mode == FileProvider.ARCHIVES:
            should_accept = archive_tools.is_archive_file
        else:
            should_accept = lambda file: True

        try:
            files = [ os.path.join(self.base_dir, filename) for filename in
                      # Explicitly convert all files to Unicode, even when
                      # os.listdir returns a mixture of byte/unicode strings.
                      # (MComix bug #3424405)
                      [ i18n.to_unicode(fn) for fn in os.listdir(self.base_dir) ]
                      if should_accept(os.path.join(self.base_dir, filename)) ]

            FileProvider.sort_files(files)

            return files
        except OSError:
            log.warning('! ' + _('Could not open %s: Permission denied.'), self.base_dir)
            return []

    def next_directory(self):
        """ Switches to the next sibling directory. Next call to
            list_file() returns files in the new directory.
            Returns True if the directory was changed, otherwise False. """

        directories = self.__get_sibling_directories(self.base_dir)
        current_index = directories.index(self.base_dir)
        if current_index < len(directories) - 1:
            self.base_dir = directories[current_index + 1]
            return True
        else:
            return False


    def previous_directory(self):
        """ Switches to the previous sibling directory. Next call to
            list_file() returns files in the new directory.
            Returns True if the directory was changed, otherwise False. """

        directories = self.__get_sibling_directories(self.base_dir)
        current_index = directories.index(self.base_dir)
        if current_index > 0:
            self.base_dir = directories[current_index - 1]
            return True
        else:
            return False

    def __get_sibling_directories(self, dir):
        """ Returns a list of all sibling directories of ,
            already sorted. """

        parent_dir = os.path.dirname(dir)
        directories = [ os.path.join(parent_dir, directory)
                for directory in os.listdir(parent_dir)
                if os.path.isdir(os.path.join(parent_dir, directory)) ]

        tools.alphanumeric_sort(directories)
        return directories


class PreDefinedFileProvider(FileProvider):
    """ Returns only a list of files as passed to the constructor. """

    def __init__(self, files):
        """  is a list of files that should be shown. The list is filtered
            to contain either only images, or only archives, depending on what the first
            file is, since FileHandler will probably have problems of archives and images
            are mixed in a file list. """

        should_accept = self.__get_file_filter(files)

        self.__files = [ ]

        for file in files:
            if os.path.isdir(file):
                provider = OrderedFileProvider(file)
                self.__files.extend(provider.list_files())

            elif should_accept(file):
                self.__files.append(os.path.abspath(file))


    def list_files(self, mode=FileProvider.IMAGES):
        """ Returns the files as passed to the constructor. """

        return self.__files

    def __get_file_filter(self, files):
        """ Determines what kind of files should be filtered in the given list
        of . Returns either a filter accepting only images, or only archives,
        depending on what type of file is found first in the list. """

        for file in files:
            if os.path.isfile(file):
                if image_tools.is_image_file(file):
                    return image_tools.is_image_file
                if archive_tools.is_archive_file(file):
                    return archive_tools.is_archive_file

        # Default filter only accepts images.
        return image_tools.is_image_file


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/histogram.py0000644000175000017500000000561614476523373016372 0ustar00moritzmoritz"""histogram.py - Draw histograms (RGB) from pixbufs."""

import PIL.Image as Image
import PIL.ImageDraw as ImageDraw
import PIL.ImageOps as ImageOps

from mcomix import image_tools

def draw_histogram(pixbuf, height=170, fill=170, text=True):
    """Draw a histogram from  and return it as another pixbuf.

    The returned prixbuf will be 262x px.

    The value of  determines the colour intensity of the filled graphs,
    valid values are between 0 and 255.

    If  is True a label with the maximum pixel value will be added to
    one corner.
    """
    im = Image.new('RGB', (258, height - 4), (30, 30, 30))
    hist_data = image_tools.pixbuf_to_pil(pixbuf).histogram()
    maximum = max(hist_data[:768] + [1])
    y_scale = float(height - 6) / maximum
    r = [int(hist_data[n] * y_scale) for n in range(256)]
    g = [int(hist_data[n] * y_scale) for n in range(256, 512)]
    b = [int(hist_data[n] * y_scale) for n in range(512, 768)]
    im_data = im.getdata()
    # Draw the filling colours
    for x in range(256):
        for y in range(1, max(r[x], g[x], b[x]) + 1):
            r_px = y <= r[x] and fill or 0
            g_px = y <= g[x] and fill or 0
            b_px = y <= b[x] and fill or 0
            im_data.putpixel((x + 1, height - 5 - y), (r_px, g_px, b_px))
    # Draw the outlines
    for x in range(1, 256):
        for y in list(range(r[x-1] + 1, r[x] + 1)) + [r[x]] * (r[x] != 0):
            r_px, g_px, b_px = im_data.getpixel((x + 1, height - 5 - y))
            im_data.putpixel((x + 1, height - 5 - y), (255, g_px, b_px))
        for y in range(r[x] + 1, r[x-1] + 1):
            r_px, g_px, b_px = im_data.getpixel((x, height - 5 - y))
            im_data.putpixel((x, height - 5 - y), (255, g_px, b_px))
        for y in list(range(g[x-1] + 1, g[x] + 1)) + [g[x]] * (g[x] != 0):
            r_px, g_px, b_px = im_data.getpixel((x + 1, height - 5 - y))
            im_data.putpixel((x + 1, height - 5 - y), (r_px, 255, b_px))
        for y in range(g[x] + 1, g[x-1] + 1):
            r_px, g_px, b_px = im_data.getpixel((x, height - 5 - y))
            im_data.putpixel((x, height - 5 - y), (r_px, 255, b_px))
        for y in list(range(b[x-1] + 1, b[x] + 1)) + [b[x]] * (b[x] != 0):
            r_px, g_px, b_px = im_data.getpixel((x + 1, height - 5 - y))
            im_data.putpixel((x + 1, height - 5 - y), (r_px, g_px, 255))
        for y in range(b[x] + 1, b[x-1] + 1):
            r_px, g_px, b_px = im_data.getpixel((x, height - 5 - y))
            im_data.putpixel((x, height - 5 - y), (r_px, g_px, 255))
    if text:
        maxstr = 'max: ' + str(maximum)
        draw = ImageDraw.Draw(im)
        draw.rectangle((0, 0, len(maxstr) * 6 + 2, 10), fill=(30, 30, 30))
        draw.text((2, 0), maxstr, fill=(255, 255, 255))
    im = ImageOps.expand(im, 1, (80, 80, 80))
    im = ImageOps.expand(im, 1, (0, 0, 0))
    return image_tools.pil_to_pixbuf(im)


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1698566559.0
mcomix-3.1.0/mcomix/i18n.py0000644000175000017500000000710714517410637015143 0ustar00moritzmoritz""" i18n.py - Encoding and translation handler."""

import gettext
import io
import locale
import os
import pkgutil
import sys

try:
    import chardet
except ImportError:
    chardet = None

from mcomix import preferences
from mcomix import portability
from mcomix import constants

# Translation instance to enable other modules to use
# functions other than the global _() if necessary
_translation = None

def to_unicode(string):
    """Convert  to unicode. First try the default filesystem
    encoding, and then fall back on some common encodings.
    """
    if isinstance(string, str):
        return string

    # Try chardet heuristic
    if chardet:
        probable_encoding = chardet.detect(string)['encoding'] or \
            locale.getpreferredencoding() # Fallback if chardet detection fails
    else:
        probable_encoding = locale.getpreferredencoding()

    for encoding in (
        probable_encoding,
        sys.getfilesystemencoding(),
        'utf-8',
        'latin-1'):

        try:
            ustring = str(string, encoding)
            return ustring

        except (UnicodeError, LookupError):
            pass

    return string.decode('utf-8', 'replace')

def to_utf8(string):
    """ Helper function that converts unicode objects to UTF-8 encoded
    strings. Non-unicode strings are assumed to be already encoded
    and returned as-is. """

    if isinstance(string, str):
        return string.encode('utf-8')
    else:
        return string

def install_gettext(force_lang=None):
    """ Initialize gettext with the correct directory that contains
    MComix translations. This has to be done before any calls to gettext.gettext
    have been made to ensure all strings are actually translated. """

    # Add the sources' base directory to PATH to allow development without
    # explicitly installing the package.
    sys.path.append(constants.BASE_PATH)

    # Initialize default locale
    locale.setlocale(locale.LC_ALL, '')

    if force_lang is not None:
        lang = force_lang
        lang_identifiers = [lang]
    elif preferences.prefs['language'] != 'auto':
        lang = preferences.prefs['language']
        lang_identifiers = [ lang ]
    else:
        # Get the user's current locale
        lang = portability.get_default_locale()
        lang_identifiers = gettext._expand_lang(lang)

    # Make sure GTK uses the correct language.
    os.environ['LANGUAGE'] = lang

    domain = constants.APPNAME.lower()

    # Search for .mo files manually, since gettext doesn't support packaged resources
    for lang in lang_identifiers:
        resource = os.path.join('messages', lang, 'LC_MESSAGES', '%s.mo' % domain)
        try:
            translation_content = pkgutil.get_data('mcomix', resource)
        except FileNotFoundError:
            pass
        else:
            fp = io.BytesIO(translation_content)
            translation = gettext.GNUTranslations(fp)
            break
    else:
        translation = gettext.NullTranslations()

    global _translation
    _translation = translation

def get_translation() -> gettext.NullTranslations:
    """Returns the loaded translation instance.
    (gettext.GNUTranslations is a subclass of NullTranslations.)"""
    return _translation or gettext.NullTranslations()

def _(message: str) -> str:
    """Translate the messsage using the current translator."""
    return get_translation().gettext(message)

def to_display_string(string):
    """ Converts a string to a valid UTF-8 string at the expense of data accuracy. """
    return string.encode('utf-8', 'surrogateescape').decode('utf-8', 'replace')

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701597438.0
mcomix-3.1.0/mcomix/icons.py0000644000175000017500000000477414533050376015504 0ustar00moritzmoritz"""icons.py - Load MComix specific icons."""

from gi.repository import Gtk
import pkgutil

from mcomix import image_tools
from mcomix import log
from mcomix.i18n import _


def mcomix_icons():
    """ Returns a list of differently sized pixbufs for the
    application icon. """

    sizes = ('16', '32', '48', '256')
    pixbufs = [
        image_tools.load_pixbuf_data(
            pkgutil.get_data('mcomix', f'images/mcomix-{size}.png')
        ) for size in sizes
    ]

    return pixbufs


def load_icons() -> None:
    _icons = (('gimp-flip-horizontal.png',   'mcomix-flip-horizontal'),
              ('gimp-flip-vertical.png',     'mcomix-flip-vertical'),
              ('gimp-rotate-180.png',        'mcomix-rotate-180'),
              ('gimp-rotate-270.png',        'mcomix-rotate-270'),
              ('gimp-rotate-90.png',         'mcomix-rotate-90'),
              ('gimp-thumbnails.png',        'mcomix-thumbnails'),
              ('gimp-transform.png',         'mcomix-transform'),
              ('tango-enhance-image.png',    'mcomix-enhance-image'),
              ('tango-add-bookmark.png',     'mcomix-add-bookmark'),
              ('tango-archive.png',          'mcomix-archive'),
              ('tango-image.png',            'mcomix-image'),
              ('library.png',                'mcomix-library'),
              ('comments.png',               'mcomix-comments'),
              ('zoom.png',                   'mcomix-zoom'),
              ('magnifyingglass.png',        'mcomix-lens'),
              ('double-page.png',            'mcomix-double-page'),
              ('manga.png',                  'mcomix-manga'),
              ('fitbest.png',                'mcomix-fitbest'),
              ('fitwidth.png',               'mcomix-fitwidth'),
              ('fitheight.png',              'mcomix-fitheight'),
              ('fitmanual.png',              'mcomix-fitmanual'),
              ('fitsize.png',                'mcomix-fitsize'))

    # Load window title icons.
    pixbufs = mcomix_icons()
    Gtk.Window.set_default_icon_list(pixbufs)
    # Load application icons.
    factory = Gtk.IconFactory()
    for filename, stockid in _icons:
        try:
            icon_data = pkgutil.get_data('mcomix', 'images/%s' % filename)
            pixbuf = image_tools.load_pixbuf_data(icon_data)
            iconset = Gtk.IconSet(pixbuf)
            factory.add(stockid, iconset)
        except Exception:
            log.warning(_('! Could not load icon "%s"'), filename)
    factory.add_default()


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704114443.0
mcomix-3.1.0/mcomix/image_handler.py0000644000175000017500000004237714544534413017152 0ustar00moritzmoritz"""image_handler.py - Image handler that takes care of cacheing and giving out images."""

import os
import traceback

from mcomix.preferences import prefs
from mcomix import i18n
from mcomix import tools
from mcomix import image_tools
from mcomix import thumbnail_tools
from mcomix import constants
from mcomix import callback
from mcomix import log
from mcomix.worker_thread import WorkerThread

class ImageHandler(object):

    """The FileHandler keeps track of images, pages, caches and reads files.

    When the Filehandler's methods refer to pages, they are indexed from 1,
    i.e. the first page is page 1 etc.

    Other modules should *never* read directly from the files pointed to by
    paths given by the FileHandler's methods. The files are not even
    guaranteed to exist at all times since the extraction of archives is
    threaded.
    """

    def __init__(self, window):

        #: Reference to main window
        self._window = window

        #: Caching thread
        self._thread = WorkerThread(self._cache_pixbuf, name='image',
                                    sort_orders=True)

        #: Archive path, if currently opened file is archive
        self._base_path = None
        #: List of image file names, either from extraction or directory
        self._image_files = None
        #: Index of current page
        self._current_image_index = None
        #: Set of images reading for decoding (i.e. already extracted)
        self._available_images = set()
        #: List of pixbufs we want to cache
        self._wanted_pixbufs = []
        #: Pixbuf map from page > Pixbuf
        self._raw_pixbufs = {}
        #: How many pages to keep in cache
        self._cache_pages = prefs['max pages to cache']

        self._window.filehandler.file_available += self._file_available

    def _get_pixbuf(self, index):
        """Return the pixbuf indexed by  from cache.
        Pixbufs not found in cache are fetched from disk first.
        """
        pixbuf = image_tools.MISSING_IMAGE_ICON

        if index not in self._raw_pixbufs:
            self._wait_on_page(index + 1)

            try:
                pixbuf = image_tools.load_pixbuf(self._image_files[index])
                self._raw_pixbufs[index] = pixbuf
                tools.garbage_collect()
            except Exception as e:
                self._raw_pixbufs[index] = image_tools.MISSING_IMAGE_ICON
                log.error('Could not load pixbuf for page %u: %r', index + 1, e)
        else:
            try:
                pixbuf = self._raw_pixbufs[index]
            except Exception:
                pass

        return pixbuf

    def get_pixbufs(self, number_of_bufs):
        """Returns number_of_bufs pixbufs for the image(s) that should be
        currently displayed. This method might fetch images from disk, so make
        sure that number_of_bufs is as small as possible.
        """
        result = []
        for i in range(number_of_bufs):
            result.append(self._get_pixbuf(self._current_image_index + i))
        return result

    def get_pixbuf_auto_background(self, number_of_bufs): # XXX limited to at most 2 pages
        """ Returns an automatically calculated background color
        for the current page(s). """

        pixbufs = self.get_pixbufs(number_of_bufs)

        if len(pixbufs) == 1:
            pixbufs[0] = self._window.enhancer.enhance(pixbufs[0])
            auto_bg = image_tools.get_most_common_edge_colour(pixbufs[0])
        elif len(pixbufs) == 2:
            left, right = pixbufs
            left = self._window.enhancer.enhance(left)
            right = self._window.enhancer.enhance(right)
            if self._window.is_manga_mode:
                left, right = right, left

            auto_bg = image_tools.get_most_common_edge_colour((left, right))
        else:
            assert False, 'Unexpected pixbuf count'

        return auto_bg

    def do_cacheing(self):
        """Make sure that the correct pixbufs are stored in cache. These
        are (in the current implementation) the current image(s), and
        if cacheing is enabled, also the one or two pixbufs before and
        after the current page. All other pixbufs are deleted and garbage
        collected directly in order to save memory.
        """
        if not self._window.filehandler.file_loaded:
            return

        # Flush caching orders.
        self._thread.clear_orders()
        # Get list of wanted pixbufs.
        wanted_pixbufs = self._ask_for_pages(self.get_current_page())
        if -1 != self._cache_pages:
            # We're not caching everything, remove old pixbufs.
            for index in set(self._raw_pixbufs) - set(wanted_pixbufs):
                del self._raw_pixbufs[index]
        log.debug('Caching page(s) %s', ' '.join([str(index + 1) for index in wanted_pixbufs]))
        self._wanted_pixbufs = wanted_pixbufs
        # Start caching available images not already in cache.
        wanted_pixbufs = [index for index in wanted_pixbufs
                          if index in self._available_images and not index in self._raw_pixbufs]
        orders = [(priority, index) for priority, index in enumerate(wanted_pixbufs)]
        if len(orders) > 0:
            self._thread.extend_orders(orders)

    def _cache_pixbuf(self, wanted):
        priority, index = wanted
        log.debug('Caching page %u', index + 1)
        self._get_pixbuf(index)

    def set_page(self, page_num):
        """Set up filehandler to the page .
        """
        assert 0 < page_num <= self.get_number_of_pages()
        self._current_image_index = page_num - 1
        self.do_cacheing()

    def get_virtual_double_page(self, page=None):
        """Return True if the current state warrants use of virtual
        double page mode (i.e. if double page mode is on, the corresponding
        preference is set, and one of the two images that should normally
        be displayed has a width that exceeds its height), or if currently
        on the first page.
        """
        if page == None:
            page = self.get_current_page()

        if (page == 1 and
            prefs['virtual double page for fitting images'] & constants.SHOW_DOUBLE_AS_ONE_TITLE and
            self._window.filehandler.archive_type is not None):
            return True

        if (not prefs['default double page'] or
            not prefs['virtual double page for fitting images'] & constants.SHOW_DOUBLE_AS_ONE_WIDE or
            page == self.get_number_of_pages()):
            return False

        for page in (page, page + 1):
            if not self.page_is_available(page):
                return False
            pixbuf = self._get_pixbuf(page - 1)
            width, height = pixbuf.get_width(), pixbuf.get_height()
            if prefs['auto rotate from exif']:
                rotation = image_tools.get_implied_rotation(pixbuf)
                if tools.rotation_swaps_axes(rotation):
                    width, height = height, width
            if width > height:
                return True

        return False

    def get_real_path(self):
        """Return the "real" path to the currently viewed file, i.e. the
        full path to the archive or the full path to the currently
        viewed image.
        """
        if self._window.filehandler.archive_type is not None:
            return self._window.filehandler.get_path_to_base()
        return self.get_path_to_page()

    def cleanup(self):
        """Run clean-up tasks. Should be called prior to exit."""

        self.first_wanted = 0
        self.last_wanted = 1

        self._thread.stop()
        self._base_path = None
        self._image_files = []
        self._current_image_index = None
        self._available_images.clear()
        self._raw_pixbufs.clear()
        self._cache_pages = prefs['max pages to cache']

    def page_is_available(self, page=None):
        """ Returns True if  is available and calls to get_pixbufs
        would not block. If  is None, the current page(s) are assumed. """

        if page is None:
            current_page = self.get_current_page()
            if not current_page:
                # Current 'book' has no page.
                return False
            index_list = [ current_page - 1 ]
            if self._window.displayed_double() and current_page < len(self._image_files):
                index_list.append(current_page)
        else:
            index_list = [ page - 1 ]

        for index in index_list:
            if not index in self._available_images:
                return False

        return True

    @callback.Callback
    def page_available(self, page):
        """ Called whenever a new page becomes available, i.e. the corresponding
        file has been extracted. """
        log.debug('Page %u is available', page)
        index = page - 1
        assert index not in self._available_images
        self._available_images.add(index)
        # Check if we need to cache it.
        priority = None
        if index in self._wanted_pixbufs:
            # In the list of wanted pixbufs.
            priority = self._wanted_pixbufs.index(index)
        elif -1 == self._cache_pages:
            # We're caching everything.
            priority = self.get_number_of_pages()
        if priority is not None:
            self._thread.append_order((priority, index))

    def _file_available(self, filepaths):
        """ Called by the filehandler when a new file becomes available. """
        # Find the page that corresponds to 
        if not self._image_files:
            return

        available = sorted(filepaths)
        for i, imgpath in enumerate(self._image_files):
            if tools.bin_search(available, imgpath) >= 0:
                self.page_available(i + 1)

    def get_number_of_pages(self):
        """Return the number of pages in the current archive/directory."""
        if self._image_files is not None:
            return len(self._image_files)
        else:
            return 0

    def get_current_page(self):
        """Return the current page number (starting from 1), or 0 if no file is loaded."""
        if self._current_image_index is not None:
            return self._current_image_index + 1
        else:
            return 0

    def get_path_to_page(self, page=None):
        """Return the full path to the image file for , or the current
        page if  is None.
        """
        if page is None:
            index = self._current_image_index
        else:
            index = page - 1

        if self._image_files and 0 <= index < len(self._image_files):
            return self._image_files[index]
        else:
            return None

    def get_page_filename(self, page=None, double=False):
        """Return the filename of the , or the filename of the
        currently viewed page if  is None. If  is True, return
        a tuple (p, p') where p is the filename of  (or the current
        page) and p' is the filename of the page after.
        """
        if page is None:
            page = self.get_current_page()

        first_path = self.get_path_to_page(page)
        if first_path == None:
            return None

        if double:
            second_path = self.get_path_to_page(page + 1)

            if second_path != None:
                first = os.path.basename(first_path)
                second = os.path.basename(second_path)
            else:
                return None

            return first, second

        return os.path.basename(first_path)

    def get_page_filesize(self, page=None, double=False):
        """Return the filesize of the , or the filesize of the
        currently viewed page if  is None. If  is True, return
        a tuple (s, s') where s is the filesize of  (or the current
        page) and s' is the filesize of the page after.
        """
        returnvalue_on_error = ('', '') if double else ''

        if not self.page_is_available():
            return returnvalue_on_error

        if page is None:
            page = self.get_current_page()

        first_path = self.get_path_to_page(page)
        if first_path == None:
            return returnvalue_on_error

        if double:
            second_path = self.get_path_to_page(page + 1)
            if second_path != None:
                try:
                    first = tools.format_byte_size(os.stat(first_path).st_size)
                except OSError:
                    first = ''
                try:
                    second = tools.format_byte_size(os.stat(second_path).st_size)
                except OSError:
                    second = ''
            else:
                return ('', '')
            return first, second

        try:
            size = tools.format_byte_size(os.stat(first_path).st_size)
        except OSError:
            size = ''

        return size

    def get_pretty_current_filename(self):
        """Return a string with the name of the currently viewed file that is
        suitable for printing.
        """
        if self._window.filehandler.archive_type is not None:
            name = os.path.basename(self._base_path)
        elif self._image_files:
            img_file = os.path.abspath(self._image_files[self._current_image_index])
            name = os.path.join(
                os.path.basename(os.path.dirname(img_file)),
                os.path.basename(img_file)
            )
        else:
            name = ''

        return i18n.to_unicode(name)

    def get_size(self, page=None):
        """Return a tuple (width, height) with the size of . If 
        is None, return the size of the current page.
        """
        self._wait_on_page(page)

        page_path = self.get_path_to_page(page)
        if page_path is None:
            return (0, 0)

        format, dimensions, providers = image_tools.get_image_info(page_path)
        return dimensions

    def get_mime_name(self, page=None):
        """Return a string with the name of the mime type of . If
         is None, return the mime type name of the current page.
        """
        self._wait_on_page(page)

        page_path = self.get_path_to_page(page)
        if page_path is None:
            return None

        format, dimensions, providers = image_tools.get_image_info(page_path)
        return format

    def get_thumbnail(self, page=None, width=128, height=128, create=False,
                      nowait=False):
        """Return a thumbnail pixbuf of  that fit in a box with
        dimensions x. Return a thumbnail for the current
        page if  is None.

        If  is True, and x <= 128x128, the
        thumbnail is also stored on disk.

        If  is True, don't wait for  to be available.
        """
        if not self._wait_on_page(page, check_only=nowait):
            # Page is not available!
            return None
        path = self.get_path_to_page(page)

        if path == None:
            return None

        try:
            thumbnailer = thumbnail_tools.Thumbnailer(store_on_disk=create,
                                                      size=(width, height))
            return thumbnailer.thumbnail(path)
        except Exception:
            log.debug("Failed to create thumbnail for image `%s':\n%s",
                      path, traceback.format_exc())
            return image_tools.MISSING_IMAGE_ICON

    def _wait_on_page(self, page, check_only=False):
        """Block the running (main) thread until the file corresponding to
        image  has been fully extracted.

        If  is True, only check (and return status), don't wait.
        """
        if page is None:
            index = self._current_image_index
        else:
            index = page - 1
        if index in self._available_images:
            # Already extracted!
            return True
        if check_only:
            # Asked for check only...
            return False

        log.debug('Waiting for page %u', page)
        path = self.get_path_to_page(page)
        self._window.filehandler._wait_on_file(path)
        return True

    def _ask_for_pages(self, page):
        """Ask for pages around  to be given priority extraction.
        """
        files = []
        if prefs['default double page']:
            page_width = 2
        else:
            page_width = 1
        if 0 == self._cache_pages:
            # Only ask for current page.
            num_pages = page_width
        elif -1 == self._cache_pages:
            # Ask for 10 pages.
            num_pages = min(10, self.get_number_of_pages())
        else:
            num_pages = self._cache_pages

        page_list = [page - 1 - page_width + n for n in range(num_pages)]

        # Current and next page first, followed by previous page.
        previous_page = page_list[0:page_width]
        del page_list[0:page_width]
        page_list[2*page_width:2*page_width] = previous_page
        page_list = [index for index in page_list
                     if index >= 0 and index < len(self._image_files)]

        log.debug('Ask for priority extraction around page %u: %s',
                  page, ' '.join([str(n + 1) for n in page_list]))

        for index in page_list:
            if index not in self._available_images:
                files.append(self._image_files[index])

        if len(files) > 0:
            self._window.filehandler._ask_for_files(files)

        return page_list

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1705863135.0
mcomix-3.1.0/mcomix/image_tools.py0000644000175000017500000006725714553263737016711 0ustar00moritzmoritz"""image_tools.py - Various image manipulations."""

import operator
from gi.repository import GLib, GdkPixbuf, Gdk, Gtk
import PIL
from PIL import Image
from PIL import ImageEnhance
from PIL import ImageOps
from io import StringIO

from mcomix.preferences import prefs
from mcomix import constants
from mcomix import log
from mcomix import tools
from mcomix.i18n import _

PIL_VERSION = ('Pillow', PIL.__version__)

# Unfortunately gdk_pixbuf_version is not exported, so show the GTK+ version instead.
log.info(f'GDK version: {GdkPixbuf.PIXBUF_VERSION}, GTK+: {Gtk.get_major_version()}.{Gtk.get_minor_version()}, GLib: {GLib.MAJOR_VERSION}.{GLib.MINOR_VERSION}')
log.info('PIL version: %s [%s]', PIL_VERSION[0], PIL_VERSION[1])

# Fallback pixbuf for missing images.
MISSING_IMAGE_ICON = None

_missing_icon_dialog = Gtk.Dialog()
_missing_icon_pixbuf = _missing_icon_dialog.render_icon(
        Gtk.STOCK_MISSING_IMAGE, Gtk.IconSize.LARGE_TOOLBAR)
MISSING_IMAGE_ICON = _missing_icon_pixbuf
assert MISSING_IMAGE_ICON

GTK_GDK_COLOR_BLACK = Gdk.color_parse('black')
GTK_GDK_COLOR_WHITE = Gdk.color_parse('white')


def axis_to_gdkpixbuf_flip_horizontal(i):
    return (True, False)[i]

def angle_to_gdkpixbuf_rotation(deg):
    if deg == 0:
        return GdkPixbuf.PixbufRotation.NONE
    elif deg == 90:
        return GdkPixbuf.PixbufRotation.CLOCKWISE
    elif deg == 180:
        return GdkPixbuf.PixbufRotation.UPSIDEDOWN
    elif deg == 270:
        return GdkPixbuf.PixbufRotation.COUNTERCLOCKWISE
    raise ValueError("illegal angle: " + str(deg))

def rotate_pixbuf(src, rotation):
    return src if rotation == 0 else src.rotate_simple(angle_to_gdkpixbuf_rotation(rotation))

def flip_pixbuf(src, axis):
    return src.flip(horizontal=axis_to_gdkpixbuf_flip_horizontal(axis))

def get_fitting_size(source_size, target_size,
                     keep_ratio=True, scale_up=False):
    """ Return a scaled version of 
    small enough to fit in .

    Both  and 
    must be (width, height) tuples.

    If  is True, aspect ratio is kept.

    If  is True,  is scaled up
    when smaller than .
    """
    width, height = target_size
    src_width, src_height = source_size
    if not scale_up and src_width <= width and src_height <= height:
        width, height = src_width, src_height
    else:
        if keep_ratio:
            if float(src_width) / width > float(src_height) / height:
                height = int(max(src_height * width / src_width, 1))
            else:
                width = int(max(src_width * height / src_height, 1))
    return (width, height)

def fit_pixbuf_to_rectangle(src, rect, rotation):
    return fit_in_rectangle(src, rect[0], rect[1],
                            rotation=rotation,
                            keep_ratio=False,
                            scale_up=True)

def fit_in_rectangle(src, width, height, keep_ratio=True, scale_up=False, rotation=0, scaling_quality=None):
    """Scale (and return) a pixbuf so that it fits in a rectangle with
    dimensions  x . A negative  or 
    means an unbounded dimension - both cannot be negative.

    If  is 90, 180 or 270 we rotate  first so that the
    rotated pixbuf is fitted in the rectangle.

    Unless  is True we don't stretch images smaller than the
    given rectangle.

    If  is True, the image ratio is kept, and the result
    dimensions may be smaller than the target dimensions.

    If  has an alpha channel it gets a checkboard background.
    """
    # "Unbounded" really means "bounded to RENDER_SIZE_LIMIT" - for simplicity.
    # MComix would probably choke on larger images anyway.
    if width < 0:
        width = constants.RENDER_SIZE_LIMIT
    elif height < 0:
        height = constants.RENDER_SIZE_LIMIT
    width = max(width, 1)
    height = max(height, 1)

    if tools.rotation_swaps_axes(rotation):
        width, height = height, width

    if scaling_quality is None:
        scaling_quality = prefs['scaling quality']

    src_width = src.get_width()
    src_height = src.get_height()

    width, height = get_fitting_size((src_width, src_height),
                                     (width, height),
                                     keep_ratio=keep_ratio,
                                     scale_up=scale_up)

    if src.get_has_alpha():
        composite_color_args = get_composite_color_args(0
            if prefs['checkered bg for transparent images'] else 1)
        if width == src_width and height == src_height:
            # Using anything other than nearest interpolation will result in a
            # modified image if no resizing takes place (even if it's opaque).
            scaling_quality = GdkPixbuf.InterpType.NEAREST
        src = src.composite_color_simple(width, height, scaling_quality,
                                         255, *composite_color_args)
    elif width != src_width or height != src_height:
        src = src.scale_simple(width, height, scaling_quality)

    src = rotate_pixbuf(src, rotation)

    return src


def add_border(pixbuf, thickness, colour=0x000000FF):
    """Return a pixbuf from  with a  px border of
     added.
    """
    canvas = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8,
        pixbuf.get_width() + thickness * 2,
        pixbuf.get_height() + thickness * 2)
    canvas.fill(colour)
    pixbuf.copy_area(0, 0, pixbuf.get_width(), pixbuf.get_height(),
        canvas, thickness, thickness)
    return canvas


def get_most_common_edge_colour(pixbufs, edge=2):
    """Return the most commonly occurring pixel value along the four edges
    of . The return value is a sequence, (r, g, b), with 16 bit
    values. If  is a tuple, the edges will be computed from
    both the left and the right image.

    Note: This could be done more cleanly with subpixbuf(), but that
    doesn't work as expected together with get_pixels().
    """

    def group_colors(colors, steps=10):
        """ This rounds a list of colors in C{colors} to the next nearest value,
        i.e. 128, 83, 10 becomes 130, 85, 10 with C{steps}=5. This compensates for
        dirty colors where no clear dominating color can be made out.

        @return: The color that appears most often in the prominent group."""

        # Start group
        group = (0, 0, 0)
        # List of (count, color) pairs, group contains most colors
        colors_in_prominent_group = []
        color_count_in_prominent_group = 0
        # List of (count, color) pairs, current color group
        colors_in_group = []
        color_count_in_group = 0

        for count, color in colors:

            # Round color
            rounded = [0] * len(color)
            for i, color_value in enumerate(color):
                if steps % 2 == 0:
                    middle = steps // 2
                else:
                    middle = steps // 2 + 1

                remainder = color_value % steps
                if remainder >= middle:
                    color_value = color_value + (steps - remainder)
                else:
                    color_value = color_value - remainder

                rounded[i] = min(255, max(0, color_value))

            # Change prominent group if necessary
            if rounded == group:
                # Color still fits in the previous color group
                colors_in_group.append((count, color))
                color_count_in_group += count
            else:
                # Color group changed, check if current group has more colors
                # than last group
                if color_count_in_group > color_count_in_prominent_group:
                    colors_in_prominent_group = colors_in_group
                    color_count_in_prominent_group = color_count_in_group

                group = rounded
                colors_in_group = [ (count, color) ]
                color_count_in_group = count

        # Cleanup if only one edge color group was found
        if color_count_in_group > color_count_in_prominent_group:
            colors_in_prominent_group = colors_in_group

        colors_in_prominent_group.sort(key=operator.itemgetter(0), reverse=True)
        # List is now sorted by color count, first color appears most often
        return colors_in_prominent_group[0][1]

    def get_edge_pixbuf(pixbuf, side, edge):
        """ Returns a pixbuf corresponding to the side passed in .
        Valid sides are 'left', 'right', 'top', 'bottom'. """
        pixbuf = static_image(pixbuf)
        width = pixbuf.get_width()
        height = pixbuf.get_height()
        edge = min(edge, width, height)

        subpix = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB,
                pixbuf.get_has_alpha(), 8, edge, height)
        if side == 'left':
            pixbuf.copy_area(0, 0, edge, height, subpix, 0, 0)
        elif side == 'right':
            pixbuf.copy_area(width - edge, 0, edge, height, subpix, 0, 0)
        elif side == 'top':
            pixbuf.copy_area(0, 0, width, edge, subpix, 0, 0)
        elif side == 'bottom':
            pixbuf.copy_area(0, height - edge, width, edge, subpix, 0, 0)
        else:
            assert False, 'Invalid edge side'

        return subpix

    if not pixbufs:
        return (0, 0, 0)

    if not isinstance(pixbufs, (tuple, list)):
        left_edge = get_edge_pixbuf(pixbufs, 'left', edge)
        right_edge = get_edge_pixbuf(pixbufs, 'right', edge)
    else:
        assert len(pixbufs) == 2, 'Expected two pages in list'
        left_edge = get_edge_pixbuf(pixbufs[0], 'left', edge)
        right_edge = get_edge_pixbuf(pixbufs[1], 'right', edge)

    # Find all edge colors. Color count is separate for all four edges
    ungrouped_colors = []
    for edge in (left_edge, right_edge):
        im = pixbuf_to_pil(edge)
        ungrouped_colors.extend(im.getcolors(im.size[0] * im.size[1]))

    # Sum up colors from all edges
    ungrouped_colors.sort(key=operator.itemgetter(1))
    most_used = group_colors(ungrouped_colors)[:3]
    return [color * 257 for color in most_used]

def pil_to_pixbuf(im, keep_orientation=False):
    """Return a pixbuf created from the PIL ."""
    if im.mode.startswith('RGB'):
        has_alpha = im.mode == 'RGBA'
    elif im.mode in ('LA', 'P'):
        has_alpha = True
    else:
        has_alpha = False
    target_mode = 'RGBA' if has_alpha else 'RGB'
    if im.mode != target_mode:
        im = im.convert(target_mode)
    pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(
        GLib.Bytes.new(im.tobytes()), GdkPixbuf.Colorspace.RGB,
        has_alpha, 8,
        im.size[0], im.size[1],
        (4 if has_alpha else 3) * im.size[0]
    )
    if keep_orientation:
        # Keep orientation metadata.
        orientation = None
        exif = im.getexif()
        orientation = exif.get(274, None)
        if orientation is None:
            # Maybe it's a PNG? Try alternative method.
            orientation = _get_png_implied_rotation(im)
        if orientation is not None:
            setattr(pixbuf, 'orientation', str(orientation))
    return pixbuf

def pixbuf_to_pil(pixbuf):
    """Return a PIL image created from ."""
    dimensions = pixbuf.get_width(), pixbuf.get_height()
    stride = pixbuf.get_rowstride()
    pixels = pixbuf.get_pixels()
    mode = 'RGBA' if pixbuf.get_has_alpha() else 'RGB'
    im = Image.frombuffer(mode, dimensions, pixels, 'raw', mode, stride, 1)
    return im

def is_animation(pixbuf):
    return isinstance(pixbuf, GdkPixbuf.PixbufAnimation)

def static_image(pixbuf):
    """ Returns a non-animated version of the specified pixbuf. """
    if is_animation(pixbuf):
        return pixbuf.get_static_image()
    return pixbuf

def unwrap_image(image):
    """ Returns an object that contains the image data based on
    Gtk.Image.get_storage_type or None if image is None or image.get_storage_type
    returns Gtk.ImageType.EMPTY. """
    if image is None:
        return None
    t = image.get_storage_type()
    if t == Gtk.ImageType.EMPTY:
        return None
    if t == Gtk.ImageType.PIXBUF:
        return image.get_pixbuf()
    if t == Gtk.ImageType.ANIMATION:
        return image.get_animation()
    if t == Gtk.ImageType.PIXMAP:
        return image.get_pixmap()
    if t == Gtk.ImageType.IMAGE:
        return image.get_image()
    if t == Gtk.ImageType.STOCK:
        return image.get_stock()
    if t == Gtk.ImageType.ICON_SET:
        return image.get_icon_set()
    raise ValueError()

def set_from_pixbuf(image, pixbuf):
    if is_animation(pixbuf):
        return image.set_from_animation(pixbuf)
    else:
        return image.set_from_pixbuf(pixbuf)

def load_pixbuf(path):
    """ Loads a pixbuf from a given image file. """
    pixbuf = None
    last_error = None
    providers = get_image_info(path)[2]
    for provider in providers:
        try:
            # TODO use dynamic dispatch instead of "if" chain
            if provider == constants.IMAGEIO_GDKPIXBUF:
                if prefs['animation mode'] != constants.ANIMATION_DISABLED:
                    try:
                        pixbuf = GdkPixbuf.PixbufAnimation.new_from_file(path)
                        if pixbuf.is_static_image():
                            pixbuf = pixbuf.get_static_image()
                    except GLib.GError:
                        # NOTE: Broken JPEGs sometimes result in this exception.
                        # However, one may be able to load them using
                        # Gdk.pixbuf_new_from_file, so we need to continue.
                        pass
                if pixbuf is None:
                    pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
            elif provider == constants.IMAGEIO_PIL:
                # TODO When using PIL, whether or how animations work is
                # currently undefined.
                im = Image.open(path)
                pixbuf = pil_to_pixbuf(im, keep_orientation=True)
            else:
                raise TypeError()
        except Exception as e:
            # current provider could not load image
            last_error = e
        if pixbuf is not None:
            # stop loop on success
            log.debug("provider %s succeeded in loading %s", provider, path)
            break
        log.debug("provider %s failed to load %s", provider, path)
    if pixbuf is None:
        # raising necessary because caller expects pixbuf to be not None
        raise last_error or TypeError()
    return pixbuf

def load_pixbuf_size(path, width, height):
    """ Loads a pixbuf from a given image file and scale it to fit
    inside (width, height). """
    # TODO similar to load_pixbuf, should be merged using callbacks etc.
    pixbuf = None
    last_error = None
    image_format, image_dimensions, providers = get_image_info(path)
    for provider in providers:
        try:
            # TODO use dynamic dispatch instead of "if" chain
            if provider == constants.IMAGEIO_GDKPIXBUF:
                # If we could not get the image info, still try to load
                # the image to let GdkPixbuf raise the appropriate exception.
                if (0, 0) == image_dimensions:
                    pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
                # Work around GdkPixbuf bug: https://bugzilla.gnome.org/show_bug.cgi?id=735422
                # (currently https://gitlab.gnome.org/GNOME/gdk-pixbuf/issues/45)
                elif 'GIF' == image_format:
                    pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
                else:
                    # Don't upscale if smaller than target dimensions!
                    image_width, image_height = image_dimensions
                    if image_width <= width and image_height <= height:
                        width, height = image_width, image_height
                    pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(path, width, height)
            elif provider == constants.IMAGEIO_PIL:
                im = Image.open(path)
                im.draft(None, (width, height))
                pixbuf = pil_to_pixbuf(im, keep_orientation=True)
            else:
                raise TypeError()
        except Exception as e:
            # current provider could not load image
            last_error = e
        if pixbuf is not None:
            # stop loop on success
            log.debug("provider %s succeeded in loading %s at size %s", provider, path, (width, height))
            break
        log.debug("provider %s failed to load %s at size %s", provider, path, (width, height))
    if pixbuf is None:
        # raising necessary because caller expects pixbuf to be not None
        raise last_error or TypeError()
    return fit_in_rectangle(pixbuf, width, height, GdkPixbuf.InterpType.BILINEAR)

def load_pixbuf_data(imgdata):
    """ Loads a pixbuf from the data passed in . """
    # TODO similar to load_pixbuf, should be merged using callbacks etc.
    pixbuf = None
    last_error = None
    for provider in (constants.IMAGEIO_GDKPIXBUF, constants.IMAGEIO_PIL):
        try:
            # TODO use dynamic dispatch instead of "if" chain
            if provider == constants.IMAGEIO_GDKPIXBUF:
                loader = GdkPixbuf.PixbufLoader()
                loader.write(imgdata)
                loader.close()
                pixbuf = loader.get_pixbuf()
            elif provider == constants.IMAGEIO_PIL:
                pixbuf = pil_to_pixbuf(Image.open(StringIO(imgdata)), keep_orientation=True)
            else:
                raise TypeError()
        except Exception as e:
            # current provider could not load image
            last_error = e
        if pixbuf is not None:
            # stop loop on success
            log.debug("provider %s succeeded in decoding %s bytes", provider, len(imgdata))
            break
        log.debug("provider %s failed to decode %s bytes", provider, len(imgdata))
    if pixbuf is None:
        # raising necessary because caller expects pixbuf to be not None
        raise last_error
    return pixbuf

def enhance(pixbuf, brightness=1.0, contrast=1.0, saturation=1.0,
  sharpness=1.0, autocontrast=False, invert_color=False):
    """Return a modified pixbuf from  where the enhancement operations
    corresponding to each argument has been performed. A value of 1.0 means
    no change. If  is True it overrides the  value,
    but only if the image mode is supported by ImageOps.autocontrast (i.e.
    it is L or RGB.)
    """
    im = pixbuf_to_pil(pixbuf)
    if brightness != 1.0:
        im = ImageEnhance.Brightness(im).enhance(brightness)
    if autocontrast and im.mode in ('L', 'RGB'):
        im = ImageOps.autocontrast(im, cutoff=0.1)
    elif contrast != 1.0:
        im = ImageEnhance.Contrast(im).enhance(contrast)
    if saturation != 1.0:
        im = ImageEnhance.Color(im).enhance(saturation)
    if sharpness != 1.0:
        im = ImageEnhance.Sharpness(im).enhance(sharpness)
    if invert_color:
        im = ImageOps.invert(im)
    return pil_to_pixbuf(im)

def _get_png_implied_rotation(pixbuf_or_image):
    """Same as  for PNG files.

    Lookup for Exif data in the tEXt chunk.
    """
    if isinstance(pixbuf_or_image, GdkPixbuf.Pixbuf):
        raw_exif = pixbuf_or_image.get_option('tEXt::Raw profile type exif')
    elif isinstance(pixbuf_or_image, Image.Image):
        raw_exif = pixbuf_or_image.info.get('Raw profile type exif')
    else:
        raise ValueError()
    if raw_exif is None:
        return None
    exif_lines = raw_exif.split('\n')
    if len(exif_lines) < 4 or 'exif' != exif_lines[1]:
        # Not valid Exif data.
        return None
    size = int(exif_lines[2])
    try:
        data = bytes.fromhex(''.join(exif_lines[3:]))
    except ValueError:
        # Not valid hexadecimal content.
        return None
    if size != len(data):
        # Sizes should match.
        return None
    exif = Image.Exif()
    exif.load(data)
    orientation = exif.get(274, None) # Orientation tag
    if orientation is not None:
        orientation = str(orientation)
    return orientation

def get_implied_rotation(pixbuf):
    """Return the implied rotation in degrees: 0, 90, 180, or 270.

    The implied rotation is the angle (in degrees) that the raw pixbuf should
    be rotated in order to be displayed "correctly". E.g. a photograph taken
    by a camera that is held sideways might store this fact in its Exif data,
    and the pixbuf loader will set the orientation option correspondingly.
    """
    pixbuf = static_image(pixbuf)
    orientation = getattr(pixbuf, 'orientation', None)
    if orientation is None:
        orientation = pixbuf.get_option('orientation')
    if orientation is None:
        # Maybe it's a PNG? Try alternative method.
        orientation = _get_png_implied_rotation(pixbuf)
    if orientation == '3':
        return 180
    elif orientation == '6':
        return 90
    elif orientation == '8':
        return 270
    return 0


def get_size_rotation(width, height):
    """ Determines the rotation to be applied.
    Returns the degree of rotation (0, 90, 180, 270). """

    if width != height:
        arp = prefs['auto rotate depending on size']
        if height > width:
            if arp == constants.AUTOROTATE_HEIGHT_90:
                return 90
            elif arp == constants.AUTOROTATE_HEIGHT_270:
                return 270
        else: # width > height
            if arp == constants.AUTOROTATE_WIDTH_90:
                return 90
            elif arp == constants.AUTOROTATE_WIDTH_270:
                return 270
    return 0

def combine_pixbufs( pixbuf1, pixbuf2, are_in_manga_mode ):
    if are_in_manga_mode:
        r_source_pixbuf = pixbuf1
        l_source_pixbuf = pixbuf2
    else:
        l_source_pixbuf = pixbuf1
        r_source_pixbuf = pixbuf2

    has_alpha = False

    if l_source_pixbuf.get_property( 'has-alpha' ) or \
       r_source_pixbuf.get_property( 'has-alpha' ):
        has_alpha = True

    bits_per_sample = 8

    l_source_pixbuf_width = l_source_pixbuf.get_property( 'width' )
    r_source_pixbuf_width = r_source_pixbuf.get_property( 'width' )

    l_source_pixbuf_height = l_source_pixbuf.get_property( 'height' )
    r_source_pixbuf_height = r_source_pixbuf.get_property( 'height' )

    new_width = l_source_pixbuf_width + r_source_pixbuf_width

    new_height = max( l_source_pixbuf_height, r_source_pixbuf_height )

    new_pix_buf = GdkPixbuf.Pixbuf.new(colorspace=GdkPixbuf.Colorspace.RGB,
                                       has_alpha=has_alpha,
                                       bits_per_sample=bits_per_sample,
                                       width=new_width, height=new_height)

    l_source_pixbuf.copy_area( 0, 0, l_source_pixbuf_width,
                                     l_source_pixbuf_height,
                                     new_pix_buf, 0, 0 )

    r_source_pixbuf.copy_area( 0, 0, r_source_pixbuf_width,
                                     r_source_pixbuf_height,
                                     new_pix_buf, l_source_pixbuf_width, 0 )

    return new_pix_buf

def is_image_file(path):
    """Return True if the file at  is an image file recognized by PyGTK.
    """
    return _SUPPORTED_IMAGE_REGEX.search(path) is not None

def convert_rgb16list_to_rgba8int(c):
    return 0x000000FF | (c[0] >> 8 << 24) | (c[1] >> 8 << 16) | (c[2] >> 8 << 8)

def rgb_to_y_601(color):
    return color[0] * 0.299 + color[1] * 0.587 + color[2] * 0.114

def text_color_for_background_color(bgcolor):
    return GTK_GDK_COLOR_BLACK if rgb_to_y_601(bgcolor) >= \
        65535.0 / 2.0 else GTK_GDK_COLOR_WHITE

def color_to_floats_rgba(color, alpha=1.0):
    return [c / 65535.0 for c in color[:3]] + [alpha]

def get_composite_color_args(variant):
    return ((8, 0x777777, 0x999999), (1024, 0xFFFFFF, 0xFFFFFF))[variant]

def get_image_info(path):
    """Return information about and select preferred providers for loading
    the image specified by C{path}. The result is a tuple
    C{(format, (width, height), providers)}.
    """
    image_format = None
    image_dimensions = None
    providers = ()
    try:
        gdk_image_info = GdkPixbuf.Pixbuf.get_file_info(path)
    except Exception:
        gdk_image_info = None

    if gdk_image_info is not None and gdk_image_info[0] is not None:
        image_format = gdk_image_info[0].get_name().upper()
        image_dimensions = gdk_image_info[1], gdk_image_info[2]
        # Prefer loading via GDK/Pixbuf if Gdk.pixbuf_get_file_info appears
        # to be able to handle this path.
        providers = (constants.IMAGEIO_GDKPIXBUF, constants.IMAGEIO_PIL)
    else:
        try:
            im = Image.open(path)
            image_format = im.format
            image_dimensions = im.size
            providers = (constants.IMAGEIO_PIL, constants.IMAGEIO_GDKPIXBUF)
        except IOError:
            # If the file cannot be found, or the image
            # cannot be opened and identified.
            pass
    if image_format is None:
        image_format = _('Unknown filetype')
        image_dimensions = (0, 0)
    return (image_format, image_dimensions, providers)

def get_supported_formats():
    global _SUPPORTED_IMAGE_FORMATS
    if _SUPPORTED_IMAGE_FORMATS is None:

        # Step 1: Collect PIL formats
        # Make sure all supported formats are registered.
        Image.init()
        # Not all PIL formats register a mime type,
        # fill in the blanks ourselves.
        supported_formats_pil = {
            'BMP': (['image/bmp', 'image/x-bmp', 'image/x-MS-bmp'], []),
            'ICO': (['image/x-icon', 'image/x-ico', 'image/x-win-bitmap'], []),
            'PCX': (['image/x-pcx'], []),
            'PPM': (['image/x-portable-pixmap'], []),
            'TGA': (['image/x-tga'], []),
        }
        for name, mime in list(Image.MIME.items()):
            mime_types, extensions = supported_formats_pil.get(name, ([], []))
            supported_formats_pil[name] = mime_types + [mime], extensions
        for ext, name in list(Image.EXTENSION.items()):
            assert '.' == ext[0]
            mime_types, extensions = supported_formats_pil.get(name, ([], []))
            supported_formats_pil[name] = mime_types, extensions + [ext[1:]]
        # Remove formats with no mime type or extension.
        for name in list(supported_formats_pil.keys()):
            mime_types, extensions = supported_formats_pil[name]
            if not mime_types or not extensions:
                del supported_formats_pil[name]
        # Remove archives/videos formats.
        for name in (
            'MPEG',
            'PDF',
        ):
            if name in supported_formats_pil:
                del supported_formats_pil[name]

        # Step 2: Collect GDK Pixbuf formats
        supported_formats_gdk = {}
        for format in GdkPixbuf.Pixbuf.get_formats():
            name = format.get_name().upper()
            assert name not in supported_formats_gdk
            supported_formats_gdk[name] = (
                format.get_mime_types(),
                format.get_extensions(),
            )

        # Step 3: merge format collections
        supported_formats = {}
        for provider in (supported_formats_gdk, supported_formats_pil):
            for name in list(provider.keys()):
                mime_types, extentions = provider[name]
                new_name = name.upper()
                new_mime_types, new_extensions = supported_formats.get( \
                    new_name, (set(), set()))
                new_mime_types.update([x.lower() for x in mime_types])
                new_extensions.update([x.lower() for x in extentions])
                supported_formats[new_name] = (new_mime_types, new_extensions)

        _SUPPORTED_IMAGE_FORMATS = supported_formats
    return _SUPPORTED_IMAGE_FORMATS

_SUPPORTED_IMAGE_FORMATS = None
# Set supported image extensions regexp from list of supported formats.
# Only used internally.
_SUPPORTED_IMAGE_REGEX = tools.formats_to_regex(get_supported_formats())
log.debug("_SUPPORTED_IMAGE_REGEX='%s'", _SUPPORTED_IMAGE_REGEX.pattern)

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/images/0000755000175000017500000000000014553265237015256 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/__init__.py0000644000175000017500000000000014476523373017357 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/comments.png0000644000175000017500000000105414476523373017613 0ustar00moritzmoritzPNG


IHDRabKGD	pHYstIME	4'	MIDAT8˵KSq?ǝFs?&!RK	zWiBB*'Bэy4Wb:ˣo7KƱ$|.<d$]S+Ng5H=8؋,h:`vi&y_.7Z;,Mhi4W:/S`4(YPS*mUڰIl/[OXO\ѠrJhzͨ.o	~ؠZLylHͅQǥY(Ao?X͔!!3b`";%sكI(ս<	@	zkf慮k"2L̅bfY9O&rQ?kiܺ7C3%<~D`Ms98]׀	-Y<ߔ1z~IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/double-page.png0000644000175000017500000000115614476523373020155 0ustar00moritzmoritzPNG


IHDRw=sRGBbKGD4e2E	pHYstIME-m=tEXtCommentCreated with The GIMPd%nIDATHՕ=kQ{g!ڈ,Y҈`i
6
!肕l,үllKMfgN4}_w{9s7;w
"v^]q5NϞ{WI\~~l}}LDK";?OAz.7yfFH0%L$ӂ[OsZ>Ʌ=$P8bk1H$P
J:303Ós_Y}׾''l:=(y
TG*5HYDV;2RV#5EH
2`6bG=3pMD%yݿPATpM2j6\J`7`㧕hb撙UrpΉ|+ٛ Sizh^|$/Q
_	~g@IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/fitbest.png0000644000175000017500000000133714476523373017432 0ustar00moritzmoritzPNG


IHDRw=sRGBbKGD4e2E	pHYstIME,,,tEXtCommentCreated with The GIMPd%n6IDATHՕkSQ?|H$tS#@-ABV4("-%&\G%yIk9~=	77Z*E] r;_R~;)PZjZM˱Qw~hʇV7Bp=E̥$/<==2·`&ܱ0yU>{IT҃Ta?6}ALҝ,NٚZ:ԣ@zv|Ak>\uŁAsM(;!woPq\V!@rZ$1Hg1UZQVt%%ۨ7ޖ6KcƘJiRe?ps@$'bѨ,˿?@=ƞk~8K{č<^t8RouL>zHb&n,EɈ%OУo.	Vh6NDZ0MƥHrYW$ '=):i_6_,Lz؁II}~=wG/-Wjkn6;vREi	SK{=pc
J0ЮtJeN9)%5Ez/4ws"RHA)e5_n='pb*R[/70q2U`dJ>(@PIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/fitmanual.png0000644000175000017500000000077514476523373017757 0ustar00moritzmoritzPNG


IHDRw=sRGBbKGD4e2E	pHYstIME+2cV&}IDATHǵ.DAg\JЈlBPPh6J
ށ;h@DCD#JkQج%K;4}L|`fm0FmL	‡Ojbd#^zx:=-@|MquL'/Sl
)N|#HظxSl`}~ M;]4I[1H4ڔz=PU
"/c/tpD޻2#Ҁ
"\(=ЀA(_\y@1F ""BQdq!"±lԪ9LTyGPcEf98;?SB"wVkҾڟNeJ~\X$56m-!LXIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/fitsize.png0000644000175000017500000000121114476523373017436 0ustar00moritzmoritzPNG


IHDRw=sRGBbKGD	pHYsIDATHՕ1hQݝwBu	Xp	ݴH #]UD:tqPu)TS$j	R,tqSmCҦI{;!Nd35ZCI*E] f7 ;ueM?bZj?@{69/M=cK&R޶ܰ;`;3Egr4hMYuoɡxgzLt?.u;M5b1??e6a\f~th2+//P0y6O	7i"ׁL"CX[)/r*@T fb
f/UG!Q^*.vQw{c0]ȘF}BFHj˲QZ!hҖj%JKZ,f#C
JO[+V*ZK$yz;}\lT%b;k$GMxWy>#fxtIKZeid%z4ȒawП3Cp%c/a,'cIRf*J&A./v͔`^zƴb0x/KәK@Lq۱X,+
l~v02$IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/gimp-flip-horizontal.png0000644000175000017500000000064214476523373022043 0ustar00moritzmoritzPNG


IHDRabKGDC	pHYs~tIME	(,/IDAT8Փ?K@I dk=K 4nukQH3n:(P[Ul̕"zps{ރ>%thHGY()満Y]?j XZKxFtLҩ.2̍eqBé^_ftײ}M%⎃8at%"N/lNefb``@tAYSOөQ?ej0Q.]%UJ>~u>~m:EIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/gimp-flip-vertical.png0000644000175000017500000000067614476523373021472 0ustar00moritzmoritzPNG


IHDRabKGDC	pHYs~tIME&SuKIDAT8˭JQ?k4`v
l]>@Q4szp!Y
AAALZ!Sش*c]s*iZ"	"PJ{M*rրz%&20-ie\mG29ޕ5,W7Đ$	gs3trc)(ޜR8>gjLS
[6QsҏhLk#I~)^^#8xQ"o$h,W~q!Yq|,Bv.?2ǴĆi!2-{*`"v_Rr2^sW]IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/gimp-rotate-180.png0000644000175000017500000000107414476523373020526 0ustar00moritzmoritzPNG


IHDRabKGD	pHYstIME
`XIDAT8˅kQ?W_BHneE<xYO7!C{BQZbI؍FIyl:`2̼51v
 5˅^7/9#lw@2Ӿ{`*`.y+ȜlTlmoZzBlҹ
SR1p{3ӯ͓|.%d`ٮlWf.`ٮTSXdƭXC!"̵<(	H'ŭz	n\|;ofj沘8暔ٖe(ݠ'{a?:<8)eWoChH_t2td<>v"e9[
sK*"`H# Fo~	TʗɹjEWxgRQFRNzIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/gimp-rotate-270.png0000644000175000017500000000066314476523373020531 0ustar00moritzmoritzPNG


IHDRabKGD	pHYstIMEݶ)a@IDAT8œK@?UKVWԺd,drop3;9U2JGAGBVjV[%~;J4V?`c}332[}ߓ1Ňv.}
1RܸBLDq51P4%i9R!OlK]&jf#j!EҀɹNNL&cp5z{yq>UI3k[ȽkiZ]*`:6.i{wQ*
QĸbIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/gimp-rotate-90.png0000644000175000017500000000072114476523373020444 0ustar00moritzmoritzPNG


IHDRabKGD	pHYstIME	^IDAT8œK@?Ŷ[("	'\*ѩdqSA *$jնRk$	[{>{࿇N8 5j僙*OM㕋z8oAa6icY,$(lKR6Mɤ8nE3F ]hUݻ*,~8@sQ+!BݥXj2i#ehUyV?l?ns<]ņߝY+Uuֽrr(Hi?^GQ|U!5$=;ˇ/ |QIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/gimp-thumbnails.png0000644000175000017500000000071014476523373021064 0ustar00moritzmoritzPNG


IHDRabKGDC	pHYstIME $PEUIDAT8˕1O@-5Mc08_:bt3ƭ09.@BBq1i`a1:ܹ^w/{S?`czk5L擂67LG*(_Z
r\YX0u~[Hm|u	VpQe(]LlRBѲC,xw;3梇޳pQcqc. $ 	O
q
'g1s.~}H)6|VG\|R_-r6%0 Yc,QFfa_ЙWɰGIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/gimp-transform.png0000644000175000017500000000110514476523373020730 0ustar00moritzmoritzPNG


IHDRabKGD]]]H~ 	pHYstIME;}9IDAT8ˍMkSA93s)	iJ&ڊnkwݸ	ş ݨ]KAEt[vFE.(6565uѤֳ<9أb3A`DZZQR^`0_c87`Ky e&ݨEaP٨9F
#+5-R(.d7}`[g/jZX-FF7jӈR-;L\7l)߽9
aOkhf`````+#s%]?~>{>?s&
M/윬RW.=}%7_>x&(MW_XeIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/magnifyingglass.png0000644000175000017500000000233214476523373021150 0ustar00moritzmoritzPNG


IHDRw=sRGBbKGD	pHYs

oytIME	8X
ZIDATH_LWǿki;~RVD_4N14c&d/̶eò4[Әm{pU&6	sR(~w/ta=$'ܛ9s	BƏ9G8@:)0C\׀'>Io֭۱0s!"nݾ١v#GK	ZWW_PTTGCW(㙵.ϜpyKMMMM\B +'6Fu[{frƒKw.FLf+/a$
cGw}*Wj+ڒQ(VhpzvB'
'U
XOQq-H(
dY֥ -r͆aV!PRRQO4pT.0`x!bey.%^;7ZVsl41.±E Q轾mW%	,;SF9<;쩴0UɟWMAI'P7Td"?O)²scGMw֜l!YvlȲ*;7èBR.Tײm'?r/k}
q{3+hڰ6+TcjM##pTTVp,1eo{{)͢Ʃ_jlEI17ǵ;6L&q##)MSh>v+gID,ɝnߌ۠]9x-Q0tt1il?帒1@766>i2^ڿ5%׋E?e|H`_˾~xxxlɾoliyy(.
vס/Gd)rp@JVs9nnx{׬	@z{{1b1PǔK*_J$tY&f3.{C__ 
@%+o2.CG첲|р+IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/manga.png0000644000175000017500000000170114476523373017050 0ustar00moritzmoritzPNG


IHDRw=sRGBbKGD4e2E	pHYstIME-tEXtCommentCreated with The GIMPd%nIDATHՕAh\U潾;T)kv 5TaEuKi*]L	q HwJ$ݹPD*ՙ1B
$4o޽L2󒙸H6^8{ι\ރRL,0)Ƹj)_.V>fXɳO=ᦎ=e=#Y>3		mܓ+S̞|I
{ulvnQm/
³'Nex
hu
k_C4bYΌ}'𙉳^l{x."4`v*0&Ȇ];[1FLGS!-2F"	4jyP#&83wǿޞ6Vt+X?-m껶bv+rh`<@:xN$8j="
/W=i7F1i,f=Y5zTdY./wR I2Ɯ|uJo .fM_GC;gP͝R[CTAAYʣ0'ܹU
2)+pӈz(.&6Tee[{Nޟ8, ?XGNKԁc96'z4b֭-s#5o>*"C# Q
svWBX7,Jaf;gkmOHI y5Wl7!fcM
r5JNFWɮ87NKF5~j}P߃Ԃ}IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701597438.0
mcomix-3.1.0/mcomix/images/mcomix-16.png0000644000175000017500000000133114533050376017474 0ustar00moritzmoritzPNG


IHDRk=	pHYsq6tEXtSoftwarewww.inkscape.org<fIDAT(uKTa}/31q|JzY ZD-(Z&APjQ$-SRLqQtΝǝ{6
v~́swN[}y#?ltxՓRIJ*˂z~.=86Rpʑ=B0jsuä+ugkisOXc! XLsQ'}&2v9r̎=Cۃ10IQzMu%HDe
YO`cualz~y#'Jrq29мbP{&0LU^0clo
9st:;TB$p3P$8A6R.0H)# aDqq7Df"O"̂2&
D.~Lk
j
j+=(TDJ^b$ʠ'Ux]y׏D7mHX֩ǗI8}e"<5EE_fn}}RS1-*C+q@>o}xGB}tVKYnfʵx5=uwx)06'GCIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701597438.0
mcomix-3.1.0/mcomix/images/mcomix-22.png0000644000175000017500000000215514533050376017476 0ustar00moritzmoritzPNG


IHDRٱ\	pHYs5tEXtSoftwarewww.inkscape.org<IDAT8[lTU}̙˙3tʴ0)r
X4K H@
^_KB1/>hLPQ4!QSi)ZKN9sf9ۇdg=kRr;B,j_6V`!+F5Z1<#\ѕJ᭪!-5.v%+/҉)-,:A#þNRQ$68BUHc<?9=/bkSE	gzr'Lj
N;li:"|Mr=[iޝZ~@˥"!/ZZTʅi9Xx~i;֏cT-<_hlR&Lhz/673`I1"aYU^J;׾{hQ}&-_P3s찿ާ%P#/:o/!KTIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701597438.0
mcomix-3.1.0/mcomix/images/mcomix-256.png0000644000175000017500000010005314533050376017563 0ustar00moritzmoritzPNG


IHDRf	pHYsփ	tEXtSoftwarewww.inkscape.org< IDATxwsw{
i@==!rS)ޛAB衄1c{z]o?Irήm>HFG)%}'}?>ykD1Mt^#}ΑBL=qBBB!B7&b"o}g8CR%^!FH);߲R4C_)~G)CJý\O޶GH);7H)gQfXopx
$X~PRv(˹Rrm,}!ӀS7oÀ(?W:9CJАU?B%FF@F;OS	@
ոe1uf<^:(CB)6)M@};#d(w!*RG/i;F	FB=gO!8
`pu	G{p
-8v,`@=sv[Rʺr}H"nuہM8[RNOvG!qvoDZ/,tK),IT8+
Lޒ4#1b`9ˋ,-Y))LN9柲ڻ2R"%ttѝil뤱ƶN;}ktMOOH){rs|#!*WgI1yD&Ϥ8_Vtѝew}[4
lnMIRn=h쓼һg?[B0qx%3豃9z`F,+(]ܾ|k-wKY,i}2'HSYeE̞8ﭰڒ`MK6V>NCG}6V-!ghe)g=QcaVdLw7Β7ta&]TM*[xv]m,>9xGo!J^^\9q9z^^JIajnΦ6[:hk颣,v6w}RtEqI(WLIe)J),/p`%V[GGl>
Ϥz>K ;
`~Ĩ\t$=f<%ӵ3tйg/-hmf_m΃w<

SZʀOɰ!
T3Rm"88GA8p~
S9kN=#䕞m254oE=DV9zk&*m1lT?LQ@%Էtp
<ZC''Oz+}pEq#`_VT%'Nና2"1/BúMo͞~n:cE4G.)M3bN䉈Ҝ[kYl7wၑ>8@"%i.8n"<('Niufjj>l[iQ@OR{	ٷG!BpN,'OMfԠ:ٶvXÖ7妇>u7C$"HPɠgc̣	ZA{W{_CWF3~طGw'OpК8.bm߳7Vmn;/qk-~riǒ@oo0c#Ѫ2_z
rM>'@Eq<'`NY\}\ٶ={޲M7q׮qf{η$9/z@V`aYµUרihS;pIa%}!8g}e?k|Ry_wc_y-+2{@j#=9MM@G"fcs)B98o
yȅ<Җ.]֒Q	Eap3Zx)1"\X20~$ʹ'3p1H+-%H!ǁEOgST^Q[dž'QW8n]ߏx{47d"$e18
cK6Çˊ|oPX Bm| m&g3Y,a-<zDA/Cÿ2{"ňSO@ˆ}|O/=K.#;i*jk-z|BP"~En"lnկr@1ϝCر<7u{hvN|)>PD1xc3<-|̖/g	~
^f??T4t"R:/01:NDۖR`^O!PIcU'ٶ6=Us=Dljv0CO(L=??{q7t2iJ(>gg8L\1S!(iepiG	M͚Gҕ_u[x"^#ywڼ.{2$#keSP$d4ڀ_\fys)8!:/7@=TJDβ8OB1DCgY
V?J3cBq4 ៗI92Yȡ
C!`ډ>0m"'.R>gv&W\g3O1NP5.ب\Uz}@D:Axb3Iv|d;p"`H]<OZZzHp"8Kp_EߐY5ա	:CnSՏ tG>+y(U'(Z{"2!n*"Tg~O$̗R܃":,IBrw{p<jv4C&1~0	!!G^$F`_?*33(OᮯǍ~.ϫ	8]J,9Gu>Y#x]mx/
'x6<0@8b 
G?v#V}6$i0(~aՎ:>ws6R9J鰑w"wwsdYקh9U~v71CM#0QU JW}#I Ի-ztZ@

S̾JF3/T<+SMfһQB|͞6?~&C0"Cij"gYn'k	ij{#-ˏ7GS|YiqTJӉ+Lg}ٓ+NGpg!8vP~31an^		3U}ODk/ᗇJ@U8m rF@tO@qi9WI`?<;\zHJyKJГw!KAVp'ϡ$H"3)w4*~EObWM"GnuO%d؝D!"0l&!l${A^zť̾,

ïmRR";'!D12ſ?}.*ؒ|-=aTwLm g7{=ܟ/4.4+ x$iO= ~̺
*v|V`;p
$y'n+N~aa4?9nc}DvrgXJLŎyIltWQ~xeq@Ks+,v?
H:eNap>wÚ?xQ1ce*-A5	~^$=VACw&=-S(S0cNl|96	1_1߾f`d=%!HCBݼJ7N"4?s&=U^
 ,yJ	X;bf 	D3jxtYa=2
HjK=_1Q ǍÒ}~(/)WR6=EݨFE~
+I@ "<Ѡ7"Ԟٳ'jqd7z!êYAeԗ |U־ڙ|#9fpBiog9,	z_x.+Yv`E>q1'Vĩ1D^\
D`tw۬|enY0C+KsPr׮taG">wl-j'koًq0`Doma@qӉr*0.oyYbC1OEQAԋVWB*
T=72}wXaEBK.}Wd:YאgFI@QҀB
M#V'pռ.'vɦNɮnrbO_&ZL)br_
l^8JEQHE R%Qj*!_vwHYhC"ӆN%+ֹ)%`D`b,IE᧧Ge̝0r`2g0Θ1Wn1)e&9o_97	T@1/;^|l;+{"TQůنUU)<cJ0]Gɞ
еp|g!'Cau8hnu6li;x̂J3K"P7f!U>}*U-\ddRs2p ;qԏ~=:n6?)?[rdE$P,BeLZΐ"+"JQ%Yvkgkm+v6НcӚP*`Xy,ҞPB<_ϡJԖ_ǭ&s#%{$LmΘVYI;a3gb*J
(+L!M[g.j۩nhg&mo`Fۺ2P?$PX/*
}xpuMt)eCrHb"(U'iy! ,퀗Z%U1U8s2ct%Gpk}\(6KʆZṕUte­~h#RkH”9@rqlUIC^ܛ
p\qD?v)7>HGlm0vp?r{¶YGmWw-ae\tpΟ9`YlDA3C^}6`m5QWIsΰ)?Dhkaߔ{(" $g2<\QGqI\&l2YR:eԸI4o7="k 0ӭ_;=M_+4'JI):֠
#t?4zw4` X֠WkqE(aG@v5CP$\wejyl3U:L/Kʠ\)!']{:ʩl>iC#
nnf;?}f;m]A(K	[*9P)`L44KI)EVBTJHULٵv`KORW(+*\Dyq(%~D9;joP0U'5NQ(w߽˶j=g`Qb\%Dm|FTN
]?OmݿK5]W
_ut3޿@7KaЫcx{@PjlCojS9
<9gS8t߽uU=RlK'Bz4b!4`9G8!* u*3@qnO %Ai)e]3)r\?mz?[`U-g揧02T0īQ`7ݿ`74{YȼNޝuZ0	\e.W)HVۊ{mcۖw#hOC*KSy	SԄ|w/^H":L*ܷ[HC{-kˡh~‘E^.ہQR}mvy&p
רs-	r`RH!C2q!] >jBI8d!~w3ud;s}6IW'
)8Yc5v?`"̟ IDATMt翹5Ay+n+d	QGh.~K_<~w3$+de27ë:_]*RpM9M;S>yVsN-%A
l /}PJٲ!8*|))*c8rp?#3
tvein`{>6e&ΕM]KGݟm6߷YCfߓfWNŸb'G9~~xf4\?{`7)5eVhm_?'j,xyS
R;j~<	{(g
fmnn^.ip):x?7O7O.Is8ۇ}2;YSG3e@Ei1inƝVêռn'ݡr b+"!F
⪳fqqS9n8
ojmf׹igK0~6i>?k4ŠG"꟯a0
M{ڝ;(6Yʖ@+@H?c]6_{7ء׮`MEyQ'W/>lLK{7kR.+r'.q4q70XՑCp͹s̜<)0wtx6zu-={kRPzD/7׾kxN8jRƞRjԠ@jaHI&ks3K=O|.%SiǾ~ojl?{3>w]3K7 ^36nBӦ[fpe(mD++WHaWK/,lcb3g2,/o9H Mzt׷5qkoEvQ|(4z(+8򾳹XwTT5;ȏmgyb*_`ѓIy3x"_081GêP2Nt!SDg;d{u~ꑣNԃ/?;)i6F]5`^^~x?)E|b {F(wcߺ}>RSV@TxӪ5|
~y"ǐF{mu+ܵ]]~0~Qz~9__j]g	}|>wɱ%-,ϰ$ - N0aG};ܹ-uNo_ygvN.ȺÀW~efOԗBv!:[;θB6mw#'~a)VI%҅	(Zm|GXqK)_˅m+W!җ$0>.G1qҀn7%}K[Y2SQ^# "kigjVm_TΊM
|/,IӘ5̹~ab+Tiz^q?CԟŬɣX9q~}kkJ-,@nX)k5
OSHVϟNGh6+̈/Sf[q3Go8QA6ˊ3i_iZhlɦĝnu z5lZLvRd7?j?}~qnb`(D! 8䟁tEiƕ|SY`7&E߀zv{u
Ƚͅ.pEv".O.Zka/ZuSU8cjE{T`WPT&Uߐ{~Y(5[Xi~k崮>Ζcܕ~ih`.nz.<9nx^; 8'C"8-oM" T?,#uH[wյp}11Y-ya1Ђ\`#Œ`ٴ5&wk 8g$B,]A!D"
60[?󏟢Stoy̶lBv`7e/T捅ۙz
F݅ճ)8Y$6
njX&9
KJ\Ʒ,~ݭ$og},ȌI"#	O"S_*AG<#Ti}EvWsҴ@x
:]~0ӠPpp	O"m)˴w0zGqeDy*X_ BB 8<3KO0
D 7.,HH5cJGhŜthK
/?gJ~YGg`3*XqMKGIl߫=0W;
0F+x7?RB=@mI j
67d&]2g9yM$
^<`b5
Vf֍QOq&]]r/͂[wHw)-NI՟iIuքVe1ԿR}i;߆s"`,W"4c  dݲWK:C]kSC8F<\/ZM5QLWp3F(Ԇ^hL=N/?!i[c>R5N#6?)c|"JT}b3EDG~Y^"57 {^<ߋ%_:nN!km1uo7VӽU8O*,HO
.h b>g;#y/սuz3x7j<(/8]K$-7ihu)r|gZ @b#5$aj$CLX|='s
_6gxhN jC
TS;t
~&)yNx2+ "3*:f
p{
*+LO?	-\?›)ڽG c]ds-@Hԡ)#G{ѮvU}잍M5pz:бu_3xE8KnW;򽏜QE4"
'@O&8'7PCջ;{d t0
n5{3|ZU~ۙ
lw
]_͛w]:rN&D|)O;#ƶ=}}߶`sԢUۻƅP᭏k[Q@sN`Bv.z^|ʓ(LR`}E'w|;]I`w4ٳ!s9Zjo+6iB|cux>q
WZ,7h@1#{lXua0 Ϸ>37kc
te$}&xם7/f>#7=_ WVQ]ܑdvlG=K)7Wk9X]Lٿ:dU	ٽժء\|,J|I4z@,;6T]+#U]Kh m=B
$0z(߿|-`F98J'-ܽ55/W>|!_`0;Nlv\?ی?{|}a$r#/AA9Œeg +(k"x!ΐٽ*.z{rs!Za-uĄxL
W?sÆTv?,\MK}?ic+L'bvbZZBA z
zox,nSwU;~/NLӵn#1n8i~|Cy|}6j ['vGݏ*um!0~NEb7noT^U0sPMQ˼b&0ⓦݔ׃PiTBېaK̢i>Q|Kv(~"*N$Tbڀ8wDܼh>cZ(jz	*$ŝ^'G:ɟFRF(/W|G\	k_QuA73HvNDLΚ5Un܍=c^ig&`LđC޻(g)K]
]͝@FXnk=bY B_ֻ2fQ+ϼ'xFG3DB2p?b@Xgq2|qxd:(B5SWATuۛhp'7VmlaR5]mOTOdں|?m$/֘׶b])Ap{b.m8y<; TjHiD~)! E?c^Q^?"U+?q#qByV۳qrjdIu[-;:IƠma#;glbnޣL=)#=ɋ,_9@	%'
I43Nw0nX<2q+=jD5p3<&MT86 ;b(Oĕwoӏ3Gm 	.[<}`;@O滷4!DL#vsx>8U糼7;nv~!`klDHE=Bۦ:*URb4ʸ,"⺿Yq ':JfL_Ngp"D
$B@քE`[L8fD*Goޫ-:Xv",r܌Zw
؉wD5e}TTY	C/EYu3Ȳco@>cFij_SOdRb	A
@'B7Ь@E`[7%k<%^Ɩo|%K;Ir'J3Ni^w69/;?:[Pk,	0rP?U	tran>`"18"|X2=]K\B|NrÈw9
#6Ӈj:Q0Er|;_0tL0P뾖2]l(lpX-݅@'cxy_Bc	0kVdW|mbv6(cQ5rʪ!78
Ho"!nл&t~iP=Sn:=r@T+JZ1ͫkZD5N3w8rf(	{}n;ò]M,BSG.3a+KA
ԳFx9@
'=ڶ{+ ;0skiJ7Xz* /\8qPR\
728;I|rE
R%E49614)o;k*Ca
ƶ|y'|xGcGsS9r`)_O _pszDB0{$àh5-A+wBD7@!rB|IrW{v9'v26ձM{Z}l-ELXS*r`FTdWGuWXPu
$-?ym[w挻fWkv5GWr”4dQQ|yp`AVݓN"'MW iV|J[T¼_w,X
=GfwUmU
F`3;-Zֺȷ΅G{gdeψ@f`뤃
 ټ+Oi?32)>nNŤijYś`^:N7K2[uOO"gh8Q0)lKQ==񀬂<\ބPV=kkR0YmT5v>g7k'#҈+~O默#4>WMaXc Vi4緵[U,X@R"`"S"@]0wL=qo"m}na/^\j-)Lq5}&V0iP6gmuxB[:ir+&L}lD*8dc1-G/
:NJR\;o,ם2%[8훏ij088iWP	'\8Pp%iq`}PΟ5'_7(Kჶ=,
𕇷Eٿofde!gMJ^nԪdyb;3ܝQ4s?XS CWP{vHbbFZO6e;Θ@"m[CX	|\Lό-=iOK7C(OH;uŎ>_9Q5[!_] z~hn$WG3>|J
M,2k+Do_9w4uT_~hk~3`#,%3G24bD%)3z'C{/o)#\g3
SSBkz{zyR{F´Dx
o[|x;Й]YojqeԦOpS^Q@AJ$*,,zYoLRRh|ǤK)Lfߴ{pU"&]ùS7bPKz%yOmD
S_	q0|Ο*_؎H㽚ڂ)yIZD3cYg_0!V )ijgfn/g8v9I veI[_jwI
jshQ2ob^yGC`4 ls(R=Z8;qI_ ۫w9d.yyǞF>xI"g[yq6o}BB#?ß	%24$@^<Rnѽk~wOw\DaʩEv'ޟOZ(m6MMsXG߬d:ȴ(eo@]u^g/w[ VZE2
E'WWp'k,m],P${TQXX:Az@n'
;r~Ĉ̞~߳K__dT>c(`?"͐~B߳T*y7v1=aȚFNk|Qܣu+8؍F1٠;osʽ}k|;+`|DDxcoGO4)Tͷ>1/~Rg-CVˇ^Y|Bƥ$='Q,ʒ4)i#SYL=;r
k
:a4tUMYE0E8\3Ǚ;)iLbH{ͭnpد~Jt;UCddiwA?Em>}6zh̙É'7~d!ڢnsaA1(ZAK)wD򍼱c2cMƐ!7~{[򊯳pKH"Rb6_ߓʒ4_|ѕ	Q#J)/JQVX0]V lsv2UXFgt#r`䎌dFVshy9,?a((,NOcp#^Rw˄B;Xh۶m+԰| R{oGMPg^Pi5^>EWUx`O2~-XΒ۴/^ٰa۶a-]_KU$@ȡf"QD
Mmk䌩/IО5
ycBqdb+P|ɮ؞RvZ`11nW]Yɧ`ik$DڲxncO[kOVh+nXFvx
Z۹:t(k׮~i95T^}Oy*2XL{ZDC>üݻ7'N		hB8|	G[k-ܱ<=-]vf)+|OL-~^H)d?hM|QVNG??ˏ%DI[,]AjQ
0F!DI}}}Ϳ_[
L+1ؑ$v_Ͼ.ݪ1
V{(ä#GGk"dxE@yn鉈 _
Vx/=w͗l$J*9X;:]LfqɘEbGWͻ-0!ŸsXj	q#fn<d_ϯa$Pq@3oj:q(Y %:4(\H4#UpAgG]1,B^( ]C;YWWoջQQфd?L#(
ѭuV3Fk͝a0sy(ځ·K%)e#^XoA4S{S@\7whlFc
 ,DAU\TÝ4z_[6"JY&`nwxwUUyK	?VNK!k4+mbtKV~t˷.@ xem)x,r`%kKpӇ&Ќ{f,V*UOD9klRO+`Ct´=NZ;:4 
:AHO͛TCa+{چo-{RZTɈxB@N]#ҷ-A&eXA#apV
MU/d_M=`䴏V
p}ܜ*eЋ
cy7=h%{zHʓήnTI@S=ЪddAj^:f(
kHk0ɷ0܎,)YVV9me*(,ZrGx=;Ⱥת?Vfr|dr[pU]6*JXJ4ƐQ@FCnnq]U~&L@V἟
~;kkz^Y/w00ҋ
j/XNUyysOx`mmVQ{RJ>H n#hi
WၖpM0*;W*~95ΊT@1{aCQ  NC% գ	>wTRFōug+*^=*_շt>Xk	WyG>҈`垆V
qڲh[T|!-iK0˵igOB|enAOiKG5 gZ:Fz
Oma+eŔ@iy$:/P+.8vhei(-B.ri
D%+ 4EX[mp8~PQ<miSάQn͍U1L+jq{_j{qhH9k3nJ㞨+R?tTF0 ԠFjg4>@F]e_iH!ڈm_4ףyT!UjeE.aqrv~rw	mGo/T޿#W	
.^ϹNJR
@Jk4k(83=z?͢:8mr?Y-Ձ*BS|eRCz=37Gc5ܤ+ǹG~VnH`>~yA>&-帩#B`At8FVQPӱ}~h2UjpgO{GQbi$+r`s(~8	dm~M-$=hs8n7֯bm	SJ9)xjрb¥䨱ɴQAϞhSh4a.!\&`|ZE.i$ءC৵!|(T۶=hBYS_܆QEyM8^cK=gؿ6r#ȧ~M:
"d)w7?vQ
6Ϗ;=('_߬
s]9I@7zoDtHù>zf,#lg^^;y$:vRD	C0jh;*-BB (ZX\˫]E]tQQ)HU@IH ;c;eIfN3sgyscL6a}GC~C@}쟛r)WxQ-,_U*2:BӠi>kfC;[p)t"%Q3NmCpJPRݭYחk9Z)#69	[fS9ɆvUN}D&?Z08W6[G…#{5u߫9 lx[ub?XE;W3<.)uG
e_[^G'3v90v*Mz~̾4ʁeFq$-@]ڪ{x#4RG2NbFW^
eK疒Ic=x047G0̄o]#uv\qԿ+5!+O~?č1/t`}?w4龟_{z|]KN+0,2\%\jw-۽|ܓBN7xv	o}gqɬ[sVr:B0]g9:\f'L8]46
~X#XG)^e[bRqYŤ%FްNM `y~:v}DJ܉
WA2!;_3NFPDinf,1lx2kOȎX܁aܕw8InRG4@#u{9%xff8/<@
RHDnzE$'-85ts6θgWQ{k6y3+64yM-Ft#RO3V5FR?aFۋHƑk;vs@.ΌT;.=x,4^o8ڋf~*bm8sg7i=co둗;&ÂIR(83Ӱg›+1<8̢uZf\YJ
vA+"*k!Y=qЎ5\&
>PiW#Fv*ⴞ49D젊.O1֓
}OOM$cЈ.r	=cKlv+'DiHSN?JɧէKp,x"3l~[$HZ8|m ]_,Dg_ְ#VkzE;On<`J,m*w4(j}rf?=%l9PqO־ʿ_M°o(`!B;	#.u׮L,ܾMa/_k#qQUȢ2vU=/Q
xZQA7K+7@@cAЏZ>O8M5|u>u$7#h҅MZ]~G@lQ_)uԷ;)'9'xuH	xy(/*RNFKKn^Qm )UgC׹X4Ϥdafήu<&o192rjKMe.L>c (ƶmJ.n	PLǨڳ)E
6'6b=ϘAEp/'~F$虙o'!ܩN#AGR:Z<=gei!
B(y9rpdKvu:Qft7fafifFk'݉
6߂٭f*s
VY>>-ǖ=ڬtUPs%7xm.aj
Oޜg&?WyNkKFfOJPKJvr+ؕ'+cv胿l5RiƠGxkOOtu+ Ph-j>|ӗ#HϵP6pRi q-ԆPPcehO$1u}Uund:׿;ʂDyQDAfzl,od2v/Wvju'q".#ч]j\nbno:kwPAb0C%UrRvNK1Mǚ1K:nvb
MM;E˜^.#/-}	huתO~C3?y?	J@9QU[tK<i	^>~"g
{׫Fco(~bW,(ջ^k S{*a%=k|VjcEg|`5!n3r,yBQIНkQA>Dog5˞Y̬.I<{uƇ=2}5_nS78N\src!wpo܏w2\J|5I)7rXc}ob1ֽ+1o~F8.vyߋҸRsΈd*M?sNū{>6~*C8wm6λ٭#,v:+DA9HH)T6p}oY4<Z=w'&NyJf.46f-óD/oOo=ߞR9NpfOUeL@G&Cc=l~'mELÖuaʬ~^B<:[haI0}z1o>ϾxEmͧ?&pgv1nEꝣ	Mq0"hX[ohRUGzzr)3A\9XȈDlނl@X.?>,>7Si>z09jHX701}2hXE*aO@|0CG*oN#:a_WjLЉaY߇c%K6@+KJ@@^W~qa|G!_wŝ
+!.ʚ'?~O?0xrO*Zz>20}:N;hϕqx:6̯)b=._| xrzk 2=#W.)n`]6ӯopnvSWYd;	i3tpӨo"vu"#1$c~</Qյ޹ɀdDtb.FuOɉ	1{k5MtR'A]q?|h{C#+6O^?>fjl^2FtM6SBnNkjG?1v98kƙ	ߦ
6=	`xONٟNf
LjuZuMsaXz@KwoTpFt<w}'WlZwlds:<;wzGEU.QA/<-#0)e@-K0JrKdm bqgvDU*HiA^js)'|IE~5+3緃uyPEOnw\CֱKdFގẢ"3*	97UQ|\e}*~

t$pԍK;MÏO\nӫ
rZ>t;an=)>o|k)#q׍G
kP^q\b#4{TQjQ=P$V¢oՑ9=9vW?ԦA[+^O"H&{i.5uM
#\Φ$ySHv+Vz>/lxhrRLnQZS;95~[_=p}-;rcpXuK0B]=~iO_Ds
93{\tr
bv:<e쓗›SoTmb?Vf3LKv놰jGݾΆk2k9w~L|c{18mqSkG!u#p'mrb9u7P?Vc^ﮮa-*w
RFNjc(.JΧdk>I|aŸ6GlܕpXG*ؤy#
qt7$']HlY)|螝T!ze{oS._8zy/9iSؗcxbq%#Sq%h_Ֆ=C<+юwXU4Od9} E한 }CZˤWV%a"1vH$#\&A!d2mVL .V`C.uw>Ҝ
zi@N2m\(k
#]D}mT6ÉSq&q)x
Z2+TS$)pťF*Y	/|u`*(O~~0=_`0ӗ
ù'ڵ
>F*SҒX+,W^[&u.ˇuN+$0
.5gEۯw--#HTPY(N
fK)9pUC
S{sj׈*gOLθw?.Â.ep`rYwl\
xZ~3^prwLF-ा9O>Y>Ȉ^Ѧ wb抂oJh@|WHb\.Oa.1ӻjAϷ%30yt 
T0yRWZYؿ,cG1ݍ]-ͶSX1.xq\zJ,\wڝl]i\@`NKKhB"pMq>blzowˡOLwAR-mክL|-/w{&
R	52Ϭb惆{wJ䥫2 B"Eďh
`J	"D7ۋRNRZ^]Fi5%w+L ~W%~g>#
aѶb*֗SRZj^ړ0KA;R~(BBp'TK~f2I%Dv$bnblopf@r
+
pLnWjIJgwgɝmV(c¼#1` v]'?x+3QQة
FZv<8ܖ`q{X3MG~S<@aOe
+kSYZJTAE ,)HhB
/4\T5A:"&.
L[9o3em@xbg[~NuӞvb7D	%RF?%@sPX HzurK_j@W73kjpH).VmV s׋,8v8
hX
A"Kvr%SLJ70=Nk{\Kb2݆'	*{nY_<<-W\
,z)?H)륔eRmRʵRʅ/jGܤA
Y;w~2a@&ijH&f섳˰G3w.%v2Hk"/"X(nx}%3!*qKm?>NC-Ŀew%c'`rFi⇟	B|s槦ΕXĿo,y.>,7%3ÛSFIs;[^Ks{>9hdHsK,ZHhT߼&h\L7.:zasVpIq{.-n1dNQw1nr;=Zp>~@Xj/@xSaBRsZoV%0	OJ*MUX77Zĭc:qk'@Ӏ`%`
`
ZgPe2^7*t4Y4z{ؼ	.RW򿂟=BtD$
-}
w}b!KzǞ) sǻ[*
tN=9Wa
0oæ?h;SiokWqzfޏ1nS/e`%|ȏۃ7?$뢝l
h~%|7GB|h_G$.ƴS*Ta#JsK*-N۞{ʀd,DbhTx}УwD"@poSK?vW>37TU?hc:h;ۑ7~.iIqC{	)FF!_mA)S菉DU}sRZ`c?f*4ҽ툯gorqE)
w\Qз0A"qѦ{X})_賫P
~[.LB$3qnsf=	5(	=:=_vĮiQt$$˶UGXOAz~ՃbU~U7mCu\2C,8)mAé
-tRw)$9%cHG|Ǝ\VɅˆUx_,"@q3$'A&
Nu vts۪B57Kv=IqkM^T%!%q7[+1w:OLx+σev!DL"}	"\)O}dx1@3Bh[)l+Nopʡ}mBB`[m5[y]Х`tzu:>R^ĒU,Tɇkh۩aTvsF!`to
ˍ;3"~&|i/|>;
IIP
ƾPSQQώJְL۴j==׏ep$,B_F`
Y=,;焰blrb)
m J!b[ѹP6OMCQC2mv;Uܷ]4%-g}|
0"5c|Te2fNҡY\8$
:bZ]+9OfW}<p2i4z_AFNC
b&t,0+v0AVe:J6D[XukAɌ‰RLb7ˤclp9nq+2szZ::3B8
!ơ2]:i4BI%R@l@I'`%~`ͯmi=%{^@U~^i	ZG,`G亴]^.\	i3p% bݾ[Y˵tCuzͩua&wYҏJ7;uU(5WACuPUpR^
w`[+6n^yuea-NKԁWܕu"5ydž"Y>|Rn
s{?w1A;%m170W>'gX<ڧ&UAPg=,)ŲbC֣|YJy6~DBd280ѯ=}ӧ03ӎxsV?PVqM*X}?v`l(-sUoJ)7W
mG
!Dppz/7k)J(+v'Ǔ@FJ0512sXS߄?Pp=(?KbjvrP*~|hc?!z'Odr`1XF".}QÙ"]JkPw56?`h"MN
lT'_YUqkJ_aͦI	bk
PH!(4aڲiDT7ˆ	!
̩(\Z=%oyhCN`Kb⋵Y:o:/V,ӥ~2BGW/SHsm;,ן<(o=qk@撱m?l0ouQnEx?Cݑo"A30o -7yCl)K(Ǿ6<%`&qdj$:PQrc3p+>gǂCWMh0B"|AJ lV{J׃kk#\0]0m5e%~c}?{kQtF,)x	=!F$ʌSV]CeY1tL8kcdb6n2QEO5b:tLz	'MEA`sTPBA
4
Kd2/KEfy)Tq~9RHlGmKrY?=Hr9*'i:)IJm{u=s)ۼL:BQNp
!(
j'lN^et.Ӑ+]|Ƀ}ҚDI>ͷzڑA;fqNc,*SwP.uիF.Ma|^wg-za@l_4O|vBy.1վczTx*,*
[1bM7yw5!|Srio]lEAV>xP6M,.
9o;KRz%ƴͯxU/%{h"_shX~{nO%B>|ךm~dQ~ecr"΍qLǥ$ '+BRˣu]r_"lƔm5+2FD@흿}?Cz_rIIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701597438.0
mcomix-3.1.0/mcomix/images/mcomix-48.png0000644000175000017500000000651314533050376017510 0ustar00moritzmoritzPNG


IHDR0$E
	pHYs33tEXtSoftwarewww.inkscape.org<IDATXy\umֳ,͆FFbG -
N0K1KŲ1A
EbW!lcbЎhfFfמ?^OcGȫ:nW{{9FJəj|?W%B<wgRGL{)م~CS
UIHDywd79cPܥhV"GHWb
8ش?:ge)|*%~qNuQ՚JLt17I"uFB:h69Ϫbwה|hP3B޿{u}t=
P5
 YHlG,bcľo' 𭨛v׶cSh@S&ά(ibLwu$`5ٰ<ջsl;\].ZX;>_Wֿu7f<	OtOP0oldDɌu{vf	+E;N硅呵W[}G@<Úzz쭂tx^#3KO,˥=ܲ͟B\/FuAƅU`qPIV,m2\	+qHMx'WLQ&)4)n'NqrcB`DJi+
_{7&:K/k(+Ah>|Ot\C
q$%7]6O]T_h /N"$8[vw<^mB0,LwfWʒ;TW:Ʀ7w5V}i~3ZxoQQ?CBh>ڇbrM4
EǖK<| TDB!y4dUÓ͹l0!f@663mǯr]kӤ	
eۻK82O薊
߼O.C]1@0mZSȤ9 t=YXˤ:1ΤsRp?zTk/?|!@r mXl,D+ήUy0o	#	Ĺ"#,Fqmϸcf ld*VG6	곭{AX]z/19ǮIZ7͌njwet4ŽcETE|Ti@	zʵ=qlcq}:+ϧ$;^{'zGxD>!/Q4NBkCEǖ}H'iȘӔTdϤih(
5a ]x]G:qT
_Ԇh}"Cj.};=y]w=pNԢQ|"߼7%T\G84k2i=yuI91ƲZa
?|y/w_ZG.b(A#6wx.|B;j"+
+/]\V6߿#7,0=cBnY}bsBL(0"]T*GwpJVքE|\2<ゅeΎ;G[GjyLJiTB]	]TUJgZ`v]e4xuNOlqrT<1mr׳Guy	ێnmJN)[?P?Q'ǛG&}Rʩ?lՔ߶zYraۈ.)XF')YSCeaC+UVqx`/˃k+i(!]=OlؼcqK?¬*7P[upb$Ŧ'/	3?'`Mhfh*EyratQY]ݻwohybćBO9{|~4č
 S&&=))hNeĠ "5\]m?9wNt}m*Z>_?`)A_LtAbb<3LoLo?AfDgQ}38<'9POoU|;X{hHyB{=rGy=[pQyE`'9MiVt$,#Bm8y4ow/hIzMIol6$GiaC4zEEaabaDD6zCHm`L5:q?Xe(7I*:KfZ@?y3MK-oK}B]xz_DH|+lKRGT}?Ztrpv\@PxFS?[}5dʲRr"NGdEpSWKvfL#PLoc˱@X{4l1Y!,2:;3,"-Q};\7Us*-T!#((#!)Q=[;=_"&Ae)=\@d E=< E>e/Jr&Gm_7V i"0M59X&<
1MsHo0Ls
(-F?(0 +A;;VwElDoCnGn=Y}#2GH6QsLA(a)_)_'];N;X|3	*C>_=.i.`JPPN4l.g9Em'7T4HgYMzLDk8}0h.YX_bc_Y2a6t5xGoJN{2IgW:Yz{R:1kI/f3a]hpuuqi^,U0hF4i:S9Vzy,?YET85@o;K[f@`A`i\M=@p78R.AXCIq:2},lEASc]^eUCI-h0y;HoL53~*ZJEWiu;IY7DUukYGF/^0x0xMN7&`8aIFXj}m[HB=e*h.sNM8LQ~EEVhy~{jXGEHoL8N8Vuqbc|&En?Qbrm!Y%`ptdSB*Qmca9Suo2F]MPlCYtr6U.v:xYe;b@RRFCjfZB<0OC[ssNk/C^L.Ca_ Ey+{.~2:	>	>;30/|y=3Mo5Pt4$*/22/+$
-5Rz5Ry>#%%#:5T|335Qs1Q>$#=.S7Sw1Kk<[@bAa@`3Mn#.9O??( @ 1E		)9
)	 3KYd'=X
 %7U{.A':n2Jf;Y}BaB`>[4Mk-A}'Z}NxR?^G&Y-_.[UZ^`a_[V5g(U-h=EiNNx>X|-?9(&.AXzWJ9-YP/o+[8kQaglnnlhbWJ&N*bJ0X9JW->Wx+=Vq[983~Dp9<2aSclsy||ztmeL0[9{:N/o89[,?Ym'R;83,[N>JVbg$5Kpn':PjdXL@F2^88;R~!3GdN81z-q8a>CP\iQyJeJeS|k^RE9f9FTan`Wr]y]pcVH;Hy1z'b7:FfLy8%\8=Dq;HVcq~VuTqseXJ=M%O5+l1zKxMx82}$W4^Gv;HVdqsfXK=N.R}(^%]8LwIl;8Oi?d:GUbo|~qdWI\x)LYepziBz`b'Km|rf[NAE{5Mmxj=Ut=Ur *7/>Rs"+:S*>[*P
!_-cR]c3W4IOPJA:2,q3u,nt"2Ns}~/D_-F6S{(6HG5V"(-156752.)#1V+>Zwa,B]2Y!%),--,)&!-W+C_	+@^5Y
- "$$#!	'4Z-Da%	 %$4J(:S(:S$5K -@!/p 3K%,"(!/(=UUmbXSRW`k^1Jg%4G!1!&fmu1Mp*7PqjU>680w+g(b-n36;PjFi"0
(A.?fW993y=f-L Fu(X+^$Q:a-L%U97Ih-B]$r):?YzWy"b2Igh;99=f:`>JKLLKJG-`+G2t:7^Jl'$!!0E+k)9Ss^8!P(Z*E;yMPSTUUTSQNF1P0iAo7QS{)+jC
Ml.C]	#1},?YA^]T~4Ml`0|#R9~0GRVY\]^^^\ZVSL3/f;6MLp@a\@]-?X#0{5Li
!-U0D`i_K9":Ve8Hz2m'Q@}/WZ^bdfghgeb_[E~6fH=d=f3Z/Km3yK_g/B^"/R'6{PuZ<88#L~];0s Gv*V*QV\afjmoqqpnkgc]XB}(A+],h7f;]88<[Ot&5x&3rWK8884+GiT9?#Hv#DlI]cinrvxzzywsoje^7c%En>e?;EFj+j888KX%3nEa"5EeO8888#M_8=EMU\cjer{|sike^NMG?7b =a8888PCc$2%7M%6Kf888,n8#=^U9AIQYaia,<#0	hjc[SKC;F2Pu38888f$4I,MqA88/u$[0y:[>6W'[(d1{88ALp
'*:c888&`/v&ZX5>GPXair@]ns=YztkcZRI@7_ Ah6 P888c(8.@ZT8755Aq>c_7@IR[clu^	Vzwnf]TKB9T$Be7M866V+>X6NmL8*j)g8:#=]\8AJS\enwC[zWrd6Jbypg^ULC:L(A`Eu0y8)g*jN5Mlk6K:Uw9StP88%\"Iy*Mv%:SZ9BKT]foxzqh_VMD;K.Fd+Nx"HwAq.r8Q9St2HeY886|GOW_fmth+M?UYZVG 4QluohaYQI>0[@"3I,:JIaz[yG`{/@V*UmT:Z"E=qCRY`])Fq#T<
DJ
M
MJE	=,X3S}ab[TJ*[
"IyAk'9	)		,>7ewR'R,W)T!W-8;	>	@
B
B
A	?	<9,$T*U-X'SMo!K';U+6EEYr7Ia
 #9W&J3Nq< $(,0369:;;;9741-)% #Bh*?'7Ek) $(+.13455431/,(%!@m+=);Ep"#&)+-.//.-+)&# :h"3J#1
#
*.*l8W}+=$9W
	!6+>3MoIu*X;#!8&SHv;Z.B%8h"7

2,="5J3MpAdLuLyKwNyDi6Ty%8P-?'V)@a
*:+$2(8(9&k1-E
~3s1Mt%;Z%La+?(@ )#6	!" 3K4Or
 *":

"$%"
N"+%-
;
-C_RyhwxwvxxoZ;Vy!-_4N!'$"'I)=U^veSF=6445:DN`sk>Z~
z
Ai*=VkkN75783{+i%X M#U*f3~865GbtBa
G
		W"^mH47:4|Hy+
25Y Gv$N"K}8],.!O785>bn"4H$6LqS67;9>g
%&S>GHIIHHF= Es	#+b<85FqEh;Ts5NnqB27=(Y	 (TGKMNOOOONMLJ=)D2T<;2~9eX+>V
P!}(8Rsm<7&\7^.M-JDMORSUVVVVUTRPNK#Iw%=/i,J95^^
$!{
P&6&%*:2GeRva?]0Gdo92~0p@
# AiMPSVXZ\]]]]\[YWTQN)R
4t>95\U~ 0C_Qu1Fc)9&$'5LkhtdS7w*8>h*C88888Z\68HRyHy888888VMo[EKRX^djp2Kj%8Ssrlf`ZTMGA:6o&;8!R3888:o"0(9StV8888 P-r8+Em5:AGNTZagms(;R'9Nuoic\VPIC<6c+@5/uG{8888X6Nn$!`A8888%\&`8#5h57Q3Nq+k86^8888A^}"/p8881y875\L1G\6=DJQX^ekry\'''-DKRY_fmtz>Ur *'2)8J|vohb[TMG@9?X>c7`+k88%]%]8k,>'8Md888+J88C=v6QrP7>ELSY`gnt{}wpib\UNG@:<^1K:888ELSZ`gnu{~wpib\UNGA:9b#5;w?7$Z7`88g&6K%5Jj8887Fx0N2M*Gi5OoQ7>ELRY`gmt{}vpib[UNG@:=\+Gh-R~1N0R P788k$4H*DKRX_fmsz|uoha[TMF@9AU~9ZaQB8889t);*sG88;K[iq"/`6=CJQW^dkqx~h7G[ztmg`YSLE?8J,A[DhiZK;88Hq*ZNqi88BRao{Dm5;BHOV\biov|o$=DKQW]ciouzUw9^hhd2WKf|vqke_YSMF@:-P3Ot%4Dwi\Vs"1C0:Rq;Ts

1".Ssx&4@&7,R'2zBHNTZ`fkpuz~4Ou	@S[^_\W.V6Mj~|wrmgb\VPJD;/T.d6Rw&0yRq!--%5


L *)4$/;&1##Ox|#'\EKQV\aglqa":%cJOSUUSPK2p)=drmhc^XSMG7z
8`Ap /&"%1$/=)4 +

J1F`VhP)@\,Y~?90eKRX]bU!9\12	@
CGJK
KJG
C
A4
3-Hl[d_ZTO=~>j	2u2Ei&%"P{+xL*
3*I)H,>'7:	=	?	@
B
B
B
B
A	?	=;8&w
958`:c&E
,<iBq(9(1
L#6M@r!!$*.1469;	<	=	>	>	=	<;9742/*!~vE@a+&Be K"%(+.02568899987531.,)&# 
(P|$"Oz9"%(*,.0234444321/-+(%# Iy 0G%`)@E#2Q
.!$&(*,-./00//.,+)'$"Cr-Fe!.'h)} %`LR0$A!M"C4/:|+;/5}I%(a'28W	!0$#ObZ?%\`+0];fZ;0Zt`ڔ.;mu,GpJp,pp

lVlw!wpT{.~m	q,ciC%&wa{?p>0ЄEI֖3ˊ)/$%$e)/N?)g`tƤoξ!:i`oG{{i=O?YR9WiRS^̌5L|[[AMy!+@:˞=lkbss;A	%G2%&cγbcj8nrs'5pU)=Cܱb>l(+2$&nnbN63Gs㩯<|vfMɪxim65i:^5
M+p!E8؉}$Xd,f&4$FQ
#@&o{{yfNZGE
kysA/D,	R`ͿkRQp;i#̚3tw/Cݽ5@_ }i2iR)eUŔWPRYJqe9rjjHփ1Ӵ'C˶-Xr$&C+Eǀۀqq{J
ߓby/-iζ>٠-#(^"iP7JFQXOѨѐޢusyͭ3i}: ,1)>0A
H%Ξ7kN;jdjKomkeNLQ=DoQ匚\OqMLTg{^ȟ_HG?xԣaqA\[oY^3Yos&ڶuw٬{^ ͓ !~%IFO~fNYV~:vN7,R%&'bs(E3P[sZL=6Ѵj;-M].Ԟ\s=?9sP,~	`dR5K`Ȅ{ZjvoK6+=лA厼"Pu%L;n,!QSY^~r^:T*[b,~
p<	O;O{,D:ޠyۇ>
ڵ]+K##a=~)EV Lx>"['pg&fNcj9iX[ә&L`{6h$<Ae'qI4̚l}Yv
-tpa!~@	<<0(8*/t32o8抳1Hsw>~%'X/`y9rJXތ+C|ާh1/?/$'S)~|~`
?Uk?A*9gէp7o|!ǫMLX=G	 \x|y|cH>V	:E!B@uOW4y2O=`@GxoiC!H%e)=ѷiohjo|~PKGm
\\x\n8{!?Nwx]0*8>İgkwa>ԉtq{YA^``{ztST5!SȤMngԄ:RUnX}e)Nnv8S+
hc k{~
x?zX8BC8A඀'v@jPH45[xnŨ)$+Ԗ3VrF$ݟG&8<̟?]{P̡!qjT *!;A
^_mj TB!_,4?>2#;rGxx\I@_D@&cNF͚@hXJs{X1.bJ~|Wӳ/g[;N,~<	;(FG؂k

C	P
94#GGV7aa*󱃵4=Cd%tݩ[J?`Mrk:2Ξ^U[/MX#.(,@>K	?xe_"i8(7X]+)5?/2[i8zEIL/ons2>(G2zqWlti7߿=dka=)8*H?zW>XeM2"cW?p9޶Of'akI&mr=X#gX"~,N1{{X
O=rnG*9B!6M>	%Z7?8apn`Bͩ?
h$`&mMSP;Z;~A13?\H|kA_Q~,k}F{#>6߻tI_(HP`/<1!D/,Xl
)!Pn݆il.7hN3ණS]	뀟X|ɑ:ڋOFJ^e:alHNc#7"	>\O;0(wpSn3!)0ӔtlØcg;ˋS)U;$GۇD&bk]Jwz$ZXGGjP?9)yH@wG&wA[@ß!CTOMSÆvxGR#mpa7_|Jn`#KP@=<47H	~>;@'4[̢ƻW֡X|̫g
?3sdl٪5/^s9š;_$
KX<{7x-^i[{qF04Ek?1Rx|nSN3䨀w)u5Glyy=i#'r2ISvIN嗊(,78`fOi,UoES!5_8Rr%
@ˠdIl.^L
ם4yXƤӊa^)L/7?<"'O©I&Om&ԊNat>$31eέ}LF|뷧{T
XK9+]HBy{c
 BooK^앬	SʙXAcU1
$	R	)Hgijg^Vp)eG'8>A1T~} ߎCߑ~=eK=#vIJldkm8qZ=s'0%I!$:{h꧹
ݼ;:Kg
&rR(z$ TQS>y!Fw~_Ɵ_vގe{0Ýkx綫s)nЎt+hj7^vPQĹs9iZ-Nֲbm0{ ci/ӫL[bcӘ8
x@_W+&UfSGWߏ4YGz${BFp3˹th.8v4'N0
.ׯp}Cܷt;xi3/8s*%zBs$HY,$Wˆ R"zt@C Pρ,ywvgLM:w:e:S+MuP	(Lgyv]+=77o3ZC\ =TC?~S}V,-ళЀ~|S8n`NJOVPdOi39
|8aXhA>$!|ϬoPo;7NM16	(w(zc0@4E&+D'x%][Ӵ
2}L5\<;w!$GJg{q'w<=Zyp^`~tr7Wupſx~ i>x3)Czv~=*IGW?YAG0%̌%APG_aFaK~u9lo@
i^o{I*p)3c8צa4%GNP&®}E"*qkNtx{~nv.b&zX
ϙ˼)5kغZ<Ǧ,+*\2oı\h5E:pݞ=D=H8G޹}?--Р_Ds"*j#u`{;2QNB):PҩgZ|g<}$?|d~~9~j#b%1XJ}5#2wKpJ>~8J^ŐmmCj!{?{Q&=z#y8
y`tHL3t1#>֎ۮ^LQҾ)Yg_=~|.?kQ}a:};mKST.;e6vyl9-6&&HYt!

tKp@>e$<kv}ũkJ&PJ$	?2NEc9Ț=n}6fg܌5Ŭ۽#J(|aP0@c	9xf
yA" Y|n@
vv{{aU'ϜO(S}.?le&u_-!'_H:Me4+C)%#G`RUHpM;
9
ʵБ;Ө.3(JzAZx=vH{cR)A;!L-OnѵeACBq~a}/Cv
nuT$r:S?}r"|_TO.ćY\<o!{/s0kWJS,>>skK19|XzMSGoOrU1)stO}w=;_\N&BƺJ8a͘IL]MUY	ɄPΞ>wyw+뷷zK3d0a~!G$#%`3pʎiypDqXFiTEJ0Mvrsoڡf[!],5[8e^ίD[ϝWΝ9p#\/0BKصyy{B
EX	ʖ`[Ќz0GňC=d,mQ+9ixЦH@WÁPU'_/^I[|ύ4\Vե83e߻Հq<qy'0w8ᱭ[?z/]O
w`~
R`|kM)@*곎/b1Fu52jy~e,=^/[O%
TX{:=/q%_5-һځ\Ӂf3Yɷ)i-ǖp"e( VU})%	{z|iI?[O_ 
Ue8X?	=ˇH]\~ZZdp"gLsj1yr4t9\p\	mvT/7Rb0,/Ÿ[Mtn`};gL2kT*S-7_	k#oX
`fCDf EL6z@21SʼXf;}naN;z<,^?ثxKa6[nq5]vF _8u0uM_^P&)a]CcS	g6	pǣקqo!`KWcw7'~JMXIC0Dj.tcM*9<WPݘɋkwVn?s1&ڶ{ZAfӐDf 3ٌUGZg1yt
gWy,}زiM@>J?ac	-sqNN6*d{ۑ>!H^	0*r(.E$)jPo?.z-9͛[ZOYlVUOłIz&?egٽȶnEtAf6vCؽbH HZaBƺqz"R	m-]|ᎧwU$Y~or|qU>N[ߍuU\s|Ɏָpsg5)EqPA
sڜzY([S/ɉ07.hݬQ/_h7%{*IpZNu{;ڱ]ma3O:Jz{DXF/{rҴa
fwnCov|C77tڱU/5W#@W[,3*mdw.lߍHY=F!
X@UdF	yMkɳqu;
ʋlM	
sKK;Pt2>2~#H
AYi1:lQ;{Ld8{RE?}8X
_E4^xKwnӬݸ
`|`#+2,z@NzΉ!!*Np1ܿ,xanN;f<%YZ;0'/Zd>=43xY%Rf6҄={bDqE.!8iL[c6ɚ)|h$pY`<%Wsٺ1nrl9hMIiZꖪV@Y
ZwgO=PڎpfGKic~.qT
ٺ%ȡ` ל},I%
*0&!Q@pZWpg۶򪥺=aa
0"Xkozr|b/GWLyu|Ω?

ӄ)0FNC`WYZɓۇ8cr	ސ+!4d2rQcEhEBjxT={X(_~B'4T8aBzk?ށ4wsh'o9~D	Noh]DXU08)uŌLN:*LrVVX0M kOIãgMf	\`#*o5c4?8d k(l0is&+Wd6MzKWQ}.;V\SzYY_Z1fzfǓE AFF<+ғE¯6z:`]bR}U4/]>+ג4|NZu*l2ԣ=~fO3D	-Ȕz#:Eџ^g{NH;P>ӫ*^@d6^%i/em'wwöXPˮ*dm#X{r4w!mX;5	!(0MHx
Rfl}MK%M0]*BBH$ Yr;olˀ}غ
)$/a	GԐ+Nm&BI(u
.:}۝]Y>@;mƥjgrÃ[
8=ϞQpb1; 6=^<9
/mpON1o&mFL(`v%U,ye.J%ϛuS'~8	E7]&:G y2_.LwTXdV,{yT׷r>\>tap/^w/<
@À@o*=dn8j,NG=-gP:eK&~OqYZ9
~~RY+?(Y9Raro=E>CO>}~!s{׀:{q_=Dy>B_}amC|sϬ1Min/ށmxMs1\bɴ(1;Ѿ_}s^F[4*Lq!'<
eZF@M?nb.Mԛp_mb˞ϼg$>BΦ"P	G#;a>ۀ6!|Cϟ{W+[|2Ӵ>_WvyO|o.KNB6ʭM9
C7~pxAwεg3?,s|'F1s.T
3Mb{']fOf1R4c::ͪk5z8Lj}?L|De!6:_9PCA_[S|p{ݻ~o䬅snS~%"^pGս
i\
h+@U'An
t2Pw(*gSySGpXW:q8ͺXpr$os/CT:+8pA,V M2P>/9Oo19ЍG~p)^|;>il5А/MaDin9y=i{
'wdR#np+TFzr;}^u1\pqif=K)@8hd6ųl<L
K
94
{~Ų&if1MBt88+L*!w|9
v3oTBk;BTCPk.\V$I~/0RU^}?uUJʼP"T^)~Uu{9iQ?9Vmi}JnP1czB{G̮Vg^%&E`fWW)AHOQ6Hi"Y<ɶ=^?s\-s&yGˡj2‡a6 /cXPB ?,򱢼{ˌm#[i"[(7q"n+!'ng56g{{ h̗qܿhX"1Z4&3=?S^
\~_ sM]Fh;E~ە}rprHЉh(
1$yBCQ@'Ən౟~^z&J5nPԔ^@@}r8PC` D F.=Jo4)KW+vL"n^I/t'=@"	'Y,ض 06
&HCQ{ٶѳ*|8#j@07_8f$~~RcD
'ߍ@ք-Rqi*ntiՔ'RH3ks:m{;p7h*jxoK_R,p
30\qu;ilfsE~ڙ3V|&430>ˋP(B-g*Xr{M5PgZ;%aV~"ʏwagV;)sJoTUcmͬYr?j,㦪Oo`i4֔3\`ĽW7L/kt"͌e~4L,ctPs(X.?mws͇NlaG7xӇj>S{Rz"z,ʾ~2ijA2*n[Yj+ol
N9n'̞\`>
P]U{*WS]?~BXs/
Uwlv^w.kCHJo_/![Z=[{Y_55UUh]tIg}2gŗNWʅ|a*=Gzn:{=_>·߷F^-";hBEYl5%X/,`}s7s1dَn}a$8tźh2@XU21|VX[쾧oa$45578ﭭV5au\GFB۴ӿp	&0ouF˽ϿE_Ow|ي~ @!2xw<"_Oqq1>&MBJ/_u[L@Yr(Іzn6pl{"n̹/ՃZ5uƺݏ&tM|Z5~#&E$
ض(T&I!v7wla6+-¸Ew
/m^8*ǐ8Yg}ʔxˀ[N[[
n'rI|ҟ6ڃ۹uܳU@C1hՏ'3R~#c	*n8~ysS2e-ZĢE=;TG^A7٠UOiҴuR#_^$2+g;^#:u*c̙չ:{F.(/P
xubM~Gʒ|	ëwqeT'(NphE)_+*WI`-0غN.y_OCy.C'm>iVLqqel۫(*U&X='/sh~~|헷l’%K5jk֬J$%qSvtϨJ5*kxՀ(osm_xв~Whll䭷b߾}Rzo
	]gN$azb
=$][ה|28w ov&5^X+gBeTEaJn{_:{<8a̦.TF,r
*%߈sQ8F_?voR[[뵰_ZEӾN5(y>Z>P5*	h
aZ^dg}•uz{^8!jw#|^'׆S	Ic*6R(0
@<ޫk+1(&H@dI;nrdbm1E	2lQDq%#z``lTv\k!ew$E	l M*w?Rudoj֗RH{FT"Ufͷۢnj=>,7iT|@e{{K'w9!8
u.Cm2)
n̝<ҍzϙpnQ|#!zpl#!WPT*h>ޔIk;9>#?!eTv|nQ͵tN͡w
dݳO(^)~"J)oX}
Ju7-Vd݊kT/
=PI%9yh
bں١LZ"
I|;Qt/Dz74Ɉ"L4f֤pdt6֟0K'ᐴj~!qU%kES鼰MJY8W$jr`sthUc1\5IJ΍-7RY1dg=0?̐@C#@?CҨg
Wˋ>0C	t~J%V_w?jө
4@Ei%)x~R肈B~Zۯhdƚ@^n~˩kyw?""/GFgX[0ʺTb&Ԅ۷^r{
I;eQbTu-/n?EĽB @/L<`>~2wx͙jnӲ{T-Qo$bT4[[X՚/{'7yݽ{}n}LL	-PR	$!	BH  wL{g6n{HZil	>=IN33<3>٪;#%MpBUOi
S,z pB1pr@Ŭ1XyE G#3#{HßA?4aC𳘐t#2tԥwk{gF`%Mht!A@*Z]rp9++.?]{TR'?ic3!P>xȣfiFH(/nag
4	1gAf1b׬xfAVP֘ 
sN8LؚoV n
~"sġGJ^jA%C
9ok$ ICoB=12QPk7Iz32QZ3P)ѿU~PƼ+韠Z4\Y?4#_}ghgdEwZg0`Lbl:ڗ_ 2C}vG sUh#,OcH5ҀGbm0#OMR8.9{!ߥ[G1ᆽ3GY[$ "%whD~ܬ[{uWR\:)ǩRECG/,҈DwtF3{
uͥ
Q\q+
73mO#륁Bh	k+ug
<S<#pC-ƠDA7Œz+H9:ǟ^l	uf&mCZ$% +M3ҹ,g(p|[%xh>gRp^PFe
zʈLnru1-ᛷin!~/͛J\T;")KC;40coDQ"uµ%YqacO(N7b^|amLچڐjOɉpO!]4!`dӗ'̭i Gy=aǢrm#GІ?u)p(֕CctzѨ~ɽמkgt~vj1{Oo&]xQbi7Sm|wbZEo
)yn޿X;VmCmup/Sox:`awS#Y:|&uLy.IKSo4t)!.`l^ZݿE@?sea$n:6:;w\
c"qT:0%`Cpuʆ6E:Sw6h	,scTp!kzy!"Ty?A6R=j;.-:\xՊx= \w{$
4eA߇62ܔXVﮠ'`ψ/dvak
AeX0}SV`BxZ"Ι4(72
x  b4H6*t@~Gm{u<ĈX #R#35.cu97k[յlA07QBo{94}	7PGêշ1<3SpH:;Fsl߻Aτ
?02/AWLIy{g[WCANi~bG{%
cڣ/Wx^^?տWQ'uNn._s| 
x߽%[ .@&!L,.';9	C@aEb{)mRgOLrku@
Ӂ=ylWWmœFd&0%?!iq!*Ֆ	d4[:׶+~4KIfhj,Qn>Ęf>Ԟo%1)
R\wYŲy$'#˜8@	r}/~GAy~tK	%xj.#y8{uEvI1jV?"˼wk>)5`ph,0߇N`A~36u||Wb[|;wlI},"⃼r/|@Vup>
1ఎ~:`\A7w{r	PԭkG|L+UM,F&e(:A!BY,&fL	؏o"%s4a&̠kXK6}V4OX3Tǵೳt|G"U^Jc[N֋BDJkc8%U^z~4g_l1x*@/z(ל
hNNՂ`*^/3|ȺƟ9S1	_ful܌o0C1YQxsAn{q#wW╵yb,X&4 Ij=G8 yebv$cgxGBQ~IJ*AyϴYGv*Or6$*ŴQ4ς#1<3ѩ{u5},s(9`3=?x PZh[]
džkx=uǖL
֘-tkd
~j=zcĄPC=: !5utsݿvSTvְcS4#vוSn|4 	A^(>p++5~AH=RkrB qɶKgl^
/8\*8pI7\tOy?8wX
tƸ`ࢢZnxnwTE;y~A/%#ͨDoibRD ~@)feįUEPh?-aEq θXN`œ"M](
T5w*.+L'%ˊL"ř=QWr*e$uEc"ȟl;w>p'(sј+;AˁEslF@FnC.FjdE#4S(
w/|s7Mk{x{c5-]5&-uz&`>A{R1e|?LQ/)fį(
7{-vۆD-ӹ8w\*gI\81hꢹm\6%`ޟA:2;16).Wu͵ͦ0
#}lP,϶ڇ)4ƬP8P>>\f3dgbx,J[Q(>{S8XÍ'm~&%~ԑxt_br^,7>OfZ~"}:5惍Էuq8ܯԟ:F(*?}+F0IKpn oBJ	_߽&˲_"?5Phn!hf~#;_!Mry쉠IfŅa81GxDCEwR/|6Ǭ~/qWn^U%Lz 8݈Q(g2"xzM5ZKeO!<,hRToUWE8yqh
hIٜȧE5tx|qL׏L1ѲZ_&}[3里11A*aD
O01/>ĥyr]/ IDAT&щ<^B}[/MI5%zs|]ԟ~ȇer\83#s~q?/NG7ƴ=bwz<[Ky%HNĘT@(ͥ30ʱGJo'JW~F푙T-]"\6SF$')m$)څSK1iVv7sìA`=n/Z+3#P=ueQjbB[:KgsєX.ՍWfQQ
ɑdF&QhYYjXzSN
о[ğb{h\2tPA3w}}*}z,%e7[z=ŌJbtv` jf58sj(w""ӂ/M}7&F[z֖2'#MJt(yq-uݜ:*H1?XPLGs!33b2?c(]o0)lAq$z{=\zԡԶwrY(HE}¹j.SM9&~U-!W8#%S]5xeNۇ>C烼X9H=|.r׋
t. e=qPC33ӑ_}oEɉ#m$m9	nNqU=ƀa˟DGW

7ˡ>%;1l,58f5+Sշj*Uq,Cg'gՇhVI<{29nCu= eeEW.7nV8s
unƠڽGS |>(NXlNQoڇ׭p‘^#c.3>w!؍(a5V?boڭL/P#iG
#GsXj[{xw000HekNV#nD0؊8MCv(fdҏ٨ox6&
k5 рqI"HௗJSc:H2pdAX++L\5PpfMI>n;^6duߑbO}PzN9mZYJ™3EL_׸S1u?ק5kӡTqIu;=y̤('HoxtI&mG;єh/	w.wwybXtP_
9RRB.i8O@7OSμ?ķˁI4sVrp=,EZB'$
1"A]{hD|OvFkX'~((ZeM=,ؤ~8Df|$rմ2ڒ:]Qњ>į#@s%+lQ/']~Ɂ3P[Ƽ?IW;{twcդ!R0)UO><GVxq.Bv;eL@y`|[W/34-!iig8bTb\&KnQ@0&dF@?퇘?/
bTJP<2@l?W"+It\6j[{x15	?`;xri eS՜$DG&x2-C"peh˵D;6v?pN'x}cˋkhe63kT.IPȌc{Y{۩hŵT4ugVshP|o/&pf=dEa˛76[7KfD	12	1!G(TW~%X
$-@7(ALxeN֕3<#DA¢S J(}ՇYcyuc-2=}lWŕ)"OKCmA %QTAIu':99?QUlg!U~֌}5=ڶZ+k2=SPǾ힪V>pg^G۪8 pGP}G%:{e6jaYI#;+zs&w!ygNH2Rp?nExO\6<٧CvH4oĪ\[k۩J@<uր|)$@
෶湓 }݆шՂno]3t=
M _ ߁nNzx95Ea^]}25ի	GE鄻$ p{{x@(
S0KWO[l8X[?*(2S,XbA1}rZa|iL$0hK*Lņ;ln3SpdM9뷯_1%'Tv!,!<129pG>X?,]U`%8|NV⣸\vhg,qnVv1H޺̞2LפOAPw.#_N"=֭#֔q+`(utj	CHFe`-~[V:٘
FbFQ@>Cu87KoFf\XP}mui%3CcpI:1ޯ[δ?ڻ&&.sT]x}.l~V
Z@~h3o}|d>$}@ĸ!);i$<GP%~lRfC0,]?[]
qe껹R>,j0i1.wb:<;0
Ȣ:{gŅb-3ЍY_6 (]0JA}`?pްEyg!v&Oq||"fL3Wމ}ٿn\ŤauV3k':|Hfa	2&cr8eL6	w_ɿx6ե 7e\0
2;Mt)&%]vQ8P75 ⽙hoМSX[f8>"(M*@#T5n`C8lswa~P}m@{}+P5i}OIյEYQT8|?z׉h
 \tLsxFS9!7d
rMxl=,Nw_7(ÛP:,}#o_Q`c[YO&GTaQARfÈo'B=B&z_q\Nk缿p}Է$
|IŘ ~ɩw7pdz͙n<+pCu.cZ67/PVBV2-@ʙ ,u0'91%DMz|iD9q,%osYyٺ>O}6"w.	3Xdr}n9cȨh?@ 0P#vM+G~(k缿oT3'Eo_bj^,:1ЗXTr)Dpזpş`L湅tzlh6j9TLq}a1@`MgjtQW7U%.3&
L#Ô6eF$Nvl9-͈=Ub?Z+T!
{*	P	߾B}_6-~|Mu_'
jos$!qh)HQ
ja*jQK'I:y:<4CWW
$d6Ծ^	>gUnuo(3s	a
zs7~ydŇ95ɋT 7L#%Zx,)A7p^60'߅-|txgDŷXX\+{f}E1*&};#.K
?ڬx7WNUKEnTv
SAcyďIP7h-WO-:8KǒЫ	BfXDw¯bڐ(~߽Hi8
|Ag5o!
*%DQ@<`9|56(r_*,|Pn9əy✡'bg3 p^{R0XF;[]4u
7!"h`)y$'
ohu>JFWWz÷Y *MllLSGG;xl=Hìy:ZpE"Ƥt6KZzSÎ4QXwT3$)a0~?p?#L$nDzum=|uFszl(~qfW2HfDGr̚`h3n`>VnV_Mz 6:	cG`Y3p6
zWq|R2,HN.u»	99l2sYf7dfD2G1}ȼ+R3
2"y8sTDw)AI4^XkzϗHT)$v|@Gg7ӧgv=8
]ZǪ=q8aE ^ꈢ`gܰ?)0C:K<͇Nf?&e4mk~xyvϬ*Xlz41D| gYs%}|E߽9y~h,<ºM[maIq:||=@$*pд8"Uz;(Bh ȱ'L(ow-5j7pn?g(3t{A&T8SL<;h8@GwG Ha7pٟ6ۀԫo:jya{xl|U-ʫjJOaRd)sUgRG{ihdָ<cL@C˄Nn݂|
'qR~eng|nheYI~-9ܸ$ "=*5P7
 +27gѫxccÀz}LJ&?iNx	,dXWDl1ew]\|heg43ihz魠Fq#%1ek@)[NI_!*ZM_u	Jp
<ek7gYZ,NDQyB"c24B[&AifF4qQ5ްٹ樱E~ム
R JHIC0l[;{<	:w\x!l./,x(76@\"\>,~~cn3O-R0:Ft̘AQE;/zbLF"FFYn:96v5$&&s)ߛ}%Scpʜ{*kkmKxS[{v;K#mda't:Xb#k~Fu5.oP}=XHt)|PTK|O΍?H@l
}4vGkב;!=pRz#@ﯫ FHZC{W/|m%x/>*>e*=,_%ض
(?U|=.ȷ`G-˶Q`JeKzQn7͙/.F|ۼ3/OKr{`#hoEu+г
 cbc(_^aA[ॴXe	ttu|CQpG6Bv2rIDAT:J(e¨<6:@bl$u#rS^on/
Ea2t+ݽN"2SDw4Rl:RB.+R]O7$3
E:GӉL#pH`$ëqCIJ[&	'e_Xi08s	8~/€tY%œWm)dtvҀt4!hf``Rn}E&mFCJBHe	.dIÇ)~EQ*epSذQX`3AIvQFśP݃Lϙ
;S݊ѨCjCi#*ф	g$;
шq!Gw3lgme]uy:`=s{1fD.lq~+`3AF	ٴwξM"끟!!0VӃ(vz;{^y>(T:Ԥppiâ (k͕yavLᷢm}ҳ~|+CShiD=Wi}`3KϚD(nSyj qɣxƨI7, xzb3Qt Q*[ygm	orΨƽǀrM>ZǘY5rYSwl+4bܔU7r6FU72ɱ2&
90Y<¶|a}-^F
ڛKvz"u
|8eB.Iq[&JgCqe8p1p%pꚃ 8$	CS6*飳8it%H=hዒ
nwˍ;FlC_V]ng)óMmc++:W0^_	T7Ral>UE54YH`fb4cBAn2'$1$5NxpMeuO94vcxckN"9!%\qD^]zю8cb6)l[IE]+5-=LOdhT\/7tO$DKnj,q$FNBt88$I0>̦.;oUnPu3
mzX,>|x˾Z]'pҸ	L$?-8oƧ0"
,VT:u;_'P5^ްpNc&F:8!;SǪ;s?$njbi3yy;i裮C0L6[/yI@w U|ݨ+@=jrԨbn)()$)&b*^^06	6ƅ$Gڻ:{ =VBՅI1b"\yHjÆ
6lذaÆ
6lذaÆ
6lذaÆ
6lذaÆ
6lذaÆ
6lذaÆ
6lذaÆ
6lذaÆ
6lذaÆ
6lذaÆ
6lذaÆ
6lذaÆ
6lذaÆ
6lذaÆ
6lذהּw*TIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/mcomix.png0000644000175000017500000006177314476523373017300 0ustar00moritzmoritzPNG


IHDRA	pHYsݡ=CtEXtSoftwarewww.inkscape.org< IDATxw\UlvlzFI TR_
A!t!4ҳMv7633{{$H|H_gyIq&#1:	!	!B-2!ħ#:
5*Uq`O3cGktPz}_5nJ)?>/gu%!@FJYso0TFeBd{h6R` p.0ET)$TGvlJ)c1G(	ԧ@Bh*۱Q4g>gGˤ8?tJ'-";#-68XLe}=k&:DBa7$!=ҫҫ,&9kfߡF*^Q˶l?PK4=1).gKNG]W3"!\̸EXĈdDBt>iT6f*dV\OJ)b@u3뀯{es;	HJFѸڝ,ݼ6eOU;J<aR$M}p+cLN?gTB:"#P@ڛZQqXx\EƉU!%"	!pFt2r#tL}R^ûkwΚ]T4oo~KJ֡tPIH|WqK8w^$FXm-m*i:Dzji0_)  nŹ'g!iEňR
|;oG-s#
 !D
mF#<'#¹pqY0hM-MwshAY[eRdk`wEtuU7-tuG[W)e{s#!!h#07KNy?$u_9U˨~v=={ڞwyxaԑ\QEs(TLChymEOu[ۀۥ@H1pVe'cK=tMݴ45H/$ƩzΡdd"s}_^@uC{YJ9@awmiMΕ3GJx,FZ&ptask˄i=gnFIH?WF
BN[-wͨ?-.W$iK(!h+dt&3msAW9ʖm=H^U
&T	wK*r8)<s+[ÏIְ]Ϟ_BF#.RP/} 
%ud85`˨jN$4kǹp]ev㾴bOEsH+;l=1D;]-҇nwH)_RJ_z/C.=aן1濎Eٿh9?RZ@vvQWTDa	ȯN7U2QB
'h$K
|e6|e%1kœu_t/PBlYfcގx
e4HB8
^ Y9vxx^V ;&0'{3
;fG%w=c[.RvMBJ
xЧ0:Eyf)%[R
"D/l,+_~.)=w{K"%Xn`9"}\
VQmOgǁ:l%*H;J位Nit{)>b!Tu$΅7
ѺFDF<_`
WZ3N$ajingd~;d)eYK٤/$%h_`IMj[κWuɭ骛	 ut̿ć
U=߭فeIOs9Th\g/f޺=FR(!D0}b}i~[f
TUx~RVH$_6y<ǵ	"'1TQgEafTUrK˙̨=Rri_/t==Nן}8BJ+ְ
z|s}H%$R%cbpש3Ч$GFTi<icyt*Ɵ4a!m3f~PB458GK66?PR,᱗q~ ]*9@Aes^K98(.
*;hK'iJɽ/,U;⮑pҳF_@	!/27_qog@Xqx~R)% a-{~.2,~r8+l4Θ<Ȳm	g}7~v}N&3MN.`꽁hj-K*yEp:؋Sra2Iשa*@"9ϥ8H8+|@+3LIyq(ߤ׸y=πY7Fq4b6-ߙLqm[~fNBvM`aN0e@
	$__v?}Yv;7K)@|s
(!`NePOkKaqYB52⺝$쵁H'+ 
 p/,#rD0+ 9iC|knC@L)"G% K)0լYφ`J&f኶q.~߈h(9+LCQl^뾑WŖbˋGFOWH́EmS=_|"02>|(Ǣ}{X%yqb,
dvT
KG^fǕ.`){ZtZ
6.Fݦ-fM.ŵju	6q\JqM	͆2-:ReT
`Hn
@&`{ژR=;$>KcVP*	k3Fq܈4{;VJ1e9i&2e+izHa}HńI1?s2sITR%p>*8 SY7{m_Bwk}Bs+}-ۼ4h"/<Hpǿ!MLia+0+pJ$J~BGUc}[缏?-'_8ո]8GW1sl?N?ײe3e
R/)f1
w׆dv:-c%R`?v]e+ͶRb1DOKPbso+{8Ap>MEd\kǜ4n;4?:b<0ݿg[5WoGbG=@x6) S[;LoGJ^./8r{|P&q	qJ5k	@]uW3oUpSy?٤!!L4A6Ա~fGDwOjwQ6Iy*	vD% 7,	CtAiOF
`3f[vl	NL 4Z~$fD9U	e-Ye{YRާrB@I_ohaV5]&a޼|'Jѫ'7}+pwBc>Jq*01mVz<@=%&asܪRҞvl&(d!XHv։c8Ae_xTpK"ҨFeiJ],9J!i0.S06SPAȁv"d{+}Y `Pq>_:m8:BL3?O	)B,8R~`HXe>lU^jT؄Gt=/WMKLT0`q_6<0Ճ"δ&@'bRv Z2@lT˶(-i@Q3+g 3-d.99Aei.3"8/[2(TzF)9P֑>9ۙR7RO>ˀ\3inNYz
`)Z,ú{VO5'#=v/=$KƤi
\2 „L\ָ	8O?N`GT
xvWݗC~V:?dn8s4
ROvFˏn*+>6PgJ!Epӌ~@ı)s1jBs},<ƀEP
"l£9ȔLX
L	*6,y`C-'L Cg|4
D>M,)UPTڢ]PT=mjŝQSqSϊHm;xRZ:i<2Y3,=	Wg.~-6=p#lՖy(ףagpH!7+Ś@E7H)?:(C_#$zr!L7z2r@/Z-h4}Zҍ9Pc$/z70mn4	EP|'lRǎncr;~齳|!yq,Q60i*ihܵU>ғjp9Ws:%wFn	7{-wt|5~}
ك0gv{ 8ۨ}zpԑ0aaPI!pQ抪:6<YkvPYhe jGVʀBʣ8yY||	L=!	,fsY[EkEz#cjIԭʝ癋>|\6گ[:/^3rA$]A9wK.j߼	}¦Tiv0TT\U1K[QihB"*2J:9?PÓ;";G	84@̦5!!.9cW{S9rhP$pIX\rwZU#-	e)1l;8"6ĸeA3Z~Ƅn8o
?r3@5![ ֎D B(4d(H˄PC>;Oɜ;yѸ-JM=_6I~vRۼ	@,	@s`Rg]
Y]`b&[fM6P*7Q^In`˛kx{fwkoqD8bˮB(L9y=vn6UpYq ƍ5خ1=לWa:\*)k2c=l픠=rx}?63k2"!&)-H[ɘ/"-RB:ouv!u!jOrCXзԫx"S(PrR|s1}󝅍Gn_BCdkCB7HOŐXt"emWj^RڛwJ#xu)JwbdsqK41^TJɂQ*t	&tZTwh!xq%x`ѥ~8`v=$?YWR]8V_|XezkWsca&jL"[m_FlLJH5(>vdDP|W{%DLHz!%=l6жῨ5ʼ	-8$X:fhCkyE-bUC))HCvo8|zĔ/jb:yC)λQXu կMȦ RBx1ny֘DQz7}}=~N*S#ϘeZ~}l@U%?{~1#Zßo/bKN>4$!W"`%ݙunBLw(!Dȿy4F(vTZ_AlBjݠ4<'IJR6&֕Pco5־]1SՂ"(Nfs`X_Û3b\zqj݆s[&2=okݼLusH(I~3Pʣooa.0:״d)u^TI~5-Bt*^}zw#@By~F:Uzsʣ觯>ؾ_ۢ1VogtK4*_uTv,CFfE[3(!O$rwLiog25N^|&L*nwRưWnH173<±zB=ث%o/j%u58*Y~|:$uhmq{ԉl
ڨ@4#RPm㨫sE:;REuqHۉ^@=똡.7*0]0VW@m}`H),w@4$i['18.b@, r*h_&?~(fgoc0Mymk+-h$J~68gNϟB	yf+!|Zei{:=¡	\r,lꞛ}+[x_ϹSK8/"GP-a;6@`ﭲm! %?\1HȬ#*ڥF6+Z^
pX¿H8וJV\s1q7|>
O҆R!-utfyvޒq;yxϰd\Yհ&b۱Kg6暎4|༩(ª,f/2֞%od^UA[]6
PpG6Í$+[JwO7qWjLWcxRxj\?	h+j]H316]UKB^h'YTɔRqX ^=^s[;5҆s2gd[|vأu$n`x&p0+@0	k0t}Au͔p~e	(GPgsX: dSu9~ cWU4'ᙶ9<R;vdC(BkFF)>
)ZjreJ],f`&'ۛͼxH?x)	=)=yIx4lLc'_Ȱ%pV#׻uǐvVi$;Rǝn;OfKA`}y<yk15`i=˜*jݗAulj񩰀pF278ʟZi1CmR*JzǮ5)]KQ:\-
Hcܠ|\@xr<Ŷ@mM/!٤f꽄#d2go%J8t'
Bw$T׆]$\H(nI
Vïr:#i:Ro
%Qk#sΦ LeHH'Ks?H?S'5@	ȖN&
2GlB5Dζ?[5BR`GEF{\^}=e͹s kNykhm\I{Rʸ/5rvJ\u䖎$iyYZR5-)$UfW#Rqz#F|*Ҏ_>oӝ&Z۩k
{!5'_	tg%̂~_omDv0;)	{~##&Ruֹ0w;ȸ63RxPC䱸ɋ͎T	1F0UaC::um*U
e.殫fSEtϊpܰbًc"5ծ 'Øqf^֏u33, &S@^* P-cmc*l@hȧG!0/ڦVZt	E+ظ
4TIW-cr<KdYc)!E=FOMe)^\]-ʨo9}J,4PfR'u2#]ySw̚s
km'H?{
a;ʹyD@cH5:n޷UUs׾;̈~lB}Uf2vnUh`z;#+G_L܍cK'@Kv2С'&pyX2X	K;'H)MS|R~tp
3mc 52PUkfK2mpyR;hh
1;xoSj`_jz1( 1d<1cN~%PB]zABa\	0.'geӉ7y=*~z/N%k:34:5aDZUBK&GJPR$_;L6F#	9gd˶ǹ}WVLޓՆ@S!?,{7$l|5lmj\ҀE<5>w>	'lUcv[ힽ%rm`v|l
2>*/k4Kt]Bٿ(}cwq$;A`Φ IY
@b0[X}sᕒr/r`8?Z@]S+iauֱsŁvf#pI)u.\GJ9+_^ahi"9~@g}'^+mIϕ;)l	X\VEO4e
@/i׵kM72bH)dr3ǻ؊˱hiӁzK'5cݞzZP-K}UtD?`(]a%|?uM><GirI๧tx6j|Gfϕ'*k9ꨣ9r$[VrΝgGµژ,zVtǃi/>Zfs6kre?bz!ximq/8$Y)!a㏃t!ݛQ?.JXQV‹U%塚Q	QlY】
(@")gp9ɭTV9n7eOMiKl>h[gH'{Cq4_&77ϧw[`>;+J=2H
I<<pL233	~u=xqa
Pg~K|⊰Miި^Y;V
^;;4,K)g~[R?	Cl5(rJ>""lO"_l]mV.؃K)"3bD8_h8GnF	ߡOmYmZXbsv2_^oK"I}g+-~M80FJ[vs]wLs\oo|ȏ.>se'KH"6wʩ#yiM+w7WeҎ 3_.x_:C
-쨨l+Rʃhlw:*ެ2j[bfGxa~-ʠO^,6@O(U5}|ÝUf-ؓzպZRp3˫>`Sqo…aM,۸Ǫfe*.҃LmݽSU:>OM".sBJl8nᢷ6ֲEp0JisKU2r@?C_ALK71NF*Q;3?Ľׁv=]Ro'DV%K$M@hӄe-̶֡JaF"s11`ݾu^Y9fgR15iq{AKٙ^ӇAze6'Tk \Rʚ*.ڼ
#R5yI%SL]Ss
-lk3S
"eʭo3bg6mstcTDU]9H&qSzEw3t27?*+(}nIV`<꨽wӮyq՗\8RGS-JQZGuDf>B t@I)@U\|X.A^JLq<8{[y.2;||^sO[1aCu}f_CRQ݁=ACKPqtmp)	e\Ҝin#;ګ}ҭ$%ī˶l!?R%ً7[tV H(]
t8F^-{WJDz6Ȕc6'l@ʛ&JCRƍ4Շ>G=p*
{s?s`639npIBy˗X/a6Wgui̮RL
dCNJ%i.h'l
ȎJDDq3's)?;bzc]ÆpF2dc-%P'H:IvO8I8aPKt~3Gre;5a!%9PCm9fRn5Ef"I>iܨSjNJ9H	, ~ɖXp|{e6V**&v l0d>==faI
`)a&M20wV#*|o<|>@.I)͏uYZ+AIõV}!⸹-j:
@yG97m@-D#COj>^4F/
h;I'!$JN;Poz ow7kOݥ"=%r=e7 Ɗ2 T89ytv7)-q2+g\4'1gVQz$̘Až@
0tDqD #,IuLLYq9=d[xk'Jl<̩RpyvAi[3O9%,fRkH9(7qZ静޿{:Ê5Ih(öЧ܃_c{W
`|$y`!Ɨ=$S/h`׺M+3qmrzf`$	D.kC!t	̍0wfϝsp9R*wtzӧU>n7OOvkTY颔^O%w9u&:.}^PB}~wҼJ&)WԦf]4cQH.DM8縑3|e
҂;uQ`Ghd@;V׷ө#
|rʋpQ=o	=,TlЯ?z$;o"fwt0`

m6:G,%ZzwnupWO≓pr?מ1!ipN^MwH=kE~$ɶzH IDATgq{N1kObDe=2gͭQ@,ROFW^l.-viG}y1zHd̥$_sKZdN}k[$Q,:%9>էRlu t#kr{$otHd*OVׁtBy'Oܟ]d66۽N&	M:֖7QI?n2ٍZ|HO{Iڶ{"`AkQ[1qhozgcnE5n}~n/{Ŭ{&aH5wpR+v0zwй?K05P:A@I)XBŻJM\zgN6fqLHMWq*݀陃[vsL#Y>wO:O|$W%"k
Q燰"|rSP(9VK7'R|
==Rlʹ=+{
s1Zng7(=CB'Ld,?jMշ6&t#UdZR	G҇?`J[p7F;
P`wjskM"%P󺪮+k2F4Pf$!AY9~s~zƉ[WPYU+G߿*yeKQ@YT^ƼMlh{N:'(fPq&rr{Q/>Xǘ>F{LoouHDF>̕\Wd@9J)w'LDRPpn?Bhykg%V*EcwˁFf	/OLfiό{eZqeII`ΈHUˏzjRkqJelh[v?Wn9O53+Ƴ''0wMmq~r-bYId`/gSg
&Ʌ ^XX(Lx>ٜ=ZV2gVmE2zS`j\ִRKǰS<镟6bmO@P{S?fDzkdU*ϩcߴ]*[ZieYsV%a*3GUT71G/050gA{z}apNBGI)&R{
 7&N2ZTم3V~xܙwEhpC[_Q.]7odfz1Zcj"=$`gU|kv1ɒ:"\G	Ա5V?М\r+'V%{xͭ\r1~xeAzNGJ5Ooۻg'P`½G6S\\UIb)ov4Nِ2ˁsVp2ڬpX]:H#{ebkϋP0}v꠩5QX>zHzEΓyۢ.oT?PyT??U*MSk<ݸ@RM_Z޹6'u玳GxvÉrWY1Ýu>ml3mΨNd-9wM:,"}'3o4jƹWZA\M8uz@|
hk8SUgdw'n誴YOulpϑ
6D+뚄 } l&U׷pOdfPΕ26ҬBWE0gؼد6[wQzټYyi:SbZ*@Nܞ6PY
6G:o45<|?BӴYo=RE 8@\L%}ˀ}T'lsvI2^Py}A!+˚=ulh[VNV*4k䤇|Ԧ;vzx·ߥ᳿H2
J~	ih3mlt%Hkp6ZTRUL@J?}h?-u|ylVNؐ`)CL’2a3k^?S˯,DJ7f/;cŊ1۠	X-ݧ:8'h2~":K!mTg6Mյpp޿y#AJ7EUVAd:f<1z[n3=Nc~(Jo~3-@2kz']\Nvn#1@'Únyͬ*>Qɜ7k6a$d`-ő=*/,f/Z)=I)F~SW",K@NЏ{MaSHJԶzԶZdk-e0w`(~h>4ׄjgDY>m\*3FWya6ru4x#8XsXWp.GkߧNkz?{*{QdRYP674-72"3@G6!cxRl@C!Ø5*O6dlΎ#L%nRbmm$q/[arL$D;	Vrt(GEBf/С!&d"H>_[qͥhYxrH0쇔T5ssZnaY9Z֫ &lvl9W;L%JJ ,6sKDiهsE=*7|o<Ѓ!bAIscrzIqݏٌt6!jZ9J?~(ف6:K7m4
.}zmKל+sGsIXZ,+nBn Q(Xڏ)m]Y3v:
nJʷ^\VϏ/.pT
xXջh0"=yo3		Aί?=c 7aC׷y[9T'w=;Pꉃ7N(
π,Ȇ=%'|p{T~ɓO9UgMzsTnv2.í<
f\v(n&vWkyOI߷M"5Jn$: uUĒ-Nq++k@V2n`?<]뒗L>gWl38')kZy{v(O<
Rʆ.qp?V\ 5X-
2VILT׆V&޺ZMw,߮Uwb
.IeS'	Щ)>WLNbluLTAjgF"I:7{m_	%ALD`f3a
xD38$LRb`ˣggW>QS[8Ěr>))g*=< ?i#7qq'B2:,
d%3|`2#SLNZi$,|7:\#m|pH6uXf5),P͍|sxh&GcJ!J!@ SIeb0#faZ3y|0#
h#ﮨ_L]E޾oEJYFveGvղP@#P-)y*Bgg#TEX-:bHfQhdwLKeͣW2 %i]%M	kr%qV(zF(c;'BM%V#@ǻ52)Bp9<\Ϧ/T&-iwϣ/GL-NjZUXLU𔅛jdPrhۜnɂ5Ovb6B%])D*yf
&SpeyNa[/#,1BFWwޠI:Ǥ.=B/h5aF$F|4~7͟הwIOOeEC60upu/M:Xl5Z\,Vs-F#"
T3f$b\B<%x
a]%+
ִaI<<;8*X2F3ҿ*%66pK%|YHS˲
rD/xIz02
 ο5gG8ﻨu>I(!D{/eܐ4~r܇wiD
'Rg/^MYB"nj`R"{`@MWExIlkvxtԐ	DG(!11 $~CUse쭬uFSd
cb"&Uqh;@"ѥ{RyCP
2f
F&@X'9w&^BEc'm550ʦpL~zƘ~u0p{xsaYy5ƈGcќuI*5mAx*/	p<	2`	0po9G1Z܉t&`,eծz1)25?>?5S$SI$_;h6`*1UKZoLG$ļT0fHÚ8%eHkW\.,ŗ"uq`:hNw͙ݗM	)"]|XEXk*h/kxcKLI`DF,yiψ#9fֶ=(l=эˏsٸI(S"!Qݓm>[R=2}'4w,2y3>0"lud/wgf"oPykQ
ۭ$EpD[I`QMKKMUc.OnS5:&qZv\yB '`K	-?aރ?$BIh\<yx̛ꯚV>d[fj03U6vz>]=ZƐ;~^"#f(iNbQyv~cZ~T\'ʹ8	Hkͫ4xKQ}!<rg#%|^ʮjeWuUM*[06vjgXh
ƒ|.u~Jl?,I٦DXU|;^H׀mNA=9?I5} v:ˉZ_R
DϿa4UIcv&67Ee!&B!ʦe!>ʊ _EbOGqd!"1×ujCVQJN $B;p/KO㞹sJV"OC%(RuL~dIDAT;rd2H0>ap0MbtC"HRyU:%&%!P
3}``_z*8I(!yxf"sz>7_2ѹX-GQ[CVԿ@iGhj>G߯L(d,q>=GYg}uIp[h{q}'	92_^4svEKրހtXÛ2#5ݡ1MFDu}R?7}sXI)uy'	!&UGr#
]AڑfVp6!;Pqхד##"®LDţD""aCU]/>ξOUB)_"-)3KrXx(8I'4G%@A#)ó,Fa}9
GvVaWlF`97M$g@~|#S(JN'0(5~	s?=A]s;k9xU
(;ң|Q~T>`%:(O$7,`"Э"*JR8;Iqv~([5NN۩mj/"fW[NJc:Fшv*0wV`ZBb(Rc8x~$	^)tWkD*ь	@Is&hIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/mcomix.svg0000644000175000017500000003362314476523373017304 0ustar00moritzmoritz
image/svg+xml
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/tango-add-bookmark.png0000644000175000017500000000125614476523373021433 0ustar00moritzmoritzPNG


IHDRabKGD	pHYstIME
4A1=;IDAT8˕AHa}ݻr[#k-,/BE¨ A{СvCt%((+ЦA!xqS	DZmv+]?k:s؈ "ݧ"&&.ݫr=
]5AV>	P}T] [|䘜S01>8z}C:7KPYZ^f~a[p+E
]ܚ:2(g))jJJPrV(;+TVܲxP-1;eiPM0Kwց1FFM:c,;߄iѦlLv
Ob`gn	rU݁a(C	R&q@	
eY Ɨ0B fx`ϫ#MfDd{n'B||3yy2?@mp)\/bJi"/3sV,1;ZEsSpZ8vKo[G-Cb
4ET }W(Pٲ

ٰ|r@
UPmҏHV?1(>9!dBj}Xk`<h~pb+@zP$3_F0׌Q3_Aszy]❇{
3Al'2=֮G%J=.]q{ klf_o҇)IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/tango-enhance-image.png0000644000175000017500000000163014476523373021555 0ustar00moritzmoritzPNG


IHDRabKGD	pHYstIME	2P5.tEXtCommentid logow
IDAT8mQh[uMn%AlSivIѹو1|8'DA}`騵CFFP6vOeiҮ5k99;RJ[[mqU*BO
82u1x'F8&zAárWT@xKIujMGWHG|wΎd+؎E&3= ;G@AuNqX,b>z7@J\\Uj[+%ξwuHi@v;ہ
!B*y(X&2S(oKT&Vx(s=|!xtFԮ>C
!$y쪍.JH!hjjeaS[&{CW$0W,Gw[s4dYBk$0~%mG!"@@ }wAK83BeEmPO)64{,q3KK歑*:~Q̪IBss377BiBHfs|3y^_>uEClXwhPI >jqA,ˤZ4ɫ_?Ξ'^fW-?S7.PlXM7IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/tango-image.png0000644000175000017500000000105614476523373020160 0ustar00moritzmoritzPNG


IHDRabKGD	pHYs

B(xtIME
6IDAT8˝MkA4ݘॅ\ĢSɃ@g(1z @
՞TD*C"&nٙab
!{fgDQ[3ZyBQ3ZjQ31nKVWpf,µM4(HakM0؂33sZ$(l~{?CR@4yò,.1SrzoD'S@)_@JNh'-\̟.@:} b}x{_ʧ|=({hmh9%_}z=oj%1@usIo?GN7.nքaQN&x3iv$ĉqǨLdk&|[Z?
Ƚk^CrB0EJXᖋBIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/images/zoom.png0000644000175000017500000000155314476523373016756 0ustar00moritzmoritzPNG


IHDRasRGBbKGD	pHYs

B(xtIME	PC;IDAT8˥[HSqǿsNwΜwtjd[PZv](z!* zDBZAP)[QO֜Xe3rK3l;ߋư^~~W,[c:p\$fAB8a$	`D"><$|}ՔcZ!hkomEM{"CS
Of(C'lbһ]+d9~4[B[My{ ]Զnjei]{p^aYnpaW1tL;32x?R\#Y0ɔv.*VU?zP[r؝ý>iPg$քW/(#eeY1i)NJ0=dT`j?2Zc44qSaaPxt e~ZFI铀U4LBυNgoE&ˋ/s9TF|PT
MAXhg zɖ{F4=|dt@'b022)TUgFnڼM$iR:nhzW3uy~WD-nC=_LAQ)*=9Km!"nݾ)m٧H;VIq>(e H`0Xq&_$$:^=:|%,>߷w^׮_ G`Y.3S_~!MPIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704114443.0
mcomix-3.1.0/mcomix/keybindings.py0000644000175000017500000004015514544534413016671 0ustar00moritzmoritz# -*- coding: utf-8 -*-

""" Dynamic hotkey management

This module handles global hotkeys that were previously hardcoded in events.py.
All menu accelerators are handled using GTK's built-in accelerator map. The map
doesn't seem to support multiple keybindings for one action, though, so this
module takes care of the problem.

At runtime, other modules can register a callback for a specific action name.
This action name has to be registered in BINDING_INFO, or an Exception will be
thrown. The module can pass a list of default keybindings. If the user hasn't
configured different bindings, the default ones will be used.

Afterwards, the action will be stored together with its keycode/modifier in a
dictionary:
(keycode: int, modifier: GdkModifierType) =>
    (action: string, callback: func, args: list, kwargs: dict)

Default keybindings will be stored here at initialization:
action-name: string => [keycodes: list]


Each action_name can have multiple keybindings.
"""

import os
import shutil
from gi.repository import Gtk
import json
from collections import defaultdict

from mcomix import constants
from mcomix import log
from mcomix.i18n import _

#: Bindings defined in this dictionary will appear in the configuration dialog.
#: If 'group' is None, the binding cannot be modified from the preferences dialog.
BINDING_INFO = {
    # Navigation between pages, archives, directories
    'previous_page' : { 'title' : _('Previous page'), 'group' : _('Navigation') },
    'next_page' : { 'title' : _('Next page'), 'group' : _('Navigation') },
    'previous_page_ff' : { 'title': _('Back ten pages'), 'group': _('Navigation') },
    'next_page_ff' : { 'title': _('Forward ten pages'), 'group': _('Navigation') },
    'previous_page_dynamic' : { 'title': _('Previous page (dynamic)'), 'group': _('Navigation') },
    'next_page_dynamic' : { 'title': _('Next page (dynamic)'), 'group': _('Navigation') },
    'previous_page_singlestep': { 'title': _('Previous page (always one page)'), 'group': _('Navigation') },
    'next_page_singlestep': { 'title': _('Next page (always one page)'), 'group': _('Navigation') },

    'first_page' : { 'title': _('First page'), 'group': _('Navigation') },
    'last_page' : { 'title': _('Last page'), 'group': _('Navigation') },
    'go_to' : { 'title': _('Go to page'), 'group': _('Navigation') },

    'next_archive' : { 'title': _('Next archive'), 'group': _('Navigation') },
    'previous_archive' : { 'title': _('Previous archive'), 'group': _('Navigation') },
    'next_directory' : { 'title': _('Next directory'), 'group': _('Navigation') },
    'previous_directory' : { 'title': _('Previous directory'), 'group': _('Navigation') },

    # Scrolling
    'scroll_left_bottom' : { 'title' : _('Scroll to bottom left'), 'group' : _('Scroll')},
    'scroll_middle_bottom' : { 'title' : _('Scroll to bottom center'), 'group' : _('Scroll')},
    'scroll_right_bottom' : { 'title' : _('Scroll to bottom right'), 'group' : _('Scroll')},

    'scroll_left_middle' : { 'title' : _('Scroll to middle left'), 'group' : _('Scroll')},
    'scroll_middle' : { 'title' : _('Scroll to center'), 'group' : _('Scroll')},
    'scroll_right_middle' : { 'title' : _('Scroll to middle right'), 'group' : _('Scroll')},

    'scroll_left_top' : { 'title' : _('Scroll to top left'), 'group' : _('Scroll')},
    'scroll_middle_top' : { 'title' : _('Scroll to top center'), 'group' : _('Scroll')},
    'scroll_right_top' : { 'title' : _('Scroll to top right'), 'group' : _('Scroll')},

    'scroll_down' : { 'title' : _('Scroll down'), 'group' : _('Scroll') },
    'scroll_up' : { 'title' : _('Scroll up'), 'group' : _('Scroll') },
    'scroll_right' : { 'title' : _('Scroll right'), 'group' : _('Scroll') },
    'scroll_left' : { 'title' : _('Scroll left'), 'group' : _('Scroll') },

    'smart_scroll_up' : { 'title' : _('Smart scroll up'), 'group' : _('Scroll') },
    'smart_scroll_down' : { 'title' : _('Smart scroll down'), 'group' : _('Scroll') },

    # View
    'zoom_in' : { 'title' : _('Zoom in'), 'group' : _('Zoom')},
    'zoom_out' : { 'title' : _('Zoom out'), 'group' : _('Zoom')},
    'zoom_original' : { 'title' : _('Normal size'), 'group' : _('Zoom')},

    'keep_transformation' : { 'title': _('Keep transformation'), 'group': _('Transformation') },
    'rotate_90' : { 'title': _('Rotate 90 degrees CW'), 'group': _('Transformation') },
    'rotate_180' : { 'title': _('Rotate 180 degrees'), 'group': _('Transformation') },
    'rotate_270' : { 'title': _('Rotate 90 degrees CCW'), 'group': _('Transformation') },
    'flip_horiz' : { 'title': _('Flip horizontally'), 'group': _('Transformation') },
    'flip_vert' : { 'title': _('Flip vertically'), 'group': _('Transformation') },
    'no_autorotation' : { 'title': _('Never autorotate'), 'group': _('Transformation') },

    'rotate_90_width' : { 'title': _('Rotate 90 degrees CW'), 'group': _('Autorotate by width') },
    'rotate_270_width' : { 'title': _('Rotate 90 degrees CCW'), 'group': _('Autorotate by width') },
    'rotate_90_height' : { 'title': _('Rotate 90 degrees CW'), 'group': _('Autorotate by height') },
    'rotate_270_height' : { 'title': _('Rotate 90 degrees CCW'), 'group': _('Autorotate by height') },

    'double_page' : { 'title': _('Double page mode'), 'group': _('View mode') },
    'manga_mode' : { 'title': _('Manga mode'), 'group': _('View mode') },
    'invert_scroll' : { 'title': _('Invert smart scroll'), 'group': _('View mode') },

    'lens' : { 'title': _('Magnifying lens'), 'group': _('View mode') },
    'stretch' : { 'title': _('Stretch small images'), 'group': _('View mode') },

    'best_fit_mode' : { 'title': _('Best fit mode'), 'group': _('View mode') },
    'fit_width_mode' : { 'title': _('Fit width mode'), 'group': _('View mode') },
    'fit_height_mode' : { 'title': _('Fit height mode'), 'group': _('View mode') },
    'fit_size_mode' : { 'title': _('Fit size mode'), 'group': _('View mode') },
    'fit_manual_mode' : { 'title': _('Manual zoom mode'), 'group': _('View mode') },

    # General UI
    'exit_fullscreen' : { 'title' : _('Exit from fullscreen'), 'group' : _('User interface')},

    'osd_panel' : { 'title' : _('Show OSD panel'), 'group' : _('User interface') },
    'minimize' : { 'title' : _('Minimize'), 'group' : _('User interface') },
    'fullscreen' : { 'title': _('Fullscreen'), 'group': _('User interface') },
    'toolbar' : { 'title': _('Show/hide toolbar'), 'group': _('User interface') },
    'menubar' : { 'title': _('Show/hide menubar'), 'group': _('User interface') },
    'statusbar' : { 'title': _('Show/hide statusbar'), 'group': _('User interface') },
    'scrollbar' : { 'title': _('Show/hide scrollbars'), 'group': _('User interface') },
    'thumbnails' : { 'title': _('Thumbnails'), 'group': _('User interface') },
    'hide_all' : { 'title': _('Show/hide all'), 'group': _('User interface') },
    'slideshow' : { 'title': _('Start slideshow'), 'group': _('User interface') },

    # File operations
    'delete' : { 'title' : _('Delete'), 'group' : _('File') },
    'refresh_archive' : { 'title': _('Refresh'), 'group': _('File') },
    'close' : { 'title': _('Close'), 'group': _('File') },
    'quit' : { 'title': _('Quit'), 'group': _('File') },
    'save_and_quit' : { 'title': _('Save and quit'), 'group': _('File') },
    'extract_page' : { 'title': _('Save As'), 'group': _('File') },

    'comments' : { 'title': _('Archive comments'), 'group': _('File') },
    'properties' : { 'title': _('Properties'), 'group': _('File') },
    'preferences' : { 'title': _('Preferences'), 'group': _('File') },

    'edit_archive' : { 'title': _('Edit archive'), 'group': _('File') },
    'open' : { 'title': _('Open'), 'group': _('File') },
    'enhance_image' : { 'title': _('Enhance image'), 'group': _('File') },
    'library' : { 'title': _('Library'), 'group': _('File') },
    'invert_color' : { 'title': _('Invert image colors'), 'group': _('File') },
}

# Generate 9 entries for executing command 1 to 9
for i in range(1, 10):
    BINDING_INFO['execute_command_%d' %i] = { 
            'title' : _('Execute external command') + ' (%d)' % i,
            'group' : _('External commands')
    }


class _KeybindingManager(object):
    def __init__(self, window):
        #: Main window instance
        self._window = window

        self._action_to_callback = {} # action name => (func, args, kwargs)
        self._action_to_bindings = defaultdict(list) # action name => [ (key code, key modifier), ]
        self._binding_to_action = {} # (key code, key modifier) => action name

        self._migrate_from_old_bindings()
        self._initialize()

    def register(self, name, bindings, callback, args=[], kwargs={}):
        """ Registers an action for a predefined keybinding name.
        @param name: Action name, defined in L{BINDING_INFO}.
        @param bindings: List of keybinding strings, as understood
                         by L{Gtk.accelerator_parse}. Only used if no
                         bindings were loaded for this action.
        @param callback: Function callback
        @param args: List of arguments to pass to the callback
        @param kwargs: List of keyword arguments to pass to the callback.
        """
        assert name in BINDING_INFO, "'%s' isn't a valid keyboard action." % name

        # Load stored keybindings, or fall back to passed arguments
        keycodes = self._action_to_bindings[name]
        if keycodes == []:
            keycodes = [Gtk.accelerator_parse(binding) for binding in bindings ]

        for keycode in keycodes:
            if keycode in list(self._binding_to_action.keys()):
                if self._binding_to_action[keycode] != name:
                    log.warning(_('Keybinding for "%(action)s" overrides hotkey for another action.'),
                            {"action": name})
                    log.warning('Binding %s overrides %r', keycode, self._binding_to_action[keycode])
            else:
                self._binding_to_action[keycode] = name
                self._action_to_bindings[name].append(keycode)

        # Add gtk accelerator for labels in menu
        if len(self._action_to_bindings[name]) > 0:
            key, mod = self._action_to_bindings[name][0]
            Gtk.AccelMap.change_entry('/mcomix-main/%s' % name, key, mod, True)

        self._action_to_callback[name] = (callback, args, kwargs)


    def edit_accel(self, name, new_binding, old_binding):
        """ Changes binding for an action
        @param name: Action name
        @param new_binding: Binding to be assigned to action
        @param old_binding: Binding to be removed from action [ can be empty: "" ]

        @return None: new_binding wasn't in any action
                action name: where new_binding was before
        """
        assert name in BINDING_INFO, "'%s' isn't a valid keyboard action." % name

        nb = Gtk.accelerator_parse(new_binding)
        old_action_with_nb = self._binding_to_action.get(nb)
        if old_action_with_nb is not None:
            # The new key is already bound to an action, erase the action
            self._binding_to_action.pop(nb)
            self._action_to_bindings[old_action_with_nb].remove(nb)

        if old_binding and name != old_action_with_nb:
            # The action already had a key that is now being replaced
            ob = Gtk.accelerator_parse(old_binding)
            self._binding_to_action[nb] = name

            # Remove action bound to the key.
            if ob in self._binding_to_action:
                self._binding_to_action.pop(ob)

            if ob in self._action_to_bindings[name]:
                idx = self._action_to_bindings[name].index(ob)
                self._action_to_bindings[name].pop(idx)
                self._action_to_bindings[name].insert(idx, nb)
        else:
            self._binding_to_action[nb] = name
            self._action_to_bindings[name].append(nb)

        self.save()
        return old_action_with_nb

    def clear_accel(self, name, binding):
        """ Remove binding for an action """
        assert name in BINDING_INFO, "'%s' isn't a valid keyboard action." % name

        ob = Gtk.accelerator_parse(binding)
        self._action_to_bindings[name].remove(ob)
        self._binding_to_action.pop(ob)

        self.save()

    def clear_all(self):
        """ Removes all keybindings. The changes are only persisted if
        save() is called afterwards. """
        self._action_to_callback = {}
        self._action_to_bindings = defaultdict(list)
        self._binding_to_action = {}

    def execute(self, keybinding):
        """ Executes an action that has been registered for the
        passed keyboard event. If no action is bound to the passed key, this
        method is a no-op. """
        if keybinding in self._binding_to_action:
            action = self._binding_to_action[keybinding]
            func, args, kwargs = self._action_to_callback[action]
            self._window.emit_stop_by_name('key_press_event')
            return func(*args, **kwargs)

        # Some keys enable additional modifiers (NumLock enables GDK_MOD2_MASK),
        # which prevent direct lookup simply by being pressed.
        # XXX: Looking up by key/modifier probably isn't the best implementation,
        # so limit possible states to begin with?
        for stored_binding, action in self._binding_to_action.items():
            stored_keycode, stored_flags = stored_binding
            if stored_keycode == keybinding[0] and stored_flags & keybinding[1]:
                func, args, kwargs = self._action_to_callback[action]
                self._window.emit_stop_by_name('key_press_event')
                return func(*args, **kwargs)

    def save(self):
        """ Stores the keybindings that have been set to disk. """
        # Collect keybindings for all registered actions
        action_to_keys = {}
        for action, bindings in self._action_to_bindings.items():
            if bindings is not None:
                action_to_keys[action] = [
                    Gtk.accelerator_name(keyval, modifiers) for
                    (keyval, modifiers) in bindings
                ]
        fp = open(constants.KEYBINDINGS_CONF_PATH, "w")
        json.dump(action_to_keys, fp, indent=2)
        fp.close()

    def _initialize(self):
        """ Restore keybindings from disk. """
        try:
            fp = open(constants.KEYBINDINGS_CONF_PATH, "r")
            stored_action_bindings = json.load(fp)
            fp.close()
        except Exception as e:
            log.error(_("Couldn't load keybindings: %s"), e)
            stored_action_bindings = {}

        for action in BINDING_INFO.keys():
            if action in stored_action_bindings:
                bindings = [
                    Gtk.accelerator_parse(keyname)
                    for keyname in stored_action_bindings[action] ]
                self._action_to_bindings[action] = bindings
                for binding in bindings:
                    self._binding_to_action[binding] = action
            else:
                self._action_to_bindings[action] = []

    def get_bindings_for_action(self, name):
        """ Returns a list of (keycode, modifier) for the action C{name}. """
        return self._action_to_bindings[name]

    def _migrate_from_old_bindings(self):
        """ This method deals with upgrading from MComix 1.0 and older to
        MComix 1.01, which integrated all UI hotkeys into this class. Simply
        remove old files and start from default values. """
        gtkrc = os.path.join(constants.CONFIG_DIR, 'keybindings-Gtk.rc')
        if os.path.isfile(gtkrc):
            # In case the user has made modifications to his files,
            # keep the old ones around for reference.
            if not os.path.isfile(gtkrc + '.delete-me'):
                shutil.move(gtkrc, gtkrc + '.delete-me')

            if os.path.isfile(constants.KEYBINDINGS_CONF_PATH) and \
                not os.path.isfile(constants.KEYBINDINGS_CONF_PATH + '.delete-me'):
                shutil.move(constants.KEYBINDINGS_CONF_PATH,
                        constants.KEYBINDINGS_CONF_PATH + '.delete-me')

_manager = None


def keybinding_manager(window):
    """ Returns a singleton instance of the keybinding manager. """
    global _manager
    if _manager:
        return _manager
    else:
        _manager = _KeybindingManager(window)
        return _manager

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/keybindings_editor.py0000644000175000017500000001347714476523373020255 0ustar00moritzmoritz# -*- coding: utf-8 -*-

""" Configuration tree view for the preferences dialog to edit keybindings. """

from gi.repository import Gtk

from mcomix import keybindings
from mcomix.i18n import _


class KeybindingEditorWindow(Gtk.ScrolledWindow):

    def __init__(self, keymanager):
        """ @param keymanager: KeybindingManager instance. """
        super(KeybindingEditorWindow, self).__init__()
        self.set_border_width(5)
        self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)

        self.keymanager = keymanager

        accel_column_num = max([
            len(self.keymanager.get_bindings_for_action(action))
            for action in list(keybindings.BINDING_INFO.keys())
        ])
        accel_column_num = self.accel_column_num = max([3, accel_column_num])

        # Human name, action name, true value, shortcut 1, shortcut 2, ...
        model = [str, str, 'gboolean']
        model.extend( [str, ] * accel_column_num)

        treestore = self.treestore = Gtk.TreeStore(*model)
        self.refresh_model()

        treeview = Gtk.TreeView(treestore)

        tvcol1 = Gtk.TreeViewColumn(_("Name"))
        treeview.append_column(tvcol1)
        cell1 = Gtk.CellRendererText()
        tvcol1.pack_start(cell1, True)
        tvcol1.set_attributes(cell1, text=0, editable=2)

        for idx in range(0, self.accel_column_num):
            tvc = Gtk.TreeViewColumn(_("Key %d") % (idx +1))
            treeview.append_column(tvc)
            accel_cell = Gtk.CellRendererAccel()
            accel_cell.connect("accel-edited", self.get_on_accel_edited(idx))
            accel_cell.connect("accel-cleared", self.get_on_accel_cleared(idx))
            tvc.pack_start(accel_cell, True)
            tvc.add_attribute(accel_cell, "text", 3 + idx)
            tvc.add_attribute(accel_cell, "editable", 2)

        # Allow sorting on the column
        tvcol1.set_sort_column_id(0)

        self.add_with_viewport(treeview)

    def refresh_model(self):
        """ Initializes the model from data provided by the keybinding
        manager. """
        self.treestore.clear()
        section_order = list(set(d['group']
             for d in list(keybindings.BINDING_INFO.values())))
        section_order.sort()
        section_parent_map = {}
        for section_name in section_order:
            row = [section_name, None, False]
            row.extend( [None,] * self.accel_column_num)
            section_parent_map[section_name] =  self.treestore.append(
                None, row
            )

        action_treeiter_map = self.action_treeiter_map = {}
        # Sort actions by action name
        actions = sorted(list(keybindings.BINDING_INFO.items()),
                key=lambda item: item[1]['title'])
        for action_name, action_data in actions:
            title = action_data['title']
            group_name = action_data['group']
            old_bindings = self.keymanager.get_bindings_for_action(action_name)
            acc_list =  ["", ] * self.accel_column_num
            for idx in range(0, self.accel_column_num):
                if len(old_bindings) > idx:
                    acc_list[idx] = Gtk.accelerator_name(*old_bindings[idx])

            row = [title, action_name, True]
            row.extend(acc_list)
            treeiter = self.treestore.append(
                section_parent_map[group_name],
                row
            )
            action_treeiter_map[action_name] = treeiter

    def get_on_accel_edited(self, column):
        def on_accel_edited(renderer, path, accel_key, accel_mods, hardware_keycode):
            iter = self.treestore.get_iter(path)
            col = column + 3  # accel cells start from 3 position
            old_accel = self.treestore.get(iter, col)[0]
            new_accel = Gtk.accelerator_name(accel_key, accel_mods)
            self.treestore.set_value(iter, col, new_accel)
            action_name = self.treestore.get_value(iter, 1)
            affected_action = self.keymanager.edit_accel(action_name, new_accel, old_accel)

            # Find affected row and cell
            if affected_action == action_name:
                for idx in range(0, self.accel_column_num):
                    if idx != column and self.treestore.get(iter, idx + 3)[0] == new_accel:
                        self.treestore.set_value(iter, idx + 3, "")
            elif affected_action is not None:
                titer = self.action_treeiter_map[affected_action]
                for idx in range(0, self.accel_column_num):
                    if self.treestore.get(titer, idx + 3)[0] == new_accel:
                        self.treestore.set_value(titer, idx + 3, "")

            # updating gtk accelerator for label in menu
            if self.keymanager.get_bindings_for_action(action_name)[0] == (accel_key, accel_mods):
                Gtk.AccelMap.change_entry('/mcomix-main/%s' % action_name,
                        accel_key, accel_mods, True)

        return on_accel_edited

    def get_on_accel_cleared(self, column):
        def on_accel_cleared(renderer, path, *args):
            iter = self.treestore.get_iter(path)
            col = column + 3
            accel = self.treestore.get(iter, col)[0]
            action_name = self.treestore.get_value(iter, 1)
            if accel != "":
                self.keymanager.clear_accel(action_name, accel)

                # updating gtk accelerator for label in menu
                if len(self.keymanager.get_bindings_for_action(action_name)) == 0:
                    Gtk.AccelMap.change_entry('/mcomix-main/%s' % action_name, 0, 0, True)
                else:
                    key, mods  = self.keymanager.get_bindings_for_action(action_name)[0]
                    Gtk.AccelMap.change_entry('/mcomix-main/%s' % action_name, key, mods, True)

            self.treestore.set_value(iter, col, "")
        return on_accel_cleared



# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1698566559.0
mcomix-3.1.0/mcomix/labels.py0000644000175000017500000000311614517410637015622 0ustar00moritzmoritz"""labels.py - Gtk.Label convenience classes."""

from mcomix import i18n
from gi.repository import GLib, Gtk, Pango


class FormattedLabel(Gtk.Label):
    """FormattedLabel keeps a label always formatted with some pango weight,
    style and scale, even when new text is set using set_text().
    """

    _STYLES = {
        Pango.Style.NORMAL: 'normal',
        Pango.Style.OBLIQUE: 'oblique',
        Pango.Style.ITALIC: 'italic',
    }

    def __init__(self, text: str = '', weight: Pango.Weight = Pango.Weight.NORMAL,
                 style: Pango.Style = Pango.Style.NORMAL, scale: float = 1.0) -> None:
        super(FormattedLabel, self).__init__()
        self._weight = weight
        self._style = style
        self._scale = scale
        self.set_text(text)

    def set_text(self, text: str) -> None:
        markup = '%s' % (
            int(self._scale * 10 * 1024),
            self._weight,
            self._STYLES[self._style],
            GLib.markup_escape_text(i18n.to_display_string(text))
        )
        self.set_markup(markup)


class BoldLabel(FormattedLabel):
    """A FormattedLabel that is always bold and otherwise normal."""

    def __init__(self, text: str = '') -> None:
        super(BoldLabel, self).__init__(text=text, weight=Pango.Weight.BOLD)


class ItalicLabel(FormattedLabel):
    """A FormattedLabel that is always italic and otherwise normal."""

    def __init__(self, text: str = '') -> None:
        super(ItalicLabel, self).__init__(text=text, style=Pango.Style.ITALIC)


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/last_read_page.py0000644000175000017500000001676714476523373017340 0ustar00moritzmoritz# -*- coding: utf-8 -*-

import os

from mcomix import log
from mcomix import constants
from mcomix.i18n import _

# This import is only used for legacy data that is imported
# into the library at upgrade.
from sqlite3 import dbapi2


class LastReadPage(object):
    """ Automatically stores the last page the user read for all book files,
    and restores the page the next time the archive is opened. When the book
    is finished, the page will be cleared.

    If L{enabled} is set to C{false}, all methods will do nothing. This
    simplifies code in other places, as it does not have to check each time
    if the preference option to store pages automatically is enabled.
    """

    def __init__(self, backend):
        """ Constructor.
        @param backend: Library backend instance.
        """
        #: If disabled, all methods will be no-ops.
        self.enabled = False
        #: Library backend.
        self.backend = backend

    def set_enabled(self, enabled):
        """ Enables (or disables) all functionality of this module.
        @type enabled: bool
        """
        if self.backend.enabled:
            self.enabled = enabled
        else:
            self.enabled = False

    def count(self):
        """ Number of stored book/page combinations. This method is
        not affected by setting L{enabled} to false.
        @return: The number of entries stored by this module. """

        if not self.backend.enabled:
            return 0

        cursor = self.backend.execute("""SELECT COUNT(*) FROM recent""")
        count = cursor.fetchone()
        cursor.close()

        return count

    def set_page(self, path, page):
        """ Sets C{page} as last read page for the book at C{path}.
        @param path: Path to book. Raises ValueError if file doesn't exist.
        @param page: Page number.
        """
        if not self.enabled:
            return

        full_path = os.path.abspath(path)
        book = self.backend.get_book_by_path(full_path)

        if not book:
            self.backend.add_book(
                full_path, self.backend.get_recent_collection().id)
            book = self.backend.get_book_by_path(full_path)

            if not book:
                raise ValueError("Book doesn't exist")
        else:
            self.backend.add_book_to_collection(
                book.id, self.backend.get_recent_collection().id)

        book.set_last_read_page(page)

    def clear_page(self, path):
        """ Removes stored page for book at C{path}.
        @param path: Path to book.
        """
        if not self.enabled:
            return

        full_path = os.path.abspath(path)
        book = self.backend.get_book_by_path(full_path)

        if book:
            book.set_last_read_page(None)

    def clear_all(self):
        """ Removes all stored books from the library's 'Recent' collection,
        and removes all information from the recent table. This method is
        not affected by setting L{enabled} to false. """

        if not self.backend.enabled:
            return

        # Collect books that are only present in "Recent" collection
        # and have an entry in table "recent". Those must be removed.
        sql = """SELECT c.book FROM contain c
                 JOIN (SELECT book FROM contain
                       GROUP BY book HAVING COUNT(*) = 1
                      ) t ON t.book = c.book
                 JOIN recent r ON r.book = c.book
                 WHERE c.collection = ?"""
        cursor = self.backend.execute(sql,
            (self.backend.get_recent_collection().id,))
        for book in cursor.fetchall():
            self.backend.remove_book(book)
        cursor.execute("""DELETE FROM recent""")
        cursor.execute("""DELETE FROM contain WHERE collection = ?""",
                       (self.backend.get_recent_collection().id,))
        cursor.close()

    def get_page(self, path):
        """ Gets the last read page for book at C{path}.

        @param path: Path to book.
        @return: Page that was last read, or C{None} if the book
                 wasn't opened before.
        """
        if not self.enabled:
            return None

        full_path = os.path.abspath(path)
        book = self.backend.get_book_by_path(full_path)
        if book:
            page = book.get_last_read_page()
            if page is not None and page < book.pages:
                return page
            else:
                # If the last read page was the last in the book,
                # start from scratch.
                return None
        else:
            return None

    def get_date(self, path):
        """ Gets the date at which the page for path was set.

        @param path: Path to book.
        @return: C{datetime} object, or C{None} if no page was set.
        """
        if not self.enabled:
            return None

        full_path = os.path.abspath(path)
        book = self.backend.get_book_by_path(full_path)
        if book:
            return book.get_last_read_date()
        else:
            return None

    def migrate_database_to_library(self, recent_collection):
        """ Moves all information saved in the legacy database
        constants.LASTPAGE_DATABASE_PATH into the library,
        and deleting the old database. """

        if not self.backend.enabled:
            return

        database = self._init_database(constants.LASTPAGE_DATABASE_PATH)

        if database:
            cursor = database.execute('''SELECT path, page, time_set
                                         FROM lastread''')
            rows = cursor.fetchall()
            cursor.close()
            database.close()

            for path, page, time_set in rows:
                book = self.backend.get_book_by_path(path)

                if not book:
                    # The path doesn't exist in the library yet
                    if not os.path.exists(path):
                        # File might no longer be available
                        continue

                    self.backend.add_book(path, recent_collection)
                    book = self.backend.get_book_by_path(path)

                    if not book:
                        # The book could not be added
                        continue
                else:
                    # The book exists, move into recent collection
                    self.backend.add_book_to_collection(book.id, recent_collection)

                # Set recent info on retrieved book
                # XXX: If the book calls get_backend during migrate_database,
                # the library isn't constructed yet and breaks in an
                # endless recursion.
                book.get_backend = lambda: self.backend
                book.set_last_read_page(page, time_set)

            try:
                os.unlink(constants.LASTPAGE_DATABASE_PATH)
            except IOError as e:
                log.error(_('! Could not remove file "%s"'),
                          constants.LASTPAGE_DATABASE_PATH)

    def _init_database(self, dbfile):
        """ Creates or opens new SQLite database at C{dbfile}, and initalizes
        the required table(s).

        @param dbfile: Database file name. This file needn't exist.
        @return: Open SQLite database connection.
        """
        if not dbapi2:
            return None

        db = dbapi2.connect(dbfile, isolation_level=None)
        sql = """CREATE TABLE IF NOT EXISTS lastread (
            path TEXT PRIMARY KEY,
            page INTEGER,
            time_set DATETIME
        )"""
        cursor = db.execute(sql)
        cursor.close()

        return db

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/layout.py0000644000175000017500000002531214476523373015705 0ustar00moritzmoritz""" Layout. """

import operator

from mcomix import constants
from mcomix import scrolling
from mcomix import tools
from mcomix import box


class FiniteLayout(object): # 2D only

    @staticmethod
    def create_finite_layout(box_count, orientation, spacing,
        distribution_axis, alignment_axis,
        scrollbar_update, visible_area_update, sizes_update):
        viewport_size = () # dummy
        expand_area = False
        scrollbar_requests = [False] * 2 # 2D only
        # Visible area size is recomputed depending on scrollbar visibility
        while True:
            scrollbar_update(scrollbar_requests)
            new_viewport_size = visible_area_update()
            if new_viewport_size == viewport_size:
                break
            viewport_size = new_viewport_size
            zoom_dummy_size = list(viewport_size)
            dasize = zoom_dummy_size[distribution_axis] - \
                spacing * (box_count - 1)
            if dasize <= 0:
                dasize = 1
            zoom_dummy_size[distribution_axis] = dasize
            scaled_sizes_distorted = sizes_update(zoom_dummy_size)
            result = FiniteLayout(scaled_sizes_distorted[0], scaled_sizes_distorted[1],
                viewport_size, orientation, spacing, expand_area, distribution_axis,
                alignment_axis)
            union_scaled_size = result.get_union_box().get_size()
            scrollbar_requests = list(map(operator.or_, scrollbar_requests,
                tools.smaller(viewport_size, union_scaled_size)))
            if len([_f for _f in scrollbar_requests if _f]) > 1 and not expand_area:
                expand_area = True
                viewport_size = () # start anew
        return result


    def __init__(self, content_sizes, content_distorted, viewport_size, orientation,
        spacing, wrap_individually, distribution_axis, alignment_axis):
        """ Lays out a finite number of Boxes along the first axis.
        @param content_sizes: The sizes of the Boxes to lay out.
        @param content_distorted: Booleans indicating whether the corresponding
        Box size is stretched irrespective of aspect ratio.
        @param viewport_size: The size of the viewport.
        @param orientation: The orientation to use.
        @param spacing: Number of additional pixels between Boxes.
        @param wrap_individually: True if each content box should get its own
        wrapper box, False if the only wrapper box should be the union of all
        content boxes.
        @param distribution_axis: the axis along which the Boxes are distributed.
        @param alignment_axis: the axis to center. """
        self.scroller = scrolling.Scrolling()
        self.current_index = -1
        self.wrap_individually = wrap_individually
        self._reset(content_sizes, content_distorted, viewport_size, orientation,
            spacing, wrap_individually, distribution_axis, alignment_axis)


    def set_viewport_position(self, viewport_position):
        """ Moves the viewport to the specified position.
        @param viewport_position: The new viewport position. """
        self.viewport_box = self.viewport_box.set_position(viewport_position)
        self.dirty_current_index = True


    def scroll_smartly(self, max_scroll, backwards, axis_map, index=None):
        """ Applies a "smart scrolling" step to the current viewport position.
        If there are not enough Boxes to scroll to, the viewport is not moved
        and an appropriate value is returned.
        @param max_scroll: The maximum numbers of pixels to scroll in one step.
        @param backwards: True for backwards scrolling, False otherwise.
        @param axis_map: The index of the dimension to modify.
        @param index: The index of the Box the scrolling step is related to,
        or None to use the index of the current Box.
        @return: The index of the current Box after scrolling, or -1 if there
        were not enough Boxes to scroll backwards, or the number of Boxes if
        there were not enough Boxes to scroll forwards. """
        # TODO reconsider interface
        if (index == None) or (not self.wrap_individually):
            index = self.get_current_index()
        if not self.wrap_individually:
            wrapper_index = 0
        else:
            wrapper_index = index
        o = tools.vector_opposite(self.orientation) if backwards \
            else self.orientation
        new_pos = self.scroller.scroll_smartly(self.wrapper_boxes[wrapper_index],
            self.viewport_box, o, max_scroll, axis_map)
        if new_pos == []:
            if self.wrap_individually:
                index += -1 if backwards else 1
                n = len(self.get_content_boxes())
                if (index < n) and (index >= 0):
                    self.scroll_to_predefined(tools.vector_opposite(o), index)
                return index
            else:
                index = -1 if backwards else len(self.get_content_boxes())
                return index
        self.set_viewport_position(new_pos)
        return index


    def scroll_to_predefined(self, destination, index=None):
        """ Scrolls the viewport to a predefined destination.
        @param destination: An integer representing a predefined destination.
        Either 1 (towards the greatest possible values in this dimension),
        -1 (towards the smallest value in this dimension), 0 (keep position),
        SCROLL_TO_CENTER (scroll to the center of the content in this
        dimension), SCROLL_TO_START (scroll to where the content starts in this
        dimension) or SCROLL_TO_END (scroll to where the content ends in this
        dimension).
        @param index: The index of the Box the scrolling is related to, None to
        use the index of the current Box, or UNION_INDEX to use the union box
        instead. Note that the current implementation always uses the union box
        if self.wrap_individually is False. """
        if index == None:
            index = self.get_current_index()
        if not self.wrap_individually:
            index = constants.UNION_INDEX
        if index == constants.UNION_INDEX:
            current_box = self.union_box
        else:
            if index == constants.LAST_INDEX:
                index = len(self.content_boxes) - 1
            current_box = self.wrapper_boxes[index]
        self.set_viewport_position(self.scroller.scroll_to_predefined(
            current_box, self.viewport_box, self.orientation, destination))


    def get_content_boxes(self):
        """ Returns the Boxes as they are arranged in this layout.
        @return: The Boxes as they are arranged in this layout. """
        return self.content_boxes


    def get_content_distorted(self):
        """ Returns Booleans indicating whether the corresponding content
        Box is stretched irrespective of aspect ratio.
        @return: Booleans indicating whether the corresponding content
        Box is stretched irrespective of aspect ratio. """
        return self.content_distorted


    def get_wrapper_boxes(self):
        """ Returns the wrapper Boxes as they are arranged in this layout.
        @return: The wrapper Boxes as they are arranged in this layout. """
        return self.wrapper_boxes


    def get_union_box(self):
        """ Returns the union Box for this layout.
        @return: The union Box for this layout. """
        return self.union_box


    def get_current_index(self):
        """ Returns the index of the Box that is said to be the current Box.
        @return: The index of the Box that is said to be the current Box. """
        if self.dirty_current_index:
            self.current_index = self.viewport_box.current_box_index(
                self.orientation, self.content_boxes)
            self.dirty_current_index = False
        return self.current_index


    def get_viewport_box(self):
        """ Returns the current viewport Box.
        @return: The current viewport Box. """
        return self.viewport_box


    def get_orientation(self):
        """ Returns the orientation for this layout.
        @return: The orientation for this layout. """
        return self.orientation


    def set_orientation(self, orientation):
        self.orientation = orientation


    def _reset(self, content_sizes, content_distorted, viewport_size, orientation,
        spacing, wrap_individually, distribution_axis, alignment_axis):
        # reverse order if necessary
        if orientation[distribution_axis] == -1:
            content_sizes = tuple(reversed(content_sizes))
        temp_cb_list = list(map(box.Box, content_sizes))
        # align to center
        temp_cb_list = box.Box.align_center(temp_cb_list, alignment_axis, 0,
            orientation[alignment_axis])
        # distribute
        temp_cb_list = box.Box.distribute(temp_cb_list, distribution_axis, 0,
            spacing)
        if wrap_individually:
            temp_wb_list, temp_bb = FiniteLayout._wrap_individually(temp_cb_list,
                viewport_size, orientation)
        else:
            temp_wb_list, temp_bb = FiniteLayout._wrap_union(temp_cb_list,
                viewport_size, orientation)
        # move to global origin
        bbp = temp_bb.get_position()
        for i in range(len(temp_cb_list)):
            temp_cb_list[i] = temp_cb_list[i].translate_opposite(bbp)
        for i in range(len(temp_wb_list)):
            temp_wb_list[i] = temp_wb_list[i].translate_opposite(bbp)
        temp_bb = temp_bb.translate_opposite(bbp)
        # reverse order again, if necessary
        if orientation[distribution_axis] == -1:
            temp_cb_list = tuple(reversed(temp_cb_list))
            temp_wb_list = tuple(reversed(temp_wb_list))
        # done
        self.content_boxes = temp_cb_list
        self.content_distorted = content_distorted
        self.wrapper_boxes = temp_wb_list
        self.union_box = temp_bb
        self.viewport_box = box.Box(viewport_size)
        self.orientation = orientation
        self.dirty_current_index = True


    @staticmethod
    def _wrap_individually(temp_cb_list, viewport_size, orientation):
        # calculate (potentially oversized) wrapper Boxes
        temp_wb_list = [None] * len(temp_cb_list)
        for i in range(len(temp_cb_list)):
            temp_wb_list[i] = temp_cb_list[i].wrapper_box(viewport_size,
                orientation)
        # calculate bounding Box
        temp_bb = box.Box.bounding_box(temp_wb_list)
        return (temp_wb_list, temp_bb)


    @staticmethod
    def _wrap_union(temp_cb_list, viewport_size, orientation):
        # calculate bounding Box
        temp_wb_list = [box.Box.bounding_box(temp_cb_list).wrapper_box(
            viewport_size, orientation)]
        return (temp_wb_list, temp_wb_list[0])


def create_dummy_layout():
    return FiniteLayout(((1,1),), ((False, False),), (1,1), (1,1), 0, False, 0, 0)


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704114443.0
mcomix-3.1.0/mcomix/lens.py0000644000175000017500000003061014544534413015317 0ustar00moritzmoritz"""lens.py - Magnifying lens."""

import math

from gi.repository import Gdk, GdkPixbuf, Gtk

from mcomix.preferences import prefs
from mcomix import image_tools
from mcomix import constants
from mcomix import box
from mcomix import tools


class MagnifyingLens(object):

    """The MagnifyingLens creates cursors from the raw pixbufs containing
    the unscaled data for the currently displayed images. It does this by
    looking at the cursor position and calculating what image data to put
    in the "lens" cursor.

    Note: The mapping is highly dependent on the exact layout of the main
    window images, thus this module isn't really independent from the main
    module as it uses implementation details not in the interface.
    """

    def __init__(self, window):
        self._window = window
        self._area = self._window._main_layout
        self._area.connect('motion-notify-event', self._motion_event)

        #: Stores lens state
        self._enabled = False
        #: Stores a tuple of the last mouse coordinates
        self._point = None
        #: Stores the last rectangle that was used to render the lens
        self._last_lens_rect = None

    def get_enabled(self):
        return self._enabled

    def set_enabled(self, enabled):
        self._enabled = enabled

        if enabled:
            # FIXME: If no file is currently loaded, the cursor will still be hidden.
            self._window.cursor_handler.set_cursor_type(constants.NO_CURSOR)
            self._window.osd.clear()

            if self._point:
                self._draw_lens(*self._point)
        else:
            self._window.cursor_handler.set_cursor_type(constants.NORMAL_CURSOR)
            self._clear_lens()
            self._last_lens_rect = None

    enabled = property(get_enabled, set_enabled)

    def _draw_lens(self, x, y):
        """Calculate what image data to put in the lens and update the cursor
        with it;  and  are the positions of the cursor within the
        main window layout area.
        """
        if self._window.images[0].get_storage_type() not in (Gtk.ImageType.PIXBUF,
            Gtk.ImageType.ANIMATION):
            return

        lens_size = (prefs['lens size'],) * 2 # 2D only
        border_size = 1
        rectangle = self._calculate_lens_rect(x, y, *lens_size, border_size)

        draw_region = Gdk.Rectangle()
        draw_region.x, draw_region.y, draw_region.width, draw_region.height = rectangle
        if self._last_lens_rect:
            last_region = Gdk.Rectangle()
            last_region.x, last_region.y, last_region.width, last_region.height = self._last_lens_rect
            draw_region = Gdk.rectangle_union(draw_region, last_region)

        pixbuf = self._get_lens_pixbuf(x, y, lens_size, border_size,
            (x - rectangle[0], y - rectangle[1]))
        window = self._window._main_layout.get_bin_window()
        window.begin_paint_rect(draw_region)

        self._clear_lens()

        cr = window.cairo_create()
        surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, 0, window)
        cr.set_source_surface(surface, rectangle[0], rectangle[1])
        cr.paint()

        window.end_paint()

        self._last_lens_rect = rectangle

    def _calculate_lens_rect(self, x, y, width, height, border_size):
        """ Calculates the area where the lens will be drawn on screen. This method takes
        screen space into calculation and moves the rectangle accordingly when the the rectangle
        would otherwise flow over the allocated area. """

        lens_x = max(x - width // 2, 0)
        lens_y = max(y - height // 2, 0)

        max_width, max_height = self._window.get_visible_area_size()
        max_width += int(self._window._hadjust.get_value())
        max_height += int(self._window._vadjust.get_value())
        lens_x = min(lens_x, max_width - width)
        lens_y = min(lens_y, max_height - height)

        return lens_x, lens_y, width + 2 * border_size, height + 2 * border_size

    def _clear_lens(self, current_lens_region=None):
        """ Invalidates the area that was damaged by the last call to draw_lens. """

        if not self._last_lens_rect:
            return

        window = self._window._main_layout.get_bin_window()
        crect = Gdk.Rectangle()
        crect.x, crect.y, crect.width, crect.height = self._last_lens_rect
        window.invalidate_rect(crect, True)
        window.process_updates(True)
        self._last_lens_rect = None

    def toggle(self, action):
        """Toggle on or off the lens depending on the state of ."""
        self.enabled = action.get_active()

    def _motion_event(self, widget, event):
        """ Called whenever the mouse moves over the image area. """
        self._point = (int(event.x), int(event.y))
        if self.enabled:
            self._draw_lens(*self._point)

    def _get_lens_pixbuf(self, x, y, lens_size, border_size, check_offset):
        """Get a pixbuf containing the appropiate image data for the lens
        where  and  are the positions of the cursor.
        """
        cb = self._window.layout.get_content_boxes()
        source_pixbufs = self._window.imagehandler.get_pixbufs(len(cb))
        transforms = self._window.transforms
        lens_scale = (prefs['lens magnification'],) * 2 # 2D only
        opaque = prefs['checkered bg for transparent images'] or not any(
            map(GdkPixbuf.Pixbuf.get_has_alpha, source_pixbufs))
        canvas = GdkPixbuf.Pixbuf.new(colorspace=GdkPixbuf.Colorspace.RGB,
            has_alpha=not opaque, bits_per_sample=8, width=lens_size[0],
            height=lens_size[1]) # 2D only
        canvas.fill(image_tools.convert_rgb16list_to_rgba8int(self._window.get_bg_colour()))
        for b, source_pixbuf, tf in zip(cb, source_pixbufs, transforms):
            if image_tools.is_animation(source_pixbuf):
                continue
            cpos = b.get_position()
            _scale, rotation, flips = tf.to_image_transforms() # FIXME use scale as soon as it is correctly included
            composite_color_args = image_tools.get_composite_color_args(0) if \
                source_pixbuf.get_has_alpha() and opaque else None
            self._draw_lens_pixbuf((x - cpos[0], y - cpos[1]), b.get_size(),
                source_pixbuf, rotation, flips,
                lens_size, lens_scale, canvas, prefs['scaling quality'],
                composite_color_args, (x - border_size - check_offset[0],
                y - border_size - check_offset[1])) # 2D only

        canvas = self._window.enhancer.enhance(canvas)

        return image_tools.add_border(canvas, border_size)

    def _draw_lens_pixbuf(self, ref_pos, csize, srcbuf, rotation, flips,
        lens_size, lens_scale, dstbuf, interpolation, composite_color_args,
        check_offset):
        if tools.volume(csize) == 0:
            return

        # Some computations are the same for each axis.
        def calc_1d(ref_pos, csize, src_pixbuf_size, lens_size, lens_scale):
            # compute initial scales, sizes and positions
            page_scale = csize / src_pixbuf_size
            source_ref_pos = ref_pos / page_scale
            combined_source_scale = page_scale * lens_scale
            mapped_ref_pos = source_ref_pos * combined_source_scale
            mapped_ref_pos_int = int(round(mapped_ref_pos * 2)) // 2
            mapped_size = int(round(src_pixbuf_size * combined_source_scale))
            # take rounding errors into account
            applied_source_scale = mapped_size / src_pixbuf_size
            # calculate data for clamping
            lens_size_2q, lens_size_2r = divmod(lens_size, 2)
            neg_mapped_lens_pos = lens_size_2q - mapped_ref_pos_int
            dest_lens_offset = neg_mapped_lens_pos
            dest_lens_end = dest_lens_offset + mapped_size
            # clamp to lens
            dest_lens_end = min(dest_lens_end, lens_size)
            dest_lens_offset = max(0, dest_lens_offset)
            dest_lens_size = dest_lens_end - dest_lens_offset
            return applied_source_scale, neg_mapped_lens_pos, dest_lens_offset, \
                dest_lens_size, mapped_size, mapped_ref_pos_int, lens_size_2q, lens_size_2r

        # prepare actual computation
        src_pixbuf_size = [srcbuf.get_width(), srcbuf.get_height()] # 2D only
        transpose = (1, 0) if tools.rotation_swaps_axes(rotation) else (0, 1) # 2D only
        tp = lambda x: tools.remap_axes(x, transpose)
        axis_flip = tuple(map(lambda r, f: (rotation in r) ^ f, ((270, 180), (90, 180)), tp(flips))) # 2D only

        # calculate size and position data
        applied_source_scale, neg_mapped_lens_pos, dest_lens_offset, dest_lens_size, \
            mapped_size, mapped_ref_pos_int, lens_size_2q, lens_size_2r = \
            [list(x) for x in zip(*(map(calc_1d, tp(ref_pos), tp(csize),
            src_pixbuf_size, tp(lens_size), tp(lens_scale))))]

        if min(dest_lens_size) > 0:
            # Using GdkPixbuf.Pixbuf.scale here so we do not need to worry about
            # interpolation issues when close to the edges. Also, one can exploit it
            # later to only recompute the parts of the lens where the content might
            # have changed.
            if any(flips) or any(axis_flip):
                # Unfortuantely, GdkPixbuf does not seem to provide an API for applying
                # arbitrary matrix transforms the same way, which is why we need to
                # apply inefficient workarounds.

                # keep track of (mirrored) reference point
                refpos_tracking = list(mapped_ref_pos_int)
                for i, s in enumerate(axis_flip):
                    if s:
                        # Subtracting the remainder keeps a lens with an odd number
                        # of pixels centered at the (mirrored) reference point.
                        refpos_tracking[i] = mapped_size[i] - refpos_tracking[i] - lens_size_2r[i]
                refpos_tracking = tools.vector_sub(refpos_tracking, lens_size_2q)

                # write to temporary buffer
                tempbuf = GdkPixbuf.Pixbuf.new(srcbuf.get_colorspace(),
                    srcbuf.get_has_alpha(), srcbuf.get_bits_per_sample(), *dest_lens_size)
                temp_lens_box = box.Box.intersect(box.Box(lens_size, position=refpos_tracking),
                    box.Box(mapped_size))
                srcbuf.scale(tempbuf, 0, 0, *dest_lens_size,
                    *tools.vector_opposite(temp_lens_box.get_position()),
                    *applied_source_scale, interpolation) # 2D only

                # apply all necessary transforms to temporary buffer
                tempbuf = image_tools.rotate_pixbuf(tempbuf, rotation)
                for i, f in enumerate(flips):
                    if f:
                        tempbuf = image_tools.flip_pixbuf(tempbuf, i)

                # Not sure whether it should be inverse axis remapping instead of
                # forward, but in 2D, there is no difference anyway.
                remapped_dest_lens_offset = tp(dest_lens_offset)
                remapped_dest_lens_size = tp(dest_lens_size)

                # copy result from temporary buffer to actual lens buffer
                if composite_color_args is None:
                    tempbuf.copy_area(0, 0, *remapped_dest_lens_size, dstbuf,
                        *remapped_dest_lens_offset) # 2D only
                else:
                    tempbuf.composite_color(dstbuf, *remapped_dest_lens_offset,
                        *remapped_dest_lens_size, *remapped_dest_lens_offset, 1, 1,
                        GdkPixbuf.InterpType.NEAREST, 255,
                        *tools.vector_add(tp(dest_lens_offset), check_offset),
                        *composite_color_args) # 2D only
                # unref temporary buffer
                tempbuf = None
            else:
                # no workaround needed
                if composite_color_args is None:
                    srcbuf.scale(dstbuf, *dest_lens_offset, *dest_lens_size,
                        *neg_mapped_lens_pos, *applied_source_scale, interpolation) # 2D only
                else:
                    srcbuf.composite_color(dstbuf, *dest_lens_offset, *dest_lens_size,
                        *neg_mapped_lens_pos, *applied_source_scale, interpolation,
                        255, *tools.vector_add(dest_lens_offset, check_offset),
                        *composite_color_args) # 2D only
        else:
            # If we are here, there is either no image to be drawn at all, or it is
            # out of range.
            pass
        return dstbuf


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/library/0000755000175000017500000000000014553265237015455 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/library/__init__.py0000644000175000017500000000000014476523373017556 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/library/add_progress_dialog.py0000644000175000017500000000550714476523373022033 0ustar00moritzmoritz"""library_add_progress_dialog.py - Progress bar for the library."""

from gi.repository import Gtk
from gi.repository import Pango

from mcomix import labels
from mcomix.i18n import _

_dialog = None
# The "All books" collection is not a real collection stored in the library,
# but is represented by this ID in the library's TreeModels.
_COLLECTION_ALL = -1

class _AddLibraryProgressDialog(Gtk.Dialog):

    """Dialog with a ProgressBar that adds books to the library."""

    def __init__(self, library, window, paths, collection):
        """Adds the books at  to the library, and also to the
        , unless it is None.
        """
        super(_AddLibraryProgressDialog, self).__init__(_('Adding books'), library,
            Gtk.DialogFlags.MODAL, (Gtk.STOCK_STOP, Gtk.ResponseType.CLOSE))

        self._window = window
        self._destroy = False
        self.set_size_request(400, -1)
        self.set_resizable(False)
        self.set_border_width(4)
        self.connect('response', self._response)
        self.set_default_response(Gtk.ResponseType.CLOSE)

        main_box = Gtk.VBox(False, 5)
        main_box.set_border_width(6)
        self.vbox.pack_start(main_box, False, False, 0)
        hbox = Gtk.HBox(False, 10)
        main_box.pack_start(hbox, False, False, 5)
        left_box = Gtk.VBox(True, 5)
        right_box = Gtk.VBox(True, 5)
        hbox.pack_start(left_box, False, False, 0)
        hbox.pack_start(right_box, False, False, 0)

        label = labels.BoldLabel(_('Added books:'))
        label.set_alignment(1.0, 1.0)
        left_box.pack_start(label, True, True, 0)
        number_label = Gtk.Label(label='0')
        number_label.set_alignment(0, 1.0)
        right_box.pack_start(number_label, True, True, 0)

        bar = Gtk.ProgressBar()
        main_box.pack_start(bar, False, False, 0)

        added_label = labels.ItalicLabel()
        added_label.set_alignment(0, 0.5)
        added_label.set_width_chars(64)
        added_label.set_max_width_chars(64)
        added_label.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
        main_box.pack_start(added_label, False, False, 0)
        self.show_all()

        total_paths_int = len(paths)
        total_paths_float = float(len(paths))
        total_added = 0

        for path in paths:

            if library.backend.add_book(path, collection):
                total_added += 1

                number_label.set_text('%d / %d' % (total_added, total_paths_int))

            added_label.set_text(_("Adding '%s'...") % path)
            bar.set_fraction(total_added / total_paths_float)

            while Gtk.events_pending():
                Gtk.main_iteration_do(False)

            if self._destroy:
                return

        self._response()

    def _response(self, *args):
        self._destroy = True
        self.destroy()

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1695149052.0
mcomix-3.1.0/mcomix/library/backend.py0000644000175000017500000006652214502365774017432 0ustar00moritzmoritz"""library_backend.py - Comic book library backend using sqlite."""

import os
import datetime

from typing import Any, List, Optional, TYPE_CHECKING

from mcomix import archive_tools
from mcomix import constants
from mcomix import thumbnail_tools
from mcomix import log
from mcomix import callback
from mcomix.library import backend_types
from mcomix.i18n import _
# Only for importing legacy data from last-read module
from mcomix import last_read_page

from sqlite3 import dbapi2

if TYPE_CHECKING:
    from gi.repository import GdkPixbuf


#: Identifies the 'Recent' collection that stores recently read books.
COLLECTION_RECENT = -2


class _LibraryBackend(object):

    """The LibraryBackend handles the storing and retrieval of library
    data to and from disk.
    """

    #: Current version of the library database structure.
    # See method _upgrade_database() for changes between versions.
    DB_VERSION = 7

    def __init__(self) -> None:

        def row_factory(cursor, row):
            """Return rows as sequences only when they have more than
            one element.
            """
            if len(row) == 1:
                return row[0]
            return row

        if dbapi2 is not None:
            self._con = dbapi2.connect(constants.LIBRARY_DATABASE_PATH,
                check_same_thread=False, isolation_level=None)
            self._con.row_factory = row_factory
            self.enabled = True

            self.watchlist = backend_types._WatchList(self)

            version = self._library_version()
            self._upgrade_database(version, _LibraryBackend.DB_VERSION)
        else:
            self._con = None
            self.watchlist = None
            self.enabled = False

    def get_books_in_collection(self, collection: Optional[int] = None, filter_string: Optional[str] = None) -> List[int]:
        """Return a sequence with all the books in , or *ALL*
        books if  is None. If  is not None, we
        only return books where the  occurs in the path.
        """
        if collection is None:
            if filter_string is None:
                cur = self._con.execute('''select id from Book''')
            else:
                cur = self._con.execute('''select id from Book
                    where path like ?''', ("%%%s%%" % filter_string, ))

            return cur.fetchall()
        else:
            books = []
            subcollections = self.get_all_collections_in_collection(collection)
            for coll in [collection] + subcollections:
                if filter_string is None:
                    cur = self._con.execute('''select id from Book
                        where id in (select book from Contain where collection = ?)
                        ''', (coll,))
                else:
                    cur = self._con.execute('''select id from Book
                        where id in (select book from Contain where collection = ?)
                        and path like ?''', (coll, "%%%s%%" % filter_string))
                books.extend(cur.fetchall())
            return books

    def get_book_by_path(self, path: str) -> Optional[backend_types._Book]:
        """ Retrieves a book from the library, specified by C{path}.
        If the book doesn't exist, None is returned. Otherwise, a
        L{backend_types._Book} instance is returned. """

        path = os.path.abspath(path)

        cur = self.execute('''select id, name, path, pages, format,
                                     size, added
                              from book where path = ?''', (path,))
        book = cur.fetchone()
        cur.close()

        if book:
            return backend_types._Book(*book)
        else:
            return None

    def get_book_by_id(self, id: int) -> Optional[backend_types._Book]:
        """ Retrieves a book from the library, specified by C{id}.
        If the book doesn't exist, C{None} is returned. Otherwise, a
        L{backend_types._Book} instance is returned. """

        cur = self.execute('''select id, name, path, pages, format,
                                     size, added
                              from book where id = ?''', (id,))
        book = cur.fetchone()
        cur.close()

        if book:
            return backend_types._Book(*book)
        else:
            return None

    def get_book_cover(self, book: int) -> Optional[str]:
        """Return a pixbuf with a thumbnail of the cover of , or
        None if the cover can not be fetched.
        """
        try:
            path = self._con.execute('''select path from Book
                where id = ?''', (book,)).fetchone()
        except Exception:
            log.error(_('! Non-existant book #%i'), book)
            return None

        return self.get_book_thumbnail(path)

    def get_book_path(self, book: int) -> Optional[str]:
        """Return the filesystem path to , or None if  isn't
        in the library.
        """
        try:
            path: Optional[str] = self._con.execute('''select path from Book
                where id = ?''', (book,)).fetchone()
        except Exception:
            log.error(_('! Non-existant book #%i'), book)
            return None

        return path

    def get_book_thumbnail(self, path: str) -> Optional["GdkPixbuf.Pixbuf"]:
        """ Returns a pixbuf with a thumbnail of the cover of the book at ,
        or None, if no thumbnail could be generated. """

        # Use the maximum image size allowed by the library, so that thumbnails
        # might be downscaled, but never need to be upscaled (and look ugly).
        thumbnailer = thumbnail_tools.Thumbnailer(dst_dir=constants.LIBRARY_COVERS_PATH,
                                                  store_on_disk=True,
                                                  archive_support=True,
                                                  size=(constants.MAX_LIBRARY_COVER_SIZE,
                                                        constants.MAX_LIBRARY_COVER_SIZE))
        thumb = thumbnailer.thumbnail(path)

        if thumb is None: log.warning(_('! Could not get cover for book "%s"'), path)
        return thumb

    def get_book_name(self, book: int) -> Optional[str]:
        """Return the name of , or None if  isn't in the
        library.
        """
        cur = self._con.execute('''select name from Book
            where id = ?''', (book,))
        name: Optional[str] = cur.fetchone()
        return name

    def get_book_pages(self, book: int) -> Optional[int]:
        """Return the number of pages in , or None if  isn't
        in the library.
        """
        cur = self._con.execute('''select pages from Book
            where id = ?''', (book,))
        pages: Optional[int] = cur.fetchone()
        return pages

    def get_book_format(self, book: int) -> Optional[str]:
        """Return the archive format of , or None if  isn't
        in the library.
        """
        cur = self._con.execute('''select format from Book
            where id = ?''', (book,))
        format: Optional[str] = cur.fetchone()
        return format

    def get_book_size(self, book: int) -> Optional[int]:
        """Return the size of  in bytes, or None if  isn't
        in the library.
        """
        cur = self._con.execute('''select size from Book
            where id = ?''', (book,))
        size: Optional[int] = cur.fetchone()
        return size

    def get_collections_in_collection(self, collection: Optional[int] = None) -> List[int]:
        """Return a sequence with all the subcollections in ,
        or all top-level collections if  is None.
        """
        if collection is None:
            cur = self._con.execute('''select id from Collection
                where supercollection isnull
                order by case when id = ? then ? else name end''',
                                    (COLLECTION_RECENT, _('Recent')))
        else:
            cur = self._con.execute('''select id from Collection
                where supercollection = ?
                order by name''', (collection,))
        return cur.fetchall()

    def get_all_collections_in_collection(self, collection: int) -> List[int]:
        """ Returns a sequence of  subcollections in ,
        that is, even subcollections that are again a subcollection of one
        of the previous subcollections. """

        if collection is None:
            raise ValueError("Collection must not be ")

        to_search = [collection]
        collections = []
        # This assumes that the library is built like a tree, so no circular references.
        while len(to_search) > 0:
            collection = to_search.pop()
            subcollections = self.get_collections_in_collection(collection)
            collections.extend(subcollections)
            to_search.extend(subcollections)

        return collections

    def get_all_collections(self) -> List[int]:
        """Return a sequence with all collections (flattened hierarchy).
        The sequence is sorted alphabetically by collection name.
        """
        cur = self._con.execute('''select id from Collection
            order by case when id = ? then ? else name end''',
                                (COLLECTION_RECENT, _('Recent')))
        return cur.fetchall()

    def get_collection_name(self, collection: int) -> Optional[str]:
        """Return the name field of the , or None if the
        collection does not exist.
        """
        cur = self._con.execute('''select case when id = ? then ? else name end from Collection
            where id = ?''', (COLLECTION_RECENT, _('Recent'), collection,))
        name: Optional[str] = cur.fetchone()
        return name

    def get_collection_by_name(self, name: str) -> Optional[backend_types._Collection]:
        """Return the collection called , or None if no such
        collection exists. Names are unique, so at most one such collection
        can exist.
        """
        cur = self._con.execute('''select id, name, supercollection
            from collection
            where name = ?''', (name,))
        result = cur.fetchone()
        cur.close()
        if result:
            return backend_types._Collection(*result)
        else:
            return None

    def get_collection_by_id(self, id: int) -> Optional[backend_types._Collection]:
        """ Returns the collection with ID C{id}.
        @param id: Integer value. May be C{-1} or C{None} for default collection.
        @return: L{_Collection} if found, None otherwise.
        """
        if id is None or id == -1:
            return backend_types.DefaultCollection
        elif id == COLLECTION_RECENT:
            return backend_types._Collection(COLLECTION_RECENT, _('Recent'))
        else:
            cur = self._con.execute('''select id, name, supercollection
                from collection
                where id = ?''', (id,))
            result = cur.fetchone()
            cur.close()

            if result:
                return backend_types._Collection(*result)
            else:
                return None

    def get_recent_collection(self) -> backend_types._Collection:
        """ Returns the "Recent" collection, especially created for
        storing recently opened files. """
        collection = self.get_collection_by_id(COLLECTION_RECENT)
        assert collection is not None
        return collection

    def get_supercollection(self, collection: int) -> Optional[int]:
        """Return the supercollection of ."""
        cur = self._con.execute('''select supercollection from Collection
            where id = ?''', (collection,))
        supercollection: Optional[int] = cur.fetchone()
        return supercollection

    def add_book(self, path: str, collection: Optional[int] = None) -> bool:
        """Add the archive at  to the library. If  is
        not None, it is the collection that the books should be put in.
        Return True if the book was successfully added (or was already
        added).
        """
        path = os.path.abspath(path)
        name = os.path.basename(path)
        info = archive_tools.get_archive_info(path)
        if info is None:
            return False
        format, pages, size = info

        # Thumbnail for the newly added book will be generated once it
        # is actually needed with get_book_thumbnail().
        old = self._con.execute('''select id from Book
            where path = ?''', (path,)).fetchone()
        try:
            cursor = self._con.cursor()
            if old is not None:
                cursor.execute('''update Book set
                    name = ?, pages = ?, format = ?, size = ?
                    where path = ?''', (name, pages, format, size, path))
                book_id = old
            else:
                cursor.execute('''insert into Book
                    (name, path, pages, format, size)
                    values (?, ?, ?, ?, ?)''',
                               (name, path, pages, format, size))
                book_id = cursor.lastrowid

                book = backend_types._Book(book_id, name, path, pages,
                                           format, size, datetime.datetime.now().isoformat())
                self.book_added(book)

            cursor.close()

            if collection is not None:
                self.add_book_to_collection(book_id, collection)

            return True
        except dbapi2.Error:
            log.error(_('! Could not add book "%s" to the library'), path)
            return False

    @callback.Callback
    def book_added(self, book: backend_types._Book) -> None:
        """ Event that triggers when a new book is successfully added to the
        library.
        @param book: L{_Book} instance of the newly added book.
        """
        pass

    @callback.Callback
    def book_added_to_collection(self, book: backend_types._Book, collection_id: int) -> None:
        """ Event that triggers when a book is added to the
        specified collection.
        @param book: L{_Book} instance of the added book.
        @param collection_id: ID of the collection.
        """
        pass

    def add_collection(self, name: str) -> bool:
        """Add a new collection with  to the library. Return True
        if the collection was successfully added.
        """
        try:
            # The Recent pseudo collection initializes the lowest rowid
            # with -2, meaning that instead of starting from 1,
            # auto-incremental will start from -1. Avoid this.
            cur = self._con.execute('''select max(id) from collection''')
            maxid = cur.fetchone()
            if maxid is not None and maxid < 1:
                self._con.execute('''insert into collection
                    (id, name) values (?, ?)''', (1, name))
            else:
                self._con.execute('''insert into Collection
                    (name) values (?)''', (name,))
            return True
        except dbapi2.Error:
            log.error(_('! Could not add collection "%s"'), name)
        return False

    def add_book_to_collection(self, book: int, collection: int) -> None:
        """Put  into ."""
        try:
            self._con.execute('''insert into Contain
                (collection, book) values (?, ?)''', (collection, book))
            self.book_added_to_collection(self.get_book_by_id(book),
                                          collection)
        except dbapi2.DatabaseError:  # E.g. book already in collection.
            pass
        except dbapi2.Error:
            log.error(_('! Could not add book %(book)s to collection %(collection)s'),
                      {"book": book, "collection": collection})

    def add_collection_to_collection(self, subcollection: int, supercollection: Optional[int]) -> None:
        """Put  into , or put
         in the root if  is None.
        """
        if supercollection is None:
            self._con.execute('''update Collection
                set supercollection = NULL
                where id = ?''', (subcollection,))
        else:
            self._con.execute('''update Collection
                set supercollection = ?
                where id = ?''', (supercollection, subcollection))

    def rename_collection(self, collection: int, name: str) -> bool:
        """Rename the  to . Return True if the renaming
        was successful.
        """
        try:
            self._con.execute('''update Collection set name = ?
                where id = ?''', (name, collection))
            return True
        except dbapi2.DatabaseError:  # E.g. name taken.
            pass
        except dbapi2.Error:
            log.error(_('! Could not rename collection to "%s"'), name)
        return False

    def duplicate_collection(self, collection: int) -> bool:
        """Duplicate the  by creating a new collection
        containing the same books. Return True if the duplication was
        successful.
        """
        name = self.get_collection_name(collection)
        if name is None:  # Original collection does not exist.
            return False
        copy_name = name + ' ' + _('(Copy)')
        while self.get_collection_by_name(copy_name):
            copy_name = copy_name + ' ' + _('(Copy)')
        if self.add_collection(copy_name) is None:  # Could not create the new.
            return False
        copy_collection = self._con.execute('''select id from Collection
            where name = ?''', (copy_name,)).fetchone()
        self._con.execute('''insert or ignore into Contain (collection, book)
            select ?, book from Contain
            where collection = ?''', (copy_collection, collection))
        return True

    def clean_collection(self, collection: Optional[int] = None) -> int:
        """ Removes files from  that no longer exist. If 
        is None, all collections are cleaned. Returns the number of deleted books. """
        book_ids = self.get_books_in_collection(collection)
        deleted = 0
        for id in book_ids:
            path = self.get_book_path(id)
            if path and not os.path.isfile(path):
                self.remove_book(id)
                deleted += 1

        return deleted

    def remove_book(self, book: int) -> None:
        """Remove the  from the library."""
        path = self.get_book_path(book)
        if path is not None:
            thumbnailer = thumbnail_tools.Thumbnailer(dst_dir=constants.LIBRARY_COVERS_PATH)
            thumbnailer.delete(path)
        self._con.execute('delete from Book where id = ?', (book,))
        self._con.execute('delete from Contain where book = ?', (book,))

    def remove_collection(self, collection: int) -> None:
        """Remove the  (sans books) from the library."""
        self._con.execute('''update watchlist set collection = NULL
            where collection = ?''', (collection,))
        self._con.execute('delete from Collection where id = ?', (collection,))
        self._con.execute('delete from Contain where collection = ?',
                          (collection,))
        self._con.execute('''update Collection set supercollection = NULL
            where supercollection = ?''', (collection,))

    def remove_book_from_collection(self, book: int, collection: int) -> None:
        """Remove  from ."""
        self._con.execute('''delete from Contain
            where book = ? and collection = ?''', (book, collection))

    def execute(self, *args) -> Any:
        """ Passes C{args} directly to the C{execute} method of the SQL
        connection. """
        return self._con.execute(*args)

    def begin_transaction(self) -> None:
        """ Normally, the connection is in auto-commit mode. Calling
        this method will switch to transactional mode, automatically
        starting a transaction when a DML statement is used. """
        self._con.isolation_level = 'IMMEDIATE'

    def end_transaction(self) -> None:
        """ Commits any changes to the database and switches back
        to auto-commit mode. """
        self._con.commit()
        self._con.isolation_level = None

    def close(self) -> None:
        """Commit changes and close cleanly."""
        if self._con is not None:
            self._con.commit()
            self._con.close()

        global _backend
        _backend = None

    def _table_exists(self, table: str) -> bool:
        """ Checks if C{table} exists in the database. """
        cursor = self._con.cursor()
        exists = cursor.execute('pragma table_info(%s)' % table).fetchone() is not None
        cursor.close()
        return exists

    def _library_version(self) -> int:
        """ Examines the library database structure to determine
        which version of MComix created it.

        @return C{version} from the table C{Info} if available,
        C{0} otherwise. C{-1} if the database has not been created yet."""

        # Check if Comix' tables exist
        tables = ('book', 'collection', 'contain')
        for table in tables:
            if not self._table_exists(table):
                return -1

        if self._table_exists('info'):
            cursor = self._con.cursor()
            version = cursor.execute('''select value from info
                where key = 'version' ''').fetchone()
            cursor.close()

            if not version:
                log.warning(_('Could not determine library database version!'))
                return -1
            else:
                return int(version)
        else:
            # Comix database format
            return 0

    def _create_tables(self) -> None:
        """ Creates all required tables in the database. """
        self._create_table_book()
        self._create_table_collection()
        self._create_table_contain()
        self._create_table_info()
        self._create_table_watchlist()
        self._create_table_recent()

    def _upgrade_database(self, from_version: int, to_version: int) -> None:
        """ Performs sequential upgrades to the database, bringing
        it from C{from_version} to C{to_version}. If C{from_version}
        is -1, the database structure will simply be re-created at the
        current version. """

        if from_version == -1:
            self._create_tables()
            return

        if from_version != to_version:
            upgrades = list(range(from_version, to_version))
            log.info(_("Upgrading library database version from %(from)d to %(to)d."),
                     {"from": from_version, "to": to_version})

            if 0 in upgrades:
                # Upgrade from Comix database structure to DB version 1
                # (Added table 'info')
                self._create_table_info()

            if 1 in upgrades:
                # Upgrade to database structure version 2.
                # (Added table 'watchlist' for storing auto-add directories)
                self._create_table_watchlist()

            if 2 in upgrades:
                # Changed 'added' field in 'book' from date to datetime.
                self._con.execute('''alter table book rename to book_old''')
                self._create_table_book()
                self._con.execute('''insert into book
                    (id, name, path, pages, format, size, added)
                    select id, name, path, pages, format, size, datetime(added)
                    from book_old''')
                self._con.execute('''drop table book_old''')

            if 3 in upgrades:
                # Added field 'recursive' to table 'watchlist'
                self._con.execute('''alter table watchlist rename to watchlist_old''')
                self._create_table_watchlist()
                self._con.execute('''insert into watchlist
                    (path, collection, recursive)
                    select path, collection, 0 from watchlist_old''')
                self._con.execute('''drop table watchlist_old''')

            if 4 in upgrades:
                # Added table 'recent' to store recently viewed book information and
                # create a collection (-2, Recent)
                self._create_table_recent()
                lastread = last_read_page.LastReadPage(self)
                lastread.migrate_database_to_library(COLLECTION_RECENT)

            if 5 in upgrades:
                # Changed all 'string' columns into 'text' columns
                self._con.execute('''alter table book rename to book_old''')
                self._create_table_book()
                self._con.execute('''insert into book
                    (id, name, path, pages, format, size, added)
                    select id, name, path, pages, format, size, added from book_old''')
                self._con.execute('''drop table book_old''')

                self._con.execute('''alter table collection rename to collection_old''')
                self._create_table_collection()
                self._con.execute('''insert into collection
                    (id, name, supercollection)
                    select id, name, supercollection from collection_old''')
                self._con.execute('''drop table collection_old''')

            if 6 in upgrades:
                # Non-localized name for Recent collection
                self._con.execute('''update collection set name = ? where id = ?''',
                                  ('RECENT', COLLECTION_RECENT))

            self._con.execute('''update info set value = ? where key = 'version' ''',
                              (str(_LibraryBackend.DB_VERSION),))

    def _create_table_book(self) -> None:
        self._con.execute('''create table if not exists book (
            id integer primary key,
            name text,
            path text unique,
            pages integer,
            format integer,
            size integer,
            added datetime default current_timestamp)''')

    def _create_table_collection(self) -> None:
        self._con.execute('''create table if not exists collection (
            id integer primary key,
            name text unique,
            supercollection integer)''')

    def _create_table_contain(self) -> None:
        self._con.execute('''create table if not exists contain (
            collection integer not null,
            book integer not null,
            primary key (collection, book))''')

    def _create_table_info(self) -> None:
        self._con.execute('''create table if not exists info (
            key text primary key,
            value text)''')
        self._con.execute('''insert into info
            (key, value) values ('version', ?)''',
                          (str(_LibraryBackend.DB_VERSION),))

    def _create_table_watchlist(self) -> None:
        self._con.execute('''create table if not exists watchlist (
            path text primary key,
            collection integer references collection (id) on delete set null,
            recursive boolean not null)''')

    def _create_table_recent(self) -> None:
        self._con.execute('''create table if not exists recent (
            book integer primary key,
            page integer,
            time_set datetime)''')
        self._con.execute('''insert or ignore into collection (id, name)
            values (?, ?)''', (COLLECTION_RECENT, 'RECENT'))


_backend = None


def LibraryBackend() -> _LibraryBackend:
    """ Returns the singleton instance of the library backend. """
    global _backend
    if _backend is not None:
        return _backend
    else:
        _backend = _LibraryBackend()
        return _backend

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701597438.0
mcomix-3.1.0/mcomix/library/backend_types.py0000644000175000017500000003522114533050376020637 0ustar00moritzmoritz""" Data class for library books and collections. """

import os
import threading
import datetime

from mcomix import callback
from mcomix import archive_tools
from mcomix.i18n import _


class _BackendObject(object):

    def get_backend(self):
        # XXX: Delayed import to avoid circular import
        from mcomix.library.backend import LibraryBackend
        return LibraryBackend()


class _Book(_BackendObject):
    """ Library book instance. """

    def __init__(self, id, name, path, pages, format, size, added):
        """ Creates a book instance.
        @param id: Book id
        @param name: Base name of the book
        @param path: Full path to the book
        @param pages: Number of pages
        @param format: One of the archive formats in L{constants}
        @param size: File size in bytes
        @param added: Datetime when book was added to library """

        self.id = id
        self.name = name
        self.path = path
        self.pages = pages
        self.format = format
        self.size = size
        self.added = added

    def get_collections(self):
        """ Gets a list of collections this book is part of. If it
        belongs to no collections, [DefaultCollection] is returned. """
        cursor = self.get_backend().execute(
            '''SELECT id, name, supercollection FROM collection
               JOIN contain on contain.collection = collection.id
               WHERE contain.book = ?''', (self.id,))
        rows = cursor.fetchall()
        if rows:
            return [_Collection(*row) for row in rows]
        else:
            return [DefaultCollection]

    def get_last_read_page(self):
        """ Gets the page of this book that was last read when the book was
        closed. Returns C{None} if no such page exists. """
        cursor = self.get_backend().execute(
            '''SELECT page FROM recent WHERE book = ?''', (self.id,))
        row = cursor.fetchone()
        cursor.close()
        if row:
            return row
        else:
            return None

    def get_last_read_date(self):
        """ Gets the datetime the book was most recently read. Returns
        C{None} if no information was set, or a datetime object otherwise. """
        cursor = self.get_backend().execute(
            """SELECT time_set FROM recent WHERE book = ?""", (self.id,))
        date = cursor.fetchone()
        cursor.close()

        if date:
            try:
                return datetime.datetime.strptime(date, '%Y-%m-%d %H:%M:%S.%f')
            except ValueError:
                # Certain operating systems do not store fractions
                return datetime.datetime.strptime(date, '%Y-%m-%d %H:%M:%S')
        else:
            return None

    def set_last_read_page(self, page, time=None):
        """ Sets the page that was last read when the book was closed.
        Passing C{None} as argument clears the recent information.

        @param page: Page number, starting from 1 (page 1 throws ValueError)
        @param time: Time of reading. If None, current time is used. """

        if page is not None and page < 1:
            # Avoid wasting memory by creating a recently viewed entry when
            # an archive was opened on page 1.
            raise ValueError('Invalid page (must start from 1)')

        # Remove any old recent row for this book
        cursor = self.get_backend().execute(
            '''DELETE FROM recent WHERE book = ?''', (self.id,))
        # If a new page was passed, set it as recently read
        if page is not None:
            if not time:
                time = datetime.datetime.now()
            cursor.execute('''INSERT INTO recent (book, page, time_set)
                              VALUES (?, ?, ?)''',
                           (self.id, page, time))

        cursor.close()


class _Collection(_BackendObject):
    """ Library collection instance.
    This class should NOT be instianted directly, but only with methods from
    L{LibraryBackend} instead. """

    def __init__(self, id, name, supercollection=None):
        """ Creates a collection instance.
        @param id: Collection id
        @param name: Name of the collection
        @param supercollection: Parent collection, or C{None} """

        self.id = id
        self.name = name
        self.supercollection = supercollection

    def __eq__(self, other):
        if isinstance(other, _Collection):
            return self.id == other.id
        elif isinstance(other, int):
            return self.id == other
        else:
            return False

    def get_books(self, filter_string=None):
        """ Returns all books that are part of this collection,
        including subcollections. """

        books = []
        for collection in [ self ] + self.get_all_collections():
            sql = '''SELECT book.id, book.name, book.path, book.pages, book.format,
                            book.size, book.added
                     FROM book
                     JOIN contain ON contain.book = book.id
                                     AND contain.collection = ?
                  '''

            sql_args = [collection.id]
            if filter_string:
                sql += ''' WHERE book.name LIKE '%' || ? || '%' '''
                sql_args.append(filter_string)
                sql += ''' OR book.path LIKE '%' || ? || '%' '''
                sql_args.append(filter_string)

            cursor = self.get_backend().execute(sql, sql_args)
            rows = cursor.fetchall()
            cursor.close()

            books.extend([ _Book(*cols) for cols in rows ])

        return books

    def get_collections(self):
        """ Returns a list of all direct subcollections of this instance. """

        cursor = self.get_backend().execute('''SELECT id, name, supercollection
                FROM collection
                WHERE supercollection = ?
                ORDER by name''', [self.id])
        result = cursor.fetchall()
        cursor.close()

        return [ _Collection(*row) for row in result ]

    def get_all_collections(self):
        """ Returns all collections that are subcollections of this instance,
        or subcollections of a subcollection of this instance. """

        to_search = [ self ]
        collections = [ ]
        # This assumes that the library is built like a tree, so no circular references.
        while len(to_search) > 0:
            collection = to_search.pop()
            subcollections = collection.get_collections()
            collections.extend(subcollections)
            to_search.extend(subcollections)

        return collections

    def add_collection(self, subcollection):
        """ Sets C{subcollection} as child of this collection. """

        self.get_backend().execute('''UPDATE collection
                SET supercollection = ?
                WHERE id = ?''', (self.id, subcollection.id))
        subcollection.supercollection = self.id


class _DefaultCollection(_Collection):
    """ Represents the default collection that books belong to if
    no explicit collection was specified. """

    def __init__(self):

        self.id = None
        self.name = _("All books")
        self.supercollection = None

    def get_books(self, filter_string=None):
        """ Returns all books in the library """
        sql = '''SELECT book.id, book.name, book.path, book.pages, book.format,
                        book.size, book.added
                 FROM book
              '''

        sql_args = []
        if filter_string:
            sql += ''' WHERE book.name LIKE '%' || ? || '%' '''
            sql_args.append(filter_string)
            sql += ''' OR book.path LIKE '%' || ? || '%' '''
            sql_args.append(filter_string)

        cursor = self.get_backend().execute(sql, sql_args)
        rows = cursor.fetchall()
        cursor.close()

        return [ _Book(*cols) for cols in rows ]

    def add_collection(self, subcollection):
        """ Removes C{subcollection} from any supercollections and moves
        it to the root level of the tree. """

        assert subcollection is not DefaultCollection, "Cannot change DefaultCollection"

        self.get_backend().execute('''UPDATE collection
                SET supercollection = NULL
                WHERE id = ?''', (subcollection.id,))
        subcollection.supercollection = None

    def get_collections(self):
        """ Returns a list of all root collections. """

        cursor = self.get_backend().execute('''SELECT id, name, supercollection
                FROM collection
                WHERE supercollection IS NULL
                ORDER by name''')
        result = cursor.fetchall()
        cursor.close()

        return [ _Collection(*row) for row in result ]


DefaultCollection = _DefaultCollection()


class _WatchList(object):
    """ Scans watched directories and updates the database when new books have
    been added. This object is part of the library backend, i.e.
    C{library.backend.watchlist}. """

    def __init__(self, backend):
        self.backend = backend

    def add_directory(self, path, collection=DefaultCollection, recursive=False):
        """ Adds a new watched directory. """

        directory = os.path.normpath(os.path.abspath(path))
        sql = """INSERT OR IGNORE INTO watchlist (path, collection, recursive)
                 VALUES (?, ?, ?)"""
        cursor = self.backend.execute(sql, [directory, collection.id, recursive])
        cursor.close()

    def get_watchlist(self):
        """ Returns a list of watched directories.
        @return: List of L{_WatchListEntry} objects. """

        sql = """SELECT watchlist.path,
                        watchlist.recursive,
                        collection.id, collection.name,
                        collection.supercollection
                 FROM watchlist
                 LEFT JOIN collection ON watchlist.collection = collection.id"""

        cursor = self.backend.execute(sql)
        entries = [self._result_row_to_watchlist_entry(row) for row in cursor.fetchall()]
        cursor.close()

        return entries

    def get_watchlist_entry(self, path):
        """ Returns a single watchlist entry, specified by C{path} """
        sql = """SELECT watchlist.path,
                        watchlist.recursive,
                        collection.id, collection.name,
                        collection.supercollection
                 FROM watchlist
                 LEFT JOIN collection ON watchlist.collection = collection.id
                 WHERE watchlist.path = ?"""

        cursor = self.backend.execute(sql, (os.path.normpath(path), ))
        result = cursor.fetchone()
        cursor.close()

        if result:
            return self._result_row_to_watchlist_entry(result)
        else:
            raise ValueError("Watchlist entry doesn't exist")

    def scan_for_new_files(self):
        """ Begins scanning for new files in the watched directories.
        When the scan finishes, L{new_files_found} will be called
        asynchronously. """
        thread = threading.Thread(target=self._scan_for_new_files_thread)
        thread.name += '-scan_for_new_files'
        thread.start()

    def _scan_for_new_files_thread(self):
        """ Executes the actual scanning operation in a new thread. """
        existing_books = [book.path for book in DefaultCollection.get_books()
                          # Also add book if it was only found in Recent collection
                          if book.get_collections() != [-2]]
        for entry in self.get_watchlist():
            new_files = entry.get_new_files(existing_books)
            self.new_files_found(new_files, entry)

    def _result_row_to_watchlist_entry(self, row):
        """ Converts the result of a SELECT statement to a WatchListEntry. """
        collection_id = row[2]
        if collection_id:
            collection = _Collection(*row[2:])
        else:
            collection = DefaultCollection

        return _WatchListEntry(row[0], row[1], collection)


    @callback.Callback
    def new_files_found(self, paths, watchentry):
        """ Called after scan_for_new_files finishes.
        @param paths: List of filenames for newly added files. This list
                      may be empty if no new files were found during the scan.
        @param watchentry: Watchentry for files/directory.
        """
        pass


class _WatchListEntry(_BackendObject):
    """ A watched directory. """

    def __init__(self, directory, recursive, collection):
        self.directory = os.path.normpath(os.path.abspath(directory))
        self.recursive = bool(recursive)
        self.collection = collection

    def get_new_files(self, filelist):
        """ Returns a list of files that are present in the watched directory,
        but not in the list of files passed in C{filelist}. """

        if not self.is_valid():
            return []

        old_files = frozenset([os.path.abspath(path) for path in filelist])

        if not self.recursive:
            available_files = frozenset([os.path.join(self.directory, filename)
                for filename in os.listdir(self.directory)
                if archive_tools.is_archive_file(filename)])
        else:
            available_files = []
            for dirpath, dirnames, filenames in os.walk(self.directory):
                for filename in filter(archive_tools.is_archive_file, filenames):
                    path = os.path.join(dirpath, filename)
                    available_files.append(path)

            available_files = frozenset(available_files)

        return list(available_files.difference(old_files))

    def is_valid(self):
        """ Check if the watched directory is a valid directory and exists. """
        return os.path.isdir(self.directory)

    def remove(self):
        """ Removes this entry from the watchlist, deleting its associated
        path from the database. """
        sql = """DELETE FROM watchlist WHERE path = ?"""
        cursor = self.get_backend().execute(sql, (self.directory,))
        cursor.close()

        self.directory = ""
        self.collection = None

    def set_collection(self, new_collection):
        """ Updates the collection associated with this watchlist entry. """
        if new_collection != self.collection:
            sql = """UPDATE watchlist SET collection = ? WHERE path = ?"""
            cursor = self.get_backend().execute(sql,
                    (new_collection.id, self.directory))
            cursor.close()
            self.collection = new_collection

    def set_recursive(self, recursive):
        """ Enables or disables recursive scanning. """
        if recursive != self.recursive:
            sql = """UPDATE watchlist SET recursive = ? WHERE path = ?"""
            cursor = self.get_backend().execute(sql,
                    (recursive, self.directory))
            cursor.close()
            self.recursive = recursive


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704114443.0
mcomix-3.1.0/mcomix/library/book_area.py0000644000175000017500000007532014544534413017753 0ustar00moritzmoritz"""library_book_area.py - The window of the library that displays the covers of books."""

import os
import urllib.request, urllib.parse, urllib.error
from gi.repository import Gdk, GdkPixbuf, GLib, Gtk, GObject
import PIL.Image as Image
import PIL.ImageDraw as ImageDraw

from mcomix.preferences import prefs
from mcomix import thumbnail_view
from mcomix import file_chooser_library_dialog
from mcomix import image_tools
from mcomix import constants
from mcomix import portability
from mcomix import i18n
from mcomix import status
from mcomix import log
from mcomix import message_dialog
from mcomix import tools
from mcomix.library.pixbuf_cache import get_pixbuf_cache
from mcomix.i18n import _

_dialog = None

# The "All books" collection is not a real collection stored in the library, but is represented by this ID in the
# library's TreeModels.
_COLLECTION_ALL = -1


class _BookArea(Gtk.ScrolledWindow):

    """The _BookArea is the central area in the library where the book
    covers are displayed.
    """

    # Thumbnail border width in pixels.
    _BORDER_SIZE = 1

    def __init__(self, library):
        super(_BookArea, self).__init__()

        self._library = library
        self._cache = get_pixbuf_cache()

        self._library.backend.book_added_to_collection += self._new_book_added

        self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)

        # Store Cover, book ID, book path, book size, date added to library,
        # is thumbnail loaded?

        # The SORT_ constants must correspond to the correct column here,
        # i.e. SORT_SIZE must be 3, since 3 is the size column in the ListStore.
        self._liststore = Gtk.ListStore(GdkPixbuf.Pixbuf,
                GObject.TYPE_INT, GObject.TYPE_STRING, GObject.TYPE_INT64,
                GObject.TYPE_STRING, GObject.TYPE_BOOLEAN)
        self._liststore.set_sort_func(constants.SORT_NAME, self._sort_by_name, None)
        self._liststore.set_sort_func(constants.SORT_PATH, self._sort_by_path, None)
        self.set_sort_order()
        self._liststore.connect('row-inserted', self._icon_added)
        self._iconview = thumbnail_view.ThumbnailIconView(
            self._liststore,
            1, # UID
            0, # pixbuf
            5, # status
        )
        self._iconview.generate_thumbnail = self._get_pixbuf
        self._iconview.connect('item_activated', self._book_activated)
        self._iconview.connect('selection_changed', self._selection_changed)
        self._iconview.connect_after('drag_begin', self._drag_begin)
        self._iconview.connect('drag_data_get', self._drag_data_get)
        self._iconview.connect('drag_data_received', self._drag_data_received)
        self._iconview.connect('button_press_event', self._button_press)
        self._iconview.connect('key_press_event', self._key_press)
        self._iconview.connect('popup_menu', self._popup_menu)
        self._iconview.modify_base(Gtk.StateType.NORMAL, image_tools.GTK_GDK_COLOR_BLACK)
        self._iconview.enable_model_drag_source(
            Gdk.ModifierType.BUTTON1_MASK,
            [Gtk.TargetEntry.new('text/plain', Gtk.TargetFlags.SAME_APP,
                                 constants.LIBRARY_DRAG_EXTERNAL_ID)],
            Gdk.DragAction.MOVE)
        self._iconview.drag_dest_set(
            Gtk.DestDefaults.ALL,
            [Gtk.TargetEntry.new('text/uri-list', 0,
                                 constants.LIBRARY_DRAG_EXTERNAL_ID)],
            Gdk.DragAction.COPY | Gdk.DragAction.MOVE)
        self._iconview.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
        self.add(self._iconview)

        self._iconview.set_margin(0)
        self._iconview.set_row_spacing(0)
        self._iconview.set_column_spacing(0)

        self._ui_manager = Gtk.UIManager()
        self._tooltipstatus = status.TooltipStatusHelper(self._ui_manager,
            self._library.get_status_bar())

        ui_description = """
        
            
                
                
                
                
                
                
                
                
                
                
                
                
                
                
                    
                    
                    
                    
                    
                    
                    
                
                
                    
                    
                    
                    
                    
                    
                    
                
            
        
        """

        self._ui_manager.add_ui_from_string(ui_description)
        actiongroup = Gtk.ActionGroup('mcomix-library-book-area')
        # General book actions
        actiongroup.add_actions([
            ('_title', None, _('Library books'), None, None,
                None),
            ('open', Gtk.STOCK_OPEN, _('_Open'), None,
                _('Opens the selected books for viewing.'),
                self.open_selected_book),
            ('open keep library', Gtk.STOCK_OPEN,
                _('Open _without closing library'), None,
                _('Opens the selected books, but keeps the library window open.'),
                self.open_selected_book_noclose),
            ('add', Gtk.STOCK_ADD, _('_Add...'), 'a',
                _('Add more books to the library.'),
                lambda *args: file_chooser_library_dialog.open_library_filechooser_dialog(self._library)),
            ('remove from collection', Gtk.STOCK_REMOVE,
                _('Remove from this _collection'), None,
                _('Removes the selected books from the current collection.'),
                self._remove_books_from_collection),
            ('remove from library', Gtk.STOCK_REMOVE,
                _('Remove from the _library'), None,
                _('Completely removes the selected books from the library.'),
                self._remove_books_from_library),
            ('completely remove', Gtk.STOCK_DELETE,
                _('_Remove and delete from disk'), None,
                _('Deletes the selected books from disk.'),
                self._completely_remove_book),
            ('copy to clipboard', Gtk.STOCK_COPY,
                _('_Copy'), None,
                _('Copies the selected book\'s path to clipboard.'),
                self._copy_selected),
            ('sort', None, _('_Sort'), None,
                _('Changes the sort order of the library.'), None),
            ('cover size', None, _('Cover si_ze'), None,
                _('Changes the book cover size.'), None)
       ])
        # Sorting the view
        actiongroup.add_radio_actions([
            ('by name', None, _('Book name'), None, None, constants.SORT_NAME),
            ('by path', None, _('Full path'), None, None, constants.SORT_PATH),
            ('by size', None, _('File size'), None, None, constants.SORT_SIZE),
            ('by date added', None, _('Date added'), None, None, constants.SORT_LAST_MODIFIED)],
            prefs['lib sort key'], self._sort_changed)
        actiongroup.add_radio_actions([
            ('ascending', Gtk.STOCK_SORT_ASCENDING, _('Ascending'), None, None,
                constants.SORT_ASCENDING),
            ('descending', Gtk.STOCK_SORT_DESCENDING, _('Descending'), None, None,
                constants.SORT_DESCENDING)],
            prefs['lib sort order'], self._sort_changed)

        # Library cover size
        actiongroup.add_radio_actions([
            ('huge', None, _('Huge') + '  (%dpx)' % constants.SIZE_HUGE,
                None, None, constants.SIZE_HUGE),
            ('large', None, _('Large') + '  (%dpx)' % constants.SIZE_LARGE,
                None, None, constants.SIZE_LARGE),
            ('normal', None, _('Normal') + '  (%dpx)' % constants.SIZE_NORMAL,
                None, None, constants.SIZE_NORMAL),
            ('small', None, _('Small') + '  (%dpx)' % constants.SIZE_SMALL,
                None, None, constants.SIZE_SMALL),
            ('tiny', None, _('Tiny') + '  (%dpx)' % constants.SIZE_TINY,
                None, None, constants.SIZE_TINY),
            ('custom', None, _('Custom...'), None, None, 0)],
            prefs['library cover size']
                if prefs['library cover size'] in (constants.SIZE_HUGE,
                    constants.SIZE_LARGE, constants.SIZE_NORMAL,
                    constants.SIZE_SMALL, constants.SIZE_TINY)
                else 0,
            self._book_size_changed)

        self._ui_manager.insert_action_group(actiongroup, 0)
        library.add_accel_group(self._ui_manager.get_accel_group())

    def close(self):
        """Run clean-up tasks for the _BookArea prior to closing."""

        self.stop_update()

        # We must unselect all or we will trigger selection_changed events
        # when closing with multiple books selected.
        self._iconview.unselect_all()
        # We must (for some reason) explicitly clear the ListStore in
        # order to not leak memory.
        self._liststore.clear()

    def display_covers(self, collection_id):
        """Display the books in  in the IconView."""

        adjustment = self.get_vadjustment()
        if adjustment:
            adjustment.set_value(0)

        self.stop_update()
        # Temporarily detach model to speed up updates
        self._iconview.set_model(None)
        self._liststore.clear()

        collection = self._library.backend.get_collection_by_id(collection_id)
        books = collection.get_books(self._library.filter_string)
        self.add_books(books)

        # Re-attach model here
        GLib.idle_add(self._iconview.set_model, self._liststore)

    def stop_update(self):
        """Signal that the updating of book covers should stop."""
        self._iconview.stop_update()

    def add_books(self, books):
        """ Adds new book covers to the icon view.
        @param books: List of L{_Book} instances. """
        filler = self._get_empty_thumbnail()

        for book in books:
            # Fill the liststore with a filler pixbuf.
            self._liststore.append([filler, book.id,
                                    book.path,
                                    book.size, book.added, False])

        self._iconview.draw_thumbnails_on_screen()

    def _new_book_added(self, book, collection):
        """ Callback function for L{LibraryBackend.book_added}. """
        if collection is None:
            collection = _COLLECTION_ALL

        if (collection == self._library.collection_area.get_current_collection() or
            self._library.collection_area.get_current_collection() == _COLLECTION_ALL):
            # Make sure not to show a book twice when COLLECTION_ALL is selected
            # and the book is added to another collection, triggering this event.
            if self.is_book_displayed(book):
                return

            # If the current view is filtered, only draw new books that match the filter
            if not (self._library.filter_string and
                    self._library.filter_string.lower() not in book.name.lower()):
                self.add_books([book])

    def is_book_displayed(self, book):
        """ Returns True when the current view contains the book passed.
        @param book: L{_Book} instance. """
        if not book:
            return False

        for row in self._liststore:
            if row[1] == book.id:
                return True

        return False

    def remove_book_at_path(self, path):
        """Remove the book at  from the ListStore (and thus from
        the _BookArea).
        """
        iterator = self._liststore.get_iter(path)
        filepath = self._liststore.get_value(iterator, 2)
        self._liststore.remove(iterator)
        self._cache.invalidate(filepath)

    def get_book_at_path(self, path):
        """Return the book ID corresponding to the IconView ."""
        iterator = self._liststore.get_iter(path)
        return self._liststore.get_value(iterator, 1)

    def get_book_path(self, book):
        """Return the  to the book from the ListStore.
        """
        return self._liststore.get_iter(book)

    def open_selected_book(self, *args):
        """Open the currently selected book."""
        selected = self._iconview.get_selected_items()
        if not selected:
            return
        self._book_activated(self._iconview, selected, False)

    def open_selected_book_noclose(self, *args):
        """Open the currently selected book, keeping the library open."""
        selected = self._iconview.get_selected_items()
        if not selected:
            return
        self._book_activated(self._iconview, selected, True)

    def set_sort_order(self):
        """ Orders the list store based on the key passed in C{sort_key}.
        Should be one of the C{SORT_} constants from L{constants}.
        """
        if prefs['lib sort order'] == constants.SORT_ASCENDING:
            sortorder = Gtk.SortType.ASCENDING
        else:
            sortorder = Gtk.SortType.DESCENDING

        self._liststore.set_sort_column_id(prefs['lib sort key'], sortorder)

    def _sort_changed(self, old, current):
        """ Called whenever the sorting options changed. """
        name = current.get_name()
        if name == 'by name':
            prefs['lib sort key'] = constants.SORT_NAME
        elif name == 'by path':
            prefs['lib sort key'] = constants.SORT_PATH
        elif name == 'by size':
            prefs['lib sort key'] = constants.SORT_SIZE
        elif name == 'by date added':
            prefs['lib sort key'] = constants.SORT_LAST_MODIFIED

        if name == 'ascending':
            prefs['lib sort order'] = constants.SORT_ASCENDING
        elif name == 'descending':
            prefs['lib sort order'] = constants.SORT_DESCENDING

        self.set_sort_order()

    def _sort_by_name(self, treemodel, iter1, iter2, user_data):
        """ Compares two books based on their file name without the
        path component. """
        path1 = self._liststore.get_value(iter1, 2)
        path2 = self._liststore.get_value(iter2, 2)

        # Catch None values from liststore
        if path1 is None:
            return 1
        elif path2 is None:
            return -1

        name1 = os.path.split(path1)[1].lower()
        name2 = os.path.split(path2)[1].lower()

        return tools.cmp(tools.AlphanumericSortKey(name1), tools.AlphanumericSortKey(name2))

    def _sort_by_path(self, treemodel, iter1, iter2, user_data):
        """ Compares two books based on their full path, in natural order. """
        path1 = self._liststore.get_value(iter1, 2)
        path2 = self._liststore.get_value(iter2, 2)
        return tools.cmp(tools.AlphanumericSortKey(path1), tools.AlphanumericSortKey(path2))

    def _icon_added(self, model, path, iter, *args):
        """ Justifies the alignment of all cell renderers when new data is
        added to the model. """
        width, height = self._pixbuf_size()
        for cell in self._iconview.get_cells():
            cell.set_fixed_size(width, height)
            cell.set_alignment(0.5, 0.5)

    def load_covers(self):
        self._cache.invalidate_all()
        collection = self._library.collection_area.get_current_collection()
        GLib.idle_add(self.display_covers, collection)

    def _book_size_changed(self, old, current):
        """ Called when library cover size changes. """
        old_size = prefs['library cover size']
        name = current.get_name()
        if name == 'huge':
            prefs['library cover size'] = constants.SIZE_HUGE
        elif name == 'large':
            prefs['library cover size'] = constants.SIZE_LARGE
        elif name == 'normal':
            prefs['library cover size'] = constants.SIZE_NORMAL
        elif name == 'small':
            prefs['library cover size'] = constants.SIZE_SMALL
        elif name == 'tiny':
            prefs['library cover size'] = constants.SIZE_TINY
        elif name == 'custom':
            dialog = message_dialog.MessageDialog(self._library, Gtk.DialogFlags.DESTROY_WITH_PARENT,
                Gtk.MessageType.INFO, buttons=Gtk.ButtonsType.OK)
            dialog.set_auto_destroy(False)
            dialog.set_text(_('Set library cover size'))

            # Add adjustment scale
            adjustment = Gtk.Adjustment(prefs['library cover size'], 20,
                    constants.MAX_LIBRARY_COVER_SIZE, 10, 25, 0)
            cover_size_scale = Gtk.HScale(adjustment)
            cover_size_scale.set_size_request(200, -1)
            cover_size_scale.set_digits(0)
            cover_size_scale.set_draw_value(True)
            cover_size_scale.set_value_pos(Gtk.PositionType.LEFT)
            for mark in (constants.SIZE_HUGE, constants.SIZE_LARGE,
                    constants.SIZE_NORMAL, constants.SIZE_SMALL,
                    constants.SIZE_TINY):
                cover_size_scale.add_mark(mark, Gtk.PositionType.TOP, None)

            dialog.get_message_area().pack_end(cover_size_scale, True, True, 0)
            response = dialog.run()
            size = int(adjustment.get_value())
            dialog.destroy()

            if response == Gtk.ResponseType.OK:
                prefs['library cover size'] = size

        if prefs['library cover size'] != old_size:
            self.load_covers()

    def _pixbuf_size(self, border_size=_BORDER_SIZE):
        # Don't forget the extra pixels for the border!
        # The ratio (0.67) is just above the normal aspect ratio for books.
        return (int(0.67 * prefs['library cover size']) + 2 * border_size,
                prefs['library cover size'] + 2 * border_size)

    def _get_pixbuf(self, uid):
        """ Get or create the thumbnail for the selected book . """
        assert isinstance(uid, int)
        book = self._library.backend.get_book_by_id(uid)
        if self._cache.exists(book.path):
            pixbuf = self._cache.get(book.path)
        else:
            width, height = self._pixbuf_size(border_size=0)
            try:
                pixbuf = self._library.backend.get_book_thumbnail(book.path) or image_tools.MISSING_IMAGE_ICON
            except:
                pixbuf = image_tools.MISSING_IMAGE_ICON
            pixbuf = image_tools.fit_in_rectangle(pixbuf, width, height, scale_up=True)
            self._cache.add(book.path, pixbuf)

        pixbuf = self._library._window.enhancer.enhance(pixbuf);
        pixbuf = image_tools.add_border(pixbuf, 1, 0xFFFFFFFF)

        # Display indicator of having finished reading the book.
        # This information isn't cached in the pixbuf cache, as it changes frequently.

        # Anything smaller than 50px means that the status icon will not fit
        if prefs['library cover size'] < 50:
            return pixbuf

        last_read_page = book.get_last_read_page()
        if last_read_page is None or last_read_page != book.pages:
            return pixbuf

        # Composite icon on the lower right corner of the book cover pixbuf.
        book_pixbuf = self.render_icon(Gtk.STOCK_APPLY, Gtk.IconSize.LARGE_TOOLBAR)
        translation_x = pixbuf.get_width() - book_pixbuf.get_width() - 1
        translation_y = pixbuf.get_height() - book_pixbuf.get_height() - 1
        book_pixbuf.composite(pixbuf, translation_x, translation_y,
                              book_pixbuf.get_width(), book_pixbuf.get_height(),
                              translation_x, translation_y,
                              1.0, 1.0, GdkPixbuf.InterpType.NEAREST, 0xFF)

        return pixbuf

    def _get_empty_thumbnail(self):
        """ Create an empty filler pixmap. """
        width, height = self._pixbuf_size()
        pixbuf = GdkPixbuf.Pixbuf.new(colorspace=GdkPixbuf.Colorspace.RGB,
                                      has_alpha=True,
                                      bits_per_sample=8,
                                      width=width, height=height)

        # Make the pixbuf transparent.
        pixbuf.fill(0)

        return pixbuf

    def _book_activated(self, iconview, paths, keep_library_open=False):
        """Open the book at the (liststore) ."""
        if not isinstance(paths, list):
            paths = [ paths ]

        if not keep_library_open:
            # Necessary to prevent a deadlock at exit when trying to "join" the
            # worker thread.
            self.stop_update()
        books = [ self.get_book_at_path(path) for path in paths ]
        self._library.open_book(books, keep_library_open=keep_library_open)

    def _selection_changed(self, iconview):
        """Update the displayed info in the _ControlArea when a new book
        is selected.
        """
        selected = iconview.get_selected_items()
        self._library.control_area.update_info(selected)

    def _remove_books_from_collection(self, *args):
        """Remove the currently selected books from the current collection,
        and thus also from the _BookArea.
        """
        collection = self._library.collection_area.get_current_collection()
        if collection == _COLLECTION_ALL:
            return
        selected = self._iconview.get_selected_items()
        self._library.backend.begin_transaction()
        for path in selected:
            book = self.get_book_at_path(path)
            self._library.backend.remove_book_from_collection(book, collection)
            self.remove_book_at_path(path)
        self._library.backend.end_transaction()

        coll_name = self._library.backend.get_collection_name(collection)
        message = i18n.get_translation().ngettext(
                "Removed %(num)d book from '%(collection)s'.",
                "Removed %(num)d books from '%(collection)s'.",
                len(selected))
        self._library.set_status_message(
            message % {'num': len(selected), 'collection': coll_name})

    def _remove_books_from_library(self, *args):
        """Remove the currently selected books from the library, and thus
        also from the _BookArea.
        """

        selected = self._iconview.get_selected_items()
        self._library.backend.begin_transaction()

        for path in selected:
            book = self.get_book_at_path(path)
            self._library.backend.remove_book(book)
            self.remove_book_at_path(path)

        self._library.backend.end_transaction()

        msg = i18n.get_translation().ngettext(
            'Removed %d book from the library.',
            'Removed %d books from the library.',
            len(selected))
        self._library.set_status_message(msg % len(selected))

    def _completely_remove_book(self, request_response=True, *args):
        """Remove the currently selected books from the library and the
        hard drive.
        """

        if request_response:

            choice_dialog = message_dialog.MessageDialog(self._library, 0,
                Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO)
            choice_dialog.set_default_response(Gtk.ResponseType.YES)
            choice_dialog.set_should_remember_choice('library-remove-book-from-disk',
                (Gtk.ResponseType.YES,))
            choice_dialog.set_text(
                _('Remove books from the library?'),
                _('The selected books will be removed from the library and '
                  'permanently deleted. Are you sure that you want to continue?')
            )
            response = choice_dialog.run()

        # if no request is needed or the user has told us they definitely want to delete the book
        if not request_response or (request_response and response == Gtk.ResponseType.YES):

            # get the array of currently selected books in the book window
            selected_books = self._iconview.get_selected_items()
            book_ids = [ self.get_book_at_path(book) for book in selected_books ]
            paths = [ self._library.backend.get_book_path(book_id) for book_id in book_ids ]

            # Remove books from library
            self._remove_books_from_library()

            # Remove from the harddisk
            for book_path in paths:
                try:
                    # try to delete the book.
                    # this can throw an exception if the path points to folder instead
                    # of a single file
                    os.remove(book_path)
                except Exception:
                    log.error(_('! Could not remove file "%s"'), book_path)

    def _copy_selected(self, *args):
        """ Copies the currently selected item to clipboard. """
        paths = self._iconview.get_selected_items()
        if len(paths) == 1:
            model = self._iconview.get_model()
            iter = model.get_iter(paths[0])
            path = model.get_value(iter, 2).decode('utf-8')
            pixbuf = model.get_value(iter, 0)

            self._library._window.clipboard.copy(path, pixbuf)

    def _button_press(self, iconview, event):
        """Handle mouse button presses on the _BookArea."""
        path = iconview.get_path_at_pos(int(event.x), int(event.y))

        if event.button == 3:
            if path and not iconview.path_is_selected(path):
                iconview.unselect_all()
                iconview.select_path(path)

            self._popup_book_menu()

    def _popup_book_menu(self):
        """ Shows the book panel popup menu. """

        selected = self._iconview.get_selected_items()
        books_selected = len(selected) > 0
        collection = self._library.collection_area.get_current_collection()
        is_collection_all = collection == _COLLECTION_ALL

        for action in ('open', 'open keep library', 'remove from library', 'completely remove'):
            self._set_sensitive(action, books_selected)

        self._set_sensitive('_title', False)
        self._set_sensitive('add', collection is not None)
        self._set_sensitive('remove from collection', books_selected and not is_collection_all)
        self._set_sensitive('copy to clipboard', len(selected) == 1)

        menu = self._ui_manager.get_widget('/library books')
        menu.popup(None, None, None, None, 3, Gtk.get_current_event_time())

    def _set_sensitive(self, action, sensitive):
        """ Enables the popup menu action  based on . """

        control = self._ui_manager.get_action('/library books/' + action)
        control.set_sensitive(sensitive)

    def _key_press(self, iconview, event):
        """Handle key presses on the _BookArea."""
        if event.keyval == Gdk.KEY_Delete:
            self._remove_books_from_collection()

    def _popup_menu(self, iconview):
        """ Called when the menu key is pressed to open the popup menu. """
        self._popup_book_menu()
        return True

    def _drag_begin(self, iconview, context):
        """Create a cursor image for drag-n-drop from the library.

        This method relies on implementation details regarding PIL's
        drawing functions and default font to produce good looking results.
        If those are changed in a future release of PIL, this method might
        produce bad looking output (e.g. non-centered text).

        It's also used with connect_after() to overwrite the cursor
        automatically created when using enable_model_drag_source(), so in
        essence it's a hack, but at least it works.
        """
        icon_path = iconview.get_cursor()[1]
        num_books = len(iconview.get_selected_items())
        book = self.get_book_at_path(icon_path)

        cover: GdkPixbuf.Pixbuf = self._library.backend.get_book_cover(book)
        if cover is None:
            cover = image_tools.MISSING_IMAGE_ICON

        cover = cover.scale_simple(max(0, cover.get_width() // 2),
            max(0, cover.get_height() // 2), prefs['scaling quality'])
        cover = image_tools.add_border(cover, 1, 0xFFFFFFFF)
        cover = image_tools.add_border(cover, 1)

        if num_books > 1:
            cover_width = cover.get_width()
            cover_height = cover.get_height()
            pointer = GdkPixbuf.Pixbuf.new(colorspace=GdkPixbuf.Colorspace.RGB,
                                           has_alpha=True, bits_per_sample=8,
                                           width=max(30, cover_width + 15),
                                           height=max(30, cover_height + 10))
            pointer.fill(0x00000000)
            cover.composite(pointer, 0, 0, cover_width, cover_height, 0, 0,
            1, 1, prefs['scaling quality'], 255)
            im = Image.new('RGBA', (30, 30), 0x00000000)
            draw = ImageDraw.Draw(im)
            draw.polygon(
                (8, 0, 20, 0, 28, 8, 28, 20, 20, 28, 8, 28, 0, 20, 0, 8),
                fill=(0, 0, 0), outline=(0, 0, 0))
            draw.polygon(
                (8, 1, 20, 1, 27, 8, 27, 20, 20, 27, 8, 27, 1, 20, 1, 8),
                fill=(128, 0, 0), outline=(255, 255, 255))
            text = str(num_books)
            draw.text((15 - (6 * len(text) // 2), 9), text,
                fill=(255, 255, 255))
            circle = image_tools.pil_to_pixbuf(im)
            circle.composite(pointer, max(0, cover_width - 15),
                max(0, cover_height - 20), 30, 30, max(0, cover_width - 15),
                max(0, cover_height - 20), 1, 1, prefs['scaling quality'], 255)
        else:
            pointer = cover

        Gtk.drag_set_icon_pixbuf(context, pointer, -5, -5)

    def _drag_data_get(self, iconview: thumbnail_view.ThumbnailIconView, context: Gdk.DragContext,
                       selection: Gtk.SelectionData, info: int, time: int) -> None:
        """Fill the SelectionData with (iconview) paths for the dragged books
        formatted as a string with each path separated by a comma.
        """
        paths = iconview.get_selected_items()
        text = ','.join(path.to_string() for path in paths)
        selection.set_text(text, len(text))

    def _drag_data_received(self, widget, context, x, y, data, *args):
        """Handle drag-n-drop events ending on the book area (i.e. from
        external apps like the file manager).
        """
        uris = data.get_uris()
        if not uris:
            return

        uris = [ portability.normalize_uri(uri) for uri in uris ]
        paths = [ urllib.request.url2pathname(uri).decode('utf-8') for uri in uris ]

        collection = self._library.collection_area.get_current_collection()
        collection_name = self._library.backend.get_collection_name(collection)
        self._library.add_books(paths, collection_name)


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1695057065.0
mcomix-3.1.0/mcomix/library/collection_area.py0000644000175000017500000005216414502102251021136 0ustar00moritzmoritz"""library_collection_area.py - Comic book library window that displays the collections."""

from xml.sax.saxutils import escape as xmlescape
from gi.repository import Gdk, GLib, Gtk
from typing import TYPE_CHECKING

from mcomix.preferences import prefs
from mcomix import constants
from mcomix import i18n
from mcomix import status
from mcomix import file_chooser_library_dialog
from mcomix import message_dialog
from mcomix.i18n import _
if TYPE_CHECKING:
    from mcomix.library.main_dialog import _LibraryDialog

_dialog = None
# The "All books" collection is not a real collection stored in the library,
# but is represented by this ID in the library's TreeModels.
_COLLECTION_ALL = -1
_COLLECTION_RECENT = -2


class _CollectionArea(Gtk.ScrolledWindow):

    """The _CollectionArea is the sidebar area in the library where
    different collections are displayed in a tree.
    """

    def __init__(self, library: "_LibraryDialog"):
        super(_CollectionArea, self).__init__()
        self._library = library
        self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)

        self._treestore = Gtk.TreeStore.new([str, int])  # (Name, ID) of collections.
        self._treeview = Gtk.TreeView.new_with_model(self._treestore)
        self._treeview.connect('cursor_changed', self._collection_selected)
        self._treeview.connect('drag_data_received', self._drag_data_received)
        self._treeview.connect('drag_motion', self._drag_motion)
        self._treeview.connect_after('drag_begin', self._drag_begin)
        self._treeview.connect('button_press_event', self._button_press)
        self._treeview.connect('key_press_event', self._key_press)
        self._treeview.connect('popup_menu', self._popup_menu)
        self._treeview.connect('row_activated', self._expand_or_collapse_row)
        self._treeview.set_headers_visible(False)
        self._treeview.set_rules_hint(True)
        self._set_acceptable_drop(True)
        self._treeview.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
            [('collection', Gtk.TargetFlags.SAME_WIDGET, constants.LIBRARY_DRAG_COLLECTION_ID)],
            Gdk.DragAction.MOVE)

        cellrenderer = Gtk.CellRendererText()
        column = Gtk.TreeViewColumn(None, cellrenderer, markup=0)
        self._treeview.append_column(column)
        self.add(self._treeview)

        self._ui_manager = Gtk.UIManager()
        self._tooltipstatus = status.TooltipStatusHelper(self._ui_manager,
            self._library.get_status_bar())
        ui_description = """
        
            
                
                
                
                
                
                
                
                
                
                
            
        
        """
        self._ui_manager.add_ui_from_string(ui_description)
        actiongroup = Gtk.ActionGroup('mcomix-library-collection-area')
        actiongroup.add_actions([
            ('_title', None, _("Library collections"), None, None,
                lambda *args: False),
            ('add', Gtk.STOCK_ADD, _('_Add...'), None,
                _('Add more books to the library.'),
                lambda *args: file_chooser_library_dialog.open_library_filechooser_dialog(self._library)),
            ('new', Gtk.STOCK_NEW, _('New'), None,
                _('Add a new empty collection.'),
                self.add_collection),
            ('rename', Gtk.STOCK_EDIT, _('Re_name'), None,
                _('Renames the selected collection.'),
                self._rename_collection),
            ('duplicate', Gtk.STOCK_COPY, _('_Duplicate'), None,
                _('Creates a duplicate of the selected collection.'),
                self._duplicate_collection),
            ('cleanup', Gtk.STOCK_CLEAR, _('_Clean up'), None,
                _('Removes no longer existant books from the collection.'),
                self._clean_collection),
            ('remove', Gtk.STOCK_REMOVE, _('_Remove'), None,
                _('Deletes the selected collection.'),
                self._remove_collection)])
        self._ui_manager.insert_action_group(actiongroup, 0)

        self.display_collections()

    def get_current_collection(self):
        """Return the collection ID for the currently selected collection,
        or None if no collection is selected.
        """
        treepath, focuspath = self._treeview.get_cursor()
        if treepath is not None:
            return self._get_collection_at_path(treepath)
        else:
            return None

    def display_collections(self):
        """Display the library collections by redrawing them from the
        backend data. Should be called on startup or when the collections
        hierarchy has been changed (e.g. after moving, adding, renaming).
        Any row that was expanded before the call will have it's
        corresponding new row also expanded after the call.
        """

        def _recursive_add(parent_iter, supercoll):
            for coll in self._library.backend.get_collections_in_collection(
              supercoll):
                name = self._library.backend.get_collection_name(coll)
                child_iter = self._treestore.append(parent_iter,
                    [xmlescape(name), coll])
                _recursive_add(child_iter, coll)

        def _expand_and_select(treestore, path, iterator):
            collection = treestore.get_value(iterator, 1)
            if collection == prefs['last library collection']:
                # Reset to trigger update of book area.
                prefs['last library collection'] = None
                self._treeview.expand_to_path(path)
                self._treeview.set_cursor(path)
            elif collection in expanded_collections:
                self._treeview.expand_to_path(path)

        def _expanded_rows_accumulator(treeview, path):
            collection = self._get_collection_at_path(path)
            expanded_collections.append(collection)

        expanded_collections = []
        self._treeview.map_expanded_rows(_expanded_rows_accumulator)
        self._treestore.clear()
        self._treestore.append(None, ['%s' % xmlescape(_('All books')),
            _COLLECTION_ALL])
        _recursive_add(None, None)
        self._treestore.foreach(_expand_and_select)

    def add_collection(self, *args):
        """Add a new collection to the library, through a dialog."""
        add_dialog = message_dialog.MessageDialog(self._library, 0, Gtk.MessageType.INFO,
            Gtk.ButtonsType.OK_CANCEL)
        add_dialog.set_auto_destroy(False)
        add_dialog.set_default_response(Gtk.ResponseType.OK)
        add_dialog.set_text(
            _('Add new collection?'),
            _('Please enter a name for the new collection.')
        )

        box = Gtk.HBox() # To get nice line-ups with the padding.
        add_dialog.vbox.pack_start(box, True, True, 0)
        entry = Gtk.Entry()
        entry.set_activates_default(True)
        box.pack_start(entry, True, True, 6)
        box.show_all()

        response = add_dialog.run()
        name = entry.get_text()
        add_dialog.destroy()
        if response == Gtk.ResponseType.OK and name:
            if self._library.backend.add_collection(name):
                collection = self._library.backend.get_collection_by_name(name)
                prefs['last library collection'] = collection.id
                self._library.collection_area.display_collections()
            else:
                message = _("Could not add a new collection called '%s'.") % (
                    name)
                if (self._library.backend.get_collection_by_name(name)
                  is not None):
                    message = '%s %s' % (message,
                        _('A collection by that name already exists.'))
                self._library.set_status_message(message)

    def clean_collection(self, collection):
        """ Check all books in the collection, removing those that
        no longer exist. If C{collection} is None, the whole library
        will be cleaned. """

        removed = self._library.backend.clean_collection(collection)

        msg = i18n.get_translation().ngettext(
            'Removed %d book from the library.',
            'Removed %d books from the library.',
            removed)
        self._library.set_status_message(msg % removed)

        if removed > 0:
            collection = self._library.collection_area.get_current_collection()
            GLib.idle_add(self._library.book_area.display_covers, collection)

    def _get_collection_at_path(self, path):
        """Return the collection ID of the collection at the (TreeView)
        .
        """
        iterator = self._treestore.get_iter(path)
        return self._treestore.get_value(iterator, 1)

    def _collection_selected(self, treeview):
        """Change the viewed collection (in the _BookArea) to the
        currently selected one in the sidebar, if it has been changed.
        """
        collection = self.get_current_collection()
        if (collection is None or
          collection == prefs['last library collection']):
            return
        prefs['last library collection'] = collection
        GLib.idle_add(self._library.book_area.display_covers, collection)

    def _clean_collection(self, *args):
        """ Menu item hook to clean a collection. """

        collection = self.get_current_collection()

        # The backend expects _COLLECTION_ALL to be passed as None
        if collection == _COLLECTION_ALL:
            collection = None

        self.clean_collection(collection)

    def _remove_collection(self, action=None):
        """Remove the currently selected collection from the library."""
        collection = self.get_current_collection()

        if collection not in (_COLLECTION_ALL, _COLLECTION_RECENT):
            self._library.backend.remove_collection(collection)
            prefs['last library collection'] = _COLLECTION_ALL
            self.display_collections()

    def _rename_collection(self, action):
        """Rename the currently selected collection, using a dialog."""
        collection = self.get_current_collection()
        try:
            old_name = self._library.backend.get_collection_name(collection)
        except Exception:
            return
        rename_dialog = message_dialog.MessageDialog(self._library, 0,
            Gtk.MessageType.INFO, Gtk.ButtonsType.OK_CANCEL)
        rename_dialog.set_auto_destroy(False)
        rename_dialog.set_text(
            _('Rename collection?'),
            _('Please enter a new name for the selected collection.')
        )
        rename_dialog.set_default_response(Gtk.ResponseType.OK)

        box = Gtk.HBox() # To get nice line-ups with the padding.
        rename_dialog.vbox.pack_start(box, True, True, 0)
        entry = Gtk.Entry()
        entry.set_text(old_name)
        entry.set_activates_default(True)
        box.pack_start(entry, True, True, 6)
        box.show_all()

        response = rename_dialog.run()
        new_name = entry.get_text()
        rename_dialog.destroy()
        if response == Gtk.ResponseType.OK and new_name:
            if self._library.backend.rename_collection(collection, new_name):
                self.display_collections()
            else:
                message = _("Could not change the name to '%s'.") % new_name
                if (self._library.backend.get_collection_by_name(new_name)
                  is not None):
                    message = '%s %s' % (message,
                        _('A collection by that name already exists.'))
                self._library.set_status_message(message)

    def _duplicate_collection(self, action):
        """Duplicate the currently selected collection."""
        collection = self.get_current_collection()
        if self._library.backend.duplicate_collection(collection):
            self.display_collections()
        else:
            self._library.set_status_message(
                _('Could not duplicate collection.'))

    def _button_press(self, treeview, event):
        """Handle mouse button presses on the _CollectionArea."""

        if event.button == 3:
            row = treeview.get_path_at_pos(int(event.x), int(event.y))
            if row:
                path, column, x, y = row
                collection = self._get_collection_at_path(path)
            else:
                collection = None

            self._popup_collection_menu(collection)

    def _popup_menu(self, treeview):
        """ Called to open the control's popup menu via
        keyboard controls. """

        model, iter = treeview.get_selection().get_selected()
        if iter is not None:
            book_path = model.get_path(iter)[0]
            collection = self._get_collection_at_path(book_path)
        else:
            collection = None

        self._popup_collection_menu(collection)
        return True

    def _popup_collection_menu(self, collection):
        """ Show the library collection popup. Depending on the
        value of C{collection}, menu items will be disabled or enabled. """

        is_collection_all = collection in (_COLLECTION_ALL, _COLLECTION_RECENT)

        for path in ('rename', 'duplicate', 'remove'):
            control = self._ui_manager.get_action(
                    '/library collections/' + path)
            control.set_sensitive(collection is not None and
                    not is_collection_all)

        self._ui_manager.get_action('/library collections/add').set_sensitive(collection is not None)
        self._ui_manager.get_action('/library collections/cleanup').set_sensitive(collection is not None)
        self._ui_manager.get_action('/library collections/_title').set_sensitive(False)

        menu = self._ui_manager.get_widget('/library collections')
        menu.popup(None, None, None, None, 3, Gtk.get_current_event_time())

    def _key_press(self, treeview, event):
        """Handle key presses on the _CollectionArea."""
        if event.keyval == Gdk.KEY_Delete:
            self._remove_collection()

    def _expand_or_collapse_row(self, treeview, path, column):
        """Expand or collapse the activated row."""
        if treeview.row_expanded(path):
            treeview.collapse_row(path)
        else:
            treeview.expand_to_path(path)

    def _drag_data_received(self, treeview: Gtk.TreeView, context: Gdk.DragContext, x: int, y: int,
                            selection: Gtk.SelectionData, drag_id: int, eventtime: int) -> None:
        """Move books dragged from the _BookArea to the target collection,
        or move some collection into another collection.
        """
        self._library.set_status_message('')
        drop_row = treeview.get_dest_row_at_pos(x, y)
        if drop_row is None:  # Drop "after" the last row.
            dest_path, pos = ((len(self._treestore) - 1,),
                Gtk.TreeViewDropPosition.AFTER)
        else:
            dest_path, pos = drop_row
        src_collection = self.get_current_collection()
        dest_collection = self._get_collection_at_path(dest_path)
        if drag_id == constants.LIBRARY_DRAG_COLLECTION_ID:
            if pos in (Gtk.TreeViewDropPosition.BEFORE, Gtk.TreeViewDropPosition.AFTER):
                dest_collection = self._library.backend.get_supercollection(
                    dest_collection)
            self._library.backend.add_collection_to_collection(
                src_collection, dest_collection)
            self.display_collections()
        elif drag_id == constants.LIBRARY_DRAG_BOOK_ID:
            for path_str in selection.get_text().split(','): # IconView path
                book = self._library.book_area.get_book_at_path(int(path_str))
                self._library.backend.add_book_to_collection(book,
                    dest_collection)
                if src_collection != _COLLECTION_ALL:
                    self._library.backend.remove_book_from_collection(book,
                        src_collection)
                    self._library.book_area.remove_book_at_path(int(path_str))

    def _drag_motion(self, treeview: Gtk.TreeView, context: Gdk.DragContext, x: int, y: int, time: int) -> None:
        """Set the library statusbar text when hovering a drag-n-drop over
        a collection (either books or from the collection area itself).
        Also set the TreeView to accept drops only when we are hovering over
        a valid drop position for the current drop type.

        This isn't pretty, but the details of treeviews and drag-n-drops
        are not pretty to begin with.
        """
        drop_row = treeview.get_dest_row_at_pos(x, y)
        src_collection = self.get_current_collection()
        # Why isn't the drag ID passed along with drag-motion events?
        if Gtk.drag_get_source_widget(context) is self._treeview:  # Moving collection.
            model, src_iter = treeview.get_selection().get_selected()
            if drop_row is None:  # Drop "after" the last row.
                dest_path, pos = (len(model) - 1,), Gtk.TreeViewDropPosition.AFTER
            else:
                dest_path, pos = drop_row
            dest_iter = model.get_iter(dest_path)
            if model.is_ancestor(src_iter, dest_iter):  # No cycles!
                self._set_acceptable_drop(False)
                self._library.set_status_message('')
                return
            dest_collection = self._get_collection_at_path(dest_path)
            if pos in (Gtk.TreeViewDropPosition.BEFORE, Gtk.TreeViewDropPosition.AFTER):
                dest_collection = self._library.backend.get_supercollection(
                    dest_collection)
            if (_COLLECTION_ALL in (src_collection, dest_collection) or
                _COLLECTION_RECENT in (src_collection, dest_collection) or
                src_collection == dest_collection):
                self._set_acceptable_drop(False)
                self._library.set_status_message('')
                return
            src_name = self._library.backend.get_collection_name(
                src_collection)
            if dest_collection is None:
                dest_name = _('Root')
            else:
                dest_name = self._library.backend.get_collection_name(
                    dest_collection)
            message = (_("Put the collection '%(subcollection)s' in the collection '%(supercollection)s'.") %
                       {'subcollection': src_name, 'supercollection': dest_name})
        else:  # Moving book(s).
            if drop_row is None:
                self._set_acceptable_drop(False)
                self._library.set_status_message('')
                return
            dest_path, pos = drop_row
            if pos in (Gtk.TreeViewDropPosition.BEFORE, Gtk.TreeViewDropPosition.AFTER):
                self._set_acceptable_drop(False)
                self._library.set_status_message('')
                return
            dest_collection = self._get_collection_at_path(dest_path)
            if src_collection == dest_collection or dest_collection == _COLLECTION_ALL:
                self._set_acceptable_drop(False)
                self._library.set_status_message('')
                return
            dest_name = self._library.backend.get_collection_name(
                dest_collection)
            if src_collection == _COLLECTION_ALL:
                message = _("Add books to '%s'.") % dest_name
            else:
                src_name = self._library.backend.get_collection_name(
                    src_collection)
                message = (_("Move books from '%(source collection)s' to '%(destination collection)s'.") %
                    {'source collection': src_name,
                    'destination collection': dest_name})
        self._set_acceptable_drop(True)
        self._library.set_status_message(message)
        Gdk.drag_status(context, Gdk.DragAction.MOVE, time)
        return

    def _set_acceptable_drop(self, acceptable: bool) -> None:
        """Set the TreeView to accept drops if  is True."""
        if acceptable:
            self._treeview.enable_model_drag_dest(
                [Gtk.TargetEntry.new('text/plain', Gtk.TargetFlags.SAME_APP, constants.LIBRARY_DRAG_BOOK_ID),
                 Gtk.TargetEntry.new('collection', Gtk.TargetFlags.SAME_WIDGET, constants.LIBRARY_DRAG_COLLECTION_ID)],
                 Gdk.DragAction.MOVE)
        else:
            self._treeview.enable_model_drag_dest([], Gdk.DragAction.MOVE)

    def _drag_begin(self, treeview: Gtk.TreeView, context: Gdk.DragContext) -> None:
        """Create a cursor image for drag-n-drop of collections. We use the
        default one (i.e. the row with text), but put the hotspot in the
        top left corner so that one can actually see where one is dropping,
        which unfortunately isn't the default case.
        """
        path = treeview.get_cursor()[0]
        surface = treeview.create_row_drag_icon(path)
        image_surface = surface.map_to_image(None)
        width, height = image_surface.get_width(), image_surface.get_height()
        pixbuf = Gdk.pixbuf_get_from_surface(image_surface, 0, 0, width, height)
        surface.unmap_image(image_surface)
        Gtk.drag_set_icon_pixbuf(context, pixbuf, -5, -5)

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/library/control_area.py0000644000175000017500000001464014476523373020506 0ustar00moritzmoritz"""library_control_area.py - The window in the library that contains buttons
and displays info."""

import os
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Pango

from mcomix import i18n
from mcomix import labels
from mcomix.library.watchlist import WatchListDialog
from mcomix.i18n import _

# The "All books" collection is not a real collection stored in the library,
# but is represented by this ID in the library's TreeModels.
_COLLECTION_ALL = -1


class _ControlArea(Gtk.HBox):

    """The _ControlArea is the bottom area of the library window where
    information is displayed and controls such as buttons reside.
    """

    def __init__(self, library):
        super(_ControlArea, self).__init__(False, 12)

        self._library = library
        self.set_border_width(10)

        borderbox = Gtk.Frame()
        borderbox.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        borderbox.set_size_request(350, -1)

        insidebox = Gtk.EventBox()
        insidebox.set_border_width(1)
        insidebox.set_state(Gtk.StateType.ACTIVE)

        infobox = Gtk.VBox(False, 5)
        infobox.set_border_width(10)
        self.pack_start(borderbox, True, True, 0)
        borderbox.add(insidebox)
        insidebox.add(infobox)

        self._namelabel = labels.BoldLabel()
        self._namelabel.set_alignment(0, 0.5)
        self._namelabel.set_selectable(True)
        self._namelabel.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
        infobox.pack_start(self._namelabel, False, False, 0)

        self._filelabel = Gtk.Label()
        self._filelabel.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
        self._filelabel.set_alignment(0, 0.5)
        infobox.pack_start(self._filelabel, False, False, 0)

        self._dirlabel = Gtk.Label()
        self._dirlabel.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
        self._dirlabel.set_alignment(0, 0.5)
        self._dirlabel.set_selectable(True)
        infobox.pack_start(self._dirlabel, False, False, 0)

        vbox = Gtk.VBox(False, 10)
        vbox.set_size_request(350, -1)
        self.pack_start(vbox, False, False, 0)

        # First line of controls, containing the search box
        hbox = Gtk.HBox(False)
        vbox.pack_start(hbox, True, True, 0)

        label = Gtk.Label(label=_('_Search:'))
        label.set_use_underline(True)
        hbox.pack_start(label, False, False, 0)
        search_entry = Gtk.Entry()
        search_entry.connect('activate', self._filter_books)
        search_entry.set_tooltip_text(
            _('Display only those books that have the specified text string '
              'in their full path. The search is not case sensitive.'))
        hbox.pack_start(search_entry, True, True, 6)
        label.set_mnemonic_widget(search_entry)

        # Last line of controls, containing buttons like 'Open'
        hbox = Gtk.HBox(False, 10)
        vbox.pack_end(hbox, True, True, 0)

        watchlist_button = Gtk.Button(label=_("_Watch list"), use_underline=True)
        watchlist_button.set_always_show_image(True)
        watchlist_button.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_FIND, Gtk.IconSize.BUTTON))
        watchlist_button.set_image_position(Gtk.PositionType.LEFT)
        watchlist_button.connect('clicked',
            lambda *args: WatchListDialog(self._library))
        watchlist_button.set_tooltip_text(
            _('Open the watchlist management dialog.'))
        hbox.pack_start(watchlist_button, True, True, 0)

        self._open_button = Gtk.Button(label=_("_Open list"), use_underline=True)
        self._open_button.set_always_show_image(True)
        self._open_button.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_OPEN, Gtk.IconSize.BUTTON))
        self._open_button.set_image_position(Gtk.PositionType.LEFT)
        self._open_button.connect('clicked',
            self._library.book_area.open_selected_book)
        self._open_button.set_tooltip_text(_('Open the selected book.'))
        self._open_button.set_sensitive(False)
        hbox.pack_end(self._open_button, True, True, 0)

    def update_info(self, selected):
        """Update the info box using the currently  books from
        the _BookArea.
        """

        if selected:
            book_id = self._library.book_area.get_book_at_path(selected[0])
            book = self._library.backend.get_book_by_id(book_id)
        else:
            book = None

        if book:
            name = book.name
            dir_path = os.path.dirname(book.path)
            pages = book.pages
            size = book.size
            last_page = book.get_last_read_page()
            last_date = book.get_last_read_date()
        else:
            name = dir_path = pages = size = last_page = last_date = None

        if len(selected) > 0:
            self._open_button.set_sensitive(True)
        else:
            self._open_button.set_sensitive(False)

        if name is not None:
            self._namelabel.set_text(i18n.to_unicode(name))
            self._namelabel.set_tooltip_text(i18n.to_unicode(name))
        else:
            self._namelabel.set_text('')
            self._namelabel.set_has_tooltip(False)

        infotext = []

        if last_page is not None and pages is not None and last_page != pages:
            infotext.append('%s %d/%d' % (_('Page'), last_page, pages))
        elif pages is not None:
            infotext.append(_('%d pages') % pages)

        if size is not None:
            infotext.append('%.1f MiB' % (size / 1048576.0))

        if (pages is not None and last_page is not None and
            last_date is not None and last_page == pages):
            infotext.append(_('Finished reading on %(date)s, %(time)s') % {
                'date': last_date.strftime('%x'),
                'time': last_date.strftime('%X') })

        self._filelabel.set_text(', '.join(infotext))

        if dir_path is not None:
            self._dirlabel.set_text(i18n.to_unicode(dir_path))
        else:
            self._dirlabel.set_text('')

    def _filter_books(self, entry, *args):
        """Display only the books in the current collection whose paths
        contain the string in the Gtk.Entry. The string is not
        case-sensitive.
        """
        self._library.filter_string = entry.get_text()
        if not self._library.filter_string:
            self._library.filter_string = None
        collection = self._library.collection_area.get_current_collection()
        GLib.idle_add(self._library.book_area.display_covers, collection)

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/library/main_dialog.py0000644000175000017500000001543114476523373020300 0ustar00moritzmoritz"""library_main_dialog.py - The library dialog window."""

import os
from gi.repository import Gdk, Gtk

from mcomix.preferences import prefs
from mcomix import i18n
from mcomix import tools
from mcomix import log
from mcomix import file_chooser_library_dialog
from mcomix import status
from mcomix.library import backend as library_backend
from mcomix.library import book_area as library_book_area
from mcomix.library import collection_area as library_collection_area
from mcomix.library import control_area as library_control_area
from mcomix.library import add_progress_dialog as library_add_progress_dialog
from mcomix.i18n import _

_dialog = None
# The "All books" collection is not a real collection stored in the library,
# but is represented by this ID in the library's TreeModels.
_COLLECTION_ALL = -1

class _LibraryDialog(Gtk.Window):

    """The library window. Automatically creates and uses a new
    library_backend.LibraryBackend when opened.
    """

    def __init__(self, window, file_handler):
        super(_LibraryDialog, self).__init__(Gtk.WindowType.TOPLEVEL)

        self._window = window

        self.resize(prefs['lib window width'], prefs['lib window height'])
        self.set_title(_('Library'))
        self.connect('delete_event', self.close)
        self.connect('key-press-event', self._key_press_event)

        self.filter_string = None
        self._file_handler = file_handler
        self._statusbar = Gtk.Statusbar()
        self.backend = library_backend.LibraryBackend()
        self.book_area = library_book_area._BookArea(self)
        self.control_area = library_control_area._ControlArea(self)
        self.collection_area = library_collection_area._CollectionArea(self)

        self.backend.watchlist.new_files_found += self._new_files_found

        table = Gtk.Table(2, 2, False)
        table.attach(self.collection_area, 0, 1, 0, 1, Gtk.AttachOptions.FILL,
            Gtk.AttachOptions.EXPAND|Gtk.AttachOptions.FILL)
        table.attach(self.book_area, 1, 2, 0, 1, Gtk.AttachOptions.EXPAND|Gtk.AttachOptions.FILL,
            Gtk.AttachOptions.EXPAND|Gtk.AttachOptions.FILL)
        table.attach(self.control_area, 0, 2, 1, 2, Gtk.AttachOptions.EXPAND|Gtk.AttachOptions.FILL,
            Gtk.AttachOptions.FILL)

        if prefs['show statusbar']:
            table.attach(self._statusbar, 0, 2, 2, 3, Gtk.AttachOptions.FILL, Gtk.AttachOptions.FILL)

        self.add(table)
        self.show_all()
        self.present()

    def open_book(self, books, keep_library_open=False):
        """Open the book with ID ."""

        paths = [ self.backend.get_book_path(book) for book in books ]

        if not keep_library_open:
            self.hide()

        self._window.present()

        if len(paths) > 1:
            self._file_handler.open_file(paths)
        elif len(paths) == 1:
            self._file_handler.open_file(paths[0])

    def scan_for_new_files(self):
        """ Start scanning for new files from the watch list. """

        if len(self.backend.watchlist.get_watchlist()) > 0:
            self.set_status_message(_("Scanning for new books..."))
            self.backend.watchlist.scan_for_new_files()

    def _new_files_found(self, filelist, watchentry):
        """ Called after the scan for new files finished. """

        if len(filelist) > 0:
            if watchentry.collection.id is not None:
                collection_name = watchentry.collection.name
            else:
                collection_name = None

            self.add_books(filelist, collection_name)

            if len(filelist) == 1:
                message = _("Added new book '%(bookname)s' "
                    "from directory '%(directory)s'.")
            else:
                message = _("Added %(count)d new books "
                    "from directory '%(directory)s'.")

            self.set_status_message(message % {'directory': watchentry.directory,
                'count': len(filelist), 'bookname': os.path.basename(filelist[0])})
        else:
            self.set_status_message(
                _("No new books found in directory '%s'.") % watchentry.directory)

    def get_status_bar(self):
        """ Returns the window's status bar. """
        return self._statusbar

    def set_status_message(self, message):
        """Set a specific message on the statusbar, replacing whatever was
        there earlier.
        """
        self._statusbar.pop(0)
        self._statusbar.push(0,
            ' ' * status.Statusbar.SPACING + '%s' % i18n.to_unicode(message))

    def close(self, *args):
        """Close the library and do required cleanup tasks."""
        prefs['lib window width'], prefs['lib window height'] = self.get_size()
        self.backend.watchlist.new_files_found -= self._new_files_found
        self.book_area.stop_update()
        self.book_area.close()
        file_chooser_library_dialog.close_library_filechooser_dialog()
        _close_dialog()

    def add_books(self, paths, collection_name=None):
        """Add the books at  to the library. If 
        is not None, it is the name of a (new or existing) collection the
        books should be put in.
        """
        if collection_name is None:
            collection_id = self.collection_area.get_current_collection()
        else:
            collection = self.backend.get_collection_by_name(collection_name)

            if collection is None: # Collection by that name doesn't exist.
                self.backend.add_collection(collection_name)
                collection = self.backend.get_collection_by_name(
                    collection_name)

            collection_id = collection.id

        library_add_progress_dialog._AddLibraryProgressDialog(self, self._window, paths, collection_id)

        if collection_id is not None:
            prefs['last library collection'] = collection_id

    def _key_press_event(self, widget, event, *args):
        """ Handle key press events for closing the library on Escape press. """

        if event.keyval == Gdk.KEY_Escape:
            self.hide()


def open_dialog(action, window):
    """ Shows the library window. If sqlite is not available, this method
    does nothing and returns False. Otherwise, True is returned. """
    global _dialog

    if _dialog is None:

        if library_backend.dbapi2 is None:
            text = _('! You need an sqlite wrapper to use the library.')
            window.osd.show(text)
            log.error(text)
            return False

        else:
            _dialog = _LibraryDialog(window, window.filehandler)

    else:
        _dialog.present()

    if prefs['scan for new books on library startup']:
        _dialog.scan_for_new_files()

    return True


def _close_dialog(*args):
    global _dialog

    if _dialog is not None:
        _dialog.destroy()
        _dialog = None
        tools.garbage_collect()

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/library/pixbuf_cache.py0000644000175000017500000000373414476523373020460 0ustar00moritzmoritz""" pixbuf_cache.py - Caches book covers for the library display."""
# -*- coding: utf-8 -*-


import threading

__all__ = ["get_pixbuf_cache"]

class _PixbufCache(object):

    """ Pixbuf cache for the library window. Instead of loading book covers
    from disk again after switching collection or using filtering, this class
    stores a pre-defined amount of pixbufs in memory, evicting older pixbufs
    as necessary.
    """

    def __init__(self, size):
        #: Cache size, in images
        assert size > 0
        self.cachesize = size
        #: Store book id => pixbuf
        self._cache = {}
        #: Ensure thread safety
        self._lock = threading.RLock()

    def add(self, id, pixbuf):
        """ Adds a cache object with  and associates it with the
        passed pixbuf. """

        with self._lock:
            if len(self._cache) > self.cachesize:
                first = list(self._cache.items())[0]
                self.invalidate(first[0])

            self._cache[id] = pixbuf

    def exists(self, id):
        """ Checks if there is an entry for the given id in the cache. """
        return id in self._cache

    def get(self, id):
        """ Returns the pixbuf for the given cache id, or None, if such
        an entry does not exist. """
        if id in self._cache:
            return self._cache[id]
        else:
            return None

    def invalidate(self, id):
        """ Invalidates the object with the specified cache ID. """
        with self._lock:
            if id in self._cache:
                del self._cache[id]

    def invalidate_all(self):
        """ Invalidates all cached objects. """
        with self._lock:
            self._cache.clear()


_cache = None

def get_pixbuf_cache():
    global _cache

    if _cache:
        return _cache
    else:
        # 500 items is about 130 MB of RAM with 500px thumbnails,
        # and about 35 MB at 250px.
        _cache = _PixbufCache(500)
        return _cache

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/library/watchlist.py0000644000175000017500000002312314476523373020034 0ustar00moritzmoritz""" Library watch list dialog and backend classes. """

import os
from gi.repository import Gtk, GLib
from gi.repository import GObject

from mcomix.library import backend_types
from mcomix.preferences import prefs
from mcomix.i18n import _


COL_DIRECTORY = 0
COL_COLLECTION = 0
COL_COLLECTION_ID = 1
COL_RECURSIVE = 2

class WatchListDialog(Gtk.Dialog):
    """ Dialog for managing watched directories. """

    RESPONSE_SCANNOW = 1000

    def __init__(self, library):
        """ Dialog constructor.
        @param library: Dialog parent window, should be library window.
        """
        super(WatchListDialog, self).__init__(_("Library watch list"),
            library, Gtk.DialogFlags.DESTROY_WITH_PARENT | Gtk.DialogFlags.MODAL,
            (_('_Scan now'), WatchListDialog.RESPONSE_SCANNOW,
             Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE))

        #: Stores a reference to the library
        self.library = library
        #: True if changes were made to the watchlist. Not 100% accurate.
        self._changed = False

        self.set_default_response(Gtk.ResponseType.CLOSE)

        # Initialize treeview control showing existing watch directories
        self._treeview = Gtk.TreeView(self._create_model())
        self._treeview.set_headers_visible(True)
        self._treeview.get_selection().connect('changed', self._item_selected_cb)

        dir_renderer = Gtk.CellRendererText()
        dir_column = Gtk.TreeViewColumn(_("Directory"), dir_renderer)
        dir_column.set_attributes(dir_renderer, text=COL_DIRECTORY)
        dir_column.set_expand(True)
        self._treeview.append_column(dir_column)

        collection_model = self._create_collection_model()
        collection_renderer = Gtk.CellRendererCombo()
        collection_renderer.set_property('model', collection_model)
        collection_renderer.set_property('text-column', COL_COLLECTION)
        collection_renderer.set_property('editable', True)
        collection_renderer.set_property('has-entry', False)
        collection_renderer.connect('changed', self._collection_changed_cb, collection_model)
        collection_column = Gtk.TreeViewColumn(_("Collection"), collection_renderer)
        collection_column.set_cell_data_func(collection_renderer,
                self._treeview_collection_id_to_name)
        self._treeview.append_column(collection_column)

        recursive_renderer = Gtk.CellRendererToggle()
        recursive_renderer.set_activatable(True)
        recursive_renderer.connect('toggled', self._recursive_changed_cb)
        recursive_column = Gtk.TreeViewColumn(_("With subdirectories"),
                recursive_renderer)
        recursive_column.add_attribute(recursive_renderer, 'active', COL_RECURSIVE)
        self._treeview.append_column(recursive_column)

        add_button = Gtk.Button(_("_Add"), Gtk.STOCK_ADD, use_underline=True)
        add_button.connect('clicked', self._add_cb)
        self._remove_button = remove_button = Gtk.Button(_("_Remove"), Gtk.STOCK_REMOVE, use_underline=True)
        remove_button.set_sensitive(False)
        remove_button.connect('clicked', self._remove_cb)

        button_box = Gtk.VBox()
        button_box.pack_start(add_button, False, True, 0)
        button_box.pack_start(remove_button, False, True, 2)

        main_box = Gtk.HBox()
        scroll_window = Gtk.ScrolledWindow()
        scroll_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scroll_window.add(self._treeview)
        main_box.pack_start(scroll_window, True, True, 2)
        main_box.pack_end(button_box, False, True, 0)
        self.vbox.pack_start(main_box, True, True, 0)

        auto_checkbox = Gtk.CheckButton(
            _('Automatically scan for new books when library is _opened'), use_underline=True)
        auto_checkbox.set_active(prefs['scan for new books on library startup'])
        auto_checkbox.connect('toggled', self._auto_scan_toggled_cb)
        self.vbox.pack_end(auto_checkbox, False, False, 5)

        self.resize(475, 350)
        self.connect('response', self._close_cb)
        self.show_all()

    def get_selected_watchlist_entry(self):
        """ Returns the selected watchlist entry, or C{None} if no
        item is selected. """
        selection = self._treeview.get_selection()

        model, iter = selection.get_selected()
        if iter is not None:
            path = str(model.get_value(iter, COL_DIRECTORY))
            return self.library.backend.watchlist.get_watchlist_entry(path)
        else:
            return None

    def get_watchlist_entry_for_treepath(self, treepath):
        """ Converts a tree path to WatchlistEntry object. """
        model = self._treeview.get_model()
        iter = model.get_iter(treepath)
        dirpath = str(model.get_value(iter, COL_DIRECTORY))
        return self.library.backend.watchlist.get_watchlist_entry(dirpath)

    def _create_model(self):
        """ Creates a model containing all watched directories. """
        # Watched directory, associated library collection ID
        model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT, GObject.TYPE_BOOLEAN)
        self._fill_model(model)
        return model

    def _fill_model(self, model):
        """ Empties the model's data and updates it from the database. """
        model.clear()
        for entry in self.library.backend.watchlist.get_watchlist():
            if entry.collection.id is None:
                id = -1
            else:
                id = entry.collection.id

            model.append((entry.directory, id, entry.recursive))

    def _create_collection_model(self):
        """ Creates a model containing all available collections. """
        # Collection ID, collection name
        model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT)

        ids = self.library.backend.get_all_collections()
        model.append((backend_types.DefaultCollection.name, -1))
        for id in ids:
            model.append((self.library.backend.get_collection_name(id), id))

        return model

    def _collection_changed_cb(self, column, path,
                               collection_iter, collection_model, *args):
        """ A new collection was set for a watched directory. """
        # Get new collection ID from collection model
        new_id = collection_model.get_value(collection_iter, COL_COLLECTION_ID)
        collection = self.library.backend.get_collection_by_id(new_id)

        # Update database
        self.get_watchlist_entry_for_treepath(path).set_collection(collection)

        # Update collection ID in watchlist model
        model = self._treeview.get_model()
        iter = model.get_iter(path)
        # Editing the model in the CellRendererCombo callback stops the editing
        # operation, causing GTK warnings. Delay until callback is finished.
        GLib.idle_add(model.set_value, iter, COL_COLLECTION_ID, new_id)

        self._changed = True

    def _recursive_changed_cb(self, toggle_renderer, path, *args):
        """ Recursive reading was enabled or disabled. """
        status = not toggle_renderer.get_active()
        self.get_watchlist_entry_for_treepath(path).set_recursive(status)

        # Update recursive status in watchlist model
        model = self._treeview.get_model()
        iter = model.get_iter(path)
        model.set_value(iter, COL_RECURSIVE, status)

        self._changed = True

    def _add_cb(self, button, *args):
        """ Called when a new watch list entry should be added. """
        filechooser = Gtk.FileChooserDialog(parent=self,
            action=Gtk.FileChooserAction.SELECT_FOLDER,
            buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT,
                     Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT))
        result = filechooser.run()
        if filechooser.get_filename() is not None:
            directory = filechooser.get_filename()
        else:
            directory = ""
        filechooser.destroy()

        if result == Gtk.ResponseType.ACCEPT \
            and os.path.isdir(directory):

            self.library.backend.watchlist.add_directory(directory)
            self._fill_model(self._treeview.get_model())

            self._changed = True

    def _remove_cb(self, button, *args):
        """ Called when a watch list entry should be removed. """
        entry = self.get_selected_watchlist_entry()
        if entry:
            entry.remove()

            # Remove selection from list
            selection = self._treeview.get_selection()
            model, iter = selection.get_selected()
            model.remove(iter)

    def _item_selected_cb(self, selection, *args):
        """ Called when an item is selected. Enables or disables the "Remove"
        button. """
        self._remove_button.set_sensitive(selection.count_selected_rows() > 0)

    def _auto_scan_toggled_cb(self, checkbox, *args):
        """ Toggles automatic library book scanning. """
        prefs['scan for new books on library startup'] = checkbox.get_active()

    def _treeview_collection_id_to_name(self, column, cell, model, iter, *args):
        """ Maps a collection ID to the corresponding collection name. """
        id = model.get_value(iter, COL_COLLECTION_ID)
        if id != -1:
            text = self.library.backend.get_collection_name(id)
        else:
            text = backend_types.DefaultCollection.name

        cell.set_property("text", text)

    def _close_cb(self, dialog, response, *args):
        """ Trigger scan for new files after watch dialog closes. """
        self.destroy()
        if response == Gtk.ResponseType.CLOSE and self._changed:
            self.library.scan_for_new_files()
        elif response == WatchListDialog.RESPONSE_SCANNOW:
            self.library.scan_for_new_files()


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/log.py0000644000175000017500000000172414476523373015152 0ustar00moritzmoritz# -*- coding: utf-8 -*-

""" Logging module for MComix. Provides a logger 'mcomix' with a few
pre-configured settings. Functions in this module are redirected to
this default logger. """

import sys
import logging
from logging import DEBUG, INFO, WARNING, ERROR


__all__ = [
    'debug', 'info', 'warning', 'error',
    'DEBUG', 'INFO', 'WARNING', 'ERROR',
    'getLevel', 'setLevel',
]

# Set up default logger.
__logger = logging.getLogger('mcomix')
__logger.setLevel(WARNING)
if not __logger.handlers:
    __handler = logging.StreamHandler(sys.stdout)
    __handler.setFormatter(logging.Formatter(
        '%(asctime)s [%(threadName)s] %(levelname)s: %(message)s',
        '%H:%M:%S'))
    __logger.handlers = [__handler]


def getLevel():
    return __logger.level


# The following functions direct all input to __logger.
debug = __logger.debug
info = __logger.info
warning = __logger.warning
error = __logger.error
setLevel = __logger.setLevel


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704114443.0
mcomix-3.1.0/mcomix/main.py0000644000175000017500000014052514544534413015311 0ustar00moritzmoritz"""main.py - Main window."""

import sys
import math
import os
import shutil
import threading

from gi.repository import GObject, Gdk, Gtk, GLib

from mcomix import constants
from mcomix import cursor_handler
from mcomix import i18n
from mcomix import icons
from mcomix import enhance_backend
from mcomix import event
from mcomix import file_handler
from mcomix import image_handler
from mcomix import image_tools
from mcomix import lens
from mcomix import preferences
from mcomix.preferences import prefs
from mcomix import ui
from mcomix import slideshow
from mcomix import status
from mcomix import thumbbar
from mcomix import clipboard
from mcomix import pageselect
from mcomix import osd
from mcomix import keybindings
from mcomix import zoom
from mcomix import bookmark_backend
from mcomix import message_dialog
from mcomix import callback
from mcomix.library import backend, main_dialog
from mcomix import tools
from mcomix import box
from mcomix import layout
from mcomix import log
from mcomix.transform import Matrix, Transform
from mcomix.i18n import _

from typing import List


class MainWindow(Gtk.Window):

    """The main window, is created at start and terminates the
    program when closed.
    """

    def __init__(self, fullscreen=False, is_slideshow=slideshow,
            show_library=False, manga_mode=False, double_page=False,
            zoom_mode=None, open_path=None, open_page=1):
        super(MainWindow, self).__init__(Gtk.WindowType.TOPLEVEL)

        # ----------------------------------------------------------------
        # Attributes
        # ----------------------------------------------------------------
        # Used to detect window fullscreen state transitions.
        self.was_fullscreen = False
        self.is_manga_mode = False
        self.previous_size = (None, None)
        self.was_out_of_focus = False
        #: Used to remember if changing to fullscreen enabled 'Hide all'
        self.hide_all_forced = False
        # Remember last scroll destination.
        self._last_scroll_destination = constants.SCROLL_TO_START

        self.layout = layout.create_dummy_layout()
        self.transforms: List[Matrix] = []
        self._spacing = prefs['space between two pages']
        self._waiting_for_redraw = False

        self._image_box = Gtk.HBox(False, 2) # XXX transitional(kept for osd.py)
        self._main_layout = Gtk.Layout()
        # Wrap main layout into an event box so
        # we  can change its background color.
        self._event_box = Gtk.EventBox()
        self._event_box.add(self._main_layout)
        self._event_handler = event.EventHandler(self)
        self._vadjust = self._main_layout.get_vadjustment()
        self._hadjust = self._main_layout.get_hadjustment()
        self._scroll = (
            Gtk.Scrollbar.new(Gtk.Orientation.HORIZONTAL, self._hadjust),
            Gtk.Scrollbar.new(Gtk.Orientation.VERTICAL, self._vadjust),
        )

        self.filehandler = file_handler.FileHandler(self)
        self.filehandler.file_closed += self._on_file_closed
        self.filehandler.file_opened += self._on_file_opened
        self.imagehandler = image_handler.ImageHandler(self)
        self.imagehandler.page_available += self._page_available
        self.thumbnailsidebar = thumbbar.ThumbnailSidebar(self)

        self.statusbar = status.Statusbar()
        self.clipboard = clipboard.Clipboard(self)
        self.slideshow = slideshow.Slideshow(self)
        self.cursor_handler = cursor_handler.CursorHandler(self)
        self.enhancer = enhance_backend.ImageEnhancer(self)
        self.lens = lens.MagnifyingLens(self)
        self.osd = osd.OnScreenDisplay(self)
        self.zoom = zoom.ZoomModel()
        self.uimanager = ui.MainUI(self)
        self.menubar = self.uimanager.get_widget('/Menu')
        self.toolbar = self.uimanager.get_widget('/Tool')
        self.popup = self.uimanager.get_widget('/Popup')
        self.actiongroup = self.uimanager.get_action_groups()[0]

        self.images = [Gtk.Image(), Gtk.Image()] # XXX limited to at most 2 pages

        # ----------------------------------------------------------------
        # Setup
        # ----------------------------------------------------------------
        self.set_title(constants.APPNAME)
        self.set_size_request(300, 300)  # Avoid making the window *too* small

        # Hook up keyboard shortcuts
        self._event_handler.register_key_events()

        # This is a hack to get the focus away from the toolbar so that
        # we don't activate it with space or some other key (alternative?)
        self.toolbar.set_focus_child(
            self.uimanager.get_widget('/Tool/expander'))
        self.toolbar.set_style(Gtk.ToolbarStyle.ICONS)
        self.toolbar.set_icon_size(Gtk.IconSize.LARGE_TOOLBAR)

        for img in self.images:
            self._main_layout.put(img, 0, 0)
        self.set_bg_colour(prefs['bg colour'])

        self._vadjust.step_increment = 15
        self._vadjust.page_increment = 1
        self._hadjust.step_increment = 15
        self._hadjust.page_increment = 1

        table = Gtk.Table(2, 2, False)
        table.attach(self.thumbnailsidebar, 0, 1, 2, 5, Gtk.AttachOptions.FILL,
            Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND, 0, 0)

        table.attach(self._event_box, 1, 2, 2, 3, Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND,
            Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND, 0, 0)
        table.attach(self._scroll[constants.PageAxis.HEIGHT], 2, 3, 2, 3, Gtk.AttachOptions.FILL|Gtk.AttachOptions.SHRINK,
            Gtk.AttachOptions.FILL|Gtk.AttachOptions.SHRINK, 0, 0)
        table.attach(self._scroll[constants.PageAxis.WIDTH], 1, 2, 4, 5, Gtk.AttachOptions.FILL|Gtk.AttachOptions.SHRINK,
            Gtk.AttachOptions.FILL, 0, 0)
        table.attach(self.menubar, 0, 3, 0, 1, Gtk.AttachOptions.FILL|Gtk.AttachOptions.SHRINK,
            Gtk.AttachOptions.FILL, 0, 0)
        table.attach(self.toolbar, 0, 3, 1, 2, Gtk.AttachOptions.FILL|Gtk.AttachOptions.SHRINK,
            Gtk.AttachOptions.FILL, 0, 0)
        table.attach(self.statusbar, 0, 3, 5, 6, Gtk.AttachOptions.FILL|Gtk.AttachOptions.SHRINK,
            Gtk.AttachOptions.FILL, 0, 0)

        if prefs['default double page'] or double_page:
            self.actiongroup.get_action('double_page').activate()

        if prefs['default manga mode'] or manga_mode:
            self.actiongroup.get_action('manga_mode').activate()

        # Determine zoom mode. If zoom_mode is passed, it overrides
        # the zoom mode preference.
        zoom_actions = { constants.ZoomMode.BEST: 'best_fit_mode',
                constants.ZoomMode.WIDTH: 'fit_width_mode',
                constants.ZoomMode.HEIGHT: 'fit_height_mode',
                constants.ZoomMode.SIZE: 'fit_size_mode',
                constants.ZoomMode.MANUAL: 'fit_manual_mode' }

        if zoom_mode is not None:
            zoom_action = zoom_actions[zoom_mode]
        else:
            zoom_action = zoom_actions[prefs['zoom mode']]

        if zoom_action == 'fit_manual_mode':
            # This little ugly hack is to get the activate call on
            # 'fit_manual_mode' to actually create an event (and callback).
            # Since manual mode is the default selected radio button action
            # it won't send an event if we activate it when it is already
            # the selected one.
            self.actiongroup.get_action('best_fit_mode').activate()

        self.actiongroup.get_action(zoom_action).activate()

        if prefs['stretch']:
            self.actiongroup.get_action('stretch').activate()

        if prefs['invert smart scroll']:
            self.actiongroup.get_action('invert_scroll').activate()

        if prefs['keep transformation']:
            prefs['keep transformation'] = False
            self.actiongroup.get_action('keep_transformation').activate()
        else:
            prefs['rotation'] = 0
            prefs['vertical flip'] = False
            prefs['horizontal flip'] = False

        # List of "toggles" than can be shown/hidden by the user.
        self._toggle_list = (
            # Preference        Action        Widget(s)
            ('show menubar'   , 'menubar'   , (self.menubar,)         ),
            ('show scrollbar' , 'scrollbar' , self._scroll            ),
            ('show statusbar' , 'statusbar' , (self.statusbar,)       ),
            ('show thumbnails', 'thumbnails', (self.thumbnailsidebar,)),
            ('show toolbar'   , 'toolbar'   , (self.toolbar,)         ),
        )

        # Each "toggle" widget "eats" part of the main layout visible area.
        self._toggle_axis = {
            self.thumbnailsidebar              : constants.PageAxis.WIDTH,
            self._scroll[constants.PageAxis.HEIGHT]: constants.PageAxis.WIDTH,
            self._scroll[constants.PageAxis.WIDTH] : constants.PageAxis.HEIGHT,
            self.statusbar                     : constants.PageAxis.HEIGHT,
            self.toolbar                       : constants.PageAxis.HEIGHT,
            self.menubar                       : constants.PageAxis.HEIGHT,
        }

        # Start with all "toggle" widgets hidden to avoid ugly transitions.
        for preference, action, widget_list in self._toggle_list:
            for widget in widget_list:
                widget.hide()

        toggleaction = self.actiongroup.get_action('hide_all')
        toggleaction.set_active(prefs['hide all'])

        # Sync each "toggle" widget active state with its preference.
        for preference, action, widget_list in self._toggle_list:
            self.actiongroup.get_action(action).set_active(prefs[preference])

        self.actiongroup.get_action('menu_autorotate_width').set_sensitive(False)
        self.actiongroup.get_action('menu_autorotate_height').set_sensitive(False)

        self.add(table)
        table.show()
        self._event_box.show_all()

        self._main_layout.set_events(Gdk.EventMask.BUTTON1_MOTION_MASK |
                                     Gdk.EventMask.BUTTON2_MOTION_MASK |
                                     Gdk.EventMask.BUTTON_PRESS_MASK |
                                     Gdk.EventMask.BUTTON_RELEASE_MASK |
                                     Gdk.EventMask.POINTER_MOTION_MASK)

        self._main_layout.drag_dest_set(Gtk.DestDefaults.ALL,
                                        [Gtk.TargetEntry.new('text/uri-list', 0, 0)],
                                        Gdk.DragAction.COPY |
                                        Gdk.DragAction.MOVE)

        self.connect('focus-in-event', self.gained_focus)
        self.connect('focus-out-event', self.lost_focus)
        self.connect('delete_event', self.close_program)
        self.connect('key_press_event', self._event_handler.key_press_event)
        self.connect('key_release_event', self._event_handler.key_release_event)
        self.connect('configure_event', self._event_handler.resize_event)
        self.connect('window-state-event', self._event_handler.window_state_event)

        self._main_layout.connect('button_release_event',
            self._event_handler.mouse_release_event)
        self._main_layout.connect('scroll_event',
            self._event_handler.scroll_wheel_event)
        self._main_layout.connect('button_press_event',
            self._event_handler.mouse_press_event)
        self._main_layout.connect('motion_notify_event',
            self._event_handler.mouse_move_event)
        self._main_layout.connect('drag_data_received',
            self._event_handler.drag_n_drop_event)

        self.uimanager.set_sensitivities()
        self.show()
        self.restore_window_geometry()

        if prefs['default fullscreen'] or fullscreen:
            toggleaction = self.actiongroup.get_action('fullscreen')
            toggleaction.set_active(True)


        if prefs['previous quit was quit and save']:
            fileinfo = self.filehandler.read_fileinfo_file()

            if fileinfo != None:

                open_path = fileinfo[0]
                open_page = fileinfo[1] + 1

        prefs['previous quit was quit and save'] = False

        if open_path is not None:
            self.filehandler.open_file(open_path)

        if is_slideshow:
            self.actiongroup.get_action('slideshow').activate()

        if show_library:
            self.actiongroup.get_action('library').activate()

        self.cursor_handler.auto_hide_on()
        # Make sure we receive *all* mouse motion events,
        # even if a modal dialog is being shown.
        def _on_event(event):
            if Gdk.EventType.MOTION_NOTIFY == event.type:
                self.cursor_handler.refresh()
            Gtk.main_do_event(event)
        Gdk.event_handler_set(_on_event)

    def gained_focus(self, *args):
        def _delayed_unset_out_of_focus(_):
            self.was_out_of_focus = False
            return False

        if self.was_out_of_focus:
            # Since clicking into an unfocused window triggers the
            # focus event first, then the mouse event, the mouse event
            # can no longer detect that it should be skipped. Thus, delay
            # unsetting was_out_of_focus.
            GLib.idle_add(_delayed_unset_out_of_focus, None,
                          priority=GLib.PRIORITY_DEFAULT_IDLE)

    def lost_focus(self, *args):
        self.was_out_of_focus = True

        # If the user presses CTRL for a keyboard shortcut, e.g. to
        # open the library, key_release_event isn't fired and force_single_step
        # isn't properly unset.
        self.imagehandler.force_single_step = False

    def draw_image(self, scroll_to=None):
        """Draw the current pages and update the titlebar and statusbar.
        """
        # FIXME: what if scroll_to is different?
        if not self._waiting_for_redraw:  # Don't stack up redraws.
            self._waiting_for_redraw = True
            GLib.idle_add(self._draw_image, scroll_to,
                             priority=GLib.PRIORITY_HIGH_IDLE)

    def _update_toggle_preference(self, preference, toggleaction):
        ''' Update "toggle" widget corresponding .

        Note: the widget visibily itself is left unchanged. '''
        prefs[preference] = toggleaction.get_active()
        if 'hide all' == preference:
            self._update_toggles_sensitivity()
        # Since the size of the drawing area is dependent
        # on the visible "toggles", redraw the page.
        self.draw_image()

    def _should_toggle_be_visible(self, preference):
        ''' Return  if "toggle" widget for  should be visible. '''
        if self.is_fullscreen:
            visible = not prefs['hide all in fullscreen']
        else:
            visible = not prefs['hide all']
        visible &= prefs[preference]
        if 'show thumbnails' == preference:
            visible &= self.filehandler.file_loaded
            visible &= self.imagehandler.get_number_of_pages() > 0
        return visible

    def _update_toggles_sensitivity(self):
        ''' Update each "toggle" widget sensitivity. '''
        sensitive = True
        if prefs['hide all']:
            sensitive = False
        elif prefs['hide all in fullscreen'] and self.is_fullscreen:
            sensitive = False
        for preference, action, widget_list in self._toggle_list:
            self.actiongroup.get_action(action).set_sensitive(sensitive)

    def _update_toggles_visibility(self):
        ''' Update each "toggle" widget visibility. '''
        for preference, action, widget_list in self._toggle_list:
            should_be_visible = self._should_toggle_be_visible(preference)
            for widget in widget_list:
                # No change in visibility?
                if should_be_visible != widget.get_visible():
                    (widget.show if should_be_visible else widget.hide)()

    def _draw_image(self, scroll_to):

        self._update_toggles_visibility()

        self.osd.clear()

        if not self.filehandler.file_loaded:
            self._clear_main_area()
            self._waiting_for_redraw = False
            return False

        if self.imagehandler.page_is_available():
            distribution_axis = constants.DISTRIBUTION_AXIS
            alignment_axis = constants.ALIGNMENT_AXIS
            pixbuf_count = 2 if self.displayed_double() else 1 # XXX limited to at most 2 pages
            pixbuf_list = list(self.imagehandler.get_pixbufs(pixbuf_count))
            do_not_transform = [image_tools.is_animation(x) for x in pixbuf_list]
            size_list = [[pixbuf.get_width(), pixbuf.get_height()]
                         for pixbuf in pixbuf_list]

            if self.is_manga_mode:
                orientation = constants.MANGA_ORIENTATION
            else:
                orientation = constants.WESTERN_ORIENTATION

            # Rotation handling:
            # - apply Exif rotation on individual images
            # - apply automatic rotation (size based) on whole page
            # - apply manual rotation on whole page
            if prefs['auto rotate from exif']:
                rotation_list = [image_tools.get_implied_rotation(pixbuf)
                                 for pixbuf in pixbuf_list]
            else:
                rotation_list = [0] * len(pixbuf_list)
            virtual_size = [0, 0]
            for i in range(pixbuf_count):
                if tools.rotation_swaps_axes(rotation_list[i]):
                    size_list[i].reverse()
                size = size_list[i]
                virtual_size[distribution_axis] += size[distribution_axis]
                virtual_size[alignment_axis] = max(virtual_size[alignment_axis],
                                                   size[alignment_axis])
            rotation = tools.compile_rotations(
                image_tools.get_size_rotation(*virtual_size), prefs['rotation'])
            if tools.rotation_swaps_axes(rotation):
                distribution_axis, alignment_axis = alignment_axis, distribution_axis
                orientation = list(orientation)
                orientation.reverse() # 2D only
                for i in range(pixbuf_count):
                    if do_not_transform[i]:
                        continue
                    size_list[i].reverse() # 2D only
            if rotation in (180, 270):
                orientation = tools.vector_opposite(orientation)
            for i in range(pixbuf_count):
                rotation_list[i] = tools.compile_rotations(rotation_list[i], rotation)
            if prefs['vertical flip'] and tools.rotation_swaps_axes(rotation):
                orientation = tools.vector_opposite(orientation)
            if prefs['horizontal flip'] and not tools.rotation_swaps_axes(rotation):
                orientation = tools.vector_opposite(orientation)

            self.layout = layout.FiniteLayout.create_finite_layout(
                pixbuf_count, orientation, self._spacing, distribution_axis,
                alignment_axis, self._show_scrollbars, self.get_visible_area_size,
                lambda zoom_dummy_size: self.zoom.get_zoomed_size(size_list, zoom_dummy_size,
                distribution_axis, do_not_transform,
                prefs['double page autoresize'] in (constants.DOUBLE_PAGE_AUTORESIZE_SIZE,
                constants.DOUBLE_PAGE_AUTORESIZE_FIT_SIZE),
                prefs['double page autoresize'] == constants.DOUBLE_PAGE_AUTORESIZE_FIT_SIZE))
            content_boxes = self.layout.get_content_boxes()
            scaled_sizes = list(map(box.Box.get_size, content_boxes))

            self.transforms = [Transform.ID] * pixbuf_count
            for i in range(pixbuf_count):
                if do_not_transform[i]:
                    continue
                pixbuf_list[i] = image_tools.fit_pixbuf_to_rectangle(
                    pixbuf_list[i], scaled_sizes[i], rotation_list[i])
                self.transforms[i] += Transform.from_rotation(rotation_list[i]) # FIXME also include scales

            for i in range(pixbuf_count):
                if do_not_transform[i]:
                    continue
                if prefs['horizontal flip']:
                    pixbuf_list[i] = image_tools.flip_pixbuf(pixbuf_list[i], 0)
                    self.transforms[i] += Transform.from_flips(True, False)
                if prefs['vertical flip']:
                    pixbuf_list[i] = image_tools.flip_pixbuf(pixbuf_list[i], 1)
                    self.transforms[i] += Transform.from_flips(False, True)
                pixbuf_list[i] = self.enhancer.enhance(pixbuf_list[i])

            for i in range(pixbuf_count):
                image_tools.set_from_pixbuf(self.images[i], pixbuf_list[i])

            scales = tuple(map(lambda x, y: math.sqrt(tools.div(
                tools.volume(x), tools.volume(y))), scaled_sizes, size_list))

            resolutions = tuple(map(lambda sz, sc, ds: sz + [sc, ds], size_list,
                scales, self.layout.get_content_distorted()))
            if self.is_manga_mode:
                resolutions = tuple(reversed(resolutions))
            self.statusbar.set_resolution(resolutions)
            self.statusbar.update()

            smartbg = prefs['smart bg']
            smartthumbbg = prefs['smart thumb bg'] and prefs['show thumbnails']
            if smartbg or smartthumbbg:
                bg_colour = self.imagehandler.get_pixbuf_auto_background(pixbuf_count)
            if smartbg:
                self.set_bg_colour(bg_colour)
            if smartthumbbg:
                self.thumbnailsidebar.change_thumbnail_background_color(bg_colour)

            self._main_layout.get_bin_window().freeze_updates()

            self._main_layout.set_size(*(self.layout.get_union_box().get_size()))
            for i in range(pixbuf_count):
                self._main_layout.move(self.images[i],
                    *content_boxes[i].get_position())

            for i in range(pixbuf_count):
                self.images[i].show()
            for i in range(pixbuf_count, len(self.images)):
                self.images[i].hide()

            # Reset orientation so scrolling behaviour is sane.
            if self.is_manga_mode:
                self.layout.set_orientation(constants.MANGA_ORIENTATION)
            else:
                self.layout.set_orientation(constants.WESTERN_ORIENTATION)

            if scroll_to is not None:
                destination = (scroll_to,) * 2
                if constants.SCROLL_TO_START == scroll_to:
                    index = constants.FIRST_INDEX
                elif constants.SCROLL_TO_END == scroll_to:
                    index = constants.LAST_INDEX
                else:
                    index = None
                self.scroll_to_predefined(destination, index)

            self._main_layout.get_bin_window().thaw_updates()
        else:
            # Save scroll destination for when the page becomes available.
            self._last_scroll_destination = scroll_to
            # If the pixbuf for the current page(s) isn't available,
            # hide all images to clear any old pixbufs.
            # XXX How about calling self._clear_main_area?
            for i in range(len(self.images)):
                self.images[i].hide()
            self._show_scrollbars([False] * len(self._scroll))

        self._waiting_for_redraw = False

        return False

    def _update_page_information(self) -> None:
        """ Updates the window with information that can be gathered
        even when the page pixbuf(s) aren't ready yet. """

        page_number = self.imagehandler.get_current_page()
        if not page_number:
            return
        double = self.displayed_double()

        def make_status(info):
            if not isinstance(info, tuple):
                return info
            if self.is_manga_mode:
                info = reversed(info)
            return ", ".join(info)

        filename = make_status(self.imagehandler.get_page_filename(double=double))
        filesize = make_status(self.imagehandler.get_page_filesize(double=double))
        self.statusbar.set_page_number(page_number,
                                       self.imagehandler.get_number_of_pages(),
                                       2 if double else 1)
        self.statusbar.set_filename(filename)
        self.statusbar.set_root(self.filehandler.get_base_filename())
        self.statusbar.set_filesize(filesize)
        self.statusbar.update()
        self.update_title()

    def update_icon(self, default=False):
        if (self.filehandler.archive_type is not None
            and prefs['archive thumbnail as icon']):
            pixbuf = self.imagehandler.get_thumbnail(1, 48, 48)
            pixbuf = self.enhancer.enhance(pixbuf)
            self.set_icon(pixbuf)
        elif (default):
            self.set_icon_list(icons.mcomix_icons())

    def _page_available(self, page):
        """ Called whenever a new page is ready for displaying. """
        # Refresh display when currently opened page becomes available.
        current_page = self.imagehandler.get_current_page()
        nb_pages = 2 if self.displayed_double() else 1
        if current_page <= page < (current_page + nb_pages):
            self.draw_image(scroll_to=self._last_scroll_destination)
            self._update_page_information()

        # Use first page as application icon when opening archives.
        if page == 1:
            self.update_icon(False)

    def _on_file_opened(self):
        self.uimanager.set_sensitivities()
        number, count = self.filehandler.get_file_number()
        self.statusbar.set_file_number(number, count)
        self.statusbar.update()

    def _on_file_closed(self):
        self.clear()
        self.thumbnailsidebar.hide()
        self.thumbnailsidebar.clear()
        self.uimanager.set_sensitivities()
        self.set_icon_list(icons.mcomix_icons())

    def new_page(self, at_bottom=False):
        """Draw a *new* page correctly (as opposed to redrawing the same
        image with a new size or whatever).
        """
        if not prefs['keep transformation']:
            prefs['rotation'] = 0
            prefs['horizontal flip'] = False
            prefs['vertical flip'] = False

        if at_bottom:
            scroll_to = constants.SCROLL_TO_END
        else:
            scroll_to = constants.SCROLL_TO_START

        self.draw_image(scroll_to=scroll_to)

    @callback.Callback
    def page_changed(self):
        """ Called on page change. """
        self.thumbnailsidebar.load_thumbnails()
        self._update_page_information()

    def set_page(self, num, at_bottom=False):
        if num == self.imagehandler.get_current_page():
            return
        self.imagehandler.set_page(num)
        self.page_changed()
        self.new_page(at_bottom=at_bottom)
        self.slideshow.update_delay()

    def next_book(self):
        archive_open = self.filehandler.archive_type is not None
        next_archive_opened = False
        if (self.slideshow.is_running() and \
            prefs['slideshow can go to next archive']) or \
           prefs['auto open next archive']:
            next_archive_opened = self.filehandler._open_next_archive()

        # If "Auto open next archive" is disabled, do not go to the next
        # directory if current file was an archive.
        if not next_archive_opened and \
           prefs['auto open next directory'] and \
           (not archive_open or prefs['auto open next archive']):
            self.filehandler.open_next_directory()

    def previous_book(self):
        archive_open = self.filehandler.archive_type is not None
        previous_archive_opened = False
        if (self.slideshow.is_running() and \
            prefs['slideshow can go to next archive']) or \
            prefs['auto open next archive']:
            previous_archive_opened = self.filehandler._open_previous_archive()

        # If "Auto open next archive" is disabled, do not go to the previous
        # directory if current file was an archive.
        if not previous_archive_opened and \
            prefs['auto open next directory'] and \
            (not archive_open or prefs['auto open next archive']):
            self.filehandler.open_previous_directory()

    def flip_page(self, step, single_step=False):

        if not self.filehandler.file_loaded:
            return

        current_page = self.imagehandler.get_current_page()
        number_of_pages = self.imagehandler.get_number_of_pages()

        new_page = current_page + step
        if (1 == abs(step) and
            not single_step and
            prefs['default double page'] and
            prefs['double step in double page mode']):
            if +1 == step and not self.imagehandler.get_virtual_double_page():
                new_page += 1
            elif -1 == step and not self.imagehandler.get_virtual_double_page(new_page - 1):
                new_page -= 1

        if new_page <= 0:
            # Only switch to previous page when flipping one page before the
            # first one. (Note: check for (page number <= 1) to handle empty
            # archive case).
            if -1 == step and current_page <= 1:
                return self.previous_book()
            # Handle empty archive case.
            new_page = min(1, number_of_pages)
        elif new_page > number_of_pages:
            if 1 == step:
                return self.next_book()
            new_page = number_of_pages

        if new_page != current_page:
            self.set_page(new_page, at_bottom=(-1 == step))

    def first_page(self):
        number_of_pages = self.imagehandler.get_number_of_pages()
        if number_of_pages:
            self.set_page(1)

    def last_page(self):
        number_of_pages = self.imagehandler.get_number_of_pages()
        if number_of_pages:
            self.set_page(number_of_pages)

    def page_select(self, *args):
        pageselect.Pageselector(self)

    def rotate_90(self, *args):
        prefs['rotation'] = tools.compile_rotations(prefs['rotation'], 90)
        self.draw_image()

    def rotate_180(self, *args):
        prefs['rotation'] = tools.compile_rotations(prefs['rotation'], 180)
        self.draw_image()

    def rotate_270(self, *args):
        prefs['rotation'] = tools.compile_rotations(prefs['rotation'], 270)
        self.draw_image()

    def flip_horizontally(self, *args):
        prefs['horizontal flip'] = not prefs['horizontal flip']
        self.draw_image()

    def flip_vertically(self, *args):
        prefs['vertical flip'] = not prefs['vertical flip']
        self.draw_image()

    def change_double_page(self, toggleaction):
        prefs['default double page'] = toggleaction.get_active()
        self._update_page_information()
        self.draw_image()

    def change_manga_mode(self, toggleaction):
        prefs['default manga mode'] = toggleaction.get_active()
        self.is_manga_mode = toggleaction.get_active()
        self._update_page_information()
        self.draw_image()

    def change_invert_scroll(self, toggleaction):
        prefs['invert smart scroll'] = toggleaction.get_active()

    @property
    def is_fullscreen(self):
        window_state = self.get_window().get_state()
        return 0 != (window_state & Gdk.WindowState.FULLSCREEN)

    def change_fullscreen(self, toggleaction):
        # Disable action until transition if complete.
        toggleaction.set_sensitive(False)
        if toggleaction.get_active():
            if self.previous_size != (None, None):
                self.save_window_geometry()
            self.fullscreen()
        else:
            self.unfullscreen()
        # No need to call draw_image explicitely,
        # as we'll be receiving a window state
        # change or resize event.

    def change_invert_color(self, toggleaction):
        prefs['invert color'] = not self.enhancer.invert_color
        self.enhancer.invert_color = prefs['invert color']
        self.enhancer.signal_update()

    def change_zoom_mode(self, radioaction=None, *args):
        if radioaction:
            prefs['zoom mode'] = radioaction.get_current_value()
        self.zoom.set_fit_mode(prefs['zoom mode'])
        self.zoom.set_scale_up(prefs['stretch'])
        self.zoom.reset_user_zoom()
        self.draw_image()

    def change_autorotation(self, radioaction=None, *args):
        """ Switches between automatic rotation modes, depending on which
        radiobutton is currently activated. """
        if radioaction:
            prefs['auto rotate depending on size'] = radioaction.get_current_value()
        self.draw_image()

    def change_stretch(self, toggleaction, *args):
        """ Toggles stretching small images. """
        prefs['stretch'] = toggleaction.get_active()
        self.zoom.set_scale_up(prefs['stretch'])
        self.draw_image()

    def change_toolbar_visibility(self, toggleaction):
        self._update_toggle_preference('show toolbar', toggleaction)

    def change_menubar_visibility(self, toggleaction):
        self._update_toggle_preference('show menubar', toggleaction)

    def change_statusbar_visibility(self, toggleaction):
        self._update_toggle_preference('show statusbar', toggleaction)

    def change_scrollbar_visibility(self, toggleaction):
        self._update_toggle_preference('show scrollbar', toggleaction)

    def change_thumbnails_visibility(self, toggleaction):
        self._update_toggle_preference('show thumbnails', toggleaction)

    def change_hide_all(self, toggleaction):
        self._update_toggle_preference('hide all', toggleaction)

    def change_keep_transformation(self, *args):
        prefs['keep transformation'] = not prefs['keep transformation']

    def manual_zoom_in(self, *args):
        self.zoom.zoom_in()
        self.draw_image()

    def manual_zoom_out(self, *args):
        self.zoom.zoom_out()
        self.draw_image()

    def manual_zoom_original(self, *args):
        self.zoom.reset_user_zoom()
        self.draw_image()

    def _show_scrollbars(self, request):
        """ Enables scroll bars depending on requests and preferences. """

        limit = self._should_toggle_be_visible('show scrollbar')
        for i in range(len(self._scroll)):
            if limit and request[i]:
                self._scroll[i].show()
            else:
                self._scroll[i].hide()

    def is_scrollable(self):
        """ Returns True if the current images do not fit into the viewport. """
        if self.layout is None:
            return False
        return not all(tools.smaller_or_equal(self.layout.get_union_box().get_size(),
            self.get_visible_area_size()))

    def scroll_with_flipping(self, x, y):
        """Returns true if able to scroll without flipping to
        a new page and False otherwise."""
        return self._event_handler._scroll_with_flipping(x, y)

    def scroll(self, x, y, bound=None):
        """Scroll  px horizontally and  px vertically. If  is
        'first' or 'second', we will not scroll out of the first or second
        page respectively (dependent on manga mode). The  argument
        only makes sense in double page mode.

        Return True if call resulted in new adjustment values, False
        otherwise.
        """
        old_hadjust = self._hadjust.get_value()
        old_vadjust = self._vadjust.get_value()

        visible_width, visible_height = self.get_visible_area_size()

        hadjust_upper = max(0, self._hadjust.get_upper() - visible_width)
        vadjust_upper = max(0, self._vadjust.get_upper() - visible_height)
        hadjust_lower = 0

        if bound is not None and self.is_manga_mode:
            bound = {'first': 'second', 'second': 'first'}[bound]

        if bound == 'first':
            hadjust_upper = max(0, hadjust_upper -
                self.images[1].size_request().width - 2) # XXX transitional(double page limitation)

        elif bound == 'second':
            hadjust_lower = self.images[0].size_request().width + 2 # XXX transitional(double page limitation)

        new_hadjust = old_hadjust + x
        new_vadjust = old_vadjust + y

        new_hadjust = max(hadjust_lower, new_hadjust)
        new_vadjust = max(0, new_vadjust)

        new_hadjust = min(hadjust_upper, new_hadjust)
        new_vadjust = min(vadjust_upper, new_vadjust)

        self._vadjust.set_value(new_vadjust)
        self._hadjust.set_value(new_hadjust)
        self._scroll[0].queue_resize_no_redraw()
        self._scroll[1].queue_resize_no_redraw()

        return old_vadjust != new_vadjust or old_hadjust != new_hadjust

    def scroll_to_predefined(self, destination, index=None):
        self.layout.scroll_to_predefined(destination, index)
        self.update_viewport_position()

    def update_viewport_position(self):
        viewport_position = self.layout.get_viewport_box().get_position()
        self._hadjust.set_value(viewport_position[0]) # 2D only
        self._vadjust.set_value(viewport_position[1]) # 2D only
        self._scroll[0].queue_resize_no_redraw()
        self._scroll[1].queue_resize_no_redraw()

    def update_layout_position(self):
        self.layout.set_viewport_position(
            (int(round(self._hadjust.get_value())), int(round(self._vadjust.get_value()))))

    def clear(self):
        """Clear the currently displayed data (i.e. "close" the file)."""
        self.set_title(constants.APPNAME)
        self.statusbar.set_message('')
        self.draw_image()

    def _clear_main_area(self):
        for i in self.images:
            i.hide()
        for i in self.images:
            i.clear()
        self._show_scrollbars([False] * len(self._scroll))
        self.layout = layout.create_dummy_layout()
        self._main_layout.set_size(*self.layout.get_union_box().get_size())
        self.set_bg_colour(prefs['bg colour'])

    def displayed_double(self):
        """Return True if two pages are currently displayed."""
        return (self.imagehandler.get_current_page() and
                prefs['default double page'] and
                not self.imagehandler.get_virtual_double_page() and
                self.imagehandler.get_current_page() != self.imagehandler.get_number_of_pages())

    def get_visible_area_size(self):
        """Return a 2-tuple with the width and height of the visible part
        of the main layout area.
        """
        dimensions = list(self.get_size())

        for preference, action, widget_list in self._toggle_list:
            for widget in widget_list:
                if widget.get_visible():
                    axis = self._toggle_axis[widget]
                    requisition = widget.size_request()
                    if constants.PageAxis.WIDTH == axis:
                        size = requisition.width
                    elif constants.PageAxis.HEIGHT == axis:
                        size = requisition.height
                    dimensions[axis] -= size

        return tuple(dimensions)

    def get_layout_pointer_position(self):
        """Return a 2-tuple with the x and y coordinates of the pointer
        on the main layout area, relative to the layout.
        """
        x, y = self._main_layout.get_pointer()
        x += self._hadjust.get_value()
        y += self._vadjust.get_value()

        return (x, y)

    def set_cursor(self, mode):
        """Set the cursor on the main layout area to . You should
        probably use the cursor_handler instead of using this method
        directly.
        """
        self._main_layout.get_bin_window().set_cursor(mode)

    def update_title(self):
        """Set the title acording to current state."""
        this_screen = 2 if self.displayed_double() else 1 # XXX limited to at most 2 pages
        # TODO introduce formatter to merge these string ops with the ops for status bar updates
        title = '['
        for i in range(this_screen):
            title += '%d' % (self.imagehandler.get_current_page() + i)
            if i < this_screen - 1:
                title += ','
        title += ' / %d]  %s' % (self.imagehandler.get_number_of_pages(),
            self.imagehandler.get_pretty_current_filename())
        title = i18n.to_unicode(title)

        if self.slideshow.is_running():
            title = '[%s] %s' % (_('SLIDESHOW'), title)

        self.set_title(i18n.to_display_string(title))

    def set_bg_colour(self, colour):
        """Set the background colour to . Colour is a sequence in the
        format (r, g, b). Values are 16-bit.
        """
        colour = colour[:3]
        self._event_box.modify_bg(Gtk.StateType.NORMAL, Gdk.Color(*colour))
        if prefs['thumbnail bg uses main colour']:
            self.thumbnailsidebar.change_thumbnail_background_color(prefs['bg colour'][:3])
        self._bg_colour = colour

    def get_bg_colour(self):
        return self._bg_colour

    def extract_page(self, *args):
        """Save the currently displayed images to disk, appending a number if a
        file with an identical name was already found in the target directory.
        """
        this_screen = 2 if self.displayed_double() else 1 # XXX limited to at most 2 pages
        for i in reversed(range(this_screen)) if self.is_manga_mode \
        else range(this_screen):
            file_path = self.imagehandler.get_path_to_page(
                self.imagehandler.get_current_page() + i)
            if not file_path:
                return
            file_name = os.path.split(file_path)[-1]

            if self.filehandler.archive_type is not None:
                # Prepend the archive base name to the filename being displayed
                archive_name = self.filehandler.get_pretty_current_filename()
                file_name = (
                    os.path.splitext(archive_name)[0] + '_' + file_name)

            target_dir = prefs['path of last saved in filechooser'] + os.sep
            suggest_name = i18n.to_unicode(file_name)
            attempt = 1
            while os.path.exists(target_dir + suggest_name):
                suggest_name = tools.append_number_to_filename(
                    file_name, number=attempt)
                attempt += 1

            save_dialog = Gtk.FileChooserDialog(_('Save page as'), self,
                Gtk.FileChooserAction.SAVE,
                (Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT,
                Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT)
            )
            save_dialog.set_do_overwrite_confirmation(True)
            save_dialog.set_create_folders(True)
            save_dialog.set_current_name(suggest_name)
            save_dialog.set_current_folder(target_dir)

            if save_dialog.run() == Gtk.ResponseType.ACCEPT:
                target = save_dialog.get_filename()
                if target:
                    target = i18n.to_unicode(target)
                    try:
                        shutil.copy2(file_path, target)
                    except Exception as e:
                        log.warning(e)

                prefs['path of last saved in filechooser'] = \
                    save_dialog.get_current_folder() \
                    if prefs['store last saved in directory'] \
                    else constants.HOME_DIR

            save_dialog.destroy()

    def delete(self, *args):
        """ The currently opened file/archive will be deleted after showing
        a confirmation dialog. """

        current_file = self.imagehandler.get_real_path()
        dialog = message_dialog.MessageDialog(self, Gtk.DialogFlags.MODAL, Gtk.MessageType.QUESTION,
                Gtk.ButtonsType.NONE)
        dialog.set_should_remember_choice('delete-opend-file', (Gtk.ResponseType.OK,))
        dialog.set_text(
                _('Delete "%s"?') % os.path.basename(current_file),
                _('The file will be deleted from your harddisk.'))
        dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
        dialog.add_button(Gtk.STOCK_DELETE, Gtk.ResponseType.OK)
        dialog.set_default_response(Gtk.ResponseType.OK)
        result = dialog.run()

        if result == Gtk.ResponseType.OK:
            # Go to next page/archive, and delete current file
            if self.filehandler.archive_type is not None:
                self.filehandler.last_read_page.clear_page(current_file)

                next_opened = self.filehandler._open_next_archive()
                if not next_opened:
                    next_opened = self.filehandler._open_previous_archive()
                if not next_opened:
                    self.filehandler.close_file()

                if os.path.isfile(current_file):
                    os.unlink(current_file)
            else:
                if self.imagehandler.get_number_of_pages() > 1:
                    # Open the next/previous file
                    if self.imagehandler.get_current_page() >= self.imagehandler.get_number_of_pages():
                        self.flip_page(-1)
                    else:
                        self.flip_page(+1)
                    # Unlink the desired file
                    if os.path.isfile(current_file):
                        os.unlink(current_file)
                    # Refresh the directory
                    self.filehandler.refresh_file()
                else:
                    self.filehandler.close_file()
                    if os.path.isfile(current_file):
                        os.unlink(current_file)

    def show_info_panel(self):
        """ Shows an OSD displaying information about the current page. """

        if not self.filehandler.file_loaded:
            return

        text = ''
        filename = self.imagehandler.get_pretty_current_filename()
        if filename:
            text += '%s\n' % filename
        file_number, file_count = self.filehandler.get_file_number()
        if file_count:
            text += '(%d / %d)\n' % (file_number, file_count)
        else:
            text += '\n'
        page_number = self.imagehandler.get_current_page()
        number_of_pages = self.imagehandler.get_number_of_pages()
        if page_number:
            text += '%s %d / %d' % (_('Page'), page_number, number_of_pages)
        text = text.strip('\n')
        if text:
            self.osd.show(text)

    def minimize(self, *args):
        """ Minimizes the MComix window. """
        self.iconify()

    def write_config_files(self):

        self.filehandler.write_fileinfo_file()
        preferences.write_preferences_file()
        bookmark_backend.BookmarksStore.write_bookmarks_file()

        # Write keyboard accelerator map
        keybindings.keybinding_manager(self).save()

    def save_and_terminate_program(self, *args):
        prefs['previous quit was quit and save'] = True

        self.terminate_program()

    def get_window_geometry(self):
        return self.get_position() + self.get_size()

    def save_window_geometry(self) -> None:
        x, y, width, height = self.get_window_geometry()
        prefs['window x'] = x
        prefs['window y'] = y
        prefs['window width'] = width
        prefs['window height'] = height
        prefs['window maximized'] = self.is_maximized()

    def restore_window_geometry(self):
        if self.get_window_geometry() == (prefs['window x'],
                                          prefs['window y'],
                                          prefs['window width'],
                                          prefs['window height']) \
           and self.is_maximized() == prefs['window maximized']:
            return False

        self.move(prefs['window x'], prefs['window y'])
        if prefs['window maximized']:
            self.maximize()
        else:
            self.resize(prefs['window width'], prefs['window height'])
        return True

    def update_space(self):
        self._spacing = prefs['space between two pages']
        self.draw_image()

    def close_program(self, *args):
        if not self.is_fullscreen:
            self.save_window_geometry()
        self.terminate_program()

    def terminate_program(self):
        """Run clean-up tasks and exit the program."""

        self.hide()

        if Gtk.main_level() > 0:
            Gtk.main_quit()

        if prefs['auto load last file'] and self.filehandler.file_loaded:
            prefs['path to last file'] = self.imagehandler.get_real_path()
            prefs['page of last file'] = self.imagehandler.get_current_page()

        else:
            prefs['path to last file'] = ''
            prefs['page of last file'] = 1

        if prefs['hide all'] and self.hide_all_forced and self.fullscreen:
            prefs['hide all'] = False

        self.write_config_files()

        self.filehandler.close_file()
        if main_dialog._dialog is not None:
            main_dialog._dialog.close()
        backend.LibraryBackend().close()

        # This hack is to avoid Python issue #1856.
        for thread in threading.enumerate():
            if thread is not threading.currentThread() and not isinstance(thread, threading._DummyThread):
                log.debug('Waiting for thread %s to finish before exit', thread)
                thread.join()

#: Main window instance
__main_window = None


def main_window():
    """ Returns the global main window instance. """
    return __main_window


def set_main_window(window):
    global __main_window
    __main_window = window


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/message_dialog.py0000644000175000017500000000641114476523373017332 0ustar00moritzmoritz""" Simple extension of Gtk.MessageDialog for consistent formating. Also
    supports remembering the dialog result.
"""

from gi.repository import Gtk

from mcomix.preferences import prefs
from mcomix.i18n import _


class MessageDialog(Gtk.MessageDialog):

    def __init__(self, parent=None, flags=0, type=0, buttons=0):
        """ Creates a dialog window.
        @param parent: Parent window
        @param flags: Dialog flags
        @param type: Dialog icon/type
        @param buttons: Dialog buttons. Can only be a predefined BUTTONS_XXX constant.
        """
        if parent is None:
            # Fix "mapped without a transient parent" Gtk warning.
            from mcomix import main
            parent = main.main_window()
        super(MessageDialog, self).__init__(parent=parent, flags=flags, type=type, buttons=buttons)

        #: Unique dialog identifier (for storing 'Do not ask again')
        self.dialog_id = None
        #: List of response IDs that should be remembered
        self.choices = []
        #: Automatically destroy dialog after run?
        self.auto_destroy = True

        self.remember_checkbox = Gtk.CheckButton(_('Do not ask again.'))
        self.remember_checkbox.set_no_show_all(True)
        self.remember_checkbox.set_can_focus(False)
        self.get_message_area().pack_end(self.remember_checkbox, True, True, 6)

    def set_text(self, primary, secondary=None):
        """ Formats the dialog's text fields.
        @param primary: Main text.
        @param secondary: Descriptive text.
        """
        if primary:
            self.set_markup('' +
                primary + '')
        if secondary:
            self.format_secondary_markup(secondary)

    def should_remember_choice(self):
        """ Returns True when the dialog choice should be remembered. """
        return self.remember_checkbox.get_active()

    def set_should_remember_choice(self, dialog_id, choices):
        """ This method enables the 'Do not ask again' checkbox.
        @param dialog_id: Unique identifier for the dialog (a string).
        @param choices: List of response IDs that should be remembered
        """
        self.remember_checkbox.show()
        self.dialog_id = dialog_id
        self.choices = [int(choice) for choice in choices]

    def set_auto_destroy(self, auto_destroy):
        """ Determines if the dialog should automatically destroy itself
        after run(). """
        self.auto_destroy = auto_destroy

    def run(self):
        """ Makes the dialog visible and waits for a result. Also destroys
        the dialog after the result has been returned. """

        if self.dialog_id in prefs['stored dialog choices']:
            self.destroy()
            return prefs['stored dialog choices'][self.dialog_id]
        else:
            self.show_all()
            # Prevent checkbox from grabbing focus by only enabling it after show
            self.remember_checkbox.set_can_focus(True)
            result = super(MessageDialog, self).run()

            if (self.should_remember_choice() and int(result) in self.choices):
                prefs['stored dialog choices'][self.dialog_id] = int(result)

            if self.auto_destroy:
                self.destroy()
            return result


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/0000755000175000017500000000000014553265237015620 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/__init__.py0000644000175000017500000000000014476523373017721 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/ca/0000755000175000017500000000000014553265237016203 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/ca/LC_MESSAGES/0000755000175000017500000000000014553265237017770 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/ca/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022071 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/ca/LC_MESSAGES/mcomix.mo0000644000175000017500000002702514476523373021631 0ustar00moritzmoritz	(
)
5
>
)E
<o


	



!0	=	G
Q9\
!])Bk
j	u
 +"7 W%xrRcu

LR
e+p	A\p	
Hd	is+4.AN
Z
hOs)?D[ox	4T8W	\f
z	+5A#M)q
X,{=
(48>
S^jv

jx,9$ 0%H#n$"s<J7  
!!
!!-!O!m!
!;!$!'!."0F"w"""""Q#g#
########$!$`9$$$0$$%
$%2%K%\%c%k%:%%%%&
&	&&&3&8&>&
C&N&	Z&Jd&&&&&&
&'''--'9['''
''
'W'	=(,G(t(!(0(((())))6)O)
T))b)P) ))**6*E*+	+&+ 2+(S+
|+ ++n+&,A,	,,
,
,,,-- -(-9-L-R-Y-s----------	.	X(n8]W.Pw'u)e06$\HxZfA&OTk{/o;j=LiD^Jty@IabF` zpgs|QlS-hVd3#MN>4B9
CvUcq,E7Y[5_<2K
}R!r~G"%*m:1+?%d comments%d pages(Copy)A collection by that name already exists.A file named '%s' already exists. Do you want to replace it?AccessedAdd a new empty collection.Add booksAdd books to '%s'.Add more books to the library.Add new collection?Adding '%s'...Adding booksAll booksAll filesAll imagesAlways use this selected colour as the background colour.AppearanceArchiveArchives are stored as ZIP files.Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically pick a background colour that fits the viewed image.Automatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.BackgroundBehaviourBest fit modeBrazilian Portuguese translationBzip2 compressed tar archiveCatalan translationCommentsCould not add a new collection called '%s'.Could not change the name to '%s'.Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCroatian translationCzech translationDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Double page modeDutch translationEdit archiveEnhance imageFilesFirst pageFit _height modeFit _width modeFit height modeFit width modeFli_p horizontallyFlip _verticallyFlip two pages, instead of one, each time we flip pages in double page mode.French translationFullscreenGerman translation and Nautilus thumbnailerGreek translationGzip compressed tar archiveH_ide allHungarian translationIcon designImageImagesIndonesian translationIt reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKorean translationLast pageLibraryLocationM_anual zoom modeMagnifying LensMagnifying _lensMagnifying lensManga modeManual zoom modeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNext pageNo images in '%s'OpenOpen the selected book.OwnerPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Polish translationPr_eferencesPreferencesPrevious pagePropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.RAR archiveRemove books from the library?Remove from archiveRename collection?Replacing it will overwrite its contents.RootRotat_e 90 degrees CCWRotate 180 de_greesRotationRussian translationSLIDESHOWS_crollbarsSaveScrollSet the magnification factor of the magnifying lens.Set the size of the magnifying lens. It is a square with a side of this many pixels.Simplified Chinese translationSizeSlideshowSpanish translationSt_atusbarStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.T_oolbarsTar archiveTh_umbnailsThe new archive could not be saved!The original files have not been removed.ThumbnailsTraditional Chinese translationTransparencyTreat all files found within archives, that have one of these file endings, as comments.Unknown filetypeUse a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.ZIP archive_About_Best fit mode_Bookmarks_Close_Double page mode_Edit_Edit archive..._File_First page_Fullscreen_Go_Help_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Open..._Previous page_Quit_Rotate 90 degrees CW_Toolbar_ViewProject-Id-Version: Comix 3.2
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2009-01-25 22:33+0100
Last-Translator: Carles Escrig i Royo 
Language-Team: CA 
Language: 
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Generated-By: pygettext.py 1.5
X-Poedit-Language: Catalan
X-Poedit-Country: SPAIN
%d comentaris%d pàgines(Copia)Ja existeix una col·lecció amb aquest nom.Ja existeix un fitxer anomenat '%s'. Voleu substituir-lo?AcceditAfegeix una col·lecció nova buida.Afegeix llibresAfegeix llibres a '%s'.Afegeix més llibres a la biblioteca.Voleu afegir una nova col·lecció?Afegint '%s'...Afegint llibresTots els llibresTots els fitxersTotes les imatgesUsa sempre aquest color per al fons.AspecteArxiuEls arxius es desen en format ZIP.Ajusta automàticament el contrast (tant la claror com la foscor), de manera independent per a cada banda de color.Obre automàticament el següent arxiu del directori quan passem de l'última pàgina, o l'arxiu anterior si girem la primera pàgina.Selecciona automàticament un color per al fons que combine amb la imatge.Rota automàticament les imatges quan s'hi especifique una orientació a les metadades de la imatge, com per exemple a les etiquetes Exif.FonsComportamentMillor encaixTraducció al portuguès brasilerArxiu tar comprimit amb bzip2Traducció al catalàComentarisNo s'ha pogut afegir una nova col·lecció amb el nom '%s'.No s'ha pogut canviar el nom a '%s'.No s'ha pogut duplicar la col·lecció.No s'ha pogut obrir %s: No existeix el fitxer.No s'ha pogut obrir %s: S'ha denegat el permís.No s'ha pogut llegir %sTraducció al croatTraducció al xecVisualitzacióMostra només els llibres que tenen la cadena de text especificada en el nom complet. Les majúscules i minúscules no es tindran en compte.Mode de doble pàginaTraducció a l'holandèsEdita l'arxiuMillora la imatgeFitxersPrimera pàginaAjusta a l'al_çadaA_justa a l'ampladaAjusta a l'alçadaAjusta a l'ampladaInverteix _horitzontalmentInverteix _verticalmentGira dues pàgines, en lloc de una, cada cop que canviem de pàgina en el mode de doble pàgina.Traducció al francèsPantalla completaTraducció a l'alemany i generador de miniaturesTraducció al grecArxiu tar comprimit amb gzipAmaga-ho _totTraducció a l'hongarèsDisseny d'iconesImatgeImatgesTraducció a l'indonesiLlegeix arxius ZIP, RAR i tar, així com fitxers d'imatge.Traducció a l'italiàTraducció al japonèsTraducció al coreàÚltima pàginaBibliotecaUbicacióZoom m_anualLupa_LupaLupaMode mangaZoom manualModificatMou els llibres de '%(source collection)s' a '%(destination collection)s'.NomPàgina següentNo hi ha imatges a '%s'ObreObre el llibre seleccionat.PropietariPàginaPermisosTraducció al persaIntroduïu un nom per a la nova col·lecció.Introduïu un nom nou per a la col·lecció seleccionada.Traducció al polonèsPr_eferènciesPreferènciesPàgina anteriorPropietatsCol·loca la col·lecció '%(subcollection)s' en la col·lecció '%(supercollection)s'.Arxiu RARVoleu esborrar els llibres de la biblioteca?Suprimeix del fitxerVoleu reanomenar la col·lecció?Substituint-lo hi sobreescriureu els continguts.ArrelRota 90 graus a l'_esquerraRota 180 _grausRotacióTraducció al rusPRESENTACIÓBarres de _desplaçamentDesaDesplaçamentConfigura el factor d'augment de la lupa.Configura la mida de la lupa. Es tracta d'un quadrat d'aquest nombre de píxels.Traducció al xinès simplificatMidaProjecció de diapositivesTraducció al castellàB_arra d'estatDesa les miniatures dels fitxers oberts d'acord amb l'especificació freedesktop.org. Aquestes miniatures es comparteixen amb moltes altres aplicacions, com ara bé la majoria de gestors de fitxers.Barres d'_einesArxiu tarMiniat_uresNo s'ha pogut desar l'arxiu nou!No s'han eliminat els fitxers originals.MiniaturesTraducció al xinès tradicionalTransparènciaTracta tots els fitxers que hi ha dintre dels arxius, que tinguen una d'aquestes extensions, com a comentaris.Tipus de fitxer desconegutUsa un fons a quadrats gris per a les imatges transparents. Si aquesta opció està inhabilitada, el fons serà completament blanc.Arxiu ZIPQu_ant aMillor ajus_t_MarcadorsTan_ca_Doble pàgina_Edita_Edita l'arxiu..._FitxerPr_imera pàgina_Pantalla completaVé_s_Ajuda_Manté la transformacióÚ_ltima pàgina_Biblioteca..._Mode mangaBarra de m_enúPàgi_na següent_Obre..._Pàgina anteriorI_x_Rota 90 graus a la dreta_Barra d'eines_Visualitza././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/ca/__init__.py0000644000175000017500000000000014476523373020304 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/cs/0000755000175000017500000000000014553265237016225 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/cs/LC_MESSAGES/0000755000175000017500000000000014553265237020012 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/cs/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022113 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/cs/LC_MESSAGES/mcomix.mo0000644000175000017500000002700514476523373021651 0ustar00moritzmoritz	(
)
5
>
)E
<o


	



!0	=	G
Q9\
!])Bk
j	u
 +"7 W%xrRcu

LR
e+p	A\p	
Hd	is+4.AN
Z
hOs)?D[ox	4T8W	\f
z	+5A#M)q
X,{=
(48>
S^jv
i7GS$[@	 
"8Oi3+WwB lV  	  % &!>!Y!-e!!!!&!(! "2"L"b"yk"""#$#7#
?#M#g#####O#1$L$:\$$%$$$$$%%F+%r%%%%%% %&&&&&
4&S?&&&%&	&&	&'''%.'.T'''
''
'M'
('(C(W(+m((4((	(()").)6)#<)K`)&))))
***
*	*+%!+G+"P+s+M+++
j,u,,
,	,,,,,,,	--!-8-	J-
T-b-s-
--	-1--	-	X(n8]W.Pw'u)e06$\HxZfA&OTk{/o;j=LiD^Jty@IabF` zpgs|QlS-hVd3#MN>4B9
CvUcq,E7Y[5_<2K
}R!r~G"%*m:1+?%d comments%d pages(Copy)A collection by that name already exists.A file named '%s' already exists. Do you want to replace it?AccessedAdd a new empty collection.Add booksAdd books to '%s'.Add more books to the library.Add new collection?Adding '%s'...Adding booksAll booksAll filesAll imagesAlways use this selected colour as the background colour.AppearanceArchiveArchives are stored as ZIP files.Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically pick a background colour that fits the viewed image.Automatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.BackgroundBehaviourBest fit modeBrazilian Portuguese translationBzip2 compressed tar archiveCatalan translationCommentsCould not add a new collection called '%s'.Could not change the name to '%s'.Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCroatian translationCzech translationDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Double page modeDutch translationEdit archiveEnhance imageFilesFirst pageFit _height modeFit _width modeFit height modeFit width modeFli_p horizontallyFlip _verticallyFlip two pages, instead of one, each time we flip pages in double page mode.French translationFullscreenGerman translation and Nautilus thumbnailerGreek translationGzip compressed tar archiveH_ide allHungarian translationIcon designImageImagesIndonesian translationIt reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKorean translationLast pageLibraryLocationM_anual zoom modeMagnifying LensMagnifying _lensMagnifying lensManga modeManual zoom modeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNext pageNo images in '%s'OpenOpen the selected book.OwnerPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Polish translationPr_eferencesPreferencesPrevious pagePropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.RAR archiveRemove books from the library?Remove from archiveRename collection?Replacing it will overwrite its contents.RootRotat_e 90 degrees CCWRotate 180 de_greesRotationRussian translationSLIDESHOWS_crollbarsSaveScrollSet the magnification factor of the magnifying lens.Set the size of the magnifying lens. It is a square with a side of this many pixels.Simplified Chinese translationSizeSlideshowSpanish translationSt_atusbarStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.T_oolbarsTar archiveTh_umbnailsThe new archive could not be saved!The original files have not been removed.ThumbnailsTraditional Chinese translationTransparencyTreat all files found within archives, that have one of these file endings, as comments.Unknown filetypeUse a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.ZIP archive_About_Best fit mode_Bookmarks_Close_Double page mode_Edit_Edit archive..._File_First page_Fullscreen_Go_Help_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Open..._Previous page_Quit_Rotate 90 degrees CW_Toolbar_ViewProject-Id-Version: PACKAGE VERSION
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2009-01-25 21:54+0100
Last-Translator: Jan Nekvasil 
Language-Team: LANGUAGE 
Language: 
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Generated-By: pygettext.py 1.5
%d komentářů%d stránek(Kopie)Sbírka tohoto jména již existuje.Soubor jménem „%s“ již existuje. Přejete si jej nahradit?PoužitýPřidat novou prázdnou sbírku.Přidat knihyPřidat knihy do „%s“.Přidat další knihy do knihovny.Přidat novou sbírku?Přidává se „%s“…Přidávají se knihyVšechny knihyVšechny souboryVšechny obrázkyVždy používat vybranou barvu jako barvu pozadí.VzhledArchivArchivy jsou uchovávány jako ZIP soubory.Automaticky upravit kontrast (světlost i tmavost) odděleně pro každý rozsah barev.Automaticky otevírat následující archiv ve složce po otočení poslední strany, či naopak archiv předchozí při nalistování před první stranu.Automaticky vybírat barvu hodící se k prohlíženému obrázku.Automaticky pootáčet obrázky pokud jejich metadata obsahují údaj o orientaci, jako např. značku Exif.PozadíChováníOptimální přizpůsobeníPřeklad do brazilské portugalštinyArchiv tar komprimovaný pomocí bzip2Překlad do katalánštinyKomentářeNelze přidat novou sbírku jménem „%s“.Nelze změnit jméno na „%s“.Sbírku nelze zduplikovat.Nelze otevřít %s: Soubor neexistuje.Nelze otevřít %s: Přístup odmítnut.%s nelze načístPřeklad do chorvatštinyPřeklad do češtinyZobrazitZobrazit pouze ty knihy, jejichž celá cesta obsahuje daný výraz. Vyhledávání nerozlišuje velká a malá písmena.Dvoustránkový režimPřeklad do holandštinyUpravit archivVylepšit obrázekSouboryPrvní stranaPřizpůsobit na _výškuPřizpůsobit na _šířkuPřizpůsobit na výškuPřizpůsobit na šířkuPřevrátit _vodorovněPřevrátit _svisleObracet dvě strany místo jedné při listování ve dvoustránkovém režimu.Překlad do francouzštinyCelá obrazovkaPřeklad do němčiny a vytvářeč náhledů pro NautilusPřeklad do řečtinyArchiv tar komprimovaný pomocí gzip_Skrýt všePřeklad do maďarštinyIkonyObrázekObrázkyPřeklad do indonéštinyČte ZIP, RAR a tar archivy, stejně jako prosté obrázkové soubory.Překlad do italštinyPřeklad do japonštinyPřeklad do korejštinyPoslední stranaKnihovnaUmístěníRežim ručního _přiblíženíLupa_LupaLupaManga režimRuční přiblíženíZměněnýPřesunout knihy z „%(source collection)s“ do „%(destination collection)s“.JménoNásledující strana„%s“ neobsahuje žádné obrázkyOtevřítOtevřít vybranou knihu.VlastníkStranaOprávněníPřeklad do perštinyProsím vložte jméno nové sbírky.Prosím vložte nové jméno vybrané sbírky.Překlad do polštiny_NastaveníNastaveníPředchozí stranaVlastnostiVložit sbírku „%(subcollection)s“ do sbírky „%(supercollection)s“.Archiv RAROdstranit knihy z knihovny?Odstranit z archivuPřejmenovat sbírku?Nahrazením bude přepsán původní obsah.KořenPootočit o 90° _proti směru hodinových ručičekPootočit _o 180°OrientacePřeklad do maďarštinyPROMÍTÁNÍ OBRÁZKŮ_PosuvníkyUložitPosunNastavit úroveň zvětšení lupy.Nastavit velikost lupy, čtverce o velikosti strany daného počtu pixelů.Překlad do zjednodušené čínštinyVelikostPromítání snímkůPřeklad do španělštiny_Stavová lištaUkládat náhledy pro otevírané soubory v souladu se specifikací freedesktop.org. Tyto náhledy jsou používány mnoha jinými aplikacemi, jako například většinou správců souborů._Lišty nástrojůArchiv tar_NáhledyNelze uložit nový archiv!Původní soubory nebyly odstraněny.NáhledyPřeklad do tradiční čínštinyPrůhlednostZacházet se soubory s jednou z těchto přípon v názvu jako s komentáři.Neznámý typ souboruPoužívat šedé šachovnicové pozadí pro průhledné obrázky. Není-li tato volba nastavena, je použito prosté bílé pozadí.Archiv ZIP_O programu_Optimální přiblížení_Záložky_Zavřít_Dvoustránkový režim_UpravitUpravit _archiv_SouborP_rvní strana_Celá obrazovka_Přejít_Nápověda_Zachovat transformaciP_oslední strana_Knihovna_Manga režimLišta _nabídky_Následující strana_Otevřít…_Předchozí strana_UkončitPootočit o 90° _ve směru hodinových ručiček_Lišta nástrojů_Zobrazit././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/cs/__init__.py0000644000175000017500000000000014476523373020326 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/de/0000755000175000017500000000000014553265237016210 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/de/LC_MESSAGES/0000755000175000017500000000000014553265237017775 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/de/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022076 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1705863369.0
mcomix-3.1.0/mcomix/messages/de/LC_MESSAGES/mcomix.mo0000644000175000017500000014245514553264311021631 0ustar00moritzmoritz(\"(.).)0.,Z.(.:..K/'W/#//#///%0590o0090000	1-:1h12
22	"2B,2Ao22222
3)3.F3<u33
333	33Z4l444944=4*595F5O5	\5	f5
p5{595C566
 6+6368D6!}6	66]6-7!K7m7>882X9#99SA:B:7:k;8|;U;< <4<
C<	N<
X<f<	o< y<q<=)=r===&==P
>[>a>z>
>>
>>>
>>7>"?1A?%s?-?+?"?-@D@ d@%@@1@@
A/AIA	^AhA
zA
AAA/A.A%B ,B
MB	XBbBwBrBBC6CLC^CmCzCC
CCCCCD: D[D	`D
jD	uD2DDD=E&!F
HFSFdFsFF
FFFFFF
G*GFGYGjG|G37H"kHLHHH<H:I	MI
WIbIrII+I
I
III	
JJ*J/JEJWJcJ
iJwJ8~J7JJK&KDKWKuKK!KAKLL*L?>L~L@LLLLLM
M	M#M
+M9MMM
`MnMMMMMMMM!M+N
FNQN0bN.N	NNNNHN8O
=O
KOVO\OmO
qOOO	OOOO%O

P/PBHP PP
PP3P/Q01QbQwQQ
QQ3Q5Q$R%aPa`ara$aaa
bbb*b)b$!c(Fc5oc2c1c
d.d!Hdjd<eMebe	vee,ee"eke,Tf#f)ftf5Dg#zgagh
h'h,hLh[hXhhhhh;h{)i)i/iij7jKj,jjjj$j	j
jWjxDkJkllllJlZ>mm(nnnn	nn
nnnooo$o7oVo
eopo}o	oo
oooo
oooooppp#p'p6px|K|}}%}.}5}@G}+}}}f}8D~)}~~J<')QnKo1ApT 'H\	hr	|8Wxq)&;iU
ʆ	5
M
X?c01E Ef-3ڈ(573m<'#/5e}ъڊ+>5T"
ċ܋|?/L`!pǍٍNG	MWi:v	[$e͐ =Vs !ԑ2>/O_ߓLNiu~=
0N_x

ϕM֕b$#Ŗ.,LDyؗQXEa
˘ט

%;Tnڙ?[g3/
	Vty
ě֛+F1]>Kߜ&+RYj4y8>&=T\&i?DО<4q,D$#/<#Bfm7Ew-<\
w	ˢۢ
&S4Fϣ]ף
5@O[
m9{%ۤ
k"Y?>(g%|e.07!h7§'ɧ&9Yat
	¨HҨ,"BeHy©ԩ
/Kc
|ѪSe*~or_cYL!vȮ
?Mj%ί)
%05Vְ%.T\#e̱(()Ri
N\n,(%ճ(<$:a;ش%0AV^u
7"z.0*F/v$"͹%ԹnA08̻%+I g-
ʼ-ؼv:Og4GQcQ
fq~
	 'BO
\	gq
}	"	/9?QXey
!*
=&Ho 


g4NgTlk8P67@?&1*wF}('B#2Zr>~~[o
?X}3Mwj$EUyW7+QLpZ% ]8
VjVqUSY_O9LX
oE!;^.&%Rz"|ficDv/usIA6ytfb	C G*'s-;0r1{#q%N.h>p#`k/tc[5KH
!'	JFe<)(WIz,DB^d+A!m	|
`id9nm0Q)"Pn$"\KaMhJT 5-<]Y
:(,v\R3eC$Hx&=xG{2Olub:=S@4_a of %s! Callback %(function)r failed: %(error)s! Corrupt preferences file "%s", deleting...! Could not add book "%s" to the library! Could not add book %(book)s to collection %(collection)s! Could not add collection "%s"! Could not add file %(sourcefile)s to archive %(archivefile)s, aborting...! Could not create archive at path "%s"! Could not get cover for book "%s"! Could not load icon "%s"! Could not parse bookmarks file %s! Could not read %s! Could not remove file "%s"! Could not rename collection to "%s"! Could not save thumbnail "%(thumbpath)s": %(error)s! Extraction error: %s! Non-existant book #%i! Worker thread processing %(function)r failed: %(error)s! You need an sqlite wrapper to use the library."%s" does not appear to have a valid executable."%s" does not have a valid working directory.%(filename)s's extracted size is %(actual_size)d bytes, but should be %(expected_size)d bytes. The archive might be corrupt or in an unsupported format.%d comments%d pages%s archives%s images%s is an image viewer specifically designed to handle comic books.%s is licensed under the terms of the GNU General Public License.'%s' is disabled for archives.(Copy)...when height exceeds width...when width exceeds height7z archiveA collection by that name already exists.A copy of this license can be obtained from %sA file named '%s' already exists. Do you want to replace it?AccessedAdd _BookmarkAdd _separatorAdd a new empty collection.Add booksAdd books to '%s'.Add information about all files opened from within MComix to the shared recent files list.Add more books to the library.Add new collection?AddedAdded %(count)d new books from directory '%(directory)s'.Added books:Added new book '%(bookname)s' from directory '%(directory)s'.Adding '%s'...Adding booksAdvancedAll archivesAll booksAll filesAll imagesAlwaysAlways use this selected colour as the background colour.Always use this selected colour as the thumbnail background colour.Animated imagesAnimation mode:AppearanceArchiveArchive commentsArchive-related variables can only be used for archives.Archives are stored as ZIP files.AscendingAuto-detect (Default)Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically hide all toolbars in fullscreenAutomatically open next directoryAutomatically open the first file in the next sibling directory when flipping past the last page of the last file in a directory, or the previous directory when flipping past the first page of the first file.Automatically open the first file of the previous archive when navigating to it, instead of opening the last file of the previous archive.Automatically open the first file of the previous directory when navigating to it, instead of opening the last file of the previous directory.Automatically open the last viewed file on startupAutomatically open the next archiveAutomatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically open, on startup, the file that was open when MComix was last closed.Automatically pick a background colour that fits the viewed image.Automatically rotate images according to their metadataAutomatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.Automatically scan for new books when library is _openedAutomatically use the colour that fits the viewed image for the thumbnail background.Autorotate by heightAutorotate by widthBack ten pagesBackgroundBehaviourBest fit modeBilinearBook nameBrazilian Portuguese translationBy enabling this setting, the first page of a book will be used as application icon instead of the standard icon.Bzip2 compressed tar archiveCatalan translationChanges how images are scaled. Slower algorithms result in higher quality resizing, but longer page loading times.Changes the book cover size.Changes the sort order of the library.Clear _dialog choicesClears all dialog choices that you have previously chosen not to be asked again.CloseCloses all opened files.Co_mments...CollectionCommandCommand labelCommand line is empty.Comment extensions:Comment filesCommentsCompletely removes the selected books from the library.Continue reading from page %d?Controls how animated images should be displayed.Copies the current page to clipboard.Copies the selected book's path to clipboard.Could not add a new collection called '%s'.Could not change the name to '%s'.Could not determine library database version!Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCould not run command %(cmdlabel)s: %(exception)sCouldn't load keybindings: %sCover si_zeCreates a duplicate of the selected collection.Croatian translationCustom...Czech translationDate addedDebug optionsDeleteDelete "%s"?Delete information about recently opened files?Deletes the current file or archive from disk.Deletes the selected books from disk.Deletes the selected collection.DescendingDirectoryDisabled in archivesDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Do not ask again.Double page modeDuring a slideshow automatically open the next archiveDutch translationEdit BookmarksEdit archiveEdit external commandsEn_hance image...Enhance imageEscape key closes programExecute external commandExit from fullscreenExternal commandsExtraction and cacheExtraction of %(archivefile)s might have failed: %(error)sFileFile nameFile orderFile sizeFile-related variables can only be used for files.FilesFiles will be opened and displayed according to the sort order specified here. This option does not affect ordering within archives.Files within archives will be sorted according to the order specified here. Natural order will sort numbered files based on their natural order, i.e. 1, 2, ..., 10, while literal order uses standard C sorting, i.e. 1, 2, 34, 5.Finished reading on %(date)s, %(time)sFirst pageFit _height modeFit _size modeFit _width modeFit height modeFit size modeFit to same sizeFit to size modeFit width modeFixed height for other pages:Fixed height for wide pages:Fixed width for other pages:Fixed width for wide pages:Fli_p horizontallyFlip _verticallyFlip horizontallyFlip pages when scrolling "off the page" with the scroll wheel or with the arrow keys. It takes n consecutive "steps" with the scroll wheel or the arrow keys for the pages to be flipped.Flip pages when scrolling off the edges of the pageFlip two pages in double page modeFlip two pages, instead of one, each time we flip pages in double page mode.Flip verticallyForward ten pagesFraction of page to scroll per space key press (in percent):French translationFull pathFullscreenFullscreen modeGalician translationGerman translationGerman translation and Nautilus thumbnailerGo to pageGo to page...Greek translationGzip compressed tar archiveH_ide allHebrew translationHugeHungarian translationHyperbolic (slow)Icon designImageImage qualityImagesIncomplete escape sequence. For a literal '%', use '%%'.Incomplete quote sequence. For a literal '"', use '%"'.Indonesian translationInstalled Pillow version is: %sInvalid escape sequence: %%%sInvalid path: '%s'Invert (negate) image colors.Invert image colorsInvert smart scrollInvert smart scrolling direction.It reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKeep transformationKeeps the currently selected transformation for the next pages.Key %dKeybinding for "%(action)s" overrides hotkey for another action.Korean translationLHA archiveLabelLanguage (needs restart):LargeLast modifiedLast pageLibraryLibrary booksLibrary collectionsLibrary watch listLiteral orderLithuanian translationLocationMComix developerM_anual zoom modeMagnification factor:Magnifying LensMagnifying _lensMagnifying lensMagnifying lens size (in pixels):Maintain relative size or fit to same size.Manga modeManual zoom modeMaximum number of concurrent extraction threads:Maximum number of pages to store in the cache:Mi_nimizeMinimizeMobiPocket ebookModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNatural orderNavigationNeverNever autorotateNewNext _archiveNext archiveNext directoryNext pageNext page (always one page)Next page (dynamic)No images in '%s'No new books found in directory '%s'.No sortingNo version of GObject was found on your system.No version of the Python Imaging Library was found on your system.Non-supported archive format: %sNormalNormal (fast)Normal sizeNumber of "steps" to take before flipping the page:Number of pixels to scroll per arrow key press:Number of pixels to scroll per mouse wheel turn:Only for title pagesOnly for wide imagesOpenOpen _withOpen _without closing libraryOpen first file when navigating to previous archiveOpen first file when navigating to previous directoryOpen the selected book.Open the watchlist management dialog.Opens the archive editor.Opens the selected books for viewing.Opens the selected books, but keeps the library window open.Original vision/developer of ComixOwnerPDF documentPagePage auto-resizing:PermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Please note that the only files that are automatically added to this list are those files in archives that MComix recognizes as comments.Please refer to the external command documentation for a list of usable variables and other hints.Polish translatinPolish translationPr_eferencesPrefer same scalePrefer same sizePreferencesPreview:Previous a_rchivePrevious archivePrevious directoryPrevious pagePrevious page (always one page)Previous page (dynamic)Proper_tiesPropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.Python Imaging Library Fork (Pillow) 6.0.0 or higher is required.QuitQuits and restores the currently opened file next time the program starts.RAR archiveRe_freshRe_nameRecentRefreshReloads the currently opened files or archive.Remove books from the library?Remove from archiveRemove from the _libraryRemove from this _collectionRemoved %(num)d book from '%(collection)s'.Removed %(num)d books from '%(collection)s'.Removed %d book from the library.Removed %d books from the library.Removes no longer existant books from the collection.Removes the selected books from the current collection.Rename collection?Renames the selected collection.Replace existing bookmark on page %s?Replace existing bookmarks on pages %s?Replacing it will overwrite its contents.Required Pillow version is: 6.0.0 or higherReset to defaults.Resets all keyboard shortcuts to their default values.RootRotat_e 90 degrees CCWRotate 180 de_greesRotate 180 degreesRotate 90 degrees CCWRotate 90 degrees CWRotationRun _commandRussian translationSLIDESHOWS_aturation:S_crollbarsS_harpness:SaveSave AsSave As opens at the last directory saved intoSave _AsSave and quitSave changes to commands?Save page asSave the selected values as default for future files.Scaling modeScanning for new books...ScrollScroll downScroll leftScroll rightScroll to bottom centerScroll to bottom leftScroll to bottom rightScroll to centerScroll to middle leftScroll to middle rightScroll to top centerScroll to top leftScroll to top rightScroll upSelecting "No" will create a new bookmark without affecting the other bookmarks.Set library cover sizeSet the magnification factor of the magnifying lens.Set the max number of pages to cache. A value of -1 will cache the entire archive.Set the maximum number of concurrent threads for formats that support it.Set the number of "steps" needed to flip to the next or previous page.  Less steps will allow for very fast page turning but you might find yourself accidentally turning pages.Set the number of pixels to scroll on a page when using a mouse wheel.Set the number of pixels to scroll on a page when using the arrow keys.Set the size of the magnifying lens. It is a square with a side of this many pixels.Sets the desired output log level.Sets the percentage by which the page will be scrolled down or up when the space key is pressed.ShortcutsShow OSD panelShow file numbersShow filenameShow filesizeShow only one page where appropriate:Show page numbersShow page numbers on thumbnailsShow pathShow resolutionShow the library on startup.Show the version number and exit.Show this help and exit.Show/hide allShow/hide menubarShow/hide scrollbarsShow/hide statusbarShow/hide toolbarSimplified Chinese translationSizeSlideshowSlideshow delay (in seconds):Slideshow step (in pixels):SmallSmart scroll downSmart scroll upSort archives by:Sort files and directories by:Space between two pages (in pixels):Spanish translationSpecify the number of pixels to scroll while in slideshow mode. A positive value will scroll forward, a negative value will scroll backwards, and a value of 0 will cause the slideshow to always flip to a new page.St_atusbarStart _slideshowStart slideshowStart the application in double page mode.Start the application in fullscreen mode.Start the application in manga mode.Start the application in slideshow mode.Start the application with zoom set to best fit mode.Start the application with zoom set to fit height.Start the application with zoom set to fit width.Stop slideshowStore information about recently opened files:Store thumbnails for opened filesStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.Stretch images to fit to the screen, depending on zoom mode.Stretch small imagesSwedish translationT_oolbarsTar archiveTemporarily override the interface language.Th_umbnailsThe archive is password-protected:The current book already contains marked pages. Do you want to replace them with a new bookmark on page %d?The file will be deleted from your harddisk.The new archive could not be saved!The original files have not been removed.The selected books will be removed from the library and permanently deleted. Are you sure that you want to continue?This error might be caused by missing GTK+ libraries.This is a separator pseudo-command.This will remove all entries from the "Recent" menu, and clear information about last read pages.Thumbnail size (in pixels):ThumbnailsTinyTraditional Chinese translationTransformationTransparencyTreat all files found within archives, that have one of these file endings, as comments.TypeUkrainian translationUnknown filetypeUpgrading library database version from %(from)d to %(to)d.Use a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.Use archive thumbnail as application iconUse checkered background for transparent imagesUse dynamic background colourUse fullscreen by defaultUse smart scrollingUse this colour as background:Use this colour as the thumbnail background:User interfaceVector iconView images and comic book archives.View modeView modesWhen active, the ESC key closes the program, instead of only disabling fullscreen mode.When showing the first page of an archive, or an image's width exceeds its height, only a single page will be displayed.While in slideshow mode allow the next archive to automatically be opened.With subdirectoriesWith this preference set, the space key and mouse wheel do not only scroll down or up, but also sideways and so try to follow the natural reading order of the comic book.Working directoryXZ compressed tar archiveYou do not have the required versions of GTK+ 3.0 and PyGObject installed.You don't have the required version of the Python Imaging Library Fork (Pillow) installed.You have made changes to the list of external commands that have not been saved yet. Press "Yes" to save all changes, or "No" to discard them.You stopped reading here on %(date)s, %(time)s. If you choose "Yes", reading will resume on page %(page)d. Otherwise, the first page will be loaded.ZIP archiveZoomZoom _InZoom _OutZoom inZoom modesZoom out[OPTION...] [PATH]_About_Add_Add..._Auto-rotate image_Automatically adjust contrast_Best fit mode_Bookmarks_Brightness:_Cancel_Clean up_Close_Contrast:_Copy_Delete_Double page mode_Duplicate_Edit_Edit Bookmarks..._Edit archive..._Edit commands_File_First page_Fullscreen_Go_Go to page..._Help_Import_Invert image colors_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Normal Size_Open_Open list_Open..._Previous page_Quit_Recent_Remove_Remove and delete from disk_Reset keys_Rotate 90 degrees CW_Save and quit_Scan now_Search:_Sort_Toolbar_Tools_Transform image_View_Watch list_ZoomProject-Id-Version: MComix 2.0.0
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2024-01-21 19:55+0100
Last-Translator: Moritz Brunner 
Language-Team: 
Language: de
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Plural-Forms: nplurals=2; plural=(n != 1);
X-Language: de_DE
X-Source-Language: en
X-Generator: Poedit 3.4.2
 von %s! Callback-Funktion %(function)r fehlgeschlagen: %(error)s! Korrupte Options-Datei "%s", wird gelöscht...! Buch %s konnte nicht zur Bibliothek hinzugefügt werden! Konnte Buch %(book)s nicht zur Sammlung %(collection)s hinzufügen! Sammlung "%s" konnte nicht hinzugefügt werden! Konnte Datei %(sourcefile)s nicht zum Archiv %(archivefile)s hinzufügen, breche ab...! Konnte Archiv "%s" nicht erstellen! Konnte Deckblatt für Buch "%s" nicht laden! Icon %s kann nicht geladen werden! Lesezeichen-Speicher %s kann nicht gelesen werden! %s kann nicht gelesen werden! Konnte %s nicht entfernen! Sammlung konnte nicht zu "%s" umbenannt werden! Konnte Vorschaubild "%(thumbpath)s" nicht speichern: %(error)s! Extraktions-Fehler: %s! Nicht vorhandenes Buch #%i! Arbeiter-Thread in Bearbeitung von %(function)r fehlgeschlagen: %(error)s! Sie benötigen ein sqlite-Modul, um die Bibliothek benutzen zu können."%s" scheint keine gültige ausführbare Datei zu haben."%s" hat kein gültiges Arbeitsverzeichnis.Die entpackte Größe von %(filename)s ist %(actual_size)d Bytes, sollte aber %(expected_size)d Bytes sein. Das Archiv könnte korrupt oder in einem nicht unterstützten Format sein.%d Kommentare%d Seiten%s-Archive%s-Bilder%s ist ein Bildbetrachter, der speziell für den Umgang mit Comicbüchern entworfen wurde.%s ist unter der "GNU General Public License" lizensiert."%s" ist in Archiven deaktiviert.(Kopieren)...wenn Höhe Breite übersteigt...wenn Breite Höhe übersteigt7zip-ArchivEine Sammlung mit diesem Namen existiert bereits.Eine Kopie dieser Lizenz kann unter %s bezogen werdenEine Datei mit dem Namen »%s« existiert bereits. Möchten Sie sie ersetzen?Zugegriffen_Lesezeichen hinzufügen_Trennzeile hinzufügenEine neue, leere Sammlung hinzufügen.Bücher hinzufügenBücher zu »%s« hinzufügen.Die zuletzt innerhalb von MComix geöffneten Dateien merken, um sie später über das Menü schnell wieder öffnen zu können.Weitere Bücher zu der Bibliothek hinzufügen.Neue Sammlung hinzufügen?HinzugefügtEs wurden %(count)d neue Bücher aus dem Verzeichnis '%(directory)s' hinzugefügt.Hinzugefügte Bücher:Neues Buch '%(bookname)s' wurde aus dem Verzeichnis '%(directory)s' hinzugefügt.Füge »%s« hinzu ...Bücher hinzufügenErweitertAlle ArchiveAlle BücherAlle DateienAlle BilderImmerDiese ausgewählte Farbe immer als Hintergrundfarbe verwenden.Diese ausgewählte Farbe immer als Vorschaubild-Hintergrundfarbe verwenden.Animierte BilderAnimationsmodus:AussehenArchivArchiv-KommentareVariablen für Archive können nur in Archiven verwendet werden.Archive werden als ZIP-Dateien gespeichert.AufsteigendAutomatisch (Voreinstellung)Kontrast automatisch einstellen (sowohl Helligkeit, als auch Dunkelheit) getrennt für jedes Farbband.Bei Vollbild automatisch alle Werkzeugleisten ausblendenAutomatisch nächstes Verzeichnis öffnenAutomatisch erste Datei in benachbartem Verzeichnis öffnen, wenn nach der letzten Seite in der letzten Datei eines Verzeichnisses umgeblättert wird. Wenn in der ersten Datei eines Verzeichnisses nach vorne geblättert wird, wird das vorherige Verzeichnis geöffnet.Die erste Datei des vorherigen Archives wird automatisch geöffnet, wenn dazu gewechselt wird. Ansonsten wird die letzte Datei des Archives geöffnet.Die erste Datei des vorherigen Verzeichnisses wird automatisch geöffnet, wenn dorthin gewechselt wird. Ansonsten wird die letzte Datei im Verzeichnis geöffnet.Beim Starten automatisch die letzte angesehene Datei öffnenAutomatisch das nächste Archiv öffnenAutomatisch das nächste Archiv im Verzeichnis öffnen, wenn nach der letzten Seite umgeblättert wird oder das vorherige Archiv, wenn über die erste Seite geblättert wird.Automatisch beim Starten die letzte Datei öffnen, die offen war, als MComix das letzte Mal geschlossen wurde.Automatisch eine Hintergrundfarbe wählen, die zum angeschauten Bild passt.Bilder automatisch gemäß ihrer Metadaten drehenBilder automatisch drehen, wenn die Ausrichtung in den Metadaten, wie zum Beispiel einem Exif-Etikett des Bildes, angegeben wurde.Aut_omatisch nach Büchern suchen, wenn Bibliothek geöffnet wirdAutomatisch eine Hintergrundfarbe wählen, die zum angeschauten Vorschau-Bild passt.Automatisch nach Höhe rotierenAutomatisch nach Breite rotierenZehn Seiten zurückHintergrundVerhaltenAm besten passender ModusBilinearBuch-NamePortugiesische ÜbersetzungWenn diese Option aktiviert ist, wird anstelle des normalen Icons die erste Seite eines Buches als Anwendungsicon verwendet.Bzip2-komprimiertes Tar-ArchivKatalanische ÜbersetzungÄndert Bild-Skalierungs-Modus. Langsamere Algorithmen resultieren in besserer Bildqualität, aber längeren Ladezeiten.Ändert die Deckblattgröße der Bücher.Ändert die Sortierung der Bibliothek._Dialogantworten löschenLöscht alle Dialogantworten, bei denen Sie vorher angegeben hatten, nicht mehr gefragt werden zu wollen.SchließenSchließt alle offenen Dateien._Kommentare...SammlungBefehlProgramm-BezeichnungBefehlszeile ist leer.Kommentarerweiterungen:KommentareKommentareEntfernt die ausgewählten Bücher komplett aus der Bibliothek.Auf Seite %d weiterlesen?Steuert wie animierte Bilder dargestellt werden.Kopiert die aktuelle Seite in die Zwischenablage.Kopiert den Dateipfad des ausgewählten Buches in die Zwischenablage.Eine neue Sammlung mit Namen »%s« konnte nicht hinzugefügt werden.Name konnte nicht zu »%s« geändert werden.Konnte Bibliothek-Datenbankversion nicht bestimmen!Sammlung konnte nicht dupliziert werden.%s kann nicht geöffnet werden: Datei nicht gefunden.%s kann nicht geöffnet werden: Zugriff verweigert.%s kann nicht gelesen werdenKonnte Programm %(cmdlabel)s nicht ausführen: %(exception)sKonnte Tastaturbelegung nicht laden: %sDeckblatt_größeErstellt eine Kopie der ausgewählten Sammlung.Kroatische ÜbersetzungBenutzerdefiniert...Tschechische ÜbersetzungHinzufüge-DatumDebug-EinstellungenLöschen"%s" löschen?Liste zuletzt geöffneter Dateien löschen?Löscht die aktuelle Seite oder das Archiv von der Festplatte.Löscht die ausgewählten Bücher von der Festplatte.Löscht die ausgewählte Sammlung.AbsteigendVerzeichnisIn Archiven deaktiviertAnzeigeNur die Bücher anzeigen, die die angegebene Zeichenkette in ihrem vollständigen Pfad haben. Die Suche ist unabhängig von Groß- und Kleinschreibung.Nicht noch einmal fragen.Doppelseitiger ModusAutomatisch das nächste Archiv während einer Diaschau öffnenHolländische ÜbersetzungLesezeichen bearbeitenArchiv bearbeitenExterne Programme bearbeitenBild aufbesser_n...Bild aufbessernEscape-Taste beendet das ProgrammExternes Programm ausführenVollbildmodus verlassenExterne ProgrammeEntpacken und ZwischenspeicherEntpacken des Archivs %(archivefile)s wahrscheinlich fehlgeschlagen: %(error)sDateiDateinameDatei-ReihenfolgeDateigrößeDatei-Variablen können nur für Dateien verwendet werden.DateienDateien werden nach dem hier ausgewählten Merkmal sortiert geöffnet und angezeigt. Diese Option hat keinen Einfluss auf die Sortierung von Dateien in Archiven.Dateien in Archiven werden nach der hier angegebenen Sortierung angezeigt. Natürliche Sortierung sortiert nummerierte Dateien anhand ihrer natürlichen Reihenfolge, z.B. 1, 2, ..., 10. Buchstäbliche Sortierung benutzt die Standard-C-Reihenfolge, also 1, 2, 34, 5.Gelesen am %(date)s, um %(time)s UhrErste SeiteModus mit angepasster _HöheModus mit fester Grö_sseModus mit angepasster _BreiteModus mit angepasster HöheModus mit fester GrößeAuf gleiche Größe anpassenModus mit fester GrößeModus mit angepasster BreiteFeste Höhe für andere Seiten:Feste Höhe für breite Seiten:Feste Breite für andere Seiten:Feste Größe für breite Seiten:_Horizontal spiegeln_Vertikal spiegelnHorizontal spiegelnSeiten umblättern, wenn mit dem Mausrad oder den Pfeiltasten »von der Seite« herunter gerollt wird. Es nimmt drei aufeinander folgende »Schritte« mit dem Mausrad oder den Pfeiltasten auf, um die Seiten umzublättern.Seiten beim Rollen über den Rand der Seite hinaus umblätternZwei Seiten im doppelseitigen Modus umblätternJedesmal zwei, anstatt eine Seite umblättern, wenn im doppelseitigen Modus umgeblättert wird.Vertikal spiegelnZehn Seiten vorAnteil an Seite, um den bei Druck der Leertaste gescrollt wird (in Prozent):Französische ÜbersetzungVoller PfadVollbildVollbild-ModusGallische ÜbersetzungDeutsche ÜbersetzungDeutsche Übersetzung und Vorschaubildersteller für NautilusGehe zu SeiteGehe zu Seite...Griechische ÜbersetzungGzip-komprimiertes Tar-ArchivAlle verstec_kenHebräische ÜbersetzungRiesigUngarische ÜbersetzungHyperbolisch (langsam)Symbol-DesignBildBildqualitätBilderUnvollständige Escape-Sequenz. Benutze '%%', um das '%'-Zeichen zu erhalten.Anführungszeichen nicht geschlossen. Benutze %", um ein Anführungszeichen im Befehl zu erhalten.Indonesische ÜbersetzungInstallierte Pillow-Version ist: %sUngültige Escape-Sequenz: %%%sUngültiger Pfad: '%s'Farben invertieren (umkehren).Farben invertierenIntelligentes Rollen umkehrenRichtung des intelligenten Rollens umkehren.Es liest ZIP-, RAR- und Tar-Archive ebenso wie einfache Bilddateien.Italienische ÜbersetzungJapanische ÜbersetzungVeränderung behaltenBehält die derzeit ausgewählten Transformationen für die nächsten Seiten bei.Taste %dTaste für "%(action)s" überschreibt andere Aktion mit selber Taste.Koreanische ÜbersetzungLHA-ArchivBezeichnungSprache (erfordert Neustart):GroßZuletzt geändertLetzte SeiteBibliothekBücher in BibliothekSammlungen in BibliothekÜberwachte VerzeichnisseBuchstäbliche SortierungLitauische ÜbersetzungOrtEntwickler von MComixM_anueller VergrößerungsmodusVergrößerungsfaktor:Lupe_LupeLupeLupengröße (in Pixeln):Relative Größe beibehalten oder auf gleiche Größe anpassen.Manga-ModusManueller VergrößerungsmodusMaximale Anzahl gleichzeitig gestarteter Entpacker:Maximale Anzahl von Seiten im Zwischenspeicher:Mi_nimierenMinimierenMobiPocket E-BookGeändertBücher von »%(source collection)s« nach »%(destination collection)s« verschieben.NameNatürliche SortierungNavigationNiemalsNiemals automatisch rotierenNeuNächstes _ArchivNächstes ArchivNächstes VerzeichnisNächste SeiteNächste Seite (immer einzeln)Nächste Seite (dynamisch)Keine Bilder in »%s«Keine neuen Bücher im Verzeichnis '%s' gefunden.Keine SortierungKeine GObject-Version konnte auf Ihrem System gefunden werden.Keine Python Image Library-Version konnte auf Ihrem System gefunden werden.Nicht unterstütztes Archiv-Format: %sNormalNormal (schnell)OrginalgrößeAnzahl von "Schritten", bevor Seite gewechselt wird:Anzahl von Pixeln, um die bei Knopfdruck gescrollt wird:Anzahl von Pixeln, um die bei Mausrad-Bewegung gescrollt wird:Nur für TitelblätterNur für breite BilderÖffnenÖ_ffnen mitÖffnen _ohne Bibliothek zu schließenErste Datei öffnen, wenn zum vorherigen Archiv gewechselt wirdErste Datei öffnen, wenn zum vorherigen Verzeichnis gewechselt wirdDas ausgewählte Buch öffnen.Öffnet den Editor zum Verwalten überwachter Verzeichnisse.Öffnet den Archiv-Editor.Öffnet die ausgewählten Bücher zum Lesen.Öffnet die ausgewählten Bücher, aber lässt die Bibliothek offen.Ursprüngliche Entwicklung von ComixEigentümerPDF-DokumentSeiteSeitengröße automatisch anpassen:RechtePersische ÜbersetzungBitte geben Sie einen Namen für die neue Sammlung ein.Bitte geben Sie einen neuen Namen für die ausgewählte Sammlung ein.Bitte beachten Sie, dass nur Dateien in Archiven automatisch zu dieser Liste hinzugefügt werden, die MComix als Kommentare erkennt.Die Dokumentation für externe Befehle enthält eine Liste aller Variablen und Hinweise zur Benutzung.Polnische ÜbersetzungPolnische ÜbersetzungEinstell_ungenGleiches Verhältnis bevorzugenGleiche Größe bevorzugenEinstellungenVorschau:Vorheriges A_rchivVorheriges ArchivVorheriges VerzeichnisVorherige SeiteVorherige Seite (immer einzeln)Vorherige Seite (dynamisch)Eigenschaf_tenEigenschaftenDie Sammlung »%(subcollection)s« in die Sammlung »%(supercollection)s« stellen.Python Imaging Library Fork (Pillow) 6.0.0 oder höher wird benötigt.BeendenBeendet das Programm und stellt die aktuell geöffnete Datei beim nächsten Start wieder her.RAR-Archiv_Aktualisieren_UmbenennenZuletzt geöffnetAktualisierenLäd die aktuell geöffneten Dateien oder das Archiv neu.Bücher aus der Bibliothek entfernen?Aus Archiv entfernenAus _Bibliothek entfernenAus _Sammlung entfernen%(num)d Buch wurde aus »%(collection)s« entfernt.%(num)d Bücher wurden aus »%(collection)s« entfernt.%d Buch wurde aus der Bibliothek entfernt.%d Bücher wurden aus der Bibliothek entfernt.Entfernt Bücher aus der Bibliothek, die nicht mehr existieren.Entfernt die ausgewählten Bücher von der aktuellen Sammlung.Sammlung umbenennen?Benennt die ausgewählte Sammlung um.Existierendes Lesezeichen auf Seite %s ersetzen?Existierende Lesezeichen auf den Seiten %s ersetzen?Durch Ersetzen geht ihr alter Inhalt verloren.Benötigte Pillow-Version ist: 6.0.0 oder höherAuf Voreinstellung zurücksetzen.Setzt alle Tastenbelegungen auf Standard-Werte zurück.Wurzel90 Grad gegen den Uhr_zeigersinn drehen180 Grad _drehen180 Grad drehen90 Grad gegen den Uhrzeigersinn drehen90 Grad im Uhrzeigersinn drehenDrehungBefehl _ausführenRussische ÜbersetzungDIASCHAUS_ättigung:Ro_llleisteS_chärfe:SpeichernSpeichern unter"Speichern unter" öffnet das zuletzt zum Speichern benutzte VerzeichnisSpeichern _unterSpeichern und beendenÄnderungen an Befehlen speichern?Seite speichern alsDie ausgewählten Werte als Standard für zukünftige Dateien verwenden.Skalierungs-ModusSuche nach neuen Büchern...RollenNach unten rollenNach links rollenNach rechts rollenNach unten zur Mitte rollenNach links unten rollenNach unten rechts rollenZentrierenZur Mitte links rollenZur Mitte rechts rollenNach oben zur Mitte rollenNach links oben rollenNach oben rechts rollenNach oben rollen"Nein" erzeugt ein neues Lesezeichen, ohne die anderen Lesezeichen zu beeinflussen.Deckblattgröße ändernDen Vergrößerungsfaktor der Lupe setzen.Maximale Anzahl von Seiten im Zwischenspeicher. Ein Wert von -1 hält das komplette Archiv im Zwischenspeicher.Setzt die Anzahl maximal gleichzeitig gestarteter Threads zum Entpacken von Dateiformaten, die dies unterstützen.Setzt die Anzahl von "Schritten", die notwendig sind, um auf die nächste oder vorhergehende Seite zu wechseln. Weniger Schritte erlauben schnellen Seitenwechsel, aber können dazu führen, dass ein Seitenwechsel aus Versehen zu früh ausgelöst wird.Setzt die Anzahl von Pixeln, um die eine Seite gescrollt wird, wenn das Mausrad benutzt werden.Setzt die Anzahl von Pixeln, um die eine Seite gescrollt wird, wenn die Pfeiltasten benutzt werden.Die Größe der Lupe setzen. Es ist ein Quadrat mit einer Seite von dieser Anzahl Pixeln.Setzt den gewünschten Log-Level.Setzt den prozentualen Anteil, um den eine Seite nach unten oder oben gerollt wird, wenn die Leertaste betätigt wird.TastenkürzelInformationsbereich anzeigenDateinummer anzeigenDateiname anzeigenDateigröße anzeigenNur eine Seite zeigen, wo angebracht:Seitennummer anzeigenSeitennummer auf Vorschaubildern anzeigenPfad anzeigenAuflösung anzeigenZeigt die Bibliothek beim Starten an.Zeigt die Versionsnummer an und beendet das Programm.Zeigt diesen Hilfetext an.Zeige/verstecke allesZeige/verstecke MenübarZeige/verstecke RollleistenZeige/verstecke StatusleisteZeige/verstecke WerkzeugleisteVereinfachte chinesische ÜbersetzungGrößeDiaschauDiaschauverzögerung (in Sekunden):Diaschau-Scrolling (in Pixel):KleinIntelligent nach unten rollenIntelligent nach oben rollenSortiere Archive nach:Sortiere Dateien und Verzeichnisse nach:Abstand zwischen zwei Seiten (in Pixel):Spanische ÜbersetzungGibt die Anzahl von Pixel an, um die im Diaschau-Modus gescrollt wird. Ein positiver Wert scrollt nach vorne, ein negativer Wert scrollt zurück. Ein Wert von 0 führt dazu, dass immer sofort zur nächsten Seite gewechselt wird.St_atusleisteDiaschau _startenDiaschau startenStartet die Anwendung im Doppelseiten-Modus.Startet die Anwendung im Vollbild-Modus.Startet die Anwendung im Manga-Modus.Startet die Anwendung im Diaschau-Modus.Startet die Anwendung im automatisch angepassten Zoom-Modus.Startet die Anwendung im Zoom-Modus mit angepasster Höhe.Startet die Anwendung im Zoom-Modus mit angepasster Breite.Diaschau anhaltenZuletzt geöffnete Dateien speichern:Vorschaubilder für geöffnete Dateien speichernVorschaubilder für geöffnete Dateien gemäß der Spezifikation von freedesktop.org speichern. Diese Vorschaubilder werden mit vielen anderen Anwendungen, zumeist Dateimanagern, gemeinsam benutzt.Streckt kleine Bilder abhängig vom Zoom-Modus so, dass sie den Bildschirm ausfüllen.Kleine Bilder streckenSchwedische Übersetzung_WerkzeugleistenTar-ArchivSprache der Benutzeroberfläche vorübergehend ändern._VorschaubilderDas Archiv ist passwortgeschützt:Das geöffnete Buch enthält bereits markierte Seiten. Wollen Sie diese mit einem neuen Lesezeichen auf Seite %d ersetzen?Die Datei wird von Ihrer Festplatte gelöscht.Das neue Archiv konnte nicht gespeichert werden!Die Originaldateien wurden nicht entfernt.Die ausgewählten Bücher werden aus der Bibliothek entfernt und die Originaldatei wird gelöscht. Sind Sie sicher, dass Sie fortfahren möchten?Dieser Fehler kann durch fehlende GTK+-Bibliotheken verursacht werden.Das ist ein Pseudo-Befehl für eine Trennzeile.Dies entfernt alle Einträge aus dem "Zuletzt geöffnet"-Menü, und löscht Informationen zu zuletzt gelesenen Seiten.Vorschaubildergröße (in Pixeln):VorschaubilderWinzigTraditionell-chinesische ÜbersetzungBild-VeränderungTransparenzAlle in den Archiven gefundenen Dateien, die eine dieser Erweiterungen haben, werden als Kommentare behandelt.TypUkrainische ÜbersetzungUnbekannter DateitypAktualisiere Bibliothek-Datenbankversion von %(from)d auf %(to)d.Einen grauen, karierten Hintergrund für transparente Bilder benutzen. Wenn diese Einstellung nicht gesetzt ist, ist der Hintergrund stattdessen einfach weiß.Archiv-Vorschaubild als Anwendungsicon verwendenKarierten Hintergrund für transparente Bilder verwendenDynamische Hintergrundfarbe verwendenAls Vorgabe Vollbild benutzenIntelligentes Rollen benutzenFarbe als Hintergrund verwenden:Farbe als Vorschaubild-Hintergrund verwenden:BenutzeroberflächeVektor-SymbolBetrachter für Bilder und Comicbuch-Archive.AnsichtVorgabemodiWenn aktiv, wird das Programm durch Drücken der Escape-Taste beendet. Ansonsten wird nur der Vollbildmodus verlassen.Wenn die erste Seite in einem Archiv angezeigt wird, oder ein Bild breiter als hoch ist, wird nur eine einzelne Seite dargestellt.Öffnet automatisch das nächste Archiv im Diaschau-Modus.Mit UnterverzeichnissenWenn diese Einstellung aktiv ist, rollen die Leertaste und das Mausrad nicht nur nach unten oder oben, sondern auch seitwärts. Damit wird versucht, dem natürlichen Lesefluss eines Comicbuches zu folgen.ArbeitsverzeichnisXZ-komprimiertes Tar-ArchivSie haben nicht die notwendigen Versionen von GTK+ 3.0 und PyGObject installiert.Sie haben nicht die notwendigen Version des Python Imaging Library Fork (Pillow).Sie haben Änderung an der Liste der externen Befehle vorgenommen, die noch nicht gespeichert wurden. Verwenden Sie "Ja", um alle Änderungen zu speichern, und "Nein", um sie zu verwerfen.Sie haben zuletzt am %(date)s um %(time)s hier aufgehört zu lesen. Wenn Sie "Ja" wählen, wird Seite %(page)d geöffnet. Ansonsten wird die erste Seite geladen.ZIP-ArchivVergrößern_Heranzoomen_WegzoomenVergrößernZoom-ModiVerkleinern[OPTION...] [PFAD]_Über_Hinzufügen_Hinzufügen..._Automatisch rotierenKontrast _automatisch einstellenAm besten _passender ModusLesezei_chen_Helligkeit:_Abbrechen_SäubernS_chließen_Kontrast:Kopieren_Löschen_Doppelseitiger Modus_DuplizierenBea_rbeitenLesezeichen _bearbeiten...Ar_chive bearbeiten ..._Externe Programme bearbeitenDate_i_Erste Seite_Vollbild_Gehe_Gehe zu Seite..._Hilfe_ImportierenFarben _invertierenVeränderung _behalten_Letzte SeiteBibliothe_k ..._Manga-Modus_Menüleiste_Nächste Seite_Normale GrößeÖ_ffnenAuswahl ö_ffnenÖ_ffnen ..._Vorherige Seite_Beenden_Zuletzt geöffnet_EntfernenAus Bibliothek entfernen und _löschen_Tasten zurücksetzen90 Grad im _Uhrzeigersinn drehen_Speichern und beendenJetzt _suchen_Suche:_Sortierung_Werkzeugleiste_WerkzeugeBild ändernAnsic_ht_VerzeichnisseVer_größern././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/de/__init__.py0000644000175000017500000000000014476523373020311 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/el/0000755000175000017500000000000014553265237016220 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/el/LC_MESSAGES/0000755000175000017500000000000014553265237020005 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/el/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022106 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/el/LC_MESSAGES/mcomix.mo0000644000175000017500000001426514476523373021650 0ustar00moritzmoritzY	
	 )=FN_
q|+
	
*	8	J		f	p	v								
				




'
:
G

S
a

m
x





	




+
7Bbn
u

.4JSYq_4
6S`wB-''4*E%p<@;,?h%!n$'#,7!Df*$#".7f#$#;_s&&>0N#"*:;v	+
*C(S|( $(
M%[	C:;
E74NY3?I6G.T*1PWK$%S 
-#8M,2UJ@V9L/)OF="H!&'>	50R+BQ(<AXDAccessedAdvancedAll filesAll imagesArchiveBehaviourBilinearBrazilian Portuguese translationBzip2 compressed tar archiveCatalan translationCommentsDisplayDouble page modeDutch translationFirst pageFit _height modeFit _width modeFit height modeFit width modeFli_p horizontallyFlip _verticallyFrench translationGerman translation and Nautilus thumbnailerGo to pageGo to page...Greek translationGzip compressed tar archiveH_ide allImageItalian translationLast pageLibraryLocationMagnification factor:Magnifying _lensMagnifying lensManga modeManual zoom modeModifiedNext pageOpenOwnerPagePermissionsPolish translationPr_eferencesPreferencesPrevious pageProper_tiesPropertiesRAR archiveRotat_e 90 degrees CCWRotate 180 de_greesS_crollbarsScrollSimplified Chinese translationSlideshowSpanish translationSt_atusbarStretch small imagesTar archiveTh_umbnailsThumbnailsTraditional Chinese translationZIP archive_About_Bookmarks_Close_Double page mode_Edit_File_First page_Fullscreen_Go_Go to page..._Help_Keep transformation_Last page_Manga mode_Menubar_Next page_Open..._Previous page_Quit_Rotate 90 degrees CW_Toolbar_View_ZoomProject-Id-Version: PACKAGE VERSION
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2006-09-27 10:18+0200
Last-Translator: Paul Chatzidimitriou 
Language-Team: LANGUAGE 
Language: 
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Generated-By: pygettext.py 1.5
Η πρόσβαση πραγματοποιήθηκεΠροχωρημένοΌλα τα αρχείαΌλες οι εικόνεςΑρχείοΣυμπεριφοράΔιγραμμικόςΒραζιλιάνικη/Πορτογαλική μετάφρασηΣυμπιεσμένο αρχείο tar bzip2Καταλανική μετάφρασηΣχόλιαΕμφάνισηΠροβολή διπλής σελίδαςΟλλανδική μετάφρασηΠρώτη σελίδαΠροσαρμοζμένη κατά _ύψος προβολήΠροσαρμοζμένη κατά _πλάτος προβολήΠροσαρμοζμένη κατά ύψος προβολήΠροσαρμοζμένη κατά πλάτος προβολήΟριζόντια αναστροφήΚάθετη αναστροφήΓαλλική μετάφρασηΓερμανική μετάφραση και υπεύθυνος προεπισκοπήσεων στο NautilusΠήγαινε στην σελίδαΠήγαινε στην σελίδα...Ελληνική μετάφρασηΣυμπιεσμένο αρχείο tar gzipΑπόκρυψη όλωνΕικόναΙταλική μετάφρασηΤελευταία σελίδαΒιβλιοθήκηΤοποθεσίαΠαράγοντας μεγέθυνσης:Μεγενθυτικοί _φακοίΜεγενθυτικοί φακοίΠροβολή MagnaΧειροκίνητη προβολή ζουμΤροποποιήθηκεΕπόμενη σελίδαΆνοιγμαΙδιοκτήτηςΣελίδαΆδειες χρήσηςΠολωνική μετάφρασηΠρ_οτιμήσειςΠροτιμήσειςΠροηγούμενη σελίδαΙδιότη_τεςΙδιότητεςΑρχείο RARΠεριστροφ_ή 180 μοιρώνΠεριστροφή 180 μοι_ρώνΓ_ραμμή κύλισηςΚύλισηΑπλουστευμένη Κινεζική μετάφρασηΠροβολή εναλλαγής εικόνωνΙσπανική μετάφρασηΓρ_αμμή κατάστασηςΤέντωμα μικρών εικόνωνΑρχείο tarΜι_κρογραφίεςΠροεπισκοπήσειςΠαραδοσιακή Κινέζικη μετάφρασηΑρχείο ZIP_Περί_Σελιδοδείκτες_Κλείσιμο_Προβολή διπλής σελίδας_Επεξεργασία_Αρχείο_Πρώτη σελίδα_Πλήρης οθόνη_Πήγαινε_Πήγαινε στην σελίδα..._Βοήθεια_Διατήρηση μετατροπής_Τελευταία σελίδα_Προβολή Magna_Μενού_Επόμενη σελίδα_Άνοιγμα..._Προηγούμενη σελίδα_Έξοδος_Περιστροφή 90 μοιρών_Εργαλειοθήκη_Προβολή_Ζουμ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/el/__init__.py0000644000175000017500000000000014476523373020321 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/es/0000755000175000017500000000000014553265237016227 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/es/LC_MESSAGES/0000755000175000017500000000000014553265237020014 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/es/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022115 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/es/LC_MESSAGES/mcomix.mo0000644000175000017500000011640014476523373021651 0ustar00moritzmoritz
Kl$$)$,$+$(#%:L%%K%'%#&?&#Z&~&&%&5&'"'0:'k'((B(A\((
()(.(<	)F)
O)])	y))Z))*$*9**d*=q****	*	*
**9*C4+
x++!+	++]+-+,!Y,{,2L-#--S5.B.7.k/8p/U/
/	
0
0"0	+0 50qV000r0l1&1P122
'222
F2T27]22%2-2+3"43-W33 3%3334/(4X4	m4w4
4
44/4.4%5 45
U5	`5j5rr55566?6Q6`6m6
666	6
6	666&f7
77777
77788+8E8X8i83$9"X9L{9<9:	:
":-:=:R:+e:
:::	::::;;#;
);7;>;U;h;!|;A;;;?	<@I<<<<<
<	<<
<<=='=8=J=`=p==!=
==.=	=>H>Z>_>e>
i>w>>	>>%>
>B> #?D?
K?Y?3e?/?0??@$@)@G@%_@@%@<@"A%A+A0AXg}%+ۅ)1&G$n%+)N&&u.xˇDY
`gdȉ,-wZ Ҋ-)W/s ċ'
!)KS(k#&'((9O/d3%Ȏ0CBcC:-:hRFʑ,.(,W~=9A&{
 
ד~diS6s; "*J+f(	ϖwٖQlٗFV&Й
ߙ


)4=%Io		ȚҚ	&/@SWk	r|›қ			)3#Os
ʜޜY"eYb1^>]f`cqF|rC7k_	[26Sm+&d{ ~q]aK=Zi!V|E3d;.THJ%/r*\HM:Wm'V2zQ/pUl?DQnoy
+u*P6O-'L`c?G,h#9Ix5EP4K8GzX@j4Xx :@&~^N)}
SkbRCoLhB#t$F5
NIvs7Mw><li-BTwge{)<u!8$=WU0%9a}A
j[pZ0OD3;v.	(R(yA1nJ"stg_\,f of %s! Callback %(function)r failed: %(error)s! Corrupt preferences file "%s", deleting...! Could neither find pysqlite2 nor sqlite3.! Could not add book "%s" to the library! Could not add book %(book)s to collection %(collection)s! Could not add collection "%s"! Could not add file %(sourcefile)s to archive %(archivefile)s, aborting...! Could not create archive at path "%s"! Could not get cover for book "%s"! Could not load icon "%s"! Could not parse bookmarks file %s! Could not read %s! Could not remove file "%s"! Could not rename collection to "%s"! Could not save thumbnail "%(thumbpath)s": %(error)s! Extraction error: %s! Non-existant book #%i! You need an sqlite wrapper to use the library.%(filename)s's extracted size is %(actual_size)d bytes, but should be %(expected_size)d bytes. The archive might be corrupt or in an unsupported format.%d comments%d pages%s is an image viewer specifically designed to handle comic books.%s is licensed under the terms of the GNU General Public License.(Copy)7z archiveA collection by that name already exists.A copy of this license can be obtained from %sA file named '%s' already exists. Do you want to replace it?AccessedAdd _BookmarkAdd a new empty collection.Add booksAdd books to '%s'.Add information about all files opened from within MComix to the shared recent files list.Add more books to the library.Add new collection?AddedAdded %(count)d new books from directory '%(directory)s'.Added books:Added new book '%(bookname)s' from directory '%(directory)s'.Adding '%s'...Adding booksAdvancedAll booksAll filesAll imagesAlwaysAlways use this selected colour as the background colour.Always use this selected colour as the thumbnail background colour.AppearanceArchiveArchives are stored as ZIP files.AscendingAuto-detect (Default)Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically hide all toolbars in fullscreenAutomatically open next directoryAutomatically open the first file in the next sibling directory when flipping past the last page of the last file in a directory, or the previous directory when flipping past the first page of the first file.Automatically open the last viewed file on startupAutomatically open the next archiveAutomatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically open, on startup, the file that was open when MComix was last closed.Automatically pick a background colour that fits the viewed image.Automatically rotate images according to their metadataAutomatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.Automatically scan for new books when library is _openedAutomatically use the colour that fits the viewed image for the thumbnail background.BackgroundBehaviourBest fit modeBilinearBook nameBrazilian Portuguese translationBy enabling this setting, the first page of a book will be used as application icon instead of the standard icon.Bzip2 compressed tar archiveCatalan translationChanges how images are scaled. Slower algorithms result in higher quality resizing, but longer page loading times.Changes the book cover size.Changes the sort order of the library.Clears all dialog choices that you have previously chosen not to be asked again.Closes all opened files.Co_mments...CollectionComment extensions:Comment filesCommentsCompletely removes the selected books from the library.Continue reading from page %d?Copies the current page to clipboard.Copies the selected book's path to clipboard.Could not add a new collection called '%s'.Could not change the name to '%s'.Could not determine library database version!Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCouldn't load keybindings: %sCover si_zeCreates a duplicate of the selected collection.Croatian translationCustom...Czech translationDate addedDebug optionsDelete "%s"?Delete information about recently opened files?Deletes the current file or archive from disk.Deletes the selected books from disk.Deletes the selected collection.DescendingDirectoryDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Do not ask again.Double page modeDuring a slideshow automatically open the next archiveDutch translationEdit BookmarksEdit archiveEn_hance image...Enhance imageEscape key closes programExit from fullscreenFile nameFile orderFile sizeFilesFiles will be opened and displayed according to the sort order specified here. This option does not affect ordering within archives.Finished reading on %(date)s, %(time)sFirst pageFit _height modeFit _size modeFit _width modeFit height modeFit to heightFit to size modeFit to widthFit to width or height:Fit width modeFixed size for this mode:Fli_p horizontallyFlip _verticallyFlip pages when scrolling "off the page" with the scroll wheel or with the arrow keys. It takes n consecutive "steps" with the scroll wheel or the arrow keys for the pages to be flipped.Flip pages when scrolling off the edges of the pageFlip two pages in double page modeFlip two pages, instead of one, each time we flip pages in double page mode.Fraction of page to scroll per space key press (in percent):French translationFull pathFullscreenFullscreen modeGalician translationGerman translationGerman translation and Nautilus thumbnailerGo to page...Greek translationGzip compressed tar archiveH_ide allHebrew translationHugeHungarian translationHyperbolic (slow)Icon designImageImage qualityImagesIndonesian translationInvalid path: '%s'Invert smart scrollInvert smart scrolling direction.It reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKeeps the currently selected transformation for the next pages.Keybinding for "%(action)s" overrides hotkey for another action.Korean translationLHA archiveLanguage (needs restart):LargeLast modifiedLast pageLibraryLibrary booksLibrary collectionsLibrary watch listLocationMComix developerM_anual zoom modeMagnification factor:Magnifying LensMagnifying _lensMagnifying lensMagnifying lens size (in pixels):Manga modeManual zoom modeMaximum number of pages to store in the cache:Mi_nimizeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNeverNewNext _archiveNext archiveNext directoryNext pageNo images in '%s'No new books found in directory '%s'.No sortingNo version of the Python Imaging Library was found on your system.Non-supported archive format: %sNormalNormal (fast)Normal sizeNumber of "steps" to take before flipping the page:Number of pixels to scroll per arrow key press:Number of pixels to scroll per mouse wheel turn:Only for title pagesOnly for wide imagesOpenOpen _without closing libraryOpen the selected book.Open the watchlist management dialog.Opens the archive editor.Opens the selected books for viewing.Opens the selected books, but keeps the library window open.Original vision/developer of ComixOwnerPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Please note that the only files that are automatically added to this list are those files in archives that MComix recognizes as comments.Polish translatinPolish translationPr_eferencesPreferencesPrevious a_rchivePrevious archivePrevious directoryPrevious pageProper_tiesPropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.Quits and restores the currently opened file next time the program starts.RAR archiveRe_freshRe_nameRecentReloads the currently opened files or archive.Remove books from the library?Remove from archiveRemove from the _libraryRemove from this _collectionRemoved %(num)d book from '%(collection)s'.Removed %(num)d books from '%(collection)s'.Removed %d book from the library.Removed %d books from the library.Removes no longer existant books from the collection.Removes the selected books from the current collection.Rename collection?Renames the selected collection.Replace existing bookmark on page %s?Replace existing bookmarks on pages %s?Replacing it will overwrite its contents.Reset to defaults.RootRotat_e 90 degrees CCWRotate 180 de_greesRotationRussian translationSLIDESHOWS_aturation:S_crollbarsS_harpness:SaveSave _AsSave page asSave the selected values as default for future files.Scaling modeScanning for new books...ScrollScroll downScroll leftScroll rightScroll to bottom centerScroll to bottom leftScroll to bottom rightScroll to centerScroll to middle leftScroll to middle rightScroll to top centerScroll to top leftScroll to top rightScroll upSelecting "No" will create a new bookmark without affecting the other bookmarks.Set library cover sizeSet the magnification factor of the magnifying lens.Set the max number of pages to cache. A value of -1 will cache the entire archive.Set the number of "steps" needed to flip to the next or previous page.  Less steps will allow for very fast page turning but you might find yourself accidentally turning pages.Set the number of pixels to scroll on a page when using a mouse wheel.Set the number of pixels to scroll on a page when using the arrow keys.Set the size of the magnifying lens. It is a square with a side of this many pixels.Sets the desired output log level.Sets the percentage by which the page will be scrolled down or up when the space key is pressed.Show OSD panelShow file numbersShow filenameShow only one page where appropriate:Show page numbersShow page numbers on thumbnailsShow pathShow resolutionShow the library on startup.Show the version number and exit.Show this help and exit.Simplified Chinese translationSizeSlideshowSlideshow delay (in seconds):Slideshow step (in pixels):SmallSmart scroll downSmart scroll upSpanish translationSpecify the number of pixels to scroll while in slideshow mode. A positive value will scroll forward, a negative value will scroll backwards, and a value of 0 will cause the slideshow to always flip to a new page.St_atusbarStart _slideshowStart slideshowStart the application in double page mode.Start the application in fullscreen mode.Start the application in manga mode.Start the application in slideshow mode.Start the application with zoom set to best fit mode.Start the application with zoom set to fit height.Start the application with zoom set to fit width.Stop slideshowStore information about recently opened files:Store thumbnails for opened filesStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.Stretch images to fit to the screen, depending on zoom mode.Stretch small imagesSwedish translationT_oolbarsTar archiveTh_umbnailsThe file will be deleted from your harddisk.The new archive could not be saved!The original files have not been removed.The selected books will be removed from the library and permanently deleted. Are you sure that you want to continue?This error might be caused by missing GTK+ libraries.This will remove all entries from the "Recent" menu, and clear information about last read pages.Thumbnail size (in pixels):ThumbnailsTinyTraditional Chinese translationTransparencyTreat all files found within archives, that have one of these file endings, as comments.TypeUkrainian translationUnknown filetypeUpgrading library database version from %(from)d to %(to)d.Use a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.Use archive thumbnail as application iconUse checkered background for transparent imagesUse dynamic background colourUse fullscreen by defaultUse smart scrollingUse this colour as background:Use this colour as the thumbnail background:User interfaceView images and comic book archives.View modesWhen active, the ESC key closes the program, instead of only disabling fullscreen mode.When showing the first page of an archive, or an image's width exceeds its height, only a single page will be displayed.While in slideshow mode allow the next archive to automatically be opened.With subdirectoriesWith this preference set, the space key and mouse wheel do not only scroll down or up, but also sideways and so try to follow the natural reading order of the comic book.You stopped reading here on %(date)s, %(time)s. If you choose "Yes", reading will resume on page %(page)d. Otherwise, the first page will be loaded.ZIP archiveZoom _InZoom _OutZoom inZoom modesZoom out[OPTION...] [PATH]_About_Add_Add..._Automatically adjust contrast_Best fit mode_Bookmarks_Brightness:_Cancel_Clean up_Close_Contrast:_Copy_Delete_Double page mode_Duplicate_Edit_Edit Bookmarks..._Edit archive..._File_First page_Fullscreen_Go_Go to page..._Help_Import_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Normal Size_Open_Open..._Previous page_Quit_Recent_Remove_Remove and delete from disk_Rotate 90 degrees CW_Save and quit_Scan now_Search:_Sort_Toolbar_Tools_Transform image_View_Watch list_ZoomProject-Id-Version: comix.HEAD
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2012-09-06 18:00+0100
Last-Translator: Carlos Feliu 
Language-Team: Spanish 
Language: es
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Generated-By: pygettext.py 1.5
X-Generator: KBabel 1.9.1
Plural-Forms: nplurals=2; plural=(n != 1);
 de %s! %(function)r callback fallida: %(error)s! Archivo de preferencias "%s" corrupto, eliminando...! No se pudieron encontrar ni pysqlite2 ni sqlite3.! No se puedo añadir el libro "%s" a la biblioteca! No se puedo añadir el libro %(book)s a la colección %(collection)s! No se pudo añadir la colección "%s"! No se pudo añadir el archivo %(sourcefile)s al archivador %(archivefile)s, abortando...! No se pudo crear un archivador en la ruta "%s"! No se pudo conseguir la portada para el libro "%s"! No se pudo cargar el icono "%s"! No se pudo analizar el archivo de marcadores %s! No se pudo leer %s!  No se pudo eliminar  el archivo "%s"! No se pudo renombrar la colección a "%s"! No se puedo guardar la miniatura "%(thumbpath)s": %(error)s! Error de extración: %s! Libro no existente #%i! Necesita un envoltorio sqlite para usar la librería.El tamaño de %(filename)s al ser extraído es de %(actual_size)d bytes, pero debería ser de %(expected_size)d bytes. El archivador puede estar corrupto o en un formato no soportado.%d comentarios%d páginas%s es un visor de imágenes específicamente diseñado para cómics.%s utiliza la licencia general pública GNU.(Copia)Archivador 7zYa existe una colección con este nombre.Una copia de esta licencia puede ser obtenida en %sYa existe un archivo de nombre '%s'. ¿Desea reemplazarlo?AccedidoAñadir _marcadorAgregar una nueva colección vacía.Agregar librosAgregar libros a '%s'.Agregar información sobre todos los archivos abiertos desde MComix a la lista compartida de archivos recientes.Agregar más libros a la biblioteca.¿Agregar una nueva colección?AñadidoAñadido/s %(count)d nuevo/s libro(s) desde la carpeta '%(directory)s'.Libros agregados:Se añadió el nuevo libro '%(bookname)s' desde la carpeta '%(directory)s'.Agregando '%s'...Agregando librosAvanzadoTodos los librosTodos los archivosTodas las imágenesSiempreUsar siempre el color seleccionado como color de fondo.Usar siempre el color seleccionado como color de fondo.AparienciaArchivadorLos archivadores se guardan como paquetes ZIP.AscendienteAuto-detectar (por defecto)Ajusta el contraste automáticamente, tanto las partes claras como las oscuras, para cada banda de color por separado.Ocultar automáticamente todas las barras de herramientos en pantalla completaAbrir automáticamente la carpeta siguienteAbrir automáticamente el primer archivo en el siguiente carpeta hermana cuando se pase la última página del último archivo en una carpeta, o en la anterior carpeta cuando se pase hacia atrás la primera página del primer archivo.Abrir automáticamente el último archivo visto al iniciarAbrir automáticamente el archivador siguienteAbre automáticamente el archivador siguiente en la carpeta, cuando se pasa la última página, o el archivador anterior cuando se pasa la primera.Abre automáticamente, cuando la aplicación se inicia, el archivo que se encontraba abierto la última vez que MComix fue cerrado.Obtiene automáticamente un color de fondo que se ajusta a la imagen presentada.Rotar imágenes automáticamente en función de sus metadatosRotar imágenes automáticamente cuando se especifique una orientación en sus metadatos, por ejemplo en una etiqueta Exif.Buscar nuevos libros automáticamente al abrir la bibliotecaUsar automáticamente el color de fondo que se ajuste a la imagen presentada para el fondo de la miniatura.FondoComportamientoModo de mejor ajusteBilinearNombre del libroTraducción al portugués brasileñoHabilitando esta opción, la primera página del libro será utilizada como icono de aplicación en lugar del icono estándar.Archivador tar comprimido con bzip2Traducción al catalánCambia el escalado de las imágenes. Algoritmos más lentos resultan en tamaños de más calidad, pero mayor tiempo de carga de página.Cambia el tamaño de la portada del libro.Cambia la ordenación de la biblioteca.Elimina todas las opciones de diálogo previamente elegidas para no ser preguntadas de nuevo.Cierra todos los archivos abiertos.Co_mentarios...ColecciónExtensiones de los comentarios:ComentariosComentariosElimina completamente los libros seleccionados de la biblioteca.¿Continuar leyendo a partir de la página %d?Copia la página actual al portapapeles.Copia la ruta del libro seleccionado al portapapeles.No se puede agregar una nueva colección llamada '%s'.No se puede cambiar el nombre a '%s'.¡No se puedo determinar la versión de la base de datos de la biblioteca!No se puede duplicar la colección.No se puede abrir %s: el archivo no existe.No se puede abrir %s: permiso denegado.No se puede leer %sNo se pudieron cargar las combinaciones de teclas: %s_Tamaño de la portadaCrea un duplicado de la colección seleccionada.Traducción al croataPersonalizado...Traducción al checoFecha en la que fue añadidoOpciones de depuración¿Eliminar "%s"?¿Eliminar información sobre los archivos recientemente abiertos?Elimina el archivo o archivador actual del disco.Elimina los libros seleccionados en disco.Elimina la colección seleccionada.DescendienteCarpetaVisorMostrar únicamente aquellos libros que tengan el texto especificado en su ruta completa. La búsqueda no distingue mayúsculas de minúsculas.No preguntar de nuevo.Modo a doble páginaAbrir automáticamente el archivador siguiente durante una presentaciónTraducción al holandésEditar marcadoresEditar archivadorAu_mentar imagen...Mejorar la imagenTecla de escape cierra el programaSalir de pantalla completaNombre de archivoOrden de archivosTamaño de archivoArchivosLos archivos se abrirán y mostrarán de acuerdo al orden especificado aquí. Esta opción no afecta al orden dentro de los archivadores.Dejó de leer en %(date)s, %(time)sPrimera páginaAjustar a la a_lturaModo de ajuste a la a_nchuraModo ajustar a la a_nchuraModo ajustar a la alturaAjustar a la alturaModo de ajuste al tamañoAjustar a la anchuraAjustar a anchura o altura:Modo ajustar a la anchuraTamaño fijo para este modo:Voltear _horizontalmenteVoltear _verticalmentePasa las páginas cuando se desplaza "fuera de la página" con la rueda del ratón o con las teclas de dirección. Toma n "pasos" consecutivos de la rueda del ratón o de las teclas de flechas para activarse.Pasar páginas al desplazarse fuera de los bordes de la páginaPasar ambas páginas en el modo a doble páginaCambia las dos páginas, en lugar de una, cada vez que se pasan las páginas en el modo a doble página.Fracción de la página a desplazar por pulsación de la barra de espacio (en porcentaje):Traducción al francésRuta completaPantalla completaModo de pantalla completaTraducción al gallegoTraducción al alemánTraducción al alemán y realizador de miniaturas de NautilusIr a la página...Traducción al griegoArchivador tar comprimido con gzip_Ocultar todoTraducción al hebreoEnormeTraducción al húngaroHiperbólico (lento)Diseño de iconosImagenCalidad de la imagenImágenesTraducción al indonésRuta no válida: '%s'Invertir desplazamiento inteligenteInvertir la dirección del desplazamiento inteligente.Lee archivadores ZIP, RAR y tar, así como archivos comunes de imágenes.Traducción al italianoTraducción al japonésMantiene la transformación actualmente seleccionada para las siguientes páginas.La combinación de teclas para "%(action)s" anula la hotkey para otra acción.Traducción al coreanoArchivador LHAIdioma (requiere reinicio del programa):GrandeModificado por última vezÚltima páginaBibliotecaLibros de la bilbiotecaColecciones de bibliotecaLista de preferencias de la bibliotevaLugarDesarrollador de MComixModo de ampliación m_anualFactor de ampliación:Lupa_LupaLupaTamaño de la lupa (en píxeles):Modo mangaModo de ampliación manualMáximo número de páginas a almacenar en caché:Mi_nimizarModificadoMover libros desde la colección '%(source collection)s' hacia la colección '%(destination collection)s'.NombreNuncaNuevoSiguiente _archivadorArchivador siguienteCarpeta siguientePágina siguienteNo hay imágenes en '%s'No se encontraron nuevos libros en la carpeta '%s'Sin ordenaciónNo se encontró ninguna versión de Python Imaging Library en su sistema.Formato de archivador no soportado: %sNormalNormal (rápido)Tamaño normalNúmero de "pasos" a dar antes de pasar la página:Número de píxeles a desplazar por cada pulsación de tecla de dirección:Número de píxeles a desplazar por cada movimiento de rueda del ratón:Solamente para las páginas de títuloSolamente para imágenes anchasAbrirAbrir _sin cerrar la bibliotecaAbrir el libro seleccionado.Abrir el diálogo de gestión de la lista de preferencias.Abre el editor de archivadores.Abre los libros seleccionados para su lectura.Abre los libros seleccionados, pero mantiene abierta la ventana de la biblioteca.Visión original/desarrollador de ComixPropietarioPáginaPermisosTraducción al persaIngrese un nombre para la nueva colección.Ingrese un nombre nuevo para la colección seleccionada.Tenga en cuenta que los únicos archivos que se agregan automáticamente a esta lista son aquellos archivos en los archivadores que MComix reconoce como comentarios.Traducción al polacoTraducción al polacoPrefere_nciasPreferenciasAnterior a_rchivadorArchivador anteriorCarpeta anteriorPágina anteriorPropie_dadesPropiedadesColoca la colección '%(subcollection)s' dentro de la colección '%(supercollection)s'.Sale y restaura el archivo actualmente abierto la próxima vez que el programa se inicie.Archivador RARRe_frescarRe_nombrarRecienteRecarga los archivos o el archivador actualmente abierto/s.¿Eliminar los libros de la biblioteca?Quitar del archivadorEliminar de la bib_liotecaEliminar de esta _colecciónSe han quitado %(num)d libro(s) desde '%(collection)s'.Se han quitado %(num)d libro(s) desde '%(collection)s'.Eliminado %d libro de la biblioteca.Eliminados %d libros de la biblioteca.Elimina de la colección libros ya no existentes.Elimina los libros seleccionados de la colección actual.¿Renombrar la colección?Ren¿Reemplazar el marcador existente en la página %s?¿Reemplazar los marcadores existentes en las páginas %s?Al reemplazarlo, su contenido será sobreescrito.Restaurar a valores por defecto.RaízRo_tar 90 grados en sentido antihorarioRotar 180 _gradosRotaciónTraducción al rusoDIAPOSITIVASS_aturación:Barras de _desplazamientoE_nfoque:GuardarGuardar _comoGuardar página comoGuardar los valores seleccionados como valores por defecto para archivos futuros.Modo a escalaBuscando nuevos libros...DesplazamientoDesplazar hacia abajoDesplazar hacia la izquierdaDesplazar hacia la derechaDesplazarse al punto central inferiorDesplazarse a la esquina inferior izquierdaDesplazarse a la esquina inferior derechaDesplazarse al centroDesplazarse al punto central izquierdoDesplazarse al punto central derechoDesplazarse al punto central superiorDesplazarse a la esquina superior izquierdaDesplazarse a la esquina superior derechaDesplazar hacia arribaSeleccionar "No" creará un nuevo marcador sin afectar a los otros marcadores.Fijar tamaño de portada de bibliotecaDetermina el factor de ampliación de la lupa.Indicar el número máximo de páginas a almacenar en caché. Un valor de -1 almacenará en caché el archivador entero.Establecer el número de "pasos" necesarios para pasar a la página anterior o siguiente. Menos pasos permitirán un paso de página muy rápido, pero puede provocar avances o retrocesos accidentales.Fijar el número de píxeles a desplazar en la página cuando se usa la rueda del ratón.Fijar el número de píxeles a desplazar en la página cuando se usan las flechas de dirección.Determina el tamaño de la lupa. La lupa es un cuadrado que tiene esta cantidad de píxeles de lado.Establece el nivel de log de salida deseado.Establece el porcentaje por el cual la página se desplazará hacia arriba o abajo cuando se pulse la tecla de espacio.Mostrar el panel OSD en pantallaMostrar números de archivoMostrar nombre de archivoMuestra solo una página donde sea apropiado:Mostrar números de páginaMostrar el número de página en las miniaturasMostrar rutaMostrar resoluciónMostrar la biblioteca al inicio.Mostrar el número de versión y salir.Mostrar esta ayuda y salir.Traducción al chino simplificadoTamañoMuestra de diapositivasTiempo entre diapositivas (en segundos):Paso en diapositivas (en píxeles):PequeñoDesplazamiento inteligente hacia abajoDesplazamiento inteligente hacia arribaTraducción al españolEspecificar el número de píxeles a desplazar cuando se esté en modo de presentación. Un valor positivo desplazará hacia adelante, un valor negativo desplazará hacia atrás, y un valor de 0 hará que la presentación siempre pase a una nueva página.Barra de _estadoIniciar _diapositivasEmpezar diapositivasIniciar la aplicación en modo a doble página.Iniciar la aplicación en modo a pantalla completa.Iniciar la aplicación en modo manga.Iniciar la aplicación en modo de presentación.Iniciar la aplicación con zoom establecido a modo de mejor ajuste.Iniciar la aplicación con zoom establecido a ajustar a la altura.Iniciar la aplicación con zoom establecido a ajustar a la anchura.Detener diapositivasGuardar información sobre los últimos archivos abiertos:Guardar miniaturas para los archivos abiertosGuarda las miniaturas para los archivos abiertos de acuerdo a la especificación de freedesktop.org. Estas miniaturas son compartidas por muchas otras aplicaciones, como por la mayoría de los administradores de archivos.Estirar imágenes para que se ajusten a la pantalla, dependiendo del modo de zoom.Estirar imágenes pequeñasTraducción al suecoCaja de _herramientasArchivador tarM_iniaturasEl archivo será eliminado de su disco duro.¡El archivador nuevo no se ha podido guardar!Los archivos originales no se han eliminado.Los libros seleccionados serán eliminados de la biblioteca y permanentemente borrados. ¿Está seguro de que desea continuar?Este error puede ser causado por la falta de librerías GTK+.Esto eliminará todas las entradas del menú "Recent", y Tamaño de la miniatura (en píxeles):MiniaturasDiminutoTraducción al chino tradicionalTransparenciaTrata como comentarios a todos los archivos que se encuentren en los archivadores y que tengan esta terminación en el nombre.TipoTraducción al ucranianoTipo de archivo desconocidoActualizando la versión de la base de datos de la biblioteca de %(from)d a %(to)d.Usa un fondo gris a cuadros para las imágenes transparentes. Si esta preferencia no está activada, el fondo será blanco liso.Usar miniatura de archivador como icono de aplicaciónUsar un fondo cuadriculado para las imágenes transparentesUsar un color de fondo dinámicoUsar pantalla completa por defectoUsar desplazamiento inteligenteUsar este color como fondo:Usar este color como fondo para miniaturas:Interfaz de usuarioVer archivadores de imágenes y cómics.Ver modosCuando está activado, la tecla ESC cierra el programa, en lugar de únicamente desactivar el modo a pantalla completa.Cuando se muestre la primera página de un archivador, o la anchura de una imagen supere a su altura, solo se verá una única página.Cuando se esté en modo de presentación, permitir que el archivador siguiente se abra de forma automática.Con subcarpetasCon esta preferencia activada, la barra espaciadora y la rueda del ratón no solamente desplazan hacia arriba o abajo, sino también hacia los lados, intentando seguir el flujo de lectura natural del cómic.Dejó de leer aquí en %(date)s, %(time)s. Si elige "Yes", la lectura se reanudará en la página %(page)d. Si no es así, se cargará la primera página.Archivador ZIP_Aumentar zoom_Reducir zoomAmpliar zoomModos de zoomReducir zoom"[OPTION...] [PATH]_Acerca de_Añadir_Añadir...Ajustar el contraste automáticamenteModo de _mejor ajuste_Marcadores_Brillo:_Cancelar_Limpiar_Cerrar_Contraste:_Copia_EliminarModo a _doble página_Duplicar_Editar_Editar marcadores..._Editar archivador..._Archivo_Primera página_Pantalla completa_Ir_Ir a la página...Ay_uda_Importar_Mantener transformaciónÚ_ltima página_Biblioteca...Modo _mangaBarra de _menúPágina _siguiente_Tamaño normal_Abrir_Abrir…Página _anterior_Salir_Reciente_Eliminar_Eliminar y borrar en disco_Rotar 90 grados en sentido horario_Guardar y salirE_scanear ahoraBuscar:_OrdenarBarra de _herramientas_Herramientas_Transformar imagen_VerLista de _preferencias_Zoom././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/es/__init__.py0000644000175000017500000000000014476523373020330 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/fa/0000755000175000017500000000000014553265237016206 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/fa/LC_MESSAGES/0000755000175000017500000000000014553265237017773 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/fa/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022074 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/fa/LC_MESSAGES/mcomix.mo0000644000175000017500000001350514476523373021632 0ustar00moritzmoritz\	
		 3Pdm

		.	+A	
m	
x										



4
E

U
`
q
	z














(<HO	nx



&,2>JN]c
x
Y@Xu

(:2A[
oz/).(K!t#A1M9g
*>Gg'CSbtB&,E'NvO(=Qp
	

	
8*Fq	9(=G>? "I;7R!6CM
Z9K1X-4T[O:'(W#0&<Q/5YND=P2,S	JA%L$)*B83
V.FU+@E\HAccessedAll filesAll imagesArchiveBehaviourBilinearBrazilian Portuguese translationBzip2 compressed tar archiveCatalan translationCommentsCroatian translationCzech translationDisplayDouble page modeDutch translationFirst pageFit _height modeFit _width modeFit height modeFit width modeFli_p horizontallyFlip _verticallyFrench translationGerman translation and Nautilus thumbnailerGo to pageGo to page...Greek translationGzip compressed tar archiveH_ide allHungarian translationImageItalian translationJapanese translationLast pageLibraryLocationMagnification factor:Magnifying _lensMagnifying lensManga modeManual zoom modeModifiedNext pageOpenOwnerPagePermissionsPolish translationPr_eferencesPreferencesPrevious pageProper_tiesPropertiesRAR archiveRotat_e 90 degrees CCWRotate 180 de_greesRussian translationS_crollbarsScrollSimplified Chinese translationSlideshowSpanish translationSt_atusbarStretch small imagesTar archiveTh_umbnailsThumbnailsTraditional Chinese translationZIP archive_About_Bookmarks_Close_Double page mode_Edit_File_First page_Fullscreen_Go_Go to page..._Help_Keep transformation_Last page_Manga mode_Menubar_Next page_Open..._Previous page_Quit_Rotate 90 degrees CW_Toolbar_ViewProject-Id-Version: comix 3.6.4
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2008-05-21 12:40+0330
Last-Translator: Meelad Zakaria 
Language-Team: Persian 
Language: 
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Generated-By: pygettext.py 1.5
آخرین دسترسیهمهٔ پرونده‌هاهمهٔ تصاویربایگانیرفتاردوخطیترجمهٔ پرتغالی برزیلیبایگانی tar فشرده‌شده به صورت bzip2ترجمهٔ کاتالانیتوضیحاتترجمهٔ کرواتیترجمهٔ چکینمایشحالت دو صفحه‌ایترجمهٔ هلندیصفحهٔ اولحالت منطبق با _ارتفاع صفحهحالت منطبق با عر_ض صفحهحالت منطبق با ارتفاع صفحهحالت منطبق با عرض صفحه_پشت و رو کردن افقیپشت و رو کردن _عمودیترجمهٔ فرانسویترجمهٔ آلمانی و مسطوره‌ساز ناتیلوسرفتن به صفحهٔرفتن به صفحهٔ...ترجمهٔ یونانیبایگانی tar فشرده‌شده به صورت gzipمخ_فی کردن همهترجمهٔ مجاریتصویرترجمهٔ ایتالیاییترجمهٔ ژاپنیآخرین صفحهکتاب‌خانهمکانضریب بزرگ‌نمایی:_عدسی بزرگ‌نماعدسی بزرگ‌نماحالت ژاپنیحالت زوم دستیآخرین تغییرصفحهٔ بعدباز کردنمالکصفحهاجازه‌هاترجمهٔ لهستانیتر_جیحاتترجیحاتصفحهٔ قبلویژ_گی‌هاویژگی‌هابایگانی RARچرخان_دن ۹۰ درجه‌ای در خلاف جهت ساعتچرخاندن ۱۸۰ در_جه‌ایترجمهٔ روسینوارهای ل_غزشلغزشترجمهٔ چینی ساده‌شدهنمایش اسلایدیترجمهٔ اسپانیایینوار و_ضعیتتصاویر کوچک به اندازهٔ کادر نمایش داده شوندبایگانی tarمس_طوره‌هامسطوره‌هاترجمهٔ چینی سنتیبایگانی ZIP_دربارهنشان_ک‌هاب_ستنحالت _دو صفحه‌ای_ویرایش_پروندهصفحهٔ _اولتمام‌_صفحه_رفتن_رفتن به صفحهٔ...را_هنما_نگهداشتن تغییر شکل‌هاصفحهٔ آ_خرحالت _ژاپنینوار _منوصفحهٔ _بعد_باز کردن...صفحهٔ _قبل_خروج_چرخاندن ۹۰ درجه‌ای در جهت ساعت_نوار ابزار_نما././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/fa/__init__.py0000644000175000017500000000000014476523373020307 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/fr/0000755000175000017500000000000014553265237016227 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/fr/LC_MESSAGES/0000755000175000017500000000000014553265237020014 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/fr/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022115 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/fr/LC_MESSAGES/mcomix.mo0000644000175000017500000013437414476523373021663 0ustar00moritzmoritz	d ++)+,+++(+,:T,,K,',##-G-#b---%-5-.*.9B.0|.0.-.////	/B/A0U0t0{00
0)0.0<1V1
_1m1|1	11Z12/2C29I22=22222	3	
3
339&3C`3
33383!4	#4-4]C4-4!4425#56S6B67B7kz787U8u888
8	8
88	8 8q9v99r9:&7:^:Pt::::
::
;;);
=;K;7T;;%;-;+;"+<-N<|< <%<<1<'=E=/Q==	==
=
===/=.>%>> d>
>	>>>r>*?UOUeU|UUU	UPUV4*VR_VIVVFWGWTpGp	SpL]pFp2p$q9,q9fq
q'q4q?r
HrVrir%rrrar*7s!bssIssKs5tFtVt^trtttt3tAt	%u/u7u]Qu2u	u u^
v;lv5vvEw)x9xex[Hy;y{yR\z_z*{*:{!e{
{{{{{"{{"q|||,K}+x}}g},~ 3~T~
e~p~y~~)~~~G)H-r7;%R:%-.?&1f0'	8B?T2-ǂ&
'3	Q[)5Nbt%Ʉ+JbjyZ%Ƈ݇1GZ)p!҈:#YwS!3IQ^ċ؋!

/7L`yMM3*Lw$0G4I^Q{	͎P׎(=IRqx
Ǐ"

(5B]cj p
>'
	
$R2

&Бّ
!@Z6q
P#(0A2P*( ד""$/T&s-W-<jz
3A#Zo
˗%
0
N\UiFǘ	&0
B)M'wԙd[V7;&%?be1ț=
H)Oy(#Ӝ	%
/=T`l@A
H"Vy!ɞ$#4!O q"$#ڟU7m/fՠv<=dNl(^s
!@2[0ݤ'(0Yv#*!ۥ"  A	H'Rz#$Ħ,/D%4L*b+$(ިUI]IA.IxC1u
.̫,.(2W}>G`"		"$GVncҮ׮^e95%.[)#,ذ:@8V`>ղʳo>JO[j
q	
 µ%	)2A	JT\i
q|
	̶
	1	7A_o
ʷݷ"$Af~

Ƹ
۸pSk
tn[N;	E9#q04~&\	TrUY{lQ%%K6'
c&Hb-Rsdey|fV^ZFZKS 
>|ADQCeufW>$(/ R
==yp:x1w;G2,5*/k:g3a)B@q\LTo,$!h1VDBj]JC0`9	}<)dPEPF!]XAm*v}5"N7'sLn2jUHIhm3M(bMtzaX`i_8@.?^+Jl[xr7Oz<"-_+4#o.v{Oi?~WGw68IcugY of %s! Callback %(function)r failed: %(error)s! Corrupt preferences file "%s", deleting...! Could neither find pysqlite2 nor sqlite3.! Could not add book "%s" to the library! Could not add book %(book)s to collection %(collection)s! Could not add collection "%s"! Could not add file %(sourcefile)s to archive %(archivefile)s, aborting...! Could not create archive at path "%s"! Could not get cover for book "%s"! Could not load icon "%s"! Could not parse bookmarks file %s! Could not read %s! Could not remove file "%s"! Could not rename collection to "%s"! Could not save thumbnail "%(thumbpath)s": %(error)s! Extraction error: %s! Non-existant book #%i! Worker thread processing %(function)r failed: %(error)s! You need an sqlite wrapper to use the library."%s" does not appear to have a valid executable."%s" does not have a valid working directory.%(filename)s's extracted size is %(actual_size)d bytes, but should be %(expected_size)d bytes. The archive might be corrupt or in an unsupported format.%d comments%d pages%s archives%s images%s is an image viewer specifically designed to handle comic books.%s is licensed under the terms of the GNU General Public License.'%s' is disabled for archives.(Copy)...when height exceeds width...when width exceeds height7z archiveA collection by that name already exists.A copy of this license can be obtained from %sA file named '%s' already exists. Do you want to replace it?AccessedAdd _BookmarkAdd _separatorAdd a new empty collection.Add booksAdd books to '%s'.Add information about all files opened from within MComix to the shared recent files list.Add more books to the library.Add new collection?AddedAdded %(count)d new books from directory '%(directory)s'.Added books:Added new book '%(bookname)s' from directory '%(directory)s'.Adding '%s'...Adding booksAdvancedAll archivesAll booksAll filesAll imagesAlwaysAlways use this selected colour as the background colour.Always use this selected colour as the thumbnail background colour.AppearanceArchiveArchive commentsArchive-related variables can only be used for archives.Archives are stored as ZIP files.AscendingAuto-detect (Default)Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically hide all toolbars in fullscreenAutomatically open next directoryAutomatically open the first file in the next sibling directory when flipping past the last page of the last file in a directory, or the previous directory when flipping past the first page of the first file.Automatically open the last viewed file on startupAutomatically open the next archiveAutomatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically open, on startup, the file that was open when MComix was last closed.Automatically pick a background colour that fits the viewed image.Automatically rotate images according to their metadataAutomatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.Automatically scan for new books when library is _openedAutomatically use the colour that fits the viewed image for the thumbnail background.Autorotate by heightAutorotate by widthBack ten pagesBackgroundBehaviourBest fit modeBilinearBook nameBrazilian Portuguese translationBy enabling this setting, the first page of a book will be used as application icon instead of the standard icon.Bzip2 compressed tar archiveCatalan translationChanges how images are scaled. Slower algorithms result in higher quality resizing, but longer page loading times.Changes the book cover size.Changes the sort order of the library.Clear _dialog choicesClears all dialog choices that you have previously chosen not to be asked again.CloseCloses all opened files.Co_mments...CollectionCommandCommand labelCommand line is empty.Comment extensions:Comment filesCommentsCompletely removes the selected books from the library.Continue reading from page %d?Copies the current page to clipboard.Copies the selected book's path to clipboard.Could not add a new collection called '%s'.Could not change the name to '%s'.Could not determine library database version!Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCould not run command %(cmdlabel)s: %(exception)sCouldn't load keybindings: %sCover si_zeCreates a duplicate of the selected collection.Croatian translationCustom...Czech translationDate addedDebug optionsDeleteDelete "%s"?Delete information about recently opened files?Deletes the current file or archive from disk.Deletes the selected books from disk.Deletes the selected collection.DescendingDirectoryDisabled in archivesDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Do not ask again.Double page modeDuring a slideshow automatically open the next archiveDutch translationEdit BookmarksEdit archiveEdit external commandsEn_hance image...Enhance imageEscape key closes programExecute external commandExit from fullscreenExternal commandsExtraction and cacheFileFile nameFile orderFile sizeFile-related variables can only be used for files.FilesFiles will be opened and displayed according to the sort order specified here. This option does not affect ordering within archives.Files within archives will be sorted according to the order specified here. Natural order will sort numbered files based on their natural order, i.e. 1, 2, ..., 10, while literal order uses standard C sorting, i.e. 1, 2, 34, 5.Finished reading on %(date)s, %(time)sFirst pageFit _height modeFit _size modeFit _width modeFit height modeFit size modeFit to heightFit to size modeFit to widthFit to width or height:Fit width modeFixed size for this mode:Fli_p horizontallyFlip _verticallyFlip horizontallyFlip pages when scrolling "off the page" with the scroll wheel or with the arrow keys. It takes n consecutive "steps" with the scroll wheel or the arrow keys for the pages to be flipped.Flip pages when scrolling off the edges of the pageFlip two pages in double page modeFlip two pages, instead of one, each time we flip pages in double page mode.Flip verticallyForward ten pagesFraction of page to scroll per space key press (in percent):French translationFull pathFullscreenFullscreen modeGalician translationGerman translationGerman translation and Nautilus thumbnailerGo to pageGo to page...Greek translationGzip compressed tar archiveH_ide allHebrew translationHugeHungarian translationHyperbolic (slow)Icon designImageImage qualityImagesIncomplete escape sequence. For a literal '%', use '%%'.Incomplete quote sequence. For a literal '"', use '%"'.Indonesian translationInvalid escape sequence: %%%sInvalid path: '%s'Invert smart scrollInvert smart scrolling direction.It reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKeep transformationKeeps the currently selected transformation for the next pages.Key %dKeybinding for "%(action)s" overrides hotkey for another action.Korean translationLHA archiveLabelLanguage (needs restart):LargeLast modifiedLast pageLibraryLibrary booksLibrary collectionsLibrary watch listLiteral orderLocationMComix developerM_anual zoom modeMagnification factor:Magnifying LensMagnifying _lensMagnifying lensMagnifying lens size (in pixels):Manga modeManual zoom modeMaximum number of concurrent extraction threads:Maximum number of pages to store in the cache:Mi_nimizeMinimizeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNatural orderNavigationNeverNever autorotateNewNext _archiveNext archiveNext directoryNext pageNext page (always one page)Next page (dynamic)No images in '%s'No new books found in directory '%s'.No sortingNo version of the Python Imaging Library was found on your system.Non-supported archive format: %sNormalNormal (fast)Normal sizeNumber of "steps" to take before flipping the page:Number of pixels to scroll per arrow key press:Number of pixels to scroll per mouse wheel turn:Only for title pagesOnly for wide imagesOpenOpen _withOpen _without closing libraryOpen the selected book.Open the watchlist management dialog.Opens the archive editor.Opens the selected books for viewing.Opens the selected books, but keeps the library window open.Original vision/developer of ComixOwnerPDF documentPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Please note that the only files that are automatically added to this list are those files in archives that MComix recognizes as comments.Please refer to the external command documentation for a list of usable variables and other hints.Polish translatinPolish translationPr_eferencesPreferencesPreview:Previous a_rchivePrevious archivePrevious directoryPrevious pagePrevious page (always one page)Previous page (dynamic)Proper_tiesPropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.QuitQuits and restores the currently opened file next time the program starts.RAR archiveRe_freshRe_nameRecentRefreshReloads the currently opened files or archive.Remove books from the library?Remove from archiveRemove from the _libraryRemove from this _collectionRemoved %(num)d book from '%(collection)s'.Removed %(num)d books from '%(collection)s'.Removed %d book from the library.Removed %d books from the library.Removes no longer existant books from the collection.Removes the selected books from the current collection.Rename collection?Renames the selected collection.Replace existing bookmark on page %s?Replace existing bookmarks on pages %s?Replacing it will overwrite its contents.Reset to defaults.Resets all keyboard shortcuts to their default values.RootRotat_e 90 degrees CCWRotate 180 de_greesRotate 180 degreesRotate 90 degrees CCWRotate 90 degrees CWRotationRun _commandRussian translationSLIDESHOWS_aturation:S_crollbarsS_harpness:SaveSave AsSave _AsSave and quitSave changes to commands?Save page asSave the selected values as default for future files.Scaling modeScanning for new books...ScrollScroll downScroll leftScroll rightScroll to bottom centerScroll to bottom leftScroll to bottom rightScroll to centerScroll to middle leftScroll to middle rightScroll to top centerScroll to top leftScroll to top rightScroll upSelecting "No" will create a new bookmark without affecting the other bookmarks.Set library cover sizeSet the magnification factor of the magnifying lens.Set the max number of pages to cache. A value of -1 will cache the entire archive.Set the maximum number of concurrent threads for formats that support it.Set the number of "steps" needed to flip to the next or previous page.  Less steps will allow for very fast page turning but you might find yourself accidentally turning pages.Set the number of pixels to scroll on a page when using a mouse wheel.Set the number of pixels to scroll on a page when using the arrow keys.Set the size of the magnifying lens. It is a square with a side of this many pixels.Sets the desired output log level.Sets the percentage by which the page will be scrolled down or up when the space key is pressed.ShortcutsShow OSD panelShow file numbersShow filenameShow only one page where appropriate:Show page numbersShow page numbers on thumbnailsShow pathShow resolutionShow the library on startup.Show the version number and exit.Show this help and exit.Show/hide allShow/hide menubarShow/hide scrollbarsShow/hide statusbarShow/hide toolbarSimplified Chinese translationSizeSlideshowSlideshow delay (in seconds):Slideshow step (in pixels):SmallSmart scroll downSmart scroll upSort archives by:Sort files and directories by:Spanish translationSpecify the number of pixels to scroll while in slideshow mode. A positive value will scroll forward, a negative value will scroll backwards, and a value of 0 will cause the slideshow to always flip to a new page.St_atusbarStart _slideshowStart slideshowStart the application in double page mode.Start the application in fullscreen mode.Start the application in manga mode.Start the application in slideshow mode.Start the application with zoom set to best fit mode.Start the application with zoom set to fit height.Start the application with zoom set to fit width.Stop slideshowStore information about recently opened files:Store thumbnails for opened filesStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.Stretch images to fit to the screen, depending on zoom mode.Stretch small imagesSwedish translationT_oolbarsTar archiveTh_umbnailsThe archive is password-protected:The file will be deleted from your harddisk.The new archive could not be saved!The original files have not been removed.The selected books will be removed from the library and permanently deleted. Are you sure that you want to continue?This error might be caused by missing GTK+ libraries.This is a separator pseudo-command.This will remove all entries from the "Recent" menu, and clear information about last read pages.Thumbnail size (in pixels):ThumbnailsTinyTraditional Chinese translationTransformationTransparencyTreat all files found within archives, that have one of these file endings, as comments.TypeUkrainian translationUnknown filetypeUpgrading library database version from %(from)d to %(to)d.Use a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.Use archive thumbnail as application iconUse checkered background for transparent imagesUse dynamic background colourUse fullscreen by defaultUse smart scrollingUse this colour as background:Use this colour as the thumbnail background:User interfaceView images and comic book archives.View modeView modesWhen active, the ESC key closes the program, instead of only disabling fullscreen mode.When showing the first page of an archive, or an image's width exceeds its height, only a single page will be displayed.While in slideshow mode allow the next archive to automatically be opened.With subdirectoriesWith this preference set, the space key and mouse wheel do not only scroll down or up, but also sideways and so try to follow the natural reading order of the comic book.Working directoryYou have made changes to the list of external commands that have not been saved yet. Press "Yes" to save all changes, or "No" to discard them.You stopped reading here on %(date)s, %(time)s. If you choose "Yes", reading will resume on page %(page)d. Otherwise, the first page will be loaded.ZIP archiveZoomZoom _InZoom _OutZoom inZoom modesZoom out[OPTION...] [PATH]_About_Add_Add..._Auto-rotate image_Automatically adjust contrast_Best fit mode_Bookmarks_Brightness:_Cancel_Clean up_Close_Contrast:_Copy_Delete_Double page mode_Duplicate_Edit_Edit Bookmarks..._Edit archive..._Edit commands_File_First page_Fullscreen_Go_Go to page..._Help_Import_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Normal Size_Open_Open..._Previous page_Quit_Recent_Remove_Remove and delete from disk_Reset keys_Rotate 90 degrees CW_Save and quit_Scan now_Search:_Sort_Toolbar_Tools_Transform image_View_Watch list_ZoomProject-Id-Version: MComix 0.93
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2015-04-05 08:54+0100
Last-Translator: Benoit Pierre 
Language-Team: 
Language: fr_FR
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Plural-Forms: nplurals=2; plural=(n > 1);
X-Generator: Poedit 1.7.5
X-Language: fr
X-Source-Language: en
 de %s! Échec de la fonction de rappel %(function)r : %(error)s! Fichier de préférences corrompu "%s",suppression…Erreur : impossible de localiser pysqlite2 et sqlite3.! Impossible d'ajouter %s à la bibliothèque! Impossible d'ajouter le livre %(book)s à la collection %(collection)s! Impossible d'ajouter la collection "%s"! Impossible d'ajouter le fichier %(sourcefile)s à l'archive %(archivefile)s, Processus abandonné.! Impossible de créer une archive à l'emplacement spécifié "%s"! Impossible de charger la couverture de '%s'! Impossible de charger l'icône "%s"! Impossible de lire le fichier de signets %s! Impossible de lire %s! Impossible de supprimer "%s"! Impossible de renommer la collection en "%s"! Impossible d'enregistrer la vignette "%(thumbpath)s" : %(error)sErreur d'extraction : %s! Le livre #%i n'existe pas.! Échec de la fonction de rappel %(function)r : %(error)s! Il faut la composante sqlite pour utiliser la bibliothèque"%s" ne semble pas avoir un exécutable valide."%s" n'a pas de répertoire de travail valide.La taille après décompression du fichier %(filename)s est de %(actual_size)d octets, mais elle devrait être de %(expected_size)d octets. L'archive est peut-être corrompue ou dans un format inconnu.%d commentaires%d pagesArchives %sImages %s%s est un visionneur d'images, dédié à la lecture des bandes dessinées. %s est disponible suivant les termes de la GNU General Public License.'%s' ne peut être utilisé que pour les archives.(Copie)… lorsque que la hauteur est plus grande que la largeur… lorsque que la largeur est plus grande que la hauteurArchive 7zUne collection de ce nom existe déjà.Une copie de cette licence peut être obtenue sur %sUn fichier nommé '%s' existe déjà. Voulez-vous le remplacer?Accédé le _Ajouter un signetAjouter un séparateurAjouter une nouvelle collection vide.Ajouter des livresAjouter des livres à '%s'.Ajoute les informations sur les fichiers ouverts par MComix dans la liste des documents récents.Ajoute plus de livres à la bibliothèque.Ajouter une nouvelle collection ?Ajoutés%(count)d nouveaux livres ajoutés depuis le répertoire '%(directory)s'.Livres ajoutés :Nouveau livre '%(bookname)s' ajouté depuis le répertoire '%(directory)s'.Ajout de '%s'…Ajout de livresAvancéToutes les archivesTous les livresTous les fichiersToutes les imagesToujoursToujours utiliser cette couleur comme arrière-planToujours utiliser cette couleur comme arrière-plan des vignettesApparenceArchiveCommentaires de l'archiveLes variables liées à l'archive courante ne peuvent être utilisées que pour les archives.Les archives sont enregistrés comme fichiers ZIP.AscendantDétection automatique (défaut)Ajustement automatique du contraste (clarté et obscurité), séparément pour chaque couleur.Masquer automatiquement les barres d'outils en plein écranOuvrir automatiquement le dossier répertoire suivantOuvre automatiquement le premier fichier du prochain répertoire enfant lorsque la dernière page du dernier fichier d'un répertoire est passée, ou du répertoire précédent lorsqu'il s'agit de la première page du premier fichier.Ouvrir automatiquement la dernière archive visualisée au démarrageOuvrir automatiquement l'archive suivanteOuvre automatiquement la prochaine archive du répertoire courant en tournant la dernière page de l'archive lue ou de la précédente s'il s'agit d'une première page.Ouvre automatiquement au démarrage le dernier fichier qui était ouvert quand MComix a été fermé.Sélectionne automatiquement une couleur d'arrière-plan correspondant à l'image affichéeRotation automatique des images suivant leurs métadonnéesRotation automatique des images quand une orientation est spécifiée dans les métadonnées de l'image, comme un tag EXIF.Rechercher automatiquement les nouveaux livres à l'_ouverture de la bibliothèqueSélectionne automatiquement une couleur d'arrière-plan correspondant aux vignettes affichéesRotation automatique basée sur la hauteurRotation automatique basée sur la largeurRetourner de 10 pages en arrièreArrière-planComportementTaille idéaleBilinéaireNom du livreTraduction en portugais brésilienEn activant ce paramètre, la première page d'un livre sera utilisée comme icône du fichier à la place de l'icône par défaut.Archive Tar compressée avec Bzip2Traduction catalaneModifie la méthode de mise à l'échelle des images. Les algorithmes les plus lents offrent un meilleur rendu, mais le chargement des images s'en trouve ralenti.Modifie la taille de la couverture du livre.Modifie l'ordre de tri de la bibliothèque.Oublier les choix des dialoguesOublie les choix des dialogues pour lesquels vous avez précédemment sélectionner "Ne plus demander".FermerFerme tous les fichiers ouverts.Co_mmentaires…CollectionCommandeLibellé de la commandeLa ligne de commande est vide.Extensions des fichiers de commentaires :Fichiers de commentairesCommentairesSupprime définitivement les livres sélectionnés de la bibliothèque.Continuer la lecture depuis la page %d ?Copie la page courante dans le presse-papier.Copie le chemin du livre courant dans le presse-papier.Impossible d'ajouter une nouvelle collection appelée '%s'.Impossible de changer le nom en '%s'.Impossible de déterminer la version de la base de données de la bibliothèque !Impossible de dupliquer la collectionImpossible d'ouvrir %s : fichier inexistant.Impossible d'ouvrir %s : permission refusée.Impossible de lire %sImpossible d'exécuter la commande %(cmdlabel)s : %(exception)sImpossible de charger les raccourcis clavier : %sTaille de la cou_vertureCrée un double de la collection sélectionnée.Traduction croatePersonnalisée…Traduction tchèqueDate d'ajoutOptions de debugSupprimerSupprimer "%s" ?Effacer les informations sur les fichiers récemment ouverts ?Supprime le fichier ou l'archive courant du disqueSupprime les livres sélectionnés du disque.Supprime la collection sélectionnée.DescendantRépertoireDésactivé pour les archivesAffichageAffiche uniquement les livres avec la chaîne de caractère spécifiée dans le chemin complet. La recherche n'est pas sensible à la casse.Ne plus demander.Mode double pageOuvrir automatiquement l'archive suivanteTraduction néerlandaiseÉditer les signetsÉditer l'archiveÉditer les commandes externesA_mélioration de l'image…Amélioration de l'imageLa touche Échappe ferme le programmeExécuter une commande externeQuitter le mode plein écranÉditer les commandes externesDécompression et cacheFichierNom de fichierOrdre des fichiersTaille du fichierLes variables liées au fichier courant ne peuvent être utilisées que pour les fichiers.FichiersLes fichiers seront ouverts et affichés selon l'ordre de tri spécifié ici. Cette option n'affecte pas l'ordre des fichiers à l'intérieur des archives.Les fichiers seront triés à l'intérieur des archives selon l'ordre spécifié. L'ordre naturel triera les fichiers numérotés selon leur ordre naturel (1, 2,…,  10), alors que l'ordre littéral utilisera le tri standard C (1, 2, 34, 5).Achevé de lire le %(date)s, %(time)sPremière pageAdapter à la _hauteurAdapter à la pa_geAdapter à la lar_geurAjuster à la hauteurAdapter à la pageAjuster à la hauteurAdapter à la pageAjuster à la largeurAjuster à la largeur ou à la hauteur :Ajuster à la largeurDéfini la taille pour ce mode :Miroir h_orizontalMiroir _verticalMiroir horizontalTourne les pages lors du défilement en-dehors de la page avec la mollette de la souris ou les flèches du clavier. La page est tournée après 3 actions de défilement successives.Tourner les pages lors du défilement en-dehors de la pageTourner 2 pages en mode double pageTourne 2 pages au lieu d'une à chaque fois que l'on tourne une page en mode double page.Miroir verticalAvancer de 10 pagesFraction de la page à faire défiler lors de l'appui sur la touche espace (en %) :Traduction françaiseChemin completPlein écranMode plein écranTraduction galicienneTraduction allemandeTraduction allemande et développement du générateur de vignettes pour NautilusAller à la page...Aller à la page…Traduction grecArchive Tar compressée avec GzipTout mas_querTraduction hébraïqueÉnormeTraduction hongroiseHyperbolique (lent)Réalisation des icônesImageQualité d'imageImagesSéquence d'échappement incomplète. Pour une '%' littérale, utilisez '%%'.Séquence d'échappement incomplète. Pour une '"' littérale, utilisez '%"'.Traduction indonésienneSéquence d'échappement non valide : %%%sEmplacement non valide : '%s'Inversion du défilement intelligentInverse la direction du défilement intelligent.Il lit les archives ZIP, RAR et Tar aussi bien que les fichiers images.Traduction italienneTraduction japonaiseMémoriser la transformationConserve la transformation actuellement sélectionnée pour les prochaines pages.Touche %dLe raccourci pour "%(action)s" remplace le raccourci clavier d'une autre action.Traduction coréenneArchive LHALibelléLangue (redémarrage requis) :GrandeDernière modificationDernière pageBibliothèqueLivres de la bibliothèqueCollections de la bibliothèqueListe de suivi de la bibliothèqueOrdre littéralEmplacement DéveloppeurZoom m_anuelFacteur d'agrandissement :Loupe_LoupeLoupeTaille de la loupe (en pixels) :Mode mangaZoom manuelNombre maximal de taches concurrentes pour la décompression :Nombre maximal de pages dans le cache :Mi_nimiserMinimiserModifié le Déplacer les livres de '%(source collection)s' vers '%(destination collection)s'.NomOrdre naturelNavigationJamaisDésactiver les rotations automatiques_NouveauArchive sui_vanteArchive suivanteRépertoire suivantPage suivantePage suivante (toujours une page)Page suivante (dynamique)Pas d'images dans '%s'Pas de nouveau livre trouvé dans le répertoire '%s'.Pas de triAucun version du Python Imaging Library n'a été détectée sur votre système.Format d'archive non supporté : %sNormaleNormale (rapide)Taille normaleÉtapes à exécuter avant un changement de page :Défilement (en pixel) avec les flèches :Défilement (en pixel) avec la molette :Uniquement pour les pages titresUniquement pour les grandes imagesOuvrir_Ouvrir avecOuvrir _sans fermer la bibliothèqueOuvrir le livre sélectionné.Ouvre l'éditeur de la liste de suivi.Ouvre l'éditeur d'archive.Ouvre les livres sélectionnés pour lecture.Ouvre les livres sélectionnés, mais conserve la fenêtre de la bibliothèque ouverte.Visionnaire et développeur original de ComixPropriétaire Document PDFPagePermissions Traduction persaneVeuillez saisir un nom pour la nouvelle collection.Veuillez saisir un nouveau nom pour la collection sélectionnée.Les seuls fichiers ajoutés automatiquement à cette liste sont les fichiers de l'archive que MComix reconnait comme commentaires.Veuillez vous référer à la documentation sur les commandes externes pour obtenir la liste des variables supportées et des astuces concernant leur utilisation.Traduction polonaiseTraduction polonaise_PréférencesPréférencesPré-visualisation:Archive pré_cédenteArchive précédenteRépertoire précédentPage précédentePage précédente (toujours une page)Page précédente (dynamique)_PropriétésPropriétésDéplacer la collection '%(subcollection)s' dans la collection '%(supercollection)s'.QuitterQuitte et rouvre le fichier actuellement ouvert au prochain lancement.Archive RARAct_ualiser_RenommerFichiers récentsActualiserRecharge le fichier ou l'archive courant.Effacer le livre de la bibliothèque ?Effacer de l'archiveEffacer de la _bibliothèque…Effacer de cette _collectionSuppression de %(num)d livre de '%(collection)s'.Suppression de %(num)d livres de '%(collection)s'.Suppression du livre %d de la bibliothèque.Suppression des livres %d de la bibliothèque.Efface de la collection les livres qui n'existent plus.Efface les livres sélectionnés de la collection courante.Renommer la collection ?Renomme la collection sélectionnée.Remplacer le signet actuel pour la page %s ? Remplacer les signets actuels pour les pages %s ? Le remplacement du fichier écrasera son contenu.Réinitialiser.Réinitialise tout les raccourcis à leur valeur par défaut.RacineRotation de 90 degrés sens _anti-horaireRotation de 180 de_grésRotation de 180 degrésRotation de 90 degrés sens anti-horaireRotation de 90 degrés sens horaireRotationLancer la _commandeTraduction russeDIAPORAMA_Saturation :Barres de _défilement_Netteté :EnregistrerEnregistrer sous…_Enregistrer sous…Enregistrer et quitterSauvegarder les changements à la liste des commandes externes ?Enregistrer la page sousEnregistrer les valeurs sélectionnées pour les futurs fichiers.InterpolationRecherche de nouvelles archives…DéfilementDéfilement vers le basDéfilement à gaucheDéfilement à droiteAller au centre du bas de l'imageAller dans le coin inférieur gaucheAller dans le coin inférieur droitAller au centre de l'imageAller au milieu gauche de l'imageAller au milieu droit de l'imageAller au centre du haut de l'imageAller dans le coin supérieur gaucheAller dans le coin supérieur droitDéfilement vers le hautSélectionner « Non » créera un nouveau signet sans affecter les autres signets.Définir la taille des couvertures de la bibliothèque.Défini le facteur d'agrandissement de la loupeDéfinir le nombre maximal de pages dans le cache. Une valeur de -1 stockera toute l'archive en cache.Défini le nombre maximum de taches concurrentes à utiliser lors de la décompression pour les formats le supportant.Défini le nombre d'étapes nécessaire avant un changement de page. Moins d'étapes signifie un changement rapide de page. Mais vous pourriez changer de page accidentellement.Défini le nombre de pixels à faire défiler avec la moletteDéfinit le nombre de pixels à faire défiler à l'aide des touches flèches.Défini la taille de la loupe. C'est un carré dont la taille du côté est égal à ce paramètre en pixel.Définit le niveau de sortie du journal.Définit le pourcentage de défilement d'une page vers le haut ou vers le bas à chaque appui sur la touche espace.RaccourcisAfficher le panneau OSDAfficher les numéros de fichiersAfficher le nom de fichierN'afficher qu'une seule page lorsque nécessaire :Afficher les numéros de pageAfficher le numéro de la page sur les vignettesAfficher le cheminAfficher la résolutionAffiche la bibliothèque au démarrage.Affiche le numéro de version et quitte.Affiche cette aide et quitteTout afficher/masquerAfficher/masquer la barre des menusAfficher/masquer les barres de défilementAfficher/masquer la barre d'étatAfficher/masquer la barre d'outilsTraduction en chinois simplifiéTailleDiaporamaIntervalle du diaporama (en secondes) :Pas du diaporama (en pixels) :PetiteDéfilement intelligent vers le basDéfilement intelligent vers le hautTrier les archives par :Trier les fichiers et les répertoires par :Traduction espagnoleDéfini le nombre de pixels à faire défiler lors du diaporama. Une valeur positive indique un mouvement vers l'avant, une valeur négative indique un mouvement vers l'arrière et une valeur de 0 effectuera un saut de page.Barre d'_étatDémarrer le d_iaporamaDémarre le diaporamaDémarre l'application en mode double pageDémarre l'application en mode plein écranDémarre l'application en mode mangaDémarre l'application en mode diaporamaDémarre l'application avec le zoom réglé pour s'adapter à la meilleure dimension.Démarre l'application avec le zoom réglé pour s'adapter à la hauteur.Démarre l'application avec le zoom réglé pour s'adapter à la largeur.Arrêter le diaporamaEnregistrer les informations sur les fichiers récemment ouverts:Enregistrer les vignettes des fichiers ouvertsEnregistrer les vignettes des fichiers ouverts selon les spécifications de freedesktop.org. Ces vignettes sont partagés par d'autres applications comme les gestionnaires de fichiers.Étire les images pour les adapter à l'écran, selon le mode zoom.Étirer les petites imagesTraduction suédoiseBarre d'ou_tilsArchive Tar_VignettesL'archive est protégée par un mot de passe :Le fichier sera effacer de votre disque dur.Impossible d'enregistrer la nouvelle archive !Les fichiers originaux n'ont pas été supprimés.Les livres sélectionnés seront supprimés de la bibliothèque et définitivement effacés. Voulez-vous vraiment continuer ?L'erreur peut être due à des bibliothèques GTK+ manquantes.Ceci est un séparateur.Ceci va effacer toutes les entrées concernant le menu « Récemment ouverts » et supprimer les informations sur les dernières pages lues.Taille des vignettes (en pixels) :VignettesMinusculeTraduction en chinois traditionnelTransformationTransparenceConsidérer tous les fichiers des archives qui utilisent une de ces extensions comme fichiers de commentaires.TypeTraduction ukrainienneType de fichier inconnuMise à jour de la version de la base de données de la bibliothèque de %(from)d vers %(to)d.Utilise un arrière-plan gris en damier pour les images transparentes. Si ce paramètre n'est pas activé, l'arrière-plan est blanc.Utiliser la vignette de l'archive comme icône du fichierArrière-plan en damier pour les images transparentesUtiliser une couleur d'arrière-plan dynamiqueUtiliser le mode plein écran par défautUtiliser le défilement intelligentUtiliser cette couleur comme arrière-plan :Utiliser cette couleur comme arrière-plan des vignettes :Interface utilisateurVisionne des images et des archives de bandes dessinés.Modes d'affichageModes d'affichageAppuyer sur la touche Échap ferme le programme plutôt que de désactiver le mode plein écran.Une seule image s'affichera pour la première page d'une archive ou lorsque la largeur d'une image est supérieure à sa hauteur,Lors d'un diaporama, ouvre automatiquement l'archive suivante.Inclure les sous-dossiersAvec ce paramètre, le défilement se fait verticalement à l'aide de la touche Espace ou la molette de la souris mais également latéralement en essayant de conserver l'ordre de lecture naturel de la bande dessinée.Répertoire de travailVous avez des changements non-sauvegardés à la liste des commandes externes. Pressez "Oui" pour les sauvegardez, ou "Non" pour les annuler.Vous avez arrêté de lire à cet endroit le %(date)s à %(time)s. Si vous choisissez « Oui  », la lecture se poursuivra depuis la page %(page)d. Autrement, elle reprendra à la première page du livre.Archive ZIPZoomZoom a_vantZoom a_rrièreZoomerModes de zoomDézoomer[OPTION...] [FICHIER]À _propos_Ajouter…_Ajouter…_Rotation automatique des images_Ajuster automatiquement le contrasteAjuster l'image à la _fenêtre_Signets_Luminosité :_Annuler_Nettoyer_Fermer_Contraste :_Copier_Supprimer_Double pageDupli_querÉ_dition_Éditer les signets…_Éditer l'archive…Éditer les commandes externes_FichierP_remière page_Plein écranA_ller à_Aller à la page…Aid_e_ImporterMémoriser la _transformation_Dernière page_Bibliothèque…_MangaBarre de _menusPage _suivanteTaille _normale_Ouvrir_Ouvrir…Page _précédente_QuitterFichiers _récents_Effacer_Effacer et supprimer du disque_Réinitialiser les raccourcisRotation de 90 degrés sens _horaireE_nregistrer et quitter_Rechercher maintenant_Rechercher :_Trier parBarre d'_outils_OutilsTra_nsformer l'image_Affichage_Liste de suivi_Zoom././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/fr/__init__.py0000644000175000017500000000000014476523373020330 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/gl/0000755000175000017500000000000014553265237016222 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/gl/LC_MESSAGES/0000755000175000017500000000000014553265237020007 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/gl/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022110 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/gl/LC_MESSAGES/mcomix.mo0000644000175000017500000002703514476523373021651 0ustar00moritzmoritz	(
)
5
>
)E
<o


	



!0	=	G
Q9\
!])Bk
j	u
 +"7 W%xrRcu

LR
e+p	A\p	
Hd	is+4.AN
Z
hOs)?D[ox	4T8W	\f
z	+5A#M)q
X,{=
(48>
S^jv
'7+c l#$8&H	oy&iMm n 
  "    !5
!C!!b!*!&!!!""4"""""##(#B#]#v###\#$6$/H$x$$$$$$$$5%A%X%o%%
%%%%%%
%%
&G&a&f&w&&&&&&&/&A'S'
h'v'''M''!'(0(.G(v(#|((	((((
))%()ON)")))))
****%*+"+
N+!Y+
{++,),,,,
,,,-
--&-8-K-O-V-p-----	-----.	X(n8]W.Pw'u)e06$\HxZfA&OTk{/o;j=LiD^Jty@IabF` zpgs|QlS-hVd3#MN>4B9
CvUcq,E7Y[5_<2K
}R!r~G"%*m:1+?%d comments%d pages(Copy)A collection by that name already exists.A file named '%s' already exists. Do you want to replace it?AccessedAdd a new empty collection.Add booksAdd books to '%s'.Add more books to the library.Add new collection?Adding '%s'...Adding booksAll booksAll filesAll imagesAlways use this selected colour as the background colour.AppearanceArchiveArchives are stored as ZIP files.Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically pick a background colour that fits the viewed image.Automatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.BackgroundBehaviourBest fit modeBrazilian Portuguese translationBzip2 compressed tar archiveCatalan translationCommentsCould not add a new collection called '%s'.Could not change the name to '%s'.Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCroatian translationCzech translationDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Double page modeDutch translationEdit archiveEnhance imageFilesFirst pageFit _height modeFit _width modeFit height modeFit width modeFli_p horizontallyFlip _verticallyFlip two pages, instead of one, each time we flip pages in double page mode.French translationFullscreenGerman translation and Nautilus thumbnailerGreek translationGzip compressed tar archiveH_ide allHungarian translationIcon designImageImagesIndonesian translationIt reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKorean translationLast pageLibraryLocationM_anual zoom modeMagnifying LensMagnifying _lensMagnifying lensManga modeManual zoom modeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNext pageNo images in '%s'OpenOpen the selected book.OwnerPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Polish translationPr_eferencesPreferencesPrevious pagePropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.RAR archiveRemove books from the library?Remove from archiveRename collection?Replacing it will overwrite its contents.RootRotat_e 90 degrees CCWRotate 180 de_greesRotationRussian translationSLIDESHOWS_crollbarsSaveScrollSet the magnification factor of the magnifying lens.Set the size of the magnifying lens. It is a square with a side of this many pixels.Simplified Chinese translationSizeSlideshowSpanish translationSt_atusbarStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.T_oolbarsTar archiveTh_umbnailsThe new archive could not be saved!The original files have not been removed.ThumbnailsTraditional Chinese translationTransparencyTreat all files found within archives, that have one of these file endings, as comments.Unknown filetypeUse a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.ZIP archive_About_Best fit mode_Bookmarks_Close_Double page mode_Edit_Edit archive..._File_First page_Fullscreen_Go_Help_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Open..._Previous page_Quit_Rotate 90 degrees CW_Toolbar_ViewProject-Id-Version: 
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 
Last-Translator: Marcelo Góes 
Language-Team: 
Language: 
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
%d comentarios%d páxinas(Copia)Unha colección con ese nome xa existe.Un arquivo co nome '%s' xa existe. Desexa substituílo?AccedidoEngadir nova colección baleira.Engadir librosEngadir libros a '%s'.Engadir máis libros á biblioteca.Engadir nova colección?Engadindo '%s'...Engadindo libros.Todos os librosTodos os documentosTodas as imaxesSempre usar esa cor como cor de fondo.AparenciaArquivoOs arquivos gravaranse en formato ZIP.Axustar o contraste (claridade e escuridade) automaticamente, de maneira separada para cada banda de cor.Abre o próximo arquivo do directorio automaticamente cando se pase a derradeira páxina, ou o arquivo anterior cando se pase atrás antes da primeira páxina.Escoller unha cor de fondo compatíbel coa imaxe visualizada automaticamente.Rotar automaticamente as imaxes cando a orientación se especifique nos metadatos, como no caso dos tag Exif.Fondo da pantallaComportamentoModo de encaixe óptimoTradución ao portugués do BrasilArquivo tar comprimido con bzip2Tradución ao catalánComentariosImposíbel engadir unha nova colección chamada '%s'.Imposíbel renomear como '%s'.Imposíbel duplicar a colección.Imposíbel abrir %s: O arquivo non existe.Imposíbel abrir %s: Permiso denegado.Non foi posíbel ler %sTradución ao croataTradución ao checoSuperficie de visualizaciónMostrar só os libros que teñan o texto especificado no seu roteiro completo. A busca non diferencia entre letras maiúsculas e minúsculas.Modo de páxina dobreTradución ao holandésEditar arquivoMellorar imaxeArquivosPrimeira páxinaModo de axuste de al_turaModo de axuste de la_rguraModo de axustar a alturaModo de axustar a larguraInverter _horizontalmenteInverter _verticalmentePasar dúas páxinas, en lugar dunha, cada vez que se pase páxina no modo de páxina dobre.Tradución ao francésPantalla completaTradución ao alemán e thumbnailer do NautilusTradución ao gregoArquivo tar comprimido con gzipE_sconder todosTradución ao húngaroDeseño das iconasImaxeImaxesTradución ao indonesioLe arquivos ZIP, RAR e tar, así como imaxes normais.Tradución ao italianoTradución ao xaponésTradución ao coreanoÚltima páxinaBibliotecaLocalModo de _zoom manualLente de aumento_Lente de aumentoLente de aumentoModo mangaModo de zoom manualModificadoMover libros de '%(source collection)s' a '%(destination collection)s'.NomePróxima páxinaNon hai imaxes en '%s'AbrirAbrir o libro seleccionado.PropietarioPáxinaPermisosTradución ao persaPor favor, introduza o nome da nova colección.Por favor, introduza un novo nome para a colección seleccionada.Tradución ao polacoPr_eferenciasPreferenciasPáxina anteriorPropiedadesColoque a colección '%(subcollection)s' na colección '%(supercollection)s'.Arquivo RAREliminar libros desta biblioteca?Eliminar do arquivoRenomear a colección?A substitución sobreescribirá o seu contido.RaízRo_tación de 90 graos antihorariosRotación de 180 graos horariosRotaciónTradución ao rusoPresentación de imaxesBarra de progresi_onGardarProgresión das imaxesAxustar o factor da lente de aumento.Axustar o tamaño da lente de aumento. É un cadrado con ese tamaño de pixels.Tradución ao chinés simplificadoTamañoPresentación de imaxesTradución ao castelánB_arra de estadoGarda as miniaturas de arquivos abertos de acordo coa especificación do freedesktop.org. Esas miniaturas son compartidas por varios outros programas, como a maior parte dos xestores de arquivos.Barra de _ferramentasArquivo tarM_iniaturasO novo arquivo non puido ser gravado!Os arquivos orixinais non foron eliminados.MiniaturasTradución ao chinés tradicionalTransparenciaTratar todos os documentos atopados dentro de arquivos, incluíndo os que teñen estas terminacións de arquivo, como comentarios.Tipo de arquivo descoñecidoUsar un fondo cuadriculado cinza para as imaxes transparentes. Se esta preferencia non estiver marcada, o fondo usado será branco.Arquivo ZIPS_obreModo de axuste ó_ptimoF_avoritos_FecharModo de páxina _dobre_Editar_Editar arquivo..._ArquivoP_rimeira páxina_Pantalla completa_IrA_xuda_Manter a transformación_Ultima páxinaBib_lioteca..._Modo mangaBarra de men_u_Próxima páxina_Abrir...Pá_xina anterior_SaírRo_tación de 90 graos horariosBarra de ferramentas_Exhibir././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/gl/__init__.py0000644000175000017500000000000014476523373020323 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/he/0000755000175000017500000000000014553265237016214 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/he/LC_MESSAGES/0000755000175000017500000000000014553265237020001 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/he/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022102 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/he/LC_MESSAGES/mcomix.mo0000644000175000017500000011510114476523373021633 0ustar00moritzmoritz-"")"K"'##6#Z#n#0###B#A$P$W$t$
$)$.$<$2%
;%I%	e%o%Z%%%&9&P&=]&&&&	&	&
&&9&C '
d'o'!w'	'']'-(!E(g(28)#k))S!*Bu*7*k*8\+U+
+	+
,,	, !,qB,,,r,X-&u-P--.
..
&.4.
H.V.7_..%.-.+
/"6/Y/ y/%/////0	#0-0
?0J0/W0.0%0 0
0	11r111611122,2
>2L2f2	{2
2	222
%303A3P3`3
p3~3333333434"4L5<`55	5
5555+5
)676I6	e6o666666
66667!7A67x77?77788
 8	.888
@8N8b8
u88888888!8
9#9.49	c9m9Hv99
999
999	::%":
H: S:t:
{::3:/:0:*;?;T;Y;w;%;;%;<;"2<U<[<`<l<+<4<<k=}======
==
=O>JT>>>>>.>>?%?>?X[?D?5?7/@g@ z@M@)@A&A+ABAVA_A	sA}AAAAAA5AABB!B-B9BFB^BtBBBBBBB	CPC`C4wCRCCFDGDT?E`EEF
F%$FJF\F	|FFFF	FFFFGG&G8GWGkG
AHLH]HmH.|H!HH<sIII	III,I#(J)LJtvJaJMK
iKtKyKKXKKLL{+L)L/LMM9MMM,lMMWMxNJyNNNOP$P	-P7P?PHPOPTP\PoPP
PPP	PP
PPPP
PQ
QQ.Q4Q@QLQPQ_QeQmQ
QQQQ
QQQQQQQQQR-R	<sA|Q:i"*{J+3L,-2	18nW|3	fHc0<=5BK\k.qpX?ZuL
Wo7}T?yeJrIB9V[)4w@kg]1DCN"QIF]VRhM#=86:_&Gvc~de
uM;2
;Xm!$smKdYlS vi'Yt
Eb$OA0f)>j%6.HO%/^(_n }[UU`&D,Pt9#yCSl7-^4jzaP of %s! Callback %(function)r failed: %(error)s! Could not add file %(sourcefile)s to archive %(archivefile)s, aborting...! Could not create archive at path "%s"! Could not parse bookmarks file %s! Could not read %s! Extraction error: %s! You need an sqlite wrapper to use the library.%d comments%d pages%s is an image viewer specifically designed to handle comic books.%s is licensed under the terms of the GNU General Public License.(Copy)...when height exceeds width...when width exceeds height7z archiveA collection by that name already exists.A copy of this license can be obtained from %sA file named '%s' already exists. Do you want to replace it?AccessedAdd _BookmarkAdd a new empty collection.Add booksAdd books to '%s'.Add information about all files opened from within MComix to the shared recent files list.Add more books to the library.Add new collection?AddedAdded %(count)d new books from directory '%(directory)s'.Added books:Added new book '%(bookname)s' from directory '%(directory)s'.Adding '%s'...Adding booksAdvancedAll booksAll filesAll imagesAlwaysAlways use this selected colour as the background colour.Always use this selected colour as the thumbnail background colour.AppearanceArchiveArchives are stored as ZIP files.AscendingAuto-detect (Default)Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically hide all toolbars in fullscreenAutomatically open next directoryAutomatically open the first file in the next sibling directory when flipping past the last page of the last file in a directory, or the previous directory when flipping past the first page of the first file.Automatically open the last viewed file on startupAutomatically open the next archiveAutomatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically open, on startup, the file that was open when MComix was last closed.Automatically pick a background colour that fits the viewed image.Automatically rotate images according to their metadataAutomatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.Automatically scan for new books when library is _openedAutomatically use the colour that fits the viewed image for the thumbnail background.BackgroundBehaviourBest fit modeBilinearBook nameBrazilian Portuguese translationBy enabling this setting, the first page of a book will be used as application icon instead of the standard icon.Bzip2 compressed tar archiveCatalan translationChanges how images are scaled. Slower algorithms result in higher quality resizing, but longer page loading times.Changes the book cover size.Changes the sort order of the library.Clears all dialog choices that you have previously chosen not to be asked again.Closes all opened files.Co_mments...CollectionCommandCommand labelComment extensions:Comment filesCommentsCompletely removes the selected books from the library.Continue reading from page %d?Copies the current page to clipboard.Copies the selected book's path to clipboard.Could not add a new collection called '%s'.Could not change the name to '%s'.Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCover si_zeCreates a duplicate of the selected collection.Croatian translationCustom...Czech translationDate addedDelete "%s"?Delete information about recently opened files?Deletes the current file or archive from disk.Deletes the selected books from disk.Deletes the selected collection.DescendingDirectoryDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Do not ask again.Double page modeDuring a slideshow automatically open the next archiveDutch translationEdit BookmarksEdit archiveEdit external commandsEn_hance image...Enhance imageEscape key closes programExit from fullscreenFile nameFile orderFile sizeFilesFiles will be opened and displayed according to the sort order specified here. This option does not affect ordering within archives.First pageFit _height modeFit _size modeFit _width modeFit height modeFit to heightFit to size modeFit to widthFit to width or height:Fit width modeFixed size for this mode:Fli_p horizontallyFlip _verticallyFlip pages when scrolling "off the page" with the scroll wheel or with the arrow keys. It takes n consecutive "steps" with the scroll wheel or the arrow keys for the pages to be flipped.Flip pages when scrolling off the edges of the pageFlip two pages in double page modeFlip two pages, instead of one, each time we flip pages in double page mode.Fraction of page to scroll per space key press (in percent):French translationFull pathFullscreenFullscreen modeGalician translationGerman translationGerman translation and Nautilus thumbnailerGo to page...Greek translationGzip compressed tar archiveH_ide allHebrew translationHugeHungarian translationHyperbolic (slow)Icon designImageImage qualityImagesIndonesian translationInvalid path: '%s'Invert smart scrollInvert smart scrolling direction.It reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKeeps the currently selected transformation for the next pages.Korean translationLHA archiveLanguage (needs restart):LargeLast modifiedLast pageLibraryLibrary booksLibrary collectionsLibrary watch listLiteral orderLocationMComix developerM_anual zoom modeMagnification factor:Magnifying LensMagnifying _lensMagnifying lensMagnifying lens size (in pixels):Manga modeManual zoom modeMaximum number of pages to store in the cache:Mi_nimizeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNatural orderNeverNewNext _archiveNext archiveNext directoryNext pageNo images in '%s'No new books found in directory '%s'.No sortingNon-supported archive format: %sNormalNormal (fast)Normal sizeNumber of "steps" to take before flipping the page:Number of pixels to scroll per arrow key press:Number of pixels to scroll per mouse wheel turn:Only for title pagesOnly for wide imagesOpenOpen _without closing libraryOpen the selected book.Open the watchlist management dialog.Opens the archive editor.Opens the selected books for viewing.Opens the selected books, but keeps the library window open.Original vision/developer of ComixOwnerPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Please note that the only files that are automatically added to this list are those files in archives that MComix recognizes as comments.Polish translatinPolish translationPr_eferencesPreferencesPrevious a_rchivePrevious archivePrevious directoryPrevious pageProper_tiesPropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.Quits and restores the currently opened file next time the program starts.RAR archiveRe_freshRe_nameRecentReloads the currently opened files or archive.Remove books from the library?Remove from archiveRemove from the _libraryRemove from this _collectionRemoved %(num)d book from '%(collection)s'.Removed %(num)d books from '%(collection)s'.Removed %d book from the library.Removed %d books from the library.Removes no longer existant books from the collection.Removes the selected books from the current collection.Rename collection?Renames the selected collection.Replace existing bookmark on page %s?Replace existing bookmarks on pages %s?Replacing it will overwrite its contents.Reset to defaults.RootRotat_e 90 degrees CCWRotate 180 de_greesRotationRussian translationSLIDESHOWS_aturation:S_crollbarsS_harpness:SaveSave _AsSave page asSave the selected values as default for future files.Scaling modeScanning for new books...ScrollScroll downScroll leftScroll rightScroll to bottom centerScroll to bottom leftScroll to bottom rightScroll to centerScroll to middle leftScroll to middle rightScroll to top centerScroll to top leftScroll to top rightScroll upSelecting "No" will create a new bookmark without affecting the other bookmarks.Set library cover sizeSet the magnification factor of the magnifying lens.Set the max number of pages to cache. A value of -1 will cache the entire archive.Set the number of "steps" needed to flip to the next or previous page.  Less steps will allow for very fast page turning but you might find yourself accidentally turning pages.Set the number of pixels to scroll on a page when using a mouse wheel.Set the number of pixels to scroll on a page when using the arrow keys.Set the size of the magnifying lens. It is a square with a side of this many pixels.Sets the percentage by which the page will be scrolled down or up when the space key is pressed.Show OSD panelShow file numbersShow filenameShow only one page where appropriate:Show page numbersShow page numbers on thumbnailsShow pathShow resolutionSimplified Chinese translationSizeSlideshowSlideshow delay (in seconds):Slideshow step (in pixels):SmallSmart scroll downSmart scroll upSort archives by:Sort files and directories by:Spanish translationSpecify the number of pixels to scroll while in slideshow mode. A positive value will scroll forward, a negative value will scroll backwards, and a value of 0 will cause the slideshow to always flip to a new page.St_atusbarStart _slideshowStart slideshowStop slideshowStore information about recently opened files:Store thumbnails for opened filesStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.Stretch images to fit to the screen, depending on zoom mode.Stretch small imagesSwedish translationT_oolbarsTar archiveTh_umbnailsThe file will be deleted from your harddisk.The new archive could not be saved!The original files have not been removed.The selected books will be removed from the library and permanently deleted. Are you sure that you want to continue?This will remove all entries from the "Recent" menu, and clear information about last read pages.Thumbnail size (in pixels):ThumbnailsTinyTraditional Chinese translationTransparencyTreat all files found within archives, that have one of these file endings, as comments.TypeUkrainian translationUnknown filetypeUse a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.Use archive thumbnail as application iconUse checkered background for transparent imagesUse dynamic background colourUse fullscreen by defaultUse smart scrollingUse this colour as background:Use this colour as the thumbnail background:User interfaceWhen active, the ESC key closes the program, instead of only disabling fullscreen mode.When showing the first page of an archive, or an image's width exceeds its height, only a single page will be displayed.While in slideshow mode allow the next archive to automatically be opened.With subdirectoriesWith this preference set, the space key and mouse wheel do not only scroll down or up, but also sideways and so try to follow the natural reading order of the comic book.You stopped reading here on %(date)s, %(time)s. If you choose "Yes", reading will resume on page %(page)d. Otherwise, the first page will be loaded.ZIP archiveZoom _InZoom _OutZoom inZoom out_About_Add_Add..._Auto-rotate image_Automatically adjust contrast_Best fit mode_Bookmarks_Brightness:_Cancel_Clean up_Close_Contrast:_Copy_Delete_Double page mode_Duplicate_Edit_Edit Bookmarks..._Edit archive..._File_First page_Fullscreen_Go_Go to page..._Help_Import_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Normal Size_Open_Open..._Previous page_Quit_Recent_Remove_Remove and delete from disk_Rotate 90 degrees CW_Save and quit_Scan now_Search:_Sort_Toolbar_Tools_Transform image_View_Watch list_ZoomProject-Id-Version: mcomix 0.99
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2013-01-07 22:51+0200
Last-Translator: Isratine Citizen 
Language-Team: Hebrew 
Language: he
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Plural-Forms: nplurals=2; plural=(n != 1);
X-Generator: Poedit 1.5.4
 של %s! Callback %(function)r failed: %(error)s! Could not add file %(sourcefile)s to archive %(archivefile)s, aborting...! Could not create archive at path "%s"! Could not parse bookmarks file %s! Could not read %s! Extraction error: %s! You need an sqlite wrapper to use the library.%d הערות%d עמודים‏%s הינו מציג תמונות שתוכנן במיוחד לטיפול ולניהול של חוברות קומיקס.‏%s רשוי תחת הרישיון הציבורי הכללי של GNU.(עותק)...כאשר גובה עובר רוחב...כאשר רוחב עובר גובהארכיון 7zאוסף הקרוי בשם זה כבר קיים.ניתן להשיג עותק של רישיון זה בכתובת %sקובץ בשם '%s' כבר קיים. האם ברצונך להחליפו?נוגש לאחרונההוסף _סימניההוסף אוסף ריק חדש.הוסף חוברותהוסף חוברות אל '%s'.הוסף מידע אודות כל הקבצים שנפתחו מתוך MComix אל רשימת הקבצים האחרונים המשותפת.הוסף עוד חוברות אל הספרייה.להוסיף אוסף חדש?תאריך הוספההתווספו %(count)d חוברות חדשות מן המדור '%(directory)s'.חוברות שנוספו:התווספה חוברת חדשה '%(bookname)s' מן המדור '%(directory)s'.מתווספות כעת '%s'...חוברות מתווספות כעתמתקדםכל החוברותכל הקבציםכל התמונותתמידהשתמש תמיד בצבע הנבחר כצבע הרקע.השתמש תמיד בצבע הנבחר כצבע רקע תמונות־ציפורן.מראהארכיוןהארכיונים מאוחסנים כקבציי ZIP.סדר מיון עולהאיתור אוטומטי (משתמט)התאם ניגודיות (הן אפילה והן בהירות) אוטומטית, באופן נפרד עבור כל פס צבע.הסתר אוטומטית את כל סרגלי הכלים במסך מלאפתח את המדור הבא אוטומטיתפתח אוטומטית את הקובץ הבא שבמדור הקבצים המקביל הבא בעת דפדוף מעבר לעמוד האחרון, או של הקובץ האחרון שבמדור, או המדור הקודם בעת דפדוף מעבר לעמוד הראשון של הקובץ הראשון.פתח אוטומטית את הקובץ האחרון בעת הפעלהפתח את הארכיון הבא אוטומטיתפתח אוטומטית את הארכיון הבא שבמדור הקבצים בעת דפדוף מעבר לעמוד האחרון, או של הארכיון הקודם בעת דפדוף מעבר לעמוד הראשון.פתח אוטומטית, בזמן הפעלה, את הקובץ שהיה פתוח בזמן שהיישום MComix נסגר באחרונה.קטוף אוטומטית צבע רקע שמתאים לתמונה שנצפית.סובב אוטומטי של תמונות בהתאם לנתונים המוצמדים (Metadata) הטמונים בתוכןסובב אוטומטי של תמונות במידה ומצוין כיוון בנתונים המוצמדים (Metadata) של התמונה, כמו למשל בתווית Exif.סרוק חוברות אוטומטית כאשר הספרייה הינה _פתוחהבחר אוטומטית צבע רקע שמתאים לרקע תמונות־ציפורן.רקעהתנהגותמצב התאמה מיטביתדו־קווישם חוברתתרגום לפורטוגזית ברזילאיתבהתרת הגדרה זו, MComix יעשה שימוש בעמוד החוברת הראשון כצלמית יישום במקום הצלמית האופיינית לו.ארכיון Tar מכווץ Bzip2תרגום לקטלניתמשנה את אופן התאמת הממדים של התמונות. אלגוריתמים איטיים יניבו איכות שינוי גודל גבוהה, אולם זמני טעינת עמודים יהיו ארוכים יותר.שינוי גודל כריכת החוברת.שינוי סדר המיון של הספרייה.איפוס כל ברירות דו שיח שבעבר בחרת שלא להישאל עליהם בשנית.סגירת כל הקבצים הפתוחים.הע_רותאוסףפקודהסיווג פקודהסיומות הערה:קבצי הערותהערותהסרה מוחלטת של החוברות הנבחרות מן הספרייה.להמשיך לקרוא מעמוד %d?העתקת העמוד הנוכחי אל לוח גזירים.העתקת נתיב החוברת הנבחרת אל לוח גזירים.לא ניתן להוסיף אוסף חדש בשם '%s'.לא ניתן לשנות את השם אל '%s'.לא ניתן לשכפל אוסף.לא ניתן לפתוח את הקובץ ‫%s: קובץ לא קיים.לא ניתן לפתוח את הקובץ ‫%s הרשאה נדחתה.לא ניתן לקרוא את %s_גודל כריכהיצירת עותק זהה של האוסף הנבחרץתרגום לקרואטיתמותאם...תרגום לצ'כיתתאריך הוספהלמחוק את "%s"?למחוק מידע אודות קבצים שנפתחו לאחרונה?מחיקת הקובץ או הארכיון הנוכחי מן הכונן.מחיקת החוברות הנבחרות מן הכונן.מחיקת האוסף הנבחר.סדר מיון יורדמדורתצוגההצגה של חוברות שלהן מחרוזת התמליל שמצוינת בנתיב המלא שלהן, בלבד. החיפוש אינו תלוי רישיות.אל תשאל אותי שנית.מצב עמוד כפולבמהלך מצגת פתח אוטומטית את הארכיון הבא.תרגום להולנדיתעריכת סמניותעריכת ארכיוןעריכת פקודות חיצוניותש_פר תמונה...שיפור תמונהמקש Escape סוגר תוכניתצא ממסך מלאשם קובץמסדר קבציםגודל קובץקבציםקבצים יפתחו ויוצגו בהתאם אל סדר המיון שמצוין כאן. אפשרות זו לא משפיעה על הסדר שכבר מצוי בתוך ארכיונים.עמוד ראשוןמצב התאמה ל_גובהמצב התאמה ל_גודלמצב התאמה ל_רוחבמצב התאמה לגובההתאמה לגובהמצב התאמה לגודלהתאמה לרוחבמצב התאמה לגובה או לרוחב:מצב התאמה לרוחבגודל קבוע עבור מצב זה:הפוך במאו_זןהפוך במאו_נךדפדוף עמודים בעת גלילה "מחוץ לקצוות" באמצעות גולל העכבר או באמצעות מקשי החיצים. הדבר מצריך מספר "צעדים" עקביים עם גולל העכבר או מקשי החיצים על מנת לדפדף בעמודים.דפדף עמודים בעת גלילה מחוץ לקצוות של העמודדפדף שני עמודים במצב של עמוד כפולדפדף שני עמודים, במקום אחד, בכל פעם שאנחנו מדפדפים עמודים במצב של עמוד כפול.מקטע עמוד לגולל לכל לחיצה על מקש הרווח (באחוזים):תרגום לצרפתיתנתיב מלאמסך מלאמצב מסך מלאתרגום לגליציאניתתרגום לגרמניתNautilus תרגום לגרמנית וגם מחולל תמונות־ציפורן עבורלך אל עמוד...תרגום ליווניתארכיון Tar מכווץ Gzipה_סתר הכלתרגום לעבריתענקתרגום להונגריתהיפרבולי (איטי)עיצוב צלמיתתמונהאיכות תמונהתמונותתרגום לאינדונזיתנתיב שגוי: '%s'היפוך גלילה חכמההיפוך כיוון גלילה חכמה.ביכולתו לקרוא ארכיוני ZIP, RAR וגם tar, וכמו כן גם קבצי תמונה רגילים.תרגום לאיטלקיתתרגום ליפניתהשארת השינוי הנבחר הנוכחי עבור בעמודים הבאים.תרגום לקוריאניתארכיון LHAשפה (עליך לאתחל את היישום):גדולשונה לאחרונהעמוד אחרוןספרייהספריית חוברותספריית אוספיםרשימת צפיהסדר מילולימיקוםMComix ‏מפתחמצב זום י_דנימידת ההגדלה:עדשת מגדלתעדשת מ_גדלתעדשת מגדלתגודל עדשת המגדלת (בפיקסלים):מצב מנגה (חוברת קומיקס יפנית)מצב זום ידנימספר עמודים מרבי לאחסון בתוך המטמון:_מזערשונה לאחרונההעברת חוברות מהאוסף '%(source collection)s' אל האוסף '%(destination collection)s'.שםסדר טבעילעולם לאחדש_ארכיון הבאארכיון הבאמדור הבאעמוד הבאלא קיימות תמונות בתוך '%s'לא נמצאו חוברות חדשות במדור '%s'.ללא מיוןתסדיר ארכיון שלא נתמך: %sרגילרגיל (מהיר)גודל מקורימספר של "צעדים" שיש לקחת לפני דפדוף העמוד:מספר פיקסלים לגולל לכל לחיצה על מקש חץ:מספר פיקסלים לגולל לכל סיבוב של גלגל העכבר:עבור כותרות עמודים בלבדעבור תמונות רחבות בלבדפתיחהפתח _ללא סגירת הספרייהפתח את החוברת הנבחרת.ניהול רשימת צפיה.פתיחת עורך הארכיון.פתיחת החוברת הנבחרת לצפיה.פתיחת החוברות הנבחרות, והשארת חלון הספרייה פתוח.Comix המפתח המקורי שלבעליםעמודהרשאותתרגום לפרסיתנא להזין שם עבור האוסף החדש.נא להזין שם חדש עבור האוסף הנוכחי.נא לשים לב שהקבצים היחידים שייווספו אוטומטית אל רשימה זו יהיו קבצים בארכיונים שהיישום MComix מזהם כהערות.תרגום לפולניתתרגום לפולניתה_עדפותהעדפותא_רכיון קודםארכיון קודםמדור קודםעמוד קודםמ_אפייניםמאפייניםהשמת האוסף '%(subcollection)s' בתוך האוסף '%(supercollection)s'.יציאה ושחזור הקובץ שפתוח כעת בפעם הבאה בה התוכנית תופעל.ארכיון RAR_רענן_שינוי שם...קבצים אחרוניםטעינה מחדש של הקבצים או הארכיון הנוכחיים.להסיר חוברות מן הספרייה?הסר מן ארכיוןהסר מן ה_ספרייההסר מן _אוסף זההוסרה חוברת %(num)d מהאוסף '%(collection)s'.הוסרו %(num)d חוברות מהאוסף '%(collection)s'.הוסרה חוברת %d מן הספרייה.הוסרו %d חוברות מן הספרייה.הסרת חוברות שאינן קיימות עוד מהאוסף.הסרת החוברות הנבחרות מן האוסף הנוכחי.לשנות שם אוסף?שינוי שם האוסף הנבחר.להחליף את הסימנייה שבעמוד %s?להחליף סימניות קיימות בעמודים %s?החלפתו של הקובץ תשכתב את תכולתו של הקובץ.שחזר אל ברירות המחדל.שורשסובב 90 מעלות _נגד כיוון השעון_סובב 180 מעלותרוטציהתרגום לרוסיתמצגת_רוויה:סרגלי _גלילהח_דות:שמרשמור _בשםשמירת עמוד בשםשמור את הערך הנבחר כערך משתמט (ברירת מחדל) עבור קבצים עתידיים.מצב סילומיותסורק כעת עבור חוברות חדשות...גלילהגלילה מטהגלילה שמאלהגלילה ימינהגלילה אל מרכז תחתוןגלילה אל שמאל תחתוןגלילה אל ימין תחתוןגלילה אל המרכזגלילה אל אמצע שמאלגלילה אל אמצע ימיןגלילה אל מרכז עליוןגלילה אל שמאל עליוןגלילה אל ימין עליוןגלילה מעלהבבוחרך "לא" תיווצר סימנייה חדשה וזאת מבלי להשפיע על הסימניות האחרות.קבע כריכה עבור הספרייהקבע את מידת ההגדלה של עדשת המגדלת.קבע את מספר העמודים המרבי להטמנה. ערך של 1- יטמין את הארכיון בשלמותו.קבע את המספר של "צעדים" שדרושים כדי לדפדף אל העמוד הבא או הקודם. פחות צעדים יקנו דפדוף מהיר מאוד של עמודים אולם יש סיכוי שתמצא/י את עצמך מעלעל/ת עמודים באופן מקרי ובלתי מכוון.קבע את מספר הפיקסלים לגולל בעמוד בעת שימוש בגלגל העכבר.קבע את מספר הפיקסלים לגולל בעמוד בעת שימוש במקשי החצים.קבע את גודל עדשת המגדלת. כעדשת המגדלת היא ריבוע עם מספר פיקסלים זה בצידיה.קביעת האחוז שבו העמוד יגולל מטה או מעלה כאשר מקש הרווח נלחץ.הצג לוח OSDהצג מספרי קבציםהצג שם קובץהצג עמוד אחד בלבד במקום שהדבר הולם:הצג מספרי עמודהצג מספרי עמודים לצד תמונות־ציפורןהצג נתיבהצג רזולוציהתרגום לסינית מפושטתגודלמצגתשיהוי מצגת (בשניות):צעדי מצגת (בשניות):קטןגלילה חכמה מטהגלילה חכמה מעלהסדר ארכיונים לפי:סדר קבצים ומדורים לפי:תרגום לספרדיתציין את מספר הפיקסלים לגולל במהלך מצב מצגת. ערך חיובי יגולל הלאה, ערך שלילי יגולל לאחור, וערך של 0 יגרום למצגת לדפדף תמיד אל עמוד חדש.שורת _מצבהתחל _מצגתהתחל מצגתהפסק מצגתאחסן מידע אודות קבצים שנפתחו לאחרונה:אחסן תמונות־ציפורן עבור קבצים פתוחיםאחסן תמונות־ציפורן עבור קבצים פתוחים בהתאם למפרט של freedesktop.org. תמונות־ציפורן אלה משותפות על ידי יישומים רבים אחרים, כגון רוב מנהלי הקבצים.מתח תמונות כדי התאמה למסך, תלוי במצב זום.מתח תמונות קטנותתרגום לשוודית_סרגלי כליםארכיון Tarתמונות־_ציפורןהקובץ יימחק מן הכונן הקשיח שלך.לא ניתן היה לשמור את הארכיון החדש!הקבצים הנוכחיים לא הוסרו.החוברות הנבחרות יוסרו מן הספרייה וימחקו לצמיתות. האם עדיין ברצונך להמשיך?אפשרות זו תסיר את כל הרשומות שבתפריט "קבצים אחרונים", ותאפס מידע אודות עמודים שנקראו לאחרונה.מידת תמונות־ציפורן (בפיקסלים):תמונות־ציפורןזעירתרגום לסינית מסורתיתשקיפותהתייחסות אל כל הקבצים הנמצאים בתוך ארכיונים, שלהם יש אחת מסיומות הקבצים הללו, כהערות.טיפוסתרגום לאוקראיניתטיפוס קובץ לא מוכרהשתמש ברקע אפור משובץ עבור תמונות שקופות. במידה והעדפה זו אינה מוגדרת, צבע הרקע יהיה לבן.השתמש בתמונות־ציפורן כצלמית יישוםהשתמש ברקע משובץ עבור תמונות שקופותהשתמש בצבע רקע משתנה (דינמי)השתמש במסך מלא באופן משתמטהשתמש בגלילה חכמההשתמש בצבע זה כרקע:השתמש בצבע זה כרקע עבור תמונות־ציפורן:ממשק משתמשבמידה ואפשרות זו פעילה, המקש ESC יסגור את התוכנית, במקום לנטרל מצב של מסך מלא.בעת הצגת העמוד הראשון של ארכיון, או כשרוחב התמונה עולה על גובהה, רק עמוד בודד יוצג.בהיותי במצב מצגת פתח אוטומטית את הארכיון הבא.עם מדורי משנהבקביעת העדפה זו, המקש רווח וגלגל העכבר לא יגוללו רק מטה או מעלה, אלא גם לצדדים ובכך MComix ינסה לעקוב אחר סדר הקריאה הטבעי של חוברת הקומיקס.הפסקת לקרוא כאן בתאריך %(date)s, %(time)s. בבוחרך "כן", הקריאה תימשך בעמוד %(page)d. אחרת, העמוד הראשון יוטען במקום.ארכיון ZIPזום _פנימהזום ה_חוצהזום פנימהזום החוצה_אודותהוס_ףהוס_ף...סובב תמונה _אוטומטיתהתאם ניגודיות _אוטומטיתמצב התאמה מי_טבית_סימניות_בהירות:_ביטול_טיהור_סגור_ניגודיות:_העתק_מחקמצב עמוד _כפולש_כפולע_רוך_ערוך סימניות...ערוך _ארכיון..._קובץעמוד _ראשוןמ_סך מלא_לך_לך אל עמוד..._עזרה_ייבואה_שאר שינוייםעמוד _אחרון_ספרייה...מצב _מנגהשורת _תפריטעמוד ה_באגודל _מקורי_פתח_פתח...עמוד _קודםי_ציאהקבצים _אחרוניםה_סרהסר ומחק מן ה_כונןסובב 90 מעלות _עם כיוון השעון_שמור וצא_סרוק עכשיו_חיפוש:_מיוןסרגל _כלים_כלים_שנה תמונה_תצוגהרשימת _צפיה_זום././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/he/__init__.py0000644000175000017500000000000014476523373020315 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/hr/0000755000175000017500000000000014553265237016231 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/hr/LC_MESSAGES/0000755000175000017500000000000014553265237020016 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/hr/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022117 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/hr/LC_MESSAGES/mcomix.mo0000644000175000017500000002574014476523373021661 0ustar00moritzmoritz	
	


)%
<O


	



		'
19<
v!]	B
	
 "?S+\" %$9KrS


&6FUhLy
+"	>H^jpwA	'9IZ
juH	!-+A4m

O7Cbv)	
4TN	
	#)
1X>{$07
FQXjp


)28$#8H
"-	:0Du|$RE{
"	 , 	@ +J !v " + ' !'!9!J!Q!!!"
""
'"5"G"Z"k"}""f"#
#A'#i#!y#######>#&$:$L$^$
n$y$$	$
$	$$$	$K$'%+%>%P%W%p%x%%%%(%%	%%&&F$&
k&v&&&$&&:&#'<'K'	X'b'	i'#s'R'!'	((#(8(@(
(
(	( )))I)R)q)X))q)
b*m*y******	***+++,+=+
L+
Z+e+
y+++1+++	W'l7\V-Ou&s(d/5#}[GvYe@%NSy.m:i<KhC]Irw?H~`a{_xnfqzPjR,gUc2E"LM=3A8
BtTbo+D6XZ4^;1J
Q p|F!$)k90*>%d comments%d pages(Copy)A collection by that name already exists.A file named '%s' already exists. Do you want to replace it?AccessedAdd a new empty collection.Add booksAdd books to '%s'.Add more books to the library.Add new collection?Adding '%s'...Adding booksAll booksAll filesAll imagesAlways use this selected colour as the background colour.AppearanceArchiveArchives are stored as ZIP files.Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically pick a background colour that fits the viewed image.BackgroundBehaviourBest fit modeBrazilian Portuguese translationBzip2 compressed tar archiveCatalan translationCommentsCould not add a new collection called '%s'.Could not change the name to '%s'.Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCroatian translationCzech translationDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Double page modeDutch translationEdit archiveEnhance imageFilesFirst pageFit _height modeFit _width modeFit height modeFit width modeFli_p horizontallyFlip _verticallyFlip two pages, instead of one, each time we flip pages in double page mode.French translationFullscreenGerman translation and Nautilus thumbnailerGreek translationGzip compressed tar archiveH_ide allHungarian translationIcon designImageImagesIndonesian translationIt reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKorean translationLast pageLibraryLocationM_anual zoom modeMagnifying LensMagnifying _lensMagnifying lensManga modeManual zoom modeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNext pageNo images in '%s'OpenOpen the selected book.OwnerPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Polish translationPr_eferencesPreferencesPrevious pagePropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.RAR archiveRemove books from the library?Remove from archiveRename collection?Replacing it will overwrite its contents.RootRotat_e 90 degrees CCWRotate 180 de_greesRussian translationSLIDESHOWS_crollbarsSaveScrollSet the magnification factor of the magnifying lens.Set the size of the magnifying lens. It is a square with a side of this many pixels.Simplified Chinese translationSizeSlideshowSpanish translationSt_atusbarStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.T_oolbarsTar archiveTh_umbnailsThe new archive could not be saved!The original files have not been removed.ThumbnailsTraditional Chinese translationTransparencyTreat all files found within archives, that have one of these file endings, as comments.Unknown filetypeUse a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.ZIP archive_About_Best fit mode_Bookmarks_Close_Double page mode_Edit_Edit archive..._File_First page_Fullscreen_Go_Help_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Open..._Previous page_Quit_Rotate 90 degrees CW_Toolbar_ViewProject-Id-Version: comix 4.0.1
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2008-12-13 18:00+0100
Last-Translator: Adrian C. 
Language-Team: Croatian
Language: 
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Generated-By: pygettext.py 1.5
Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;
%d komentara%d stranica(Kopiranje)Kolekcija s tim imenom već postoji.Datoteka imena '%s' već postoji. Želite je zamijeniti?PristupljenoDodaj novu praznu kolekciju.Dodaj knjigeDodaj knjige u '%s'.Dodaj još knjiga u biblioteku.Dodaj novu kolekciju?Dodajem '%s'...Dodajem knjigeSve knjigeSve datotekeSve slikeUvijek koristi označenu boju kao boju pozadine.IzgledArhivaArhive se spremaju kao ZIP datoteke.Automatski prilagodi kontrast (osvjetljenje i crninu), posebno za svaki skup boja.Automatski otvori sljedeću arhivu u direktoriju kod okretanja zadnje stranice, ili prošlu arhivu kod okretanja prve stranice.Automatski izaberi boju pozadine koja se slaže sa prikazanom slikom.PozadinaPonašanjeNajbolje pristajanjeBrazilsko portugalski prijevodTar arhiva sažeta programom Bzip2Katalonski prijevodKomentariNemoguće dodati novu kolekciju imena '%s'.Nemoguće promijeniti ime u '%s'.Nemoguće udvostručiti kolekciju.Nemoguće otvoriti %s: Datoteka ne postoji.Nemoguće otvoriti %s: Pristup odbijen.Neuspješno čitanje %sHrvatski prijevodČeški prijevodPrikazPrikaži samo one knjige koje imaju naznačeni tekst u njihovoj punoj putanji. Pretraga nije osjetljiva na velika i mala slova.Prikaz dvostrukih stranicaNizozemski prijevodUredi arhivuPoboljšaj slikuDatotekePrva stranicaPrilagodi _visiniPri_lagodi širiniPrilagodi visiniPrilagodi širiniOkreni _vodoravnoOkreni _okomitoOkreni dvije stranice, umjesto jedne, svaki put kad okrenemo stranicu kod prikaza dvostrukih stranica.Francuski prijevodCijeli zaslonNjemački prijevod i Nautilus-ov program za prikazivanje sličicaGrčki prijevodTar arhiva sažeta programom Gzip_Sakrij sveMađarski prijevodDizajn ikonaSlikaSlikeIndoneški prijevodČita ZIP, RAR i tar arhive, kao i obične grafičke datoteke.Talijanski prijevodJapanski prijevodKorejski prijevodZadnja stranicaBibliotekaLokacijaR_učno uvećanjePovećalo_PovećaloPovećaloManga prikazRučno uvećanjeMijenjanoPremjesti knjige iz '%(source collection)s' u '%(destination collection)s'.ImeSljedeća stranicaNema slika u '%s'OtvoriOtvori označenu knjigu.VlasnikStranicaDozvolePerzijski prijevodUnesite ime za novu kolekciju.Unesite novo ime za označenu kolekciju.Poljski prijevodPo_stavkePostavkePrethodna stranicaSvojstvaStavi kolekciju '%(subcollection)s' u kolekciju '%(supercollection)s'.RAR arhivaObrisati knjige iz biblioteke?Ukloni iz arhivePreimenuj kolekciju?Zamjenom će sadržaj biti prepisan.KorijenOkre_ni za 90 stupnjeva obrnuto od smjera kazaljke na satuOkreni za 180 _stupnjevaRuski prijevodPREZENTACIJAKli_začiSpremiPomicanjePostavi faktor uvećanja povećala.Postavi veličinu povećala. To je pravokutnik stranica veličine ovoliko piksela.Pojednostavljeni kineski prijevodVeličinaPrezentacijaŠpanjolski prijevodS_tanjeSpremaj sličice za otvorene datoteke prema freedesktop.org specifikaciji. Ove sličice se dijele između mnogo aplikacija, kao npr. većine upravitelja datotekama.A_latne trakeTar arhivaSliči_ceNeuspješna pohrana nove arhive!Izvorne datoteke nisu obrisane.SličiceTradicionalni kineski prijevodTransparentnostTretiraj sve datoteke unutar arhiva, koje imaju jednu od ovih ekstenzija, kao komentare.Nepoznat tip datotekeKoristi sivu kvadratnu pozadinu za transparentne slike. Ukoliko nije postavljeno pozadina će biti čisto bijela.ZIP arhiva_O programu_Najbolje pristajanje_Oznake_ZatvoriPrikaz _dvostrukih stranica_Uredi_Uredi arhivu..._DatotekaP_rva stranica_Preko cijelog zaslona_Idi na_Pomoć_Zadrži transformaciju_Zadnja stranica_Biblioteka..._Manga prikaz_Izbornici_Sljedeća stranica_Otvori..._Prethodna stranica_Izlaz_Okreni za 90 stupnjeva u smjeru kazaljke na satu_Alati_Prikaz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/hr/__init__.py0000644000175000017500000000000014476523373020332 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/hu/0000755000175000017500000000000014553265237016234 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/hu/LC_MESSAGES/0000755000175000017500000000000014553265237020021 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/hu/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022122 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/hu/LC_MESSAGES/mcomix.mo0000644000175000017500000002737614476523373021673 0ustar00moritzmoritz	(
)
5
>
)E
<o


	



!0	=	G
Q9\
!])Bk
j	u
 +"7 W%xrRcu

LR
e+p	A\p	
Hd	is+4.AN
Z
hOs)?D[ox	4T8W	\f
z	+5A#M)q
X,{=
(48>
S^jv
,,!BN%.*
8Wk
4	3p'R* } "!+!7!L!'l!!
!3!8!&#",J"3w""""
""#######$*$B$\$w$_$$%-%F%&Y%%%%%%%W%0&A&S&
e&
s&~&&	&
&	&
&&&U&2'7'+I'
u'#'
''''$'1'-(@(P(
_(m(X|(
(,()-))H)r))))))))*6*eH*"*****++
++&+!,
A,O,n,l~,,-
-	---
--
--	...0.8.@.[.
j.x.	....
....	X(n8]W.Pw'u)e06$\HxZfA&OTk{/o;j=LiD^Jty@IabF` zpgs|QlS-hVd3#MN>4B9
CvUcq,E7Y[5_<2K
}R!r~G"%*m:1+?%d comments%d pages(Copy)A collection by that name already exists.A file named '%s' already exists. Do you want to replace it?AccessedAdd a new empty collection.Add booksAdd books to '%s'.Add more books to the library.Add new collection?Adding '%s'...Adding booksAll booksAll filesAll imagesAlways use this selected colour as the background colour.AppearanceArchiveArchives are stored as ZIP files.Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically pick a background colour that fits the viewed image.Automatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.BackgroundBehaviourBest fit modeBrazilian Portuguese translationBzip2 compressed tar archiveCatalan translationCommentsCould not add a new collection called '%s'.Could not change the name to '%s'.Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCroatian translationCzech translationDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Double page modeDutch translationEdit archiveEnhance imageFilesFirst pageFit _height modeFit _width modeFit height modeFit width modeFli_p horizontallyFlip _verticallyFlip two pages, instead of one, each time we flip pages in double page mode.French translationFullscreenGerman translation and Nautilus thumbnailerGreek translationGzip compressed tar archiveH_ide allHungarian translationIcon designImageImagesIndonesian translationIt reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKorean translationLast pageLibraryLocationM_anual zoom modeMagnifying LensMagnifying _lensMagnifying lensManga modeManual zoom modeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNext pageNo images in '%s'OpenOpen the selected book.OwnerPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Polish translationPr_eferencesPreferencesPrevious pagePropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.RAR archiveRemove books from the library?Remove from archiveRename collection?Replacing it will overwrite its contents.RootRotat_e 90 degrees CCWRotate 180 de_greesRotationRussian translationSLIDESHOWS_crollbarsSaveScrollSet the magnification factor of the magnifying lens.Set the size of the magnifying lens. It is a square with a side of this many pixels.Simplified Chinese translationSizeSlideshowSpanish translationSt_atusbarStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.T_oolbarsTar archiveTh_umbnailsThe new archive could not be saved!The original files have not been removed.ThumbnailsTraditional Chinese translationTransparencyTreat all files found within archives, that have one of these file endings, as comments.Unknown filetypeUse a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.ZIP archive_About_Best fit mode_Bookmarks_Close_Double page mode_Edit_Edit archive..._File_First page_Fullscreen_Go_Help_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Open..._Previous page_Quit_Rotate 90 degrees CW_Toolbar_ViewProject-Id-Version: Comix 4.0.3
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2009-01-26 19:34+0100
Last-Translator: Ernő Drabik 
Language-Team: 
Language: 
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Megjegyzések: %d%d lapok(Másolás)A megadott nevű gyűjtemény már létezik.A megadott nevű állomány már létezik: '%s'. Akarja a cserét?Utolsó elérésÚj, üres gyűjtemény hozzáadása.Könyvek hozzáadásaKönyvek hozzáadása a következőhöz: '%s'.Több könyv hozzáadása a könyvtárhoz.Új gyűjtemény hozzáadása?Hozzáadás '%s'...Könyvek hozzáadásaMinden könyvMinden fájlMinden képMindig a kiválasztott szín legyen a háttérszín.MegjelenésArchívumAz arhívumok ZIP állományként lesznek tárolva.A kontraszt automatikus beállítása (mind a világosságot és a  sötétséget is) minden egyes színsávhoz.Automatikusan megnyitja a következő arhívumot, ha az aktuális utolsó lapját elhagyjuk, illetve az előzőt, ha az első kép elé lépünk.A program magától válasszon egy háttérszínt, ami az aktuális képhez illik.Ha a képek metaadatában meghatározott megjelenítési irány található (például: egy EXIF cimkében), akkor a program automatikusan elvégzi az elforgatást.HáttérViselkedésLegjobb illeszkedésBrazíliai portugál fordítástar archívum (bzip2-vel tömörített)Katalán fordításMegjegyzésekNem lehetett hozzáadni a '%s' nevű gyűjteményt.A nevet nem lehet megváltoztatni a következőre: '%s'.A gyűjteményt nem lehet duplikálni.%s-t nem lehet megnyitni: Nincs ilyen fájl.%s-t nem lehet megnyitni: Hozzáférés megtagadva.Nem olvasható a(z) %sHorvát fordításCseh fordításKépernyőCsak azokat a könyveket jeleníti meg, melyek a teljes elérési útvonalukban tartalmazzák a megadott szövegrészt. A keresés nem kisbetű/nagybetű érzékeny.Dupla lapos módHolland fordításArchívum szerkesztéseKép kiemeléseFájlokElső oldalMagasság_hoz igazítás_Szélességhez igazításMagassághoz igazításSzélességhez igazításTükrözés _vízszintesenTükrözés _függőlegesenKét lapot lapozzon a program egy helyett, amikor a dupla lapos megjelenítési mód az aktív.Francia fordításTeljes képernyőNémet fordítás, Nautilus bélyegkép modulGörög fordítástar archívum (gzip-pel tömörített)M_inden elrejtveMagyar fordításIkonok kivitelezéseKépKépekIndonéz fordításA program a szokásos képfájlok mellett megnyitja a ZIP, RAR és tar arhívumokat is.Olasz fordításJapán fordításKoreai fordításUtolsó oldalKönyvtárHelyKé_zi nagyításNagyítóN_agyítóNagyítóManga módKézi nagyításMódosítvaKönyvek mozgatása innen: '%(source collection)s' ide: '%(destination collection)s'.NévKövetkező oldalNincsenek képek a következő helyen: '%s'MegnyitásA kiválasztott könyv megnyitása.TulajdonosOldalJogosultságokPerzsa fordításAdja meg az új gyűjtemény nevét.Adja meg a kiválasztott gyűjtemény új nevét.Lengyel fordításB_eállításokBeállításokElőző oldalTulajdonságokA '%(subcollection)s' gyűjtemény áthelyezése a következőbe: '%(supercollection)s'.RAR archívumEltávolítja a könyveket a könyvtárból?Törlés a gyűjteményből.Átnevezi a gyűjteményt?A csere felülírja az eredeti tartalmat.AlapkönyvtárA kép _elforgatása -90 fokkalA kép e_lforgatása 180 fokkalElforgatásOrosz fordításDiavetítésGö_rdítősávokMentésGördítésA nagyító nagyítási tényezőjének beállítása.A nagyító lencse méreteinek megadása. Ez egy négyzet, aminek az oldalai megadott pixelszámúak.Egyszerűsített kínai fordításMéretDiavetítésSpanyol fordításÁllap_otsorA megnyitott fájlok bélyegképei legyenek eltárolva a freedesktop.org előírásainak megfelelően. Ezen bélyegképeket más alkalmazások, például a legtöbb fájlkezelő is használhatja majd._EszköztárakTar archívum_BélyegképekNem lehet elmenteni az új arhívumot!Az eredeti fájlok megmaradtak.BélyegképekHagyományos kínai fordításÁtlátszóságAz itt megadott kiterjesztésű állományokat az arhívumokban megjegyzésfájlként kezeli majd a program.Ismeretlen fájltípusAz átlátszó képek háttere tarka legyen (szürke). Ha ez a beállítás nem aktív, akkor a háttérszín teljesen fehér lesz.Zip archívum_Névjegy_Legjobb illeszkedés_Könyvjelzők_Bezárás_Dupla lapos módS_zerkesztés_Arhívum szerkesztése..._Fájl_Első oldal_Teljes képernyő_Ugrás_SúgóÁ_talakítás megtartása_Utolsó oldal_Gyűjtemény_Manga mód_Menüsor_Következő oldal_Megnyitás...Előző _oldal_KilépésA kép elfo_rgatása 90 fokkal_Eszköztár_Nézet././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/hu/__init__.py0000644000175000017500000000000014476523373020335 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/id/0000755000175000017500000000000014553265237016214 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/id/LC_MESSAGES/0000755000175000017500000000000014553265237020001 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/id/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022102 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/id/LC_MESSAGES/mcomix.mo0000644000175000017500000002620614476523373021642 0ustar00moritzmoritz	
	


)%
<O


	



		'
19<
v!]	B
	
 "?S+\" %$9KrS


&6FUhLy
+"	>H^jpwA	'9IZ
juH	!-+A4m

O7Cbv)	
4TN	
	#)
1X>{$07
FQXjp


)28
'7F~$

'4?A
"]X 
 ! @ [ u -~    % (!%d comments%d pages(Copy)A collection by that name already exists.A file named '%s' already exists. Do you want to replace it?AccessedAdd a new empty collection.Add booksAdd books to '%s'.Add more books to the library.Add new collection?Adding '%s'...Adding booksAll booksAll filesAll imagesAlways use this selected colour as the background colour.AppearanceArchiveArchives are stored as ZIP files.Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically pick a background colour that fits the viewed image.BackgroundBehaviourBest fit modeBrazilian Portuguese translationBzip2 compressed tar archiveCatalan translationCommentsCould not add a new collection called '%s'.Could not change the name to '%s'.Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCroatian translationCzech translationDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Double page modeDutch translationEdit archiveEnhance imageFilesFirst pageFit _height modeFit _width modeFit height modeFit width modeFli_p horizontallyFlip _verticallyFlip two pages, instead of one, each time we flip pages in double page mode.French translationFullscreenGerman translation and Nautilus thumbnailerGreek translationGzip compressed tar archiveH_ide allHungarian translationIcon designImageImagesIndonesian translationIt reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKorean translationLast pageLibraryLocationM_anual zoom modeMagnifying LensMagnifying _lensMagnifying lensManga modeManual zoom modeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNext pageNo images in '%s'OpenOpen the selected book.OwnerPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Polish translationPr_eferencesPreferencesPrevious pagePropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.RAR archiveRemove books from the library?Remove from archiveRename collection?Replacing it will overwrite its contents.RootRotat_e 90 degrees CCWRotate 180 de_greesRussian translationSLIDESHOWS_crollbarsSaveScrollSet the magnification factor of the magnifying lens.Set the size of the magnifying lens. It is a square with a side of this many pixels.Simplified Chinese translationSizeSlideshowSpanish translationSt_atusbarStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.T_oolbarsTar archiveTh_umbnailsThe new archive could not be saved!The original files have not been removed.ThumbnailsTraditional Chinese translationTransparencyTreat all files found within archives, that have one of these file endings, as comments.Unknown filetypeUse a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.ZIP archive_About_Best fit mode_Bookmarks_Close_Double page mode_Edit_Edit archive..._File_First page_Fullscreen_Go_Help_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Open..._Previous page_Quit_Rotate 90 degrees CW_Toolbar_ViewProject-Id-Version: Comix-4.0.1
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2008-12-13 21:23+0700
Last-Translator: Andhika Padmawan 
Language-Team: Indonesia 
Language: 
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Generated-By: pygettext.py 1.5
X-Poedit-Language: Indonesian
X-Poedit-Country: INDONESIA
X-Poedit-SourceCharset: utf-8
%d komentar%d halaman(Salin)Koleksi dengan nama tersebut telah ada.Berkas bernama '%s' telah ada. Anda ingin menggantinya?DiaksesTambah koleksi kosong baru.Tambah bukuTambah buku ke '%s'.Tambah lebih banyak buku ke pustaka.Tambah koleksi baru?Menambah '%s'...Menambah bukuSemua bukuSemua berkasSemua gambarSelalu gunakan warna terpilih ini sebagai warna latar belakang.PenampilanArsipArsip disimpan sebagai berkas ZIP.Otomatis menyesuaikan kontras (baik kecerahan dan kegelapan), terpisah untuk tiap pita warna.Otomatis membuka arsip berikutnya di direktori ketika melipat halaman terakhir, atau arsip sebelumnya ketika melipat halaman pertama.Secara otomatis mengambil warna latar belakang yang cocok dengan citra yang ditampilkan.Latar BelakangPerilakuMode ukuran terbaikTerjemahan bahasa Portugis BrasilArsip tar terkompres Bzip2Terjemahan bahasa CatalanKomentarTak dapat menambah koleksi baru bernama '%s'.Tak dapat mengubah nama ke '%s'.Tak dapat menduplikasi koleksi.Tak dapat membuka %s: Tak ada berkas.Tak dapat membuka %s: Hak akses ditolak.Tak dapat membaca %sTerjemahan bahasa KroasiaTerjemahan bahasa CekoTampilanHanya tampilkan buku yang mempunyai benang teks di alamat penuh mereka. Pencarian tidak sensitif huruf.Mode halaman gandaTerjemahan bahasa BelandaSunting arsipTingkatkan citraBerkasHalaman pertamaMode sesuai _tinggiMode sesuai _lebarMode sesuai tinggiMode sesuai lebarPut_ar horizontalPutar _vertikalLipat dua halaman, ketimbang satu, tiap kali kami menggulung dalam mode halaman ganda.Terjemahan bahasa PrancisLayar PenuhTerjemahan bahasa Jerman dan pembuat miniatur gambar NautilusTerjemahan bahasa YunaniArsip tar terkompres GzipSembuny_ikan semuaTerjemahan bahasa HungariaDesain ikonGambarCitraTerjemahan bahasa IndonesiaComix membaca arsip ZIP, RAR dan tar, begitu pula dengan berkas citra biasa.Terjemahan bahasa ItaliaTerjemahan bahasa JepangTerjemahan bahasa KoreaHalaman terakhirPerpustakanLokasiMode pembesaran m_anualLensa PembesarLensa _pembesarLensa pembesarMode mangaMode pembesaran manualDimodifikasiPindah buku dari '%(source collection)s' ke '%(destination collection)s'.NamaHalaman berikutnyaTak ada citra di '%s'BukaBuka buku terpilih.PemilikHalamanHak AksesTerjemahan bahasa PersiaSilakan masukkan nama untuk koleksi baru.Silakan masukkan nama baru untuk koleksi terpilih.Terjemahan bahasa PolandiaP_engaturanPengaturanHalaman sebelumnyaPropertiTaruh koleksi '%(subcollection)s' di koleksi '%(supercollection)s'.Arsip RARHapus buku dari pustaka?Hapus dari arsipGanti nama koleksi?Mengganti berkas akan menimpa isinya.RootRotasi 90 derajat b_erlawanan arah jarum jamRotasi 180 de_rajatTerjemahan bahasa RusiaSALINDIABatang _gulungSimpanGulungAtur faktor pembesaran dari lensa pembesar.Atur ukuran lensa pembesar. Berupa kotak dengan sisi dari sebanyak ini piksel.Terjemahan bahasa China yang disederhanakanUkuranPresentasiTerjemahan bahasa SpanyolBatang st_atusSimpan miniatur untuk berkas yang dibuka menurut spesifikasi freedesktop.org. Miniatur ini dikongsikan oleh banyak aplikasi lain, seperti kebanyakan manajer berkas.B_atang alatArsip TarMiniat_urBerkas baru tak dapat disimpan!Berkas asli belum dihapus.Miniatur gambarTerjemahan bahasa China tradisionalTransparansiPerlakukan semua berkas yang ditemukan dalam arsip, yang mempunyai salah satu akhiran berkas ini, sebagai komentarl.Tipe berkas tak diketahuiGunakan warna latar belakang yang telah diperiksa untuk citra transparan. Jika pengaturan ini tidak diatur, latar belakang menjadi putih polos.Arsip ZIP_TentangMode ukuran te_rbaikB_ookmark_TutupMode _halaman ganda_Sunting_Sunting arsip..._BerkasHalaman _pertamaLa_yar penuh_KeBa_ntuan_Jaga transformasiHalaman _terakhirPus_taka..._Mode mangaBatang _menuHalaman _berikutnyaB_uka...Halaman _sebelumnya_Keluar_Rotasi 90 derajat searah jarum jamBatang _alat_Tampilan././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/id/__init__.py0000644000175000017500000000000014476523373020315 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/it/0000755000175000017500000000000014553265237016234 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/it/LC_MESSAGES/0000755000175000017500000000000014553265237020021 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/it/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022122 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/it/LC_MESSAGES/mcomix.mo0000644000175000017500000012537614476523373021672 0ustar00moritzmoritzw'')','+'((:<(w(K('(#)/)#J)n))%)5))*0**0[*-**S+_+Bh+A++,,0,
M,)X,.,<,,
,--	0-:-ZM----9-.=(.f.u..	.	.
..9.C.
//:/8B/!{/	//]/-0!I0k02<1#o11S%2By272k28`3U33
3		4
4!4	*4 44qU444r4k5&5P566
&616
96G6^6
r66766%6-7+47"`7-77 7%781*8\8z8/88	88
8
89/
9.=9%l9 9
9	999r9X:j:6{::::::
	;;1;J;	_;
i;	t;2~;;;<<& =
G=R=c=r==
=======>#>3>"?L5??<??	?
??	@@+1@
]@k@}@	@@@@@@@
@A8
A7CA{AAAA!AAA;BOB?dB@BBBC
C$C
*C	8CBC
JCXClC
CCCCCCCC!D
"D-D.>D	mDwDHDD
DDD
DDE	EE%,E
REB]E EE
EE3E/F0FFwFFF
FFF%F
G%'G<MG"GGGGG+G4H9HsH7III\IiIuI~III
II
IOIJ)JtJJJJ.JJJJKX0KDK5K7L`D`W`h`w`}```````
````
`aaaa-a3a;aCa`ava	aaaaaaaaaaec*lcEc*c4dI=d+ded2e3Le#e2ee$e.f=Df5ffDf,g+Bgng1h	=hTGhCh%hi/i/>ini)zi7i+ijj)jAj`jojbj!j%k1k?:kzkGkkkk
ll'l9l>@lKlllEl'"m
Jm UmkvmUm,8nenJ>o*ooiEpPp5q6q?qiqdr}r
r!r	rr#rrtsss/bttjtu9u
FuQuYuouuuu9u u&v6Av:xv%v@v$w*?w'jww:w3wx/4xdxyxxxxx:x,y&>y"eyyyyyyBzYzDqzzzzz{'{#;{_{%v{
{{{;{
|||'}}}&~ +~L~i~%|~~$~~'~%?W=#/afScz΁<!4HfvЂقDF>)Ƀ#4
.BqL_Nd	q{
Dž܅"	*4#Ko3φ"46
k
vIˇЇ'9-VP.0:A=|F) J	O Yz,Š0ߊHYo|)5͋0F\
h
s~͍
ٍO_4		1&-EUcB643h#p/Ll&q	Ƒ
ڑ
"*M:b"ߒ#<Wp͓U4i0ϔPZ!V|iӖ%=ycݗ
%D,a%՘!
:
E,S*'%ۙ6MCSk03(0WA57Ϝ5&T{Q?Ǟޞ
**!(LpuI'0hX&	
!"k.WΡ}&;8,7d.ң1n4`+$8٦|
çѧ
	#$2"Wz
ʨ	&5
;IY^ryȩ٩"(1:"X{

ʪcPlr#&}^[K{D*w"
kBvVmAj%% ~'[_"`/4?gT7
R:DfE?09w`UqSy11Jhtu~!+6-0|>C,QFK@OaM45$Ch!zdJLU'T2]F()]=r@NW(xbgZ}teO{HZ;P/S6*\nN5ioGH.IAVY#m\x|RiM;-< vX_cpnYu>		L$y9&eI8:b^<)fpsk,osld38Q=3z+a
W7GqjBE.
X2 of %s! Callback %(function)r failed: %(error)s! Corrupt preferences file "%s", deleting...! Could neither find pysqlite2 nor sqlite3.! Could not add book "%s" to the library! Could not add book %(book)s to collection %(collection)s! Could not add collection "%s"! Could not add file %(sourcefile)s to archive %(archivefile)s, aborting...! Could not create archive at path "%s"! Could not get cover for book "%s"! Could not load icon "%s"! Could not parse bookmarks file %s! Could not read %s! Could not remove file "%s"! Could not rename collection to "%s"! Could not save thumbnail "%(thumbpath)s": %(error)s! Extraction error: %s! Non-existant book #%i! You need an sqlite wrapper to use the library."%s" does not appear to have a valid executable."%s" does not have a valid working directory.%(filename)s's extracted size is %(actual_size)d bytes, but should be %(expected_size)d bytes. The archive might be corrupt or in an unsupported format.%d comments%d pages%s is an image viewer specifically designed to handle comic books.%s is licensed under the terms of the GNU General Public License.'%s' is disabled for archives.(Copy)...when height exceeds width...when width exceeds height7z archiveA collection by that name already exists.A copy of this license can be obtained from %sA file named '%s' already exists. Do you want to replace it?AccessedAdd _BookmarkAdd _separatorAdd a new empty collection.Add booksAdd books to '%s'.Add information about all files opened from within MComix to the shared recent files list.Add more books to the library.Add new collection?AddedAdded %(count)d new books from directory '%(directory)s'.Added books:Added new book '%(bookname)s' from directory '%(directory)s'.Adding '%s'...Adding booksAdvancedAll booksAll filesAll imagesAlwaysAlways use this selected colour as the background colour.Always use this selected colour as the thumbnail background colour.AppearanceArchiveArchive-related variables can only be used for archives.Archives are stored as ZIP files.AscendingAuto-detect (Default)Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically hide all toolbars in fullscreenAutomatically open next directoryAutomatically open the first file in the next sibling directory when flipping past the last page of the last file in a directory, or the previous directory when flipping past the first page of the first file.Automatically open the last viewed file on startupAutomatically open the next archiveAutomatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically open, on startup, the file that was open when MComix was last closed.Automatically pick a background colour that fits the viewed image.Automatically rotate images according to their metadataAutomatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.Automatically scan for new books when library is _openedAutomatically use the colour that fits the viewed image for the thumbnail background.Back ten pagesBackgroundBehaviourBest fit modeBilinearBook nameBrazilian Portuguese translationBy enabling this setting, the first page of a book will be used as application icon instead of the standard icon.Bzip2 compressed tar archiveCatalan translationChanges how images are scaled. Slower algorithms result in higher quality resizing, but longer page loading times.Changes the book cover size.Changes the sort order of the library.Clears all dialog choices that you have previously chosen not to be asked again.Closes all opened files.Co_mments...CollectionCommandCommand labelCommand line is empty.Comment extensions:Comment filesCommentsCompletely removes the selected books from the library.Continue reading from page %d?Copies the current page to clipboard.Copies the selected book's path to clipboard.Could not add a new collection called '%s'.Could not change the name to '%s'.Could not determine library database version!Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCould not run command %(cmdlabel)s: %(exception)sCouldn't load keybindings: %sCover si_zeCreates a duplicate of the selected collection.Croatian translationCustom...Czech translationDate addedDebug optionsDelete "%s"?Delete information about recently opened files?Deletes the current file or archive from disk.Deletes the selected books from disk.Deletes the selected collection.DescendingDirectoryDisabled in archivesDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Do not ask again.Double page modeDuring a slideshow automatically open the next archiveDutch translationEdit BookmarksEdit archiveEdit external commandsEn_hance image...Enhance imageEscape key closes programExecute external commandExit from fullscreenFile nameFile orderFile sizeFile-related variables can only be used for files.FilesFiles will be opened and displayed according to the sort order specified here. This option does not affect ordering within archives.Files within archives will be sorted according to the order specified here. Natural order will sort numbered files based on their natural order, i.e. 1, 2, ..., 10, while literal order uses standard C sorting, i.e. 1, 2, 34, 5.Finished reading on %(date)s, %(time)sFirst pageFit _height modeFit _size modeFit _width modeFit height modeFit to heightFit to size modeFit to widthFit to width or height:Fit width modeFixed size for this mode:Fli_p horizontallyFlip _verticallyFlip pages when scrolling "off the page" with the scroll wheel or with the arrow keys. It takes n consecutive "steps" with the scroll wheel or the arrow keys for the pages to be flipped.Flip pages when scrolling off the edges of the pageFlip two pages in double page modeFlip two pages, instead of one, each time we flip pages in double page mode.Forward ten pagesFraction of page to scroll per space key press (in percent):French translationFull pathFullscreenFullscreen modeGalician translationGerman translationGerman translation and Nautilus thumbnailerGo to page...Greek translationGzip compressed tar archiveH_ide allHebrew translationHugeHungarian translationHyperbolic (slow)Icon designImageImage qualityImagesIncomplete escape sequence. For a literal '%', use '%%'.Incomplete quote sequence. For a literal '"', use '%"'.Indonesian translationInvalid escape sequence: %%%sInvalid path: '%s'Invert smart scrollInvert smart scrolling direction.It reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKeeps the currently selected transformation for the next pages.Keybinding for "%(action)s" overrides hotkey for another action.Korean translationLHA archiveLabelLanguage (needs restart):LargeLast modifiedLast pageLibraryLibrary booksLibrary collectionsLibrary watch listLiteral orderLocationMComix developerM_anual zoom modeMagnification factor:Magnifying LensMagnifying _lensMagnifying lensMagnifying lens size (in pixels):Manga modeManual zoom modeMaximum number of pages to store in the cache:Mi_nimizeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNatural orderNeverNewNext _archiveNext archiveNext directoryNext pageNo images in '%s'No new books found in directory '%s'.No sortingNo version of the Python Imaging Library was found on your system.Non-supported archive format: %sNormalNormal (fast)Normal sizeNumber of "steps" to take before flipping the page:Number of pixels to scroll per arrow key press:Number of pixels to scroll per mouse wheel turn:Only for title pagesOnly for wide imagesOpenOpen _withOpen _without closing libraryOpen the selected book.Open the watchlist management dialog.Opens the archive editor.Opens the selected books for viewing.Opens the selected books, but keeps the library window open.Original vision/developer of ComixOwnerPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Please note that the only files that are automatically added to this list are those files in archives that MComix recognizes as comments.Please refer to the external command documentation for a list of usable variables and other hints.Polish translatinPolish translationPr_eferencesPreferencesPreview:Previous a_rchivePrevious archivePrevious directoryPrevious pageProper_tiesPropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.Quits and restores the currently opened file next time the program starts.RAR archiveRe_freshRe_nameRecentReloads the currently opened files or archive.Remove books from the library?Remove from archiveRemove from the _libraryRemove from this _collectionRemoved %(num)d book from '%(collection)s'.Removed %(num)d books from '%(collection)s'.Removed %d book from the library.Removed %d books from the library.Removes no longer existant books from the collection.Removes the selected books from the current collection.Rename collection?Renames the selected collection.Replace existing bookmark on page %s?Replace existing bookmarks on pages %s?Replacing it will overwrite its contents.Reset to defaults.RootRotat_e 90 degrees CCWRotate 180 de_greesRotationRun _commandRussian translationSLIDESHOWS_aturation:S_crollbarsS_harpness:SaveSave _AsSave changes to commands?Save page asSave the selected values as default for future files.Scaling modeScanning for new books...ScrollScroll downScroll leftScroll rightScroll to bottom centerScroll to bottom leftScroll to bottom rightScroll to centerScroll to middle leftScroll to middle rightScroll to top centerScroll to top leftScroll to top rightScroll upSelecting "No" will create a new bookmark without affecting the other bookmarks.Set library cover sizeSet the magnification factor of the magnifying lens.Set the max number of pages to cache. A value of -1 will cache the entire archive.Set the number of "steps" needed to flip to the next or previous page.  Less steps will allow for very fast page turning but you might find yourself accidentally turning pages.Set the number of pixels to scroll on a page when using a mouse wheel.Set the number of pixels to scroll on a page when using the arrow keys.Set the size of the magnifying lens. It is a square with a side of this many pixels.Sets the desired output log level.Sets the percentage by which the page will be scrolled down or up when the space key is pressed.Show OSD panelShow file numbersShow filenameShow only one page where appropriate:Show page numbersShow page numbers on thumbnailsShow pathShow resolutionShow the library on startup.Show the version number and exit.Show this help and exit.Simplified Chinese translationSizeSlideshowSlideshow delay (in seconds):Slideshow step (in pixels):SmallSmart scroll downSmart scroll upSort archives by:Sort files and directories by:Spanish translationSpecify the number of pixels to scroll while in slideshow mode. A positive value will scroll forward, a negative value will scroll backwards, and a value of 0 will cause the slideshow to always flip to a new page.St_atusbarStart _slideshowStart slideshowStart the application in double page mode.Start the application in fullscreen mode.Start the application in manga mode.Start the application in slideshow mode.Start the application with zoom set to best fit mode.Start the application with zoom set to fit height.Start the application with zoom set to fit width.Stop slideshowStore information about recently opened files:Store thumbnails for opened filesStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.Stretch images to fit to the screen, depending on zoom mode.Stretch small imagesSwedish translationT_oolbarsTar archiveTh_umbnailsThe file will be deleted from your harddisk.The new archive could not be saved!The original files have not been removed.The selected books will be removed from the library and permanently deleted. Are you sure that you want to continue?This error might be caused by missing GTK+ libraries.This is a separator pseudo-command.This will remove all entries from the "Recent" menu, and clear information about last read pages.Thumbnail size (in pixels):ThumbnailsTinyTraditional Chinese translationTransparencyTreat all files found within archives, that have one of these file endings, as comments.TypeUkrainian translationUnknown filetypeUpgrading library database version from %(from)d to %(to)d.Use a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.Use archive thumbnail as application iconUse checkered background for transparent imagesUse dynamic background colourUse fullscreen by defaultUse smart scrollingUse this colour as background:Use this colour as the thumbnail background:User interfaceView images and comic book archives.View modesWhen active, the ESC key closes the program, instead of only disabling fullscreen mode.When showing the first page of an archive, or an image's width exceeds its height, only a single page will be displayed.While in slideshow mode allow the next archive to automatically be opened.With subdirectoriesWith this preference set, the space key and mouse wheel do not only scroll down or up, but also sideways and so try to follow the natural reading order of the comic book.Working directoryYou have made changes to the list of external commands that have not been saved yet. Press "Yes" to save all changes, or "No" to discard them.You stopped reading here on %(date)s, %(time)s. If you choose "Yes", reading will resume on page %(page)d. Otherwise, the first page will be loaded.ZIP archiveZoom _InZoom _OutZoom inZoom modesZoom out[OPTION...] [PATH]_About_Add_Add..._Auto-rotate image_Automatically adjust contrast_Best fit mode_Bookmarks_Brightness:_Cancel_Clean up_Close_Contrast:_Copy_Delete_Double page mode_Duplicate_Edit_Edit Bookmarks..._Edit archive..._Edit commands_File_First page_Fullscreen_Go_Go to page..._Help_Import_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Normal Size_Open_Open..._Previous page_Quit_Recent_Remove_Remove and delete from disk_Rotate 90 degrees CW_Save and quit_Scan now_Search:_Sort_Toolbar_Tools_Transform image_View_Watch list_ZoomProject-Id-Version: MComix
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2013-04-26 18:00+0100
Last-Translator: Giovanni Scafora 
Language-Team: Arch Linux Italian Team 
Language: it
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Plural-Forms: nplurals=2; plural=(n != 1)
 di %s! Callback %(function)r fallito: %(error)s! Il file delle preferenze "%s" è corrotto, eliminazione in corso...! Impossibile trovare pysqlite2 e sqlite3.! Impossibile aggiungere il libro "%s" alla libraria! Impossibile aggiungere il libro %(book)s alla collezione %(collection)s! Impossibile aggiungere la collezione "%s"! Impossibile aggiungere il file %(sourcefile)s all'archivio %(archivefile)s, operazione annullata...! Impossibile creare un archivio nel percorso "%s"! Impossibile prelevare la copertina del libro "%s"! Impossibile caricare l'icona "%s"! Impossibile analizzare il file %s dei segnalibri! Impossibile leggere %s! Impossibile rimuovere il file "%s"! Impossibile rinominare la collezione in "%s"! Impossibile salvare la miniatura "%(thumbpath)s": %(error)s! Si è verificato un errore durante l'estrazione: %s! #%i libro inesistente! Per usare la libreria è necessario disporre di un wrapper sqlite."%s" non sembra essere un valido eseguibile."%s" non ha una valida directory di lavoro.%(filename)s's è stato estratto, la dimensione è di %(actual_size)d byte, ma dovrebbe essere di %(expected_size)d byte. L'archivio potrebbe essere corrotto oppure è un formato non supportato.%d commenti%d pagine%s è un visualizzatore di immagini specificamente progettato per gestire i fumetti.%s è distribuito sotto i termini della GNU General Public License.'%s' è disabilitato per gli archivi.(Copia)...quando l'altezza è superiore alla larghezza...quando la larghezza è superiore all'altezzaArchivio 7zEsiste già una collezione con quel nome.Una copia di questa licenza può essere scaricata da %sIl file '%s' già esiste. Vuoi sostituirlo?Visitata_Aggiungi un segnalibroAggiungi un _separatoreAggiungi una collezione vuota.Aggiungi libriAggiungi libri a '%s'.Aggiunge le informazioni di tutti i file aperti da MComix ad una lista condivisa dei file recenti.Aggiungi piu libri alla libreria.Vuoi aggiungere una nuova collezione?AggiuntoAggiunti %(count)d nuovi libri dalla directory '%(directory)s'.Libri aggiunti:Aggiunto un nuovo libro '%(bookname)s' dalla directory '%(directory)s'.Aggiunta di '%s' in corso...Aggiungi libriAvanzateTutti i libriTutti i documentiTutte le immaginiSempreUsa sempre questo colore selezionato come colore dello sfondo.Usa sempre questo colore selezionato come colore di sfondo delle miniature.AspettoArchivioLe variabili degli archivi possono essere usate solo per gli archivi.Gli archivi sono salvati come file ZIP.AscendenteRileva automaticamente (default)Regola automaticamente il contrasto (luminosità ed oscurità), separatamente per ciascuna banda di colore.Nascondi automaticamente tutte le barre degli strumenti in modalità a schermo interoApri automaticamente la directory successivaApre automaticamente il primo file presente nella prossima directory, quando si giunge all'ultima pagina dell'ultimo file di una directory, o la directory precedente quando si giunge alla prima pagina del primo file.Apri automaticamente l'ultimo file visualizzato all'apertura del programmaApri automaticamente l'archivio successivoApre automaticamente l'archivio successivo nella directory, dopo aver visualizzato l'ultima pagina o l'archivio precedente dopo la prima pagina.All'avvio, apre automaticamente il file che è stato aperto quando MComix è stato chiuso l'ultima volta.Usa automaticamente un colore di sfondo che si adatti all'immagine visualizzata.Ruota automaticamente le immagini in base ai metadataRuota automaticamente le immagini quando nei metadata dell'immagine è specificato un orientamento, come ad esempio in un tag Exif.Cerca automaticamente nuovi libri quando la libreria è _apertaUtilizza automaticamente il colore che si adatta all'immagine visualizzata per lo sfondo delle miniature.Indietro di dieci pagineSfondoComportamentoMigliore modalità di adattamentoBilineareNome del librotraduzione in portoghese brasilianoAbilitando questa impostazione, la prima pagina di un libro sarà usata come icona dell'applicazione al posto dell'icona standard.Archivio tar (compresso bzip2)traduzione in catalanoCambia in base a come le immagini sono ridimensionate. Algoritmi più lenti producono un ridimensionamento di qualità superiore, ma tempi più lunghi per il caricamento delle pagine.Cambia la dimensione della copertina del libro.Cambia l'ordine della libreria.Cancella tutte le scelte nella finestra di dialogo che precedentemente hai scelto di non chiedere di nuovoChiude tutti i file aperti.C_ommenti...CollezioneComandoEtichetta del comandoLa riga di comando è vuota.Estensioni dei commenti:File di commentoCommentiRimuovi completamente i libri selezionati dalla libreria.Continui a leggere da pagina %d?Copia la pagina attuale negli appunti.Copia negli appunti il percorso del libro selezionato.Impossibile aggiungere una nuova collezione chiamata '%s'.Impossibile cambiare il nome in '%s'.Impossibile determinare la versione del database della libreria!Impossibile duplicare la collezione.Impossibile aprire %s: il file non esiste.Impossibile aprire %s: permesso negato.Impossibile leggere %sImpossibile avviare il comando %(cmdlabel)s: %(exception)sImpossibile caricare le associazioni dei tasti : %sDimen_sione della copertinaCrea un duplicato della collezione selezionata.traduzione in croatoPersonalizza...traduzione in cecoData di inserimentoOpzioni di debugVuoi eliminare "%s"?Vuoi eliminare le informazioni dei file aperti di recente?Elimina l'attuale file o archivio dal disco.Elimina i libri selezionati dal disco.Elimina la collezione selezionata.DiscendenteCartellaDisabilitato negli archiviMostraVisualizza solo quei libri che hanno la stringa di testo specificata nel loro percorso completo. La ricerca non è case sensitive.Non chiedere di nuovo.Modalità pagina doppiaDurante una presentazione apri automaticamente l'archivio successivotraduzione in olandeseModifica i segnalibriModifica l'archivioEdita i comandi esterni_Migliora l'immagine...Migliora l'immagineIl tasto escape chiude il programmaEsegui comando esternoEsci dalla modalità a schermo interoNome del fileOrdine dei fileDimensione del fileLe variabili dei file possono essere usate solo per i file.FileI file saranno aperti e visualizzati secondo l'ordine qui specificato. Questa opzione non influisce sull'ordine all'interno degli archivi.I file presenti all'interno degli archivi saranno ordinati secondo l'ordine specificato qui. Ordine naturale ordinerà i file numerati in base al loro ordine naturale, ad esempio 1, 2, ..., 10, mentre l'ordine letterale utilizza l'ordinamento standard C, ad esempio 1, 2, 34, 5.Lettura terminata il %(date)s, %(time)sPrima paginaMod_alità adatta all'altezzaA_datta modalità di ridimensionamentoMo_dalità adatta alla larghezzaModalità adatta all'altezzaAdatta all'altezzaAdatta modalità di ridimensionamentoAdatta alla larghezzaAdatta alla larghezza o all'altezza:Modalità adatta alla larghezzaRidimensionamento per questa modalità:Rifletti _orizzontalmenteRifletti _verticalmenteVolta le pagine quando si scorre "a fine pagina" con la rotellina del mouse o con i tasti freccia. Per voltare pagina, bisogna compiere tre "scatti" con la rotellina del mouse oppure con i tasti freccia.Volta le pagine quando ci si muove oltre inizio o fine paginaCapovolgi due pagine in modalità pagina doppiaCapovolge due pagine, invece di una, ogni volta che capovolgiamo le pagine in modalità pagina doppia.Avanti di dieci pagineFrazione di pagina da scorrere quando si preme la barra di spazio (in percentuale):traduzione in francesePercorso completoSchermo interoModalità a schermo interotraduzione in galizianotraduzione in tedescotraduzione in tedesco e gestione delle miniature in NautilusVai alla pagina...traduzione in grecoArchivio tar (compresso gzip)_Nascondi tuttotraduzione in ebraicoEnormetraduzione in unghereseIperbolico (lento)design dell'iconaImmagineQualità dell'immagineImmaginiLa sequenza di escape è incompleta. Per un letterale '%', use '%%'.Sequenza di citazione incompleta. Per un letterale '"', utilizza '%"'.traduzione in indonesianoLa sequenza di escape è incompleta: %%%sIl percorso non è esatto: '%s'Inverti lo scorrimento intelligenteInverti lo scorrimento intelligente della direzione.Legge gli archivi ZIP, RAR, tar e le immagini.traduzione in italianotraduzione in giapponeseMantiene la trasformazione attualmente selezionata per le pagine successive.L'associazione del tasto "%(action)s" sovrascrive il tasto di scelta rapida di un'altra azione.traduzione in coreanoArchivio LHAEtichettaLingua (necessita di riavvio):LargaUltima modificaUltima paginaLibreriaLibri della libreriaCollezioni della libreriaLista delle collezioni da guardareOrdine letteralePosizioneSviluppatore di MComixModa_lità di ingrandimento manualeFattore di ingrandimento:Lente di ingrandimentoL_ente di ingrandimentoLente di ingrandimentoDimensione della lente di ingrandimento (in pixel):Modalità mangaModalità di ingrandimento manualeMassimo numero di pagine da memorizzare nella cache:Mi_nimizzaModificataSposta i libri da '%(source collection)s' a '%(destination collection)s'.NomeOrdine naturaleMaiNuovoAr_chivio successivoArchivio successivoCartella successivaPagina successivaNon ci sono immagini in '%s'Non ci sono nuovi libri nella directory '%s'.Nessun ordinamentoNon è stata trovata nessuna versione di Python Imaging Library nel tuo sistema.Il formato dell'archivio non è supportato: %sNormaleNormale (veloce)Dimensione realeNumero di "scatti" da compiere prima di voltare la pagina:Numero di pixel da scorrere quando si preme il tasto freccia:Numero di pixel da scorrere per ogni scatto della rotellina del mouse:Solo per i titoli delle pagineSolo per le immagini di grandi dimensioniApri_Apri conApri _senza chiudere la libreriaApri il libro selezionato.Apri la finestra di dialogo della watchlist.Apri l'editor dell'archivio.Apri i libri selezionati per la visualizzazione.Apri i libri selezionati, ma mantieni aperta la finestra della libreria.Sviluppatore di ComixProprietarioPaginaPermessitraduzione in persianoDigitare un nome per la nuova collezione.Digitare un nuovo nome per la collezione selezionata.Nota che gli unici file che sono automaticamente aggiunti a questa lista sono quelli presenti negli archivi che MComix riconosce come commenti.Si prega di far riferimento alla documentazione del comando esterno per ottenere una lista di variabili utilizzabili ed altri suggerimenti.traduzione in polaccotraduzione in polacco_PreferenzePreferenzeAnteprima:Arc_hivio precedenteArchivio precedenteCartella precedentePagina precedente_ProprietàProprietàMette la collezione '%(subcollection)s' nella collezione '%(supercollection)s'.Chiudi e ripristina il file attualmente aperto per la prossima volta che si avvia il programma.Archivio RARA_ggiornaRi_nomina_RecenteRicarica i file o gli archivi attualmente aperti.Vuoi rimuovere i libri dalla libreria?Rimuovi dall'archivioRimuove dalla _libreriaRimuovi da questa _collezioneRimosso %(num)d libro da '%(collection)s'.Rimossi %(num)d libri da '%(collection)s'.Rimossoo %d libro dalla libreria.Rimossi %d libri dalla libreria.Rimuove dalla collezione i libri che non ci sono più.Rimuove i libri selezionati dall'attuale collezione.Vuoi rinominare la collezione?Rinomina la collezione selezionata.Vuoi sostituire il segnalibro esistente sulla pagina %s?Vuoi sostituire i segnalibri esistenti sulle pagine %s?Sostituendolo, sovrascriverai il suo contenuto.Ripristina i valori di default.RootRuota di 90 gradi in s_enso antiorarioRuota di 180 _gradiRotazioneEsegui _comandotraduzione in russoPRESENTAZIONES_aturazione:Barre di s_corrimento_Nitidezza:Salva_Salva comeVuoi salvare i comandi modificati?Salva la pagina comeSalva i valori selezionati come default per i futuri file.Modalità di ridimensionamentoRicerca di nuovi libri in corso...ScorrimentoScorri verso il bassoScorri a sinistraScorri a destraScorri in basso e centraScorri in basso a sinistraScorri in basso a destraScorri al centroScorri a sinistra e centraScorri a destra e centraScorri in alto e centraScorri in alto a sinistraScorri in alto a destraScorri verso l'altoSelezionando "No" creerà un nuovo segnalibro senza influenzare gli altri segnalibri.Imposta la dimensione della copertina della libreriaImposta il fattore di ingrandimento della lente.Imposta il numero massimo di pagine da memorizzare nella cache. Un valore pari a -1, memorizzerà l'intero archivio nella cache.Imposta il numero di "scatti" da compiere per passare alla pagina successiva o a quella precedente. Un minor numero di scatti consentirà di voltare pagina velocemente, ma anche di sfogliarle accidentalmente.Imposta il numero di pixel da scorrere su una pagina quando si una la rotellina del mouse.Imposta il numero di pixel da scorrere su una pagina, quando si usano i tasti freccia.Imposta la dimensione della lente di ingrandimento. È un quadrato con un lato di questo numero di pixel.Imposta il livello di log desiderato.Imposta la percentuale di cui la pagina scorrerà verso il basso o verso l'alto, quando viene premuta la barra di spazio.Mostra il pannello OSDMostra i numeri dei fileMostra il nome del fileMostra solo una pagina quando:Mostra i numeri delle pagineMostra i numeri delle pagine nelle miniatureMostra il percorsoMostra la risoluzioneMostra la libreria all'avvio.Mostra il numero di versione ed esce.Mostra questo aiuto ed esce.traduzione in cinese semplificatoDimensionePresentazioneIntervallo della presentazione (in secondi):Intervallo della presentazione (in pixel):PiccolaScorrimento intelligente verso il bassoScorrimento intelligente verso l'altoOrdina gli archivi per:Ordina file e directory per:traduzione in spagnoloSpecifica il numero di pixel da scorrere durante la modalità presentazione. Un valore positivo li farà scorrere in avanti, un valore negativo all'indietro ed un valore pari a 0 farà sì che la presentazione visualizzi sempre una nuova pagina.Barra di st_atoA_vvia la presentazioneAvvia la presentazioneAvvia l'applicazione in modalità pagina doppia.Avvia l'applicazione in modalità a schermo intero.Avvia l'applicazione in modalità manga.Avvia l'applicazione in modalità presentazione.Avvia l'applicazione con lo zoom impostato al fine di ottenere il migliore adattamento.Avvia l'applicazione con lo zoom adattato in altezza.Avvia l'applicazione con lo zoom adattato in larghezza.Ferma la presentazioneMemorizza le informazioni dei file aperti di recente:Memorizza le miniature dei file apertiSalva le miniature dei file aperti in base alle specifiche di freedesktop.org. Queste miniature sono condivise da molte altre applicazioni, come, ad esempio, la maggior parte dei gestori di file.Ingrandisci le immagini adattandole allo schermo, a seconda della modalità zoom.Ingrandisci le immagini piccoletraduzione in svedese_Barra degli strumentiArchivio tarMiniat_ureIl file sarà cancellato dal tuo harddisk.Il nuovo archivio non può essere salvato!I file originali non sono stati rimossi.I libri selezionati saranno rimossi dalla libreria e definitivamente cancellati. Sei sicuro di voler continuare?Questo errore potrebbe essere causato dalla mancanza delle librerie GTK+.Questo è un separatore pseudo-comando.Questo rimuoverà tutte le voci presenti nel menu "Recenti" e le informazioni delle ultime pagine lette.Dimensioni della miniatura (in pixel):MiniatureMolto piccolatraduzione in cinese tradizionaleTrasparenzaTratta tutti i file trovati con queste estensioni, all'interno degli archivi, come se fossero dei commenti.Tipotraduzione in ucrainoTipo di file sconosciutoAggiornamento in corso della versione del database della libreria da %(from)d a %(to)d.Utilizza uno sfondo grigio a scacchi per le immagini trasparenti. Se questa opzione non è impostata, lo sfondo sarà bianco.Usa la miniatura dell'archivio come icona dell'applicazioneUtilizza lo sfondo a scacchi per le immagini trasparentiUsa colore di sfondo dinamicoUsa la modalità a schermo intero di defaultUsa lo scorrimento intelligenteUsa questo colore come sfondo:Usa questo colore come sfondo delle miniature:Interfaccia utenteVisualizza le immagini e gli archivi dei fumetti.Modalità di visualizzazioneQuando è attivo, il tasto ESC chiude il programma, invece di disabilitare solo la modalità a schermo intero.Quando la prima pagina di un archivio, o la larghezza di un'immagine supera la sua altezza, sarà visualizzata solo una pagina singola.Durante la modalità presentazione consente automaticamente l'apertura dell'archivio successivo.Con le sottodirectoryCon questa preferenza si imposta anche lo scorrimento laterale e cerca così di seguire l'ordine naturale di lettura del fumetto.Directory di lavoroHai appena modificato la lista dei comandi esterni che non sono stati ancora salvati. Premere "Sì" per salvare tutte le modifiche oppure "No" per non salvarle.Ti sei fermato a leggere qui il %(date)s, %(time)s. Se hai scelto "Yes", la lettura riprenderà dalla pagina %(page)d. Altrimenti, sarà caricata la prima pagina.Archivio ZIPZoom _avantiZoom _indietroZoom avantiModalità di zoomZoom indietro[OPZIONE...] [PERCORSO]_Informazioni_Aggiungi_Aggiungi..._Rotazione automatica dell'immagineR_egola automaticamente il contrastoMi_gliore modalità di adattamentoS_egnalibri_Luminosità:_Annulla_Pulisci_Chiudi_Contrasto:_Copia_Elimina_Modalità pagina doppia_Duplica_Modifica_Modifica i segnalibri..._Modifica archivio..._Edita comandi_FileP_rima pagina_Schermo interoVa_i_Vai alla pagina..._Aiuto_Importa_Mantieni trasformazione_Ultima pagina_Libreria...M_odalità mangaBarra dei _menùP_agina successiva_Dimensione normale_Apri_Apri..._Pagina precedenteEsc_i_Recenti_Rimuovi_Rimuovi ed elimina dal disco_Ruota di 90 gradi in senso orarioSal_va ed esci_Cerca adesso_Cerca:_Ordina_Barra degli strumenti_Strumenti_Trasforma l'immagine_Visualizza_Watch list_Zoom././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/it/__init__.py0000644000175000017500000000000014476523373020335 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/ja/0000755000175000017500000000000014553265237016212 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/ja/LC_MESSAGES/0000755000175000017500000000000014553265237017777 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/ja/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022100 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/ja/LC_MESSAGES/mcomix.mo0000644000175000017500000011626214476523373021642 0ustar00moritzmoritz4
3L##) #,J#+w#(#:#$K'$'s$#$$#$$%%/%5U%%%0%%&&B&A&'
%')0'.Z'<''
''	'(Z(q(((9((=(/)>)K)	T)	^)
h)s)9z)C)
)*!*	-*7*]M*-*!*2*#.+R+S+B8,7{,k,8-UX-
-	-
--	- -q.w...&.P.=/V/
c/n/
//7//%/-0+D0"p0-00 0%1(1:1X1/d11	11
1
11/1.2%J2 p2
2	22r2!3336D3{3333
333	3
4	
44&4
D4O4`4o44
444444453 5"T5Lw5<56	6
6)696N6+a6
666	6666777
%737:7Q7d7!x7A777?8@E88888
8	88
8899#949F9\9l9}9!9
99.9	9:H
:V:[:a:
e:s::	::%:
:B: ;@;
G;U;/a;0;;;;;<%'<M<%g<<<"<<<<=+=4D=y=>>(>5>A>S>d>
w>>
>O>J>7?C?L?T?.[?????X?DL@5@7@@ AM3A)AAAAAAA	BB"B.B:B?BHB5UBBBBBBB	BPB9C4PCRCFCGDTgD"DDD
E%E4EFE	fEpEE!EEEE	EF$F@FFFXFhF
|FFF*F)F$F("G5KG2G1GG.G!$HFH<H)I>I	RI\IhI,tI#I)ItI5dJaJJ
K#K(KHKXUKKKK;K{L)L/LL
M$M8M,WMM$M
MWMJNfNzN%OOO	OO
OOOPPPP;P
JPUPbP	jPtP
{PPPP
PPPPPPPPPQQQ
/Q:QFQRQ
[QfQsQyQQQQQQQQ	QQQRRR#R)R5Rd;RSDSBSE/T>uTWT9UFUAU;
V/IVHyV V5VDWN^WW!WUWAX,Y&iei?|iiiijj-jFIj<j9j6k>kEk[kbkklTlhll$lll6l m0Q--ވ	.4L!!ɉ%!%<G?BĊKKS??ߋ!4A6vf!!;]qKF-8pfo׏G1-4	S]!{*W^0**1\1|$EӔ,{!+M>
?
JU\ov

+Ř"+7K
es



Ù"Ι(+B
Va
{"ך%.B
V
al
"
(/
;I
]k

1rmD)(kpN~SaJ;	&(\}w$*8c0^2beTikfDYPy6:]]4
A~R?wEl*5`/W26dKZ{yG_47=eP?|CLv&s%>R|H%WIOs/u1
<VQv@T b<x,+'jzCYio
O'AB"G_lz7t,g3:Z)$}U9Hqd;#[.mLxK!f"ncaX-gF
hh`Q-[5^X+8UnptE>=\oMr.I#9B0 {jSJq	VN@!3MFu of %s! Callback %(function)r failed: %(error)s! Corrupt preferences file "%s", deleting...! Could neither find pysqlite2 nor sqlite3.! Could not add book "%s" to the library! Could not add book %(book)s to collection %(collection)s! Could not add collection "%s"! Could not add file %(sourcefile)s to archive %(archivefile)s, aborting...! Could not create archive at path "%s"! Could not get cover for book "%s"! Could not load icon "%s"! Could not parse bookmarks file %s! Could not read %s! Could not remove file "%s"! Could not rename collection to "%s"! Could not save thumbnail "%(thumbpath)s": %(error)s! Extraction error: %s! Non-existant book #%i! You need an sqlite wrapper to use the library.%(filename)s's extracted size is %(actual_size)d bytes, but should be %(expected_size)d bytes. The archive might be corrupt or in an unsupported format.%d comments%d pages%s is an image viewer specifically designed to handle comic books.%s is licensed under the terms of the GNU General Public License.(Copy)7z archiveA collection by that name already exists.A copy of this license can be obtained from %sA file named '%s' already exists. Do you want to replace it?AccessedAdd _BookmarkAdd a new empty collection.Add booksAdd books to '%s'.Add information about all files opened from within MComix to the shared recent files list.Add more books to the library.Add new collection?AddedAdded %(count)d new books from directory '%(directory)s'.Added books:Added new book '%(bookname)s' from directory '%(directory)s'.Adding '%s'...Adding booksAdvancedAll booksAll filesAll imagesAlwaysAlways use this selected colour as the background colour.Always use this selected colour as the thumbnail background colour.AppearanceArchiveArchives are stored as ZIP files.AscendingAuto-detect (Default)Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically hide all toolbars in fullscreenAutomatically open next directoryAutomatically open the last viewed file on startupAutomatically open the next archiveAutomatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically open, on startup, the file that was open when MComix was last closed.Automatically pick a background colour that fits the viewed image.Automatically rotate images according to their metadataAutomatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.Automatically scan for new books when library is _openedAutomatically use the colour that fits the viewed image for the thumbnail background.BackgroundBehaviourBest fit modeBilinearBook nameBrazilian Portuguese translationBy enabling this setting, the first page of a book will be used as application icon instead of the standard icon.Bzip2 compressed tar archiveCatalan translationChanges the book cover size.Changes the sort order of the library.Clears all dialog choices that you have previously chosen not to be asked again.Closes all opened files.Co_mments...CollectionComment extensions:Comment filesCommentsCompletely removes the selected books from the library.Continue reading from page %d?Copies the current page to clipboard.Copies the selected book's path to clipboard.Could not add a new collection called '%s'.Could not change the name to '%s'.Could not determine library database version!Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCouldn't load keybindings: %sCover si_zeCreates a duplicate of the selected collection.Croatian translationCustom...Czech translationDate addedDebug optionsDelete "%s"?Delete information about recently opened files?Deletes the current file or archive from disk.Deletes the selected books from disk.Deletes the selected collection.DescendingDirectoryDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Do not ask again.Double page modeDuring a slideshow automatically open the next archiveDutch translationEdit BookmarksEdit archiveEn_hance image...Enhance imageEscape key closes programExit from fullscreenFile nameFile orderFile sizeFilesFinished reading on %(date)s, %(time)sFirst pageFit _height modeFit _size modeFit _width modeFit height modeFit to heightFit to size modeFit to widthFit to width or height:Fit width modeFixed size for this mode:Fli_p horizontallyFlip _verticallyFlip pages when scrolling off the edges of the pageFlip two pages in double page modeFlip two pages, instead of one, each time we flip pages in double page mode.Fraction of page to scroll per space key press (in percent):French translationFull pathFullscreenFullscreen modeGalician translationGerman translationGerman translation and Nautilus thumbnailerGo to page...Greek translationGzip compressed tar archiveH_ide allHebrew translationHugeHungarian translationHyperbolic (slow)Icon designImageImage qualityImagesIndonesian translationInvalid path: '%s'Invert smart scrollInvert smart scrolling direction.It reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKeeps the currently selected transformation for the next pages.Keybinding for "%(action)s" overrides hotkey for another action.Korean translationLHA archiveLanguage (needs restart):LargeLast modifiedLast pageLibraryLibrary booksLibrary collectionsLibrary watch listLocationMComix developerM_anual zoom modeMagnification factor:Magnifying LensMagnifying _lensMagnifying lensMagnifying lens size (in pixels):Manga modeManual zoom modeMaximum number of pages to store in the cache:Mi_nimizeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNeverNewNext _archiveNext archiveNext directoryNext pageNo images in '%s'No new books found in directory '%s'.No sortingNo version of the Python Imaging Library was found on your system.Non-supported archive format: %sNormalNormal (fast)Normal sizeNumber of pixels to scroll per arrow key press:Number of pixels to scroll per mouse wheel turn:Only for title pagesOnly for wide imagesOpenOpen _without closing libraryOpen the selected book.Open the watchlist management dialog.Opens the archive editor.Opens the selected books for viewing.Opens the selected books, but keeps the library window open.Original vision/developer of ComixOwnerPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Please note that the only files that are automatically added to this list are those files in archives that MComix recognizes as comments.Polish translatinPolish translationPr_eferencesPreferencesPrevious a_rchivePrevious archivePrevious directoryPrevious pageProper_tiesPropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.Quits and restores the currently opened file next time the program starts.RAR archiveRe_freshRe_nameRecentReloads the currently opened files or archive.Remove books from the library?Remove from archiveRemove from the _libraryRemove from this _collectionRemoved %(num)d book from '%(collection)s'.Removed %(num)d books from '%(collection)s'.Removed %d book from the library.Removed %d books from the library.Removes no longer existant books from the collection.Removes the selected books from the current collection.Rename collection?Renames the selected collection.Replace existing bookmark on page %s?Replace existing bookmarks on pages %s?Replacing it will overwrite its contents.Reset to defaults.RootRotat_e 90 degrees CCWRotate 180 de_greesRotationRussian translationSLIDESHOWS_aturation:S_crollbarsS_harpness:SaveSave _AsSave page asSave the selected values as default for future files.Scaling modeScanning for new books...ScrollScroll downScroll leftScroll rightScroll upSelecting "No" will create a new bookmark without affecting the other bookmarks.Set library cover sizeSet the magnification factor of the magnifying lens.Set the max number of pages to cache. A value of -1 will cache the entire archive.Set the number of pixels to scroll on a page when using a mouse wheel.Set the number of pixels to scroll on a page when using the arrow keys.Set the size of the magnifying lens. It is a square with a side of this many pixels.Sets the desired output log level.Show OSD panelShow file numbersShow filenameShow only one page where appropriate:Show page numbersShow page numbers on thumbnailsShow pathShow resolutionShow the library on startup.Show the version number and exit.Show this help and exit.Simplified Chinese translationSizeSlideshowSlideshow delay (in seconds):Slideshow step (in pixels):SmallSmart scroll downSmart scroll upSpanish translationSt_atusbarStart _slideshowStart slideshowStart the application in double page mode.Start the application in fullscreen mode.Start the application in manga mode.Start the application in slideshow mode.Start the application with zoom set to best fit mode.Start the application with zoom set to fit height.Start the application with zoom set to fit width.Stop slideshowStore information about recently opened files:Store thumbnails for opened filesStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.Stretch images to fit to the screen, depending on zoom mode.Stretch small imagesSwedish translationT_oolbarsTar archiveTh_umbnailsThe file will be deleted from your harddisk.The new archive could not be saved!The original files have not been removed.The selected books will be removed from the library and permanently deleted. Are you sure that you want to continue?This error might be caused by missing GTK+ libraries.This will remove all entries from the "Recent" menu, and clear information about last read pages.Thumbnail size (in pixels):ThumbnailsTinyTraditional Chinese translationTransparencyTreat all files found within archives, that have one of these file endings, as comments.TypeUkrainian translationUnknown filetypeUpgrading library database version from %(from)d to %(to)d.Use a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.Use archive thumbnail as application iconUse checkered background for transparent imagesUse dynamic background colourUse fullscreen by defaultUse smart scrollingUse this colour as background:Use this colour as the thumbnail background:User interfaceView images and comic book archives.View modesWhen active, the ESC key closes the program, instead of only disabling fullscreen mode.While in slideshow mode allow the next archive to automatically be opened.With subdirectoriesWith this preference set, the space key and mouse wheel do not only scroll down or up, but also sideways and so try to follow the natural reading order of the comic book.You stopped reading here on %(date)s, %(time)s. If you choose "Yes", reading will resume on page %(page)d. Otherwise, the first page will be loaded.ZIP archiveZoom _InZoom _OutZoom inZoom modesZoom out[OPTION...] [PATH]_About_Add_Add..._Automatically adjust contrast_Best fit mode_Bookmarks_Brightness:_Cancel_Clean up_Close_Contrast:_Copy_Delete_Double page mode_Duplicate_Edit_Edit Bookmarks..._Edit archive..._File_First page_Fullscreen_Go_Go to page..._Help_Import_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Normal Size_Open_Open..._Previous page_Quit_Recent_Remove_Remove and delete from disk_Rotate 90 degrees CW_Save and quit_Scan now_Search:_Sort_Toolbar_Tools_Transform image_View_Watch list_ZoomProject-Id-Version: MComix 1.00-SVN
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2012-08-16 13:10+0900
Last-Translator: Toshiharu Kudoh 
Language-Team: Japanese
Language: ja
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Plural-Forms: nplurals=1; plural=0;
 of %s! %(function)r のコールバックに失敗しました: %(error)s設定ファイル "%s" は壊れています。消去中です...! pysqlite2 もしくは sqlite3 が見つかりませんでした。! ライブラリーに "%s" を追加できませんでした書籍 %(book)s を コレクション %(collection)s に追加できませんでしたコレクション "%s" を追加できませんでした! ファイル %(sourcefile)s を アーカイブファイル %(archivefile)s に追加できませんでした。中止します...! パス "%s" でアーカイブを作成できませんでした! 書籍 "%s" のカバーを取得できませんでした! アイコン "%s" を読めませんでした! ブックマークファイル %s をパースできませんでした! %s を読めませんでした! ファイル "%s" を削除できませんでした! コレクションを "%s" へ名称変更できませんでした! サムネイル "%(thumbpath)s" を保存できませんでした: %(error)s! 展開エラーです: %s! #%i は存在しない本です! ライブラリーを使用するためには sqlite ラッパーが必要です。%(filename)s の展開サイズは %(actual_size)d バイトですが、%(expected_size)d バイトであるべきです。アーカイブは壊れているか、サポートされていないフォーマットかも知れません。%d コメント%d ページ%s はコミックを扱うために特化してデザインされた画像ビューアーです。%s は GNU General Public License の下でライセンスされています。(コピー)7z アーカイブファイルその名前のコレクションは既に存在します。このライセンスの複製は %s から入手できます'%s' という名前のファイルは既に存在します。置き換えますか?最終アクセス時ブックマークに追加(_B)新たな空コレクションを追加します。本に追加しました本を '%s' に加えます。MComix が開いた全ファイルの情報を、最近開いたファイルの共有リストに追加します。ライブラリーに本を追加します。新規コレクションを追加しますか?追加日%(count)d 個の新しい本が ディレクトリー '%(directory)s' から追加されました。本を追加しました:新しい本 '%(bookname)s' が ディレクトリー '%(directory)s' から追加されました。'%s' を追加中...本を追加中高度な設定全ての本全てのファイル全ての画像常に選択された色を常に背景色に用います。選択された色を常にサムネイルの背景色に用います。外観アーカイブファイルアーカイブファイルは ZIP 形式で保存されます。昇順自動判別(デフォルト)各々のカラーバンド毎にコントラスト(明度と暗度)を自動調節します。全画面表示時に自動的に全てのツールバーを隠す自動的に次のディレクトリーを開く起動時に自動で最後に閲覧したファイルを開く自動的に次のアーカイブファイルを開くアーカイブファイルの最後のページの次に進もうとした時には同じディレクトリー内の次のアーカイブファイルを、或いは最初のページの前に進もうとした時には前のアーカイブファイルを自動的に開きます。MComix が最後に終了した時開いていたファイルを、起動時に自動的に開きます。画像に合う背景色を自動選択します。メタデータに従い画像を自動的に回転するExif タグのようなメタデータに画像の回転方向が示されている時は、自動的に画像を回転します。ライブラリーが開かれているとき自動的に新しい本をスキャン(_O)閲覧している画像のサムネイルの背景に合う色を自動的に用います。背景挙動ベストフィットモードバイリニア本の名前(ブラジル)ポルトガル語への翻訳この設定が有効となることにより、本の最初のページが標準アイコンの代わりにアプリケーションアイコンとして使用されます。Bzip2 圧縮されたアーカイブファイルカタロニア語への翻訳本のカバーサイズを変更します。ライブラリーのソート順を変更します。以前に再度聞かれないようにしたダイアログ選択のすべてをクリアします。全ての開かれているファイルを閉じます。コメント(_M)...コレクションコメントファイルの拡張子:コメントファイルコメントライブラリーから選択された本を完全に削除します。%d ページから続けて閲覧していきますか?現在のページをクリップボードにコピーしました。選択された本のパスをクリップボードにコピーします。'%s' という新規コレクションを追加できませんでした。'%s' へ名称変更できませんでした。ライブラリーデータベースのバージョンが特定できませんでした!コレクションを複製できませんでした。'%s' を開けませんでした。ファイルが存在しません。'%s' を開けませんでした。パーミッションが許可されていません。%s を読めませんでした! キーバインディング "%s" を読めませんでしたカバーサイズ(_Z)選択されたコレクションの複製を作成します。クロアチア語への翻訳カスタム...チェコ語への翻訳日付追加デバッグオプション"%s" を消去しますか?最近開いたファイルについての情報を消去しますか?ディスクから現在のファイルを消去します。選択された本をディスクから消去します。選択されたコレクションを消去します。降順ディレクトリー表示フルパス名に次の文字列が含まれている本のみを表示します。検索では大文字小文字を区別しません。再度聞かない。見開き表示スライドショーの間、次のアーカイブファイルを自動的に開くオランダ語への翻訳ブックマークの編集アーカイブファイルを編集画像の調整(_H)画像の調整エスケープキーでプログラムを終了する全画面表示から退出ファイル名ファイルの順序ファイルサイズファイル%(date)s 、%(time)s に閲覧終了最初のページ高さに合わせる(_H)フィットサイズモード(_S)横幅に合わせる(_W)高さに合わせる高さに合わせるサイズ固定モード横幅に合わせる横幅か高さに合わせる横幅に合わせるこのモードで固定されるサイズ:左右をフリップ(_P)上下をフリップ(_V)スクロールがページの端を越えた時、ページを送る見開き表示の時、2ページずつページを送る見開き表示の時、1ページずつではなく、2ページずつ表示を進めます。スペースキーを押すごとにスクロールするページの割合 (パーセント):フランス語への翻訳フルパス全画面表示全画面モードガリシア語への翻訳ドイツ語への翻訳ドイツ語への翻訳と Nautilus のサムネイル作成ページへ移動...ギリシア語への翻訳Gzip 圧縮されたアーカイブファイル全て隠す(_I)ヘブライ語への翻訳巨大ハンガリー語への翻訳ハイパーボリック (低速)アイコンのデザイン画像画質画像インドネシア語への翻訳無効なパスです: '%s'反対方向にスマートスクロールスマートスクロールの方向を反対にします。ZIP, RAR, tar 形式のアーカイブファイルや、普通の画像ファイルを読むことができます。イタリア語への翻訳日本語への翻訳現在選択された回転状態を次ページ以降も維持します。"%(action)s" のキーバインディングは他のアクションのホットキーをオーバーライドします。朝鮮語への翻訳LHA アーカイブファイル言語(要再起動):大最終修正時最後のページライブラリーライブラリーの本ライブラリーコレクションライブラリーウォッチリスト位置MComix の開発者手動でズームを調整する(_A)拡大率:拡大レンズ拡大レンズ(_L)拡大レンズ拡大するサイズ(ピクセル数):マンガのページ順に表示手動でズームを調整するキャッシュに蓄積するページの最大数:最小化(_N)最終修正時本をコレクション '%(source collection)s' から '%(destination collection)s' に移動します。名前しない新規次のアーカイブファイル(_A)次のアーカイブファイル次のディレクトリー次のページ'%s' の中には画像がありませんディレクトリー '%s' に新しい本はありません。ソートしないPython Imaging Library のどのバージョンもシステムには見つかりませんでした。サポートされていないアーカイブ形式です: %s普通通常 (高速)通常サイズ矢印キーを押すごとにスクロールするピクセル数:マウスホイールを回すごとにスクロールするピクセル数:タイトルページのみワイド画像のみ開くライブラリーを閉じずに開く(_W)選択された本を開きます。ウォッチリスト管理ダイアログを開きます。アーカイブファイルエディターを開きます。閲覧のために選択された本を開きます。ライブラリーウィンドウを維持したまま、選択された本を開きます。原案/Comix の開発者所有者ページパーミッションペルシア語への翻訳新規コレクションの名称を入力してください。選択されたコレクションの新しい名前を入力してください。注意:MComix がコメントファイルだと認識するアーカイブファイル内のファイルだけが、自動的にこのリストに追加されます。ポーランド語への翻訳ポーランド語への翻訳設定(_E)設定前のアーカイブファイル(_R)前のアーカイブファイル前のディレクトリー前のページプロパティー(_T)プロパティーコレクション '%(subcollection)s' をコレクション '%(supercollection)s' の中に入れます。次回プログラムを起動した際、現在開かれているファイルを復元するようにしてから終了します。RAR アーカイブファイル再読み込み(_F)名称変更(_N)最近のファイルを開く現在開かれているファイル、もしくはアーカイブファイルを再読み込みします。ライブラリーから本を削除しますか?アーカイブファイルから削除ライブラリーから削除(_L)このコレクションから削除(_C)%(num)d 個の本が '%(collection)s' から削除されました。%(num)d 個の本が '%(collection)s' から削除されました。%d 個の本がライブラリーから削除されました。%d 個の本がライブラリーから削除されました。コレクションから存在しない本を削除します。現在のコレクションから選択された本を削除します。コレクションの名前を変更しますか?選択されたコレクションの名称を変更します。ページ %s の現ブックマークを置き換えますか?ページ %s の現ブックマークを置き換えますか?置き換えると元のファイルの内容が上書きされます。デフォルトにリセットします。ルート左回りに90度回転(_E)180度回転(_G)回転ロシア語への翻訳SLIDESHOW彩度(_A):スクロールバー(_C)シャープネス(_H):保存別名で保存(_A)別名でページを保存将来のファイルのデフォルトとして選択された値を保存します。スケーリングモード新しい本を検索中...スクロールスクロールダウン左にスクロール右にスクロールスクロールアップ"No" の選択は他のブックマークへの影響なしに、新しいブックマークを作成します。ライブラリーのカバーサイズを指定レンズの拡大率を設定します。キャッシュするページの最高数を設定します。-1の値はアーカイブ全てをキャッシュします。マウスホイールを使う際にページ上でスクロールするピクセル数を設定します。矢印キーを使う際にページ上でスクロールするピクセル数を設定します。レンズが拡大する範囲のサイズを設定します。一辺がこのピクセル数の正方形状の範囲が拡大されます。望む出力ログレベルを設定する。OSD パネルを表示ファイル数を表示ファイル名を表示見開き表示の時、幅が広い画像の時は1枚だけ表示する:ページ数を表示サムネイルの上にページ数を表示するパスを表示解像度を表示起動時にライブラリーを表示する。バージョンを表示して終了する。このヘルプを表示して終了する。簡体字中国語への翻訳サイズスライドショースライドショーの間隔(秒単位):スライドショーの間隔(ピクセル数):小下にスマートスクロール上にスマートスクロールスペイン語への翻訳ステータスバー(_A)スライドショーを動かす(_S)スライドショーを動かす見開き表示でアプリケーションを起動する。全画面モードでアプリケーションを起動する。漫画のページ順でアプリケーションを起動する。スライドショーモードでアプリケーションを起動する。ベストフィットモードでアプリケーションを起動する。高さに合わせてアプリケーションを起動する。横幅に合わせてアプリケーションを起動する。スライドショーを止める最近開いたファイルの情報を保存する:開いたファイルのサムネイルを保存する開いたファイルのサムネイルを freedesktop.org の規格に従って保存します。これらのサムネイルは、ほとんどのファイルマネージャー等、多くのアプリケーションが共有して使うことができます。ズームモードに応じてスクリーンに適応するように画像を引きのばします。小さな画像を引きのばすスウェーデン語への翻訳ツールバー(_O)Tar アーカイブファイルサムネイル(_U)ファイルはあなたのハードディスクから消去されます。新しいアーカイブファイルが保存できませんでした!元のファイルは削除されません。選択された本はライブラリーから削除され、永久に消去されます。よろしいですか?このエラーは恐らく欠損している GTK+ ライブラリーによりひき起こされています。これは全てのエントリーを "最近のファイルを開く" から削除し、最後に閲覧したページについての情報をクリアします。サムネイルのサイズ(ピクセル数):サムネイル極小繁体字中国語への翻訳透過度アーカイブファイルの中にある、これらの拡張子を持つファイルを全てコメントとして扱います。種類ウクライナ語への翻訳不明なファイル形式ですライブラリーデータベースのバージョンを %(from)d から %(to)d へアップグレードしています。透過画像の背景にグレーの格子模様を使用します。もし、この項目が設定されていない場合は、代わりの背景は単色の白色となります。アプリケーションアイコンとしてアーカイブサムネイルを用いる透過画像の背景に格子模様を用いる背景色を自動選択するデフォルトで全画面表示にするスマートスクロールを使用するこの色を背景に用いる:この色をサムネイルの背景に用いる:ユーザーインターフェース画像とコミックアーカイブファイルを閲覧します。モードを表示アクティブなとき、ESC キーで全画面表示モードを無効にする代わりにプログラムを終了します。スライドショーモードの間、次のアーカイブファイルが自動的に開かれるのを許可します。サブディレクトリー含むこの設定を行うと、スペースキーとマウスホイールは上下へのスクロールを行うだけではなく左右にもスクロールするようになり、コミックを読む自然な流れに従おうとします。%(date)s 、%(time)s にこのページで閲覧を止めています。もし "Yes" を選択すれば、閲覧を %(page)d ページで再開します。そうでなければ、最初のページが読み込まれます。ZIP アーカイブファイル拡大(_I)縮小(_O)拡大ズームモード縮小[オプション...] [パス]MComix について(_A)追加(_A)追加(_A)...コントラストを自動調節する(_A)ベストフィットモード(_B)ブックマーク(_B)明度(_B):キャンセル(_C)クリーンアップ(_C)閉じる(_C)コントラスト(_C):コピー(_C)消去(_D)見開き表示(_D)複製(_D)編集(_E)ブックマークの編集(_E)...アーカイブファイルを編集(_E)ファイル(_F)最初のページ(_F)全画面表示(_F)移動(_G)ページへ移動(_G)...ヘルプ(_H)インポート(_I)回転の状態を維持する(_K)最後のページ(_L)ライブラリー(_L)...マンガのページ順に表示(_M)メニューバー(_M)次のページ(_N)通常サイズ(_N)開く(_O)開く(_O)前のページ(_P)終了(_Q)最近のファイルを開く(_R)削除(_R)削除してディスクから消去(_R)右回りに90度回転(_R)保存して終了(_S)直ちにスキャン(_S)検索(_S):ソート(_S)ツールバー(_T)ツール(_T)画像の回転(_T)表示(_V)ウォッチリスト(_W)ズーム(_Z)././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/ja/__init__.py0000644000175000017500000000000014476523373020313 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/ko/0000755000175000017500000000000014553265237016231 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/ko/LC_MESSAGES/0000755000175000017500000000000014553265237020016 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/ko/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022117 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/ko/LC_MESSAGES/mcomix.mo0000644000175000017500000013355314476523373021663 0ustar00moritzmoritz ++)+,",+O,({,:,,K,'K-#s--#---%.5-.c.z.9.0.0.-./\//0
0	0B 0Ac00000
1)1.:1<i11
111	11Z2`222922=23-3:3C3	P3	Z3
d3o39v3C334
44'4884!q4	44]4-5!?5a5226#e66S7Bo777k78V8U8889
9	(9
29@9	I9 S9qt99:r::&::P:5;;;T;
a;l;
t;;;
;;7;;1<%M<-s<+<"<-<= >=%_==1===/=#>	8>B>
T>
_>m>t>/>.>%> ?
'?	2?jk'kLkC>l>lLl1mb@mCm@m6(n3_n$n3nEn42ogoo'oDo1p7@pxp	dqnq{q	q1q1q7q-r$4r$Yr~r.r8r@r7sHs]s"rs
ss[s)
t!7tYtOjttPtu
&u1u8u
Lu
Wueuvu2}u?uu
v"v)v6v8Jv.vvvJv-#w'QwywL x'mxxECy5y:yXy/Sz8z%z%z{	"{,{
3{A{
Q{!\{q~{{|*||%|
|Z}c}j}
}
}	}}!}}
}~D~6L~D~+~~4
-?Hm*FI("rC/ـ	1Pjz5ҁ<+Eq$̂tӂHb;wʃۃ,K`
}	

„2Є
0IzԆ&:S
gu\E
9SLڈ4:
N\i}8މ*I[	oy	Ê	ԊOފF.u3 Ë$"	J,w@YVgxōՍ 
$2LSd	
	 ǎ1)6
`	nxN؏
ߏ
'.FZn3:*d>%ɑБ
ߑ)	)3]#t.ߒ)%,Eir#ܓ	

	&.=<lpk

ƕԕ%BbsK̖BӖ'8
J
X0f6Η, :MF;Ϙ-"9J\™5ڙ	8
JXuŚ՚ݚ
::N'<	2Icќ +C^io'ٝ'Z)G̞Ya'g(J%:KA_$
ޢ+))%Sy!$!ۣ!7>!NpϤ21
?)7i1>Ӧ88K8ͧ*JŨ!2
FQa9r0/ݩ.
k<O.x'ԫ۫	]lsQG7ޭ #7[
y,
ˮ
ٮ|pL:U9Ųg
x


˳
0
BP
X
c
ny


´ڴ
$5$F
ky$ϵ

)
4?
T_
q#|Ͷ


$
9DYtUoxr]R?	G?=#t04&^VvW[}pS%)M6'e*Jf1Tvhg~jZ`\J^OW 
BEHUEiyh[@((/$V
=A|s#>|5z;I209.3m:k7e
-FBu`"PXs,$%j1XFDlaLG4d9
<)fTIRH!_ZC	q*z5&P;+wNp6nYLMlo3Q>q,dOwc\bmc8
D.Cb/N	n_{u7Q!}@"-a+8 'r2y~Sk
AYK{:<Kgxi] of %s! Callback %(function)r failed: %(error)s! Corrupt preferences file "%s", deleting...! Could neither find pysqlite2 nor sqlite3.! Could not add book "%s" to the library! Could not add book %(book)s to collection %(collection)s! Could not add collection "%s"! Could not add file %(sourcefile)s to archive %(archivefile)s, aborting...! Could not create archive at path "%s"! Could not get cover for book "%s"! Could not load icon "%s"! Could not parse bookmarks file %s! Could not read %s! Could not remove file "%s"! Could not rename collection to "%s"! Could not save thumbnail "%(thumbpath)s": %(error)s! Extraction error: %s! Non-existant book #%i! Worker thread processing %(function)r failed: %(error)s! You need an sqlite wrapper to use the library."%s" does not appear to have a valid executable."%s" does not have a valid working directory.%(filename)s's extracted size is %(actual_size)d bytes, but should be %(expected_size)d bytes. The archive might be corrupt or in an unsupported format.%d comments%d pages%s archives%s images%s is an image viewer specifically designed to handle comic books.%s is licensed under the terms of the GNU General Public License.'%s' is disabled for archives.(Copy)...when height exceeds width...when width exceeds height7z archiveA collection by that name already exists.A copy of this license can be obtained from %sA file named '%s' already exists. Do you want to replace it?AccessedAdd _BookmarkAdd _separatorAdd a new empty collection.Add booksAdd books to '%s'.Add information about all files opened from within MComix to the shared recent files list.Add more books to the library.Add new collection?AddedAdded %(count)d new books from directory '%(directory)s'.Added books:Added new book '%(bookname)s' from directory '%(directory)s'.Adding '%s'...Adding booksAdvancedAll archivesAll booksAll filesAll imagesAlwaysAlways use this selected colour as the background colour.Always use this selected colour as the thumbnail background colour.Animated imagesAnimation mode:AppearanceArchiveArchive commentsArchive-related variables can only be used for archives.Archives are stored as ZIP files.AscendingAuto-detect (Default)Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically hide all toolbars in fullscreenAutomatically open next directoryAutomatically open the first file in the next sibling directory when flipping past the last page of the last file in a directory, or the previous directory when flipping past the first page of the first file.Automatically open the last viewed file on startupAutomatically open the next archiveAutomatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically open, on startup, the file that was open when MComix was last closed.Automatically pick a background colour that fits the viewed image.Automatically rotate images according to their metadataAutomatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.Automatically scan for new books when library is _openedAutomatically use the colour that fits the viewed image for the thumbnail background.Autorotate by heightAutorotate by widthBack ten pagesBackgroundBehaviourBest fit modeBilinearBook nameBrazilian Portuguese translationBy enabling this setting, the first page of a book will be used as application icon instead of the standard icon.Bzip2 compressed tar archiveCatalan translationChanges how images are scaled. Slower algorithms result in higher quality resizing, but longer page loading times.Changes the book cover size.Changes the sort order of the library.Clear _dialog choicesClears all dialog choices that you have previously chosen not to be asked again.CloseCloses all opened files.Co_mments...CollectionCommandCommand labelCommand line is empty.Comment extensions:Comment filesCommentsCompletely removes the selected books from the library.Continue reading from page %d?Controls how animated images should be displayed.Copies the current page to clipboard.Copies the selected book's path to clipboard.Could not add a new collection called '%s'.Could not change the name to '%s'.Could not determine library database version!Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCould not run command %(cmdlabel)s: %(exception)sCouldn't load keybindings: %sCover si_zeCreates a duplicate of the selected collection.Croatian translationCustom...Czech translationDate addedDebug optionsDeleteDelete "%s"?Delete information about recently opened files?Deletes the current file or archive from disk.Deletes the selected books from disk.Deletes the selected collection.DescendingDirectoryDisabled in archivesDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Do not ask again.Double page modeDuring a slideshow automatically open the next archiveDutch translationEdit BookmarksEdit archiveEdit external commandsEn_hance image...Enhance imageEscape key closes programExecute external commandExit from fullscreenExternal commandsExtraction and cacheFileFile nameFile orderFile sizeFile-related variables can only be used for files.FilesFiles will be opened and displayed according to the sort order specified here. This option does not affect ordering within archives.Files within archives will be sorted according to the order specified here. Natural order will sort numbered files based on their natural order, i.e. 1, 2, ..., 10, while literal order uses standard C sorting, i.e. 1, 2, 34, 5.Finished reading on %(date)s, %(time)sFirst pageFit _height modeFit _size modeFit _width modeFit height modeFit size modeFit to heightFit to size modeFit to widthFit to width or height:Fit width modeFixed size for this mode:Fli_p horizontallyFlip _verticallyFlip horizontallyFlip pages when scrolling "off the page" with the scroll wheel or with the arrow keys. It takes n consecutive "steps" with the scroll wheel or the arrow keys for the pages to be flipped.Flip pages when scrolling off the edges of the pageFlip two pages in double page modeFlip two pages, instead of one, each time we flip pages in double page mode.Flip verticallyForward ten pagesFraction of page to scroll per space key press (in percent):French translationFull pathFullscreenFullscreen modeGalician translationGerman translationGerman translation and Nautilus thumbnailerGo to pageGo to page...Greek translationGzip compressed tar archiveH_ide allHebrew translationHugeHungarian translationHyperbolic (slow)Icon designImageImage qualityImagesIncomplete escape sequence. For a literal '%', use '%%'.Incomplete quote sequence. For a literal '"', use '%"'.Indonesian translationInvalid escape sequence: %%%sInvalid path: '%s'Invert smart scrollInvert smart scrolling direction.It reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKeep transformationKeeps the currently selected transformation for the next pages.Key %dKeybinding for "%(action)s" overrides hotkey for another action.Korean translationLHA archiveLabelLanguage (needs restart):LargeLast modifiedLast pageLibraryLibrary booksLibrary collectionsLibrary watch listLiteral orderLithuanian translationLocationMComix developerM_anual zoom modeMagnification factor:Magnifying LensMagnifying _lensMagnifying lensMagnifying lens size (in pixels):Manga modeManual zoom modeMaximum number of concurrent extraction threads:Maximum number of pages to store in the cache:Mi_nimizeMinimizeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNatural orderNavigationNeverNever autorotateNewNext _archiveNext archiveNext directoryNext pageNext page (always one page)Next page (dynamic)No images in '%s'No new books found in directory '%s'.No sortingNo version of the Python Imaging Library was found on your system.Non-supported archive format: %sNormalNormal (fast)Normal sizeNumber of "steps" to take before flipping the page:Number of pixels to scroll per arrow key press:Number of pixels to scroll per mouse wheel turn:Only for title pagesOnly for wide imagesOpenOpen _withOpen _without closing libraryOpen the selected book.Open the watchlist management dialog.Opens the archive editor.Opens the selected books for viewing.Opens the selected books, but keeps the library window open.Original vision/developer of ComixOwnerPDF documentPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Please note that the only files that are automatically added to this list are those files in archives that MComix recognizes as comments.Please refer to the external command documentation for a list of usable variables and other hints.Polish translatinPolish translationPr_eferencesPreferencesPreview:Previous a_rchivePrevious archivePrevious directoryPrevious pagePrevious page (always one page)Previous page (dynamic)Proper_tiesPropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.QuitQuits and restores the currently opened file next time the program starts.RAR archiveRe_freshRe_nameRecentRefreshReloads the currently opened files or archive.Remove books from the library?Remove from archiveRemove from the _libraryRemove from this _collectionRemoved %(num)d book from '%(collection)s'.Removed %(num)d books from '%(collection)s'.Removed %d book from the library.Removed %d books from the library.Removes no longer existant books from the collection.Removes the selected books from the current collection.Rename collection?Renames the selected collection.Replace existing bookmark on page %s?Replace existing bookmarks on pages %s?Replacing it will overwrite its contents.Reset to defaults.Resets all keyboard shortcuts to their default values.RootRotat_e 90 degrees CCWRotate 180 de_greesRotate 180 degreesRotate 90 degrees CCWRotate 90 degrees CWRotationRun _commandRussian translationSLIDESHOWS_aturation:S_crollbarsS_harpness:SaveSave AsSave _AsSave and quitSave changes to commands?Save page asSave the selected values as default for future files.Scaling modeScanning for new books...ScrollScroll downScroll leftScroll rightScroll to bottom centerScroll to bottom leftScroll to bottom rightScroll to centerScroll to middle leftScroll to middle rightScroll to top centerScroll to top leftScroll to top rightScroll upSelecting "No" will create a new bookmark without affecting the other bookmarks.Set library cover sizeSet the magnification factor of the magnifying lens.Set the max number of pages to cache. A value of -1 will cache the entire archive.Set the maximum number of concurrent threads for formats that support it.Set the number of "steps" needed to flip to the next or previous page.  Less steps will allow for very fast page turning but you might find yourself accidentally turning pages.Set the number of pixels to scroll on a page when using a mouse wheel.Set the number of pixels to scroll on a page when using the arrow keys.Set the size of the magnifying lens. It is a square with a side of this many pixels.Sets the desired output log level.Sets the percentage by which the page will be scrolled down or up when the space key is pressed.ShortcutsShow OSD panelShow file numbersShow filenameShow filesizeShow only one page where appropriate:Show page numbersShow page numbers on thumbnailsShow pathShow resolutionShow the library on startup.Show the version number and exit.Show this help and exit.Show/hide allShow/hide menubarShow/hide scrollbarsShow/hide statusbarShow/hide toolbarSimplified Chinese translationSizeSlideshowSlideshow delay (in seconds):Slideshow step (in pixels):SmallSmart scroll downSmart scroll upSort archives by:Sort files and directories by:Spanish translationSpecify the number of pixels to scroll while in slideshow mode. A positive value will scroll forward, a negative value will scroll backwards, and a value of 0 will cause the slideshow to always flip to a new page.St_atusbarStart _slideshowStart slideshowStart the application in double page mode.Start the application in fullscreen mode.Start the application in manga mode.Start the application in slideshow mode.Start the application with zoom set to best fit mode.Start the application with zoom set to fit height.Start the application with zoom set to fit width.Stop slideshowStore information about recently opened files:Store thumbnails for opened filesStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.Stretch images to fit to the screen, depending on zoom mode.Stretch small imagesSwedish translationT_oolbarsTar archiveTh_umbnailsThe archive is password-protected:The file will be deleted from your harddisk.The new archive could not be saved!The original files have not been removed.The selected books will be removed from the library and permanently deleted. Are you sure that you want to continue?This error might be caused by missing GTK+ libraries.This is a separator pseudo-command.This will remove all entries from the "Recent" menu, and clear information about last read pages.Thumbnail size (in pixels):ThumbnailsTinyTraditional Chinese translationTransformationTransparencyTreat all files found within archives, that have one of these file endings, as comments.TypeUkrainian translationUnknown filetypeUpgrading library database version from %(from)d to %(to)d.Use a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.Use archive thumbnail as application iconUse checkered background for transparent imagesUse dynamic background colourUse fullscreen by defaultUse smart scrollingUse this colour as background:User interfaceView images and comic book archives.View modeView modesWhen active, the ESC key closes the program, instead of only disabling fullscreen mode.When showing the first page of an archive, or an image's width exceeds its height, only a single page will be displayed.While in slideshow mode allow the next archive to automatically be opened.With subdirectoriesWith this preference set, the space key and mouse wheel do not only scroll down or up, but also sideways and so try to follow the natural reading order of the comic book.Working directoryXZ compressed tar archiveYou have made changes to the list of external commands that have not been saved yet. Press "Yes" to save all changes, or "No" to discard them.You stopped reading here on %(date)s, %(time)s. If you choose "Yes", reading will resume on page %(page)d. Otherwise, the first page will be loaded.ZIP archiveZoomZoom _InZoom _OutZoom inZoom modesZoom out[OPTION...] [PATH]_About_Add_Add..._Auto-rotate image_Automatically adjust contrast_Best fit mode_Bookmarks_Brightness:_Cancel_Clean up_Close_Contrast:_Copy_Delete_Double page mode_Duplicate_Edit_Edit Bookmarks..._Edit archive..._Edit commands_File_First page_Fullscreen_Go_Go to page..._Help_Import_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Normal Size_Open_Open..._Previous page_Quit_Recent_Remove_Remove and delete from disk_Reset keys_Rotate 90 degrees CW_Save and quit_Scan now_Search:_Sort_Toolbar_Tools_Transform image_View_Watch list_ZoomProject-Id-Version: 
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2018-05-10 22:46+0900
Last-Translator: Minho Jeung 
Language-Team: 
Language: ko_KR
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
X-Poedit-SourceCharset: utf-8
Plural-Forms: nplurals=1; plural=0;
X-Generator: Poedit 2.0.4
 %s 의! 콜백 %(function)r 실패: %(error)s! 환경설정 파일 "%s"이(가) 손상되었습니다. 삭제하는중...! pysqlite2, sqlite3 둘 중 어느 것도 찾을 수 없습니다.! 책 "%s" 을 라이브러리에 추가하지 못했습니다! 책 %(book)s 을 모음집 %(collection)s 에 추가하지 못했습니다! 모음집을 추가하지 못했습니다: "%s"! 파일 %(sourcefile)s을(를) %(archivefile)s에 추가할 수 없습니다. 취소하는 중...! 다음 경로에 압축파일을 생성할 수 없습니다: "%s"! 다음 책의 커버 이미지를 얻지 못했습니다: "%s"! 다음 아이콘을 불러올 수 없습니다: "%s"! 책갈피 파일 %s를 해석할 수 없습니다! %s을(를) 읽을 수 없습니다! 다음 파일을 제거할 수 없습니다: "%s"! 모음집 이름을 다음으로 변경하지 못했습니다: "%s"! 섬네일 저장 실패 "%(thumbpath)s": %(error)s! 압축 해제 에러: %s! 존재하지 않는 책: #%i! 콜백 %(function)r 실패: %(error)s라이브러리를 사용하려면 sqlite 래퍼가 필요합니다."%s" 는 적절한 실행 파일이 아닙니다."%s" 는 적절한 작업 디렉토리가 아닙니다.파일 %(filename)s의 압축 해제된 크기는 %(actual_size)d 바이트입니다. 그러나 원래는 
%(expected_size)d 바이트여야 합니다. 이 압축 파일은 손상되었거나 지원되지 않는 파일 형식입니다.%d 주석%d 페이지%s archives%s images%s는 만화책 전문 이미지 뷰어입니다.%s는 GNU General Public License를 따릅니다.'%s' 는 압축 파일에 대해 비활성화됩니다.(Copy)...세로가 가로보다 길 경우...가로가 세로보다 길 경우7z 압축파일같은 이름의 모음집이 존재합니다.라이센스의 전문은 %s 에서 볼 수 있습니다'%s' 파일이 이미 있습니다. 덮어씌우시겠습니까?마지막 접근책갈피 추가(_B)구분자 추가(_s)비어있는 새 모음집 추가.책 추가책 추가 '%s'.메뉴의 [파일 - 최근 파일]에 MComix로 열었던 파일 정보를 추가합니다.라이브러리에 더 많은 책 추가.새 모음집을 추가합니까?추가된 날짜새 책 %(count)d 권을 디렉토리 '%(directory)s'에서 추가했습니다.책 추가:새 책 '%(bookname)s'을 디렉토리 '%(directory)s'에서 추가했습니다.추가 '%s'...책 추가고급모든 압축파일모든 책모든 파일모든 이미지항상이 색을 항상 배경색으로 사용합니다.이 색을 항상 미리보기 배경색으로 사용합니다.애니메이션 이미지애니메이션 모드:보기압축파일압축파일 주석Archive-related variables can only be used for archives.ZIP 압축파일로 저장되어 있습니다.오름차순자동 감지(디폴트)각 색 대역마다 따로 자동으로 명암 대비를 조정합니다.전체화면 상태에서 도구모음 숨김자동으로 다음 디렉토리 열기압축 파일의 처음과 끝에서 이전/다음으로 페이지를 넘기면
이전/다음 압축파일을 불러옵니다.
형제 디렉토리에 적용됩니다.프로그램 시작 시 마지막에 열었던 파일을 자동으로 열기자동으로 다음 압축파일 열기압축 파일의 처음과 끝에서 이전/다음으로 페이지를 넘기면
이전/다음 압축파일을 불러옵니다.
같은 디렉토리 안에만 적용됩니다.MComix로 열었던 마지막 파일을 자동으로 열어봅니다.자동으로 이미지에 맞는 배경색을 선택.이미지를 메타데이터에 따라 자동으로 회전이미지를 Exif 태그 등 메타데이터에 따라 
자동으로 회전시킵니다.새 책을 모음집에 자동으로 추가(_o)자동으로 미리보기에 맞는 배경색을 선택.세로 길이에 따라 자동 회전가로 길이에 따라 자동 회전10 페이지 이전으로배경색일반최적 맞춤바이리니어책 이름포르투갈어(브라질) 번역체크하면 책의 첫 페이지가 기본 아이콘 대신 
애플리케이션 아이콘으로 사용됩니다.Bzip2으로 된 Tar압축파일카탈로니아어 번역이미지 확대 방법을 변경합니다. 
느린 알고리즘은 좋은 이미지 품질을 보장하지만 시간이 오래 걸립니다.북커버 사이즈 변경.라이브러리 정렬 순서 변경.변경 취소다이얼로그에서 선택한 것을 모두 취소합니다. 다시 묻지 않습니다.닫기열린 파일 모두 닫기.주석...(_m)새 모음집명령어명령어 라벨명령행이 비어 있습니다.주석 확장자:주석 파일주석선택된 책들을 라이브러리에서 완전히 제거합니다.%d 페이지부터 계속해서 읽으시겠습니까?애니메이션 이미지가 어떻게 출력될지 결정합니다.현재 페이지를 클립보드에 복사.선택한 책 열기.새 모음집을 추가하지 못했습니다: '%s'.이름을 변경하지 못했습니다. '%s'.라이브러리 데이터베이스 버전을 특정할 수 없습니다!모음집을 복제하지 못했습니다.%s을(를) 열 수 없습니다.: 파일이 존재하지 않습니다.%s을(를) 열 수 없습니다.: 접근 권한이 거부되었습니다.%s을(를) 읽을 수 없습니다명령 %(cmdlabel)s 을 실행하지 못했습니다: %(exception)s키 설정을 불러오지 못했습니다.: %s커버 사이즈(_z)선택된 모음집의 복제본을 만듭니다.크로아티아어 번역고유설정...체코어 번역추가된 날짜디버그 옵션삭제"%s"을(를) 삭제합니까?최근 연 파일 정보를 삭제하시겠습니까?현재 파일 또는 압축파일을 디스크에서 삭제.선택된 책들을 디스크에서 삭제.선택된 모음집 제거.내림차순디렉토리압축 파일 안에서 비활성화표시전체 경로에 지정된 문자열이 책을 표시합니다. 검색은 대소문자를 구분하지 않습니다.더 이상 묻지 않음.단면/양면 보기슬라이드쇼 중 자동으로 다음 압축파일 열기네덜란드어 번역책갈피 편집압축파일 편집외부 명령어 편집이미지 보정...이미지 보정ESC 키로 프로그램 종료외부 명령 실행전체화면에서 나가기외부 명령압축 해제와 캐시파일파일명파일 순서파일 크기File-related variables can only be used for files.파일파일은 여기서 지정한 순서로 열립니다. 
이 옵션은 압축 파일 자체의 순서에는 영향을 주지 않습니다.압축 파일 안의 파일은 여기서 명시한 순서에 따라 정렬됩니다. 
자연 순서는 1, 2, 3, 23 순으로,
기본 순서는 1, 2, 23, 3 순으로 정렬됩니다.%(date)s, %(time)s 에 읽기를 마쳤습니다처음 페이지세로길이 맞춤(_H)원본 크기 보기(_s)가로길이 맞춤(_W)세로길이 맞춤원본 크기 보기세로길이 맞춤원본 크기 보기가로길이 맞춤이미지 맞춤 기준:가로길이 맞춤가로길이:좌우 뒤집기(_P)상하 뒤집기(_V)좌우 뒤집기마우스 스크롤이나 방향키를 이용해서 페이지를 전환할 수 있습니다.페이지의 가장자리에서 스크롤 할 때 페이지 넘기기양면 보기 모드일때 페이지를 2장씩 넘기기양면 보기 모드일 경우 페이지를 한 번에 2장씩 넘깁니다.상하 뒤집기10 페이지 다음으로스페이스 키로 스크롤 할 부분 (퍼센트)프랑스어 번역전체 경로전체화면풀스크린 모드갈리시아어 번역독일어 번역독일어 번역, Nautilus의 미리보기 기능 제작특정 페이지 바로가기페이지 바로가기...그리스어 번역Gzip으로 된 Tar압축파일모두 숨김(_I)히브리어 번역초대형헝가리어 번역하이퍼볼릭(느림)아이콘 디자인이미지이미지 품질이미지불완전한 이스케이프 시퀀스. '%' 대신 '%%' 를 사용하십시오.불완전한 인용 시퀀스. '"' 대신 '%"' 를 사용하십시오.인도네시아어 번역적합하지 않은 이스케이프 시퀀스: %%%s유효하지 않은 경로: '%s'스마트 스크롤 방향 뒤집기스마트 스크롤 방향 역전.ZIP, RAR, tar 압축파일 등과 일반 이미지 파일을 읽습니다.이탈리아어 번역일본어 번역회전 상태 유지현재 선택된 회전 설정을 다음 페이지에도 적용.키 %d"%(action)s"에 할당한 키는 다른 동작의 단축키보다 우선 동작합니다.한국어 번역LHA 압축파일라벨언어(재시작 필요):대형수정한 날짜마지막 페이지라이브러리라이브러리 북라이브러리 모음집라이브러리 와치 리스트기본 순서리투아니아어 번역위치MComix 개발자수동 확대/축소 조절(_A)배율:돋보기돋보기(_l)돋보기돋보기 크기(픽셀)일본만화 페이지순 보기수동 확대/축소 조절압축 해제 시 사용할 최대 스레드 수:캐시에 저장할 최대 페이지 수:최소화(_n)최소화마지막 수정'%(source collection)s' 에서 '%(destination collection)s' 으로 책 이동.이름자연 순서내비게이션하지 않음자동 회전 하지 않음새로다음 압축파일(_a)다음 압축파일다음 디렉토리다음 페이지다음 페이지(한장씩)다음 페이지 (계속해서)다음 디렉토리에 파일이 없습니다: '%s'디렉토리 '%s'에서 새 책을 찾지 못했습니다.정렬하지 않음어떤 버전의 파이썬 이미지 라이브러리(PIL)도 시스템에서 찾지 못했습니다.지원하지 않는 압축 형식: %s노멀노멀(빠름)원래 크기페이지 넘김 민감도:화살표키로 스크롤 할 픽셀 수:마우스휠로 스크롤 할 픽셀 수:타이틀 페이지만가로길이가 넓은 이미지만열기추가기능(_w)라이브러리를 닫지 않고 엽니다(_w)선택한 책 열기.와치 리스트 관리 창을 엽니다.압축 파일 편집기를 엽니다.선택된 책들을 보기 위해 엽니다.선택된 책들을 보기 위해 엽니다. 그러나 라이브러리 윈도우는 연 채로 둡니다.오리지널 버전/Comix 개발자소유자PDF 문서페이지권한페르시아어 번역새 모음집의 이름을 입력해주세요.선택된 모음집에 대한 새 이름을 입력하세요.참고: 압축파일 내 MComix가 주석으로 인식한 파일만 
이 목록에 자동으로 추가합니다.변수 목록과 그 외 정보는 external command documentation를 참조하십시오.폴란드어 번역폴란드어 번역설정(_e)환경설정미리보기:이전 압축파일(_r)이전 압축파일이전 디렉토리이전 페이지이전 페이지 (한장씩)이전 페이지 (계속해서)등록정보(_t)등록정보'%(subcollection)s' 모음집을 '%(supercollection)s' 모음집에 넣기.종료종료하고 다음 시작 시 현재 열린 파일 다시 열기.RAR 압축파일새로고침(_f)이름 변경(_n)최근 파일새로 고침현재 파일 또는 압축파일 다시 읽기.라이브러리에서 책을 제거하시겠습니까?압축파일에서 제거라이브러리에서 제거모음집에서 제거(_c)'%(collection)s' 에서 %(num)d  책 제거.%d 권의 책을 라이브러리에서 제거했습니다.더 이상 존재하지 않는 책을 모음집에서 제거합니다.선택된 책들을 현재 모음집에서 제거합니다.모음집 이름을 변경하시겠습니까?선택된 모음집 이름 변경.페이지 %s에 이미 존재하는 책갈피를 교체하시겠습니까?내용을 덮어씁니다.기본값으로 리셋.모든 키보드 단축키를 기본값으로 리셋.관리자좌측으로 90도 회전(_E)180도 회전(_G)180도 회전반시계방향 90도 회전시계방향 90도 회전이미지 회전명령어 실행러시아어 번역슬라이드쇼채도:스크롤바(_C)선명도:저장다른 이름으로 저장다른 이름으로 저장(_A)저장하고 종료변경점을 명령어 목록에 저장하시겠습니까?페이지를 다른 이름으로 저장앞으로를 위해 선택한 값을 기본값으로 저장.스케일링 모드새 책 검색...스크롤아래로 스크롤왼쪽으로 스크롤오른쪽으로 스크롤가운데 아래로 스크롤왼쪽 아래로 스크롤오른쪽 아래로 스크롤가운데로 스크롤왼쪽 가운데로 스크롤오른쪽 가운데로 스크롤가운데 위로 스크롤왼쪽 위로 스크롤오른쪽 위로 스크롤위로 스크롤"아니요"를 선택하면 다른 책갈피에 영향을 주지 않고 새 책갈피를 생성합니다.라이브러리 커버 사이즈 설정돋보기의 배율을 설정합니다.캐시할 페이지 수 지정. -1로 설정하면 압축파일 전체를 캐시합니다.이미지 포맷 시 사용할 최대 스레드 수를 설정하세요.페이지 넘김 민감도를 설정합니다. 
낮은 값을 설정할 경우 페이지가 
예기치 않게 전환될 수도 있습니다.마우스휠을 사용할 때 페이지에서 스크롤 할 픽셀 수를 설정하세요. 
'마우스휠이나 키보드 방향키로 페이지 넘기기' 옵션이 꺼져 있고,'
원래 크기로 보기 혹은 수동 줌 조절일 경우에만 동작합니다.화살표키를 사용할 때 페이지에서 스크롤 할 픽셀 수를 설정하세요.
'마우스휠이나 키보드 방향키로 페이지 넘기기' 옵션이 꺼져 있고,
원래 크기로 보기 혹은 수동 줌 조절일 경우에만 동작합니다.돋보기의 크기를 설정합니다.로그 출력 수준을 설정합니다.스페이스 키를 눌렀을 때 스크롤 될 정도를 설정합니다.바로가기OSD 패널 보이기파일 번호 보기파일명 보기파일용량 보기양면 보기 모드에서 융통성 있게 1페이지만 보기:페이지 번호 보기미리보기 페이지 번호 보기경로 보기해상도 보기시작할 때 라이브러리를 봅니다.버전 넘버를 보고, 종료합니다.도움말을 보고, 종료합니다.모두 보이기/숨기기메뉴 막대 보이기/숨기기스크롤 막대 보이기/숨기기상태 막대 보이기/숨기기도구 막대 보이기/숨기기간체 중국어 번역크기슬라이드쇼슬라이드쇼 지연시간(초)슬라이드쇼 단계(픽셀)소형아래로 스마트 스크롤위로 스마트 스크롤압축파일 정렬방식:파일과 디렉토리를 다음에 따라 정렬:스페인어 번역슬라이드쇼 모드일 때 스크롤 할 픽셀 수를 지정하세요. 
양수값은 앞으로, 음수값은 뒤로 스크롤합니다. 
0은 항상 새 페이지로 넘깁니다.상태바(_A)슬라이드쇼 실행(_S)슬라이드쇼 시작두 페이지 보기 모드로 프로그램을 시작합니다.풀스크린 모드로 프로그램을 시작합니다.망가 모드로 프로그램을 시작합니다.이 프로그램을 슬라이드쇼 모드로 시작합니다.확대/축소 최적맞춤으로 앱을 시작합니다.확대/축소 세로맞춤으로 앱을 시작합니다.확대/축소 가로맞춤으로 앱을 시작합니다.슬라이드쇼최근 파일 정보 표시:열어본 파일의 미리보기를 보관열었던 파일의 썸네일(미리보기)을 freedesktop.org 사양에 따라 
보관합니다. 이 썸네일은 파일 매니저 등 여러 프로그램과 공유됩니다.확대/축소 설정에 따라 화면에 맞도록 이미지를 늘리기.작은 이미지 늘려서 표시스웨덴어 번역도구(_O)Tar압축파일미리보기(_U)이 압축 파일은 암호로 보호되어 있습니다:파일이 하드디스크에서 삭제됩니다.새 압축파일을 저장할 수 없습니다!원본 파일이 제거되지 않았습니다.선택한 책은 라이브러리에서 (원본 파일은 보존) 제거됩니다. 계속하시겠습니까?이 에러는 GTK+ 라이브러리가 없어 발생한 것일 수 있습니다.이것은 구분자 의사 명령어입니다."최근 파일" 메뉴의 모든 항목을 제거합니다. 그리고 이전에 읽은 페이지 정보를 지웁니다.미리보기 크기(픽셀)썸네일(미리보기)최소정체 중국어 번역이미지 변형투명도목록중에 이와 같은 확장자 형식을 가진 파일을 주석으로 인식합니다.유형우크라이나어 번역알 수 없는 파일 형식라이브러리 버전을 %(from)d 에서 %(to)d 으로 업그레이드합니다.투명한 이미지에 회색 체크무늬 이미지를 배경으로 사용합니다. 
설정되지 않을 경우 흰색을 배경으로 사용합니다.압축 파일 미리보기를 애플리케이션 아이콘으로 사용투명한 이미지에 체크무늬 이미지를 사용배경색을 자동으로 선택전체화면을 기본으로 사용스마트 스크롤링 사용배경색:사용자 인터페이스이미지와 책 압축 파일을 봅니다.보기 모드보기 모드체크할 경우 ESC 키를 누르면 프로그램을 종료합니다. 
체크 해제 시는 풀스크린 모드 종료 기능입니다.압축 파일의 첫 페이지를 보여줄 때, 
혹은 이미지의 가로길이가 세로길이보다 길 경우 
한 페이지만 출력하도록 하는 옵션입니다.
주의: '항상'을 선택한 경우 첫 페이지 혹은 가로길이가 
세로길이보다 긴 이미지만 한 페이지로 출력합니다. 
항상 한 페이지만 출력하는 옵션이 아닙니다.슬라이드 쇼 모드일때 다음 압축파일을 자동으로 엽니다.하위 디렉토리 포함체크할 경우 스페이스바나 마우스휠이 
이미지의 옆면도 스크롤해 보여줍니다.
이미지 가로길이가 화면보다 클 경우에만 동작합니다.작업 디렉토리XZ으로 된 Tar압축파일편집한 외부 명령어 목록이 저장되지 않았습니다. "예" 를 누르면 저장하고 "아니오" 를 누르면 버립니다.%(date)s, %(time)s에 여기까지 읽으셨습니다. 
"네"를 선택하면 %(page)d 페이지로 돌아갑니다. 
아니면 첫 페이지를 불러옵니다.ZIP 압축파일확대/축소확대(_I)축소(_O)확대확대/축소축소[OPTION...] [PATH]MComix에 대하여(_A)추가추가(_A)이미지 자동 회전(_A)자동으로 명암 조정(_A)최적 맞춤(_B)북마크(_B)명도:취소(_C)정리(_C)닫기(_C)대비:복사(_C)삭제(_D)단면/양면 보기(_D)복제편집(_E)책갈피 편집...(_E)압축파일 편집(_E)명령 편집(_E)파일(_F)처음 페이지(_F)전체화면(_F)바로가기(_G)특정 페이지 바로가기...(_G)도움말(_H)가져오기(_I)회전 상태 유지(_K)마지막 페이지(_L)라이브러리(_L)일본만화 페이지순 보기(_M)메뉴바(_M)다음 페이지(_N)원래 크기(_N)열기(_O)열기(_O)이전 페이지(_P)종료(_Q)최근 파일(_R)제거(_R)제거하고 디스크에서 삭제키 리셋(_R)우측으로 90도 회전(_R)저장하고 종료(_S)지금 검색(_S)검색:(_S)정렬(_S)도구(_T)도구(_T)이미지 변형(_T)보기(_V)와치 리스트(_W)확대/축소(_Z)././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/ko/__init__.py0000644000175000017500000000000014476523373020332 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/lt/0000755000175000017500000000000014553265237016237 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/lt/LC_MESSAGES/0000755000175000017500000000000014553265237020024 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/lt/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022125 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/lt/LC_MESSAGES/mcomix.mo0000644000175000017500000002166314476523373021667 0ustar00moritzmoritz
	
			

!!#C8g
	
	

	
)4;%H	nx
		
 0	B
LW
g
r		
 
0;DM
R]
ao|	%


&8I
\j
v
#1>	U_qv
|	,
-	LVjo	x	
*.=C
KVbn
w	#
(
5CP\	is'$A/5=Y	l
v	 	&"+3Hcs#
4BQhy
	
*=TZm~
(,5F
N
Yds|





%0Gb
&  & G M T d v *  	    ! !!#!E![!
k!
v!
!
!
!!!!!	!	!	!!	!	!"
" "0"7"G"V"["o"
w""
"""""
"""#
	#!#6#L#	]#
g#r#	###
#	9Ub;K'7 ,w4*$H2:g
X_8I!j&NPLxpaMh%^1d+#o]s
T-E3emv.}<cA"BrqJ0S)ZG>QV=\5kFnOWzCR~Y(`@ltDfi{u|?[y6/%s archives%s images7z archiveAdd booksAdd new collection?AdvancedAll archivesAll booksAll filesAll imagesAppearanceArchive commentsAutomatically open next directoryAutomatically open the next archiveAutomatically scan for new books when library is _openedBack ten pagesBackgroundBehaviourBest fit modeBook nameCloseCo_mments...CollectionCommand line is empty.Comment filesCustom...Date addedDeleteDelete "%s"?Deletes the selected books from disk.DirectoryDisplayDo not ask again.Double page modeEdit archiveEnhance imageEscape key closes programExternal commandsFileFile nameFile sizeFirst pageFlip horizontallyFlip verticallyForward ten pagesFull pathFullscreenFullscreen modeGo to pageGo to page...H_ide allHugeImageImagesKeep transformationKey %dLHA archiveLanguage (needs restart):LargeLast pageLibraryLibrary booksLibrary collectionsLocationMagnifying lensManga modeMinimizeModifiedNameNavigationNewNext _archiveNext archiveNext directoryNext pageNo new books found in directory '%s'.No sortingNormalNormal sizeOpenOpen _withOwnerPDF documentPagePermissionsPr_eferencesPreferencesPrevious a_rchivePrevious archivePrevious directoryPrevious pageProper_tiesPropertiesQuitRe_freshRe_nameRecentRefreshRemove from archiveRemove from the _libraryRemove from this _collectionRename collection?Reset to defaults.SaveSave AsSave and quitSave page asSet library cover sizeShortcutsShow/hide toolbarSizeSmallSt_atusbarT_oolbarsTh_umbnailsThe file will be deleted from your harddisk.ThumbnailsTinyTransformationTransparencyTypeUse dynamic background colourUse smart scrollingUse this colour as background:View modeWith subdirectoriesZoomZoom _InZoom _OutZoom inZoom out_About_Add_Add..._Brightness:_Cancel_Clean up_Close_Contrast:_Copy_Delete_Edit_Edit archive..._Edit commands_File_First page_Fullscreen_Go_Go to page..._Help_Import_Last page_Library..._Manga mode_Menubar_Next page_Open_Open..._Previous page_Quit_Recent_Remove_Remove and delete from disk_Save and quit_Scan now_Search:_Sort_Toolbar_Tools_View_Watch list_ZoomProject-Id-Version: MComix 1.2.1
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2016-09-18 15:30+0300
Last-Translator: Zygimantus 
Language-Team: 
Language: lt_LT
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);
X-Generator: Poedit 1.8.9
%s archyvai%s vaizdai7z archyvasVisos knygosPridėti naują kolekciją?SudėtingiauVisi archyvaiVisos knygosVisi failaiVisi vaizdaiIšvaizdaArchyvuoti komentarusAutomatiškai atverti sekantį aplankąAutomatiškai atverti kitą archyvąAutomatiškai ieškoti naujų knygų kai yra atidaroma bibliotekaAtgal per 10 puslapiųFonasElgsenaGeriausio tilpimo rėžimasKnygos pavadinimasUždarytiKomentaraiKolekcijaKomandinė eilutė yra tuščia.Komentuoti failusPasirinktas...Pridėjimo dataIštrintiIštrinti „%s“?Ištrina pasirinktą knygą iš disko.AplankasRodymasDaugiau nebeklausti.Dvigubo puslapio rėžimasKeisti archyvąPagerinti vaizdąEscape mygtukas išjungia programąIšorinės komandosFailasFailo pavadinimasFailo dydisPirmas puslapisApsukti horizontaliaiApsukti vertikaliaiPirmyn per 10 puslapiųPilnas keliasPilnas ekranasPilno ekrano rėžimasEiti į puslapįEiti į puslapį...Slėpti viskąMilžiniškasVaizdasVaizdaiPalikti transformacijąRaktas %dLHA archyvasKalba (reikia perkrauti):DidelisPaskutinis puslapisBibliotekaBibliotekos knygosBibliotekos kolekcijosVietaPadidinimo stiklasMangos rėžimasMinimizuotiModifikuotaPavadinimasNavigacijaNaujaKitas archyvasKitas archyvasKita direktorijaKitas puslapisAplanke '%s' nebuvo rasta naujų knygų.Jokio rūšiavimoNormalusNormalusis dydisAtvertiAtverti suSavininkasPDF dokumentasPuslapisLeidimaiNustatymaiNustatymaiAnkstesnis archyvasAnkstesnis archyvasBuvusi direktorijaAnkstesnis puslapisSavybėsSavybėsIšeitiAtnaujintiPervadintiNaujausiosAtnaujintiPašalinti iš archyvoPašalinti iš bibliotekosPašalinti iš šios kolekcijosPervadinti kolekciją?Atstatyti į numatytuosius.IšsaugotiIšsaugoti kaipIšsaugoti ir išeitiIšsaugoti kaipNustatyti bibliotekos viršelių dydįNuorodosRodyti/slėpti įrankių juostąDydisMažasBūsenos juostaĮrankių juostosMiniatūrosFailas bus ištrintas iš jūsų sistemos.MiniatiūrosMažiukasTransformacijaPermatomumasTipasFonui naudoti kintančią spalvąNaudoti išmanų slinkimąNaudoti šią spalvą kaip foną:Peržiūros rėžimasSu poaplankiaisPriartintiPriartintiAtitolintiPriartintiAtitolintiApiePridėtiPridėti...Šviesumas:AtšauktiIšvalytiUždarytiKontrastas:KopijuotiIštrintiKeistiKeisti archyvą...Keisti komandasFailasPirmas puslapisPilnas ekranasEitiEiti į puslapį...PagalbaImportuotiPaskutinis puslapisBiblioteka...Mangos rėžimasMeniu juostaKitas puslapisAtvertiAtverti...Ankstesnis puslapisIšeitiNaujausiPašalintiPašalinti ir ištrinti iš diskoIšsaugoti ir išeitiNuskaityti dabarIeškoti:RūšiuotiĮrankių juostaĮrankiaiRodymasStebimų sąrašasPriartinti././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/lt/__init__.py0000644000175000017500000000000014476523373020340 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/nl/0000755000175000017500000000000014553265237016231 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/nl/LC_MESSAGES/0000755000175000017500000000000014553265237020016 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/nl/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022117 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/nl/LC_MESSAGES/mcomix.mo0000644000175000017500000001132514476523373021653 0ustar00moritzmoritzY	
	 )=FN_
q|+
	
*	8	J		f	p	v								
				




'
:
G

S
a

m
x





	




+
7Bbn
u

.4JSYD_





	

-
BMRg
}-GVhz


	/9IPY`hy



	!+=Ia	mw	/5K[	hr

	C:;
E74NY3?I6G.T*1PWK$%S 
-#8M,2UJ@V9L/)OF="H!&'>	50R+BQ(<AXDAccessedAdvancedAll filesAll imagesArchiveBehaviourBilinearBrazilian Portuguese translationBzip2 compressed tar archiveCatalan translationCommentsDisplayDouble page modeDutch translationFirst pageFit _height modeFit _width modeFit height modeFit width modeFli_p horizontallyFlip _verticallyFrench translationGerman translation and Nautilus thumbnailerGo to pageGo to page...Greek translationGzip compressed tar archiveH_ide allImageItalian translationLast pageLibraryLocationMagnification factor:Magnifying _lensMagnifying lensManga modeManual zoom modeModifiedNext pageOpenOwnerPagePermissionsPolish translationPr_eferencesPreferencesPrevious pageProper_tiesPropertiesRAR archiveRotat_e 90 degrees CCWRotate 180 de_greesS_crollbarsScrollSimplified Chinese translationSlideshowSpanish translationSt_atusbarStretch small imagesTar archiveTh_umbnailsThumbnailsTraditional Chinese translationZIP archive_About_Bookmarks_Close_Double page mode_Edit_File_First page_Fullscreen_Go_Go to page..._Help_Keep transformation_Last page_Manga mode_Menubar_Next page_Open..._Previous page_Quit_Rotate 90 degrees CW_Toolbar_View_ZoomProject-Id-Version: PACKAGE VERSION
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2006-10-15 21:30+0100
Last-Translator: 
Language-Team: LANGUAGE 
Language: 
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Generated-By: pygettext.py 1.5
GebruiktGeavanceerdAlle bestandenAlle afbeeldingenArchiefGedragBilineaerBrazilisch Portugeze vertalingBzip2 ingepakt tar archiefCatalaanse vertalingCommentaarToonDubbele pagina modusNederlandse vertalingEerste paginaRek naar hoogte modusRek naar breedte modusRek naar hoogte modusRek naar hoogteHorizontaal spiegelenVerticaal spiegelenFranse vertalingDuitse vertaling en Nautilus voorbeeldenmakerGa naar paginaGa naar pagina...Griekse vertalingGzip ingepakt tar archiefVe_rberg allesAfbeeldingItaliaanse vertalingLaatste paginaBibliotheekLocatieZoomfactor:Vergr_ootglasVergrootglasManga modusHandmatige zoom modusGewijzigdVolgende paginaOpenenEigenaarPaginaRechtenPoolse vertaling_VoorkeurenVoorkeurenVorige paginaEigenscha_ppenEigenschappenRAR archiefRot_eer 90 graden TKIRoteer 180 gradenS_crollbalkenScrollVersimpeld Chinese vertalingSlideshowSpaanse vertalingSt_atusbalkRek kleine afbeeldingenTar archiefP_reviewsVoorbeeldenTraditioneel Chinese vertalingZIP archiefIn_fo_Bladwijzers_Sluiten_Dubbele pagina modusBe_werken_Bestand_Eerste pagina_Volledig scherm_Ga naar_Ga naar pagina..._HulpBewaar _transformatie_Laatste pagina_Manga modus_MenubalkVo_lgende pagina_Openen...Vo_rige pagina_Afsluiten_Roteer 90 graden MKM_WerkbalkBeel_d_Zoom././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/nl/__init__.py0000644000175000017500000000000014476523373020332 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/pl/0000755000175000017500000000000014553265237016233 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/pl/LC_MESSAGES/0000755000175000017500000000000014553265237020020 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/pl/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022121 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/pl/LC_MESSAGES/mcomix.mo0000644000175000017500000002715514476523373021665 0ustar00moritzmoritz	(
)
5
>
)E
<o


	



!0	=	G
Q9\
!])Bk
j	u
 +"7 W%xrRcu

LR
e+p	A\p	
Hd	is+4.AN
Z
hOs)?D[ox	4T8W	\f
z	+5A#M)q
X,{=
(48>
S^jv
k
9GP$X>}&
4Lbv7)XxF, js  
  '!,3!`!|!3!&!!!$!'$"L"c"}"
""/#A#]#m#|#######$}"$$$4$$+%?%P%k%x%~%%A%%%&2&
B&M&\&y&~&&
&&	&V&'''8'@'`'m't''''*''(
((+(P:((((()()4)H)X)_)x)))))J)#*4*<*K*g*t*6+J+
W+&b+(+	+#++o+c,uv,,,-
&-1-:-R-Z-j-p-
-	------
---..1#.U.f.	X(n8]W.Pw'u)e06$\HxZfA&OTk{/o;j=LiD^Jty@IabF` zpgs|QlS-hVd3#MN>4B9
CvUcq,E7Y[5_<2K
}R!r~G"%*m:1+?%d comments%d pages(Copy)A collection by that name already exists.A file named '%s' already exists. Do you want to replace it?AccessedAdd a new empty collection.Add booksAdd books to '%s'.Add more books to the library.Add new collection?Adding '%s'...Adding booksAll booksAll filesAll imagesAlways use this selected colour as the background colour.AppearanceArchiveArchives are stored as ZIP files.Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically pick a background colour that fits the viewed image.Automatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.BackgroundBehaviourBest fit modeBrazilian Portuguese translationBzip2 compressed tar archiveCatalan translationCommentsCould not add a new collection called '%s'.Could not change the name to '%s'.Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCroatian translationCzech translationDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Double page modeDutch translationEdit archiveEnhance imageFilesFirst pageFit _height modeFit _width modeFit height modeFit width modeFli_p horizontallyFlip _verticallyFlip two pages, instead of one, each time we flip pages in double page mode.French translationFullscreenGerman translation and Nautilus thumbnailerGreek translationGzip compressed tar archiveH_ide allHungarian translationIcon designImageImagesIndonesian translationIt reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKorean translationLast pageLibraryLocationM_anual zoom modeMagnifying LensMagnifying _lensMagnifying lensManga modeManual zoom modeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNext pageNo images in '%s'OpenOpen the selected book.OwnerPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Polish translationPr_eferencesPreferencesPrevious pagePropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.RAR archiveRemove books from the library?Remove from archiveRename collection?Replacing it will overwrite its contents.RootRotat_e 90 degrees CCWRotate 180 de_greesRotationRussian translationSLIDESHOWS_crollbarsSaveScrollSet the magnification factor of the magnifying lens.Set the size of the magnifying lens. It is a square with a side of this many pixels.Simplified Chinese translationSizeSlideshowSpanish translationSt_atusbarStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.T_oolbarsTar archiveTh_umbnailsThe new archive could not be saved!The original files have not been removed.ThumbnailsTraditional Chinese translationTransparencyTreat all files found within archives, that have one of these file endings, as comments.Unknown filetypeUse a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.ZIP archive_About_Best fit mode_Bookmarks_Close_Double page mode_Edit_Edit archive..._File_First page_Fullscreen_Go_Help_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Open..._Previous page_Quit_Rotate 90 degrees CW_Toolbar_ViewProject-Id-Version: PACKAGE VERSION
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2009-02-02 14:19+0100
Last-Translator: Dariusz Jakoniuk 
Language-Team: LANGUAGE 
Language: 
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Generated-By: pygettext.py 1.5
%d komentarze%d stron(Kopia)Kolekcja o tej nazwie już istnieje.Plik o nazwie „%s“ już istnieje. Czy chcesz go nadpisać?UżytyDodaj nowa pustą kolekcję.Dodaj książkiDodaj książki do „%s“.Dodaj więcej książek do biblioteki.Dodać nową kolekcję?Dodawanie „%s“…Dodawanie książekWszystkie książkiWszystkie plikiWszystkie obrazkiZawsze używaj tego wybranego koloru, jako koloru tła.WyglądArchiwumArchiwa są przechowywane jako pliki zip.Automatycznie dostosuj kontrast (jasość i ciemność), oddzielnie dla każdego koloru.Automatycznie otwórz następne archiwum w katalogu, kiedy ukończono przeglądanie ostatniej strony, albo poprzednie archiwum, jeśli ukończono przeglądanie pierwszej ze stron.Automatycznie wybierz kolor tła, który pasuje do ogladanego obrazka.Automatycznie obróc obrazy, kiedy orjentacja jest określona w informacjach obrazu, takich jak tagi Exif.TłoZachowanieTryb najlepszego dopasowaniaTłumaczenie na portugalski brazylijskiArchiwum tar skopresowane przy użyciu bzip2Tłumaczenie na katalońskiUwagiNie można dodać nowej kolekcji o nazwie „%s“.Nie można zmienić nazwy na „%s“.Nie można zduplikować kolekcji.Nie można otworzyć %s: Brak pliku.Nie można otworzyć %s: Brak dostępu.Nie mozna odczytać %sTłumaczenie na chorwackiTłumaczenie na czeskiWyświetlanieWyświetlaj tylko te książki, które mają speczyficzny tekst w swojej pełnej ścieżce. Szukaj nie rozróżnia wielkich i małych liter.Tryb dwóch stronTłumaczenie na holenderskiEdytuj archiwumPopraw obrazekPlikiPierwsza stronaDopasuj na wysokośćDopasuj na szerokośćTryb dopasowania wysokościTryb dopasowania szerokościPrzewróć w poziomiePrzewróć w pioniePrzeglądaj po dwie strony, zamiast po jednej, za każdym razem, gdy przejdziesz do następnej strony, w trybie dwóch stron.Tłumaczenie na francuskiPełen ekranTłumaczenie na niemiecki i thumbnailer w NautilusieTłumaczenie na greckiArchiwum tar skopresowane przy użyciu gzip_Ukryj wszystkieTłumaczenie na węgierskiProjekt ikonObrazObrazkiTłumaczenie na indonezyjskiComix czyta archiwa ZIP, RAR i tar, jak również zwykłe obrazy.Tłumaczenie na włoskiTłumaczenie na japońskiTłumaczenie na kareańskiOstatnia stronaBibliotekaUmiejscowienieTryb ręcznego przybliżeniaLupa_LupaLupaTryb mangaRęczny tryb zoomuZmienionyPrzenieś książki z „%(source collection)s“ do „%(destination collection)s“.ImięNastępna stronaBrak obrazów w „%s“OtwórzOtwórz zaznaczoną książkę.WłaścicielStronaUprawnieniaTłumaczenie na perskiProszę wstawić nazwę nowej kolekcji.Wpisz nową nazwę dla wybranej kolekcji. Tłumaczenie na polski_UstawieniaPreferencjePoprzednia stronaWłaściwościUmieść kolekcję „%(subcollection)s“ w kolekcji „%(supercollection)s“.Archiwum RARUsuń książki z biblioteki?Usuń z archiwumZmienić nazwę kolekcji?Nadpisanie pliku zmieni jego zawartość.KorzeńObróć o 90° przeciwnie do ruchu wskazówek zegaraObróć o 180°ObrótTłumaczenie na rosyjskiPOKAZ SLAJDÓW_Paski przesuwaniaZapiszPrzewińUstaw współczynnik lupy.Ustaw wielkość lupy. Lupa jest kwadratem z bokiem o tej ilości pikseli.Tłumaczenie na uproszczony chinskiRozmiarPokaz slajdówTłumaczenie na hiszpański_Pasek stanuPrzechowuj miniatury dla otwartych plików zgodnie ze specyfikacją freedesktop.org. Te miniatury są współdzielone przez wiele innych programów, takich jak wiekszość menadżerów plików._Paski narzędzioweArchiwum tar_MiniaturyNowe archiwum nie może być zapisane!Oryginalne pliki nie zostały usunięte.MiniaturyTłumaczenie na tradycyjny chińskiPrzeźroczystośćTraktuj wszystkie pliki znalezione wewnątrz archiwów, jeśli mają jedno z tych zakończeń, jako komentarze.Nieznany typ plikuUżyj szarej szachownicy jako tła dla przeźroczystych obrazów. Jeśli ta opcja nie jest wybrana, tło jest białe.Archiwum ZIP_O programie_Tryb optymalnego przybliżenia_Zakładki_Zamknij_Tryb podwójnej strony_EdycjaEdytuj archiwum_PlikPierwsza strona_Pełen ekran_Przejdź_Pomoc_Utrzymaj transformacjęOstatnia strona_Biblioteka_Tryb mangaPasek menu_Nastepna strona_Otwórz…_Poprzednia strona_WyjdźObróć o 90° w kierunku ruchu wskazówek zegara_Pasek narzędzi_Widok././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/pl/__init__.py0000644000175000017500000000000014476523373020334 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9874768
mcomix-3.1.0/mcomix/messages/pt_BR/0000755000175000017500000000000014553265237016626 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768
mcomix-3.1.0/mcomix/messages/pt_BR/LC_MESSAGES/0000755000175000017500000000000014553265237020413 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/pt_BR/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022514 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/pt_BR/LC_MESSAGES/mcomix.mo0000644000175000017500000002635514476523373022261 0ustar00moritzmoritz	
	


)%
<O


	



		'
19<
v!]	B
	
 "?S+\" %$9KrS


&6FUhLy
+"	>H^jpwA	'9IZ
juH	!-+A4m

O7Cbv)	
4TN	
	#)
1X>{$07
FQXjp


)28K[g'p<$+Pj~'
&j/O4

$  6# Z z * (  !!5!:!!!!!""%";"R"f"{""["	#
"#1-#_#u######"#:$>$W$p$$
$$$$$$$$
%J%f%k%|%%%%%%%+%<&N&
g&
u&&&K&& &'-'/A'q'%v'''''''/(Q2($(((((())
)&)+*
C*#N*r*u**}++++	++++++,
,!,%,,,C,
T,b,
n,|,,,, ,,,	W'l7\V-Ou&s(d/5#}[GvYe@%NSy.m:i<KhC]Irw?H~`a{_xnfqzPjR,gUc2E"LM=3A8
BtTbo+D6XZ4^;1J
Q p|F!$)k90*>%d comments%d pages(Copy)A collection by that name already exists.A file named '%s' already exists. Do you want to replace it?AccessedAdd a new empty collection.Add booksAdd books to '%s'.Add more books to the library.Add new collection?Adding '%s'...Adding booksAll booksAll filesAll imagesAlways use this selected colour as the background colour.AppearanceArchiveArchives are stored as ZIP files.Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically pick a background colour that fits the viewed image.BackgroundBehaviourBest fit modeBrazilian Portuguese translationBzip2 compressed tar archiveCatalan translationCommentsCould not add a new collection called '%s'.Could not change the name to '%s'.Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCroatian translationCzech translationDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Double page modeDutch translationEdit archiveEnhance imageFilesFirst pageFit _height modeFit _width modeFit height modeFit width modeFli_p horizontallyFlip _verticallyFlip two pages, instead of one, each time we flip pages in double page mode.French translationFullscreenGerman translation and Nautilus thumbnailerGreek translationGzip compressed tar archiveH_ide allHungarian translationIcon designImageImagesIndonesian translationIt reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKorean translationLast pageLibraryLocationM_anual zoom modeMagnifying LensMagnifying _lensMagnifying lensManga modeManual zoom modeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNext pageNo images in '%s'OpenOpen the selected book.OwnerPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Polish translationPr_eferencesPreferencesPrevious pagePropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.RAR archiveRemove books from the library?Remove from archiveRename collection?Replacing it will overwrite its contents.RootRotat_e 90 degrees CCWRotate 180 de_greesRussian translationSLIDESHOWS_crollbarsSaveScrollSet the magnification factor of the magnifying lens.Set the size of the magnifying lens. It is a square with a side of this many pixels.Simplified Chinese translationSizeSlideshowSpanish translationSt_atusbarStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.T_oolbarsTar archiveTh_umbnailsThe new archive could not be saved!The original files have not been removed.ThumbnailsTraditional Chinese translationTransparencyTreat all files found within archives, that have one of these file endings, as comments.Unknown filetypeUse a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.ZIP archive_About_Best fit mode_Bookmarks_Close_Double page mode_Edit_Edit archive..._File_First page_Fullscreen_Go_Help_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Open..._Previous page_Quit_Rotate 90 degrees CW_Toolbar_ViewProject-Id-Version: 
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 
Last-Translator: Marcelo Góes 
Language-Team: 
Language: 
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
%d comentários%d páginas(Cópia)Uma coleção com esse nome já existe.Um arquivo com o nome '%s' já existe. Deseja substituí-lo?AcessadoAdicionar nova coleção vazia.Adicionar livrosAdicionar livros para '%s'.Adicionar mais livros à biblioteca.Adicionar nova coleção?Adicionando '%s'...Adicionando livros.Todos os livrosTodos os arquivos avulsosTodas as imagensSempre usar essa cor como cor de fundo.AparênciaArquivoArquivos são gravados em formato ZIP.Ajustar contraste (tanto claro quanto escuro) automaticamente, de maneira separada para cada banda de cor.Abre o próximo arquivo do diretório automaticamente quando virar após a última página, ou o arquivo anterior quando virar antes da primeira página.Escolher uma cor de fundo compatível com a imagem visualizada automaticamente.Tela de fundoComportamentoModo de melhor encaixeTradução para português do BrasilArquivo tar comprimido com bzip2Tradução para catalãoComentáriosImpossível adicionar uma nova coleção chamada '%s'.Impossível renomear para '%s'.Impossível duplicar coleção.Impossível abrir %s: Arquivo não existe.Impossível abrir %s: Permissão negada.Não foi possível ler %sTradução para croataTradução para checoTelaMostrar só os livros que têm o texto especificado no seu caminho completo. A busca não é sensível a maiúscula e minúscula.Modo de página duplaTradução para holandêsEditar arquivoMelhorar imagemArquivosPrimeira páginaModo ajuste de alturaModo ajuste de larguraModo ajustar alturaModo ajustar larguraInverter _horizontalmenteInverter _verticalmenteVirar duas páginas, ao invés de uma, cada vez que virar página em modo de página dupla.Tradução para francêsTela cheiaTradução para alemão e thumbnailer do NautilusTradução para gregoArquivo tar comprimido com gzipEsconder todosTradução para húngaroDesenho dos íconesImagemImagensTradução para língua indonésiaEle lê arquivos ZIP, RAR e tar, bem como imagens normais.Tradução para italianoTradução para japonêsTradução para coreanoÚltima páginaBibliotecaLocalModo de zoom manualLente de aumentoLente de aumentoLente de aumentoModo mangáModo de zoom manualModificadoMover livros de '%(source collection)s' para '%(destination collection)s'.NomePróxima páginaNão há imagens em '%s'AbrirAbrir o livro selecionado.DonoPáginaPermissõesTradução para persaPor favor, digite o nome da nova coleção.Por favor, digite um novo nome para a coleção selecionada.Tradução para polonêsPreferênciasPreferênciasPágina anteriorPropriedadesColoque a coleção '%(subcollection)s' na coleção '%(supercollection)s'.Arquivo RARRemover livros desta biblioteca?Remover do arquivoRenomear coleção?A substituição sobreescreverá seu conteúdo.RaizRo_tação de 90 graus anti-horáriosRotação de 180 grausTradução para russoApresentação de slidesBarra de rolagemSalvarRolagemAjustar o fator de aumento da lente de aumento.Ajustar o tamanho da lente de aumento. É um quadrado com esse tamanho de pixels.Tradução para chinês simplificadoTamanhoApresentação de slidesTradução para espanholBarra de statusGuarda miniaturas de arquivos abertos de acordo com a especificação do freedesktop.org. Essas miniaturas são compartilhadas por vários outros aplicativos, como a maior parte dos gerenciadores de arquivos.Barra de ferramentasArquivo tarMiniaturasO novo arquivo não pôde ser gravado!Os arquivos originais não foram removidos.MiniaturasTradução para chinês tradicionalTransparênciaTratar todos os arquivos avulsos dentro de arquivos, incluindo os que tem terminação de arquivo, como comentários.Tipo de arquivo desconhecidoUsar fundo quadriculado cinza para imagens transparentes. Se esta preferência não estiver marcada, o fundo usado é branco.Arquivo ZIPSobreModo de melhor ajusteFavoritos_FecharModo de página duplaEditarEditar arquivo..._ArquivoP_rimeira páginaTela cheia_IrA_judaManter transformação_Última páginaBiblioteca...Modo mangáBarra de menu_Próxima páginaAbrir...Pá_gina anterior_SairRo_tação de 90 graus horáriosBarra de ferramentas_Exibir././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/pt_BR/__init__.py0000644000175000017500000000000014476523373020727 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768
mcomix-3.1.0/mcomix/messages/ru/0000755000175000017500000000000014553265237016246 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768
mcomix-3.1.0/mcomix/messages/ru/LC_MESSAGES/0000755000175000017500000000000014553265237020033 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/ru/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022134 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/ru/LC_MESSAGES/mcomix.mo0000644000175000017500000011335614476523373021677 0ustar00moritzmoritzl|xy,+(:=']##% ? V 0n    	 B 
!,!3!P!
m!)x!<!!
!!"	!"+"Z>""""""""	#	#
#*#91#Ck#
##!##]#-X$!$$2y%#%%Sb&B&k&Ue''
'	'
' 'q(((((
(((
(()%$)+J)"v)) )%)*1*D*Y*
k*y**/*.*	**+r+++6+++,,%,
7,E,_,x,,,	,
,	,,
,,,,
--"-5-F-X-3.G.W.<i..
....+.
(/
3/A/S/	o/y////
////A/50I0^0e0x000	000000011
)141.E1	t1~1111
111	111B1 @2a2/m2022
222"373=3J3O3[3+o3433Z4l444444
444
5O5c5h5t5}555.55556,6)?6i6n66666666	777$70757=7
F7T7a7n74u7R77F8G8T=999!99

::*:?:S:e::	::::::;
;;<*<)?<$i<(<<.<<=	==="=,=#!>)E>to>5>#?>?
Z?e??X???@{@)@/@@A%A9AXA$gA	A
AWAxAJrBBhCzCCC	CC
CCCCCCD
D D-D5D
EGEVE\EdEzEEEEEEE,EG`GCVHNH^H>HI8INI:JEJJ,J2JNJ'?K*gKSKKLL$L>LALM5M5NM
MJMeMCN!PN'rN<NN$NO9O/P"3PVPsPPPPPPP_QtkQQ
Q:R9^F]^D^)^W_#k__!___T_GH``)```%a'ala%]b+b%b8b-c)XX0mDjB1F-#b(WB
$>1*@8uOkb(`T%~gy4&S,FU\<fGKh*"Dqza;3hOQM
dYc/9swKi #RR^=5'P ?{
4L0`I
^H72I=QAk:%|Z9L.N+E of %s! Corrupt preferences file "%s", deleting...! Could neither find pysqlite2 nor sqlite3.! Could not add book "%s" to the library! Could not add book %(book)s to collection %(collection)s! Could not add collection "%s"! Could not create archive at path "%s"! Could not get cover for book "%s"! Could not load icon "%s"! Could not parse bookmarks file %s! Could not read %s! Could not remove file "%s"! Could not rename collection to "%s"! Extraction error: %s! Non-existant book #%i! You need an sqlite wrapper to use the library.%d comments%d pages%s archives%s images%s is an image viewer specifically designed to handle comic books.'%s' is disabled for archives.(Copy)...when height exceeds width...when width exceeds height7z archiveA collection by that name already exists.A file named '%s' already exists. Do you want to replace it?AccessedAdd _BookmarkAdd _separatorAdd a new empty collection.Add booksAdd books to '%s'.Add information about all files opened from within MComix to the shared recent files list.Add more books to the library.Add new collection?Added books:Adding '%s'...Adding booksAdvancedAll archivesAll booksAll filesAll imagesAlwaysAlways use this selected colour as the background colour.Always use this selected colour as the thumbnail background colour.AppearanceArchiveArchives are stored as ZIP files.Auto-detect (Default)Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically hide all toolbars in fullscreenAutomatically open next directoryAutomatically open the first file in the next sibling directory when flipping past the last page of the last file in a directory, or the previous directory when flipping past the first page of the first file.Automatically open the last viewed file on startupAutomatically open the next archiveAutomatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically open, on startup, the file that was open when MComix was last closed.Automatically pick a background colour that fits the viewed image.Automatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.Automatically use the colour that fits the viewed image for the thumbnail background.Back ten pagesBackgroundBehaviourBest fit modeBrazilian Portuguese translationBy enabling this setting, the first page of a book will be used as application icon instead of the standard icon.Bzip2 compressed tar archiveCatalan translationCloseCo_mments...CollectionCommandCommand line is empty.Comment filesCommentsContinue reading from page %d?Copies the current page to clipboard.Could not add a new collection called '%s'.Could not change the name to '%s'.Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCould not run command %(cmdlabel)s: %(exception)sCroatian translationCzech translationDebug optionsDeleteDelete "%s"?Delete information about recently opened files?Deletes the current file or archive from disk.DirectoryDisabled in archivesDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Do not ask again.Double page modeDuring a slideshow automatically open the next archiveDutch translationEdit BookmarksEdit archiveEdit external commandsEn_hance image...Enhance imageEscape key closes programExecute external commandExit from fullscreenExternal commandsFileFile nameFile orderFile sizeFilesFirst pageFit _height modeFit _width modeFit height modeFit size modeFit width modeFli_p horizontallyFlip _verticallyFlip horizontallyFlip pages when scrolling "off the page" with the scroll wheel or with the arrow keys. It takes n consecutive "steps" with the scroll wheel or the arrow keys for the pages to be flipped.Flip pages when scrolling off the edges of the pageFlip verticallyForward ten pagesFraction of page to scroll per space key press (in percent):French translationFullscreenFullscreen modeGalician translationGerman translationGerman translation and Nautilus thumbnailerGo to pageGo to page...Greek translationGzip compressed tar archiveH_ide allHebrew translationHungarian translationIcon designImageImage qualityImagesIndonesian translationInvalid path: '%s'It reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKey %dKorean translationLHA archiveLabelLanguage (needs restart):Last pageLibraryLocationMComix developerM_anual zoom modeMagnification factor:Magnifying LensMagnifying _lensMagnifying lensManga modeManual zoom modeMaximum number of pages to store in the cache:Mi_nimizeMinimizeModifiedNameNeverNext _archiveNext archiveNext directoryNext pageNext page (always one page)No images in '%s'No version of the Python Imaging Library was found on your system.Non-supported archive format: %sNormal sizeNumber of pixels to scroll per arrow key press:Number of pixels to scroll per mouse wheel turn:OpenOpen _withOpen _without closing libraryOpen the selected book.Original vision/developer of ComixOwnerPDF documentPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Please note that the only files that are automatically added to this list are those files in archives that MComix recognizes as comments.Polish translatinPolish translationPr_eferencesPreferencesPrevious a_rchivePrevious archivePrevious directoryPrevious pagePrevious page (always one page)Proper_tiesPropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.QuitRAR archiveRe_freshRe_nameRecentRefreshReloads the currently opened files or archive.Remove books from the library?Remove from archiveRemove from the _libraryRemove from this _collectionRename collection?Replacing it will overwrite its contents.RootRotat_e 90 degrees CCWRotate 180 de_greesRotate 180 degreesRotate 90 degrees CCWRotate 90 degrees CWRotationRun _commandRussian translationSLIDESHOWS_aturation:S_crollbarsS_harpness:SaveSave AsSave _AsSave and quitSave page asScaling modeScrollSet the magnification factor of the magnifying lens.Set the max number of pages to cache. A value of -1 will cache the entire archive.Set the number of "steps" needed to flip to the next or previous page.  Less steps will allow for very fast page turning but you might find yourself accidentally turning pages.Set the number of pixels to scroll on a page when using a mouse wheel.Set the number of pixels to scroll on a page when using the arrow keys.Set the size of the magnifying lens. It is a square with a side of this many pixels.Show page numbers on thumbnailsShow the library on startup.Show the version number and exit.Show this help and exit.Show/hide allShow/hide menubarShow/hide scrollbarsShow/hide statusbarShow/hide toolbarSimplified Chinese translationSizeSlideshowSlideshow delay (in seconds):Slideshow step (in pixels):Sort archives by:Sort files and directories by:Spanish translationSpecify the number of pixels to scroll while in slideshow mode. A positive value will scroll forward, a negative value will scroll backwards, and a value of 0 will cause the slideshow to always flip to a new page.St_atusbarStart _slideshowStart slideshowStart the application in double page mode.Start the application in fullscreen mode.Start the application in manga mode.Start the application in slideshow mode.Stop slideshowStore information about recently opened files:Store thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.Swedish translationT_oolbarsTar archiveTh_umbnailsThe archive is password-protected:The file will be deleted from your harddisk.The new archive could not be saved!The original files have not been removed.The selected books will be removed from the library and permanently deleted. Are you sure that you want to continue?This error might be caused by missing GTK+ libraries.This is a separator pseudo-command.Thumbnail size (in pixels):ThumbnailsTraditional Chinese translationTransparencyTreat all files found within archives, that have one of these file endings, as comments.TypeUkrainian translationUnknown filetypeUse a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.Use archive thumbnail as application iconUse checkered background for transparent imagesUse dynamic background colourUse fullscreen by defaultUse smart scrollingUse this colour as background:User interfaceView images and comic book archives.View modeView modesWhen active, the ESC key closes the program, instead of only disabling fullscreen mode.When showing the first page of an archive, or an image's width exceeds its height, only a single page will be displayed.While in slideshow mode allow the next archive to automatically be opened.With this preference set, the space key and mouse wheel do not only scroll down or up, but also sideways and so try to follow the natural reading order of the comic book.Working directoryZIP archiveZoomZoom _InZoom _OutZoom inZoom modesZoom out[OPTION...] [PATH]_About_Auto-rotate image_Automatically adjust contrast_Best fit mode_Bookmarks_Brightness:_Cancel_Close_Contrast:_Copy_Delete_Double page mode_Edit_Edit Bookmarks..._Edit archive..._Edit commands_File_First page_Fullscreen_Go_Go to page..._Help_Import_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Normal Size_Open_Open..._Previous page_Quit_Recent_Rotate 90 degrees CW_Save and quit_Search:_Toolbar_Tools_Transform image_View_ZoomProject-Id-Version: MComix 0.90.4
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2016-01-10 18:09+0200
Last-Translator: Ulyanich Michael 
Language-Team: Russian 
Language: ru_RU
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Generated-By: pygettext.py 1.5
X-Poedit-SourceCharset: utf-8
X-Generator: Poedit 1.8.6
Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);
 из %s! Повреждённый конфигурационный файл "%s", удаляется...! Требуется установить pysqlite2 или sqlite3.! Не удалось добавить книгу "%s" в библиотекуНевозможно добавить книгу %(book)s в коллекцию %(collection)s! Не удалось добавить коллекцию "%s"! Невозможно создать архив в "%s"! Не удалось загрузить обложку для книги "%s"! Не удалось прочитать значок "%s"! Не удалось прочитать файл закладок %s! Невозможно прочитать %s! Не удалось удалить файл "%s"! Невозможно переименовать коллекцию на "%s"! Ошибка извлечения: %s! Книга #%i не существует! Для использования библиотеки необходим sqlite.%d комментариев%d страниц%s архивы%s изображения%s это программа просмотра изображений, ориентированная на чтение комиксов.'%s' отмечена неприменимой к архивам.(Копия)... когда высота больше ширины... когда ширина больше высоты7z архивКоллекция с таким именем уже существует.Файл с именем '%s' уже существует. Вы хотите заменить его?ДоступДобавить закладкуДобавить разделительДобавить новую пустую коллекцию.Добавить книгиДобавить книги в '%s'.Добавлять информацию о файлах открытых MComix в общий список недавно использовавшихся документов.Добавить еще книг в библиотеку.Добавить новую коллекцию?Добавленные книги:Добавление '%s'...Добавление книгПродвинутыеВсе архивыВсе книгиВсе файлыВсе изображенияВсегдаВсегда использовать выбранный цвет в качестве фона.Всегда использовать выбранный цвет, как цвет фона для миниатюр.Внешний видАрхивАрхивы сохраняются как ZIP файлы.Авто-определение (По умолчанию)Автоматически подстраивать контрастность (светлые и темные участки), отдельно для каждой цветовой полосы.Автоматически скрывать все панели в полноэкранном режимеАвтоматически открывать следующую директориюАвтоматически переходить в следующую директорию (на том же уровне вложенности) при прокрутке вниз последней страницы последнего файла в текущей директории, или, наоборот, в предыдущую директорию при прокрутке вверх первой страницы первого файла в текущей директории.Автоматически открывать последний просмотренный файл при запускеАвтоматически открывать следующий архивАвтоматически открывать следующий архив в текущем каталоге, при прокрутке вниз последней страницы, или предыдущий архив, при прокрутке вверх первой страницы.Автоматически при запуске открывать тот файл, который был открыт при выходе из MComix в последний раз.Автоматически подбирать цвет фона, который подходит к показываемому изображению.Автоматически разворачивать изображения, если ориентация указана в метаданных, например в Exif теге.Автоматически подбирать цвет фона миниатюр, который подходит к показываемому изображению.Назад на 10 страницФонПоведениеНаилучший режим масштабированияБразильский Португальский переводКогда эта опция включена, первая страница комикса устанавливается в качестве иконки программы вместо стандартной иконки.Tar архив сжатый bzip2Каталанский переводЗакрытьКомментарии...КоллекцияКомандаКоманда на указана.Комментировать файлыКомментарииПродолжить чтение со страницы %d?Копировать текущую страницу в буфер обмена.Не удалось добавить новую коллекцию '%s'.Невозможно изменить имя на '%s'.Не удалось скопировать коллекцию.Не удалось открыть %s: Нет такого файла.Не удалось открыть %s: Не хватает прав.Не удалось прочитать %sНе получается выполнить команду %(cmdlabel)s: %(exception)sХорватский переводЧешский переводПараметры отладкиУдалитьУдалить "%s"?Удалить информацию о недавно открытых файлах?Удалить текущий файл или архив с диска.КаталогНе применять к архивамОтображениеПоказывать только те книги, в полном пути которых есть указанная строка. Поиск нечувствителен к регистру.Не спрашивать снова.Двухстраничный режимАвтоматически открывать следующий архив во время слайдшоуГолландский переводРедактировать закладкиРедактировать архивРедактировать внешние команды_Улучшение изображения...Улучшение изображенияЗакрывать программу нажатием EscapeВыполнить внешнюю командуВыйти из полноэкранного режимаВнешние командыФайлИмя файлаПорядок файловРазмер файлаФайлыПервая страницаРастянуть по _высотеРастянуть по _ширинеРастянуть по высотеРеальный размерРастянуть по ширинеОтразить по _горизонталиОтразить по _вертикалиОтразить по _горизонталиПерелистывать при прокручивании "за страницу" колёсиком мыши или стрелками. Чтобы перелистнуть страницу, нужно будет сделать несколько нажатий на стрелки или некоторое время крутить колёсико.Перелистывать при прокручивании за край страницыОтразить по _вертикалиВперёд на 10 страницКоличество пикселей, прокручиваемых при нажатии на пробел (в процентах):Французский переводПолноэкранный режимПолноэкранный режимГалисийский переводНемецкий переводНемецкий перевод и генератор миниатюр для NautilusПерейти на страницуПерейти на страницу...Греческий переводTar архив сжатый gzipСкрыть _всёПеревод на ИвритВенгерский переводДизайн значковИзображениеКачество изображенийИзображенияИндонезийский переводНеверный путь: '%s'Она читает ZIP, RAR и tar архивы также хорошо, как обыкновенные файлы изображений.Итальянский переводЯпонский переводКлавиша %dКорейский переводLHA архивМетка (название)Язык (потребуется перезапуск):Последняя страницаБиблиотекаРасположениеРазработчик  MComix_Ручное масштабированиеКоэффициент увеличения:Лупа_ЛупаЛупаРежим мангиРучное масштабированиеМаксимальное количество страниц, хранимое в кеше:СвернутьСвернутьМодифицированИмяНикогдаСледующий архивСледующий архивСледующий каталогСледующая страницаСледующая страница (всегда одна страница)Нет изображений в '%s'Не найдена библиотека PILНе поддерживаемый формат архива: %sНормальный размерКоличество пикселей, прокручиваемых нажатием на стрелку:Количество пикселей, прокручиваемых поворотом колёсика мыши:ОткрытьОткрыть с помощьюОткрыть, _не закрывая библиотекуОткрыть выбранную книгу.Разработчик Comix, оригинальной версии программыВладелецPDF документСтраницаПраваПерсидский переводПожалуйста, введите имя для новой коллекции.Пожалуйста, введите новое имя для выбранной коллекции.Заметьте, в этом списке изначально только те файлы, которые MComix считает комментариями.Польский переводПольский перевод_ПараметрыПараметрыПредыдущий архивПредыдущий архивПредыдущий каталогПредыдущая страницаПредыдущая страница (всегда одна страница)Сво_йстваСвойстваПоместить коллекцию '%(subcollection)s' в коллекцию '%(supercollection)s'.ВыйтиRAR архивыОбновитьПереименоватьПоследниеОбновитьПереоткрыть текущие файлы или архивУдалить книги из библиотеки?Удалить из архиваУдали_ть из библиотекиУдалит_ь из коллекцииПереименовать коллекцию?Замена перепишет его содержимое.КореньПовернуть на 90 градусов _против часовой стрелкиП_овернуть на 180 градусовП_овернуть на 180 градусовПовернуть на 90 градусов _против часовой стрелкиПовернуть на 90 градусов по _часовой стрелкеПоворотВыполнить командуРусский переводСЛАЙДШОУНасыщенность:С_кроллерыРезкость:СохранитьСохранить как...Сохранить как...Сохранить и выйтиСохранить страницу какРежим масштабированияПрокруткаУстанавливает коэффициент увеличения лупы.Установить количество страниц в кеше. -1 означает сохранение всего архива.Установить количество "шагов", необходимых для перелистывания страницы. Меньшее количество шагов позволит переворачивать страницы быстрее, увеличивая шанс случайно перелистнуть страницу.Установить количество пикселей, прокручиваемых при использовании колёсика мыши.Установить количество пикселей, прокручиваемых при нажатии на стрелки.Устанавливает размер стороны квадратной лупы.Показывать номера страниц возле миниатюрОткрыть библиотеку при запуске.Показать номер версии и выйти.Показать эту помощь и выйти.Показать/скрыть  всёПоказать/скрыть панель менюПоказать/скрыть полосы прокруткиПоказать/скрыть строку состоянияПоказать/скрыть панель инструментовУпрощенный Китайский переводРазмерСлайдшоуЗадержка слайдшоу (в секундах):Шаг слайдшоу (в пикселях):Сортировать архивы по:Сортировать файлы и каталоги по:Испанский переводУказывает количество пикселей для прокрутки в режиме слайдшоу. Положительное значение означает прокрутку вперёд, отрицательное — назад, 0 — прокрутка на целую страницу._Строка состоянияНачать слайд_шоуНачать слайдшоуЗапустить приложение в двухстраничном режиме.Запустить приложение в полноэкранном режиме.Запустить приложение в режиме манги.Запустить приложение в режиме слайдшоу.Остановить слайдшоуСохранять информацию о недавно открытых файлах:Сохранять миниатюры открываемых файлов согласно спецификации freedesktop.org. Эти миниатюры используются многими другими приложениями, например, большинством файловых менеджеров.Шведский перевод_ИнструментыTar архивМи_ниатюрыАрхив защищён паролем:Файл будет удалён с вашего жёсткого диска.Новый архив не может быть сохранён!Оригинальные файлы не были удалены.Выбранные книги будут удалены не только из библиотеки но и с жёсткого диска. Вы уверены, что хотите продолжить?Эта ошибка может быть вызвана отсутствием библиотек GTK+.Разделитель (псевдокоманда).Размер миниатюры (в пикселях):МиниатюрыТрадиционный Китайский переводПрозрачностьСчитать все файлы, найденные внутри архивов, которые имеют эти расширения, комментариямиТипУкраинский переводНеизвестный тип файлаИспользовать серый клетчатый фон для прозрачных изображений. Если это опция отключена, в качестве фона будет использоваться сплошной белый цвет.Использовать обложку в качестве иконки программыИспользовать клетчатый фон для прозрачных изображений.Использовать динамический фоновый цветИспользовать полноэкранный режим по умолчаниюИспользовать умную прокруткуИспользовать этот цвет в качестве фона:Пользовательский интерфейсПросмотр изображений и архивов комиксов.Режим просмотраРежимы просмотраКогда активно, нажатие на клавишу Escape закрывает программу, вместо выполнения выхода из полноэкранного режима.Показывать только одну страницу при просмотре первой страницы архива или изображения, у которого ширина больше высоты.В режиме слайдшоу следующий архив будет открыт автоматически.Обычно пробел и колесо мыши прокручивает только вниз и вверх (если нажата клавиша Shift). С этой опцией они будут прокручивать и горизонтально, следуя естественному порядку чтения страниц комикса.Рабочий каталогZIP архивМасштабУвеличить масштабУменьшить масштабУвеличить масштабРежимы масштаба.Уменьшить масштаб[ПАРАМЕТРЫ...] [ПУТЬ]_О программеАвтоповорот изображенияАвтоматически подстроить контрастность_Наилучший масштаб_ЗакладкиЯркость:ОтменитьЗакрытьКонтрастность:_КопироватьУдалитьДвухстраничный режим_РедактироватьУправление закладками_Редактировать архив...Редактировать командыФайлПервая страницаВ полный _экранПерейтиПерейти на _страницу...Помо_щьИмпортировать_Сохранить преобразованияП_оследняя страница_Библиотека...Режим _манги_МенюСледующая страницаНормальный размер_Открыть_Открыть...П_редыдущая страницаВ_ыходПоследние открытыеПовернуть на 90 градусов по _часовой стрелкеСохранить и выйтиПоиск:П_анель инструментовП_анель инструментовПреобразовать изображениеВидМасштаб././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/ru/__init__.py0000644000175000017500000000000014476523373020347 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768
mcomix-3.1.0/mcomix/messages/sv/0000755000175000017500000000000014553265237016250 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768
mcomix-3.1.0/mcomix/messages/sv/LC_MESSAGES/0000755000175000017500000000000014553265237020035 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/sv/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022136 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/sv/LC_MESSAGES/mcomix.mo0000644000175000017500000012757114476523373021705 0ustar00moritzmoritz
 ++)+,,+?,(k,:,,K,';-#c--#---%-5.S.j.9.0.0.-/L////	0B0AS00000
0)1.*1<Y11
111	11Z1P2o22922=233*333	@3	J3
T3_39f3C333
4448(4!a4	44]4-5!/5Q52"6#U6y6S7B_777k78F8U8888

9	9
"909	99 C9qd999r:z:&::P:%;+;D;
Q;\;
d;r;;
;;7;;1<%=<-c<+<"<-<= .=%O=u=1===/=>	(>2>
D>
O>]>d>/q>.>%> >
?	"?,?A?rI???6?@(@7@D@[@
m@{@@@@@@	@
@	A2AAAGAA&B
BBBCC
"C
0C>COC\CtCCCCCC3D"DLD2EBE<TEE	E
EEEE+E
F
(F6FHF	dFnFFFFFF
FF8F7GFG]G{GG!GAGHH/H?CHH@HHHHH
I
I	I(I
0I>IRI
eIsIIIIIIII!I
J*J0;J.lJ	JJJHJK
K
KK$K5K
9KGKTK	cKmKKK%K
KBK #LDL
KLYL3eL/L0LLM$M
)M4MRM%jMM%M<M"
N0N6NCNHNTN+hN4NNsSOOOOOPP P1P
DPRPrPP
POPPJPAQMQVQ^QeQ.mQQQQQXRD^R5R7RS $SMES)SS6STT#T7TJT`TuT~TT	TTTTTTT
TTU5UOU\UvU}UUUUUUUUV%V:VMV	aVPkVV4VRWI[WWFVXGXTX":Y`]Y	YYY
Y%YZ/Z	OZYZiZ!ZZ
ZZZZ
[[;[	@[J[h[[[[[[[[
\\\*\)]$H](m]5]2]1]1^.@^!o^^<7_t__	___"_,_#`)3`t]`5`#aa,aa
aaaaaXaObTbjb;{b{b)3c/]cccccc$d	,d
6dWAdxdJe]eqef.fHfflgxg}g	gg
ggggggggh
hh,h	4h>h
EhPhVh^h
ph{hhhhhhhhhhhh
iii+i
4i?iLiRi[ijipixiiiii	iiiiiijjj jk1k'l20l4clEl#lSm-Vm*mm#mmn'$n:Lnnn>n>n)4o#^oo#p2p;p	DpHNpBp pp$q$(qMq&Vq/}q9qqq	r r>rQrrlr)r	s s@(sis@{sss	s
ss
tt"t4)tH^tttttt8t#u@u!Iu]ku5u!u!v7w!9w[wXwAUx/xlx>4yIsyyyyzz&z	>zHz&Pzwzz{2{{+{{D|U|\|x||||||||3|#},C})p},}5}#}0!~R~'p~2~~6~-D'T|


..!)PzŀK]3nҁ$"G_r

6)s
„ׄ+FWw"ʅ,&Ui*҇	:/
jx
ƈ
ވ
"'4A;A}ى" <CPNJ
T#x	Ћ	܋+;SYk"nj
,-5	cmvO֍ۍ

)6EQn+Ď?Ԏ4;J.Y*%ُ	"?3[(CҐ3:GLY$p0ƑkGȒݒ"5J"\
FN	Q
[	fp	x>"!^4I9ݕ9QbX;ږ(C?Ɨޗ"+;	LV	\
fq9
!.=Jd~řޙ,Q9%1TH88@=yd&hC
˝ڝ+!"
DRc #Ǟמ$"?bj$s˟ޟ",
'/$W|#>@@@0(¢B	(8,W"%oͤ5=(sa .$;`oU{Ѧզ
FB.Ч//On!#
kpf;ש&
٪	@J	P	Zd
mxҬ	")5
=HPY
k	vƭխ
(
5COXeu
|Ӯ
")2?H[artUo
xr]R?	G?=#t04&^
VvW[pS%)M6'e*Jf1Tvhg}jZ`\J^OW 
BEHUEiyh[@((/$V
=A|s#>|5z;I209.3m:k7e	-FBu`"PXs,$%j1XFDlaLG4d9
<)fTIRH!_ZC	q*z5&P;+wNp6nYLMlo3Q>q,dOw~c\bmc8
D.Cb/Nn_{u7Q!}@"-a+8 'r2y~SkAYK{:<Kgxi] of %s! Callback %(function)r failed: %(error)s! Corrupt preferences file "%s", deleting...! Could neither find pysqlite2 nor sqlite3.! Could not add book "%s" to the library! Could not add book %(book)s to collection %(collection)s! Could not add collection "%s"! Could not add file %(sourcefile)s to archive %(archivefile)s, aborting...! Could not create archive at path "%s"! Could not get cover for book "%s"! Could not load icon "%s"! Could not parse bookmarks file %s! Could not read %s! Could not remove file "%s"! Could not rename collection to "%s"! Could not save thumbnail "%(thumbpath)s": %(error)s! Extraction error: %s! Non-existant book #%i! Worker thread processing %(function)r failed: %(error)s! You need an sqlite wrapper to use the library."%s" does not appear to have a valid executable."%s" does not have a valid working directory.%(filename)s's extracted size is %(actual_size)d bytes, but should be %(expected_size)d bytes. The archive might be corrupt or in an unsupported format.%d comments%d pages%s archives%s images%s is an image viewer specifically designed to handle comic books.%s is licensed under the terms of the GNU General Public License.'%s' is disabled for archives.(Copy)...when height exceeds width...when width exceeds height7z archiveA collection by that name already exists.A copy of this license can be obtained from %sA file named '%s' already exists. Do you want to replace it?AccessedAdd _BookmarkAdd _separatorAdd a new empty collection.Add booksAdd books to '%s'.Add information about all files opened from within MComix to the shared recent files list.Add more books to the library.Add new collection?AddedAdded %(count)d new books from directory '%(directory)s'.Added books:Added new book '%(bookname)s' from directory '%(directory)s'.Adding '%s'...Adding booksAdvancedAll archivesAll booksAll filesAll imagesAlwaysAlways use this selected colour as the background colour.Always use this selected colour as the thumbnail background colour.Animated imagesAnimation mode:AppearanceArchiveArchive commentsArchive-related variables can only be used for archives.Archives are stored as ZIP files.AscendingAuto-detect (Default)Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically hide all toolbars in fullscreenAutomatically open next directoryAutomatically open the first file in the next sibling directory when flipping past the last page of the last file in a directory, or the previous directory when flipping past the first page of the first file.Automatically open the last viewed file on startupAutomatically open the next archiveAutomatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically open, on startup, the file that was open when MComix was last closed.Automatically pick a background colour that fits the viewed image.Automatically rotate images according to their metadataAutomatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.Automatically scan for new books when library is _openedAutomatically use the colour that fits the viewed image for the thumbnail background.Autorotate by heightAutorotate by widthBack ten pagesBackgroundBehaviourBest fit modeBilinearBook nameBrazilian Portuguese translationBy enabling this setting, the first page of a book will be used as application icon instead of the standard icon.Bzip2 compressed tar archiveCatalan translationChanges how images are scaled. Slower algorithms result in higher quality resizing, but longer page loading times.Changes the book cover size.Changes the sort order of the library.Clear _dialog choicesClears all dialog choices that you have previously chosen not to be asked again.CloseCloses all opened files.Co_mments...CollectionCommandCommand labelCommand line is empty.Comment extensions:Comment filesCommentsCompletely removes the selected books from the library.Continue reading from page %d?Controls how animated images should be displayed.Copies the current page to clipboard.Copies the selected book's path to clipboard.Could not add a new collection called '%s'.Could not change the name to '%s'.Could not determine library database version!Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCould not run command %(cmdlabel)s: %(exception)sCouldn't load keybindings: %sCover si_zeCreates a duplicate of the selected collection.Croatian translationCustom...Czech translationDate addedDebug optionsDeleteDelete "%s"?Delete information about recently opened files?Deletes the current file or archive from disk.Deletes the selected books from disk.Deletes the selected collection.DescendingDirectoryDisabled in archivesDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Do not ask again.Double page modeDuring a slideshow automatically open the next archiveDutch translationEdit BookmarksEdit archiveEdit external commandsEn_hance image...Enhance imageEscape key closes programExecute external commandExit from fullscreenExternal commandsExtraction and cacheFileFile nameFile orderFile sizeFile-related variables can only be used for files.FilesFiles will be opened and displayed according to the sort order specified here. This option does not affect ordering within archives.Files within archives will be sorted according to the order specified here. Natural order will sort numbered files based on their natural order, i.e. 1, 2, ..., 10, while literal order uses standard C sorting, i.e. 1, 2, 34, 5.Finished reading on %(date)s, %(time)sFirst pageFit _height modeFit _size modeFit _width modeFit height modeFit size modeFit to heightFit to size modeFit to widthFit to width or height:Fit width modeFixed size for this mode:Fli_p horizontallyFlip _verticallyFlip horizontallyFlip pages when scrolling "off the page" with the scroll wheel or with the arrow keys. It takes n consecutive "steps" with the scroll wheel or the arrow keys for the pages to be flipped.Flip pages when scrolling off the edges of the pageFlip two pages in double page modeFlip two pages, instead of one, each time we flip pages in double page mode.Flip verticallyForward ten pagesFraction of page to scroll per space key press (in percent):French translationFull pathFullscreenFullscreen modeGalician translationGerman translationGerman translation and Nautilus thumbnailerGo to pageGo to page...Greek translationGzip compressed tar archiveH_ide allHebrew translationHugeHungarian translationHyperbolic (slow)Icon designImageImage qualityImagesIncomplete escape sequence. For a literal '%', use '%%'.Incomplete quote sequence. For a literal '"', use '%"'.Indonesian translationInvalid escape sequence: %%%sInvalid path: '%s'Invert smart scrollInvert smart scrolling direction.It reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKeep transformationKeeps the currently selected transformation for the next pages.Key %dKeybinding for "%(action)s" overrides hotkey for another action.Korean translationLHA archiveLabelLanguage (needs restart):LargeLast modifiedLast pageLibraryLibrary booksLibrary collectionsLibrary watch listLiteral orderLithuanian translationLocationMComix developerM_anual zoom modeMagnification factor:Magnifying LensMagnifying _lensMagnifying lensMagnifying lens size (in pixels):Manga modeManual zoom modeMaximum number of concurrent extraction threads:Maximum number of pages to store in the cache:Mi_nimizeMinimizeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNatural orderNavigationNeverNever autorotateNewNext _archiveNext archiveNext directoryNext pageNext page (always one page)Next page (dynamic)No images in '%s'No new books found in directory '%s'.No sortingNo version of the Python Imaging Library was found on your system.Non-supported archive format: %sNormalNormal (fast)Normal sizeNumber of "steps" to take before flipping the page:Number of pixels to scroll per arrow key press:Number of pixels to scroll per mouse wheel turn:Only for title pagesOnly for wide imagesOpenOpen _withOpen _without closing libraryOpen the selected book.Open the watchlist management dialog.Opens the archive editor.Opens the selected books for viewing.Opens the selected books, but keeps the library window open.Original vision/developer of ComixOwnerPDF documentPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Please note that the only files that are automatically added to this list are those files in archives that MComix recognizes as comments.Please refer to the external command documentation for a list of usable variables and other hints.Polish translatinPolish translationPr_eferencesPreferencesPreview:Previous a_rchivePrevious archivePrevious directoryPrevious pagePrevious page (always one page)Previous page (dynamic)Proper_tiesPropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.QuitQuits and restores the currently opened file next time the program starts.RAR archiveRe_freshRe_nameRecentRefreshReloads the currently opened files or archive.Remove books from the library?Remove from archiveRemove from the _libraryRemove from this _collectionRemoved %(num)d book from '%(collection)s'.Removed %(num)d books from '%(collection)s'.Removed %d book from the library.Removed %d books from the library.Removes no longer existant books from the collection.Removes the selected books from the current collection.Rename collection?Renames the selected collection.Replace existing bookmark on page %s?Replace existing bookmarks on pages %s?Replacing it will overwrite its contents.Reset to defaults.Resets all keyboard shortcuts to their default values.RootRotat_e 90 degrees CCWRotate 180 de_greesRotate 180 degreesRotate 90 degrees CCWRotate 90 degrees CWRotationRun _commandRussian translationSLIDESHOWS_aturation:S_crollbarsS_harpness:SaveSave AsSave _AsSave and quitSave changes to commands?Save page asSave the selected values as default for future files.Scaling modeScanning for new books...ScrollScroll downScroll leftScroll rightScroll to bottom centerScroll to bottom leftScroll to bottom rightScroll to centerScroll to middle leftScroll to middle rightScroll to top centerScroll to top leftScroll to top rightScroll upSelecting "No" will create a new bookmark without affecting the other bookmarks.Set library cover sizeSet the magnification factor of the magnifying lens.Set the max number of pages to cache. A value of -1 will cache the entire archive.Set the maximum number of concurrent threads for formats that support it.Set the number of "steps" needed to flip to the next or previous page.  Less steps will allow for very fast page turning but you might find yourself accidentally turning pages.Set the number of pixels to scroll on a page when using a mouse wheel.Set the number of pixels to scroll on a page when using the arrow keys.Set the size of the magnifying lens. It is a square with a side of this many pixels.Sets the desired output log level.Sets the percentage by which the page will be scrolled down or up when the space key is pressed.ShortcutsShow OSD panelShow file numbersShow filenameShow only one page where appropriate:Show page numbersShow page numbers on thumbnailsShow pathShow resolutionShow the library on startup.Show the version number and exit.Show this help and exit.Show/hide allShow/hide menubarShow/hide scrollbarsShow/hide statusbarShow/hide toolbarSimplified Chinese translationSizeSlideshowSlideshow delay (in seconds):Slideshow step (in pixels):SmallSmart scroll downSmart scroll upSort archives by:Sort files and directories by:Spanish translationSpecify the number of pixels to scroll while in slideshow mode. A positive value will scroll forward, a negative value will scroll backwards, and a value of 0 will cause the slideshow to always flip to a new page.St_atusbarStart _slideshowStart slideshowStart the application in double page mode.Start the application in fullscreen mode.Start the application in manga mode.Start the application in slideshow mode.Start the application with zoom set to best fit mode.Start the application with zoom set to fit height.Start the application with zoom set to fit width.Stop slideshowStore information about recently opened files:Store thumbnails for opened filesStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.Stretch images to fit to the screen, depending on zoom mode.Stretch small imagesSwedish translationT_oolbarsTar archiveTh_umbnailsThe archive is password-protected:The file will be deleted from your harddisk.The new archive could not be saved!The original files have not been removed.The selected books will be removed from the library and permanently deleted. Are you sure that you want to continue?This error might be caused by missing GTK+ libraries.This is a separator pseudo-command.This will remove all entries from the "Recent" menu, and clear information about last read pages.Thumbnail size (in pixels):ThumbnailsTinyTraditional Chinese translationTransformationTransparencyTreat all files found within archives, that have one of these file endings, as comments.TypeUkrainian translationUnknown filetypeUpgrading library database version from %(from)d to %(to)d.Use a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.Use archive thumbnail as application iconUse checkered background for transparent imagesUse dynamic background colourUse fullscreen by defaultUse smart scrollingUse this colour as background:User interfaceView images and comic book archives.View modeView modesWhen active, the ESC key closes the program, instead of only disabling fullscreen mode.When showing the first page of an archive, or an image's width exceeds its height, only a single page will be displayed.While in slideshow mode allow the next archive to automatically be opened.With subdirectoriesWith this preference set, the space key and mouse wheel do not only scroll down or up, but also sideways and so try to follow the natural reading order of the comic book.Working directoryXZ compressed tar archiveYou have made changes to the list of external commands that have not been saved yet. Press "Yes" to save all changes, or "No" to discard them.You stopped reading here on %(date)s, %(time)s. If you choose "Yes", reading will resume on page %(page)d. Otherwise, the first page will be loaded.ZIP archiveZoomZoom _InZoom _OutZoom inZoom modesZoom out[OPTION...] [PATH]_About_Add_Add..._Auto-rotate image_Automatically adjust contrast_Best fit mode_Bookmarks_Brightness:_Cancel_Clean up_Close_Contrast:_Copy_Delete_Double page mode_Duplicate_Edit_Edit Bookmarks..._Edit archive..._Edit commands_File_First page_Fullscreen_Go_Go to page..._Help_Import_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Normal Size_Open_Open..._Previous page_Quit_Recent_Remove_Remove and delete from disk_Reset keys_Rotate 90 degrees CW_Save and quit_Scan now_Search:_Sort_Toolbar_Tools_Transform image_View_Watch list_ZoomProject-Id-Version: MComix 0.90.4
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2018-02-24 08:43+0100
Last-Translator: Jonatan Nyberg 
Language-Team: Swedish 
Language: sv
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Generated-By: pygettext.py 1.5
Plural-Forms: nplurals=2; plural=(n != 1);
X-Generator: Poedit 2.0.6
 av %s! Återanrop %(function)r misslyckades: %(error)s! Korrupt preferensfil "%s", raderar...! Kunde inte hitta varken pysqlite2 eller sqlite3.! Kunde inte lägga till boken "%s" till biblioteket! Kunde inte lägga till boken %(book)s till samlingen %(collection)sKunde inte lägga till samling "%s"! Kunde inte lägga till fil %(sourcefile)s till arkiv %(archivefile)s, avbryter...! Kunde inte skapa arkivet i sökvägen: "%s"! Kunde inte hämta omslag för boken "%s"! Kunde inte ladda ikon "%s"! Kunde inte parsa bokmärkesfil %s! Kunde inte läsa %sKunde inte ta bort filen "%s"Kunde inte döpa om samlingen till "%s"! Kunde inte spara miniatyrbild "%(thumbpath)s": %(error)s! Extraktionsfel: %s! Ickeexisterande bok #%i! Arbetstråd bearbetning %(function)r misslyckades: %(error)s! Du behöver en sqlite wrapper för att använda biblioteket."%s" verkar inte ha en giltig exekverbar."%s" har inte en giltig arbetsmapp.%(filename)ss extraherade storlek är %(actual_size)d bytes, men borde vara %(expected_size)d bytes. Arkivet kan vara skadat eller i ett format som inte stöds.%d kommentarer%d sidor%s arkiv%s bilder%s är en bildvisare speciellt utformad för att hantera serietidningar.% s är licensierad enligt villkoren i GNU General Public License.'%s' är inaktiverat för arkiv.(Kopia)... när höjden överstiger bredden... när bredden överstiger höjden7z-arkivEn samling med det namnet finns redan.En kopia av denna licens kan erhållas från %sEn fil med namnet '%s' finns redan. Vill du ersätta den?ÅtkomsttidLägg till _bokmärkeLägg till _avskiljareLägg till en ny tom samling.Lägg till böckerLägg till böcker i '%s'.Lägg till information om alla filer som öppnas inifrån MComix till den delade listan med senast öppnade filer.Lägg till fler böcker till biblioteket.Lägg till ny samling?TillagdLade till %(count)d nya böcker från katalogen '%(directory)s'.Tillagda böcker:Lade till ny bok '%(bookname)s' från katalogen '%(directory)s'.Lägger till '%s'...Lägger till böckerAvanceratAlla arkivAlla böckerAlla filerAlla bildfilerAlltidAnvänd alltid denna valda färg som bakgrundsfärg.Använd alltid denna valda färg som bakgrundsfärg till miniatyrbilder.Animerade bilderAnimeringsläge:UtseendeArkivArkiv kommentarerArkivrelaterade variabler kan bara användas för arkiv.Arkiven sparas i ZIP-format.StigandeAutomatisk detektering (standard)Justera kontrast automatiskt (både ljushet och mörkhet), separat för varje färgkomponent.Göm automatiskt alla verktygsrader i helskärmslägeÖppna automatiskt nästa katalogÖppna den första filen automatiskt i nästa broschyrkatalog när du bläddrar över den sista sidan i den sista filen i en katalog eller i den tidigare katalogen när du bläddrar över första sidan av den första filen.Öppna automatiskt den senast lästa filen vid uppstartÖppna automatiskt nästa arkivetÖppna automatiskt nästa arkiv i katalogen när vi bläddrar förbi den sista sidan, eller det föregående arkivet när vi bläddrar förbi den första sidan.Öppna automatiskt, vid uppstart, den fil som var öppnad när MComix avslutades senast.Välj automatiskt en bakgrundsfärg som passar den visade bilden.Rotera bilder automatiskt enligt deras metadataRotera bilder automatisk om en orientering finns specificerad i bildens metadata, exempelvis i en Exif-tagg.Skanna automatiskt efter nya böcker när biblioteket _öppnasVälj automatiskt en bakgrundsfärg som passar den visade miniatyrbilden.Rotera automatiskt efter höjdRotera automatiskt efter breddTillbaka tio sidorBakgrundsfärgBeteendeBästa anpassningslägeBilinjärBoknamnBrasiliansk-portugisisk översättningGenom att aktivera denna inställning kommer första sidan i en bok att användas som applikationsikon istället för standardikonen.Bzip2-komprimerat tar-arkivKatalansk översättningÄndrar hur bilderna skalas. Långsammare algoritmer resulterar i högre kvalitet storleksändring, men längre sidladdningstider.Ändrar bokomslagets storlek.Ändrar sorteringsordning för biblioteket.Rensa _dialogvalRensar alla dialogval som du tidigare valt att inte bli ombedd igen.StängStänger alla öppna filer.Ko_mmentarer...SamlingKommandoKommandotikettKommandoraden är tom.Kommentarsfiländelser:KommentarerKommentarerTar helt bort de valda böckerna från biblioteket.Fortsätt läsa från sidan %d?Kontrollerar hur animerade bilder ska visas.Kopierar den aktuella sidan till Urklipp.Kopierar den valda bokens väg till urklipp.Kunde inte lägga till en ny samling med namnet '%s'.Kunde inte ändra namnet till '%s'.Kunde inte bestämma biblioteksdatabasversionen!Kunde inte duplicera samling.Kunde inte öppna %s: Filen finns inte.Kunde inte öppna %s: Otillräckliga rättigheter.Kunde inte läsa %sKunde inte köra kommandot %(cmdlabel)s: %(exception)sDet gick inte att läsa tangentbindningar: %sOmslagssto_rlekSkapar en kopia av den valda samlingen.Kroatisk översättningAnpassa...Tjeckisk översättningDatum tillagdFelsökningsalternativTa bortTa bort "%s"?Ta bort information om nyligen öppnade filer?Tar bort aktuell fil eller arkiv från disken.Tar bort de valda böckerna från disken.Tar bort den valda samlingen.FallandeKatalogInaktiverad i arkivVisningVisa endast de böcker där den angivna textsträngen förekommer i deras sökväg. Sökningen är oberoende av gemener och versaler.Fråga inte igen.DubbelsideslägeUnder ett bildspel öppnas automatiskt nästa arkivNederländsk översättningRedigera bokmärkenRedigera arkivRedigera externa kommandonFö_rbättra bild...Förbättra bildTangenten Escape stänger programmetUtför externt kommandoAvsluta fullskärmExterna kommandonExtraction och cacheminneFilFilnamnFilordningFilstorlekFilrelaterade variabler kan bara användas för filer.FilerFilerna öppnas och visas enligt den sorteringsordning som anges här. Det här alternativet påverkar inte ordningen inom arkiv.Filer inom arkiv sorteras baserat på ordning som anges här. Naturlig ordning kommer att sortera numrerade filer baserat på deras naturliga ordning, dvs 1, 2, ..., 10, medan bokstavsordning använder standard C-sortering, d.v.s. 1, 2, 34, 5.Avslutade läsning den %(date)s, %(time)sFörsta sidanH_öjdanpassningslägePassa _storlekslägeBr_eddanpassningslägeHöjdanpassningslägeAnpassa storlekslägePassa till höjdAnpassa till storlekslägePassa till breddAnpassa till bredd eller höjd:BreddanpassningslägeFast storlek för det här läget:S_pegelvänd horisontelltSpegelvänd _vertikaltSpegelvänd horisontelltVänd blad när du scrollar "bortom sidan" med scrollhjulet eller med piltangenterna. Det krävs tre på varandra följande "steg" med scrollhjulet eller piltangenterna för att sidan ska vändas.Vänd blad vid rullning förbi sidans kanterBläddra två sidor i dubbelsideslägeBläddra två sidor, istället för en, varje gång vi byter sida i dubbelsidesläge.Spegelvänd vertikaltFramåt tio sidorAntal pixlar att rulla med piltangenterna:Fransk översättningFullständig sökvägHelskärmFullskärmslägeGalicisk översättningTysk översättningTysk översättning och miniatyrbildsskapare för NautilusGå till sidaGå till sida...Grekisk översättningGzip-komprimerat tar-arkiv_Göm allaHebreisk översättningJättestorUngersk översättningHyperbolisk (långsam)IkondesignBildBildkvalitetBilderOfullständig flyktsekvens. För en bokstavlig '%', använd '%%'.Ofullständig citatsekvens. För en bokstavlig '"', använd '%"'.Indonesisk översättningOgiltig flyktsekvens: %%%sOgiltig sökväg: "%s"Vänd smart rullningInvertera smart rullningsriktning.Den läser ZIP-, RAR- och tar-arkiv, samt vanliga bildfiler.Italiensk översättningJapansk översättningBehåll transformationBehåller den för närvarande valda transformationen för de följande sidorna.Tangent %dTangentbindning för "%(action)s" åsidosätter snabbtangent för en annan åtgärd.Koreansk översättningLHA-arkivEtikettSpråk (kräver omstart):StorSenast ändradSista sidanBibliotekBiblioteksböckerBibliotekssamlingarBevakningslista för bibliotekBokstavsordningLitauisk översättningPlatsMComix utvecklareM_anuellt zoomlägeFörstoringsfaktor:FörstoringsglasFörstorings_glasFörstoringsglasFörstoringslenstorlek (i pixlar):MangalägeManuellt zoomlägeMaximalt antal samtidiga extraktionsgängor:Maximalt antal sidor att lagra i cacheminnet:Mi_nimeraMinimeraModifieringstidFlytta böcker från '%(source collection)s' till '%(destination collection)s'.NamnNaturlig ordningNavigeringAldrigRotera aldrig automatisktNyNästa _arkivNästa arkivNästa katalogNästa sidaNästa sida (alltid en sida)Nästa sida (dynamisk)Inga bilder i '%s'Inga nya böcker hittades i katalogen '%s'.Ingen sorteringIngen version av Python Imaging Library funnet på ditt system.Arkivformat som inte stöds: %sNormalNormal (snabb)Normal storlekAntal nödvändiga "steg" innan bladet vänds:Antal pixlar att rulla med piltangenterna:Antal pixlar att rulla med mushjulet:Endast för titelsidorEndast för breda bilderÖppnaÖppna _med_Öppna utan att stänga bibliotekÖppna den markerade boken.Öppna hanteringsdialogruta för bevakningslistans.Öppnar arkivredigeraren.Öppnar de valda böckerna för visning.Öppnar de valda böckerna, men håller biblioteksfönstret öppet.Första utvecklaren av ComixÄgarePDF-dokumentSidaRättigheterPersisk översättningAnge ett namn åt den nya samlingen.Ange ett nytt namn för den markerade samlingen.Notera att de enda filer som automatiskt läggs till i denna lista är de filer i arkiv som MComix känner igen som kommentarer.Se extern kommando-dokumentation för en lista över användbara variabler och andra tips.Polsk översättningPolsk översättningInst_ällningarInställningarFörhandsvisning:Föregående a_rkivFöregående arkivFöregående katalogFöregående sidaFöregående sida (alltid en sida)Föregående sida (dynamisk)Egensk_aperEgenskaperLägg samlingen '%(subcollection)s' i samlingen '%(supercollection)s'.AvslutaAvslutar och återställer den öppnade filen nästa gång programmet startar.RAR-arkiv_UppdateraDö_pa omSenasteUppdateraUppdaterar de för tillfället öppnade filerna eller arkivet.Ta bort böcker från biblioteket?Ta bort från arkivTa bort från biblioteketTa bort från den här _samlingenTog bort %(num)d bokr från '%(collection)s'.Tog bort %(num)d böcker från '%(collection)s'.Tog bort %d bok från biblioteket.Tog bort %d böcker från biblioteket.Tar bort inte längre befintliga böcker från samlingen.Tar bort de valda böckerna från den aktuella samlingen.Döp om samling?Döper om den valda samlingen.Ersätt befintligt bokmärke på sidan %s?Ersätt befintliga bokmärken på sidorna %s?Ersätts filen så kommer dess innehåll att skrivas över.Återställ till standardinställningar.Återställer alla tangentbordskortkommandon till standardvärdena.RotRot_era 90 grader motsolsRotera 180 _graderRotera 180 graderRotera 90 grader motursRotera 90 grader medursRotationKör _kommandoRysk översättningBILDSPELF_ärgmättnad:_RullningslisterS_kärpa:SparaSpara somSpara _somSpara och avslutaSpara ändringar i kommandon?Spara sidan somSpara de valda värdena som standard för framtida filer.SkaleringslägeLetar efter nya böcker...ScrollningRulla nedåtRulla vänsterRulla högerRulla till botten centrumRulla till nedre vänstraRulla till botten högerRulla till centrumRulla till mellan vänsterRulla till mellan högerRulla till toppen centrumRulla till toppen vänsterRulla till toppen högerRulla uppåtOm du väljer "Nej" skapas ett nytt bokmärke utan att påverka andra bokmärken.Ställ in bibliotekets omslagsstorlekAnge förstoringsfaktorn för förstoringsglaset.Ange maximalt antal sidor att cacha. Ett värde av -1 kommer att cacha hela arkivet.Ange det maximala antalet samtidiga trådar för format som stöder det.Ange antal nödvändiga "steg" som krävs för att bläddra till nästa eller föregående blad. Färre steg medför snabbare vändning av blad men kan innebära att du bläddrar av misstag.Ange antal pixlar att scrolla på en sida med mushjulet.Ange antal pixlar att scrolla på en sida med piltangenterna.Ange storleken på förstoringsglaset. Det är en kvadrat vars sida är så här många pixlar bred.Ställer in önskad utgångsloggnivå.Ställer in den procentandel som sidan ska rullas ner eller uppåt när mellanslagstangenten trycks ned.KortkommandonVisa OSD-panelenVisa filnummerVisa filnamnVisa endast en sida där det är lämpligt:Visa sidnummerVisa sidnummer på miniatyrbilderVisa sökvägVisa upplösningVisa biblioteket vid uppstart.Visa versionsnummer och avsluta.Visa den här hjälpen och avsluta.Visa/dölj allaVisa / dölj menyfältetVisa/dölj rullningslisterVisa/dölj statusfältetVisa/dölj verktygsfältetFörenklad kinesisk översättningStorlekBildspelBildspelsfördröjning (i sekunder):Bildspelssteg (i pixlar):LitenSmart rulla nedåtSmart rulla uppåtSortera arkiv efter:Sortera filer och kataloger efter:Spansk översättningSpecificera antal pixlar att scrolla i bildspelsläge. En positiv siffra kommer scrolla frammåt, en negativa siffra kommer scrolla bakåt och siffran 0 kommer göra att bildspelet alltid bläddrar till en ny sida.St_atusradStarta _bildspelStarta bildspelStarta programmet i dubbelsidigt läge.Starta programmet i helskärmsläge.Starta programmet i mangaläge.Starta programmet i bildspelsläge.Starta programmet med zoominställningen till bästa passform.Starta programmet med zoominställningen för att passa höjden.Starta programmet med zoominställningen för att passa bredden.Stoppa bildspelLagra information om de senast öppnade filerna:Lagra miniatyrbilder för öppnade filerLagra miniatyrbilder för filer som öppnas, i enlighet med freedesktop.org-specifikationen. Dessa miniatyrbilder delas av många andra program, exempelvis de flesta filhanterare.Sträck ut bilder för att passa skärmen, beroende på zoomläge.Sträck ut små bilderSvensk översättning_VerktygsraderTar-arkivMiniat_yrbilderArkivet är lösenordsskyddat:Filen kommer att tas bort från hårddisken.Det nya arkivet kunde inte sparas!Originalfilerna har inte tagits bort.De valda böckerna tas bort från biblioteket och tas bort permanent. Är du säker på att du vill fortsätta?Det här felet kan orsakas av saknade GTK+-bibliotek.Detta är ett avskiljar-pseudo-kommando.Detta tar bort alla poster från menyn "Senaste" och raderar information om senaste lästa sidor.Miniatyrbildsstorlek (i pixlar):MiniatyrbilderMycket litenTraditionell kinesisk översättningTransformationTransparensBehandla alla filer funna i arkiv, som har en av dessa filändelser, som kommentarer.TypUkrainsk översättningOkänd filtypUppgradering av biblioteksdatabasversionen från %(from)d till %(to)d.Använd en grårutig bakgrund till transparenta bilder. Om den här inställningen är avaktiverad så används en helvit bakgrund istället.Använd arkivminiatyrbild som applikationsikonAnvänd rutig bakgrund för transparenta bilderAnvänd dynamisk bakgrundsfärgAnvänd helskärm som standardAnvänd smart rullningAnvänd denna färg som bakgrund:AnvändargränssnittVisa bilder och serietidningsarkiv.VisningslägeVisa lägenNär den är aktiv stänger tangenten ESC programmet, istället för att endast inaktivera helskärmsläge.När du visar den första sidan i ett arkiv eller en bilds bredd överstiger dess höjd visas bara en enda sida.Vid bildspelsläge kommer nästa arkiv automatiskt öppnas.Med underkatalogerMed denna inställningsuppsättning rullar inte mellanslags- och mushjulet ner eller uppåt, utan även sidledes och så försöker du följa serietidningens naturliga läsorder.ArbetsmappXZ-komprimerat tar-arkivDu har gjort ändringar i listan över externa kommandon som inte har sparats än. Tryck på "Ja" för att spara alla ändringar, eller "Nej" för att kassera dem.Du slutade läsa här den %(date) s, %(time)s. Om du väljer "Ja", kommer läsningen att fortsätta på sidan %(page)d. I annat fall laddas den första sidan.ZIP-arkivZoomaZooma _inZooma _utZooma inZoomlägenZooma ut[ALTERNATIV...][SÖKVÄG]_Om_Lägg till_Lägg till..._Automatisk rotera bildJustera kontrast _automatiskt_Bästa anpassningsläge_Bokmärken_Ljusstyrka:Avbryt_Städa upp_Stäng_Kontrast:Kopiera_Ta bort_Dubbelsidesläge_Duplicera_Redigera_Redigera bokmärken...R_edigera arkiv..._Redigera kommandon_Arkiv_Första sidan_Helskärmsläge_Gå_Gå till sida..._Hjälp_ImporteraBehåll transf_ormation_Sista sidanBib_liotek..._Mangaläge_Menyrad_Nästa sida_Normal storlekÖppna_Öppna...F_öregående sida_AvslutaSenaste_Ta bort_Radera och ta bort från disk_Återställ tangenter_Rotera 90 grader medsols_Spara och avsluta_Skanna nu_Sök:_Sortera_Verktygsrad_Verktyg_Transformera bild_Visa_Bevakningslista_Zooma././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/sv/__init__.py0000644000175000017500000000000014476523373020351 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768
mcomix-3.1.0/mcomix/messages/uk/0000755000175000017500000000000014553265237016237 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768
mcomix-3.1.0/mcomix/messages/uk/LC_MESSAGES/0000755000175000017500000000000014553265237020024 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/uk/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022125 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/uk/LC_MESSAGES/mcomix.mo0000644000175000017500000003442714476523373021671 0ustar00moritzmoritz	8
9
E
N
)U
<


	


1@	M	W
a9l
!]9Bk
z	
 +"$G g%rbs

Lb
u+	A*l	
"H+t	y+4	>Q^
j
xO)%OTk	4THg	lv
	;EQ#])
X<R{c
%+<BNZ^d
y
ZNl=W#60g$9';Oa^}
:/  !"-#4#!G#)i#'#)##I#1B$4t$D$E$#4%'X%%
%%'T&'|&&)&
&&-'-C',q','6'2(r5('(%(\(!S)&u))#)))*-*yI*'*#*%+5+U+j+&++++,%,A,aR,	,!,%,-)-A-P-
a-!l-P-]-#=.a.u.#..`.$/23/ f/,/2//N/C0_0%r00000h0jZ18112%2
B2IP2
33353A4C4<V444';5$c5566"666(6
!7#/7	S7]7&z777*7 7
8&8	=8"G8j8$88J8
8
	9	X(n8]W.Pw'u)e06$\HxZfA&OTk{/o;j=LiD^Jty@IabF` zpgs|QlS-hVd3#MN>4B9
CvUcq,E7Y[5_<2K
}R!r~G"%*m:1+?%d comments%d pages(Copy)A collection by that name already exists.A file named '%s' already exists. Do you want to replace it?AccessedAdd a new empty collection.Add booksAdd books to '%s'.Add more books to the library.Add new collection?Adding '%s'...Adding booksAll booksAll filesAll imagesAlways use this selected colour as the background colour.AppearanceArchiveArchives are stored as ZIP files.Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically pick a background colour that fits the viewed image.Automatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.BackgroundBehaviourBest fit modeBrazilian Portuguese translationBzip2 compressed tar archiveCatalan translationCommentsCould not add a new collection called '%s'.Could not change the name to '%s'.Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCroatian translationCzech translationDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Double page modeDutch translationEdit archiveEnhance imageFilesFirst pageFit _height modeFit _width modeFit height modeFit width modeFli_p horizontallyFlip _verticallyFlip two pages, instead of one, each time we flip pages in double page mode.French translationFullscreenGerman translation and Nautilus thumbnailerGreek translationGzip compressed tar archiveH_ide allHungarian translationIcon designImageImagesIndonesian translationIt reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKorean translationLast pageLibraryLocationM_anual zoom modeMagnifying LensMagnifying _lensMagnifying lensManga modeManual zoom modeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNext pageNo images in '%s'OpenOpen the selected book.OwnerPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Polish translationPr_eferencesPreferencesPrevious pagePropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.RAR archiveRemove books from the library?Remove from archiveRename collection?Replacing it will overwrite its contents.RootRotat_e 90 degrees CCWRotate 180 de_greesRotationRussian translationSLIDESHOWS_crollbarsSaveScrollSet the magnification factor of the magnifying lens.Set the size of the magnifying lens. It is a square with a side of this many pixels.Simplified Chinese translationSizeSlideshowSpanish translationSt_atusbarStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.T_oolbarsTar archiveTh_umbnailsThe new archive could not be saved!The original files have not been removed.ThumbnailsTraditional Chinese translationTransparencyTreat all files found within archives, that have one of these file endings, as comments.Ukrainian translationUnknown filetypeUse a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.ZIP archive_About_Best fit mode_Bookmarks_Close_Double page mode_Edit_Edit archive..._File_First page_Fullscreen_Go_Help_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Open..._Previous page_Quit_Rotate 90 degrees CW_Toolbar_ViewProject-Id-Version: 
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2009-05-04 23:52+0300
Last-Translator: Oleksandr Zaiats 
Language-Team: 
Language: 
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
X-Poedit-Language: Ukrainian
X-Poedit-Country: UKRAINE
%d коментар(я, ів)%d сторінок(Копія)Колекція з таким ім’ям вже існує.Файл з ім’ям '%s' вже існує. Хочете його замінити?ДоступДодати нову порожню колекцію.Додати книжкиДодати книжки до '%s'.Додати ще книжок до бібліотеки.Додати нову колекцію?Додаю '%s'…Додавання книжокУсі книжкиУсі файлиУсі зображенняЗавжди використовувати обраний колір, як колір тла.ВиглядАрхівАрхіви зберігаються як ZIP файли.Автоматично підлаштувати контраст (світлі та темні ділянки), окремо для кожної колірної смуги.Автоматично відкривати наступний архів у каталозі після перегортування останньої сторінки, або попередній архів після перегортування першої сторінки.Автоматично обирати колір тла, який підходить до зображення, що відображається.Автоматично обертати зображення, якщо орієнтація вказана метаданих, наприклад у Exif тазі.ТлоПоведінкаНайкращій масштабПереклад бразильськоюTar архів стиснений bzip2Переклад каталонськоюКоментаріНе можу додати нову колекцію з ім’ям '%s'.Не можу змінити ім’я на '%s'.Не можу скопіювати колекцію.Не можу відкрити %s: Нема такого файла.Не можу відкрити %s: Доступ заборонено.Не можу прочитати %sПереклад хорватськоюПереклад чеськоюПоказПоказувати лише ті книжки, в повному шляху яких є вказаний текст. Пошук не враховує  регістр.Двосторінковий режимПереклад голандськоюРедагувати архівПокращення зображенняФайлиПерша сторінкаПришалтувати за _висотоюПришалтувати за _шириноюПришалтувати за висотоюПрилаштувати за шириноюВіддзеркалити _горизонтальноВіддзеркалити _вертикальноГортати дві сторінки замість однієї в двосторінковому режимі.Переклад французькоюПовноекранний режимПереклад німецькою та ґенератор мініатюр для NautilusПереклад грецькоюTar архів стиснений gzipСховати _всеПереклад угорськоюДизайн іконокЗображенняЗображенняПереклад індонезійськоюВін читає ZIP, RAR та tar архіви так само, як і звичайні файли зображень.Переклад італійськоюПереклад японськоюПереклад корейськоюОстання сторінкаБібліотекаРозташування_Ручне масштабуванняЗбільшуюче СклоЗбільшуюче _склоЗбільшуюче склоРежим мангиРучне масштабуванняЗміненийПеремістити книжки з '%(source collection)s' до '%(destination collection)s'.Ім’яНаступна сторінкаНемає зображень у '%s'ВідкритиВідкрити обрану книгу.ВласникСторінкаПраваПереклад перськоюБудь ласка введіть ім’я для нової колекції.Будь ласка введіть нове ім’я для обраної колекції.Переклад польською_ПараметриПараметриПопередня сторінкаВластивостіПокласти колекцію '%(subcollection)s' в колекцію '%(supercollection)s'.RAR архівВидалити книжки з колекції?Видалити з архівуПерейменувати колекцію?Заміна перепише його вміст.КоріньОбернути на 90° п_роти годинникової стрілкиОбернути на 180°ОбертанняПереклад російськоюСЛАЙДШОУ_ПрокручуванняЗберезтиПрокручуванняВстановлює коефіцієнт збільшення для збільшуючого скла.Встановити розмір сторони квадратного збільшуючого скла.Переклад спрощеною китайськоюРозмірСлайдшоуПереклад іспанською_СтатусЗберігати мініатюри для відкритих файлів, відповідно до специфікації freedesktop.org. Ці мініатюри використовуються багатьма іншими додатками, наприклад більшістю файлових менеджерів.П_анеліTar архів_МініатюриНе можу зберегти новий архів!Оригінальні файли не були видалені.МініатюриПереклад традиційною китайськоюПрозорістьВважати усі файли всередені архівів, які мають один з цих суфіксів, коментарями.Переклад українськоюНевідомий тип файлуВикористовувати сірий клітчастий фон для прозорих зображень. Якщо цей параметр не встановленно, для тла буде використовуватися білий колір.ZIP архів_Про програму_Найкращій масштаб_Закладки_Закрити_Двосторінковий режим_Правка_Редагувати архів…_ФайлП_ерша сторінкаПовно_екранний режимПере_йти_ДовідкаЗ_берегти перетворення_Остання сторінка_Бібліотека…Режим _манги_Меню_Наступна сторінка_Відкрити…_Попередня сторінкаВи_хідОбернути на 90° _за годинниковою стрілкою_Панелі_Вигляд././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/uk/__init__.py0000644000175000017500000000000014476523373020340 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768
mcomix-3.1.0/mcomix/messages/zh_CN/0000755000175000017500000000000014553265237016621 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768
mcomix-3.1.0/mcomix/messages/zh_CN/LC_MESSAGES/0000755000175000017500000000000014553265237020406 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/zh_CN/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022507 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/zh_CN/LC_MESSAGES/mcomix.mo0000644000175000017500000012374014476523373022250 0ustar00moritzmoritzL!`,a,)h,,,+,(,:-O-Ko-'-#-.#".F.Z.%w.5...9/0%\> >
>	>>>r>H?Z?6k??????
?@!@:@O@a@v@	{@
@	@2@@@&XA
AAAAA
A
AAABB-BHLHcHlH}HHHHH!H
HI0I.EI	tI~IIHII
I
IIIJ
J J-J		rHrOr`rtr	rrJrss5s;Bs~s:ssssttt+t>t0Et9vttt	tt0tu1u8uMMu$uuu0vvv0iw'w[w+xJxcx	|xxxx	xxxQx!)yKy`dyy$y	z<#z`zgz
zzzzzzzz-z{1{!M{*o{){ {*{|#/| S|t|/| ||'|
}#}0}@}M}Z}a}'u}-}!}}~
~~*~]1~~~0~~~~
1>Tgz	*`&X	΀*7Vi!Bd9	ʂ-Ԃ#0F.S ك'4;HHUH4FYoo߅$11>p
}Ȇ	Ն߆(;BRi	y
	ć*ׇ$
'	5?PFψֈ	0%N	t7~D #09=$wˊ"96V$ȋ0 +	L
Vah{'`ٌd:
ʍ	׍		%/
EPKWD?
/
=
HVf0m-̏gK~-ʐ' 3]O$,ґ$:>WhuĒԒ

	0
CQa}Ɠ͓ԓۓ$7McyB!D-{r?<.Kk3Ӗ	*:J$]ȗ0G^uϘ4S|f
*!$L$q'***?'U*}<J

ĜҜI!;]|K*0iC	ɞӞڞ	NMTg<}l*'-R *!;HEU?9ۡ%Ţ<Ң)9
J
X
c
ny


ͤ

 
.
9
DO
`
kv

ȥ
٥




*5FWh
y




ʦ
ئ
(8Qe
v



ħ
էw(8d}]>
PlRMxg_jmFaI9BW{7R|X\JT	@p*X,rE6b$Cok7D6x|[4
0LrhvQ&
DyJ+P{+	wWO^fUG^boep&GN-%<#%'*U;aeAK.i,h-5i2?# HM
$c:StC1Y:0F z[8qf_/BHTIj/us@~)!K\zc4ky"Nd]u52Oq
SZnsVQ>;3.L`E	m9(`<Yn
}g)3~"1AtZ!'?=vV=l of %s! Callback %(function)r failed: %(error)s! Corrupt preferences file "%s", deleting...! Could neither find pysqlite2 nor sqlite3.! Could not add book "%s" to the library! Could not add book %(book)s to collection %(collection)s! Could not add collection "%s"! Could not add file %(sourcefile)s to archive %(archivefile)s, aborting...! Could not create archive at path "%s"! Could not get cover for book "%s"! Could not load icon "%s"! Could not parse bookmarks file %s! Could not read %s! Could not remove file "%s"! Could not rename collection to "%s"! Could not save thumbnail "%(thumbpath)s": %(error)s! Extraction error: %s! Non-existant book #%i! Worker thread processing %(function)r failed: %(error)s! You need an sqlite wrapper to use the library."%s" does not appear to have a valid executable."%s" does not have a valid working directory.%(filename)s's extracted size is %(actual_size)d bytes, but should be %(expected_size)d bytes. The archive might be corrupt or in an unsupported format.%d comments%d pages%s archives%s images%s is an image viewer specifically designed to handle comic books.%s is licensed under the terms of the GNU General Public License.'%s' is disabled for archives.(Copy)7z archiveA collection by that name already exists.A copy of this license can be obtained from %sA file named '%s' already exists. Do you want to replace it?AccessedAdd _BookmarkAdd _separatorAdd a new empty collection.Add booksAdd books to '%s'.Add information about all files opened from within MComix to the shared recent files list.Add more books to the library.Add new collection?AddedAdded %(count)d new books from directory '%(directory)s'.Added books:Added new book '%(bookname)s' from directory '%(directory)s'.Adding '%s'...Adding booksAdvancedAll archivesAll booksAll filesAll imagesAlwaysAlways use this selected colour as the background colour.Always use this selected colour as the thumbnail background colour.Animation mode:AppearanceArchiveArchive commentsArchive-related variables can only be used for archives.Archives are stored as ZIP files.AscendingAuto-detect (Default)Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically hide all toolbars in fullscreenAutomatically open next directoryAutomatically open the first file in the next sibling directory when flipping past the last page of the last file in a directory, or the previous directory when flipping past the first page of the first file.Automatically open the last viewed file on startupAutomatically open the next archiveAutomatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically pick a background colour that fits the viewed image.Automatically rotate images according to their metadataAutomatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.Automatically scan for new books when library is _openedAutorotate by heightAutorotate by widthBack ten pagesBackgroundBehaviourBest fit modeBilinearBook nameBrazilian Portuguese translationBy enabling this setting, the first page of a book will be used as application icon instead of the standard icon.Bzip2 compressed tar archiveCatalan translationChanges how images are scaled. Slower algorithms result in higher quality resizing, but longer page loading times.Changes the book cover size.Changes the sort order of the library.Clear _dialog choicesClears all dialog choices that you have previously chosen not to be asked again.CloseCloses all opened files.Co_mments...CollectionCommandCommand labelCommand line is empty.Comment extensions:Comment filesCommentsCompletely removes the selected books from the library.Continue reading from page %d?Controls how animated images should be displayed.Copies the current page to clipboard.Copies the selected book's path to clipboard.Could not add a new collection called '%s'.Could not change the name to '%s'.Could not determine library database version!Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCould not run command %(cmdlabel)s: %(exception)sCouldn't load keybindings: %sCover si_zeCreates a duplicate of the selected collection.Croatian translationCustom...Czech translationDate addedDebug optionsDeleteDelete "%s"?Delete information about recently opened files?Deletes the current file or archive from disk.Deletes the selected books from disk.Deletes the selected collection.DescendingDirectoryDisabled in archivesDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Do not ask again.Double page modeDuring a slideshow automatically open the next archiveDutch translationEdit BookmarksEdit archiveEdit external commandsEn_hance image...Enhance imageEscape key closes programExecute external commandExit from fullscreenExternal commandsExtraction and cacheFileFile nameFile orderFile sizeFile-related variables can only be used for files.FilesFiles will be opened and displayed according to the sort order specified here. This option does not affect ordering within archives.Finished reading on %(date)s, %(time)sFirst pageFit _height modeFit _size modeFit _width modeFit height modeFit size modeFit to heightFit to same sizeFit to size modeFit to widthFit to width or height:Fit width modeFixed size for this mode:Fli_p horizontallyFlip _verticallyFlip horizontallyFlip pages when scrolling "off the page" with the scroll wheel or with the arrow keys. It takes n consecutive "steps" with the scroll wheel or the arrow keys for the pages to be flipped.Flip pages when scrolling off the edges of the pageFlip two pages in double page modeFlip two pages, instead of one, each time we flip pages in double page mode.Flip verticallyForward ten pagesFraction of page to scroll per space key press (in percent):French translationFull pathFullscreenFullscreen modeGalician translationGerman translationGerman translation and Nautilus thumbnailerGo to pageGo to page...Greek translationGzip compressed tar archiveH_ide allHebrew translationHugeHungarian translationHyperbolic (slow)Icon designImageImage qualityImagesIncomplete escape sequence. For a literal '%', use '%%'.Incomplete quote sequence. For a literal '"', use '%"'.Indonesian translationInstalled Pillow version is: %sInvalid escape sequence: %%%sInvalid path: '%s'Invert smart scrollInvert smart scrolling direction.It reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKeep transformationKeeps the currently selected transformation for the next pages.Key %dKeybinding for "%(action)s" overrides hotkey for another action.Korean translationLHA archiveLabelLanguage (needs restart):LargeLast modifiedLast pageLibraryLibrary booksLibrary collectionsLibrary watch listLiteral orderLithuanian translationLocationMComix developerM_anual zoom modeMagnification factor:Magnifying LensMagnifying _lensMagnifying lensMagnifying lens size (in pixels):Manga modeManual zoom modeMaximum number of concurrent extraction threads:Maximum number of pages to store in the cache:Mi_nimizeMinimizeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNatural orderNavigationNeverNever autorotateNewNext _archiveNext archiveNext directoryNext pageNext page (dynamic)No images in '%s'No new books found in directory '%s'.No sortingNo version of GObject was found on your system.No version of the Python Imaging Library was found on your system.Non-supported archive format: %sNormalNormal (fast)Normal sizeNumber of "steps" to take before flipping the page:Number of pixels to scroll per arrow key press:Number of pixels to scroll per mouse wheel turn:Only for title pagesOnly for wide imagesOpenOpen _withOpen _without closing libraryOpen first file when navigating to previous archiveOpen first file when navigating to previous directoryOpen the selected book.Open the watchlist management dialog.Opens the archive editor.Opens the selected books for viewing.Opens the selected books, but keeps the library window open.Original vision/developer of ComixOwnerPDF documentPagePage auto-resizing:PermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Please note that the only files that are automatically added to this list are those files in archives that MComix recognizes as comments.Please refer to the external command documentation for a list of usable variables and other hints.Polish translationPr_eferencesPrefer same scalePrefer same sizePreferencesPreview:Previous a_rchivePrevious archivePrevious directoryPrevious pagePrevious page (dynamic)Proper_tiesPropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.Python Imaging Library Fork (Pillow) 6.0.0 or higher is required.QuitQuits and restores the currently opened file next time the program starts.RAR archiveRe_freshRe_nameRecentRefreshReloads the currently opened files or archive.Remove books from the library?Remove from archiveRemove from the _libraryRemove from this _collectionRemoved %(num)d book from '%(collection)s'.Removed %(num)d books from '%(collection)s'.Removed %d book from the library.Removed %d books from the library.Removes no longer existant books from the collection.Removes the selected books from the current collection.Rename collection?Renames the selected collection.Replace existing bookmark on page %s?Replace existing bookmarks on pages %s?Replacing it will overwrite its contents.Required Pillow version is: 6.0.0 or higherReset to defaults.Resets all keyboard shortcuts to their default values.RootRotat_e 90 degrees CCWRotate 180 de_greesRotate 180 degreesRotate 90 degrees CCWRotate 90 degrees CWRotationRun _commandRussian translationSLIDESHOWS_aturation:S_crollbarsS_harpness:SaveSave AsSave As opens at the last directory saved intoSave _AsSave and quitSave changes to commands?Save page asSave the selected values as default for future files.Scaling modeScanning for new books...ScrollScroll downScroll leftScroll rightScroll to bottom centerScroll to bottom leftScroll to bottom rightScroll to centerScroll to middle leftScroll to middle rightScroll to top centerScroll to top leftScroll to top rightScroll upSelecting "No" will create a new bookmark without affecting the other bookmarks.Set library cover sizeSet the magnification factor of the magnifying lens.Set the max number of pages to cache. A value of -1 will cache the entire archive.Set the number of "steps" needed to flip to the next or previous page.  Less steps will allow for very fast page turning but you might find yourself accidentally turning pages.Set the number of pixels to scroll on a page when using a mouse wheel.Set the number of pixels to scroll on a page when using the arrow keys.Set the size of the magnifying lens. It is a square with a side of this many pixels.Sets the desired output log level.Sets the percentage by which the page will be scrolled down or up when the space key is pressed.ShortcutsShow OSD panelShow file numbersShow filenameShow filesizeShow only one page where appropriate:Show page numbersShow page numbers on thumbnailsShow pathShow resolutionShow the library on startup.Show the version number and exit.Show this help and exit.Show/hide allShow/hide menubarShow/hide scrollbarsShow/hide statusbarShow/hide toolbarSimplified Chinese translationSizeSlideshowSlideshow delay (in seconds):Slideshow step (in pixels):SmallSmart scroll downSmart scroll upSort archives by:Sort files and directories by:Spanish translationSpecify the number of pixels to scroll while in slideshow mode. A positive value will scroll forward, a negative value will scroll backwards, and a value of 0 will cause the slideshow to always flip to a new page.St_atusbarStart _slideshowStart slideshowStart the application in double page mode.Start the application in fullscreen mode.Start the application in manga mode.Start the application in slideshow mode.Start the application with zoom set to best fit mode.Start the application with zoom set to fit height.Start the application with zoom set to fit width.Stop slideshowStore information about recently opened files:Store thumbnails for opened filesStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.Stretch images to fit to the screen, depending on zoom mode.Stretch small imagesSwedish translationT_oolbarsTar archiveTh_umbnailsThe archive is password-protected:The current book already contains marked pages. Do you want to replace them with a new bookmark on page %d?The file will be deleted from your harddisk.The new archive could not be saved!The original files have not been removed.The selected books will be removed from the library and permanently deleted. Are you sure that you want to continue?This error might be caused by missing GTK+ libraries.This is a separator pseudo-command.This will remove all entries from the "Recent" menu, and clear information about last read pages.Thumbnail size (in pixels):ThumbnailsTinyTraditional Chinese translationTransformationTransparencyTreat all files found within archives, that have one of these file endings, as comments.TypeUkrainian translationUnknown filetypeUpgrading library database version from %(from)d to %(to)d.Use a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.Use archive thumbnail as application iconUse checkered background for transparent imagesUse dynamic background colourUse fullscreen by defaultUse smart scrollingUse this colour as background:Use this colour as the thumbnail background:User interfaceView images and comic book archives.View modeView modesWhen active, the ESC key closes the program, instead of only disabling fullscreen mode.When showing the first page of an archive, or an image's width exceeds its height, only a single page will be displayed.While in slideshow mode allow the next archive to automatically be opened.With subdirectoriesWith this preference set, the space key and mouse wheel do not only scroll down or up, but also sideways and so try to follow the natural reading order of the comic book.Working directoryYou do not have the required versions of GTK+ 3.0 and PyGObject installed.You don't have the required version of the Python Imaging Library Fork (Pillow) installed.You have made changes to the list of external commands that have not been saved yet. Press "Yes" to save all changes, or "No" to discard them.You stopped reading here on %(date)s, %(time)s. If you choose "Yes", reading will resume on page %(page)d. Otherwise, the first page will be loaded.ZIP archiveZoomZoom _InZoom _OutZoom inZoom modesZoom out[OPTION...] [PATH]_About_Add_Add..._Auto-rotate image_Automatically adjust contrast_Best fit mode_Bookmarks_Brightness:_Cancel_Clean up_Close_Contrast:_Copy_Delete_Double page mode_Duplicate_Edit_Edit Bookmarks..._Edit archive..._Edit commands_File_First page_Fullscreen_Go_Go to page..._Help_Import_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Normal Size_Open_Open list_Open..._Previous page_Quit_Recent_Remove_Remove and delete from disk_Reset keys_Rotate 90 degrees CW_Save and quit_Scan now_Search:_Sort_Toolbar_Tools_Transform image_View_Watch list_ZoomProject-Id-Version: Comix 4.0.1
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2009-01-28 13:36+0800
Last-Translator: Xie Yanbo 
Language-Team: Xie Yanbo 
Language: 
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Generated-By: pygettext.py 1.5
X-Poedit-Language: Chinese
X-Poedit-Basepath: /Users/xyb/Desktop/comix-4.0.1-rc1/
X-Poedit-Country: CHINA
X-Poedit-SourceCharset: UTF-8
 / %s! 调用 %(function)r 失败:%(error)s! 设置文件 %s 损坏,删除中...! 无法找到 pysqlite2 或 sqlite3。! 无法将“%s”添加到收藏库! 无法将“%(book)s”添加到“%(collection)s”收藏库! 无法添加收藏库“%s”! 无法添加文件 %(sourcefile)s 到 文件包 %(archivefile)s,取消中...! 无法在 “%s” 路径下创建文件包! 无法获取“%s”的封页! 无法加载图标 %s! 无法解析书签文件 %s! 无法读取 %s! 无法删除文件 %s! 无法将收藏库重命名为“%s”! 无法保存缩略图 “%(thumbpath)s”:%(error)s! 解压缩错误:%s! #%i 不存在!工作线程调用 %(function)r 失败:%(error)s! 你必须有 sqlite wrapper 才能使用该收藏库。"%s" 不是合法的可知性文件。"%s" 没有合法的工作路径。%(filename)s 的大小为 %(actual_size)dB,但应该为 %(expected_size)dB。该文件包有可能已经损毁或是不支持的文件格式。%d 个注释共%d页%s 文件包%s 图像文件%s 是专门设计适用于漫画阅读的看图程序%s 采用 GNU/GPL 授权协议“%s”在文件包中被禁用。(复制)7z 文件包已经存在使用该名称的收藏。该协议的副本可从 %s 获得名为“%s”的文件已经存在,您想要覆盖它吗?访问添加书签(_B)添加分割符(_S)添加一个空的收藏。添加书添加书到“%s”中。把所有 MComix 打开过的文件信息添加到最近文件列表中。添加更多书到收藏库。添加新的收藏?添加时间已从 %(directory)s 目录下添加 %(count)d 本新书。已添加的书籍:已从 %(directory)s 目录下添加新书 %(bookname)s。正在添加“%s”...正在添加书籍高级所有文件包所有书籍所有文件所有图像文件总是总是使用选中的该颜色作为背景色。总是使用选中的该颜色作为缩略图背景色。动图模式:外观文件包文件包注释文件包相关变量只能应用于文件包。文件包以ZIP格式保存。递增自动侦测(默认)分别为每一个颜色通道自动调整对比度(包括亮度和暗度)。全屏时自动隐藏所有工具条自动打开下一个目录当翻过所在目录下最后一个文件的最后一页时自动打开下一目录的第一个文件当翻过所在目录下第一个文件的第一页时自动打开上移目录。启动时,自动打开最后查看过的文件自动打开下一个文件包当翻过最后一页时,自动打开当前目录的下一个文件包;或翻过第一页时,自动打开上一个文件包。自动选中适合观看中图片的背景色。按照元数据描述自动旋转图像如果图像文件中包含Exif等元数据,依照元数据的描述自动旋转图像。当打开收藏库时自动扫描新书(_O)根据高度自动旋转根据宽度自动旋转上十页背景行为最佳适应模式双线性书名巴西语、葡萄牙语译者启用该设置后,书籍的第一页将取代默认图标作为应用图标。以 Bzip2 压缩的 tar 文件包加泰罗尼亚语译者图片缩放设置:较慢的算法带来更高质量的图片缩放但更长的加载时间。更改该书的封面大小。更改该收藏库的排列顺序。清除对话框选项(_D)清除所有之前设置的对话框选项,不再询问。关闭关闭所有打开文件。注释(_M)...收藏命令命令名称命令是空的。注释后缀:注释文件注释从该收藏库中完全删除选中的书。继续从 %d 页读取文件?控制动图如何显示。复制当前页面到剪切板。复制选中的书的路径到剪切板。无法添加名为“%s”的新收藏。无法修改名称为“%s”。无法确定收藏库的数据库版本!无法创建收藏的副本。无法打开%s:没有此文件。无法打开%s:权限不足。无法读取%s无法执行 %(cmdlabel)s 命令: %(exception)s无法加载快捷键设置:%s封面大小(_Z)为选中的收藏创建一个副本。克罗地亚语译者自定义...捷克语译者加入时间调试选项删除确定删除 %s ?删除最近打开的文件的信息?从硬盘上删除当前文件或文件包。从硬盘上删除选中的书。删除选中的收藏。递减目录在文件包中禁用显示只显示在完整路径名中包含指定字符串的书。搜索不区分字母大小写。不再询问。双页模式幻灯片模式下自动打开下一个文件包荷兰语译者编辑书签编辑文件包编辑外部命令增强图片(_H)...增强图像退出键关闭程序执行外部命令退出全屏模式外部命令解压缩和缓存文件文件名文件顺序文件大小文件相关变量只能应用于文件。文件文件将按指定的排序打开和显示,但不影响文件包内部原来的文件顺序。已于 %(date)s, %(time)s 完成阅读第一页适应高度模式(_H)适应尺寸模式(_S)适应宽度模式(_W)适应高度模式适应尺寸模式适应高度适应至同样大小适应大小模式适应宽度适应宽度或高度模式:适应宽度模式固定当前模式的大小:水平翻转(_P)垂直翻转(_V)水平翻转当使用鼠标滚轮或者键盘方向键滚动出页面时翻页。在翻页之前,需要连续滚动滚轮或者按方向键数次。当滚动出页面边界时翻页双页模式时一次翻两页在双页模式时,每次翻两页,而不是一页。垂直翻转下十页空格键按动时页面滚动的百分比:法语译者完整路径全屏全屏模式加利西亚语译者德语译者德语译者与 Nautilus 缩略图功能作者跳转至指定页跳至第几页...希腊语译者以 Gzip 压缩的 tar 文件包隐藏所有(_I)希伯来语译者极大匈牙利语译者双曲线 (慢)图标设计图像图像质量图像文件不完整的转义字符。如果要输入“%”,请使用“%%”。不完整的引用字符。如果要输入“"”,其使用“%"”。印尼语译者安装的 Pillow 版本为:%s错误的转义字符:%%%s无效路径:%s反向智能滚动反方向智能滚动它可以像读取普通图片文件一样读取ZIP、RAR以及tar等文件包,以及普通的图片文件。意大利语译者日语译者保持变换保持当前选中的变换设置。快捷键 %d“%(action)s”的快捷键将覆盖原有功能韩语译者LHA 文件包名称语言 (需重启程序):较大最后修改时间最后一页收藏库收藏库书籍收藏库的收藏收藏库观看列表字符串顺序立陶宛语译者位置Mcomix开发者手动缩放模式(_A)放大倍数:放大镜放大镜(_G)放大镜放大镜大小 (像素):漫画模式手动缩放模式并发解压缩使用的最大线程数:缓存中存储的最大页面数:最小化(_N)最小化修改把书从“%(source collection)s”移动到“%(destination collection)s”。名称自然数顺序导航从不永不自动旋转新增下一个文件包(_A)下一个文件包下一个目录下一页下一页(动态)“%s”中没有图像文件在 %s 目录下没有找到新书。不排序在您的系统上找不到任何版本的 PyGObject。在您的系统上找不到任何版本的 Python Imaging Library。不支持的文件包格式:%s正常正常 (快)正常大小滚动超出页面时,需要再滚动几次才翻页:方向键按动时滚动的像素:鼠标滚轮滚动的像素:仅限第一页仅限较宽的图片打开打开方式(_O)打开书但不关闭收藏库(_W)打开上一个文件包的时候,打开第一个文件打开上一个目录的时候,打开第一个文件打开选中的书。打开观看清单管理对话框。打开文件包编辑器。打开选中的书。打开选中的书但不关闭收藏库窗口。Comix的原始版本及开发者拥有者PDF 文档页面页面自动缩放权限波斯语译者请输入新收藏的名称。请输入选中的收藏的新名称。请注意,只有被MComix认为是注释的文件,才会被自动添加到这个列表中。请到 external command documentation 查看可用的变量列表和其他说明。波兰语译者首选项(_E)比例优先尺寸优先首选项预览:上一个文件包(_R)上一个文件包上一个目录上一页上一页(动态)属性(_T)属性把收藏“%(subcollection)s”放入收藏“%(supercollection)s”中。需要 Python Imaging Library Fork (Pillow) 6.0.0 版或更新版。退出退出并在下次打开程序时恢复当前打开的文件。RAR 文件包刷新(_F)重命名(_N)最近打开的刷新重新加载当前打开的文件或文件包。是否要从该收藏库中删除这些书?从文件包中删除从收藏库中删除(_L)从该收藏中删除(_C)从“%(collection)s”中删除了%(num)d本书。从“%(collection)s”中删除了%(num)d本书。从该收藏库删除了 %d 本书。从该收藏库删除了 %d 本书。从该收藏中删除已经不存在的书。从当前收藏中删除选中的书。重命名收藏?重命名选中的收藏。是否覆盖在第 %s 页已存在的书签?是否覆盖在第 %s 页已存在的书签?覆盖它会覆写它的文件内容所需 Pillow 版本为:6.0.0 或更新版重置为默认值。重置所有快捷键为默认值。根逆时针旋转90度(_E)旋转180度(_G)旋转180度逆时针旋转90度顺时针旋转90度旋转执行命令(_C)俄语译者幻灯片播放饱和度(_A):滚动条(_C)锐度(_H):保存另存为“另存为”总是打开上次保存的目录另存为(_A)保存并退出是否保存命令变更?页面另存为保存为默认值。缩放模式正在扫描新书...滚动下移左移右移滚动至底部中央滚动至底部左侧滚动至底部右侧滚动至正中央滚动至中央左侧滚动至中央右侧滚动至顶部中央滚动至顶部左侧滚动至顶部右侧上移选中“否”将创建新书签,不影响当前其他书签。设置收藏库封面大小设置放大镜的放大倍数。设置要缓存的页面数最大值,-1将缓存整个文件包。设置需要再滚动几次才下翻一页或上翻一页,次数少可以快速翻页,不过容易引起意外翻页。设置使用鼠标滚轮滚动时在页面上的滚动像素。设置使用方向键滚动时在页面上的滚动像素。设置放大镜的大小。将显示为指定像素数边长的正方形。设置输出日志等级。设置使用空格键时页面滚动的百分比。快捷键显示嵌入消息面板显示文件数显示文件名显示文件大小在特殊情况下只显示一页:显示页数在缩略图上显示页码显示路径显示分辨率启动后显示收藏库。显示版本号后退出。显示该帮助后退出。隐藏/显示所有隐藏/显示菜单栏隐藏/显示滚动条隐藏/显示状态栏隐藏/显示工具栏简体中文译者大小幻灯片放映幻灯片延迟 (秒):幻灯片移动量 (像素):较小智能向下滚动智能向上滚动文件包排序依据:文件和目录排序依据:西班牙语译者设置幻灯片模式下滚动的像素值。正数值向前滚动,负数值向后滚动,0则总是翻到新的一页。状态栏(_A)开始放映幻灯片(_S)开始幻灯片放映启动程序后进入双页显示模式。启动程序后进入全屏模式。启动程序后进入漫画模式。启动程序后进入幻灯片模式。启动程序后进入最佳适应模式。启动程序后进入适合高度模式。启动程序后进入适合宽度模式。停止幻灯片放映保存最近打开的文件的信息:存储已经打开过的文件的缩略图按照 freedesktop.org 规范,保存已经打开过的文件的缩略图。这些缩略图会与其他程序(例如大多数文件管理器)共享使用。配合缩放模式,将图片伸展到符合屏幕大小。伸展小图片瑞典语译者工具条 (_O)Tar 文件包缩略图(_U)该文件包有密码保护:当前文件已存在书签页,是否覆盖为第 %d 页的新书签?该文件将从硬盘上删除。无法保存新的文件包!原始文件没有被删除。这些书将从该收藏库删除并彻底删除。您确定要继续吗?该错误可能由缺失 GTK+ 库引起。这是一条用来表示分割符的伪命令。这将删除所有“最近打开的文件”目录下记录,并清除相应的最后阅读页信息。缩略图大小 (像素):缩略图极小繁体中文译者变换透明度把文件包中任何包含以下一个文件结尾的文件,视作注释。输入乌克兰语译者未知的文件格式将收藏库数据库版本从 %(from)d 升级到 %(to)d 。使用灰色跳棋格图案作为透明图像的背景。如果不设置该选项,则使用纯白背景。使用文件包缩略图作为应用图标使用棋盘格图像做透明图片的背景使用动态背景色默认使用全屏模式使用智能滚动使用该颜色作为背景:使用该颜色作为缩略图背景色:用户界面查看图片和漫画文件包。查看模式查看模式开启后退出键关闭程序,而不仅仅是退出全屏模式。当是文件包的第一页或图片较宽时只显示一页。幻灯片模式下允许自动打开下一个文件包。包含子目录设置该选项后,空格键和鼠标滚轮不仅可以向上滚动或向下滚动,同时可以向两侧滚动,以配合一般的漫画书阅读顺序。工作路径您没有安装所需要的 GTK+ 3.0 和 PyGObject 版本。您没有安装所需的 Pillow 库版本你已经更改了外部命令列表,但没有保存。点击“是”来进行保存,或者点击“否”来丢弃修改。您在 %(date)s,%(time)s 阅读到第 %(page)d 页,如果选中“是”将从该页开始继续阅读,否则将从首页开始读取。ZIP 文件包缩放(_Z)放大(_I)缩小(_O)放大缩放模式缩小[选项...] [路径]关于(_A)添加(_A)添加(_A)...自动旋转图像(_A)自动调整对比度(_A)最佳适应模式(_B)书签(_B)亮度(_B):取消(_C)清除(_C)关闭(_C)对比度(_C):复制(_C)删除(_D)双页模式(_D)副本(_D)编辑(_E)编辑书签(_E)...编辑文件包(_E)…编辑命令(_E)文件(_F)第一页(_F)全屏(_F)跳转(_G)跳至第几页(_G)...帮助(_H)导入(_I)保持变换(_K)最后一页(_L)收藏库(_L)...漫画模式(_M)菜单栏(_M)下一页(_N)正常大小(_N)打开(_O)打开(_O)打开(_O)...上一页(_P)退出(_Q)最近打开的文件(_R)删除(_R)删除并从硬盘上删除(_R)重置快捷键顺时针旋转90度(_R)保存并退出(_S)立即扫描(_S)搜索(_S):排序(_S)工具栏(_T)工具(_T)图形变换(_T)查看(_V)观看清单(_W)缩放(_Z)././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/zh_CN/__init__.py0000644000175000017500000000000014476523373020722 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768
mcomix-3.1.0/mcomix/messages/zh_TW/0000755000175000017500000000000014553265237016653 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768
mcomix-3.1.0/mcomix/messages/zh_TW/LC_MESSAGES/0000755000175000017500000000000014553265237020440 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/zh_TW/LC_MESSAGES/__init__.py0000644000175000017500000000000014476523373022541 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/zh_TW/LC_MESSAGES/mcomix.mo0000644000175000017500000012071114476523373022275 0ustar00moritzmoritz`*a*)h*,*+*(*:+O+Ko+'+#+,#",F,Z,%w,5,,,0-03--d--+.7.B@.A..../
%/)0/.Z/<//
///	00Z%0000900=1>1M1Z1	c1	m1
w1191C1
2228+2!d2	22]2-3!23T32%4#X4|4S5Bb575k58I6U6667
7	7
%737	<7 F7qg777r
8}8&8P89919
>9I9
Q9_9v9
99799%9-:+L:"x:-:: :%
;0;1B;t;;/;;	;;
;

<<</,<.\<%< <
<	<<<r=w==6=====>
(>6>P>i>~>>	>
>	>2>>>r?&V@
}@@@@@
@
@@@AA)ACAVAgAyA34B"hBLBBB<B7C	JC
TC_CoCC+C
C
CCC	
DD'D,DBDTD`D
fDtD8{D7DDE!E4E!HEAjEEEE?E)F@0FqFFFFF
F	FF
FFF
GG"G3GEG[GkG|G!G
GG.G	GHHHH^H
cH
qH|HHH
HHH	HHH%H
IB"I eII
II3I/I0JQSQ\QiQ	}QQQQQQQ
QQQ5Q-R:RTR[RgRsRRRRRRRSS+S	?SPISS4SRS9TFTG1UTyU"U`U	RV\VkV
}V%VVV	VVV!WeJeVeZeieoewe
eeee
eeeeeeeef!f7f	FfPfYf_fhfofffff>h*Dh)oh)h#h:h"iX=i)iiiij.j%Hj;njjj;jk&8k_kkk9l1=lollllll+l1 mRm_mpmmmm_m!$nFn\n>innKn
o!o4o;oHoUobo-oo<oo	oo-o)pGpNp0dp-ppp*qqqD~r3r$rRs%osKssst t't.t	AtKtXt`qt!ttl
uzu$u<uuuv+v2v9vFv\v	uvv-v#v$v-v+wEw$bww'w!ww0w (xIx!Zx|x	xxxxxx0x0y!FyhyyyyyTyyz0zOz_zlz|zzzzzzzz{{{'({P{{W{{|	|||||
}}*}=}J}c}!v}}}}}-I~w~9~~~*~"	/9I_1l -=JQ^EeE6Lphف3	@=J
!̂ӂ*7>Of	s
}	!уHgtɄ܄		%	BFL ȅ<Յ!*4_oÆ!܆03d	$ʇheXΈވ	0	@J
]h@oE
-@0M~͊oQZ'-ԋ!W:'ЌՌ)@GXeu
	
э3+8NU\cjՎ-B4w!<HB̐9'I3q	ȑۑ'#BO_$~’֒/BI$Y$~Гt
$”'$-4*b**-''O<=M
]
ky'ΗK<9!vl$18K	XBb<ՙc0v!ɚ'0X$hYQ<S=xJÝ
Sa
h
s~


Ǟ՞

%
3
>
I
T
b
mx

ʟ
۟




!
,7N
_m
~



Ġ
Ҡݠ
:N
_
m
x


rma[(sy@6QNhUg"U^QPBtS]FX7\	JfDSCh//7qW).ApP.D*51Vz\v!uRc`j0M$[I3k+tFoHG~mV) Il_e5xKMJ$wjL2Re@TYifb{!AY#0BGgKT>=p|:6xn-*+=	b8Ez>2
,dc#~(k4O:"X;<uNrv
-<,9s
&il`%H
Zy%dq}^OE4Wa1;}'3]8Z{?|?nwC9o_&L ' of %s! Callback %(function)r failed: %(error)s! Corrupt preferences file "%s", deleting...! Could neither find pysqlite2 nor sqlite3.! Could not add book "%s" to the library! Could not add book %(book)s to collection %(collection)s! Could not add collection "%s"! Could not add file %(sourcefile)s to archive %(archivefile)s, aborting...! Could not create archive at path "%s"! Could not get cover for book "%s"! Could not load icon "%s"! Could not parse bookmarks file %s! Could not read %s! Could not remove file "%s"! Could not rename collection to "%s"! Could not save thumbnail "%(thumbpath)s": %(error)s! Extraction error: %s! Non-existant book #%i! You need an sqlite wrapper to use the library."%s" does not appear to have a valid executable."%s" does not have a valid working directory.%(filename)s's extracted size is %(actual_size)d bytes, but should be %(expected_size)d bytes. The archive might be corrupt or in an unsupported format.%d comments%d pages%s is an image viewer specifically designed to handle comic books.%s is licensed under the terms of the GNU General Public License.'%s' is disabled for archives.(Copy)...when height exceeds width...when width exceeds height7z archiveA collection by that name already exists.A copy of this license can be obtained from %sA file named '%s' already exists. Do you want to replace it?AccessedAdd _BookmarkAdd _separatorAdd a new empty collection.Add booksAdd books to '%s'.Add information about all files opened from within MComix to the shared recent files list.Add more books to the library.Add new collection?AddedAdded %(count)d new books from directory '%(directory)s'.Added books:Added new book '%(bookname)s' from directory '%(directory)s'.Adding '%s'...Adding booksAdvancedAll booksAll filesAll imagesAlwaysAlways use this selected colour as the background colour.Always use this selected colour as the thumbnail background colour.AppearanceArchiveArchive commentsArchive-related variables can only be used for archives.Archives are stored as ZIP files.AscendingAuto-detect (Default)Automatically adjust contrast (both lightness and darkness), separately for each colour band.Automatically hide all toolbars in fullscreenAutomatically open next directoryAutomatically open the first file in the next sibling directory when flipping past the last page of the last file in a directory, or the previous directory when flipping past the first page of the first file.Automatically open the last viewed file on startupAutomatically open the next archiveAutomatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.Automatically open, on startup, the file that was open when MComix was last closed.Automatically pick a background colour that fits the viewed image.Automatically rotate images according to their metadataAutomatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.Automatically scan for new books when library is _openedAutomatically use the colour that fits the viewed image for the thumbnail background.Autorotate by heightAutorotate by widthBack ten pagesBackgroundBehaviourBest fit modeBilinearBook nameBrazilian Portuguese translationBy enabling this setting, the first page of a book will be used as application icon instead of the standard icon.Bzip2 compressed tar archiveCatalan translationChanges how images are scaled. Slower algorithms result in higher quality resizing, but longer page loading times.Changes the book cover size.Changes the sort order of the library.Clears all dialog choices that you have previously chosen not to be asked again.CloseCloses all opened files.Co_mments...CollectionCommandCommand labelCommand line is empty.Comment extensions:Comment filesCommentsCompletely removes the selected books from the library.Continue reading from page %d?Copies the current page to clipboard.Copies the selected book's path to clipboard.Could not add a new collection called '%s'.Could not change the name to '%s'.Could not determine library database version!Could not duplicate collection.Could not open %s: No such file.Could not open %s: Permission denied.Could not read %sCould not run command %(cmdlabel)s: %(exception)sCouldn't load keybindings: %sCover si_zeCreates a duplicate of the selected collection.Croatian translationCustom...Czech translationDate addedDebug optionsDeleteDelete "%s"?Delete information about recently opened files?Deletes the current file or archive from disk.Deletes the selected books from disk.Deletes the selected collection.DescendingDirectoryDisabled in archivesDisplayDisplay only those books that have the specified text string in their full path. The search is not case sensitive.Do not ask again.Double page modeDuring a slideshow automatically open the next archiveDutch translationEdit BookmarksEdit archiveEdit external commandsEn_hance image...Enhance imageEscape key closes programExecute external commandExit from fullscreenExternal commandsFileFile nameFile orderFile sizeFile-related variables can only be used for files.FilesFiles will be opened and displayed according to the sort order specified here. This option does not affect ordering within archives.Files within archives will be sorted according to the order specified here. Natural order will sort numbered files based on their natural order, i.e. 1, 2, ..., 10, while literal order uses standard C sorting, i.e. 1, 2, 34, 5.Finished reading on %(date)s, %(time)sFirst pageFit _height modeFit _size modeFit _width modeFit height modeFit size modeFit to heightFit to size modeFit to widthFit to width or height:Fit width modeFixed size for this mode:Fli_p horizontallyFlip _verticallyFlip horizontallyFlip pages when scrolling "off the page" with the scroll wheel or with the arrow keys. It takes n consecutive "steps" with the scroll wheel or the arrow keys for the pages to be flipped.Flip pages when scrolling off the edges of the pageFlip two pages in double page modeFlip two pages, instead of one, each time we flip pages in double page mode.Flip verticallyForward ten pagesFraction of page to scroll per space key press (in percent):French translationFull pathFullscreenFullscreen modeGalician translationGerman translationGerman translation and Nautilus thumbnailerGo to pageGo to page...Greek translationGzip compressed tar archiveH_ide allHebrew translationHugeHungarian translationHyperbolic (slow)Icon designImageImage qualityImagesIncomplete escape sequence. For a literal '%', use '%%'.Incomplete quote sequence. For a literal '"', use '%"'.Indonesian translationInvalid escape sequence: %%%sInvalid path: '%s'Invert smart scrollInvert smart scrolling direction.It reads ZIP, RAR and tar archives, as well as plain image files.Italian translationJapanese translationKeep transformationKeeps the currently selected transformation for the next pages.Key %dKeybinding for "%(action)s" overrides hotkey for another action.Korean translationLHA archiveLabelLanguage (needs restart):LargeLast modifiedLast pageLibraryLibrary booksLibrary collectionsLibrary watch listLiteral orderLocationMComix developerM_anual zoom modeMagnification factor:Magnifying LensMagnifying _lensMagnifying lensMagnifying lens size (in pixels):Manga modeManual zoom modeMaximum number of pages to store in the cache:Mi_nimizeMinimizeModifiedMove books from '%(source collection)s' to '%(destination collection)s'.NameNatural orderNavigationNeverNever autorotateNewNext _archiveNext archiveNext directoryNext pageNext page (dynamic)No images in '%s'No new books found in directory '%s'.No sortingNo version of the Python Imaging Library was found on your system.Non-supported archive format: %sNormalNormal (fast)Normal sizeNumber of "steps" to take before flipping the page:Number of pixels to scroll per arrow key press:Number of pixels to scroll per mouse wheel turn:Only for title pagesOnly for wide imagesOpenOpen _withOpen _without closing libraryOpen the selected book.Open the watchlist management dialog.Opens the archive editor.Opens the selected books for viewing.Opens the selected books, but keeps the library window open.Original vision/developer of ComixOwnerPagePermissionsPersian translationPlease enter a name for the new collection.Please enter a new name for the selected collection.Please note that the only files that are automatically added to this list are those files in archives that MComix recognizes as comments.Please refer to the external command documentation for a list of usable variables and other hints.Polish translatinPolish translationPr_eferencesPreferencesPreview:Previous a_rchivePrevious archivePrevious directoryPrevious pagePrevious page (dynamic)Proper_tiesPropertiesPut the collection '%(subcollection)s' in the collection '%(supercollection)s'.QuitQuits and restores the currently opened file next time the program starts.RAR archiveRe_freshRe_nameRecentRefreshReloads the currently opened files or archive.Remove books from the library?Remove from archiveRemove from the _libraryRemove from this _collectionRemoved %(num)d book from '%(collection)s'.Removed %(num)d books from '%(collection)s'.Removed %d book from the library.Removed %d books from the library.Removes no longer existant books from the collection.Removes the selected books from the current collection.Rename collection?Renames the selected collection.Replace existing bookmark on page %s?Replace existing bookmarks on pages %s?Replacing it will overwrite its contents.Reset to defaults.RootRotat_e 90 degrees CCWRotate 180 de_greesRotate 180 degreesRotate 90 degrees CCWRotate 90 degrees CWRotationRun _commandRussian translationSLIDESHOWS_aturation:S_crollbarsS_harpness:SaveSave AsSave _AsSave and quitSave changes to commands?Save page asSave the selected values as default for future files.Scaling modeScanning for new books...ScrollScroll downScroll leftScroll rightScroll to bottom centerScroll to bottom leftScroll to bottom rightScroll to centerScroll to middle leftScroll to middle rightScroll to top centerScroll to top leftScroll to top rightScroll upSelecting "No" will create a new bookmark without affecting the other bookmarks.Set library cover sizeSet the magnification factor of the magnifying lens.Set the max number of pages to cache. A value of -1 will cache the entire archive.Set the number of "steps" needed to flip to the next or previous page.  Less steps will allow for very fast page turning but you might find yourself accidentally turning pages.Set the number of pixels to scroll on a page when using a mouse wheel.Set the number of pixels to scroll on a page when using the arrow keys.Set the size of the magnifying lens. It is a square with a side of this many pixels.Sets the desired output log level.Sets the percentage by which the page will be scrolled down or up when the space key is pressed.ShortcutsShow OSD panelShow file numbersShow filenameShow only one page where appropriate:Show page numbersShow page numbers on thumbnailsShow pathShow resolutionShow the library on startup.Show the version number and exit.Show this help and exit.Show/hide allShow/hide menubarShow/hide scrollbarsShow/hide statusbarShow/hide toolbarSimplified Chinese translationSizeSlideshowSlideshow delay (in seconds):Slideshow step (in pixels):SmallSmart scroll downSmart scroll upSort archives by:Sort files and directories by:Spanish translationSpecify the number of pixels to scroll while in slideshow mode. A positive value will scroll forward, a negative value will scroll backwards, and a value of 0 will cause the slideshow to always flip to a new page.St_atusbarStart _slideshowStart slideshowStart the application in double page mode.Start the application in fullscreen mode.Start the application in manga mode.Start the application in slideshow mode.Start the application with zoom set to best fit mode.Start the application with zoom set to fit height.Start the application with zoom set to fit width.Stop slideshowStore information about recently opened files:Store thumbnails for opened filesStore thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.Stretch images to fit to the screen, depending on zoom mode.Stretch small imagesSwedish translationT_oolbarsTar archiveTh_umbnailsThe file will be deleted from your harddisk.The new archive could not be saved!The original files have not been removed.The selected books will be removed from the library and permanently deleted. Are you sure that you want to continue?This error might be caused by missing GTK+ libraries.This is a separator pseudo-command.This will remove all entries from the "Recent" menu, and clear information about last read pages.Thumbnail size (in pixels):ThumbnailsTinyTraditional Chinese translationTransformationTransparencyTreat all files found within archives, that have one of these file endings, as comments.TypeUkrainian translationUnknown filetypeUpgrading library database version from %(from)d to %(to)d.Use a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.Use archive thumbnail as application iconUse checkered background for transparent imagesUse dynamic background colourUse fullscreen by defaultUse smart scrollingUse this colour as background:Use this colour as the thumbnail background:User interfaceView images and comic book archives.View modeView modesWhen active, the ESC key closes the program, instead of only disabling fullscreen mode.When showing the first page of an archive, or an image's width exceeds its height, only a single page will be displayed.While in slideshow mode allow the next archive to automatically be opened.With subdirectoriesWith this preference set, the space key and mouse wheel do not only scroll down or up, but also sideways and so try to follow the natural reading order of the comic book.Working directoryYou have made changes to the list of external commands that have not been saved yet. Press "Yes" to save all changes, or "No" to discard them.You stopped reading here on %(date)s, %(time)s. If you choose "Yes", reading will resume on page %(page)d. Otherwise, the first page will be loaded.ZIP archiveZoomZoom _InZoom _OutZoom inZoom modesZoom out[OPTION...] [PATH]_About_Add_Add..._Auto-rotate image_Automatically adjust contrast_Best fit mode_Bookmarks_Brightness:_Cancel_Clean up_Close_Contrast:_Copy_Delete_Double page mode_Duplicate_Edit_Edit Bookmarks..._Edit archive..._Edit commands_File_First page_Fullscreen_Go_Go to page..._Help_Import_Keep transformation_Last page_Library..._Manga mode_Menubar_Next page_Normal Size_Open_Open..._Previous page_Quit_Recent_Remove_Remove and delete from disk_Rotate 90 degrees CW_Save and quit_Scan now_Search:_Sort_Toolbar_Tools_Transform image_View_Watch list_ZoomProject-Id-Version: MComix zh_TW
Report-Msgid-Bugs-To: https://sourceforge.net/projects/mcomix
PO-Revision-Date: 2013-07-05 21:37+0800
Last-Translator: Wayne Su 
Language-Team: 
Language: 
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Plural-Forms: nplurals=2; plural=1;
X-Poedit-Language: Chinese
X-Poedit-Country: TAIWAN
X-Poedit-SourceCharset: utf-8
 / %s! 呼叫 %(function)r 失敗:%(error)s! 設定檔 %s 損壞,正在刪除...! 無法找到 pysqlite2 或 sqlite3。! 無法將 %s 加入這個書庫!無法將書籍 %(book)s 加入到書集 %(collection)s! 無法新增 %s 書集! 無法將檔案 %(sourcefile)s 加入壓縮檔 %(archivefile)s 中,正在取消...! 無法在 %s 路徑下建立壓縮檔! 無法取得 %s 的封面! 無法載入「%s」圖示! 無法解析書籤檔 %s! 無法讀取 %s! 無法移除檔案 %s! 無法將書集重新命名為 %s! 無法儲存預覽縮圖「%(thumbpath)s」:%(error)s! 解壓縮錯誤:%s! 不存在的書籍 #%i! 你必須有 sqlite wrapper 才能使用這個書庫。「%s」無法正確執行。「%s」沒有可用的工作目錄。%(filename)s 的大小是 %(actual_size)dB,但應該是 %(expected_size)dB 才對。這個壓縮檔可能有損壞或是不支援的格式。%d 個註解%d 頁%s 是特別設計適用於漫畫書籍的看圖程式。%s 依據 GNU 的 GPL 相關條款提供授權。「%s」不用於壓縮檔。(複製)...高度大於寬度時...寬度大於高度時7Z 壓縮檔已有相同名稱的書集。可以在 %s 取得授權協定的複製本檔案 %s 已經存在,你要替換掉它嗎?存取時間新增書籤(_B)插入分隔行(_S)新增新的空白書集。新增書籍將書籍加入 %s 。將 MComix 開啟過的檔案資訊加入共享的「最近開啟過的的檔案」清單中。將更多書籍加入書庫中。要新增書集嗎?新增時間已經從 %(directory)s 目錄新增 %(count)d 本新書了。已經加入的書籍:已經將新書「%(bookname)s」從 %(directory)s 目錄加到書庫了。正在加入 %s ...正在加入書籍進階所有書籍所有檔案所有圖片永遠都要永遠使用選取的顏色當背景顏色。永遠使用選取的顏色當預覽縮圖的背景顏色。外觀壓縮檔壓縮檔註解壓縮檔相關變數只能用於壓縮檔。已儲存為 ZIP 壓縮檔。遞增自動偵測 (預設)分別為每個色頻自動調整明暗對比。全螢幕時模式自動隱藏所有工具列自動開啟下一個目錄於所在目錄最後一頁時向後翻頁,則自動開啟下一個目錄的第一個檔案;於第一個檔案的第一頁時向前翻頁,則自動開啟上一個目錄。啟動時自動開啟最後檢視的檔案自動開啟下一個壓縮檔當在目前壓縮檔翻頁離開最後一頁時,自動開啟目錄中的下一個壓縮檔;當翻頁跳出第一頁時,則自動開啟上一個壓縮檔。當 MComix 啟動時,自動開啟上次最後開啟過的檔案。自動挑選適合觀看中圖片的背景顏色。根據內建資料自動旋轉圖片當圖片內建的資料 (如 EXTIF) 中有指定方向時,自動旋轉圖片。書庫開啟時自動搜尋新書(_O)自動挑選適合的顏色當觀看中圖片的預覽縮圖背景顏色。依據高度自動旋轉依據寬度自動旋轉往後十頁背景行為最佳大小模式雙線性書籍名稱巴西葡萄牙文翻譯啟用這個設定後,書籍的第一頁會被當作應用程式圖示,取代標準圖示。以 BZIP2 壓縮的 TAR 壓縮檔加泰羅尼亞文翻譯變更圖片縮放設定:較慢的演算法可以有較高品質,但頁面載入時間也會比較久。變更書籍封面大小。變更這個書庫的排列順序。清除你之前已選過的確認選項,不要再重問。關閉關閉所有開啟的檔案。註解(M)...書集指令指令標題指令列是空的。註解檔的副檔名:註解檔註解從這個書庫中完全移除選取的書。要從第%d頁開始繼續讀嗎?將目前這頁複製到剪貼簿。將選取的書的路徑複製到剪貼簿。無法新增 %s 書集。無法重新命名為 %s 。無法偵測書庫的資料版本!無法備份書集。無法開啟 %s:沒有這個檔案。無法開啟 %s:沒有權限。無法讀取 %s無法執行 %(cmdlabel)s 指令:%(exception)s無法載入快速鍵設定:%s封面大小(_Z)建立選取的書集的備份。克羅埃西亞文翻譯自訂...捷克文翻譯加入的時間除錯選項刪除要刪除 %s ?要刪除「最近開啟過的檔案」資訊?從磁碟刪除目前這個檔案或壓縮檔。從硬碟刪除選取的書籍。刪除選取的書集。遞減目錄不用於壓縮檔顯示只顯示完整路徑中有指定關鍵字的書籍,搜尋時不區分大小寫。不要再問了。雙頁模式投影片放映時自動開啟下一個壓縮檔荷蘭文翻譯編輯書籤編輯壓縮檔編輯外部指令圖片強化(_H)...強化圖片ESC 鍵關閉程式執行外部指令離開全螢幕模式外部指令檔案檔案名稱檔案順序檔案大小檔案相關變數只能用於檔案。檔案檔案開啟與顯示的順序都會依據這裏的設定決定,但不會影響到壓縮檔裏面原本的檔案順序。壓縮檔理的檔案會安照這裏指定的順序排序。自然順序會依據數字大小:1、2、....、10、34;文字順序會依據標準 C 語言次序:1、2、34、5。在 %(date)s %(time)s 讀完第一頁符合高度模式(_H)符合大小模式(_S)符合寬度模式(_W)符合高度模式符合大小模式符合高度符合大小模式符合寬度符合寬度或高度:符合寬度模式這個模式下的固定大小:水平翻轉(_P)垂直翻轉(_V)水平翻轉以滾輪或方向鍵捲動到「超出本頁」時翻頁。要翻頁時,需要連續滾動滑鼠滾輪或按方向鍵數次。當捲動到頁面的頂端或底部時翻頁雙頁模式時一次翻兩頁雙頁模式時一次翻兩頁,而不是只翻一頁。垂直翻轉往前十頁按空白鍵時的捲動量 (百分比):法文翻譯完整路徑全螢幕全螢幕模式加里西亞文翻譯德文翻譯德文翻譯與 Nautilus 預覽縮圖功能作者指定頁數...指定頁數...希臘文翻譯以 GZIP 壓縮的 TAR 壓縮檔全部隱藏(_I)希伯來文翻譯極大匈牙利文翻譯雙曲線 (慢)圖示設計圖片圖片品質圖片不完整的跳脫字元。若要輸入「%」,請使用「%%」。不完整的轉義字元。若要輸入「\」,請使用「%\」。印尼文翻譯變數字元不正確:%%%s不正確的路徑:%s反向智慧型捲動反方向智慧型捲動。可以讀取一般圖片檔,又能讀取 ZIP、RAR、TAR 壓縮檔 (包含以 gzip、bzip2 壓縮處理的)。義大利文翻譯日文翻譯保持形變設定下張圖片也保持目前選取的形變設定。按鍵 %d以「%(action)s」功能的快速鍵替換掉原有功能。韓文翻譯LHA 壓縮檔標題語系 (要重新啟動程式):較大最後修改時間最後一頁書庫書庫的書書庫的書集書庫觀看清單文字順序位置MComix 開發者手動縮放模式(_A)放大倍率放大鏡放大鏡(_L)放大鏡放大鏡大小 (像素):漫畫模式手動縮放模式儲存在快取的最大頁數:縮到最小(_N)縮到最小修改時間將書籍從 %(source collection)s 移到 %(destination collection)s 。檔案名稱自然順序導覽永遠不要永遠不自動旋轉新增下一個壓縮檔(_A)下一個壓縮檔下一個目錄下一頁下一頁 (動態)%s 裏沒有圖片在 %s 目錄裏沒有找到新書。不排序在你的系統裏找不到任何版本的 Python Imaging 程式庫。不支援的壓縮檔格式:%s正常正常 (快)正常大小已捲動到超出頁面時,要再捲動幾次才翻頁:按方向鍵時捲動的像數:使用滑鼠滾輪時要捲動的像數:只限第一頁只限較寬的圖片開啟用其他工具開啟(_W)開啟但不關閉書庫(_W)開啟選取的書籍。開啟觀看清單管理視窗。開啟壓縮檔編輯程式。開啟選取的書。開啟選取的書,但不關閉書庫視窗。Comix 的原始設計開發者擁有者頁次權限波斯文翻譯請輸入新書集的名稱。請輸入這個書集的新名稱。請注意:只有壓縮檔內被 MComix 認為是註解的檔案,才會被自動加入這個清單。請到「external command documentation」查閱可用變數清單和其他說明。波蘭文翻譯波蘭文翻譯偏好設定(_E)偏好設定預覽:上一個壓縮檔(_R)上一個壓縮檔上一個目錄上一頁上一頁 (動態)屬性(_T)屬性將 %(subcollection)s 書集放到 %(supercollection)s 書集。離開離開並在下次啟動程式時自動載入目前開啟的檔案。RAR 壓縮檔重新整理(_F)重新命名(_N)最近開啟過的重新整理重新載入目前開啟的檔案或壓縮檔。從這個書庫移除書籍?從壓縮檔移除從這個書庫中移除(_L)從這個書集中移除(_C)已經移除 %(num)d 本 %(collection)s 書集的書。已經移除 %(num)d 本 %(collection)s 書集的書。已經從這個書庫移除 %d 本書。已經從這個書庫移除 %d 本書。從書集中移除已不存在的書。從目前這個書集中移除選取的書。重新命名書集?將選取的書集重新命名。要替換掉設在第 %s 頁的書籤嗎?要替換掉設在第 %s 頁的書籤嗎?替換後將會覆蓋掉原有內容。重設為預設值。Root逆時針旋轉 90 度(_E)旋轉 180 度(_G)旋轉 180 度逆時針旋轉 90 度順時針旋轉 90 度旋轉執行指令(_C)俄文翻譯投影片放映飽和度(_A):捲軸(_C)清晰度(_H):儲存另存為另存為(_A)儲存後離開儲存指令變更?頁面另存為儲存選取的數值當以後檔案的預設值。縮放模式正在掃描新書...捲動下移左移右移捲動到底部中央捲動到底部左側捲動到底部右側捲動到正中央捲動到中央左側捲動到中央右側捲動到頂部中央捲動到頂部左側捲動到頂部右側上移選擇「否」就可以建立不影響其他書籤的新書籤。設定書庫封面大小設定放大鏡的放大倍率。設定快取最大頁數,-1 表示整個壓縮檔都要。設定要滾動多少次才翻回上一頁或翻到下一頁,次數少則可以快速瀏覽;你可以多試幾次找出適合自己的設定。設定在檢視頁面時每滾動滑鼠滾輪就要捲動多少像數。設定在檢視頁面時每按方向鍵就要捲動多少像數。設定放大鏡的大小,請填入正方形的邊長。設定需要的紀錄檔輸出等級。設定按空白鍵後頁面捲動量的百分比。快速鍵顯示嵌入訊息面板顯示檔案號碼顯示檔案名稱在特殊的狀況時只顯示一頁:顯示頁碼在預覽縮圖上顯示頁碼顯示路徑顯示解析度啟動時顯示這個書庫。顯示這個版本號碼後退出。顯示這個說明後退出。全部顯示/隱藏顯示/隱藏選單列顯示/隱藏捲軸顯示/隱藏狀態列顯示/隱藏工具列簡體中文翻譯大小投影片放映投影片放映停留時間 (秒):投影片放映移動量 (像數):較小智慧型向下捲智慧型向上捲壓縮檔排序:檔案與目錄排序:西班牙文翻譯設定在投影片模式時捲動的像數,正值則向前捲動、負值則向後捲動、0 則翻到下一頁。狀態列(_A)開始投影片放映(_S)開始投影片放映啟動程式後進入雙頁模式。啟動程式後進入全螢幕模式。啟動程式後進入漫畫模式。啟動程式後進入投影片放映模式。啟動程式後進入最佳大小模式。啟動程式後進入符合高度模式。啟動程式後進入符合寬度模式。停止投影片放映儲存「最近開啟過的檔案」資訊:儲存開啟過的檔案的預覽縮圖依據 freedesktop.org 的規定方式,儲存開啟過的檔案的預覽縮圖。這些預覽縮圖也可由各種檔案管理程式等其他許多應用程式分享共用。配合縮放模式,將圖片伸展到符合螢幕大小。伸展小圖片瑞典文翻譯工具列(_O)TAR 壓縮檔預覽縮圖(_U)檔案將會從你的硬碟中刪除。無法儲存新壓縮檔!原始檔案沒有被移除。這些書將從書庫中移除並被永遠刪除,你確定要繼續嗎?這個錯誤可能是因為有 GTK+ 的程式庫不見了。這是做分隔用的假指令。這樣將會移除「最近開啟過的檔案」選單裏的項目、並清除最後讀取頁數的資訊。預覽縮圖大小 (像素):預覽縮圖極小正體中文翻譯形變設定透明度壓縮檔內檔名結尾相符的檔案,將被認定為註解。類型烏克蘭文翻譯不明的檔案類型將書庫的資料庫版本從 %(from)d 升級到 %(to)d 。透明色圖片使用灰色方格背景。如果未設定這個項目,則將使用白色背景。使用壓縮檔的預覽縮圖當程式的圖示透明色圖片使用方格背景使用動態背景顏色預設使用全螢幕模式使用智慧型捲動用這個顏色當背景:用這個顏色當預覽縮圖背景:使用者介面檢視圖片和漫畫書壓縮檔。檢視模式檢視模式啟用後,按 ESC 鍵就關閉這個程式,而不是只用來停用全螢幕模式。遇到壓縮檔的第一頁或比較寬的圖片時,只會單獨顯示一頁。在投影片模式下允許自動開啟下一個壓縮檔。含子目錄通常按空白鍵和滾動滑鼠滾輪時只會上下捲動,但設定這個項目後,也會側向捲動,以儘量配合一般漫畫書的閱讀順序。工作目錄你已經變更了外部指令清單而還沒有儲存,請按「是」儲存所有設定、按「否」放棄變更。你在 %(date)s %(time)s 時讀到這裏就停了。如果你選「是」,可以從第 %(page)d 頁開始讀;否則就會載入第一頁。ZIP 壓縮檔縮放放大(_I)縮小(_O)放大縮放模式縮小[選項...] [路徑]關於(_A)加入(_A)...加入(_A)...自動旋轉圖片(_A)自動調整對比(_A)最佳大小模式(_B)書籤(_B)亮度(_B):取消(_C)清除(_C)關閉(_C)對比(_C):複製(_C)刪除(_D)雙頁模式(_D)備份(_D)編輯(_E)編輯書籤(_E)...編輯壓縮檔(_E)...編輯指令(_E)檔案(_F)第一頁(_F)全螢幕(_F)前往(_G)指定頁數(_G)...說明(_H)匯入(_I)保持形變設定(_K)最後一頁(_L)書庫(_L)...漫畫模式(_M)選單列(_M)下一頁(_N)正常大小(_N)開啟(_O)開啟(_O)...上一頁(_P)離開(_Q)最近開啟過的(_R)移除(_R)移除並從磁碟中刪除(_R)順時針旋轉 90 度(_R)儲存後離開(_S)立即掃描(_S)搜尋(_S):排序(_S)工具列(_T)工具(_T)圖片形變(_T)檢視(_V)觀看清單(_W)縮放(_Z)././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/messages/zh_TW/__init__.py0000644000175000017500000000000014476523373020754 0ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1698566559.0
mcomix-3.1.0/mcomix/openwith.py0000644000175000017500000005746414517410637016234 0ustar00moritzmoritz""" openwith.py - Logic and storage for Open with... commands. """
import sys
import os
import re
from gi.repository import Gtk, GLib, GObject

from mcomix.preferences import prefs
from mcomix import message_dialog
from mcomix import process
from mcomix import callback
from mcomix import i18n
from mcomix.i18n import _


DEBUGGING_CONTEXT, NO_FILE_CONTEXT, IMAGE_FILE_CONTEXT, ARCHIVE_CONTEXT = -1, 0, 1, 2


class OpenWithException(Exception): pass


class OpenWithManager(object):
    def __init__(self):
        """ Constructor. """
        pass

    @callback.Callback
    def set_commands(self, cmds):
        prefs['openwith commands'] = [(cmd.get_label(), cmd.get_command(),
            cmd.get_cwd(), cmd.is_disabled_for_archives())
            for cmd in cmds]

    def get_commands(self):
        try:
            return [OpenWithCommand(label, command, cwd, disabled_for_archives)
                    for label, command, cwd, disabled_for_archives
                    in prefs['openwith commands']]
        except ValueError:
            # Backwards compatibility for early versions with only two parameters
            return [OpenWithCommand(label, command, '', False)
                    for label, command in prefs['openwith commands']]


class OpenWithCommand(object):
    def __init__(self, label, command, cwd, disabled_for_archives):
        self.label = label
        self.command = command.strip()
        self.cwd = cwd.strip()

        self.disabled_for_archives = bool(disabled_for_archives)

    def get_label(self):
        return self.label

    def get_command(self):
        return self.command

    def get_cwd(self):
        return self.cwd

    def is_disabled_for_archives(self):
        return self.disabled_for_archives

    def is_separator(self):
        return bool(re.match(r'^-+$', self.get_label().strip()))

    def execute(self, window):
        """ Spawns a new process with the given executable
        and arguments. """
        if (self.is_disabled_for_archives() and
            window.filehandler.archive_type is not None):
            window.osd.show(_("'%s' is disabled for archives.") % self.get_label())
            return

        current_dir = os.getcwd()
        try:
            if self.is_valid_workdir(window):
                workdir = self.parse(window, text=self.get_cwd())[0]
                os.chdir(workdir)

            # Redirect process output to null here?
            # FIXME: Close process when finished to avoid zombie process
            args = self.parse(window)
            if sys.platform == 'win32':
                proc = process.Win32Popen(args)
            else:
                proc = process.popen(args, stdout=process.NULL)
            del proc

        except Exception as e:
            text = _("Could not run command %(cmdlabel)s: %(exception)s") % \
                {'cmdlabel': self.get_label(), 'exception': str(e)}
            window.osd.show(text)
        finally:
            os.chdir(current_dir)

    def is_executable(self, window):
        """ Check if a name is executable. This name can be either
        a relative path, when the executable is in PATH, or an
        absolute path. """
        args = self.parse(window)
        if len(args) == 0:
            return False

        if self.is_valid_workdir(window):
            workdir = self.parse(window, text=self.get_cwd())[0]
        else:
            workdir = os.getcwd()

        exe = process.find_executable((args[0],), workdir=workdir)

        return exe is not None

    def is_valid_workdir(self, window, allow_empty=False):
        """ Check if the working directory is valid. """
        cwd = self.get_cwd().strip()
        if not cwd:
            return allow_empty

        args = self.parse(window, text=cwd)
        if len(args) > 1:
            return False

        dir = args[0]
        if os.path.isdir(dir) and os.access(dir, os.X_OK):
            return True

        return False

    def parse(self, window, text='', check_restrictions=True):
        """ Parses the command string and replaces special characters
        with their respective variable contents. Returns a list of
        arguments.
        If check_restrictions is False, no checking will be done
        if one of the variables isn't valid in the current file context. """
        if not text:
            text = self.get_command()
        if not text.strip():
            raise OpenWithException(_('Command line is empty.'))

        args = self._commandline_to_arguments(text, window,
            self._get_context_type(window, check_restrictions))
        # Environment variables must be expanded after MComix variables,
        # as win32 will eat %% and replace it with %.
        args = [os.path.expandvars(arg) for arg in args]
        return args

    def _commandline_to_arguments(self, line, window, context_type):
        """ Parse a command line string into a list containing
        the parts to pass to Popen. The following two functions have
        been contributed by Ark . """
        result = []
        buf = ""
        quote = False
        escape = False
        inarg = False
        for c in line:
            if escape:
                if c == '%' or c == '"':
                    buf += c
                else:
                    buf += self._expand_variable(c, window, context_type)
                escape = False
            elif c == ' ' or c == '\t':
                if quote:
                    buf += c
                elif inarg:
                    result.append(buf)
                    buf = ""
                    inarg = False
            else:
                if c == '"':
                    quote = not quote
                elif c == '%':
                    escape = True
                else:
                    buf += c
                inarg = True

        if escape:
            raise OpenWithException(
                _("Incomplete escape sequence. "
                  "For a literal '%', use '%%'."))
        if quote:
            raise OpenWithException(
                _("Incomplete quote sequence. "
                  "For a literal '\"', use '%\"'."))

        if inarg:
            result.append(buf)
        return result

    def _expand_variable(self, identifier, window, context_type):
        """ Replaces variables with their respective file
        or archive path. """

        if context_type == DEBUGGING_CONTEXT:
            return '%' + identifier

        if not (context_type & IMAGE_FILE_CONTEXT) and identifier in ('f', 'd', 'b', 's', 'F', 'D', 'B', 'S'):
            raise OpenWithException(
                _("File-related variables can only be used for files."))

        if not (context_type & ARCHIVE_CONTEXT) and identifier in ('a', 'c', 'A', 'C'):
            raise OpenWithException(
                _("Archive-related variables can only be used for archives."))

        if identifier == '/':
            return os.path.sep
        elif identifier == 'a':
            return window.filehandler.get_base_filename()
        elif identifier == 'd':
            return os.path.basename(os.path.dirname(window.imagehandler.get_path_to_page()))
        elif identifier == 'f':
            return window.imagehandler.get_page_filename()
        elif identifier == 'c':
            return os.path.basename(os.path.dirname(window.filehandler.get_path_to_base()))
        elif identifier == 'b':
            if (context_type & ARCHIVE_CONTEXT):
                return window.filehandler.get_base_filename() # same as %a
            else:
                return os.path.basename(os.path.dirname(window.imagehandler.get_path_to_page())) # same as %d
        elif identifier == 's':
            if (context_type & ARCHIVE_CONTEXT):
                return os.path.basename(os.path.dirname(window.filehandler.get_path_to_base())) # same as %c
            else:
                return os.path.basename(os.path.dirname(os.path.dirname(window.imagehandler.get_path_to_page())))
        elif identifier == 'A':
            return window.filehandler.get_path_to_base()
        elif identifier == 'D':
            return os.path.normpath(os.path.dirname(window.imagehandler.get_path_to_page()))
        elif identifier == 'F':
            return os.path.normpath(window.imagehandler.get_path_to_page())
        elif identifier == 'C':
            return os.path.dirname(window.filehandler.get_path_to_base())
        elif identifier == 'B':
            if (context_type & ARCHIVE_CONTEXT):
                return window.filehandler.get_path_to_base() # same as %A
            else:
                return os.path.normpath(os.path.dirname(window.imagehandler.get_path_to_page())) # same as %D
        elif identifier == 'S':
            if (context_type & ARCHIVE_CONTEXT):
                return os.path.dirname(window.filehandler.get_path_to_base()) # same as %C
            else:
                return os.path.dirname(os.path.dirname(window.imagehandler.get_path_to_page()))
        else:
            raise OpenWithException(
                _("Invalid escape sequence: %%%s") % identifier)

    def _get_context_type(self, window, check_restrictions=True):
        if not check_restrictions:
            return DEBUGGING_CONTEXT # ignore context, reflect variable name
        context = 0
        if not window.filehandler.file_loaded:
            context = NO_FILE_CONTEXT # no file loaded
        elif window.filehandler.archive_type is not None:
            context = IMAGE_FILE_CONTEXT|ARCHIVE_CONTEXT # archive loaded
        else:
            context = IMAGE_FILE_CONTEXT # image loaded (no archive)
        if not window.imagehandler.get_current_page():
            context &= ~IMAGE_FILE_CONTEXT # empty archive
        return context


class OpenWithEditor(Gtk.Dialog):
    """ The editor for changing and creating external commands. This window
    keeps its own internal model once initialized, and will overwrite
    the external model (i.e. preferences) only when properly closed. """

    def __init__(self, window, openwithmanager):
        super(OpenWithEditor, self).__init__(_('Edit external commands'), parent=window)
        self.set_destroy_with_parent(True)
        self._window = window
        self._openwith = openwithmanager
        self._changed = False

        self._command_tree = Gtk.TreeView()
        self._command_tree.get_selection().connect('changed', self._item_selected)
        self._add_button = Gtk.Button(stock=Gtk.STOCK_ADD)
        self._add_button.connect('clicked', self._add_command)
        self._add_sep_button = Gtk.Button.new_with_mnemonic(_('Add _separator'))
        self._add_sep_button.connect('clicked', self._add_sep_command)
        self._remove_button = Gtk.Button(stock=Gtk.STOCK_REMOVE)
        self._remove_button.connect('clicked', self._remove_command)
        self._remove_button.set_sensitive(False)
        self._up_button = Gtk.Button(stock=Gtk.STOCK_GO_UP)
        self._up_button.connect('clicked', self._up_command)
        self._up_button.set_sensitive(False)
        self._down_button = Gtk.Button(stock=Gtk.STOCK_GO_DOWN)
        self._down_button.connect('clicked', self._down_command)
        self._down_button.set_sensitive(False)
        self._run_button = Gtk.Button.new_with_mnemonic(_('Run _command'))
        self._run_button.connect('clicked', self._run_command)
        self._run_button.set_sensitive(False)
        self._test_field = Gtk.Entry()
        self._test_field.set_property('editable', False)
        self._exec_label = Gtk.Label()
        self._exec_label.set_alignment(0, 0)
        self._set_exec_text('')
        self._save_button = self.add_button(Gtk.STOCK_SAVE, Gtk.ResponseType.ACCEPT)
        self.set_default_response(Gtk.ResponseType.ACCEPT)

        self._layout()
        self._setup_table()

        self.connect('response', self._response)
        self._window.page_changed += self.test_command
        self._window.filehandler.file_opened += self.test_command
        self._window.filehandler.file_closed += self.test_command

        self.resize(600, 400)

    def save(self):
        """ Serializes the tree model into a list of OpenWithCommands
        and passes these back to the Manager object for persistance. """
        commands = self.get_commands()
        self._openwith.set_commands(commands)
        self._changed = False

    def get_commands(self):
        """ Retrieves a list of OpenWithCommand instances from
        the list model. """
        model = self._command_tree.get_model()
        iter = model.get_iter_first()
        commands = []
        while iter:
            label, command, cwd, disabled_for_archives = model.get(iter, 0, 1, 2, 3)
            commands.append(OpenWithCommand(label, command, cwd, disabled_for_archives))
            iter = model.iter_next(iter)
        return commands

    def get_command(self):
        """ Retrieves the selected command object. """
        selection = self._command_tree.get_selection()
        if not selection:
            return None

        model, iter = self._command_tree.get_selection().get_selected()
        if (iter and model.iter_is_valid(iter)):
            command = OpenWithCommand(*model.get(iter, 0, 1, 2, 3))
            return command
        else:
            return None

    def test_command(self):
        """ Parses the currently selected command and displays the output in the
        text box next to the button. """
        command = self.get_command()
        self._run_button.set_sensitive(False)
        if not command:
            return

        # Test only if the selected field is a valid command
        if command.is_separator():
            self._test_field.set_text(_('This is a separator pseudo-command.'))
            self._set_exec_text('')
            return

        try:
            args = list(map(self._quote_if_necessary, command.parse(self._window)))
            self._test_field.set_text(" ".join(map(i18n.to_display_string, args)))
            self._run_button.set_sensitive(True)

            if not command.is_valid_workdir(self._window, allow_empty=True):
                self._set_exec_text(
                    _('"%s" does not have a valid working directory.') % command.get_label())
            elif not command.is_executable(self._window):
                self._set_exec_text(
                    _('"%s" does not appear to have a valid executable.') % command.get_label())
            else:
                self._set_exec_text('')
        except OpenWithException as e:
            self._test_field.set_text(str(e))
            self._set_exec_text('')

    def _add_command(self, button):
        """ Add a new empty label-command line to the list. """
        row = (_('Command label'), '', '', False, True)
        selection = self._command_tree.get_selection()
        if selection and selection.get_selected()[1]:
            model, iter = selection.get_selected()
            model.insert_before(iter, row)
        else:
            self._command_tree.get_model().append(row)
        self._changed = True

    def _add_sep_command(self, button):
        """ Adds a new separator line. """
        row = ('-', '', '', False, False)
        selection = self._command_tree.get_selection()
        if selection and selection.get_selected()[1]:
            model, iter = selection.get_selected()
            model.insert_before(iter, row)
        else:
            self._command_tree.get_model().append(row)
        self._changed = True

    def _remove_command(self, button):
        """ Removes the currently selected command from the list. """
        model, iter = self._command_tree.get_selection().get_selected()
        if (iter and model.iter_is_valid(iter)):
            model.remove(iter)
            self._changed = True

    def _up_command(self, button):
        """ Moves the selected command up by one. """
        model, iter = self._command_tree.get_selection().get_selected()
        if (iter and model.iter_is_valid(iter)):
            path = model.get_path(iter)[0]

            if path >= 1:
                up = model.get_iter(path - 1)
                model.swap(iter, up)
            self._changed = True

    def _down_command(self, button):
        """ Moves the selected command down by one. """
        model, iter = self._command_tree.get_selection().get_selected()
        if (iter and model.iter_is_valid(iter)):
            path = model.get_path(iter)[0]

            if path < len(self.get_commands()) - 1:
                down = model.get_iter(path + 1)
                model.swap(iter, down)
            self._changed = True

    def _run_command(self, button):
        """ Executes the selected command in the current context. """
        command = self.get_command()
        if command and not command.is_separator():
            command.execute(self._window)

    def _item_selected(self, selection):
        """ Enable or disable buttons that depend on an item being selected. """
        for button in (self._remove_button, self._up_button,
                self._down_button):
            button.set_sensitive(selection.count_selected_rows() > 0)

        if selection.count_selected_rows() > 0:
            self.test_command()
        else:
            self._test_field.set_text('')

    def _set_exec_text(self, text):
        self._exec_label.set_text(text)

    def _layout(self):
        """ Create and lay out UI components. """
        # All these boxes basically are just for adding a 4px border
        vbox = self.get_content_area()
        hbox = Gtk.HBox()
        vbox.pack_start(hbox, True, True, 4)
        content = Gtk.VBox()
        content.set_spacing(6)
        hbox.pack_start(content, True, True, 4)

        scroll_window = Gtk.ScrolledWindow()
        scroll_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scroll_window.add(self._command_tree)
        content.pack_start(scroll_window, True, True, 0)

        buttonbox = Gtk.HBox()
        buttonbox.pack_start(self._add_button, False, False, 0)
        buttonbox.pack_start(self._add_sep_button, False, False, 0)
        buttonbox.pack_start(self._remove_button, False, False, 0)
        buttonbox.pack_start(self._up_button, False, False, 0)
        buttonbox.pack_start(self._down_button, False, False, 0)
        content.pack_start(buttonbox, False, False, 0)

        preview_box = Gtk.HBox()
        preview_box.pack_start(Gtk.Label(_('Preview:')), False, False, 0)
        preview_box.pack_start(self._test_field, True, True, 4)
        preview_box.pack_start(self._run_button, False, False, 0)
        content.pack_start(preview_box, False, False, 0)

        content.pack_start(self._exec_label, False, False, 0)

        linklabel = Gtk.Label()
        linklabel.set_markup(_('Please refer to the external command documentation '
            'for a list of usable variables and other hints.') % \
                'https://sourceforge.net/p/mcomix/wiki/External_Commands')
        linklabel.set_alignment(0, 0)
        content.pack_start(linklabel, False, False, 4)

    def _setup_table(self):
        """ Initializes the TreeView with settings and data. """
        for i, label in enumerate((_('Label'), _('Command'), _('Working directory'))):
            renderer = Gtk.CellRendererText()
            renderer.connect('edited', self._text_changed, i)
            column = Gtk.TreeViewColumn(label, renderer)
            column.set_property('resizable', True)
            column.set_attributes(renderer, text=i, editable=4)
            if (i == 1):
                column.set_expand(True)  # Command column should scale automatically
            self._command_tree.append_column(column)

        # The 'Disabled in archives' field is shown as toggle button
        renderer = Gtk.CellRendererToggle()
        renderer.connect('toggled', self._value_changed,
                len(self._command_tree.get_columns()))
        column = Gtk.TreeViewColumn(_('Disabled in archives'), renderer)
        column.set_attributes(renderer, active=len(self._command_tree.get_columns()),
                activatable=4)
        self._command_tree.append_column(column)

        # Label, command, working dir, disabled for archives, line is editable
        model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING,
                GObject.TYPE_BOOLEAN, GObject.TYPE_BOOLEAN)
        for command in self._openwith.get_commands():
            model.append((command.get_label(), command.get_command(), command.get_cwd(),
                command.is_disabled_for_archives(), not command.is_separator()))
        self._command_tree.set_model(model)

        self._command_tree.set_headers_visible(True)
        self._command_tree.set_reorderable(True)

    def _text_changed(self, renderer, path, new_text, column):
        """ Called when the user edits a field in the table. """
        # Prevent changing command to separator, and completely removing label
        if column == 0 and (not new_text.strip() or re.match(r'^-+$', new_text)):
            return

        model = self._command_tree.get_model()
        iter = model.get_iter(path)
        # Editing the model in the cellrenderercallback stops the editing
        # operation, causing GTK warnings. Delay until callback is finished.
        def delayed_set_value():
            old_value = model.get_value(iter, column)
            model.set_value(iter, column, new_text)
            self._changed = old_value != new_text
            self.test_command()
        GLib.idle_add(delayed_set_value)

    def _value_changed(self, renderer, path, column):
        """ Called when a toggle field is changed """
        model = self._command_tree.get_model()
        iter = model.get_iter(path)
        # Editing the model in the cellrenderercallback stops the editing
        # operation, causing GTK warnings. Delay until callback is finished.
        def delayed_set_value():
            value = not renderer.get_active()
            model.set_value(iter, column, value)
            self._changed = True

        GLib.idle_add(delayed_set_value)

    def _response(self, dialog, response):
        if response == Gtk.ResponseType.ACCEPT:
            # The Save button is only enabled if all commands are valid
            self.save()
            self.hide()
        else:
            if self._changed:
                confirm_diag = message_dialog.MessageDialog(self, Gtk.DialogFlags.MODAL,
                    Gtk.MessageType.INFO, Gtk.ButtonsType.YES_NO)
                confirm_diag.set_text(_('Save changes to commands?'),
                    _('You have made changes to the list of external commands that '
                      'have not been saved yet. Press "Yes" to save all changes, '
                      'or "No" to discard them.'))
                response = confirm_diag.run()

                if response == Gtk.ResponseType.YES:
                    self.save()

    def _quote_if_necessary(self, arg):
        """ Quotes a command line argument if necessary. """
        if arg == "":
            return '""'
        if sys.platform == 'win32':
            # based on http://msdn.microsoft.com/en-us/library/17w5ykft%28v=vs.85%29.aspx
            backslash_counter = 0
            needs_quoting = False
            result = ""
            for c in arg:
                if c == '\\':
                    backslash_counter += 1
                else:
                    if c == '\"':
                        result += '\\' * (2 * backslash_counter + 1)
                    else:
                        result += '\\' * backslash_counter
                    backslash_counter = 0
                    result += c
                if c == ' ':
                    needs_quoting = True

            if needs_quoting:
                result += '\\' * (2 * backslash_counter)
                result = '"' + result + '"'
            else:
                result += '\\' * backslash_counter
            return result
        else:
            # simplified version of
            # http://www.gnu.org/software/bash/manual/bashref.html#Double-Quotes
            arg = arg.replace('\\', '\\\\')
            arg = arg.replace('"', '\\"')
            if " " in arg:
                return '"' + arg + '"'
            return arg


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/openwith_menu.py0000644000175000017500000000670414476523373017255 0ustar00moritzmoritz""" openwith_menu.py - Menu shell for the Open with... menu. """

from gi.repository import Gtk

from mcomix import openwith
from mcomix.i18n import _

# Reference to the OpenWith command manager
_openwith_manager = openwith.OpenWithManager()
# Reference to the edit dialog (to keep only one instance)
_openwith_edit_diag = None

class OpenWithMenu(Gtk.Menu):
    def __init__(self, ui, window):
        """ Constructor. """
        super(OpenWithMenu, self).__init__()

        self._window = window
        self._openwith_manager = _openwith_manager

        actiongroup = Gtk.ActionGroup('mcomix-openwith')
        actiongroup.add_actions([
            ('edit_commands', Gtk.STOCK_EDIT, _('_Edit commands'),
             None, None, self._edit_commands)])

        action = actiongroup.get_action('edit_commands')
        action.set_accel_group(ui.get_accel_group())
        self.edit_button = action.create_menu_item()
        self.append(self.edit_button)

        self._construct_menu()

        self._window.filehandler.file_opened += self._set_sensitivity
        self._window.filehandler.file_closed += self._set_sensitivity
        self._openwith_manager.set_commands += self._construct_menu

        self.show_all()

    def _construct_menu(self, *args):
        """ Build the menu entries from scratch. """
        for item in self.get_children():
            if item != self.edit_button:
                self.remove(item)

        commandlist = self._openwith_manager.get_commands()

        if len(commandlist) > 0:
            separator = Gtk.SeparatorMenuItem()
            separator.show()
            self.prepend(separator)

        for command in reversed(commandlist):
            if not command.is_separator():
                menuitem = Gtk.MenuItem(command.get_label())
                menuitem.connect('activate', self._commandmenu_clicked,
                        command.get_command(), command.get_label(),
                        command.get_cwd(), command.is_disabled_for_archives())
            else:
                menuitem = Gtk.SeparatorMenuItem()

            menuitem.show()
            self.prepend(menuitem)

        self._set_sensitivity()

    def _set_sensitivity(self):
        """ Enables or disables menu items depending on files being loaded. """
        sensitive = self._window.filehandler.file_loaded
        for item in self.get_children():
            if item != self.edit_button:
                item.set_sensitive(sensitive)

    def _commandmenu_clicked(self, menuitem, cmd, label, cwd, disabled_in_archives):
        """ Execute the command associated with the clicked menu. """
        command = openwith.OpenWithCommand(label, cmd, cwd, disabled_in_archives)
        command.execute(self._window)

    def _edit_commands(self, *args):
        """ When clicked, opens the command editor to set up the menu. Make
        sure the dialog isn't opened more than once. """
        global _openwith_edit_diag
        if not _openwith_edit_diag:
            _openwith_edit_diag = openwith.OpenWithEditor(self._window,
                    self._openwith_manager)
            _openwith_edit_diag.connect_after('response', self._dialog_closed)

        _openwith_edit_diag.show_all()
        _openwith_edit_diag.present()

    def _dialog_closed(self, *args):
        """ Watch for the dialog getting closed and unset the local instance. """
        global _openwith_edit_diag
        _openwith_edit_diag.destroy()
        _openwith_edit_diag = None

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/osd.py0000644000175000017500000001163714476523373015162 0ustar00moritzmoritz""" osd.py - Onscreen display showing currently opened file. """
# -*- coding: utf-8 -*-

import textwrap

from gi.repository import Gdk, GLib
from gi.repository import Pango, PangoCairo

from mcomix import image_tools


class OnScreenDisplay(object):

    """ The OSD shows information such as currently opened file, archive and
    page in a black box drawn on the bottom end of the screen.

    The OSD will automatically be erased after TIMEOUT seconds.
    """

    TIMEOUT = 3

    def __init__(self, window):
        #: MainWindow
        self._window = window
        #: Stores the last rectangle that was used to render the OSD
        self._last_osd_rect = None
        #: Timeout event ID registered while waiting to hide the OSD
        self._timeout_event = None

    def show(self, text):
        """ Shows the OSD on the lower portion of the image window. """

        # Determine text to draw
        text = self._wrap_text(text)
        layout = self._window._image_box.create_pango_layout(text)

        # Set up font information
        font = layout.get_context().get_font_description()
        font.set_weight(Pango.Weight.BOLD)
        layout.set_alignment(Pango.Alignment.CENTER)

        # Scale font to fit within the screen size
        max_width, max_height = self._window.get_visible_area_size()
        self._scale_font(font, layout, max_width, max_height)

        # Calculate surrounding box
        layout_width, layout_height = layout.get_pixel_size()
        pos_x = max(int(max_width // 2) - int(layout_width // 2) +
                    int(self._window._hadjust.get_value()), 0)
        pos_y = max(int(max_height) - int(layout_height * 1.1) +
                    int(self._window._vadjust.get_value()), 0)

        rect = (pos_x - 10, pos_y - 20,
                layout_width + 20, layout_height + 20)

        self._draw_osd(layout, rect)

        self._last_osd_rect = rect
        if self._timeout_event:
            GLib.source_remove(self._timeout_event)
        self._timeout_event = GLib.timeout_add_seconds(
            OnScreenDisplay.TIMEOUT, self.clear)

    def clear(self):
        """ Removes the OSD. """
        if self._timeout_event:
            GLib.source_remove(self._timeout_event)
        self._timeout_event = None
        self._clear_osd()
        return 0 # To unregister gobject timer event

    def _wrap_text(self, text, width=70):
        """ Wraps the text to be C{width} characters at most. """
        parts = text.split('\n')
        result = []

        for part in parts:
            if part:
                result.extend(textwrap.wrap(part, width))
            else:
                result.append(part)

        return "\n".join(result)

    def _clear_osd(self):
        """ Clear the last OSD region. """

        if not self._last_osd_rect:
            return

        window = self._window._main_layout.get_bin_window()
        gdk_rect = Gdk.Rectangle()
        gdk_rect.x, gdk_rect.y, gdk_rect.width, gdk_rect.height = self._last_osd_rect
        window.invalidate_rect(gdk_rect, True)
        window.process_updates(True)
        self._last_osd_rect = None

    def _scale_font(self, font, layout, max_width, max_height):
        """ Scales the font used by C{layout} until max_width/max_height is reached. """

        SIZE_MIN, SIZE_MAX = 10, 60
        for font_size in range(SIZE_MIN, SIZE_MAX, 5):
            old_size = font.get_size()
            font.set_size(font_size * Pango.SCALE)
            layout.set_font_description(font)

            if layout.get_pixel_size()[0] > max_width:
                font.set_size(old_size)
                layout.set_font_description(font)
                break

    def _draw_osd(self, layout, rect):
        """ Draws the text specified in C{layout} into a box at C{rect}. """

        draw_region = Gdk.Rectangle()
        draw_region.x, draw_region.y, draw_region.width, draw_region.height = rect
        if self._last_osd_rect:
            last_region = Gdk.Rectangle()
            last_region.x, last_region.y, last_region.width, last_region.height = self._last_osd_rect
            draw_region = Gdk.rectangle_union(draw_region, last_region)

        gdk_rect = Gdk.Rectangle()
        gdk_rect.x = draw_region.x
        gdk_rect.y = draw_region.y
        gdk_rect.width = draw_region.width
        gdk_rect.height = draw_region.height
        window = self._window._main_layout.get_bin_window()
        window.begin_paint_rect(gdk_rect)

        self._clear_osd()

        cr = window.cairo_create()
        cr.set_source_rgb(*image_tools.GTK_GDK_COLOR_BLACK.to_floats())
        cr.rectangle(*rect)
        cr.fill()
        extents = layout.get_extents()[0]
        cr.set_source_rgb(*image_tools.GTK_GDK_COLOR_WHITE.to_floats())
        cr.translate(rect[0] + extents.x / Pango.SCALE,
                     rect[1] + extents.y / Pango.SCALE)
        PangoCairo.update_layout(cr, layout)
        PangoCairo.show_layout(cr, layout)

        window.end_paint()

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/pageselect.py0000644000175000017500000001316214476523373016504 0ustar00moritzmoritz"""pageselect.py - The dialog window for the page selector."""

from gi.repository import Gtk

from mcomix.preferences import prefs
from mcomix.worker_thread import WorkerThread
from mcomix import callback
from mcomix.i18n import _


class Pageselector(Gtk.Dialog):

    """The Pageselector takes care of the popup page selector
    """

    def __init__(self, window):
        self._window = window
        super(Pageselector, self).__init__("Go to page...", window,
                                     Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT)
        self.add_buttons(_('_Go'), Gtk.ResponseType.OK,
                         _('_Cancel'), Gtk.ResponseType.CANCEL,)
        self.set_default_response(Gtk.ResponseType.OK)
        self.connect('response', self._response)
        self.set_resizable(True)

        self._number_of_pages = self._window.imagehandler.get_number_of_pages()

        self._selector_adjustment = Gtk.Adjustment(value=self._window.imagehandler.get_current_page(),
                              lower=1,upper=self._number_of_pages,
                              step_incr=1, page_incr=1 )

        self._page_selector = Gtk.VScale.new(self._selector_adjustment)
        self._page_selector.set_draw_value(False)
        self._page_selector.set_digits( 0 )

        self._page_spinner = Gtk.SpinButton.new(self._selector_adjustment, 0.0, 0)
        self._page_spinner.connect( 'changed', self._page_text_changed )
        self._page_spinner.set_activates_default(True)
        self._page_spinner.set_numeric(True)
        self._pages_label = Gtk.Label(label=_(' of %s') % self._number_of_pages)
        self._pages_label.set_alignment(0, 0.5)

        self._image_preview = Gtk.Image()
        self._image_preview.set_size_request(
            prefs['thumbnail size'], prefs['thumbnail size'])

        self.connect('configure-event', self._size_changed_cb)
        self.set_size_request(prefs['pageselector width'],
                prefs['pageselector height'])

        # Group preview image and page selector next to each other
        preview_box = Gtk.HBox()
        preview_box.set_border_width(5)
        preview_box.set_spacing(5)
        preview_box.pack_start(self._image_preview, True, True, 0)
        preview_box.pack_end(self._page_selector, False, True, 0)
        # Below them, group selection spinner and current page label
        selection_box = Gtk.HBox()
        selection_box.set_border_width(5)
        selection_box.pack_start(self._page_spinner, True, True, 0)
        selection_box.pack_end(self._pages_label, False, True, 0)

        self.get_content_area().pack_start(preview_box, True, True, 0)
        self.get_content_area().pack_end(selection_box, False, True, 0)
        self.show_all()

        self._selector_adjustment.connect('value-changed', self._cb_value_changed)

        # Set focus on the input box.
        self._page_spinner.select_region(0, -1)
        self._page_spinner.grab_focus()

        # Currently displayed thumbnail page.
        self._thumbnail_page = 0
        self._thread = WorkerThread(self._generate_thumbnail, name='preview')
        self._update_thumbnail(int(self._selector_adjustment.props.value))
        self._window.imagehandler.page_available += self._page_available

    def _cb_value_changed(self, *args):
        """ Called whenever the spinbox value changes. Updates the preview thumbnail. """
        page = int(self._selector_adjustment.props.value)
        if page != self._thumbnail_page:
            self._update_thumbnail(page)

    def _size_changed_cb(self, *args):
        # Window cannot be scaled down unless the size request is reset
        self.set_size_request(-1, -1)
        # Store dialog size
        prefs['pageselector width'] = self.get_allocation().width
        prefs['pageselector height'] = self.get_allocation().height

        self._update_thumbnail(int(self._selector_adjustment.props.value))

    def _page_text_changed(self, control, *args):
        """ Called when the page selector has been changed. Used to instantly update
            the preview thumbnail when entering page numbers by hand. """
        if control.get_text().isdigit():
            page = int(control.get_text())
            if page > 0 and page <= self._number_of_pages:
                control.set_value(page)

    def _response(self, widget, event, *args):
        if event == Gtk.ResponseType.OK:
            self._window.set_page(int(self._selector_adjustment.props.value))

        self._window.imagehandler.page_available -= self._page_available
        self._thread.stop()
        self.destroy()

    def _update_thumbnail(self, page):
        """ Trigger a thumbnail update. """
        width = self._image_preview.get_allocation().width
        height = self._image_preview.get_allocation().height
        self._thumbnail_page = page
        self._thread.clear_orders()
        self._thread.append_order((page, width, height))

    def _generate_thumbnail(self, params):
        """ Generate the preview thumbnail for the page selector.
        A transparent image will be used if the page is not yet available. """
        page, width, height = params

        pixbuf = self._window.imagehandler.get_thumbnail(page,
            width=width, height=height, nowait=True)
        self._thumbnail_finished(page, pixbuf)

    @callback.Callback
    def _thumbnail_finished(self, page, pixbuf):
        # Don't bother if we changed page in the meantime.
        if page == self._thumbnail_page:
            self._image_preview.set_from_pixbuf(pixbuf)

    def _page_available(self, page):
        if page == int(self._selector_adjustment.props.value):
            self._update_thumbnail(page)

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/portability.py0000644000175000017500000000517014476523373016732 0ustar00moritzmoritz"""Portability functions for MComix."""

import ctypes
import locale
import sys

from mcomix import constants


def uri_prefix() -> str:
    """The prefix used for creating file URIs. This is 'file://' on
    Linux, but 'file:' on Windows due to urllib using a different
    URI creating scheme here."""
    if sys.platform == "win32":
        return "file:"
    else:
        return "file://"


def normalize_uri(uri: str) -> str:
    """Normalize URIs passed into the program by different applications,
    normally via drag-and-drop."""

    if uri.startswith("file://localhost/"):  # Correctly formatted.
        return uri[16:]
    elif uri.startswith("file:///"):  # Nautilus etc.
        return uri[7:]
    elif uri.startswith("file:/"):  # Xffm etc.
        return uri[5:]
    else:
        return uri


def invalid_filesystem_chars() -> str:
    """List of characters that cannot be used in filenames on the target platform."""
    if sys.platform == "win32":
        return r':*?"<>|' + "".join([chr(i) for i in range(0, 32)])
    else:
        return ""


def get_default_locale() -> str:
    """Gets the user's default locale."""
    if sys.platform == "win32":
        windll = ctypes.windll.kernel32
        code = windll.GetUserDefaultUILanguage()
        return locale.windows_locale[code]
    else:
        lang, _ = locale.getdefaultlocale(("LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"))
        if lang:
            return str(lang)
        else:
            return "C"


def is_system_ui_dark_themed() -> constants.SystemThemeLightness:
    """Determine if the system is configured to use a dark theme by default."""
    if sys.platform == "win32":
        import winreg

        try:
            with winreg.OpenKey(
                winreg.HKEY_CURRENT_USER,
                "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
            ) as personalize_handle:
                _, values_count, _ = winreg.QueryInfoKey(personalize_handle)
                for key_index in range(values_count):
                    key_name, key_value, _ = winreg.EnumValue(
                        personalize_handle, key_index
                    )

                    if key_name == "AppsUseLightTheme":
                        if key_value == 0:
                            return constants.SystemThemeLightness.DARK
                        else:
                            return constants.SystemThemeLightness.LIGHT

                return constants.SystemThemeLightness.LIGHT
        except OSError:
            return constants.SystemThemeLightness.UNKNOWN
    else:
        return constants.SystemThemeLightness.UNKNOWN


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1705863135.0
mcomix-3.1.0/mcomix/preferences.py0000644000175000017500000001546314553263737016700 0ustar00moritzmoritz""" preferences.py - Contains the preferences and the functions to read and
write them.  """

import json
import os
import pickle
import shutil
import sys

from mcomix import constants

# All the preferences are stored here.
prefs = {
    'comment extensions': constants.ACCEPTED_COMMENT_EXTENSIONS,
    'auto load last file': False,
    'page of last file': 1,
    'path to last file': '',
    'number of key presses before page turn': 3,
    'auto open next archive': True,
    'auto open next directory': True,
    'open first file in prev archive': False,
    'open first file in prev directory': False,
    'sort by': constants.SORT_NAME,  # Normal files obtained by directory listing
    'sort order': constants.SORT_ASCENDING,
    'sort archive by': constants.SORT_NAME,  # Files in archives
    'sort archive order': constants.SORT_ASCENDING,
    'bg colour': [5000, 5000, 5000],
    'thumb bg colour': [5000, 5000, 5000],
    'smart bg': False,
    'smart thumb bg': False,
    'thumbnail bg uses main colour': False,
    'checkered bg for transparent images': True,
    'cache': True,
    'stretch': False,
    'default double page': False,
    'default fullscreen': False,
    'zoom mode': constants.ZoomMode.BEST,
    'default manga mode': False,
    'lens magnification': 2,
    'lens size': 200,
    'virtual double page for fitting images': constants.SHOW_DOUBLE_AS_ONE_TITLE | \
                                              constants.SHOW_DOUBLE_AS_ONE_WIDE,
    'double step in double page mode': True,
    'show page numbers on thumbnails': True,
    'thumbnail size': 80,
    'create thumbnails': True,
    'archive thumbnail as icon' : False,
    'number of pixels to scroll per key event': 50,
    'number of pixels to scroll per mouse wheel event': 50,
    'slideshow delay': 3000,
    'slideshow can go to next archive': True,
    'number of pixels to scroll per slideshow event': 50,
    'smart scroll': True,
    'invert smart scroll': False,
    'smart scroll percentage': 0.5,
    'flip with wheel': True,
    'store recent file info': True,
    'hide all': False,
    'hide all in fullscreen': True,
    'stored hide all values': [True, True, True, True, True],
    'path of last browsed in filechooser': constants.HOME_DIR,
    'store last saved in directory': True,
    'path of last saved in filechooser': constants.HOME_DIR,
    'last filter in main filechooser': 0,
    'last filter in library filechooser': 1,
    'show menubar': True,
    'previous quit was quit and save': False,
    'show scrollbar': True,
    'show statusbar': True,
    'show toolbar': True,
    'show thumbnails': True,
    'rotation': 0,
    'auto rotate from exif': True,
    'auto rotate depending on size': constants.AUTOROTATE_NEVER,
    'vertical flip': False,
    'horizontal flip': False,
    'keep transformation': False,
    'stored dialog choices': {},
    'brightness': 1.0,
    'contrast': 1.0,
    'saturation': 1.0,
    'sharpness': 1.0,
    'auto contrast': False,
    'invert color': False,
    'max pages to cache': 7,
    'window x': 0,
    'window y': 0,
    'window height': 600,
    'window width': 640,
    'window maximized': False,
    'pageselector height': -1,
    'pageselector width': -1,
    'library cover size': 125,
    'last library collection': None,
    'lib window height': 600,
    'lib window width': 500,
    'lib sort key': constants.SORT_PATH,
    'lib sort order': constants.SORT_ASCENDING,
    'language': 'auto',
    'statusbar fields': constants.STATUS_PAGE | constants.STATUS_RESOLUTION | \
                        constants.STATUS_PATH | constants.STATUS_FILENAME | constants.STATUS_FILESIZE,
    'max threads': 3,
    'max extract threads': 1,
    'wrap mouse scroll': False,
    'scaling quality': 2,  # GdkPixbuf.InterpType.BILINEAR
    'escape quits': False,
    'fit to size width wide': 3790,
    'fit to size height wide': 960,
    'fit to size width other': 1450,
    'fit to size height other': 1800,
    'scan for new books on library startup': True,
    'openwith commands': [],  # (label, command) pairs
    'animation mode': constants.ANIMATION_NORMAL,
    'double page autoresize': constants.DOUBLE_PAGE_AUTORESIZE_SIZE,
    'space between two pages': 2,
}


def migrate_home_config_path() -> None:
    """ Migrate the old configuration directory in the user's
    home directory to %APPDATA% on Win32, if the directory
    doesn't already exist. """
    if sys.platform == "win32":
        old_config_dir = os.path.join(os.path.expanduser("~"), "MComix")
        if os.path.isdir(old_config_dir) and not os.path.isdir(constants.CONFIG_DIR):
            shutil.move(old_config_dir, constants.CONFIG_DIR)


def read_preferences_file() -> None:
    """Read preferences data from disk."""

    saved_prefs = None

    migrate_home_config_path()

    if os.path.isfile(constants.PREFERENCE_PATH):
        try:
            config_file = open(constants.PREFERENCE_PATH, 'r')
            saved_prefs = json.load(config_file)
            config_file.close()
        except:
            # Gettext might not be installed yet at this point.
            corrupt_name = "%s.broken" % constants.PREFERENCE_PATH
            print(('! Corrupt preferences file, moving to "%s".' %
                   corrupt_name))
            if os.path.isfile(corrupt_name):
                os.unlink(corrupt_name)

            try:
                # File cannot be moved without closing it first
                config_file.close()
            except:
                pass

            os.rename(constants.PREFERENCE_PATH, corrupt_name)

    elif os.path.isfile(constants.PREFERENCE_PICKLE_PATH):
        try:
            config_file = open(constants.PREFERENCE_PICKLE_PATH, 'rb')
            version = pickle.load(config_file)
            saved_prefs = pickle.load(config_file)
            config_file.close()

            # Remove legacy format preferences file
            os.unlink(constants.PREFERENCE_PICKLE_PATH)
        except Exception:
            # Gettext might not be installed yet at this point.
            print(('! Corrupt legacy preferences file "%s", ignoring...' %
                   constants.PREFERENCE_PICKLE_PATH))

    if saved_prefs:
        for key in saved_prefs:
            if key in prefs:
                prefs[key] = saved_prefs[key]

def write_preferences_file():
    """Write preference data to disk."""
    # TODO: it might be better to save only those options that were (ever)
    # explicitly changed by the used, leaving everything else as default
    # and available (if really needed) to change of defaults on upgrade.
    config_file = open(constants.PREFERENCE_PATH, 'w')
    # XXX: constants.VERSION? It's *preferable* to not complicate the YAML
    # file by adding a `{'version': constants.VERSION, 'prefs': config}`
    # dict or a list.  Adding an extra init line sounds bad too.
    json.dump(prefs, config_file, indent=2)
    config_file.close()

# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1705863135.0
mcomix-3.1.0/mcomix/preferences_dialog.py0000644000175000017500000011562414553263737020217 0ustar00moritzmoritz# -*- coding: utf-8 -*-

"""preferences_dialog.py - Preferences dialog."""

import operator
from gi.repository import Gdk, GdkPixbuf, Gtk, GObject

from mcomix.preferences import prefs
from mcomix import preferences_page
from mcomix import image_tools
from mcomix import constants
from mcomix import message_dialog
from mcomix import keybindings
from mcomix import keybindings_editor
from mcomix.i18n import _

_dialog = None

class _PreferencesDialog(Gtk.Dialog):

    """The preferences dialog where most (but not all) settings that are
    saved between sessions are presented to the user.
    """

    def __init__(self, window):
        super(_PreferencesDialog, self).__init__(_('Preferences'), window)

        # Button text is set later depending on active tab
        self.reset_button = self.add_button('', constants.RESPONSE_REVERT_TO_DEFAULT)
        self.add_button(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE)

        self._window = window
        self.set_resizable(True)
        self.set_default_response(Gtk.ResponseType.CLOSE)

        self.connect('response', self._response)

        notebook = self.notebook = Gtk.Notebook()
        self.vbox.pack_start(notebook, True, True, 0)
        self.set_border_width(4)
        notebook.set_border_width(6)

        page_inits = (
            (_('Appearance'), self._init_appearance_tab),
            (_('Behaviour'), self._init_behaviour_tab),
            (_('Display'), self._init_display_tab),
            (_('Advanced'), self._init_advanced_tab),
        )

        for title, page_init in page_inits:
            container = Gtk.ScrolledWindow()
            container.set_policy(
                Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
            container.set_min_content_height(400)
            container.set_propagate_natural_height(True)
            container.set_overlay_scrolling(False)
            page = page_init()
            container.add(page)
            notebook.append_page(container, Gtk.Label(label=title))

        # Shortcuts is already a ScrolledWindow
        self.shortcuts = self._init_shortcuts_tab()
        notebook.append_page(
            self.shortcuts, Gtk.Label(label=_('Shortcuts')))

        notebook.connect('switch-page', self._tab_page_changed)
        # Update the Reset button's tooltip
        self._tab_page_changed(notebook, None, 0)

        self.show_all()

    def _init_appearance_tab(self):
        # ----------------------------------------------------------------
        # The "Appearance" tab.
        # ----------------------------------------------------------------
        page = preferences_page._PreferencePage(None)

        page.new_section(_('User interface'))

        page.add_row(Gtk.Label(label=_('Language (needs restart):')),
            self._create_language_control())

        page.add_row(self._create_pref_check_button(
            _('Escape key closes program'), 'escape quits',
            _('When active, the ESC key closes the program, instead of only '
              'disabling fullscreen mode.')))

        page.new_section(_('Background'))

        fixed_bg_button, dynamic_bg_button = self._create_binary_pref_radio_buttons(
            _('Use this colour as background:'),
            'color box bg',
            _('Always use this selected colour as the background colour.'),
            _('Use dynamic background colour'),
            'smart bg',
            _('Automatically pick a background colour that fits the viewed image.'))
        page.add_row(fixed_bg_button, self._create_color_button('bg colour'))
        page.add_row(dynamic_bg_button)

        page.new_section(_('Thumbnails'))

        thumb_fixed_bg_button, thumb_dynamic_bg_button = self._create_binary_pref_radio_buttons(
            _('Use this colour as the thumbnail background:'),
            'color box thumb bg',
            _('Always use this selected colour as the thumbnail background colour.'),
            _('Use dynamic background colour'),
            'smart thumb bg',
            _('Automatically use the colour that fits the viewed image for the thumbnail background.'))
        page.add_row(thumb_fixed_bg_button, self._create_color_button('thumb bg colour'))
        page.add_row(thumb_dynamic_bg_button)

        page.add_row(self._create_pref_check_button(
            _('Show page numbers on thumbnails'),
            'show page numbers on thumbnails', None))

        page.add_row(self._create_pref_check_button(
            _('Use archive thumbnail as application icon'),
            'archive thumbnail as icon',
            _('By enabling this setting, the first page of a book will be used as application icon instead of the standard icon.')))

        page.add_row(Gtk.Label(label=_('Thumbnail size (in pixels):')),
            self._create_pref_spinner('thumbnail size',
            1, 20, 500, 1, 10, 0, None))

        page.new_section(_('Transparency'))

        page.add_row(self._create_pref_check_button(
            _('Use checkered background for transparent images'),
            'checkered bg for transparent images',
            _('Use a grey checkered background for transparent images. If this preference is unset, the background is plain white instead.')))

        return page

    def _init_behaviour_tab(self):
        # ----------------------------------------------------------------
        # The "Behaviour" tab.
        # ----------------------------------------------------------------
        page = preferences_page._PreferencePage(None)

        page.new_section(_('Scroll'))

        page.add_row(self._create_pref_check_button(
            _('Use smart scrolling'),
            'smart scroll',
            _('With this preference set, the space key and mouse wheel '
              'do not only scroll down or up, but also sideways and so '
              'try to follow the natural reading order of the comic book.')))

        page.add_row(self._create_pref_check_button(
            _('Flip pages when scrolling off the edges of the page'),
            'flip with wheel',
            _('Flip pages when scrolling "off the page" with the scroll wheel or with the arrow keys. It takes n consecutive "steps" with the scroll wheel or the arrow keys for the pages to be flipped.')))

        page.add_row(self._create_pref_check_button(
            _('Automatically open the next archive'),
            'auto open next archive',
            _('Automatically open the next archive in the directory when flipping past the last page, or the previous archive when flipping past the first page.')))

        page.add_row(self._create_pref_check_button(
            _('Automatically open next directory'),
            'auto open next directory',
            _('Automatically open the first file in the next sibling directory when flipping past the last page of the last file in a directory, or the previous directory when flipping past the first page of the first file.')))

        page.add_row(self._create_pref_check_button(
            _('Open first file when navigating to previous archive'),
            'open first file in prev archive',
            _('Automatically open the first file of the previous archive when navigating to it, instead of opening the last file of the previous archive.')))

        page.add_row(self._create_pref_check_button(
            _('Open first file when navigating to previous directory'),
            'open first file in prev directory',
            _('Automatically open the first file of the previous directory when navigating to it, instead of opening the last file of the previous directory.')))

        page.add_row(Gtk.Label(label=_('Number of pixels to scroll per arrow key press:')),
            self._create_pref_spinner('number of pixels to scroll per key event',
            1, 1, 500, 1, 3, 0,
            _('Set the number of pixels to scroll on a page when using the arrow keys.')))

        page.add_row(Gtk.Label(label=_('Number of pixels to scroll per mouse wheel turn:')),
            self._create_pref_spinner('number of pixels to scroll per mouse wheel event',
            1, 1, 500, 1, 3, 0,
            _('Set the number of pixels to scroll on a page when using a mouse wheel.')))

        page.add_row(Gtk.Label(label=_('Fraction of page to scroll '
            'per space key press (in percent):')),
            self._create_pref_spinner('smart scroll percentage',
            0.01, 1, 100, 1, 5, 0,
            _('Sets the percentage by which the page '
            'will be scrolled down or up when the space key is pressed.')))

        page.add_row(Gtk.Label(label=_('Number of "steps" to take before flipping the page:')),
            self._create_pref_spinner('number of key presses before page turn',
            1, 1, 100, 1, 3, 0,
            _('Set the number of "steps" needed to flip to the next or previous page.  Less steps will allow for very fast page turning but you might find yourself accidentally turning pages.')))

        page.new_section(_('Double page mode'))

        page.add_row(self._create_pref_check_button(
            _('Flip two pages in double page mode'),
            'double step in double page mode',
            _('Flip two pages, instead of one, each time we flip pages in double page mode.')))

        page.add_row(Gtk.Label(label=_('Show only one page where appropriate:')),
            self._create_doublepage_as_one_control())

        page.add_row(Gtk.Label(_('Page auto-resizing:')),
            self._create_double_page_autoresize_control())

        page.add_row(Gtk.Label(label=_('Space between two pages (in pixels):')),
            self._create_pref_spinner('space between two pages',
            1, 0, 2, 1, 2, 0, None))

        page.new_section(_('Files'))

        page.add_row(self._create_pref_check_button(
            _('Automatically open the last viewed file on startup'),
            'auto load last file',
            _('Automatically open, on startup, the file that was open when MComix was last closed.')))

        page.add_row(Gtk.Label(label=_('Store information about recently opened files:')),
            self._create_store_recent_combobox())

        page.add_row(self._create_pref_check_button(_('Save As opens at the last directory saved into'),
            'store last saved in directory', 'Open the Save As dialog at the directory in which the last file was saved.'))

        return page

    def _init_display_tab(self):
        # ----------------------------------------------------------------
        # The "Display" tab.
        # ----------------------------------------------------------------
        page = preferences_page._PreferencePage(None)

        page.new_section(_('Fullscreen'))

        page.add_row(self._create_pref_check_button(
            _('Use fullscreen by default'),
            'default fullscreen', None))

        page.add_row(self._create_pref_check_button(
            _('Automatically hide all toolbars in fullscreen'),
            'hide all in fullscreen', None))

        page.new_section(_('Fit to size mode'))

        page.add_row(Gtk.Label(label=_('Fixed width for wide pages:')),
            self._create_pref_spinner('fit to size width wide',
            1, 10, constants.RENDER_SIZE_LIMIT, 10, 50, 0, None))

        page.add_row(Gtk.Label(label=_('Fixed height for wide pages:')),
            self._create_pref_spinner('fit to size height wide',
            1, 10, constants.RENDER_SIZE_LIMIT, 10, 50, 0, None))

        page.add_row(Gtk.Label(label=_('Fixed width for other pages:')),
            self._create_pref_spinner('fit to size width other',
            1, 10, constants.RENDER_SIZE_LIMIT, 10, 50, 0, None))

        page.add_row(Gtk.Label(label=_('Fixed height for other pages:')),
            self._create_pref_spinner('fit to size height other',
            1, 10, constants.RENDER_SIZE_LIMIT, 10, 50, 0, None))

        page.new_section(_('Slideshow'))

        page.add_row(Gtk.Label(label=_('Slideshow delay (in seconds):')),
            self._create_pref_spinner('slideshow delay',
            1000.0, 0.01, 3600.0, 0.1, 1, 2, None))

        page.add_row(Gtk.Label(label=_('Slideshow step (in pixels):')),
            self._create_pref_spinner('number of pixels to scroll per slideshow event',
            1, -500, 500, 1, 1, 0,
            _('Specify the number of pixels to scroll while in slideshow mode. A positive value will scroll forward, a negative value will scroll backwards, and a value of 0 will cause the slideshow to always flip to a new page.')))

        page.add_row(self._create_pref_check_button(
            _('During a slideshow automatically open the next archive'),
            'slideshow can go to next archive',
            _('While in slideshow mode allow the next archive to automatically be opened.')))

        page.new_section(_('Rotation'))

        page.add_row(self._create_pref_check_button(
            _('Automatically rotate images according to their metadata'),
            'auto rotate from exif',
            _('Automatically rotate images when an orientation is specified in the image metadata, such as in an Exif tag.')))

        page.new_section(_('Image quality'))

        page.add_row(Gtk.Label(label=_('Scaling mode')),
            self._create_scaling_quality_combobox())

        return page

    def _init_advanced_tab(self):
        # ----------------------------------------------------------------
        # The "Advanced" tab.
        # ----------------------------------------------------------------

        page = preferences_page._PreferencePage(None)

        page.new_section(_('File order'))

        page.add_row(Gtk.Label(label=_('Sort files and directories by:')),
            self._create_sort_by_control())

        page.add_row(Gtk.Label(label=_('Sort archives by:')),
            self._create_archive_sort_by_control())

        page.new_section(_('Extraction and cache'))

        page.add_row(Gtk.Label(label=_('Maximum number of concurrent extraction threads:')),
            self._create_pref_spinner('max extract threads',
            1, 1, 16, 1, 4, 0,
            _('Set the maximum number of concurrent threads for formats that support it.')))

        page.add_row(self._create_pref_check_button(
            _('Store thumbnails for opened files'),
            'create thumbnails',
            _('Store thumbnails for opened files according to the freedesktop.org specification. These thumbnails are shared by many other applications, such as most file managers.')))

        page.add_row(Gtk.Label(label=_('Maximum number of pages to store in the cache:')),
            self._create_pref_spinner('max pages to cache',
            1, -1, 500, 1, 3, 0,
            _('Set the max number of pages to cache. A value of -1 will cache the entire archive.')))

        page.new_section(_('Magnifying Lens'))

        page.add_row(Gtk.Label(label=_('Magnifying lens size (in pixels):')),
            self._create_pref_spinner('lens size',
            1, 50, 400, 1, 10, 0,
            _('Set the size of the magnifying lens. It is a square with a side of this many pixels.')))

        page.add_row(Gtk.Label(label=_('Magnification factor:')),
            self._create_pref_spinner('lens magnification',
            1, 1.1, 10.0, 0.1, 1.0, 1,
            _('Set the magnification factor of the magnifying lens.')))

        page.new_section(_('Comments'))

        page.add_row(Gtk.Label(label=_('Comment extensions:')),
            self._create_extensions_entry())

        page.new_section(_('Animated images'))

        page.add_row(Gtk.Label(_('Animation mode:')),
            self._create_animation_mode_combobox())

        return page

    def _init_shortcuts_tab(self):
        # ----------------------------------------------------------------
        # The "Shortcuts" tab.
        # ----------------------------------------------------------------
        km = keybindings.keybinding_manager(self._window)
        page = keybindings_editor.KeybindingEditorWindow(km)
        self.shortcuts = page
        return page

    def _tab_page_changed(self, notebook, page_ptr, page_num):
        """ Dynamically switches the "Reset" button's text and tooltip
        depending on the currently selected tab page. """
        new_page = notebook.get_nth_page(page_num)
        if new_page == self.shortcuts:
            self.reset_button.set_label(_("_Reset keys"))
            self.reset_button.set_tooltip_text(
                _("Resets all keyboard shortcuts to their default values."))
            self.reset_button.set_sensitive(True)
        else:
            self.reset_button.set_label(_('Clear _dialog choices'))
            self.reset_button.set_tooltip_text(
                _('Clears all dialog choices that you have previously chosen not to be asked again.'))
            self.reset_button.set_sensitive(len(prefs['stored dialog choices']) > 0)

    def _response(self, dialog, response):
        if response == Gtk.ResponseType.CLOSE:
            _close_dialog()

        elif response == constants.RESPONSE_REVERT_TO_DEFAULT:
            if self.notebook.get_nth_page(self.notebook.get_current_page()) == self.shortcuts:
                # "Shortcuts" page is active, reset all keys to their default value
                km = keybindings.keybinding_manager(self._window)
                km.clear_all()
                self._window._event_handler.register_key_events()
                km.save()
                self.shortcuts.refresh_model()
            else:
                # Reset stored choices
                prefs['stored dialog choices'] = {}
                self.reset_button.set_sensitive(False)

        else:
            # Other responses close the dialog, e.g. clicking the X icon on the dialog.
            _close_dialog()

    def _create_language_control(self):
        """ Creates and returns the combobox for language selection. """
        # Source: http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
        languages = [
            (_('Auto-detect (Default)'), 'auto'),
            ('Català', 'ca'),  # Catalan
            ('čeština', 'cs'),  # Czech
            ('Deutsch', 'de'),  # German
            ('ελληνικά', 'el'),  # Greek
            ('English', 'en'),  # English
            ('Español', 'es'),  # Spanish
            ('فارسی', 'fa'),  # Persian
            ('Français', 'fr'),  # French
            ('Galego', 'gl'),  # Galician
            ('עברית', 'he'),  # Hebrew
            ('Hrvatski jezik', 'hr'),  # Croatian
            ('Magyar', 'hu'),  # Hungarian
            ('Bahasa Indonesia', 'id'),  # Indonesian
            ('Italiano', 'it'),  # Italian
            ('日本語', 'ja'),  # Japanese
            ('한국어', 'ko'),  # Korean
            ('Nederlands', 'nl'),  # Dutch
            ('Język polski', 'pl'),  # Polish
            ('Português', 'pt_BR'),  # Portuguese
            ('pусский язык', 'ru'),  # Russian
            ('Svenska', 'sv'),  # Swedish
            ('українська мова', 'uk'),  # Ukrainian
            ('简体中文', 'zh_CN'),  # Chinese (simplified)
            ('正體中文', 'zh_TW')]  # Chinese (traditional)
        languages.sort(key=operator.itemgetter(0))

        box = self._create_combobox(languages, prefs['language'],
                self._language_changed_cb)

        return box

    def _language_changed_cb(self, combobox, *args):
        """ Called whenever the language was changed. """
        model_index = combobox.get_active()
        if model_index > -1:
            iter = combobox.get_model().iter_nth_child(None, model_index)
            text, lang_code = combobox.get_model().get(iter, 0, 1)
            prefs['language'] = lang_code

    def _create_doublepage_as_one_control(self):
        """ Creates the ComboBox control for selecting virtual double page options. """
        items = (
                (_('Never'), 0),
                (_('Only for title pages'), constants.SHOW_DOUBLE_AS_ONE_TITLE),
                (_('Only for wide images'), constants.SHOW_DOUBLE_AS_ONE_WIDE),
                (_('Always'), constants.SHOW_DOUBLE_AS_ONE_TITLE | constants.SHOW_DOUBLE_AS_ONE_WIDE))

        box = self._create_combobox(items,
                prefs['virtual double page for fitting images'],
                self._double_page_changed_cb)

        box.set_tooltip_text(
            _("When showing the first page of an archive, or an image's width "
              "exceeds its height, only a single page will be displayed."))

        return box

    def _double_page_changed_cb(self, combobox, *args):
        """ Called when a new option was selected for the virtual double page option. """
        iter = combobox.get_active_iter()
        if combobox.get_model().iter_is_valid(iter):
            value = combobox.get_model().get_value(iter, 1)
            prefs['virtual double page for fitting images'] = value
            self._window.draw_image()

    def _create_double_page_autoresize_control(self):
        """ Creates the ComboBox control for selecting double page autoresize options. """
        items = (
                (_('Prefer same scale'), constants.DOUBLE_PAGE_AUTORESIZE_SCALE),
                (_('Prefer same size'), constants.DOUBLE_PAGE_AUTORESIZE_SIZE),
                (_('Fit to same size'), constants.DOUBLE_PAGE_AUTORESIZE_FIT_SIZE))

        box = self._create_combobox(items,
                prefs['double page autoresize'],
                self._double_page_autoresize_changed_cb)

        box.set_tooltip_text(
            _("Maintain relative size or fit to same size."))

        return box

    def _double_page_autoresize_changed_cb(self, combobox, *args):
        """ Called when a new option was selected for the double page autoresize option. """
        iter = combobox.get_active_iter()
        if combobox.get_model().iter_is_valid(iter):
            value = combobox.get_model().get_value(iter, 1)
            prefs['double page autoresize'] = value
            self._window.draw_image()

    def _create_sort_by_control(self):
        """ Creates the ComboBox control for selecting file sort by options. """
        sortkey_items = (
                (_('No sorting'), 0),
                (_('File name'), constants.SORT_NAME),
                (_('File size'), constants.SORT_SIZE),
                (_('Last modified'), constants.SORT_LAST_MODIFIED))

        sortkey_box = self._create_combobox(sortkey_items, prefs['sort by'],
            self._sort_by_changed_cb)

        sortorder_items = (
                (_('Ascending'), constants.SORT_ASCENDING),
                (_('Descending'), constants.SORT_DESCENDING))

        sortorder_box = self._create_combobox(sortorder_items,
                prefs['sort order'],
                self._sort_order_changed_cb)

        box = Gtk.HBox()
        box.pack_start(sortkey_box, True, True, 0)
        box.pack_start(sortorder_box, True, True, 0)

        label = _("Files will be opened and displayed according to the sort order "
              "specified here. This option does not affect ordering within archives.")
        sortkey_box.set_tooltip_text(label)
        sortorder_box.set_tooltip_text(label)

        return box

    def _sort_by_changed_cb(self, combobox, *args):
        """ Called when a new option was selected for the virtual double page option. """
        iter = combobox.get_active_iter()
        if combobox.get_model().iter_is_valid(iter):
            value = combobox.get_model().get_value(iter, 1)
            prefs['sort by'] = value

            self._window.filehandler.refresh_file()

    def _sort_order_changed_cb(self, combobox, *args):
        """ Called when sort order changes (ascending or descending) """
        iter = combobox.get_active_iter()
        if combobox.get_model().iter_is_valid(iter):
            value = combobox.get_model().get_value(iter, 1)
            prefs['sort order'] = value

            self._window.filehandler.refresh_file()

    def _create_archive_sort_by_control(self):
        """ Creates the ComboBox control for selecting archive sort by options. """
        sortkey_items = (
                (_('No sorting'), 0),
                (_('Natural order'), constants.SORT_NAME),
                (_('Literal order'), constants.SORT_NAME_LITERAL))

        sortkey_box = self._create_combobox(sortkey_items, prefs['sort archive by'],
            self._sort_archive_by_changed_cb)

        sortorder_items = (
                (_('Ascending'), constants.SORT_ASCENDING),
                (_('Descending'), constants.SORT_DESCENDING))

        sortorder_box = self._create_combobox(sortorder_items,
                prefs['sort archive order'],
                self._sort_archive_order_changed_cb)

        box = Gtk.HBox()
        box.pack_start(sortkey_box, True, True, 0)
        box.pack_start(sortorder_box, True, True, 0)

        label = _("Files within archives will be sorted according to the order specified here. "
                  "Natural order will sort numbered files based on their natural order, "
                  "i.e. 1, 2, ..., 10, while literal order uses standard C sorting, "
                  "i.e. 1, 2, 34, 5.")
        sortkey_box.set_tooltip_text(label)
        sortorder_box.set_tooltip_text(label)

        return box

    def _sort_archive_by_changed_cb(self, combobox, *args):
        """ Called when a new option was selected for the virtual double page option. """
        iter = combobox.get_active_iter()
        if combobox.get_model().iter_is_valid(iter):
            value = combobox.get_model().get_value(iter, 1)
            prefs['sort archive by'] = value

            self._window.filehandler.refresh_file()

    def _sort_archive_order_changed_cb(self, combobox, *args):
        """ Called when sort order changes (ascending or descending) """
        iter = combobox.get_active_iter()
        if combobox.get_model().iter_is_valid(iter):
            value = combobox.get_model().get_value(iter, 1)
            prefs['sort archive order'] = value

            self._window.filehandler.refresh_file()

    def _create_store_recent_combobox(self):
        """ Creates the combobox for "Store recently opened files". """
        items = (
                (_('Never'), False),
                (_('Always'), True))

        # Map legacy 0/1/2 values:
        if prefs['store recent file info'] == 0:
            selection = False
        elif prefs['store recent file info'] in (1, 2):
            selection = True
        else:
            selection = prefs['store recent file info']

        box = self._create_combobox(items, selection, self._store_recent_changed_cb)
        box.set_tooltip_text(
            _('Add information about all files opened from within MComix to the shared recent files list.'))
        return box

    def _store_recent_changed_cb(self, combobox, *args):
        """ Called when option "Store recently opened files" was changed. """
        iter = combobox.get_active_iter()
        if not combobox.get_model().iter_is_valid(iter):
            return

        value = combobox.get_model().get_value(iter, 1)
        last_value = prefs['store recent file info']
        prefs['store recent file info'] = value
        self._window.filehandler.last_read_page.set_enabled(value)

        # If "Never" was selected, ask to purge recent files.
        if (bool(last_value) is True and value is False
            and (self._window.uimanager.recent.count() > 0
                 or self._window.filehandler.last_read_page.count() > 0)):

            dialog = message_dialog.MessageDialog(self, Gtk.DialogFlags.MODAL,
                Gtk.MessageType.INFO, Gtk.ButtonsType.YES_NO)
            dialog.set_default_response(Gtk.ResponseType.YES)
            dialog.set_text(
                _('Delete information about recently opened files?'),
                _('This will remove all entries from the "Recent" menu,'
                  ' and clear information about last read pages.'))
            response = dialog.run()

            if response == Gtk.ResponseType.YES:
                self._window.uimanager.recent.remove_all()
                self._window.filehandler.last_read_page.clear_all()

    def _create_scaling_quality_combobox(self):
        """ Creates combo box for image scaling quality """
        items = (
                (_('Normal (fast)'), int(GdkPixbuf.InterpType.TILES)),
                (_('Bilinear'), int(GdkPixbuf.InterpType.BILINEAR)),
                (_('Hyperbolic (slow)'), int(GdkPixbuf.InterpType.HYPER)))

        selection = prefs['scaling quality']

        box = self._create_combobox(items, selection, self._scaling_quality_changed_cb)
        box.set_tooltip_text(
            _('Changes how images are scaled. Slower algorithms result in higher quality resizing, but longer page loading times.'))

        return box

    def _scaling_quality_changed_cb(self, combobox, *args):
        """ Called whan image scaling quality changes. """
        iter = combobox.get_active_iter()
        if combobox.get_model().iter_is_valid(iter):
            value = combobox.get_model().get_value(iter, 1)
            last_value = prefs['scaling quality']
            prefs['scaling quality'] = value

            if value != last_value:
                self._window.draw_image()

    def _create_animation_mode_combobox(self):
        """ Creates combo box for animation mode """
        items = (
                (_('Never'), constants.ANIMATION_DISABLED),
                (_('Normal'), constants.ANIMATION_NORMAL))

        selection = prefs['animation mode']

        box = self._create_combobox(items, selection, self._animation_mode_changed_cb)
        box.set_tooltip_text(
            _('Controls how animated images should be displayed.'))

        return box

    def _animation_mode_changed_cb(self, combobox, *args):
        """ Called whenever animation mode has been changed. """
        iter = combobox.get_active_iter()
        if combobox.get_model().iter_is_valid(iter):
            value = combobox.get_model().get_value(iter, 1)
            last_value = prefs['animation mode']
            prefs['animation mode'] = value

            if value != last_value:
                self._window.filehandler.refresh_file()

    def _create_combobox(self, options, selected_value, change_callback):
        """ Creates a new dropdown combobox and populates it with the items
        passed in C{options}.

        @param options: List of tuples: (Option display text, option value)
        @param selected_value: One of the values passed in C{options} that will
            be pre-selected when the control is created.
        @param change_callback: Function that will be called when the 'changed'
            event is triggered.
        @returns Gtk.ComboBox
        """
        assert options and len(options[0]) == 2, "Invalid format for options."

        # Use the first list item to determine typing of model fields.
        # First field is textual description, second field is value.
        model = Gtk.ListStore(GObject.TYPE_STRING, type(options[0][1]))
        for text, value in options:
            model.append((text, value))

        box = Gtk.ComboBox(model=model)
        renderer = Gtk.CellRendererText()
        box.pack_start(renderer, True)
        box.add_attribute(renderer, "text", 0)

        # Set active box option
        iter = model.get_iter_first()
        while iter:
            if model.get_value(iter, 1) == selected_value:
                box.set_active_iter(iter)
                break
            else:
                iter = model.iter_next(iter)

        if change_callback:
            box.connect('changed', change_callback)

        return box


    def _create_extensions_entry(self):
        entry = Gtk.Entry()
        entry.set_size_request(200, -1)
        entry.set_text(', '.join(prefs['comment extensions']))
        entry.connect('activate', self._entry_cb)
        entry.connect('focus_out_event', self._entry_cb)
        entry.set_tooltip_text(
            _('Treat all files found within archives, that have one of these file endings, as comments.'))
        return entry


    def _create_pref_check_button(self, label, prefkey, tooltip_text):
        button = Gtk.CheckButton(label)
        button.set_active(prefs[prefkey])
        button.connect('toggled', self._check_button_cb, prefkey)
        if tooltip_text:
            button.set_tooltip_text(tooltip_text)
        return button


    def _create_binary_pref_radio_buttons(self, label1, prefkey1, tooltip_text1,
        label2, prefkey2, tooltip_text2):
        button1 = Gtk.RadioButton(label=label1)
        button1.connect('toggled', self._check_button_cb, prefkey1)
        if tooltip_text1:
            button1.set_tooltip_text(tooltip_text1)
        button2 = Gtk.RadioButton(group=button1, label=label2)
        button2.connect('toggled', self._check_button_cb, prefkey2)
        if tooltip_text2:
            button2.set_tooltip_text(tooltip_text2)
        button2.set_active(prefs[prefkey2])
        return button1, button2


    def _create_color_button(self, prefkey):
        rgba = image_tools.color_to_floats_rgba(prefs[prefkey])
        button = Gtk.ColorButton.new_with_rgba(Gdk.RGBA(*rgba))
        button.connect('color_set', self._color_button_cb, prefkey)
        return button


    def _check_button_cb(self, button, preference):
        """Callback for all checkbutton-type preferences."""

        prefs[preference] = button.get_active()

        if preference == 'color box bg' and button.get_active():

            if not prefs['smart bg'] or not self._window.filehandler.file_loaded:
                self._window.set_bg_colour(prefs['bg colour'])

        elif preference == 'smart bg' and button.get_active():

            # if the color is no longer using the smart background then return it to the chosen color
            if not prefs[preference]:
                self._window.set_bg_colour(prefs['bg colour'])
            else:
                # draw_image() will set the main background to the smart background
                self._window.draw_image()

        elif preference == 'color box thumb bg' and button.get_active():

            if prefs[preference]:
                prefs['smart thumb bg'] = False
                prefs['thumbnail bg uses main colour'] = False

                self._window.thumbnailsidebar.change_thumbnail_background_color(prefs['thumb bg colour'])
            else:
                self._window.draw_image()

        elif preference == 'smart thumb bg' and button.get_active():

            if prefs[preference]:
                prefs['color box thumb bg'] = False
                prefs['thumbnail bg uses main colour'] = False

                if self._window.imagehandler.page_is_available():
                    pixbuf_count = 2 if self._window.displayed_double() else 1 # XXX limited to at most 2 pages
                    bg_colour = self._window.imagehandler.get_pixbuf_auto_background(pixbuf_count)
                    self._window.thumbnailsidebar.change_thumbnail_background_color(bg_colour)

            else:
                self._window.draw_image()

        elif preference in ('checkered bg for transparent images',
          'no double page for wide images', 'auto rotate from exif'):
            self._window.draw_image()

        elif (preference == 'hide all in fullscreen' and
            self._window.is_fullscreen):
            self._window.draw_image()

        elif preference == 'show page numbers on thumbnails':
            self._window.thumbnailsidebar.toggle_page_numbers_visible()

        elif preference == 'archive thumbnail as icon':
            self._window.update_icon(True)

    def _color_button_cb(self, colorbutton, preference):
        """Callback for the background colour selection button."""

        colour = colorbutton.get_color()

        if preference == 'bg colour':
            prefs['bg colour'] = colour.red, colour.green, colour.blue

            if not prefs['smart bg'] or not self._window.filehandler.file_loaded:
                self._window.set_bg_colour(prefs['bg colour'])

        elif preference == 'thumb bg colour':

            prefs['thumb bg colour'] = colour.red, colour.green, colour.blue

            if not prefs['smart thumb bg'] or not self._window.filehandler.file_loaded:
                self._window.thumbnailsidebar.change_thumbnail_background_color( prefs['thumb bg colour'] )


    def _create_pref_spinner(self, prefkey, scale, lower, upper, step_incr,
        page_incr, digits, tooltip_text):
        value = prefs[prefkey] / scale
        adjustment = Gtk.Adjustment(value, lower, upper, step_incr, page_incr)
        spinner = Gtk.SpinButton.new(adjustment, 0.0, digits)
        spinner.set_size_request(80, -1)
        spinner.connect('value_changed', self._spinner_cb, prefkey)
        if tooltip_text:
            spinner.set_tooltip_text(tooltip_text)
        return spinner


    def _spinner_cb(self, spinbutton, preference):
        """Callback for spinner-type preferences."""
        value = spinbutton.get_value()

        if preference == 'lens size':
            prefs[preference] = int(value)

        elif preference == 'lens magnification':
            prefs[preference] = value

        elif preference == 'slideshow delay':
            prefs[preference] = int(round(value * 1000))
            self._window.slideshow.update_delay()

        elif preference == 'number of pixels to scroll per slideshow event':
            prefs[preference] = int(value)

        elif preference == 'number of pixels to scroll per key event':
            prefs[preference] = int(value)

        elif preference == 'number of pixels to scroll per mouse wheel event':
            prefs[preference] = int(value)

        elif preference == 'smart scroll percentage':
            prefs[preference] = value / 100.0

        elif preference == 'thumbnail size':
            prefs[preference] = int(value)
            self._window.thumbnailsidebar.resize()
            self._window.draw_image()

        elif preference == 'max pages to cache':
            prefs[preference] = int(value)
            self._window.imagehandler.do_cacheing()

        elif preference == 'number of key presses before page turn':
            prefs['number of key presses before page turn'] = int(value)
            self._window._event_handler._extra_scroll_events = 0

        elif preference in ('fit to size width wide', 'fit to size height wide',
            'fit to size width other', 'fit to size height other',):
            prefs[preference] = int(value)
            self._window.change_zoom_mode()

        elif preference == 'max extract threads':
            prefs[preference] = int(value)

        elif preference == 'space between two pages':
            prefs[preference] = int(value)
            self._window.update_space()


    def _entry_cb(self, entry, event=None):
        """Callback for entry-type preferences."""
        text = entry.get_text()
        extensions = [e.strip() for e in text.split(',')]
        prefs['comment extensions'] = [e for e in extensions if e]
        self._window.filehandler.update_comment_extensions()

def open_dialog(action, window):
    """Create and display the preference dialog."""

    global _dialog

    # if the dialog window is not created then create the window
    if _dialog is None:
        _dialog = _PreferencesDialog(window)
    else:
        # if the dialog window already exists bring it to the forefront of the screen
        _dialog.present()

def _close_dialog():

    global _dialog

    # if the dialog window exists then destroy it
    if _dialog is not None:
        _dialog.destroy()
        _dialog = None


# vim: expandtab:sw=4:ts=4
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0
mcomix-3.1.0/mcomix/preferences_page.py0000644000175000017500000000311414476523373017661 0ustar00moritzmoritz"""preferences_page.py - MComix preference page."""

from gi.repository import Gtk

from mcomix import preferences_section

class _PreferencePage(Gtk.VBox):

    """The _PreferencePage is a conveniece class for making one "page"
    in a preferences-style dialog that contains one or more
    _PreferenceSections.
    """

    def __init__(self, right_column_width):
        """Create a new page where any possible right columns have the
        width request .
        """
        super(_PreferencePage, self).__init__(False, 12)
        self.set_border_width(12)
        self._right_column_width = right_column_width
        self._section = None

    def new_section(self, header):
        """Start a new section in the page, with the header text from
        
. """ self._section = preferences_section._PreferenceSection(header, self._right_column_width) self.pack_start(self._section, False, False, 0) def add_row(self, left_item, right_item=None): """Add a row to the page (in the latest section), containing one or two items. If the left item is a label it is automatically aligned properly. """ if isinstance(left_item, Gtk.Label): left_item.set_alignment(0, 0.5) if right_item is None: self._section.contentbox.pack_start(left_item, True, True, 0) else: left_box, right_box = self._section.new_split_vboxes() left_box.pack_start(left_item, True, True, 0) right_box.pack_start(right_item, True, True, 0) # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0 mcomix-3.1.0/mcomix/preferences_section.py0000644000175000017500000000361414476523373020416 0ustar00moritzmoritz"""preferences_section.py - Preference dialog section.""" from gi.repository import Gtk from mcomix import labels class _PreferenceSection(Gtk.VBox): """The _PreferenceSection is a convenience class for making one "section" of a preference-style dialog, e.g. it has a bold header and a number of rows which are indented with respect to that header. """ def __init__(self, header, right_column_width): """Contruct a new section with the header set to the text in
, and the width request of the (possible) right columns set to that of . """ super(_PreferenceSection, self).__init__(False, 0) self._right_column_width = right_column_width self.contentbox = Gtk.VBox(False, 6) label = labels.BoldLabel(header) label.set_alignment(0, 0.5) hbox = Gtk.HBox(False, 0) hbox.pack_start(Gtk.HBox(True, True, 0), False, False, 6) hbox.pack_start(self.contentbox, True, True, 0) self.pack_start(label, False, False, 0) self.pack_start(hbox, False, False, 6) def new_split_vboxes(self): """Return two new VBoxes that are automatically put in the section after the previously added items. The right one has a width request equal to the right_column_width value passed to the class contructor, in order to make it easy for all "right column items" in a page to line up nicely. """ left_box = Gtk.VBox(False, 6) right_box = Gtk.VBox(False, 6) if self._right_column_width != None: right_box.set_size_request(self._right_column_width, -1) hbox = Gtk.HBox(False, 12) hbox.pack_start(left_box, True, True, 0) hbox.pack_start(right_box, False, False, 0) self.contentbox.pack_start(hbox, True, True, 0) return left_box, right_box # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0 mcomix-3.1.0/mcomix/process.py0000644000175000017500000001435714476523373016055 0ustar00moritzmoritz"""process.py - Process spawning module.""" import sys import os import subprocess from mcomix import i18n NULL = open(os.devnull, 'r+b') PIPE = subprocess.PIPE STDOUT = subprocess.STDOUT # Convert argument vector to system's file encoding where necessary # to prevent automatic conversion when appending Unicode strings # to byte strings later on. def _fix_args(args): fixed_args = [] for arg in args: if isinstance(arg, str): fixed_args.append(arg.encode(sys.getfilesystemencoding())) else: fixed_args.append(arg) return fixed_args def _get_creationflags(): if 'win32' == sys.platform: # Do not create a console window. return 0x08000000 else: return 0 # Cannot spawn processes with PythonW/Win32 unless stdin # and stderr are redirected to a pipe/devnull as well. def call(args, stdin=NULL, stdout=NULL, stderr=NULL): return 0 == subprocess.call(_fix_args(args), stdin=stdin, stdout=stdout, stderr=stderr, creationflags=_get_creationflags()) def popen(args, stdin=NULL, stdout=PIPE, stderr=NULL): return subprocess.Popen(_fix_args(args), stdin=stdin, stdout=stdout, stderr=stderr, creationflags=_get_creationflags()) if 'win32' == sys.platform: _exe_dir = os.path.dirname(os.path.abspath(sys.argv[0])) def find_executable(candidates, workdir=None, is_valid_candidate=None): """ Find executable in path. Return an absolute path to a valid executable or None. default to the current working directory if not set. is an optional function that must return True if the path passed in argument is a valid candidate (to check for version number, symlinks to an unsupported variant, etc...). If a candidate has a directory component, it will be checked relative to . On Windows: - '.exe' will be appended to each candidate if not already - MComix executable directory is prepended to the path on Windows (to support embedded tools/executables in the distribution). - will be inserted first in the path. On Unix: - a valid candidate must have execution right """ if workdir is None: workdir = os.getcwd() workdir = os.path.abspath(workdir) search_path = os.environ['PATH'].split(os.pathsep) if 'win32' == sys.platform: if workdir is not None: search_path.insert(0, workdir) search_path.insert(0, _exe_dir) is_valid_exe = lambda exe: \ os.path.isfile(exe) and \ os.access(exe, os.R_OK|os.X_OK) if is_valid_candidate is None: is_valid = is_valid_exe else: is_valid = lambda exe: \ is_valid_exe(exe) and \ is_valid_candidate(exe) for name in candidates: # On Windows, must end with '.exe' if 'win32' == sys.platform: if not name.endswith('.exe'): name = name + '.exe' # Absolute path? if os.path.isabs(name): if is_valid(name): return name # Does candidate have a directory component? elif os.path.dirname(name): # Yes, check relative to working directory. path = os.path.normpath(os.path.join(workdir, name)) if is_valid(path): return path # Look in search path. else: for dir in search_path: path = os.path.abspath(os.path.join(dir, name)) if is_valid(path): return path return None def Win32Popen(cmd): """ Spawns a new process on Win32. cmd is a list of parameters. This method's sole purpose is calling CreateProcessW, not CreateProcessA as it is done by subprocess.Popen. """ import ctypes # Declare common data types DWORD = ctypes.c_uint WORD = ctypes.c_ushort LPTSTR = ctypes.c_wchar_p LPBYTE = ctypes.POINTER(ctypes.c_ubyte) HANDLE = ctypes.c_void_p class StartupInfo(ctypes.Structure): _fields_ = [("cb", DWORD), ("lpReserved", LPTSTR), ("lpDesktop", LPTSTR), ("lpTitle", LPTSTR), ("dwX", DWORD), ("dwY", DWORD), ("dwXSize", DWORD), ("dwYSize", DWORD), ("dwXCountChars", DWORD), ("dwYCountChars", DWORD), ("dwFillAttribute", DWORD), ("dwFlags", DWORD), ("wShowWindow", WORD), ("cbReserved2", WORD), ("lpReserved2", LPBYTE), ("hStdInput", HANDLE), ("hStdOutput", HANDLE), ("hStdError", HANDLE)] class ProcessInformation(ctypes.Structure): _fields_ = [("hProcess", HANDLE), ("hThread", HANDLE), ("dwProcessId", DWORD), ("dwThreadId", DWORD)] LPSTRARTUPINFO = ctypes.POINTER(StartupInfo) LPROCESS_INFORMATION = ctypes.POINTER(ProcessInformation) ctypes.windll.kernel32.CreateProcessW.argtypes = [LPTSTR, LPTSTR, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_bool, DWORD, ctypes.c_void_p, LPTSTR, LPSTRARTUPINFO, LPROCESS_INFORMATION] ctypes.windll.kernel32.CreateProcessW.restype = ctypes.c_bool # Convert list of arguments into a single string cmdline = subprocess.list2cmdline(cmd) buffer = ctypes.create_unicode_buffer(cmdline) # Resolve executable path. exe = find_executable((cmd[0],)) # Some required structures for the method call... startupinfo = StartupInfo() ctypes.memset(ctypes.addressof(startupinfo), 0, ctypes.sizeof(startupinfo)) startupinfo.cb = ctypes.sizeof(startupinfo) processinfo = ProcessInformation() # Spawn new process success = ctypes.windll.kernel32.CreateProcessW(exe, buffer, None, None, False, 0, None, None, ctypes.byref(startupinfo), ctypes.byref(processinfo)) if success: ctypes.windll.kernel32.CloseHandle(processinfo.hProcess) ctypes.windll.kernel32.CloseHandle(processinfo.hThread) return processinfo.dwProcessId else: raise ctypes.WinError(ctypes.GetLastError(), i18n.to_unicode(ctypes.FormatError())) # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1698566559.0 mcomix-3.1.0/mcomix/properties_dialog.py0000644000175000017500000001143514517410637020076 0ustar00moritzmoritz"""properties_dialog.py - Properties dialog that displays information about the archive/file.""" from gi.repository import Gtk import os import time import stat try: import pwd _has_pwd = True except ImportError: # Running on non-Unix machine. _has_pwd = False from mcomix import i18n from mcomix import strings from mcomix import properties_page from mcomix import tools from mcomix.i18n import _ class _PropertiesDialog(Gtk.Dialog): def __init__(self, window): super(_PropertiesDialog, self).__init__(_('Properties'), window, 0, (Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE)) self._window = window self.resize(500, 430) self.set_resizable(True) self.set_default_response(Gtk.ResponseType.CLOSE) notebook = Gtk.Notebook() self.set_border_width(4) notebook.set_border_width(6) self.vbox.pack_start(notebook, True, True, 0) self._archive_page = properties_page._Page() notebook.append_page(self._archive_page, Gtk.Label(label=_('Archive'))) self._image_page = properties_page._Page() notebook.append_page(self._image_page, Gtk.Label(label=_('Image'))) self._update_archive_page() self._window.page_changed += self._on_page_change self._window.filehandler.file_opened += self._on_book_change self._window.filehandler.file_closed += self._on_book_change self._window.imagehandler.page_available += self._on_page_available self.show_all() def _on_page_change(self): self._update_image_page() def _on_book_change(self): self._update_archive_page() def _on_page_available(self, page_number): if 1 == page_number: self._update_page_image(self._archive_page, 1) current_page_number = self._window.imagehandler.get_current_page() if current_page_number == page_number: self._update_image_page() def _update_archive_page(self): self._update_image_page() page = self._archive_page page.reset() window = self._window if window.filehandler.archive_type is None: return # In case it's not ready yet, bump the cover extraction # in front of the queue. path = window.imagehandler.get_path_to_page(1) if path is not None: window.filehandler._ask_for_files([path]) self._update_page_image(page, 1) filename = window.filehandler.get_pretty_current_filename() page.set_filename(filename) path = window.filehandler.get_path_to_base() main_info = ( _('%d pages') % window.imagehandler.get_number_of_pages(), _('%d comments') % window.filehandler.get_number_of_comments(), strings.ARCHIVE_DESCRIPTIONS[window.filehandler.archive_type] ) page.set_main_info(main_info) self._update_page_secondary_info(page, path) page.show_all() def _update_image_page(self): page = self._image_page page.reset() window = self._window if not window.imagehandler.page_is_available(): return self._update_page_image(page) path = window.imagehandler.get_path_to_page() filename = os.path.basename(path) page.set_filename(filename) width, height = window.imagehandler.get_size() main_info = ( '%dx%d px' % (width, height), window.imagehandler.get_mime_name(), ) page.set_main_info(main_info) self._update_page_secondary_info(page, path) page.show_all() def _update_page_image(self, page, page_number=None): if not self._window.imagehandler.page_is_available(page_number): return thumb = self._window.imagehandler.get_thumbnail(page_number, width=128, height=128) page.set_thumbnail(thumb) def _update_page_secondary_info(self, page, location): secondary_info = [ (_('Location'), i18n.to_display_string(i18n.to_unicode(os.path.dirname(location)))), ] try: stats = os.stat(location) except OSError as e: page.set_secondary_info(secondary_info) return if _has_pwd: uid = pwd.getpwuid(stats.st_uid)[0] else: uid = str(stats.st_uid) secondary_info.extend(( (_('Size'), tools.format_byte_size(stats.st_size)), (_('Accessed'), time.strftime('%Y-%m-%d, %H:%M:%S', time.localtime(stats.st_atime))), (_('Modified'), time.strftime('%Y-%m-%d, %H:%M:%S', time.localtime(stats.st_mtime))), (_('Permissions'), oct(stat.S_IMODE(stats.st_mode))), (_('Owner'), uid) )) page.set_secondary_info(secondary_info) # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1698566559.0 mcomix-3.1.0/mcomix/properties_page.py0000644000175000017500000000667614517410637017566 0ustar00moritzmoritz"""properties_page.py - A page to put in the properties dialog window.""" from gi.repository import Gtk from mcomix import i18n from mcomix import image_tools from mcomix import labels class _Page(Gtk.ScrolledWindow): """A page to put in the Gtk.Notebook. Contains info about a file (an image or an archive.) """ def __init__(self): super(_Page, self).__init__() self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self._vbox = Gtk.VBox(False, 12) self.add_with_viewport(self._vbox) self.set_border_width(12) topbox = Gtk.HBox(False, 12) self._vbox.pack_start(topbox, True, True, 0) self._thumb = Gtk.Image() self._thumb.set_size_request(128, 128) topbox.pack_start(self._thumb, False, False, 0) borderbox = Gtk.Frame() borderbox.set_shadow_type(Gtk.ShadowType.ETCHED_IN) borderbox.set_size_request(-1, 130) topbox.pack_start(borderbox, True, True, 0) insidebox = Gtk.EventBox() insidebox.set_border_width(1) insidebox.set_state(Gtk.StateType.ACTIVE) borderbox.add(insidebox) self._insidebox = insidebox self._mainbox = None self._extrabox = None self.reset() def reset(self): self._thumb.clear() if self._mainbox is not None: self._mainbox.destroy() self._mainbox = Gtk.VBox(False, 5) self._mainbox.set_border_width(10) self._insidebox.add(self._mainbox) if self._extrabox is not None: self._extrabox.destroy() self._extrabox = Gtk.HBox(False, 10) self._vbox.pack_start(self._extrabox, False, False, 0) def set_thumbnail(self, pixbuf): pixbuf = image_tools.add_border(pixbuf, 1) self._thumb.set_from_pixbuf(pixbuf) def set_filename(self, filename): """Set the filename to be displayed to . Call this before set_main_info(). """ label = labels.BoldLabel(i18n.to_display_string(i18n.to_unicode(filename))) label.set_alignment(0, 0.5) label.set_selectable(True) self._mainbox.pack_start(label, False, False, 0) self._mainbox.pack_start(Gtk.VBox(True, True, 0), True, True, 0) # Just to add space (better way?) def set_main_info(self, info): """Set the information in the main info box (below the filename) to the values in the sequence . """ for text in info: label = Gtk.Label(label=text) label.set_alignment(0, 0.5) label.set_selectable(True) self._mainbox.pack_start(label, False, False, 0) def set_secondary_info(self, info): """Set the information below the main info box to the values in the sequence . Each entry in info should be a tuple (desc, value). """ left_box = Gtk.VBox(True, 8) right_box = Gtk.VBox(True, 8) self._extrabox.pack_start(left_box, False, False, 0) self._extrabox.pack_start(right_box, False, False, 0) for desc, value in info: desc_label = labels.BoldLabel('%s:' % desc) desc_label.set_alignment(1.0, 1.0) left_box.pack_start(desc_label, True, True, 0) value_label = Gtk.Label(label=value) value_label.set_alignment(0, 1.0) value_label.set_selectable(True) right_box.pack_start(value_label, True, True, 0) # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0 mcomix-3.1.0/mcomix/recent.py0000644000175000017500000000475614476523373015661 0ustar00moritzmoritz"""recent.py - Recent files handler.""" import urllib.request, urllib.parse, urllib.error from gi.repository import Gtk, GLib, GObject import sys from mcomix import preferences from mcomix import i18n from mcomix import portability from mcomix import archive_tools from mcomix import image_tools from mcomix import log class RecentFilesMenu(Gtk.RecentChooserMenu): def __init__(self, ui, window): super(RecentFilesMenu, self).__init__() self._window = window self._manager = Gtk.RecentManager.get_default() self.set_sort_type(Gtk.RecentSortType.MRU) self.set_show_tips(True) # Missing icons crash GTK on Win32 if sys.platform == 'win32': self.set_show_icons(False) self.set_show_numbers(True) rfilter = Gtk.RecentFilter() supported_formats = {} supported_formats.update(image_tools.get_supported_formats()) supported_formats.update(archive_tools.get_supported_formats()) for name in sorted(supported_formats): mime_types, extensions = supported_formats[name] patterns = ['*.%s' % ext for ext in extensions] for mime in mime_types: rfilter.add_mime_type(mime) for pat in patterns: rfilter.add_pattern(pat) self.add_filter(rfilter) self.connect('item_activated', self._load) def _load(self, *args): uri = self.get_current_uri() path = urllib.request.url2pathname(uri[7:]) did_file_load = self._window.filehandler.open_file(path) if not did_file_load: self.remove(path) def count(self): """ Returns the amount of stored entries. """ return len(self._manager.get_items()) def add(self, path): if not preferences.prefs['store recent file info']: return uri = portability.uri_prefix() + urllib.request.pathname2url(path) self._manager.add_item(uri) def remove(self, path): if not preferences.prefs['store recent file info']: return uri = portability.uri_prefix() + urllib.request.pathname2url(path) try: self._manager.remove_item(uri) except GLib.GError: # Could not remove item pass def remove_all(self): """ Removes all entries to recently opened files. """ try: self._manager.purge_items() except GObject.GError as error: log.debug(error) # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1699196178.0 mcomix-3.1.0/mcomix/run.py0000644000175000017500000002124414521726422015163 0ustar00moritzmoritz import os import sys import optparse import signal if __name__ == '__main__': print('PROGRAM TERMINATED', file=sys.stderr) print('Please do not run this script directly! Use the mcomix script or mcomixstarter.py instead.', file=sys.stderr) sys.exit(1) # These modules must not depend on GTK, Pillow, # or any other optional libraries. from mcomix import ( constants, log, portability, preferences, ) def wait_and_exit(): """ Wait for the user pressing ENTER before closing. This should help the user find possibly missing dependencies when starting, since the Python window will not close down immediately after the error. """ if sys.platform == 'win32' and not sys.stdin.closed and not sys.stdout.closed: print() input("Press ENTER to continue...") sys.exit(1) def print_version(opt, value, parser, *args, **kwargs): """Print the version number and exit.""" print(constants.APPNAME + ' ' + constants.VERSION) sys.exit(0) def parse_arguments(argv): """ Parse the command line passed in . Returns a tuple containing (options, arguments). Errors parsing the command line are handled in this function. """ from mcomix.i18n import _ parser = optparse.OptionParser( usage="%%prog %s" % _('[OPTION...] [PATH]'), description=_('View images and comic book archives.'), add_help_option=False) parser.add_option('--help', action='help', help=_('Show this help and exit.')) parser.add_option('-s', '--slideshow', dest='slideshow', action='store_true', help=_('Start the application in slideshow mode.')) parser.add_option('-l', '--library', dest='library', action='store_true', help=_('Show the library on startup.')) parser.add_option('-v', '--version', action='callback', callback=print_version, help=_('Show the version number and exit.')) parser.add_option('--lang', dest='language_code', action='store', help=_('Temporarily override the interface language.')) viewmodes = optparse.OptionGroup(parser, _('View modes')) viewmodes.add_option('-f', '--fullscreen', dest='fullscreen', action='store_true', help=_('Start the application in fullscreen mode.')) viewmodes.add_option('-m', '--manga', dest='manga', action='store_true', help=_('Start the application in manga mode.')) viewmodes.add_option('-d', '--double-page', dest='doublepage', action='store_true', help=_('Start the application in double page mode.')) parser.add_option_group(viewmodes) fitmodes = optparse.OptionGroup(parser, _('Zoom modes')) fitmodes.add_option('-b', '--zoom-best', dest='zoommode', action='store_const', const=constants.ZoomMode.BEST, help=_('Start the application with zoom set to best fit mode.')) fitmodes.add_option('-w', '--zoom-width', dest='zoommode', action='store_const', const=constants.ZoomMode.WIDTH, help=_('Start the application with zoom set to fit width.')) fitmodes.add_option('-h', '--zoom-height', dest='zoommode', action='store_const', const=constants.ZoomMode.HEIGHT, help=_('Start the application with zoom set to fit height.')) parser.add_option_group(fitmodes) debugopts = optparse.OptionGroup(parser, _('Debug options')) debugopts.add_option('-W', dest='loglevel', action='store', choices=('all', 'debug', 'info', 'warn', 'error'), default='warn', metavar='[ all | debug | info | warn | error ]', help=_('Sets the desired output log level.')) # This supresses an error when MComix is used with cProfile debugopts.add_option('-o', dest='output', action='store', default='', help=optparse.SUPPRESS_HELP) parser.add_option_group(debugopts) opts, args = parser.parse_args(argv) # Fix up log level to use constants from log. if opts.loglevel == 'all': opts.loglevel = log.DEBUG if opts.loglevel == 'debug': opts.loglevel = log.DEBUG if opts.loglevel == 'info': opts.loglevel = log.INFO elif opts.loglevel == 'warn': opts.loglevel = log.WARNING elif opts.loglevel == 'error': opts.loglevel = log.ERROR return opts, args def setup_dependencies(): """Check for PyGTK and PIL dependencies.""" from mcomix.i18n import _ try: from gi import require_version require_version('PangoCairo', '1.0') require_version('Gtk', '3.0') require_version('Gdk', '3.0') from gi.repository import Gdk, GLib, Gtk # noqa # Older GLib requires initialization before using threads if GLib.check_version(2, 32, 0) is not None: GLib.threads_init() except AssertionError: log.error(_("You do not have the required versions of GTK+ 3.0 and PyGObject installed.")) wait_and_exit() except ImportError: log.error(_('No version of GObject was found on your system.')) log.error(_('This error might be caused by missing GTK+ libraries.')) wait_and_exit() try: import PIL.Image try: pil_major_version = int(PIL.__version__[0:PIL.__version__.index('.')]) except (ValueError, IndexError): pil_major_version = 0 if pil_major_version < 6: log.error(_("You don't have the required version of the Python Imaging Library Fork (Pillow) installed.")) log.error(_('Installed Pillow version is: %s') % PIL.__version__) log.error(_('Required Pillow version is: 6.0.0 or higher')) wait_and_exit() except ImportError: log.error(_('Python Imaging Library Fork (Pillow) 6.0.0 or higher is required.')) log.error(_('No version of the Python Imaging Library was found on your system.')) wait_and_exit() def run(): """Run the program.""" # Load configuration and setup localisation. preferences.read_preferences_file() from mcomix import i18n i18n.install_gettext() # Retrieve and parse command line arguments. argv = sys.argv[1:] opts, args = parse_arguments(argv) # First things first: set the log level. log.setLevel(opts.loglevel) # Reconfigure stdout to replace characters that cannot be printed if hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(errors='replace') if opts.language_code: i18n.install_gettext(opts.language_code) setup_dependencies() from gi.repository import Gdk, GLib, Gtk if not os.path.exists(constants.DATA_DIR): os.makedirs(constants.DATA_DIR, 0o700) if not os.path.exists(constants.CONFIG_DIR): os.makedirs(constants.CONFIG_DIR, 0o700) from mcomix import icons icons.load_icons() open_path = None open_page = 1 if len(args) == 1: open_path = args[0] elif len(args) > 1: open_path = args elif preferences.prefs['auto load last file'] \ and preferences.prefs['path to last file'] \ and os.path.isfile(preferences.prefs['path to last file']): open_path = preferences.prefs['path to last file'] open_page = preferences.prefs['page of last file'] # Some languages require a RTL layout if preferences.prefs['language'] in ('he', 'fa'): Gtk.widget_set_default_direction(Gtk.TextDirection.RTL) Gdk.set_program_class(constants.APPNAME) GLib.set_prgname(constants.APPNAME) settings = Gtk.Settings.get_default() if settings: # Enable icons for menu items. settings.props.gtk_menu_images = True # Prefer dark theme if system theme mode is set to dark if portability.is_system_ui_dark_themed() == constants.SystemThemeLightness.DARK: settings.set_property('gtk-application-prefer-dark-theme', True) from mcomix import main window = main.MainWindow(fullscreen = opts.fullscreen, is_slideshow = opts.slideshow, show_library = opts.library, manga_mode = opts.manga, double_page = opts.doublepage, zoom_mode = opts.zoommode, open_path = open_path, open_page = open_page) main.set_main_window(window) if 'win32' != sys.platform: # Add a SIGCHLD handler to reap zombie processes. def on_sigchld(signum, frame): try: os.waitpid(-1, os.WNOHANG) except OSError: pass signal.signal(signal.SIGCHLD, on_sigchld) for sig in (signal.SIGINT, signal.SIGTERM): signal.signal(sig, lambda signum, stack: GLib.idle_add(window.terminate_program)) try: Gtk.main() except KeyboardInterrupt: # Will not always work because of threading. window.terminate_program() # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0 mcomix-3.1.0/mcomix/scrolling.py0000644000175000017500000002423514476523373016367 0ustar00moritzmoritz""" Smart scrolling. """ from mcomix import tools from mcomix import constants from mcomix import box import math class Scrolling(object): def __init__(self): self.clear_cache() def scroll_smartly(self, content_box, viewport_box, orientation, max_scroll, axis_map=None): """ Returns a new viewport position when reading forwards using the given orientation. If there is no space left to go, the empty list is returned. Note that all params are lists of ints (except max_scroll which might also contain floats) where each index corresponds to one dimension. The lower the index, the faster the corresponding position changes when reading. If you need to override this behavior, use the optional axis_map. @param content_box: The Box of the content to display. @param viewport_box: The viewport Box we are looking through. @param orientation: The orientation which shows where "forward" points to. Either 1 (towards larger values in this dimension when reading) or -1 (towards smaller values in this dimension when reading). Note that you can emulate "reading backwards" by flipping the sign of this argument. @param max_scroll: The maximum number of pixels to scroll in one step. (Floats allowed.) @param axis_map: The index of the dimension to modify. @return: A new viewport_position if you can read further or the empty list if there is nothing left to read. """ # Translate content and viewport so that content position equals origin offset = content_box.get_position() content_size = content_box.get_size() viewport_position = tools.vector_sub(viewport_box.get_position(), offset) viewport_size = viewport_box.get_size() # Remap axes if axis_map is not None: content_size, viewport_size, viewport_position, orientation, \ max_scroll = list(map(lambda v: tools.remap_axes(v, axis_map), [content_size, viewport_size, viewport_position, orientation, max_scroll])) result = list(viewport_position) carry = True reset_all_axes = False for i in range(len(content_size)): invisible_size = content_size[i] - viewport_size[i] o = orientation[i] # Find a nice starting point if o == 1: if viewport_position[i] < 0: result[i] = 0 carry = False if viewport_position[i] <= -viewport_size[i]: reset_all_axes = True break else: # o == -1 if viewport_position[i] > invisible_size: result[i] = invisible_size carry = False if viewport_position[i] > content_size[i]: reset_all_axes = True break if reset_all_axes: # We don't see anything at all because we are somewhere way before # the content box. Let's go to it. for i in range(len(content_size)): invisible_size = content_size[i] - viewport_size[i] o = orientation[i] if o == 1: result[i] = 0 else: # o == -1 result[i] = invisible_size # This code is somewhat similar to a simple ripple-carry adder. if carry: for i in range(len(content_size)): invisible_size = content_size[i] - viewport_size[i] o = orientation[i] ms = min(max_scroll[i], invisible_size) # Let's calculate the grid we want to snap to. if ms != 0: steps_to_take = int(math.ceil(float(invisible_size) / ms)) if ms == 0 or steps_to_take >= invisible_size: # special case: We MUST go forward by at least 1 pixel. if o >= 0: result[i] += 1 carry = result[i] > invisible_size if carry: result[i] = 0 continue else: result[i] -= 1 carry = result[i] < 0 if carry: result[i] = invisible_size continue break # If orientation is -1, we need to round half up instead of # half down. positions = self._cached_bs(invisible_size, steps_to_take, o == -1) # Where are we now (according to the grid)? index = tools.bin_search(positions, viewport_position[i]) if index < 0: # We're somewhere between two valid grid points, so # let's go to the next one. index = ~index if o >= 0: # index tends to be greater, so we need to go back # manually, if needed. index -= 1 # Let's go to where we're headed for. index += o carry = index < 0 or index >= len(positions) if carry: # There is no space left in this dimension, so let's go # back in this one and one step forward in the next one. result[i] = 0 if o > 0 else invisible_size else: # We found a valid grid point in this dimension, so let's # stop here. result[i] = positions[index] break if carry: # No space left. return [] # Undo axis remapping, if any if axis_map is not None: result = tools.remap_axes(result, tools.inverse_axis_map(axis_map)) return tools.vector_add(result, offset) def scroll_to_predefined(self, content_box, viewport_box, orientation, destination): """ Returns a new viewport position when scrolling towards a predefined destination. Note that all params are lists of integers where each index corresponds to one dimension. @param content_box: The Box of the content to display. @param viewport_box: The viewport Box we are looking through. @param orientation: The orientation which shows where "forward" points to. Either 1 (towards larger values in this dimension when reading) or -1 (towards smaller values in this dimension when reading). @param destination: An integer representing a predefined destination. Either 1 (towards the greatest possible values in this dimension), -1 (towards the smallest value in this dimension), 0 (keep position), SCROLL_TO_CENTER (scroll to the center of the content in this dimension), SCROLL_TO_START (scroll to where the content starts in this dimension) or SCROLL_TO_END (scroll to where the content ends in this dimension). @return: A new viewport position as specified above. """ content_position = content_box.get_position() content_size = content_box.get_size() viewport_size = viewport_box.get_size() result = list(viewport_box.get_position()) for i in range(len(content_size)): o = orientation[i] d = destination[i] if d == 0: continue if d < constants.SCROLL_TO_END or d > 1: raise ValueError("invalid destination " + d + " at index "+ i) if d == constants.SCROLL_TO_END: d = o if d == constants.SCROLL_TO_START: d = -o c = content_size[i] v = viewport_size[i] invisible_size = c - v result[i] = content_position[i] + (box.Box._box_to_center_offset_1d( invisible_size, o) if d == constants.SCROLL_TO_CENTER else invisible_size if d == 1 else 0) # if d == -1 return result def _cached_bs(self, num, denom, half_up): """ A simple (and ugly) caching mechanism used to avoid recomputations. The current implementation offers a cache with only two entries so it's only useful for the two "fastest" dimensions. """ if (self._cache0[0] != num or self._cache0[1] != denom or self._cache0[2] != half_up): self._cache0, self._cache1 = self._cache1, self._cache0 if (self._cache0[0] != num or self._cache0[1] != denom or self._cache0[2] != half_up): self._cache0 = (num, denom, half_up, Scrolling._bresenham_sums(num, denom, half_up)) return self._cache0[3] def clear_cache(self): """ Clears all caches that are used internally. """ self._cache0 = (0, 0, False, []) self._cache1 = (0, 0, False, []) @staticmethod def _bresenham_sums(num, denom, half_up): """ This algorithm is derived from Bresenham's line algorithm in order to distribute the remainder of num/denom equally. See https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm for details. """ if num < 0: raise ValueError("num < 0") if denom < 1: raise ValueError("denom < 1") quotient = num // denom remainder = num % denom needs_up = half_up and (remainder != 0) and ((denom & 1) == 0) up_flag = False error = denom >> 1 result = [0] partial_sum = 0 for i in range(denom): error -= remainder if error < 0: error += denom partial_sum += quotient + 1 else: partial_sum += quotient # round half up, if necessary if up_flag: partial_sum -= 1 up_flag = False elif needs_up and error == 0: partial_sum += 1 up_flag = True result.append(partial_sum) return result # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0 mcomix-3.1.0/mcomix/slideshow.py0000644000175000017500000000357614476523373016401 0ustar00moritzmoritz"""slideshow.py - Slideshow handler.""" from gi.repository import Gtk, GLib from mcomix.preferences import prefs from mcomix.i18n import _ class Slideshow(object): """Slideshow handler that manages starting and stopping of slideshows.""" def __init__(self, window): self._window = window self._running = False self._id = None def _start(self): if not self._running: self._id = GLib.timeout_add(prefs['slideshow delay'], self._next) self._running = True self._window.update_title() def _stop(self): if self._running: GLib.source_remove(self._id) self._running = False self._window.update_title() def _next(self): if prefs['number of pixels to scroll per slideshow event'] != 0: self._window.scroll_with_flipping(0, prefs['number of pixels to scroll per slideshow event']) else: self._window.flip_page(+1) return True def toggle(self, action): """Toggle a slideshow on or off.""" if action.get_active(): self._start() self._window.uimanager.get_widget('/Tool/slideshow').set_stock_id( Gtk.STOCK_MEDIA_STOP ) self._window.uimanager.get_widget('/Tool/slideshow').set_tooltip_text( _('Stop slideshow') ) else: self._stop() self._window.uimanager.get_widget('/Tool/slideshow').set_stock_id( Gtk.STOCK_MEDIA_PLAY ) self._window.uimanager.get_widget('/Tool/slideshow').set_tooltip_text( _('Start slideshow') ) def is_running(self): """Return True if a slideshow is currently running.""" return self._running def update_delay(self): """Update the delay time a started slideshow is using.""" if self.is_running(): self._stop() self._start() # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1698566559.0 mcomix-3.1.0/mcomix/status.py0000644000175000017500000002277014517410637015712 0ustar00moritzmoritz"""status.py - Statusbar for main window.""" from gi.repository import Gdk, Gtk from mcomix import i18n from mcomix import constants from mcomix.preferences import prefs from mcomix.i18n import _ class Statusbar(Gtk.EventBox): SPACING = 5 def __init__(self): super(Statusbar, self).__init__() self._loading = True # Status text, page number, file number, resolution, path, filename, filesize self.status = Gtk.Statusbar() self.add(self.status) # Create popup menu for enabling/disabling status boxes. self.ui_manager = Gtk.UIManager() self.tooltipstatus = TooltipStatusHelper(self.ui_manager, self.status) ui_description = """ """ self.ui_manager.add_ui_from_string(ui_description) actiongroup = Gtk.ActionGroup('mcomix-statusbar') actiongroup.add_toggle_actions([ ('pagenumber', None, _('Show page numbers'), None, None, self.toggle_status_visibility), ('filenumber', None, _('Show file numbers'), None, None, self.toggle_status_visibility), ('resolution', None, _('Show resolution'), None, None, self.toggle_status_visibility), ('rootpath', None, _('Show path'), None, None, self.toggle_status_visibility), ('filename', None, _('Show filename'), None, None, self.toggle_status_visibility), ('filesize', None, _('Show filesize'), None, None, self.toggle_status_visibility)]) self.ui_manager.insert_action_group(actiongroup, 0) # Hook mouse release event self.connect('button-release-event', self._button_released) self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK|Gdk.EventMask.BUTTON_RELEASE_MASK) # Default status information self._page_info = '' self._file_info = '' self._resolution = '' self._root = '' self._filename = '' self._filesize = '' self._update_sensitivity() self.show_all() self._loading = False def set_message(self, message): """Set a specific message (such as an error message) on the statusbar, replacing whatever was there earlier. """ self.status.pop(0) self.status.push(0, " " * Statusbar.SPACING + message) def set_page_number(self, page, total, this_screen): """Update the page number.""" page_info = "" for i in range(this_screen): page_info += '%d' % (page + i) if i < this_screen - 1: page_info +=',' page_info += ' / %d' % total self._page_info = page_info def get_page_number(self): """Returns the bar's page information.""" return self._page_info def set_file_number(self, fileno, total): """Updates the file number (i.e. number of current file/total files loaded).""" if total > 0: self._file_info = '(%d / %d)' % (fileno, total) else: self._file_info = '' def get_file_number(self): """ Returns the bar's file information.""" return self._file_info def set_resolution(self, dimensions): # 2D only """Update the resolution data. Takes an iterable of tuples, (x, y, scale, distorted), describing the original resolution of an image as well as the currently displayed scale and whether scaling was done irrespective of aspect ratio, resulting in a distorted image. """ resolution = "" for i in range(len(dimensions)): d = dimensions[i] resolution += '%dx%d (%.1f%%%s)' % (d[0], d[1], d[2] * 100.0, "*" if d[3] else "") if i < len(dimensions) - 1: resolution += ', ' self._resolution = resolution def set_root(self, root): """Set the name of the root (directory or archive).""" self._root = i18n.to_display_string(i18n.to_unicode(root)) def set_filename(self, filename): """Update the filename.""" self._filename = i18n.to_display_string(i18n.to_unicode(filename)) def set_filesize(self, size): """Update the filesize.""" if size is None: size = "" self._filesize = size def update(self): """Set the statusbar to display the current state.""" space = " " * Statusbar.SPACING text = (space + "|" + space).join(self._get_status_text()) self.status.pop(0) self.status.push(0, space + text) def push(self, context_id, message): """ Compatibility with Gtk.Statusbar. """ assert context_id >= 0 self.status.push(context_id + 1, message) def pop(self, context_id): """ Compatibility with Gtk.Statusbar. """ assert context_id >= 0 self.status.pop(context_id + 1) def _get_status_text(self): """ Returns an array of text fields that should be displayed. """ fields = [] if prefs['statusbar fields'] & constants.STATUS_PAGE: fields.append(self._page_info) if prefs['statusbar fields'] & constants.STATUS_FILENUMBER: fields.append(self._file_info) if prefs['statusbar fields'] & constants.STATUS_RESOLUTION: fields.append(self._resolution) if prefs['statusbar fields'] & constants.STATUS_PATH: fields.append(self._root) if prefs['statusbar fields'] & constants.STATUS_FILENAME: fields.append(self._filename) if prefs['statusbar fields'] & constants.STATUS_FILESIZE: fields.append(self._filesize) return fields def toggle_status_visibility(self, action, *args): """ Called when status entries visibility is to be changed. """ # Ignore events as long as control is still loading. if self._loading: return actionname = action.get_name() if actionname == 'pagenumber': bit = constants.STATUS_PAGE elif actionname == 'resolution': bit = constants.STATUS_RESOLUTION elif actionname == 'rootpath': bit = constants.STATUS_PATH elif actionname == 'filename': bit = constants.STATUS_FILENAME elif actionname == 'filenumber': bit = constants.STATUS_FILENUMBER elif actionname == 'filesize': bit = constants.STATUS_FILESIZE if action.get_active(): prefs['statusbar fields'] |= bit else: prefs['statusbar fields'] &= ~bit self.update() self._update_sensitivity() def _button_released(self, widget, event, *args): """ Triggered when a mouse button is released to open the context menu. """ if event.button == 3: self.ui_manager.get_widget('/Statusbar').popup(None, None, None, None, event.button, event.time) def _update_sensitivity(self): """ Updates the action menu's sensitivity based on user preferences. """ page_visible = prefs['statusbar fields'] & constants.STATUS_PAGE fileno_visible = prefs['statusbar fields'] & constants.STATUS_FILENUMBER resolution_visible = prefs['statusbar fields'] & constants.STATUS_RESOLUTION path_visible = prefs['statusbar fields'] & constants.STATUS_PATH filename_visible = prefs['statusbar fields'] & constants.STATUS_FILENAME filesize_visible = prefs['statusbar fields'] & constants.STATUS_FILESIZE for name, visible in (('pagenumber', page_visible), ('filenumber', fileno_visible), ('resolution', resolution_visible), ('rootpath', path_visible), ('filename', filename_visible), ('filesize', filesize_visible)): action = self.ui_manager.get_action('/Statusbar/' + name) action.set_active(visible) class TooltipStatusHelper(object): """ Attaches to a L{Gtk.UIManager} to provide statusbar tooltips when selecting menu items. """ def __init__(self, uimanager, statusbar): self._statusbar = statusbar uimanager.connect('connect-proxy', self._on_connect_proxy) uimanager.connect('disconnect-proxy', self._on_disconnect_proxy) def _on_connect_proxy(self, uimgr, action, widget): """ Connects the widget's selection handlers to the status bar update. """ tooltip = action.get_property('tooltip') if isinstance(widget, Gtk.MenuItem) and tooltip: cid = widget.connect('select', self._on_item_select, tooltip) cid2 = widget.connect('deselect', self._on_item_deselect) setattr(widget, 'app::connect-ids', (cid, cid2)) def _on_disconnect_proxy(self, uimgr, action, widget): """ Disconnects the widget's selection handlers. """ cids = getattr(widget, 'app::connect-ids', ()) for cid in cids: widget.disconnect(cid) def _on_item_select(self, menuitem, tooltip): self._statusbar.push(0, " " * Statusbar.SPACING + tooltip) def _on_item_deselect(self, menuitem): self._statusbar.pop(0) # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0 mcomix-3.1.0/mcomix/strings.py0000644000175000017500000000777614476523373016077 0ustar00moritzmoritz# -*- coding: utf-8 -*- """ strings.py - Constant strings that need internationalization. This file should only be imported after gettext has been correctly initialized and installed in the global namespace. """ from mcomix.constants import ZIP, RAR, TAR, GZIP, BZIP2, XZ, PDF, SEVENZIP, LHA, ZIP_EXTERNAL, MOBI from mcomix.i18n import _ ARCHIVE_DESCRIPTIONS = { ZIP : _('ZIP archive'), RAR : _('RAR archive'), TAR : _('Tar archive'), GZIP : _('Gzip compressed tar archive'), BZIP2 : _('Bzip2 compressed tar archive'), XZ : _('XZ compressed tar archive'), PDF : _('PDF document'), SEVENZIP : _('7z archive'), LHA : _('LHA archive'), ZIP_EXTERNAL: _('ZIP archive'), MOBI : _('MobiPocket ebook'), } AUTHORS = ( ('Pontus Ekberg', _('Original vision/developer of Comix')), ('Louis Casillas', _('MComix developer')), ('Moritz Brunner', _('MComix developer')), ('Ark', _('MComix developer')), ('Benoit Pierre', _('MComix developer')), ) TRANSLATORS = ( ('Emfox Zhou', _('Simplified Chinese translation')), ('Xie Yanbo', _('Simplified Chinese translation')), ('Zach Cheung', _('Simplified Chinese translation')), ('AN Long', _('Simplified Chinese translation')), ('Manuel Quiñones', _('Spanish translation')), ('Carlos Feliu', _('Spanish translation')), ('Marcelo Góes', _('Brazilian Portuguese translation')), ('Christoph Wolk', _('German translation and Nautilus thumbnailer')), ('Chris Leick', _('German translation')), ('Raimondo Giammanco', _('Italian translation')), ('Giovanni Scafora', _('Italian translation')), ('GhePeU', _('Italian translation')), ('Arthur Nieuwland', _('Dutch translation')), ('Achraf Cherti', _('French translation')), ('Benoît H.', _('French translation')), ('Joseph M. Sleiman', _('French translation')), ('Frédéric Chateaux', _('French translation')), ('Kamil Leduchowski', _('Polish translatin')), ('Darek Jakoniuk', _('Polish translation')), ('Paul Chatzidimitriou', _('Greek translation')), ('Carles Escrig Royo', _('Catalan translation')), ('Hsin-Lin Cheng', _('Traditional Chinese translation')), ('Wayne Su', _('Traditional Chinese translation')), ('Mamoru Tasaka', _('Japanese translation')), ('Keita Haga', _('Japanese translation')), ('Toshiharu Kudoh', _('Japanese translation')), ('Ernő Drabik', _('Hungarian translation')), ('Artyom Smirnov', _('Russian translation')), ('Евгений Лежнин', _('Russian translation')), ('Adrian C.', _('Croatian translation')), ('김민기', _('Korean translation')), ('Gyeongmin Bak', _('Korean translation')), ('Minho Jeung', _('Korean translation')), ('Maryam Sanaat', _('Persian translation')), ('Andhika Padmawan', _('Indonesian translation')), ('Jan Nekvasil', _('Czech translation')), ('Олександр Заяц', _('Ukrainian translation')), ('Roxerio Roxo Carrillo', _('Galician translation')), ('Martin Karlsson', _('Swedish translation')), ('Jonatan Nyberg', _('Swedish translation')), ('Isratine Citizen', _('Hebrew translation')), ('Zygi Mantus', _('Lithuanian translation')), ) ARTISTS = ( ('Victor Castillejo', _('Icon design')), ('FeRD (Frank Dana)', _('Vector icon')), ) # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704114443.0 mcomix-3.1.0/mcomix/thumbbar.py0000644000175000017500000002525514544534413016173 0ustar00moritzmoritz"""thumbbar.py - Thumbnail sidebar for main window.""" import urllib.request, urllib.parse, urllib.error from gi.repository import GObject, Gdk, GdkPixbuf, Gtk import cairo from mcomix.preferences import prefs from mcomix import image_tools from mcomix import tools from mcomix import constants from mcomix import thumbnail_view class ThumbnailSidebar(Gtk.ScrolledWindow): """A thumbnail sidebar including scrollbar for the main window.""" # Thumbnail border width in pixels. _BORDER_SIZE = 1 def __init__(self, window): super(ThumbnailSidebar, self).__init__() self._window = window #: Thumbnail load status self._loaded = False #: Selected row in treeview self._currently_selected_row = 0 self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.ALWAYS) self.get_vadjustment().step_increment = 15 self.get_vadjustment().page_increment = 1 # Disable stupid overlay scrollbars... if hasattr(self.props, 'overlay_scrolling'): self.props.overlay_scrolling = False # models - contains data self._thumbnail_liststore = Gtk.ListStore(int, GdkPixbuf.Pixbuf, bool) # view - responsible for laying out the columns self._treeview = thumbnail_view.ThumbnailTreeView( self._thumbnail_liststore, 0, # UID 1, # pixbuf 2, # status ) self._treeview.set_headers_visible(False) self._treeview.generate_thumbnail = self._generate_thumbnail self._treeview.set_activate_on_single_click(True) self._treeview.connect_after('drag_begin', self._drag_begin) self._treeview.connect('drag_data_get', self._drag_data_get) self._treeview.connect('row-activated', self._row_activated_event) self._treeview.connect('button_press_event', self._mouse_press_event) # enable drag and dropping of images from thumbnail bar to some file # manager self._treeview.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [('text/uri-list', 0, 0)], Gdk.DragAction.COPY) # Page column self._thumbnail_page_treeviewcolumn = Gtk.TreeViewColumn(None) self._treeview.append_column(self._thumbnail_page_treeviewcolumn) self._text_cellrenderer = Gtk.CellRendererText() # Right align page numbers. self._text_cellrenderer.set_property('xalign', 1.0) self._thumbnail_page_treeviewcolumn.set_sizing(Gtk.TreeViewColumnSizing.FIXED) self._thumbnail_page_treeviewcolumn.pack_start(self._text_cellrenderer, False) self._thumbnail_page_treeviewcolumn.add_attribute(self._text_cellrenderer, 'text', 0) self._thumbnail_page_treeviewcolumn.set_visible(False) # Pixbuf column self._thumbnail_image_treeviewcolumn = Gtk.TreeViewColumn(None) self._treeview.append_column(self._thumbnail_image_treeviewcolumn) self._pixbuf_cellrenderer = Gtk.CellRendererPixbuf() self._thumbnail_image_treeviewcolumn.set_sizing(Gtk.TreeViewColumnSizing.FIXED) self._thumbnail_image_treeviewcolumn.set_fixed_width(self._pixbuf_size) self._thumbnail_image_treeviewcolumn.pack_start(self._pixbuf_cellrenderer, True) self._thumbnail_image_treeviewcolumn.add_attribute(self._pixbuf_cellrenderer, 'pixbuf', 1) self._treeview.set_fixed_height_mode(True) self._treeview.set_can_focus(False) self.add(self._treeview) self.change_thumbnail_background_color(prefs['thumb bg colour']) self.show_all() self._window.page_changed += self._on_page_change self._window.imagehandler.page_available += self._on_page_available def toggle_page_numbers_visible(self): """ Enables or disables page numbers on the thumbnail bar. """ visible = prefs['show page numbers on thumbnails'] if visible: number_of_pages = self._window.imagehandler.get_number_of_pages() number_of_digits = tools.number_of_digits(number_of_pages) self._text_cellrenderer.set_property('width-chars', number_of_digits + 1) w = self._text_cellrenderer.get_preferred_size(self._treeview)[1].width self._thumbnail_page_treeviewcolumn.set_fixed_width(w) self._thumbnail_page_treeviewcolumn.set_visible(visible) def get_width(self): """Return the width in pixels of the ThumbnailSidebar.""" return self.size_request().width def show(self, *args): """Show the ThumbnailSidebar.""" self.load_thumbnails() super(ThumbnailSidebar, self).show() def hide(self): """Hide the ThumbnailSidebar.""" super(ThumbnailSidebar, self).hide() self._treeview.stop_update() def clear(self): """Clear the ThumbnailSidebar of any loaded thumbnails.""" self._loaded = False self._treeview.stop_update() self._thumbnail_liststore.clear() self._currently_selected_page = 0 def resize(self): """Reload the thumbnails with the size specified by in the preferences. """ self.clear() self._thumbnail_image_treeviewcolumn.set_fixed_width(self._pixbuf_size) self.load_thumbnails() def change_thumbnail_background_color(self, colour): """ Changes the background color of the thumbnail bar. """ self.set_thumbnail_background(colour) # Force a redraw of the widget. self._treeview.queue_draw() def set_thumbnail_background(self, color): rgba = Gdk.RGBA(*image_tools.color_to_floats_rgba(color)) self._pixbuf_cellrenderer.set_property('cell-background-rgba', rgba) self._text_cellrenderer.set_property('background-rgba', rgba) fg_color = image_tools.text_color_for_background_color(color) fg_rgba = Gdk.RGBA(*(fg_color.to_floats() + (1.0,))) self._text_cellrenderer.set_property('foreground-rgba', fg_rgba) @property def _pixbuf_size(self): # Don't forget the extra pixels for the border! return prefs['thumbnail size'] + 2 * self._BORDER_SIZE def load_thumbnails(self): """Load the thumbnails, if it is appropriate to do so.""" if (not self._window.filehandler.file_loaded or self._window.imagehandler.get_number_of_pages() == 0 or self._loaded): return self.toggle_page_numbers_visible() # Detach model for performance reasons model = self._treeview.get_model() self._treeview.set_model(None) # Create empty preview thumbnails. filler = self._get_empty_thumbnail() for row in range(self._window.imagehandler.get_number_of_pages()): self._thumbnail_liststore.append((row + 1, filler, False)) self._loaded = True # Re-attach model self._treeview.set_model(model) # Update current image selection in the thumb bar. self._set_selected_row(self._currently_selected_row) def _generate_thumbnail(self, uid): """ Generate the pixbuf for C{path} at demand. """ assert isinstance(uid, int) page = uid pixbuf = self._window.imagehandler.get_thumbnail(page, prefs['thumbnail size'], prefs['thumbnail size'], nowait=True) if pixbuf is not None: pixbuf = self._window.enhancer.enhance(pixbuf); pixbuf = image_tools.add_border(pixbuf, self._BORDER_SIZE) return pixbuf def _set_selected_row(self, row, scroll=True): """Set currently selected row. If is True, the tree is automatically scrolled to ensure the selected row is visible. """ self._currently_selected_row = row self._treeview.get_selection().select_path(row) if self._loaded and scroll: self._treeview.scroll_to_cell(row, use_align=True, row_align=0.25) def _get_selected_row(self): """Return the index of the currently selected row.""" try: return self._treeview.get_selection().get_selected_rows()[1][0][0] except IndexError: return 0 def _row_activated_event(self, treeview, path, column): """Handle events due to changed thumbnail selection.""" selected_row = self._get_selected_row() self._set_selected_row(selected_row, scroll=False) self._window.set_page(selected_row + 1) def _mouse_press_event(self, widget, event): if self._window.was_out_of_focus: # if the window was out of focus and the user clicks on # the thumbbar then do not select that page because they # more than likely have many pages open and are simply trying # to give mcomix focus again return True return False def _drag_data_get(self, treeview, context, selection, *args): """Put the URI of the selected file into the SelectionData, so that the file can be copied (e.g. to a file manager). """ selected = self._get_selected_row() path = self._window.imagehandler.get_path_to_page(selected + 1) uri = 'file://localhost' + urllib.request.pathname2url(path) selection.set_uris([uri]) def _drag_begin(self, treeview, context): """We hook up on drag_begin events so that we can set the hotspot for the cursor at the top left corner of the thumbnail (so that we might actually see where we are dropping!). """ path = treeview.get_cursor()[0] surface = treeview.create_row_drag_icon(path) # Because of course a cairo.Win32Surface does not have # get_width/get_height, that would be to easy... cr = cairo.Context(surface) x1, y1, x2, y2 = cr.clip_extents() width, height = x2 - x1, y2 - y1 pixbuf = Gdk.pixbuf_get_from_surface(surface, 0, 0, width, height) Gtk.drag_set_icon_pixbuf(context, pixbuf, -5, -5) def _get_empty_thumbnail(self): """ Create an empty filler pixmap. """ pixbuf = GdkPixbuf.Pixbuf.new(colorspace=GdkPixbuf.Colorspace.RGB, has_alpha=True, bits_per_sample=8, width=self._pixbuf_size, height=self._pixbuf_size) # Make the pixbuf transparent. pixbuf.fill(0) return pixbuf def _on_page_change(self): row = self._window.imagehandler.get_current_page() - 1 if row == self._currently_selected_row: return self._set_selected_row(row) def _on_page_available(self, page): """ Called whenever a new page is ready for display. """ if self.get_visible(): self._treeview.draw_thumbnails_on_screen() # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1695149037.0 mcomix-3.1.0/mcomix/thumbnail_tools.py0000644000175000017500000002665214502365755017601 0ustar00moritzmoritz"""thumbnail.py - Thumbnail module for MComix implementing (most of) the freedesktop.org "standard" at http://jens.triq.net/thumbnail-spec/ """ import os import re import shutil import tempfile import mimetypes import threading import locale import PIL.Image as Image from urllib.request import pathname2url from typing import Optional, TYPE_CHECKING from hashlib import md5 from mcomix.preferences import prefs from mcomix import constants from mcomix import archive_tools from mcomix import tools from mcomix import image_tools from mcomix import portability from mcomix import callback from mcomix import log from mcomix.i18n import _ if TYPE_CHECKING: from gi.repository import GdkPixbuf class Thumbnailer(object): """ The Thumbnailer class is responsible for managing MComix internal thumbnail creation. Depending on its settings, it either stores thumbnails on disk and retrieves them later, or simply creates new thumbnails each time it is called. """ def __init__(self, dst_dir=constants.THUMBNAIL_PATH, store_on_disk=None, size=None, force_recreation=False, archive_support=False): """ set the thumbnailer's storage directory. If on disk is True, it changes the thumbnailer's behaviour to store files on disk, or just create new thumbnails each time it was called when set to False. Defaults to the 'create thumbnails' preference if not set. The dimensions for the created thumbnails is set by , a (width, height) tupple. Defaults to the 'thumbnail size' preference if not set. If is True, thumbnails stored on disk will always be re-created instead of being re-used. If is True, support for archive thumbnail creation (based on cover detection) is enabled. Otherwise, only image files are supported. """ self.dst_dir = dst_dir if store_on_disk is None: self.store_on_disk = prefs['create thumbnails'] else: self.store_on_disk = store_on_disk if size is None: self.width = self.height = prefs['thumbnail size'] self.default_sizes = True else: self.width, self.height = size self.default_sizes = False self.force_recreation = force_recreation self.archive_support = archive_support def thumbnail(self, filepath: str, threaded: bool = False) -> Optional["GdkPixbuf.Pixbuf"]: """ Returns a thumbnail pixbuf for , transparently handling both normal image files and archives. If a thumbnail file already exists, it is re-used. Otherwise, a new thumbnail is created from . Returns None if thumbnail creation failed, or if the thumbnail creation is run asynchrounosly. """ # Update width and height from preferences if they haven't been set explicitly if self.default_sizes: self.width = prefs['thumbnail size'] self.height = prefs['thumbnail size'] if self._thumbnail_exists(filepath): thumbpath = self._path_to_thumbpath(filepath) pixbuf = image_tools.load_pixbuf(thumbpath) self.thumbnail_finished(filepath, pixbuf) return pixbuf else: if threaded: thread = threading.Thread(target=self._create_thumbnail, args=(filepath,)) thread.name += '-thumbnailer' thread.setDaemon(True) thread.start() return None else: return self._create_thumbnail(filepath) @callback.Callback def thumbnail_finished(self, filepath, pixbuf): """ Called every time a thumbnail has been completed. is the file that was used as source, is the resulting thumbnail. """ pass def delete(self, filepath): """ Deletes the thumbnail for (if it exists) """ thumbpath = self._path_to_thumbpath(filepath) if os.path.isfile(thumbpath): try: os.remove(thumbpath) except IOError as error: log.error(_("! Could not remove file \"%s\""), thumbpath) log.error(error) def _create_thumbnail_pixbuf(self, filepath): """ Creates a thumbnail pixbuf from , and returns it as a tuple along with a file metadata dictionary: (pixbuf, tEXt_data) """ if self.archive_support: mime = archive_tools.archive_mime_type(filepath) else: mime = None if mime is not None: cleanup = [] try: tmpdir = tempfile.mkdtemp(prefix='mcomix_archive_thumb.') cleanup.append(lambda: shutil.rmtree(tmpdir, True)) archive = archive_tools.get_recursive_archive_handler(filepath, tmpdir, type=mime) if archive is None: return None, None cleanup.append(archive.close) files = archive.list_contents() wanted = self._guess_cover(files) if wanted is None: return None, None archive.extract(wanted, tmpdir) image_path = os.path.join(tmpdir, wanted) if not os.path.isfile(image_path): return None, None pixbuf = image_tools.load_pixbuf_size(image_path, self.width, self.height) if self.store_on_disk: tEXt_data = self._get_text_data(image_path) # Use the archive's mTime instead of the extracted file's mtime tEXt_data['tEXt::Thumb::MTime'] = str(int(os.stat(filepath).st_mtime)) else: tEXt_data = None return pixbuf, tEXt_data finally: for fn in reversed(cleanup): fn() elif image_tools.is_image_file(filepath): pixbuf = image_tools.load_pixbuf_size(filepath, self.width, self.height) if self.store_on_disk: tEXt_data = self._get_text_data(filepath) else: tEXt_data = None return pixbuf, tEXt_data else: return None, None def _create_thumbnail(self, filepath): """ Creates the thumbnail pixbuf for , and saves the pixbuf to disk if necessary. Returns the created pixbuf, or None, if creation failed. """ pixbuf, tEXt_data = self._create_thumbnail_pixbuf(filepath) self.thumbnail_finished(filepath, pixbuf) if pixbuf and self.store_on_disk: thumbpath = self._path_to_thumbpath(filepath) self._save_thumbnail(pixbuf, thumbpath, tEXt_data) return pixbuf def _get_text_data(self, filepath): """ Creates a tEXt dictionary for . """ mime = mimetypes.guess_type(filepath)[0] or "unknown/mime" uri = portability.uri_prefix() + pathname2url(os.path.normpath(filepath)) stat = os.stat(filepath) # MTime could be floating point number, so convert to long first to have a fixed point number mtime = str(int(stat.st_mtime)) size = str(stat.st_size) format, (width, height), providers = image_tools.get_image_info(filepath) return { 'tEXt::Thumb::URI': uri, 'tEXt::Thumb::MTime': mtime, 'tEXt::Thumb::Size': size, 'tEXt::Thumb::Mimetype': mime, 'tEXt::Thumb::Image::Width': str(width), 'tEXt::Thumb::Image::Height': str(height), 'tEXt::Software': 'MComix %s' % constants.VERSION } def _save_thumbnail(self, pixbuf, thumbpath, tEXt_data): """ Saves as , with additional metadata from . If already exists, it is overwritten. """ try: directory = os.path.dirname(thumbpath) if not os.path.isdir(directory): os.makedirs(directory, 0o700) if os.path.isfile(thumbpath): os.remove(thumbpath) option_keys = [] option_values = [] for key, value in list(tEXt_data.items()): option_keys.append(key) option_values.append(value) pixbuf.savev(thumbpath, 'png', option_keys, option_values) os.chmod(thumbpath, 0o600) except Exception as ex: log.warning( _('! Could not save thumbnail "%(thumbpath)s": %(error)s'), { 'thumbpath' : thumbpath, 'error' : ex } ) def _thumbnail_exists(self, filepath): """ Checks if the thumbnail for already exists. This function will return False if the thumbnail exists and it's mTime doesn't match the mTime of , it's size is different from the one specified in the thumbnailer, or if is True. """ if not self.force_recreation: thumbpath = self._path_to_thumbpath(filepath) if os.path.isfile(thumbpath): # Check the thumbnail's stored mTime try: img = Image.open(thumbpath) except IOError: return False info = img.info stored_mtime = int(float(info['Thumb::MTime'])) # The source file might no longer exist file_mtime = os.path.isfile(filepath) and int(os.stat(filepath).st_mtime) or stored_mtime return stored_mtime == file_mtime and \ max(*img.size) == max(self.width, self.height) else: return False else: return False def _path_to_thumbpath(self, filepath): """ Converts to an URI for the thumbnail in . """ uri = portability.uri_prefix() + pathname2url(os.path.normpath(filepath)) return self._uri_to_thumbpath(uri) def _uri_to_thumbpath(self, uri): """ Return the full path to the thumbnail for with being the base thumbnail directory. """ uri = uri.encode(locale.getpreferredencoding()) if isinstance(uri, str) else uri md5hash = md5(uri).hexdigest() thumbpath = os.path.join(self.dst_dir, md5hash + '.png') return thumbpath def _guess_cover(self, files): """Return the filename within that is the most likely to be the cover of an archive using some simple heuristics. """ # Ignore MacOSX meta files. files = filter(lambda filename: '__MACOSX' not in os.path.normpath(filename).split(os.sep), files) # Ignore credit files if possible. files = filter(lambda filename: 'credit' not in os.path.split(filename)[1].lower(), files) images = list(filter(image_tools.is_image_file, files)) tools.alphanumeric_sort(images) front_re = re.compile('(cover|front)', re.I) candidates = list(filter(front_re.search, images)) candidates = [c for c in candidates if 'back' not in c.lower()] if candidates: return candidates[0] if images: return images[0] return None # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0 mcomix-3.1.0/mcomix/thumbnail_view.py0000644000175000017500000001205314476523373017403 0ustar00moritzmoritz""" Gtk.IconView subclass for dynamically generated thumbnails. """ import queue from gi.repository import Gtk, GLib from mcomix.preferences import prefs from mcomix.worker_thread import WorkerThread class ThumbnailViewBase(object): """ This class provides shared functionality for Gtk.TreeView and Gtk.IconView. Instantiating this class directly is *impossible*, as it depends on methods provided by the view classes. """ def __init__(self, uid_column, pixbuf_column, status_column): """ Constructs a new ThumbnailView. @param uid_column: index of unique identifer column. @param pixbuf_column: index of pixbuf column. @param status_column: index of status boolean column (True if pixbuf is not temporary filler) """ #: Keep track of already generated thumbnails. self._uid_column = uid_column self._pixbuf_column = pixbuf_column self._status_column = status_column #: Ignore updates when this flag is True. self._updates_stopped = True #: Worker thread self._thread = WorkerThread(self._pixbuf_worker, name='thumbview', unique_orders=True, max_threads=prefs["max threads"]) def generate_thumbnail(self, uid): """ This function must return the thumbnail for C{uid}. """ raise NotImplementedError() def get_visible_range(self): """ See L{Gtk.IconView.get_visible_range}. """ raise NotImplementedError() def stop_update(self): """ Stops generation of pixbufs. """ self._updates_stopped = True self._thread.stop() def draw_thumbnails_on_screen(self, *args): """ Prepares valid thumbnails for currently displayed icons. This method is supposed to be called from the expose-event callback function. """ visible = self.get_visible_range() if not visible: # No valid paths available return pixbufs_needed = [] start = visible[0][0] end = visible[1][0] # Read ahead/back and start caching a few more icons. Currently invisible # icons are always cached only after the visible icons have been completed. additional = (end - start) // 2 required = list(range(start, end + additional + 1)) + \ list(range(max(0, start - additional), start)) model = self.get_model() # Filter invalid paths. required = [path for path in required if 0 <= path < len(model)] with self._thread: # Flush current pixmap generation orders. self._thread.clear_orders() for path in required: iter = model.get_iter(path) uid, generated = model.get(iter, self._uid_column, self._status_column) # Do not queue again if thumbnail was already created. if not generated: pixbufs_needed.append((uid, iter)) if len(pixbufs_needed) > 0: self._updates_stopped = False self._thread.extend_orders(pixbufs_needed) def _pixbuf_worker(self, order): """ Run by a worker thread to generate the thumbnail for a path.""" uid, iter = order pixbuf = self.generate_thumbnail(uid) if pixbuf is not None: GLib.idle_add(self._pixbuf_finished, iter, pixbuf) def _pixbuf_finished(self, iter, pixbuf): """ Executed when a pixbuf was created, to actually insert the pixbuf into the view store. C{pixbuf_info} is a tuple containing (index, pixbuf). """ if self._updates_stopped: return 0 model = self.get_model() model.set(iter, self._status_column, True, self._pixbuf_column, pixbuf) # Remove this idle handler. return 0 class ThumbnailIconView(Gtk.IconView, ThumbnailViewBase): def __init__(self, model, uid_column, pixbuf_column, status_column): assert 0 != (model.get_flags() & Gtk.TreeModelFlags.ITERS_PERSIST) super(ThumbnailIconView, self).__init__(model) ThumbnailViewBase.__init__(self, uid_column, pixbuf_column, status_column) self.set_pixbuf_column(pixbuf_column) # Connect events self.connect('draw', self.draw_thumbnails_on_screen) def get_visible_range(self): return Gtk.IconView.get_visible_range(self) class ThumbnailTreeView(Gtk.TreeView, ThumbnailViewBase): def __init__(self, model, uid_column, pixbuf_column, status_column): assert 0 != (model.get_flags() & Gtk.TreeModelFlags.ITERS_PERSIST) super(ThumbnailTreeView, self).__init__(model) ThumbnailViewBase.__init__(self, uid_column, pixbuf_column, status_column) # Connect events self.connect('draw', self.draw_thumbnails_on_screen) def get_visible_range(self): return Gtk.TreeView.get_visible_range(self) # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694865534.0 mcomix-3.1.0/mcomix/tools.py0000644000175000017500000001574414501314176015524 0ustar00moritzmoritz"""tools.py - Contains various helper functions.""" import bisect import gc import itertools import math import operator import os import re import sys from functools import reduce from typing import (Any, Iterable, List, Mapping, Sequence, Tuple, TypeVar, Union) Numeric = TypeVar('Numeric', int, float) NUMERIC_REGEXP = re.compile(r"\d+|\D+") # Split into numerics and characters PREFIXED_BYTE_UNITS = ("B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB") def cmp(a: str, b: str) -> int: """ Forward port of Python2's cmp function """ return (a > b) - (a < b) class AlphanumericSortKey: """ Compares two strings by their natural order (i.e. 1 before 10) """ def __init__(self, filename: str) -> None: self.filename_parts: List[Union[int, str]] = [ int(part) if part.isdigit() else part for part in NUMERIC_REGEXP.findall(filename.lower()) ] def __lt__(self, other: 'AlphanumericSortKey') -> bool: for left, right in itertools.zip_longest(self.filename_parts, other.filename_parts, fillvalue=''): if not isinstance(left, type(right)): left_str = str(left) right_str = str(right) if left_str < right_str: return True elif left_str > right_str: return False else: if left < right: return True elif left > right: return False return False def alphanumeric_sort(filenames: List[str]) -> None: """Do an in-place alphanumeric sort of the strings in , such that for an example "1.jpg", "2.jpg", "10.jpg" is a sorted ordering. """ filenames.sort(key=AlphanumericSortKey) def bin_search(lst: List, value: Any) -> int: """ Binary search for sorted list C{lst}, looking for C{value}. @return: List index on success. On failure, it returns the 1's complement of the index where C{value} would be inserted. This implies that the return value is non-negative if and only if C{value} is contained in C{lst}. """ index = bisect.bisect_left(lst, value) if index != len(lst) and lst[index] == value: return index else: return ~index def get_home_directory() -> str: """On UNIX-like systems, this method will return the path of the home directory, e.g. /home/username. On Windows, it will return an MComix sub-directory of . """ if sys.platform == 'win32': return os.path.join(os.path.expanduser('~'), 'MComix') else: return os.path.expanduser('~') def get_config_directory() -> str: """Return the path to the MComix config directory. On UNIX, this will be $XDG_CONFIG_HOME/mcomix, on Windows it will be in %APPDATA%/MComix. See http://standards.freedesktop.org/basedir-spec/latest/ for more information on the $XDG_CONFIG_HOME environmental variable. """ if sys.platform == 'win32': return os.path.join(os.path.expandvars('%APPDATA%'), 'MComix') else: base_path = os.getenv('XDG_CONFIG_HOME', os.path.join(get_home_directory(), '.config')) return os.path.join(base_path, 'mcomix') def get_data_directory() -> str: """Return the path to the MComix data directory. On UNIX, this will be $XDG_DATA_HOME/mcomix, on Windows it will be the same directory as get_config_directory(). See http://standards.freedesktop.org/basedir-spec/latest/ for more information on the $XDG_DATA_HOME environmental variable. """ if sys.platform == 'win32': return os.path.join(os.path.expandvars('%APPDATA%'), 'MComix') else: base_path = os.getenv('XDG_DATA_HOME', os.path.join(get_home_directory(), '.local/share')) return os.path.join(base_path, 'mcomix') def number_of_digits(n: int) -> int: if 0 == n: return 1 return int(math.log10(abs(n))) + 1 def decompose_byte_size_exponent(n: float) -> Tuple[float, int]: e = 0 while n > 1024.0: n /= 1024.0 e += 1 return (n, e) def byte_size_exponent_to_prefix(e: int) -> str: return PREFIXED_BYTE_UNITS[min(e, len(PREFIXED_BYTE_UNITS) - 1)] def format_byte_size(n: float) -> str: nn, e = decompose_byte_size_exponent(n) return ('%d %s' if nn == int(nn) else '%.1f %s') % \ (nn, byte_size_exponent_to_prefix(e)) def garbage_collect() -> None: """ Runs the garbage collector. """ gc.collect(0) def div(a: Numeric, b: Numeric) -> float: return float(a) / float(b) def volume(t: List[int]) -> int: return reduce(operator.mul, t, 1) def relerr(approx: Numeric, ideal: Numeric) -> float: return abs(div(approx - ideal, ideal)) def smaller(a: List, b: List) -> List: """ Returns a list with the i-th element set to True if and only if the i-th element in a is less than the i-th element in b. """ return list(map(operator.lt, a, b)) def smaller_or_equal(a: List, b: List) -> List: """ Returns a list with the i-th element set to True if and only if the i-th element in a is less than or equal to the i-th element in b. """ return list(map(operator.le, a, b)) def scale(t: Sequence[Numeric], factor: Numeric) -> List[Numeric]: return [x * factor for x in t] def vector_sub(a: List[Numeric], b: List[Numeric]) -> List[Numeric]: """ Subtracts vector b from vector a. """ return list(map(operator.sub, a, b)) def vector_add(a: List[Numeric], b: List[Numeric]) -> List[Numeric]: """ Adds vector a to vector b. """ return list(map(operator.add, a, b)) def vector_opposite(a: List[Numeric]) -> List[Numeric]: """ Returns the opposite vector -a. """ return list(map(operator.neg, a)) def remap_axes(vector, order): return [vector[i] for i in order] def inverse_axis_map(order): identity = list(range(len(order))) return [identity[order[i]] for i in identity] def compile_rotations(*rotations): return reduce(lambda a, x: a + (x % 360) % 360, rotations, 0) def rotation_swaps_axes(rotation: int) -> bool: return rotation in (90, 270) def fixed_strings_regex(strings: Iterable[str]) -> str: # introduces a matching group unique_strings = set(strings) return r'(%s)' % '|'.join(sorted([re.escape(s) for s in unique_strings])) def formats_to_regex(formats: Mapping) -> re.Pattern: """ Returns a compiled regular expression that can be used to search for file extensions specified in C{formats}. """ return re.compile(r'\.' + fixed_strings_regex( itertools.chain.from_iterable([e[1] for e in formats.values()])) + r'$', re.I) def append_number_to_filename(filename: str, number: int) -> str: """ Generate a new string from filename with an appended number right before the extension. """ file_no_ext = os.path.splitext(filename)[0] ext = os.path.splitext(filename)[1] return file_no_ext + (" (%s)" % (number)) + ext # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0 mcomix-3.1.0/mcomix/transform.py0000644000175000017500000001553214476523373016406 0ustar00moritzmoritz""" A series of a few simple linear transforms. """ # Allow class name in annotations while still defining class from __future__ import annotations import sys from typing import Optional, Tuple class Matrix: """Simple linear transformations represented as a 2x2 matrix. Note that the current implementation does not support arbitrary matrices. Instead, the predefined Matrix objects and conversion functions in the Transform class should be used to create Matrix objects.""" def __init__(self, m1: float, m2: float, m3: float, m4: float) -> None: """Initialize a row-major transformation matrix.""" self.m: Tuple[float, float, float, float] = (m1, m2, m3, m4) def __str__(self) -> str: return str(self.m) def __repr__(self) -> str: args = ", ".join([str(x) for x in self.m]) return f'Matrix({args})' def __bool__(self) -> bool: """Does this matrix perform any operations (is not identity)?""" return self != Transform.ID def __add__(self, other: object) -> Matrix: """Combine Matrices: self + other.""" if not isinstance(other, Matrix): return NotImplemented a = self.m b = other.m return Matrix( b[0] * a[0] + b[1] * a[2], b[0] * a[1] + b[1] * a[3], b[2] * a[0] + b[3] * a[2], b[2] * a[1] + b[3] * a[3], ) def __radd__(self, other: object) -> Matrix: """Combine Matrices: other + self.""" if isinstance(other, Matrix): return other + self return NotImplemented def __eq__(self, other: object) -> bool: """Test equality: self == other.""" if isinstance(other, Matrix): return len(self.m) == len(other.m) and all([ self.m[i] == other.m[i] for i in range(len(self.m)) ]) return NotImplemented def __ne__(self, other: object) -> bool: """Test inequality: self != other.""" if isinstance(other, Matrix): return not self == other return NotImplemented def and_then(self, next: Matrix) -> Matrix: """The matrix result of transforming self by 'next'.""" return self + next def and_then_all(self, *nexts: Matrix) -> Matrix: """The matrix resulting from self transformed by each 'nexts' in turn.""" out = self for m in nexts: out += m return out def swaps_axes(self) -> bool: """ Determines whether this matrix includes a transpose of the axes. """ return self.m[0] == 0 def rotated(self, deg: int) -> Matrix: """The matrix result of rotating self by 'deg' degrees.""" return self + Transform.from_rotation(deg) def scaled(self, s0: float, s1: Optional[float] = None) -> Matrix: """The matrix result of scaling self by the provided factor(s). t.scale(s) will scale both dimensions by s. t.scale(s0, s1) will scale x by s0, and y by s1.""" if s1 is None: s1 = s0 return self + Transform.from_scales(s0, s1) def flipped(self, x: bool = False, y: bool = False) -> Matrix: """The matrix result of applying the specified flips.""" if x and y: return self + Transform.ROT180 if x: return self + Transform.INVX if y: return self + Transform.INVY return self def flipped_x(self) -> Matrix: """The matrix result of flipping self horizontally.""" return self.flipped(x=True) def flipped_y(self) -> Matrix: """The matrix result of flipping self vertically.""" return self.flipped(y=True) def to_image_transforms(self) -> Tuple[Tuple[float, float], int, Tuple[bool, bool]]: """ Decomposes the transform to a sequence of hints to basic transform instructions typically found in image processing libraries. The sequence will refer to the positive scaling factors to be applied for each axis first, followed by at most one rotation, and finally followed by at most one flip. @return a tuple (s, r, f) where s is a sequence of positive scaling factors for the corresponding axes, r is one of (0, 90, 180, 270), referring to the clockwise rotation to be applied, and f is a sequence of bools where True refers to the corresponding axis to be flipped. """ s: Tuple[float, float] = (abs(self.m[0] + self.m[1]), abs(self.m[2] + self.m[3])) r: int = 90 if self.swaps_axes() else 0 f: Tuple[bool, bool] = ( self.swaps_axes() ^ (self.m[0] < 0 or self.m[1] < 0), (self.m[2] < 0 or self.m[3] < 0) ) if all(f): f = (False, False) r += 180 if f[0] and r == 90: # Try to avoid horizontal flips because they tend to be slow, given # a certain combination of hardware, memory layout and libraries. f = (False, True) r = 270 return (s, r, f) class Transform(Matrix): ID = Matrix(1, 0, 0, 1) ROT90 = Matrix(0, -1, 1, 0) ROT180 = Matrix(-1, 0, 0, -1) ROT270 = Matrix(0, 1, -1, 0) INVX = Matrix(-1, 0, 0, 1) INVY = Matrix(1, 0, 0, -1) TRP = Matrix(0, 1, 1, 0) TRPINV = Matrix(0, -1, -1, 0) @classmethod def __call__(cls, *values: float) -> Matrix: """Construct a new Matrix by calling Transform(values).""" return Matrix(*values) @classmethod def from_rotation(cls, deg: int) -> Matrix: """Get the predefined Matrix for a supported rotation.""" if abs(deg) not in (0, 90, 180, 270): raise ValueError("illegal rotation angle: " + str(deg)) if deg < 0: deg = 360 + deg if deg == 90: return cls.ROT90 if deg == 180: return cls.ROT180 if deg == 270: return cls.ROT270 return cls.ID @classmethod def from_scales(cls, s0: float, s1: float) -> Matrix: """Get a Matrix representing the given x and y scales.""" if s0 == 0 or s1 == 0: raise ValueError("illegal scaling factor(s): " + str((s0, s1))) return Matrix(s0, 0, 0, s1) @classmethod def from_flips(cls, x: bool, y: bool) -> Matrix: """Get the predefined Matrix representing the given x and y flips.""" if x and y: return cls.ROT180 if x: return cls.INVX if y: return cls.INVY return cls.ID @classmethod def from_image_transforms( cls, t: Tuple[Tuple[float, float], int, Tuple[bool, bool]] ) -> Matrix: """Create a Matrix transform from a set of image transforms.""" s, r, f = t[0:3] return ( cls.from_scales(*s) + cls.from_rotation(r) + cls.from_flips(*f) ) # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704114443.0 mcomix-3.1.0/mcomix/ui.py0000644000175000017500000005567314544534413015013 0ustar00moritzmoritz"""ui.py - UI definitions for main window. """ from gi.repository import Gtk from mcomix import bookmark_menu from mcomix import openwith_menu from mcomix import edit_dialog from mcomix import enhance_dialog from mcomix import preferences_dialog from mcomix import recent from mcomix import dialog_handler from mcomix import constants from mcomix import status from mcomix import file_chooser_main_dialog from mcomix.preferences import prefs from mcomix.library import main_dialog as library_main_dialog from mcomix.i18n import _ class MainUI(Gtk.UIManager): def __init__(self, window): super(MainUI, self).__init__() self._window = window self._tooltipstatus = status.TooltipStatusHelper(self, window.statusbar) def _action_lambda(fn, *args): return lambda *_: fn(*args) # ---------------------------------------------------------------- # Create actions for the menus. # ---------------------------------------------------------------- self._actiongroup = Gtk.ActionGroup('mcomix-main') self._actiongroup.add_actions([ ('copy_page', Gtk.STOCK_COPY, _('_Copy'), None, _('Copies the current page to clipboard.'), window.clipboard.copy_page), ('delete', Gtk.STOCK_DELETE, _('_Delete'), None, _('Deletes the current file or archive from disk.'), window.delete), ('next_page', Gtk.STOCK_GO_FORWARD, _('_Next page'), None, _('Next page'), _action_lambda(window.flip_page, +1)), ('previous_page', Gtk.STOCK_GO_BACK, _('_Previous page'), None, _('Previous page'), _action_lambda(window.flip_page, -1)), ('first_page', Gtk.STOCK_GOTO_FIRST, _('_First page'), None, _('First page'), _action_lambda(window.first_page)), ('last_page', Gtk.STOCK_GOTO_LAST, _('_Last page'), None, _('Last page'), _action_lambda(window.last_page)), ('go_to', Gtk.STOCK_JUMP_TO, _('_Go to page...'), None, _('Go to page...'), window.page_select), ('refresh_archive', Gtk.STOCK_REFRESH, _('Re_fresh'), None, _('Reloads the currently opened files or archive.'), window.filehandler.refresh_file), ('next_archive', Gtk.STOCK_MEDIA_NEXT, _('Next _archive'), None, _('Next archive'), window.filehandler._open_next_archive), ('previous_archive', Gtk.STOCK_MEDIA_PREVIOUS, _('Previous a_rchive'), None, _('Previous archive'), window.filehandler._open_previous_archive), ('next_directory', Gtk.STOCK_REDO, _('Next directory'), None, _('Next directory'), window.filehandler.open_next_directory), ('previous_directory', Gtk.STOCK_UNDO, _('Previous directory'), None, _('Previous directory'), window.filehandler.open_previous_directory), ('zoom_in', Gtk.STOCK_ZOOM_IN, _('Zoom _In'), None, None, window.manual_zoom_in), ('zoom_out', Gtk.STOCK_ZOOM_OUT, _('Zoom _Out'), None, None, window.manual_zoom_out), ('zoom_original', Gtk.STOCK_ZOOM_100, _('_Normal Size'), None, None, window.manual_zoom_original), ('minimize', Gtk.STOCK_LEAVE_FULLSCREEN, _('Mi_nimize'), None, None, window.minimize), ('close', Gtk.STOCK_CLOSE, _('_Close'), None, _('Closes all opened files.'), _action_lambda(window.filehandler.close_file)), ('quit', Gtk.STOCK_QUIT, _('_Quit'), None, None, window.close_program), ('save_and_quit', Gtk.STOCK_QUIT, _('_Save and quit'), None, _('Quits and restores the currently opened file next time the program starts.'), window.save_and_terminate_program), ('rotate_90', 'mcomix-rotate-90', _('_Rotate 90 degrees CW'), None, None, window.rotate_90), ('rotate_180','mcomix-rotate-180', _('Rotate 180 de_grees'), None, None, window.rotate_180), ('rotate_270', 'mcomix-rotate-270', _('Rotat_e 90 degrees CCW'), None, None, window.rotate_270), ('flip_horiz', 'mcomix-flip-horizontal', _('Fli_p horizontally'), None, None, window.flip_horizontally), ('flip_vert', 'mcomix-flip-vertical', _('Flip _vertically'), None, None, window.flip_vertically), ('extract_page', Gtk.STOCK_SAVE_AS, _('Save _As'), None, None, window.extract_page), ('menu_zoom', 'mcomix-zoom', _('_Zoom')), ('menu_recent', Gtk.STOCK_FILE, _('_Recent')), ('menu_bookmarks_popup', 'comix-add-bookmark', _('_Bookmarks')), ('menu_bookmarks', None, _('_Bookmarks')), ('menu_toolbars', None, _('T_oolbars')), ('menu_edit', None, _('_Edit')), ('menu_open_with', Gtk.STOCK_OPEN, _('Open _with'), ''), ('menu_open_with_popup', Gtk.STOCK_OPEN, _('Open _with'), ''), ('menu_file', None, _('_File')), ('menu_view', None, _('_View')), ('menu_view_popup', 'comix-image', _('_View')), ('menu_go', None, _('_Go')), ('menu_go_popup', Gtk.STOCK_GO_FORWARD, _('_Go')), ('menu_tools', None, _('_Tools')), ('menu_help', None, _('_Help')), ('menu_transform', 'mcomix-transform', _('_Transform image')), ('menu_autorotate', None, _('_Auto-rotate image')), ('menu_autorotate_width', None, _('...when width exceeds height')), ('menu_autorotate_height', None, _('...when height exceeds width')), ('expander', None, None, None, None, None)]) self._actiongroup.add_toggle_actions([ ('fullscreen', Gtk.STOCK_FULLSCREEN, _('_Fullscreen'), None, _('Fullscreen mode'), window.change_fullscreen), ('double_page', 'mcomix-double-page', _('_Double page mode'), None, _('Double page mode'), window.change_double_page), ('toolbar', None, _('_Toolbar'), None, None, window.change_toolbar_visibility), ('menubar', None, _('_Menubar'), None, None, window.change_menubar_visibility), ('statusbar', None, _('St_atusbar'), None, None, window.change_statusbar_visibility), ('scrollbar', None, _('S_crollbars'), None, None, window.change_scrollbar_visibility), ('thumbnails', None, _('Th_umbnails'), None, None, window.change_thumbnails_visibility), ('hide_all', None, _('H_ide all'), None, None, window.change_hide_all), ('manga_mode', 'mcomix-manga', _('_Manga mode'), None, _('Manga mode'), window.change_manga_mode), ('invert_scroll', Gtk.STOCK_UNDO, _('Invert smart scroll'), None, _('Invert smart scrolling direction.'), window.change_invert_scroll), ('keep_transformation', None, _('_Keep transformation'), None, _('Keeps the currently selected transformation for the next pages.'), window.change_keep_transformation), ('slideshow', Gtk.STOCK_MEDIA_PLAY, _('Start _slideshow'), None, _('Start slideshow'), window.slideshow.toggle), ('lens', 'mcomix-lens', _('Magnifying _lens'), None, _('Magnifying lens'), window.lens.toggle), ('stretch', None, _('Stretch small images'), None, _('Stretch images to fit to the screen, depending on zoom mode.'), window.change_stretch), ('invert_color', None, _('_Invert image colors'), None, _('Invert image colors'), window.change_invert_color)]) # Note: Don't change the default value for the radio buttons unless # also fixing the code for setting the correct one on start-up in main.py. self._actiongroup.add_radio_actions([ ('best_fit_mode', 'mcomix-fitbest', _('_Best fit mode'), None, _('Best fit mode'), constants.ZoomMode.BEST), ('fit_width_mode', 'mcomix-fitwidth', _('Fit _width mode'), None, _('Fit width mode'), constants.ZoomMode.WIDTH), ('fit_height_mode', 'mcomix-fitheight', _('Fit _height mode'), None, _('Fit height mode'), constants.ZoomMode.HEIGHT), ('fit_size_mode', 'mcomix-fitsize', _('Fit _size mode'), None, _('Fit to size mode'), constants.ZoomMode.SIZE), ('fit_manual_mode', 'mcomix-fitmanual', _('M_anual zoom mode'), None, _('Manual zoom mode'), constants.ZoomMode.MANUAL)], 3, window.change_zoom_mode) # Automatically rotate image if width>height or height>width self._actiongroup.add_radio_actions([ ('no_autorotation', None, _('Never'), None, None, constants.AUTOROTATE_NEVER), ('rotate_90_width', 'mcomix-rotate-90', _('_Rotate 90 degrees CW'), None, None, constants.AUTOROTATE_WIDTH_90), ('rotate_270_width', 'mcomix-rotate-270', _('Rotat_e 90 degrees CCW'), None, None, constants.AUTOROTATE_WIDTH_270), ('rotate_90_height', 'mcomix-rotate-90', _('_Rotate 90 degrees CW'), None, None, constants.AUTOROTATE_HEIGHT_90), ('rotate_270_height', 'mcomix-rotate-270', _('Rotat_e 90 degrees CCW'), None, None, constants.AUTOROTATE_HEIGHT_270)], prefs['auto rotate depending on size'], window.change_autorotation) self._actiongroup.add_actions([ ('about', Gtk.STOCK_ABOUT, _('_About'), None, None, dialog_handler.open_dialog)], (window, 'about-dialog')) self._actiongroup.add_actions([ ('comments', 'mcomix-comments', _('Co_mments...'), None, None, dialog_handler.open_dialog)], (window, 'comments-dialog')) self._actiongroup.add_actions([ ('properties', Gtk.STOCK_PROPERTIES, _('Proper_ties'), None, None, dialog_handler.open_dialog)], (window,'properties-dialog')) self._actiongroup.add_actions([ ('preferences', Gtk.STOCK_PREFERENCES, _('Pr_eferences'), None, None, preferences_dialog.open_dialog)], window) # Some actions added separately since they need extra arguments. self._actiongroup.add_actions([ ('edit_archive', Gtk.STOCK_EDIT, _('_Edit archive...'), None, _('Opens the archive editor.'), edit_dialog.open_dialog), ('open', Gtk.STOCK_OPEN, _('_Open...'), None, None, file_chooser_main_dialog.open_main_filechooser_dialog), ('enhance_image', 'mcomix-enhance-image', _('En_hance image...'), None, None, enhance_dialog.open_dialog)], window) self._actiongroup.add_actions([ ('library', 'mcomix-library', _('_Library...'), None, None, library_main_dialog.open_dialog)], window) # fix some gtk magic: removing unreqired accelerators Gtk.AccelMap.change_entry('/mcomix-main/%s' % 'close', 0, 0, True) ui_description = """ """ self.add_ui_from_string(ui_description) self.insert_action_group(self._actiongroup, 0) self.bookmarks = bookmark_menu.BookmarksMenu(self, window) self.get_widget('/Menu/menu_bookmarks').set_submenu(self.bookmarks) self.get_widget('/Menu/menu_bookmarks').show() self.bookmarks_popup = bookmark_menu.BookmarksMenu(self, window) self.get_widget('/Popup/menu_bookmarks_popup').set_submenu(self.bookmarks_popup) self.get_widget('/Popup/menu_bookmarks_popup').show() self.recent = recent.RecentFilesMenu(self, window) self.get_widget('/Menu/menu_file/menu_recent').set_submenu(self.recent) self.get_widget('/Menu/menu_file/menu_recent').show() self.recentPopup = recent.RecentFilesMenu(self, window) self.get_widget('/Popup/menu_recent').set_submenu(self.recentPopup) self.get_widget('/Popup/menu_recent').show() openwith = openwith_menu.OpenWithMenu(self, window) self.get_widget('/Menu/menu_file/menu_open_with').set_submenu(openwith) self.get_widget('/Menu/menu_file/menu_open_with').show() openwith = openwith_menu.OpenWithMenu(self, window) self.get_widget('/Popup/menu_open_with_popup').set_submenu(openwith) self.get_widget('/Popup/menu_open_with_popup').show() window.add_accel_group(self.get_accel_group()) # Is there no built-in way to do this? self.get_widget('/Tool/expander').set_expand(True) self.get_widget('/Tool/expander').set_sensitive(False) def set_sensitivities(self): """Sets the main UI's widget's sensitivities appropriately.""" general = ('properties', 'edit_archive', 'extract_page', 'save_and_quit', 'close', 'delete', 'copy_page', 'slideshow', 'rotate_90', 'rotate_180', 'rotate_270', 'flip_horiz', 'flip_vert', 'next_page', 'previous_page', 'first_page', 'last_page', 'go_to', 'refresh_archive', 'next_archive', 'previous_archive', 'next_directory', 'previous_directory', 'keep_transformation', 'enhance_image') comment = ('comments',) general_sensitive = False comment_sensitive = False if self._window.filehandler.file_loaded: general_sensitive = True if self._window.filehandler.get_number_of_comments(): comment_sensitive = True for name in general: self._actiongroup.get_action(name).set_sensitive(general_sensitive) for name in comment: self._actiongroup.get_action(name).set_sensitive(comment_sensitive) self.bookmarks.set_sensitive(general_sensitive) self.bookmarks_popup.set_sensitive(general_sensitive) # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0 mcomix-3.1.0/mcomix/version_tools.py0000644000175000017500000000021514476523373017270 0ustar00moritzmoritz"""Provide an imported version-comparison class""" from mcomix._vendor.packaging.version import LegacyVersion __all__ = ["LegacyVersion"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694148347.0 mcomix-3.1.0/mcomix/worker_thread.py0000644000175000017500000001227514476523373017234 0ustar00moritzmoritz""" Worker thread class. """ import threading import traceback from mcomix import log from mcomix.i18n import _ class WorkerThread(object): def __init__(self, process_order, name=None, max_threads=1, sort_orders=False, unique_orders=False): """Create a new pool of worker threads. Optional will be added to spawned thread names. will be called to process each work order. At most will be started for processing. If is True, the orders queue will be sorted after each addition. If is True, duplicate orders will not be added to the queue. """ self._name = name self._process_order = process_order self._max_threads = max_threads self._sort_orders = sort_orders self._unique_orders = unique_orders self._stop = False self._threads = [] # Queue of orders waiting for processing. self._orders_queue = [] if self._unique_orders: # Track orders. self._orders_set = set() self._condition = threading.Condition() def __enter__(self): return self._condition.__enter__() def __exit__(self, exc_type, exc_value, traceback): return self._condition.__exit__(exc_type, exc_value, traceback) def _start(self, nb_threads=1): for n in range(nb_threads): if len(self._threads) == self._max_threads: break thread = threading.Thread(target=self._run) if self._name is not None: thread.name += '-' + self._name thread.setDaemon(False) thread.start() self._threads.append(thread) def _order_uid(self, order): if isinstance(order, tuple) or isinstance(order, list): return order[0] return order def _run(self): order_uid = None while True: with self._condition: if order_uid is not None: self._orders_set.remove(order_uid) while not self._stop and 0 == len(self._orders_queue): self._condition.wait() if self._stop: return order = self._orders_queue.pop(0) if self._unique_orders: order_uid = self._order_uid(order) try: self._process_order(order) except Exception as e: log.error(_('! Worker thread processing %(function)r failed: %(error)s'), { 'function' : self._process_order, 'error' : e }) log.debug('Traceback:\n%s', traceback.format_exc()) def must_stop(self): """Return true if we've been asked to stop processing. Can be used by the processing function to check if it must abort early. """ return self._stop def clear_orders(self): """Clear the current orders queue.""" with self._condition: if self._unique_orders: # We can't just clear the set, as some orders # can be in the process of being processed. for order in self._orders_queue: order_uid = self._order_uid(order) self._orders_set.remove(order_uid) self._orders_queue = [] def append_order(self, order): """Append work order to the thread orders queue.""" with self._condition: if self._unique_orders: order_uid = self._order_uid(order) if order_uid in self._orders_set: # Duplicate order. return self._orders_set.add(order_uid) self._orders_queue.append(order) if self._sort_orders: self._orders_queue.sort() self._condition.notifyAll() self._start() def extend_orders(self, orders_list): """Append work orders to the thread orders queue.""" with self._condition: if self._unique_orders: nb_added = 0 for order in orders_list: order_uid = self._order_uid(order) if order_uid in self._orders_set: # Duplicate order. continue self._orders_set.add(order_uid) self._orders_queue.append(order) nb_added += 1 else: self._orders_queue.extend(orders_list) nb_added = len(orders_list) if 0 == nb_added: return if self._sort_orders: self._orders_queue.sort() self._condition.notifyAll() self._start(nb_threads=nb_added) def stop(self): """Stop the worker threads and flush the orders queue.""" self._stop = True with self._condition: self._condition.notifyAll() for thread in self._threads: thread.join() self._threads = [] self._stop = False self._orders_queue = [] if self._unique_orders: self._orders_set.clear() # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1705863135.0 mcomix-3.1.0/mcomix/zoom.py0000644000175000017500000003514414553263737015361 0ustar00moritzmoritz""" Handles zoom and fit of images in the main display area. """ import operator from mcomix import constants from mcomix.preferences import prefs from mcomix import tools from mcomix import box from functools import reduce from typing import List, Tuple, Sequence IDENTITY_ZOOM = 1.0 IDENTITY_ZOOM_LOG = 0 USER_ZOOM_LOG_SCALE1 = 4.0 MIN_USER_ZOOM_LOG = -20 MAX_USER_ZOOM_LOG = 12 class ZoomModel(object): """ Handles zoom and fit modes. """ def __init__(self) -> None: #: User zoom level. self._user_zoom_log = IDENTITY_ZOOM_LOG #: Image fit mode. Determines the base zoom level for an image by #: calculating its maximum size. self._fitmode = constants.ZoomMode.MANUAL self._scale_up = False def set_fit_mode(self, fitmode: int) -> None: if fitmode < constants.ZoomMode.BEST or \ fitmode > constants.ZoomMode.SIZE: raise ValueError("No fit mode for id %d." % fitmode) self._fitmode = fitmode def get_scale_up(self) -> bool: return self._scale_up def set_scale_up(self, scale_up: bool) -> None: self._scale_up = scale_up def _set_user_zoom_log(self, zoom_log: int) -> None: self._user_zoom_log = min(max(zoom_log, MIN_USER_ZOOM_LOG), MAX_USER_ZOOM_LOG) def zoom_in(self) -> None: self._set_user_zoom_log(self._user_zoom_log + 1) def zoom_out(self) -> None: self._set_user_zoom_log(self._user_zoom_log - 1) def reset_user_zoom(self) -> None: self._set_user_zoom_log(IDENTITY_ZOOM_LOG) def get_zoomed_size(self, image_sizes: List[Sequence[int]], screen_size: Tuple[int, int], distribution_axis: constants.PageAxis, do_not_transform: List[bool], prefer_same_size: bool, fit_same_size: bool) -> Tuple[int, int]: scale_up = self._scale_up if prefer_same_size: # Preprocessing step: scale all images to the same size image_boxes = [box.Box(s) for s in image_sizes] # Scale up to the same size if this is allowed, otherwise scale down. if scale_up: # Scale up to union. pre_limits = box.Box.bounding_box(image_boxes).get_size() else: # Scale down to intersection. pre_limits = reduce(box.Box.intersect, image_boxes, image_boxes[0]).get_size() new_image_sizes = [tuple(tools.scale(s, ZoomModel._preferred_scale( s, pre_limits, distribution_axis))) for s in image_sizes] new_image_sizes2 = [new_image_sizes[i] if not do_not_transform[i] else image_sizes[i] for i in range(len(new_image_sizes))] image_sizes = new_image_sizes2 union_size = _union_size(image_sizes, distribution_axis) limits = ZoomModel._calc_limits(union_size, screen_size, self._fitmode, scale_up) prefscale = ZoomModel._preferred_scale(union_size, limits, distribution_axis) preferred_scales = tuple([prefscale if not dnt else IDENTITY_ZOOM for dnt in do_not_transform]) prescaled = list(map(lambda size, scale, dnt: tuple(_scale_image_size(size, scale)), image_sizes, preferred_scales, do_not_transform)) prescaled_union_size = _union_size(prescaled, distribution_axis) def _other_preferences(limits: Sequence[int], distribution_axis: constants.PageAxis) -> bool: for i in range(len(limits)): if i == distribution_axis: continue if limits[i] is not None: return True return False other_preferences = _other_preferences(limits, distribution_axis) if limits[distribution_axis] is not None and \ (prescaled_union_size[distribution_axis] > screen_size[distribution_axis] or not other_preferences): distributed_scales = ZoomModel._scale_distributed(image_sizes, distribution_axis, limits[distribution_axis], scale_up, do_not_transform) if other_preferences: preferred_scales = list(map(min, preferred_scales, distributed_scales)) else: preferred_scales = distributed_scales if not scale_up: preferred_scales = [min(x, IDENTITY_ZOOM) for x in preferred_scales] user_scale = 2 ** (self._user_zoom_log / USER_ZOOM_LOG_SCALE1) res_scales = [preferred_scales[i] * (user_scale if not do_not_transform[i] else IDENTITY_ZOOM) for i in range(len(preferred_scales))] res = list(map(lambda size, scale: list(_scale_image_size(size, scale)), image_sizes, res_scales)) distorted = [False] * len(res) if prefer_same_size and fit_same_size: # While the algorithm so far tries hard to keep the aspect ratios of the # original images, in extreme cases, it is not possible to both keep aspect # ratios as well as make the images fit to the same size, especially after # applying user_scale. In those cases, we will make them fit. # Simple approach: For each dimension, we fit each image to either the # minimum size (if scale_up is false) or maximum size (if scale_up is true) # of all images, given the scaled sizes computed so far. op = operator.gt if scale_up else operator.lt exs = [None] * len(limits) for d in range(len(limits)): if d == distribution_axis: continue for i in res: if exs[d] is None or op(i[d], exs[d]): exs[d] = i[d] for d in range(len(limits)): if d == distribution_axis: continue for i in range(len(res)): if (res[i][d] != exs[d]) and not do_not_transform[i]: res[i][d] = exs[d] distorted[i] = True return (res, distorted) @staticmethod def _preferred_scale(image_size, limits, distribution_axis): """ Returns scale that makes an image of size image_size respect the limits imposed by limits. If no proper value can be determined, IDENTITY_ZOOM is returned. """ min_scale = None for i in range(len(limits)): if i == distribution_axis: continue l = limits[i] if l is None: continue s = tools.div(l, image_size[i]) if min_scale is None or s < min_scale: min_scale = s if min_scale is None: min_scale = IDENTITY_ZOOM return min_scale @staticmethod def _calc_limits(union_size, screen_size, fitmode, allow_upscaling): """ Returns a list or a tuple with the i-th element set to int x if fitmode limits the size at the i-th axis to x, or None if fitmode has no preference for this axis. """ manual = fitmode == constants.ZoomMode.MANUAL if fitmode == constants.ZoomMode.BEST or \ (manual and allow_upscaling and all(tools.smaller(union_size, screen_size))): return screen_size if fitmode == constants.ZoomMode.SIZE: if union_size[constants.PageAxis.WIDTH] > union_size[constants.PageAxis.HEIGHT]: return [int(prefs['fit to size width wide']), int(prefs['fit to size height wide'])] else: return [int(prefs['fit to size width other']), int(prefs['fit to size height other'])] result = [None] * len(screen_size) if not manual: fixed_size = None if fitmode == constants.ZoomMode.WIDTH: axis = constants.PageAxis.WIDTH elif fitmode == constants.ZoomMode.HEIGHT: axis = constants.PageAxis.HEIGHT else: assert False, 'Cannot map fitmode to axis' result[axis] = fixed_size if fixed_size is not None else screen_size[axis] return result @staticmethod def _scale_distributed(sizes, axis, max_size, allow_upscaling, do_not_transform): """ Calculates scales for a list of boxes that are distributed along a given axis (without any gaps). If the resulting scales are applied to their respective boxes, their new total size along axis will be as close as possible to max_size. The current implementation ensures that equal box sizes are mapped to equal scales. @param sizes: A list of box sizes. @param axis: The axis along which those boxes are distributed. @param max_size: The maximum size the scaled boxes may have along axis. @param allow_upscaling: True if upscaling is allowed, False otherwise. @param do_not_transform: True if the resulting scale must be 1, False otherwise. @return: A list of scales where the i-th scale belongs to the i-th box size. If sizes is empty, the empty list is returned. If there are more boxes than max_size, an approximation is returned where all resulting scales will shrink their respective boxes to 1 along axis. In this case, the scaled total size might be greater than max_size. """ n = len(sizes) # trivial cases first if n == 0: return [] if n >= max_size: # In this case, only one solution or only an approximation is available. # if n > max_size, the result won't fit into max_size. return [IDENTITY_ZOOM if dnt else tools.div(1, s[axis]) for s, dnt in zip(sizes, do_not_transform)] total_axis_size = sum([s[axis] for s in sizes]) total_dnt_axis_size = sum([s[axis] for s, dnt in zip(sizes, do_not_transform) if dnt]) if ((total_axis_size <= max_size) and not allow_upscaling) or \ (total_axis_size == total_dnt_axis_size): # identity return [IDENTITY_ZOOM] * n # non-trival case # initial guess scale = tools.div(max_size - total_dnt_axis_size, total_axis_size - total_dnt_axis_size) scaling_data = [None] * n total_axis_size = 0 # This loop collects some data we need for the actual computations later. for i in range(n): this_size = sizes[i] # Shortcut: If the size cannot be changed, accept the original size. if do_not_transform[i]: total_axis_size += this_size[axis] scaling_data[i] = [IDENTITY_ZOOM, IDENTITY_ZOOM, False, IDENTITY_ZOOM, 0.0] continue # Initial guess: The current scale works for all tuples. ideal = tools.scale(this_size, scale) ideal_vol = tools.volume(ideal) # Let's use a dummy to compute the actual (rounded) size along axis # so we can rescale the rounded tuple with a better local_scale # later. This rescaling is necessary to ensure that the sizes in ALL # dimensions are monotonically scaled (with respect to local_scale). # A nice side effect of this is that it keeps the aspect ratio better. dummy_approx = _round_nonempty((ideal[axis],))[0] local_scale = tools.div(dummy_approx, this_size[axis]) total_axis_size += dummy_approx can_be_downscaled = dummy_approx > 1 if can_be_downscaled: forced_size = dummy_approx - 1 forced_scale = tools.div(forced_size, this_size[axis]) forced_approx = _scale_image_size(this_size, forced_scale) forced_vol_err = tools.relerr(tools.volume(forced_approx), ideal_vol) else: forced_scale = None forced_vol_err = None scaling_data[i] = [local_scale, ideal, can_be_downscaled, forced_scale, forced_vol_err] # Now we need to find at most total_axis_size - max_size occasions to # scale down some tuples so the whole thing would fit into max_size. If # we are lucky, there will be no gaps at the end (or at least fewer gaps # than we would have if we always rounded down). dirty=True # This flag prevents infinite loops if nothing can be made any smaller. while dirty and (total_axis_size > max_size): # This algorithm needs O(n*n) time. Let's hope that n is small enough. dirty=False current_index = 0 current_min = None for i in range(n): d = scaling_data[i] if not d[2]: # Ignore elements that cannot be made any smaller. continue if (current_min is None) or (d[4] < current_min[4]): # We are searching for the tuple where downscaling results # in the smallest relative volume error (compared to the # respective ideal volume). current_min = d current_index = i for i in range(current_index, n): # We must scale down ALL equal tuples. Otherwise, images that # are of equal size might appear to be of different size # afterwards. The downside of this approach is that it might # introduce more gaps than necessary. d = scaling_data[i] if (not d[2]) or (d[1] != current_min[1]): continue d[0] = d[3] d[2] = False # only once per tuple total_axis_size -= 1 dirty=True else: # If we are here and total_axis_size < max_size, we could try to # upscale some tuples similarly to the other loop (i.e. smallest # relative volume error first, equal boxes in conjunction with each # other). However, this is not as useful as the other loop, slightly # more complicated and it won't do anything if all tuples are equal. pass return [d[0] for d in scaling_data] def _scale_image_size(size, scale): return _round_nonempty(tools.scale(size, scale)) def _round_nonempty(t): result = [0] * len(t) for i in range(len(t)): x = int(round(t[i])) result[i] = x if x > 0 else 1 return result def _union_size(image_sizes, distribution_axis): if len(image_sizes) == 0: return [] n = len(image_sizes[0]) union_size = [reduce(max, [x[i] for x in image_sizes]) for i in range(n)] union_size[distribution_axis] = sum([x[distribution_axis] for x in image_sizes]) return union_size # vim: expandtab:sw=4:ts=4 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768 mcomix-3.1.0/mcomix.egg-info/0000755000175000017500000000000014553265237015503 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1705863838.0 mcomix-3.1.0/mcomix.egg-info/PKG-INFO0000644000175000017500000005725314553265236016613 0ustar00moritzmoritzMetadata-Version: 2.1 Name: mcomix Version: 3.1.0 Summary: GTK comic book viewer Author: Pontus Ekberg Maintainer: The MComix Team License: MComix is licensed under the terms of the GNU General Public License, which can be found below, or at http://www.gnu.org/licenses/gpl-2.0.html. --- GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. Project-URL: Homepage, https://mcomix.sourceforge.io Project-URL: Documentation, https://sourceforge.net/projects/mcomix/Wiki/Home/ Project-URL: Repository, https://sourceforge.net/p/mcomix/git/ci/master/tree/ Project-URL: Changelog, https://sourceforge.net/p/mcomix/news/ Keywords: comix,comics,manga,images,reader,image viewer,cbr,cbz Classifier: Development Status :: 6 - Mature Classifier: Environment :: X11 Applications :: GTK Classifier: Intended Audience :: End Users/Desktop Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2) Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: POSIX :: BSD Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Multimedia :: Graphics :: Viewers Requires-Python: >=3.7 Description-Content-Type: text/markdown License-File: COPYING Requires-Dist: PyGObject>=3.36.0 Requires-Dist: pycairo>=1.16.0 Requires-Dist: Pillow>=6.0.0 Provides-Extra: fileformats Requires-Dist: chardet; extra == "fileformats" Requires-Dist: PyMuPDF>=1.19.2; extra == "fileformats" Provides-Extra: dev Requires-Dist: pyinstaller; os_name == "nt" and extra == "dev" Requires-Dist: build; extra == "dev" Requires-Dist: pip-review; extra == "dev" Requires-Dist: python-lsp-server[flake8]; extra == "dev" Requires-Dist: pylsp-mypy; extra == "dev" Requires-Dist: pyls-isort; extra == "dev" Requires-Dist: python-lsp-black; extra == "dev" Requires-Dist: types-Pillow; extra == "dev" Requires-Dist: pygobject-stubs; extra == "dev" # MComix README ## About MComix is a user-friendly, customizable image viewer. It is specifically designed to handle comic books (both Western comics and manga) and supports a variety of container formats. MComix is a fork of Comix. It is written in Python and uses GTK 3 through the PyGObject bindings. ## Installation Please follow the [installation instructions](https://sourceforge.net/p/mcomix/wiki/Installation/) on the Wiki. Most users will find it convenient to use the package provided by their operating system package manager. ## Dependencies For a list of packages and libraries needed to run MComix, please refer to [our documentation](https://sourceforge.net/p/mcomix/wiki/Home/#Dependencies). ## Credits Thanks to everyone who have contributed translations, suggestions, bug reports, fixes and donations! Icons with a filename starting with "gimp" are taken from The GIMP, and icons with a filename starting with "tango" are taken from the Tango Desktop Project. Most other icons are made by Victor Castillejo, creator of the GNOME-Colors icon theme. The directory mcomix/_vendor/packaging/ contains portions of 'packaging' version 21.0, (c) Donald Stufft and individual contributors. The packaging code is made available under the terms of either the Apache 2.0 license or BSD 2-clause license (user's choice). See mcomix/_vendor/packaging-21.0.dist-info/LICENSE for details. ## Contact Please use the [issue tracker](https://sourceforge.net/p/mcomix/_list/tickets) to get in touch with the MComix developers. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1705863838.0 mcomix-3.1.0/mcomix.egg-info/SOURCES.txt0000644000175000017500000002000314553265236017361 0ustar00moritzmoritzCOPYING ChangeLog.md MANIFEST.in README.md pyproject.toml mcomix/__init__.py mcomix/__main__.py mcomix/about_dialog.py mcomix/archive_extractor.py mcomix/archive_packer.py mcomix/archive_tools.py mcomix/bookmark_backend.py mcomix/bookmark_dialog.py mcomix/bookmark_menu.py mcomix/bookmark_menu_item.py mcomix/box.py mcomix/callback.py mcomix/clipboard.py mcomix/comment_dialog.py mcomix/constants.py mcomix/cursor_handler.py mcomix/dialog_handler.py mcomix/edit_comment_area.py mcomix/edit_dialog.py mcomix/edit_image_area.py mcomix/enhance_backend.py mcomix/enhance_dialog.py mcomix/event.py mcomix/file_chooser_base_dialog.py mcomix/file_chooser_library_dialog.py mcomix/file_chooser_main_dialog.py mcomix/file_chooser_simple_dialog.py mcomix/file_handler.py mcomix/file_provider.py mcomix/histogram.py mcomix/i18n.py mcomix/icons.py mcomix/image_handler.py mcomix/image_tools.py mcomix/keybindings.py mcomix/keybindings_editor.py mcomix/labels.py mcomix/last_read_page.py mcomix/layout.py mcomix/lens.py mcomix/log.py mcomix/main.py mcomix/message_dialog.py mcomix/openwith.py mcomix/openwith_menu.py mcomix/osd.py mcomix/pageselect.py mcomix/portability.py mcomix/preferences.py mcomix/preferences_dialog.py mcomix/preferences_page.py mcomix/preferences_section.py mcomix/process.py mcomix/properties_dialog.py mcomix/properties_page.py mcomix/recent.py mcomix/run.py mcomix/scrolling.py mcomix/slideshow.py mcomix/status.py mcomix/strings.py mcomix/thumbbar.py mcomix/thumbnail_tools.py mcomix/thumbnail_view.py mcomix/tools.py mcomix/transform.py mcomix/ui.py mcomix/version_tools.py mcomix/worker_thread.py mcomix/zoom.py mcomix.egg-info/PKG-INFO mcomix.egg-info/SOURCES.txt mcomix.egg-info/dependency_links.txt mcomix.egg-info/entry_points.txt mcomix.egg-info/requires.txt mcomix.egg-info/top_level.txt mcomix/_vendor/packaging/__about__.py mcomix/_vendor/packaging/__init__.py mcomix/_vendor/packaging/_structures.py mcomix/_vendor/packaging/py.typed mcomix/_vendor/packaging/version.py mcomix/archive/__init__.py mcomix/archive/archive_base.py mcomix/archive/archive_recursive.py mcomix/archive/lha_external.py mcomix/archive/mobi.py mcomix/archive/password.py mcomix/archive/pdf_external.py mcomix/archive/pdf_multi.py mcomix/archive/rar.py mcomix/archive/rar_external.py mcomix/archive/sevenzip_external.py mcomix/archive/tar.py mcomix/archive/zip.py mcomix/archive/zip_external.py mcomix/archive/native_pdf/__init__.py mcomix/archive/native_pdf/child.py mcomix/archive/native_pdf/manager.py mcomix/archive/native_pdf/parent.py mcomix/images/__init__.py mcomix/images/comments.png mcomix/images/double-page.png mcomix/images/fitbest.png mcomix/images/fitheight.png mcomix/images/fitmanual.png mcomix/images/fitsize.png mcomix/images/fitwidth.png mcomix/images/gimp-flip-horizontal.png mcomix/images/gimp-flip-vertical.png mcomix/images/gimp-rotate-180.png mcomix/images/gimp-rotate-270.png mcomix/images/gimp-rotate-90.png mcomix/images/gimp-thumbnails.png mcomix/images/gimp-transform.png mcomix/images/library.png mcomix/images/magnifyingglass.png mcomix/images/manga.png mcomix/images/mcomix-16.png mcomix/images/mcomix-22.png mcomix/images/mcomix-24.png mcomix/images/mcomix-256.png mcomix/images/mcomix-32.png mcomix/images/mcomix-48.png mcomix/images/mcomix.ico mcomix/images/mcomix.png mcomix/images/mcomix.svg mcomix/images/tango-add-bookmark.png mcomix/images/tango-archive.png mcomix/images/tango-enhance-image.png mcomix/images/tango-image.png mcomix/images/zoom.png mcomix/library/__init__.py mcomix/library/add_progress_dialog.py mcomix/library/backend.py mcomix/library/backend_types.py mcomix/library/book_area.py mcomix/library/collection_area.py mcomix/library/control_area.py mcomix/library/main_dialog.py mcomix/library/pixbuf_cache.py mcomix/library/watchlist.py mcomix/messages/__init__.py mcomix/messages/ca/__init__.py mcomix/messages/ca/LC_MESSAGES/__init__.py mcomix/messages/ca/LC_MESSAGES/mcomix.mo mcomix/messages/cs/__init__.py mcomix/messages/cs/LC_MESSAGES/__init__.py mcomix/messages/cs/LC_MESSAGES/mcomix.mo mcomix/messages/de/__init__.py mcomix/messages/de/LC_MESSAGES/__init__.py mcomix/messages/de/LC_MESSAGES/mcomix.mo mcomix/messages/el/__init__.py mcomix/messages/el/LC_MESSAGES/__init__.py mcomix/messages/el/LC_MESSAGES/mcomix.mo mcomix/messages/es/__init__.py mcomix/messages/es/LC_MESSAGES/__init__.py mcomix/messages/es/LC_MESSAGES/mcomix.mo mcomix/messages/fa/__init__.py mcomix/messages/fa/LC_MESSAGES/__init__.py mcomix/messages/fa/LC_MESSAGES/mcomix.mo mcomix/messages/fr/__init__.py mcomix/messages/fr/LC_MESSAGES/__init__.py mcomix/messages/fr/LC_MESSAGES/mcomix.mo mcomix/messages/gl/__init__.py mcomix/messages/gl/LC_MESSAGES/__init__.py mcomix/messages/gl/LC_MESSAGES/mcomix.mo mcomix/messages/he/__init__.py mcomix/messages/he/LC_MESSAGES/__init__.py mcomix/messages/he/LC_MESSAGES/mcomix.mo mcomix/messages/hr/__init__.py mcomix/messages/hr/LC_MESSAGES/__init__.py mcomix/messages/hr/LC_MESSAGES/mcomix.mo mcomix/messages/hu/__init__.py mcomix/messages/hu/LC_MESSAGES/__init__.py mcomix/messages/hu/LC_MESSAGES/mcomix.mo mcomix/messages/id/__init__.py mcomix/messages/id/LC_MESSAGES/__init__.py mcomix/messages/id/LC_MESSAGES/mcomix.mo mcomix/messages/it/__init__.py mcomix/messages/it/LC_MESSAGES/__init__.py mcomix/messages/it/LC_MESSAGES/mcomix.mo mcomix/messages/ja/__init__.py mcomix/messages/ja/LC_MESSAGES/__init__.py mcomix/messages/ja/LC_MESSAGES/mcomix.mo mcomix/messages/ko/__init__.py mcomix/messages/ko/LC_MESSAGES/__init__.py mcomix/messages/ko/LC_MESSAGES/mcomix.mo mcomix/messages/lt/__init__.py mcomix/messages/lt/LC_MESSAGES/__init__.py mcomix/messages/lt/LC_MESSAGES/mcomix.mo mcomix/messages/nl/__init__.py mcomix/messages/nl/LC_MESSAGES/__init__.py mcomix/messages/nl/LC_MESSAGES/mcomix.mo mcomix/messages/pl/__init__.py mcomix/messages/pl/LC_MESSAGES/__init__.py mcomix/messages/pl/LC_MESSAGES/mcomix.mo mcomix/messages/pt_BR/__init__.py mcomix/messages/pt_BR/LC_MESSAGES/__init__.py mcomix/messages/pt_BR/LC_MESSAGES/mcomix.mo mcomix/messages/ru/__init__.py mcomix/messages/ru/LC_MESSAGES/__init__.py mcomix/messages/ru/LC_MESSAGES/mcomix.mo mcomix/messages/sv/__init__.py mcomix/messages/sv/LC_MESSAGES/__init__.py mcomix/messages/sv/LC_MESSAGES/mcomix.mo mcomix/messages/uk/__init__.py mcomix/messages/uk/LC_MESSAGES/__init__.py mcomix/messages/uk/LC_MESSAGES/mcomix.mo mcomix/messages/zh_CN/__init__.py mcomix/messages/zh_CN/LC_MESSAGES/__init__.py mcomix/messages/zh_CN/LC_MESSAGES/mcomix.mo mcomix/messages/zh_TW/__init__.py mcomix/messages/zh_TW/LC_MESSAGES/__init__.py mcomix/messages/zh_TW/LC_MESSAGES/mcomix.mo share/applications/mcomix.desktop share/icons/hicolor/16x16/apps/mcomix.png share/icons/hicolor/16x16/mimetypes/application-x-cb7.png share/icons/hicolor/16x16/mimetypes/application-x-cbr.png share/icons/hicolor/16x16/mimetypes/application-x-cbt.png share/icons/hicolor/16x16/mimetypes/application-x-cbz.png share/icons/hicolor/22x22/apps/mcomix.png share/icons/hicolor/22x22/mimetypes/application-x-cb7.png share/icons/hicolor/22x22/mimetypes/application-x-cbr.png share/icons/hicolor/22x22/mimetypes/application-x-cbt.png share/icons/hicolor/22x22/mimetypes/application-x-cbz.png share/icons/hicolor/24x24/apps/mcomix.png share/icons/hicolor/24x24/mimetypes/application-x-cb7.png share/icons/hicolor/24x24/mimetypes/application-x-cbr.png share/icons/hicolor/24x24/mimetypes/application-x-cbt.png share/icons/hicolor/24x24/mimetypes/application-x-cbz.png share/icons/hicolor/256x256/apps/mcomix.png share/icons/hicolor/32x32/apps/mcomix.png share/icons/hicolor/32x32/mimetypes/application-x-cb7.png share/icons/hicolor/32x32/mimetypes/application-x-cbr.png share/icons/hicolor/32x32/mimetypes/application-x-cbt.png share/icons/hicolor/32x32/mimetypes/application-x-cbz.png share/icons/hicolor/48x48/apps/mcomix.png share/icons/hicolor/48x48/mimetypes/application-x-cb7.png share/icons/hicolor/48x48/mimetypes/application-x-cbr.png share/icons/hicolor/48x48/mimetypes/application-x-cbt.png share/icons/hicolor/48x48/mimetypes/application-x-cbz.png share/icons/hicolor/scalable/apps/mcomix.svg share/man/man1/mcomix.1.gz share/metainfo/mcomix.metainfo.xml share/mime/packages/mcomix.xml././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1705863838.0 mcomix-3.1.0/mcomix.egg-info/dependency_links.txt0000644000175000017500000000000114553265236021550 0ustar00moritzmoritz ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1705863838.0 mcomix-3.1.0/mcomix.egg-info/entry_points.txt0000644000175000017500000000006014553265236020774 0ustar00moritzmoritz[console_scripts] mcomix = mcomix.__main__:main ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1705863838.0 mcomix-3.1.0/mcomix.egg-info/requires.txt0000644000175000017500000000036014553265236020101 0ustar00moritzmoritzPyGObject>=3.36.0 pycairo>=1.16.0 Pillow>=6.0.0 [dev] build pip-review python-lsp-server[flake8] pylsp-mypy pyls-isort python-lsp-black types-Pillow pygobject-stubs [dev:os_name == "nt"] pyinstaller [fileformats] chardet PyMuPDF>=1.19.2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1705863838.0 mcomix-3.1.0/mcomix.egg-info/top_level.txt0000644000175000017500000000000714553265236020231 0ustar00moritzmoritzmcomix ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694889147.0 mcomix-3.1.0/pyproject.toml0000644000175000017500000000375614501372273015434 0ustar00moritzmoritz[project] name = "mcomix" dynamic = ["version"] description = "GTK comic book viewer" readme = {file = "README.md", content-type = "text/markdown"} requires-python = ">=3.7" license = {file = "COPYING"} authors = [{name = "Pontus Ekberg"}] maintainers = [{name = "The MComix Team"}] keywords = ["comix", "comics", "manga", "images", "reader", "image viewer", "cbr", "cbz"] classifiers = [ "Development Status :: 6 - Mature", "Environment :: X11 Applications :: GTK", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Operating System :: POSIX :: BSD", "Programming Language :: Python :: 3", "Topic :: Multimedia :: Graphics :: Viewers" ] dependencies = [ "PyGObject>=3.36.0", "pycairo>=1.16.0", "Pillow>=6.0.0", ] [project.optional-dependencies] fileformats = [ "chardet", "PyMuPDF>=1.19.2" ] dev = [ # For building Windows package "pyinstaller; os_name == 'nt'", # For creating source package "build", # For updating virtual environment "pip-review", # For language server/IDE "python-lsp-server[flake8]", "pylsp-mypy", "pyls-isort", "python-lsp-black", # For mypy "types-Pillow", "pygobject-stubs" ] [project.urls] Homepage = "https://mcomix.sourceforge.io" Documentation = "https://sourceforge.net/projects/mcomix/Wiki/Home/" Repository = "https://sourceforge.net/p/mcomix/git/ci/master/tree/" Changelog = "https://sourceforge.net/p/mcomix/news/" [project.scripts] mcomix = "mcomix.__main__:main" [build-system] requires = ["setuptools>=61.0.0", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] include = ["mcomix*"] [tool.setuptools.dynamic] version = {attr = "mcomix.constants.VERSION"} [tool.mypy] disallow_untyped_defs = true disallow_any_unimported = true no_implicit_optional = true check_untyped_defs = true warn_return_any = true show_error_codes = true warn_unused_ignores = true ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768 mcomix-3.1.0/setup.cfg0000644000175000017500000000004614553265237014336 0ustar00moritzmoritz[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9674768 mcomix-3.1.0/share/0000755000175000017500000000000014553265237013617 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768 mcomix-3.1.0/share/applications/0000755000175000017500000000000014553265237016305 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694848160.0 mcomix-3.1.0/share/applications/mcomix.desktop0000644000175000017500000000173614501252240021162 0ustar00moritzmoritz[Desktop Entry] Version=1.0 Name=MComix Name[it]=MComix GenericName=Comic Book Viewer GenericName[sv]=Serieboksläsare GenericName[fr]=Visionneur de bandes dessinées GenericName[de]=Comic-Betrachter Comment=A viewer for comic book archives Comment[it]=Un visualizzatore di fumetti Comment[es]=Un visor de comics Comment[sv]=En serieboksläsare Comment[fr]=Visionneur d'images spécialisé dans la lecture des bandes dessinées Comment[pl]=Przeglądarka komiksów Comment[de]=Betrachter für Comic-Archive Exec=mcomix %f Icon=mcomix Terminal=false Type=Application StartupNotify=true Categories=Graphics;Viewer; MimeType=application/x-cb7;application/x-ext-cb7;application/x-cbr;application/x-ext-cbr;application/x-cbt;application/x-ext-cbt;application/x-cbz;application/x-ext-cbz;application/pdf;application/x-pdf;application/x-ext-pdf;image/bmp;image/x-MS-bmp;image/x-bmp;image/gif;image/jpeg;image/png;image/tiff;image/x-portable-bitmap;image/x-portable-graymap;image/x-portable-pixmap; ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9574769 mcomix-3.1.0/share/icons/0000755000175000017500000000000014553265237014732 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9674768 mcomix-3.1.0/share/icons/hicolor/0000755000175000017500000000000014553265237016371 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9574769 mcomix-3.1.0/share/icons/hicolor/16x16/0000755000175000017500000000000014553265237017156 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768 mcomix-3.1.0/share/icons/hicolor/16x16/apps/0000755000175000017500000000000014553265237020121 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/icons/hicolor/16x16/apps/mcomix.png0000644000175000017500000000133114477111712022112 0ustar00moritzmoritzPNG  IHDR k= pHYsq6tEXtSoftwarewww.inkscape.org<fIDAT(uKTa}/31q|JzY ZD-(Z&APjQ$-SRLqQtΝǝ{6 v~́swN[}y#?ltxՓRIJ*˂z~.=86Rpʑ=B0jsuä+ugkisO Xc! XLsQ'}&2v9r̎=Cۃ10IQzMu%HDe YO`cualz~y#'Jrq29мbP{&0LU^0clo 9st: ;TB$p3P$8 A6R.0H)# aDqq7Df"O" ̂2& D.~Lk j j+=(TDJ^b$ʠ'Ux]y׏D7mHX֩ǗI8}e"<5EE_fn}}RS1- *C +q@>o}xGB}tVKYnfʵx5=uwx)06'GCIENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768 mcomix-3.1.0/share/icons/hicolor/16x16/mimetypes/0000755000175000017500000000000014553265237021172 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/icons/hicolor/16x16/mimetypes/application-x-cb7.png0000644000175000017500000000103414477111712025110 0ustar00moritzmoritzPNG  IHDRabKGDC pHYs B(xtIME IDAT8˭1hTAww=b A I#MR (X( ;H) h,Ebl"*Q]۱x9#xQفٙ4h0lڑВF4ѳWc[6#H>/bJi"/3sV,1;ZEsSpZ8vKo[G-Cb 4ET }W(Pٲ ٰ|r@ UPmҏHV?1(>9! d Bj}Xk`<h~pb+@zP$3_F0׌Q3_Aszy]❇{ 3Al'2=֮G%J=.]q{ klf_o҇)IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/icons/hicolor/16x16/mimetypes/application-x-cbr.png0000644000175000017500000000103414477111712025203 0ustar00moritzmoritzPNG  IHDRabKGDC pHYs B(xtIME IDAT8˭1hTAww=b A I#MR (X( ;H) h,Ebl"*Q]۱x9#xQفٙ4h0lڑВF4ѳWc[6#H>/bJi"/3sV,1;ZEsSpZ8vKo[G-Cb 4ET }W(Pٲ ٰ|r@ UPmҏHV?1(>9! d Bj}Xk`<h~pb+@zP$3_F0׌Q3_Aszy]❇{ 3Al'2=֮G%J=.]q{ klf_o҇)IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/icons/hicolor/16x16/mimetypes/application-x-cbt.png0000644000175000017500000000103414477111712025205 0ustar00moritzmoritzPNG  IHDRabKGDC pHYs B(xtIME IDAT8˭1hTAww=b A I#MR (X( ;H) h,Ebl"*Q]۱x9#xQفٙ4h0lڑВF4ѳWc[6#H>/bJi"/3sV,1;ZEsSpZ8vKo[G-Cb 4ET }W(Pٲ ٰ|r@ UPmҏHV?1(>9! d Bj}Xk`<h~pb+@zP$3_F0׌Q3_Aszy]❇{ 3Al'2=֮G%J=.]q{ klf_o҇)IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/icons/hicolor/16x16/mimetypes/application-x-cbz.png0000644000175000017500000000103414477111712025213 0ustar00moritzmoritzPNG  IHDRabKGDC pHYs B(xtIME IDAT8˭1hTAww=b A I#MR (X( ;H) h,Ebl"*Q]۱x9#xQفٙ4h0lڑВF4ѳWc[6#H>/bJi"/3sV,1;ZEsSpZ8vKo[G-Cb 4ET }W(Pٲ ٰ|r@ UPmҏHV?1(>9! d Bj}Xk`<h~pb+@zP$3_F0׌Q3_Aszy]❇{ 3Al'2=֮G%J=.]q{ klf_o҇)IENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9574769 mcomix-3.1.0/share/icons/hicolor/22x22/0000755000175000017500000000000014553265237017150 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768 mcomix-3.1.0/share/icons/hicolor/22x22/apps/0000755000175000017500000000000014553265237020113 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/icons/hicolor/22x22/apps/mcomix.png0000644000175000017500000000215514477111712022111 0ustar00moritzmoritzPNG  IHDRٱ\ pHYs5tEXtSoftwarewww.inkscape.org<IDAT8[lTU}̙˙3tʴ0)r X4K H@  ^_KB1/>hLPQ4!QSi)ZKN9sf9ۇdg=kRr;B,j_6V`!+F5Z1<#\ѕJ᭪!-56R|TܸB+%+_ )(k1;.taQ"颭 -h+ZbwEThN ^|@Ypt'ZZ#7%F{p+ 3Xf(ٯ(rҲp ru) eJZQ.9NvCH'9?S`Fڡ*nm(;p/ `~n7`4h\1Z}*8zB06Rx3/+mGM.vY0U- Zjݮ/21gcn)Zdcʬ~d">ߠm@d{S`G"@$+KV>;mli1d!W*8t %sn+Va kX] 8@+@mo6 ׶cIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/icons/hicolor/22x22/mimetypes/application-x-cbr.png0000644000175000017500000000126714477111712025205 0ustar00moritzmoritzPNG  IHDRĴl;bKGDC pHYs B(xtIME %DIDAT8kSA;1IR#>6R|TܸB+%+_ )(k1;.taQ"颭 -h+ZbwEThN ^|@Ypt'ZZ#7%F{p+ 3Xf(ٯ(rҲp ru) eJZQ.9NvCH'9?S`Fڡ*nm(;p/ `~n7`4h\1Z}*8zB06Rx3/+mGM.vY0U- Zjݮ/21gcn)Zdcʬ~d">ߠm@d{S`G"@$+KV>;mli1d!W*8t %sn+Va kX] 8@+@mo6 ׶cIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/icons/hicolor/22x22/mimetypes/application-x-cbt.png0000644000175000017500000000126714477111712025207 0ustar00moritzmoritzPNG  IHDRĴl;bKGDC pHYs B(xtIME %DIDAT8kSA;1IR#>6R|TܸB+%+_ )(k1;.taQ"颭 -h+ZbwEThN ^|@Ypt'ZZ#7%F{p+ 3Xf(ٯ(rҲp ru) eJZQ.9NvCH'9?S`Fڡ*nm(;p/ `~n7`4h\1Z}*8zB06Rx3/+mGM.vY0U- Zjݮ/21gcn)Zdcʬ~d">ߠm@d{S`G"@$+KV>;mli1d!W*8t %sn+Va kX] 8@+@mo6 ׶cIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/icons/hicolor/22x22/mimetypes/application-x-cbz.png0000644000175000017500000000126714477111712025215 0ustar00moritzmoritzPNG  IHDRĴl;bKGDC pHYs B(xtIME %DIDAT8kSA;1IR#>6R|TܸB+%+_ )(k1;.taQ"颭 -h+ZbwEThN ^|@Ypt'ZZ#7%F{p+ 3Xf(ٯ(rҲp ru) eJZQ.9NvCH'9?S`Fڡ*nm(;p/ `~n7`4h\1Z}*8zB06Rx3/+mGM.vY0U- Zjݮ/21gcn)Zdcʬ~d">ߠm@d{S`G"@$+KV>;mli1d!W*8t %sn+Va kX] 8@+@mo6 ׶cIENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9674768 mcomix-3.1.0/share/icons/hicolor/24x24/0000755000175000017500000000000014553265237017154 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768 mcomix-3.1.0/share/icons/hicolor/24x24/apps/0000755000175000017500000000000014553265237020117 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/icons/hicolor/24x24/apps/mcomix.png0000644000175000017500000000235714477111712022121 0ustar00moritzmoritzPNG  IHDRA pHYsUtEXtSoftwarewww.inkscape.org<|IDAT8YlTU;{ivʴ,[+DCʖ&<^QDQ &"E} A1RЀHD6-S[vLv|hB k.v%+/҉)-,:A#þNRQ$68BUHc<?9=/bkSE gzr'Lj N;l i:"|Mr=[iޝZ~@˥"!/ZZTʅi9Xx~i;֏cT-<_hlR&Lhz/673`I1"aYU^J;׾{hQ}&-_P3s찿ާ%P#/:o/!KTIENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768 mcomix-3.1.0/share/icons/hicolor/24x24/mimetypes/0000755000175000017500000000000014553265237021170 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/icons/hicolor/24x24/mimetypes/application-x-cb7.png0000644000175000017500000000200614477111712025106 0ustar00moritzmoritzPNG  IHDRbKGDC pHYs oy vpAgxLIDATXVKQ7?֝u7-RCtiNEt$R!- +Uvqvug^c݆]C~. pC ^u H,G&''ܷJ_F:inoU(%[q!I#BB:!@$]*GԳgWR%xoo,L_ ɬ%H)F퀢pʲm8r͑^40jK˩Si./r۷B{X*P*˪ ʧO@\*mB!,,˲4͝ f fcc#%ח?~$˗\]}{pD›`@X[sgd$%V`i/\yhk;w51TUCڻRY\.ƊESM67ОXLTD"!r2tzqϞpXZ;q\WUw/?sׁT*^%`~~lzH>|CoB&'><~c}}ccs3m@QNw'g{ENc{clT,׏Dt]$ѝ^K}{j<}<2RRAXw͛[[B]]˗}B8@Ǡ98MM,@L ˍP.+JuDs9n& ˂867]bg$ zyT]nMeMƍ'D#Ai>_,261}{uL!/D&OA8̭q+/*(IEfen 7Q &ϟ8S@{|CoB&'><~c}}ccs3m@QNw'g{ENc{clT,׏Dt]$ѝ^K}{j<}<2RRAXw͛[[B]]˗}B8@Ǡ98MM,@L ˍP.+JuDs9n& ˂867]bg$ zyT]nMeMƍ'D#Ai>_,261}{uL!/D&OA8̭q+/*(IEfen 7Q &ϟ8S@{|CoB&'><~c}}ccs3m@QNw'g{ENc{clT,׏Dt]$ѝ^K}{j<}<2RRAXw͛[[B]]˗}B8@Ǡ98MM,@L ˍP.+JuDs9n& ˂867]bg$ zyT]nMeMƍ'D#Ai>_,261}{uL!/D&OA8̭q+/*(IEfen 7Q &ϟ8S@{|CoB&'><~c}}ccs3m@QNw'g{ENc{clT,׏Dt]$ѝ^K}{j<}<2RRAXw͛[[B]]˗}B8@Ǡ98MM,@L ˍP.+JuDs9n& ˂867]bg$ zyT]nMeMƍ'D#Ai>_,261}{uL!/D&OA8̭q+/*(IEfen 7Q &ϟ8S@{HFG)%}'}?>ykD1Mt^#}ΑBL=qB BB!B7&b"o}g8CR%^!FH);߲R4C_)~G)CJý\O޶GH);7H)gQfXopx $X~PRv( ˹Rrm,}!ӀS7oÀ(?W:9CJАU?B%FF@F;OS @ ոe1uf<^:(CB)6)M@};#d(w!*RG/i;F FB=gO!8 `pu G{p -8v,`@=sv[Rʺr}H"nuہM8[RNOvG!qvoDZ/,tK),IT8+ Lޒ4#1b`9ˋ,-Y))LN9柲ڻ2R"%ttѝil뤱ƶN;}ktMOOH){rs|#!*WgI1yD&Ϥ8_Vtѝew} [4 lnMIRn=h쓼һg?[B0qx%3豃9z`F,+(]ܾ|k-wKY,i}2'HSYeE̞8 ﭰڒ`MK6V>NCG}6V-! ghe)g=QcaVdLw7Β7ta&]TM*[xv]m,>9xGo!J^^\9q9z^^JIajnΦ6[:hk颣,v6w}RtEqI(WLIe)J),/p`%V[GGl> Ϥz>K ; `~Ĩ\t$=f<%ӵ3tйg/-hmf_m΃w< S ZʀOɰ! T3Rm"88GA8p~ S9kN=#䕞m254oE=DV9zk&*m1lT?LQ@%Էtp <ZC''Oz+}pEq#`_VT%'Nና2"1/BúMo͞~n:cE4G.)M3bN䉈Ҝ[kYl7wၑ>8@"%i.8n"<('Niufjj>l[iQ@OR{ ٷG!BpN,'OMfԠ:ٶvXÖ7妇>u7C$"HPɠgc̣ ZA{W{_CWF3~طGw'OpК8.bm߳7Vmn;/qk-~riǒ@oo0c#Ѫ2_ z rM>'@Eq<'`NY\}\ٶ={޲M7q׮qf{η $9/z@V`aYµUרihS;pIa%}!8g}e?k|Ry_wc_y-+2{@j#=9MM@G"f cs)B98o yȅ<Җ.]֒Q Eap3Zx)1"\X20~$ʹ'3p1H+-%H!ǁEO gST^Q[dž'QW8n]ߏx{47d"$e18 cK6Çˊ|oPX Bm| m&g3Y,a-<zDA/Cÿ2{"ňSO@ˆ}|O/=K.#;i*jk-z|BP"~En"lnկr@1ϝCر<7u{hvN|)>PD1x c3<-|̖/g ~ ^f??T4t"R:/01:NDۖR`^O!PIcU'ٶ6=Us=Dljv0CO(L=??{q7t2iJ(>gg8L\1S!(iepiG M͚Gҕ_u[x"^#ywڼ.{2$#keSP$d4ڀ_\fys)8!:/7@=TJDβ8OB1DCgY V?J3cBq 4 ៗI92Yȡ C!`ډ>0 m"'.R>gv&W\g3O1NP5.ب\Uz}@D:Axb3Iv|d;p"`H]<OZZzHp"8Kp_EߐY5ա :CnSՏ tG>+y(U'(Z{"2!n*"Tg~O$̗R܃":,IBrw{p<jv4C&1~0 !!G^$F`_?*33(OᮯǍ~.ϫ 8]J,9Gu>Y#x]mx/ 'x6<0@ 8b G?v#V}6$ i0(~aՎ:>ws6R9J鰑w"wwsdYקh9U~v71CM#0QU JW}#I Ի-ztZ@ S̾JF3/T<+SMfһQB|͞6?~&C0"Cij"gYn'k ij{#-ˏ7GS|YiqTJ Ӊ+Lg}ٓ+NGpg!8vP~31an^ 3U}ODk/ᗇJ@U8m rF@  tO@qi9WI`?<;\zHJyKJГw!KAVp'ϡ$H"3)w4*~ EO bWM"GnuO%d؝D!"0l&!l$ {A^zť̾,  ïmRR";'!D12ſ?}.*ؒ|-=aTwLm g7{=ܟ/4.4+ x$iO= ~̺ *v|V`;p $y'n+N~aa4?9nc}DvrgXJLŎyIltWQ~xeq@Ks+,v? H:eNap>wÚ?xQ1ce*-A5 ~^$=VACw& =-S(S0cNl|96 1_1߾f`d=%!HCBݼJ7N"4?s&=U^ ,yJ X;bf D3jxtYa=2 HjK= _1Q ǍÒ}~ (/)WR6=EݨFE~ +I@ "<Ѡ7"Ԟٳ'jqd7z!êYAeԗ |U־ڙ|#9fpBiog9, z_x.+Yv` E>q1'Vĩ1D^\ D` tw۬|enY0C+KsPr׮taG">wl-j'ko ًq0`Doma@qӉr*0.oy YbC1OEQAԋVWB * T=72} wXaEB K.}Wd:YאgFI@QҀB M#V'pռ.'vɦNɮnrbO_&ZL)br_ l^8JEQHE R%Qj*!_vwHYhC"ӆN%+ֹ)%`D`b,IE᧧Ge̝0r`2g0Θ1Wn1)e&9o_97 T@1/;^|l;+{"TQůنUU )<cJ0]Gɞ еp|g!'Cau8hnu6 li;x̂J3K"P7f!U>}*U-\ddRs2p ;qԏ~=:n6?)?[rdE$P,BeLZΐ"+"JQ%Yvkgkm+v6НcӚP*`Xy,ҞPB<_ϡJԖ_ǭ&s#%{$LmΘVYI;a3gb*J (+L!M[g.j۩nhg&mo`Fۺ2P?$PX/* }xpuMt)eCrHb"(U'iy! ,퀗Z%U1U8s2ct%Gpk}\(6KʆZṕUte­~h#RkH”9@rqlUIC^ܛ p\qD?v )7>H Glm0vp?r{¶YGmWw-ae\tpΟ9`YlDA3C^} 6`m5Q WIsΰ)?Dhkaߔ{(" $g2<\QGqI\&l2YR:eԸI4o7="k 0ӭ_;=M_+4' JI ):֠ #t?4zw4` X֠WkqE(aG@v5 CP$\wejyl3U:L/Kʠ\)!']{:ʩl>iC# nnf;?}f;m]A(K [*9P)`L44KI)EVBTJHULٵv`KORW(+*\Dyq(%~D9;joP0U'5NQ(w߽˶j=g`Qb\%Dm|FTN ]?OmݿK5]W _ut3޿@7KaЫcx{@PjlCojS9 <9gS8t߽uU=RlK'Bz4b!4`9G8!* u*3@qnO %Ai)e]3)r\?mz?[`U-g揧02T0īQ`7ݿ`74{YȼNޝuZ0 \e.W)HVۊ{mcۖw#hOC*KSy SԄ|w/^H":L*ܷ[HC{-kˡh~‘E^.ہQR}mvy&p רs- r`RH!C2q!] >jBI8d!~w3ud;s}6IW' )8Yc5v?`"̟ IDATMt翹5Ay+n+d QGh.~K_<~w3$+de27ë:_]*RpM9M;S>yVsN-%A l /}PJٲ!8*|))*c8rp?#3 tvein`{>6e&ΕM]KGݟm6߷YCfߓ f WNŸb'G9~~xf4\?{`7)5eVhm_?'j,xy S R;j~< {(g fmnn^.ip):x?7O7O.Is8ۇ}2;YSG3e @Ei1inƝVêռn'ݡr b+"!F ⪳fqqS9n8 ojmf׹igK0~ 6i>?k4ŠG"꟯a0 M{ڝ;(6Yʖ@+@H?c]6_{7ء׮`MEyQ'W/>lLK{7kR.+r'.q4q70XՑCp͹s̜<)0wtx6zu-={kRPzD/7׾kxN8jRƞRjԠ@jaHI&ks3K=O|.%SiǾ~ojl?{3>w]3K7 ^36nBӦ[fpe(mD++WHaWK/,lcb3g2 ,/o9H Mzt׷5qkoEvQ|(4z(+8򾳹XwTT5;ȏmgyb*_`ѓIy3x"_081GêP 2Nt!S Dg;d{u~ꑣNԃ/?;)i6F]5`^^~x?)E|b {F(w cߺ}>RSV@TxӪ5| ~y "ǐF{mu+ܵ]]~0~Qz~9__j]g }|>wɱ%-,ϰ$ - N0aG};ܹ-uNo_ygvN.ȺÀW~efOԗBv!:[;θB6mw#'~a)VI%҅ (Zm|GXqK)_˅m+W!җ$0>.G1qҀn7%}K[Y2SQ^# "kigjVm_TΊM |/,IӘ5̹~ab+Tiz^q?CԟŬɣX9q~}kkJ-,@nX)k5  OSHVϟNGh6+̈/Sf[q3Go8QA6ˊ3i_iZh lɦĝnu z5lZLvRd7?j?} ~qnb`(D! 8䟁tEiƕ|SY`7&E߀zv{u Ƚͅ.pEv".O.Zka/ZuSU8cjE{T`WPT&Uߐ{~Y(5[Xi~k崮>Ζcܕ~ih`.nz.<9nx^; 8'C"8-oM" T?,#uH[wյp}11Y-ya1Ђ\`#Œ` ٴ5&wk 8g$B,]A!D" 60 [?󏟢Stoy̶lBv`7e/T捅ۙz F݅ճ)8Y$6 njX&9  KJ\Ʒ,~ݭ$og},ȌI"# O"S_*AG<#Ti}EvWsҴ@ x :]~0ӠPpp O"m)˴w0zGqeDy*X_ BB 8<3KO0 D 7.,HH5cJGhŜthK /?gJ~YG g`3*XqMKGIl߫=0W; 0F+x7?RB=@mI j 67d&]2g9yM$ ^<`b5 Vf֍QOq&]]r/͂[wHw)-NI՟iIuքVe1ԿR}i;߆s"`,W"4c  dݲWK:C]kSC8F<\/ZM5QLWp3F(Ԇ^hL=N/?!i[c>R5N#6?)c|"JT}b3EDG~Y^"57 {^<ߋ%_:nN!km1uo7VӽU8O*,HO .h b>g;#y/սuz3x7j <(/ 8]K$- 7ihu )r|gZ @b # 5$aj$CLX|='s _6gxhN jC TS;t ~&)yNx2+ "3*:f p{ *+LO? -\?›)ڽG c]ds-@Hԡ)#G{ѮvU}잍M5pz:бu_3x E8KnW;򽏜QE4" '@O&8'7PCջ;{d t0 n5{3|ZU~ۙ lw ]_͛w]:rN&D|)O;#ƶ=}}߶`sԢUۻƅP᭏k[Q@sN`Bv.z^|ʓ(LR`}E'w|;]I`w4ٳ!s9Zjo+6iB |cux>q WZ,7h@1#{lXua0 Ϸ>37kc te$}&xם7/f>#7=_ WVQ]ܑdv lG=K)7Wk9X]Lٿ:dU ٽժء\|,J|I 4z@,;6T]+#U]Kh m=B $0z(߿|-`F98J'-ܽ5 5/W>|!_`0;Nlv\?ی?{|}a$r#/AA9 Œeg +(k"x!ΐٽ*.z{rs!Za-uĄ xL W?sÆTv?,\MK}?ic+L'bvbZZBA z zox,nSwU;~/ NLӵn#1n8i~|Cy|}6j ['vGݏ*um!0~NEb7noT^U0sPMQ˼b&0ⓦݔ׃PiTBېaK̢i>Q|Kv(~"*N$Tbڀ8wDܼh>cZ(jz *$ŝ^'G:ɟFRF(/W|G\ k_QuA73HvNDLΚ5Un܍=c^ig&`LđC޻(g)K] ]͝@FXnk=bY B_ֻ2fQ+ϼ'xFG3DB2p ?b@Xgq2| qxd:(B5SWATuۛhp'7VmlaR5]mOT  Odں|?m$/֘׶b])Ap{b.m8y<; TjHiD~)! E?c^Q^?"U+?q#qByV۳qrjdIu[-;:IƠma#;gl bnޣL=)#= ɋ,_9@ %' I43Nw0nX<2q+=jD5p3<&MT86 ;b(Oĕwoӏ3Gm  .[<}`;@O滷4!DL#vsx>8U糼7;nv~!`klDHE=Bۦ:*URb4ʸ,"⺿Yq ':JfL_Ngp"D $B@ քE`[L8fD*Goޫ-:Xv",r܌Zw ؉wD5e}TT Y C/EYu3Ȳco@>cFij_SOdRb A @'B7 Ь@E`[7%k<%^Ɩo|%K;Ir'J3Ni^w69/;?:[Pk, 0rP?U tran>`"18"|X2=]K\B|NrÈw9 #6Ӈj:Q0Er|;_0tL0 P뾖2]l(lpX-݅@'cxy_Bc 0kVdW|mbv6(cQ5rʪ!78 Ho"!nл&t~iP =Sn:=r@T+JZ1ͫkZD5N3w8rf( {}n;ò]M,BSG.3a+KA ԳFx9@ '=ڶ{+ ;0skiJ7Xz*  /\8qPR\ 728;I|rE R%E49614)o;k*Ca ƶ |y'|xGcGsS9r`)_O _pszD B0{$àh5-A+wBD7@!rB|IrW{v9'v26ձM{Z}l-ELXS*r`FTdWGuWXPu $-?ym[ w挻fWkv5GWr”4dQQ|yp`AVݓN"'MW iV|J[T¼_w,X =GfwUmU F`3;-Zֺȷ΅G{gdeψ@f`뤃  ټ+Oi?32)>nNŤijYś`^:N7K2[uOO"gh8Q0) lKQ==񀬂<\ބP V=kkR0YmT5v>g7k'#҈+~O默#4>WMaXc Vi4緵[U,X@R"`"S"@ ]0wL=qo"m}na/^\j-)Lq5}&V0iP6gmuxB[:ir+&L}lD*8dc1-G/ :NJ R\;o,ם2%[8훏ij088iWP '\8Pp%iq`}PΟ5'_7(Kჶ=, 𕇷Eٿofde!gMJ^nԪdyb;3ܝQ4s?XS CWP{vHbbFZO6e;Θ@"m[CX |\Lό-=iOK7C(OH;uŎ>_9Q5[!_] z~hn$WG3>|J M,2k+Do_9w4uT _~hk~3`#,%3G24bD%)3z'C{/o)#\g3 SSBkz{zyR{F´Dx o[|x;Й]YojqeԦOpS^Q@AJ$*,,zYoLRRh|ǤK)Lfߴ{pU"&]ùS7b PKz%yOmD S_ q0|Ο*_؎H㽚ڂ)yIZD 3cYg_ 0!V )ijgfn/g8v9I veI[_jwI jshQ2ob^yGC`4 ls(R=Z8;q I_ ۫w9d.yyǞF>xI"g[yq6o}BB#?ß %24$@^<Rnѽk~wOw\DaʩEv'ޟOZ(m6MMsXG߬d:ȴ(eo@]u^g/w[ VZE2 E'WWp'k,m],P${TQXX:Az@n' ;r~Ĉ̞~߳K__dT>c(`?"͐~B߳T*y7v1=aȚFNk|Qܣu+8؍F1٠;osʽ}k|;+`|DDxcoGO4)Tͷ>1/~Rg-C Vˇ^Y|Bƥ$='Q,ʒ4)i#SYL=;r k :a4tU M YE0E8\3Ǚ;)iLbH{ͭnpد~Jt;UCddiwA?Em >}6zh̙É'7~d!ڢnsaA1(ZAK)wD򍼱c2c MƐ!7~{[򊯳pKH"Rb6_ߓʒ4_|ѕ Q#J)/JQVX 0]V lsv2UXFgt#r`䎌dFVshy9,?a((,NOcp#^Rw˄B;Xh۶m+԰| R{oGMPg^Pi5^>EWUx`O2 ~-XΒ۴/^ٰa۶a-]_KU$@ȡf"QD Mmk䌩/IО5 ycBqdb +P|ɮ؞RvZ`11nW]Yɧ`ik$DڲxncO[kOVh+nXFvx Z۹:t(k׮~i95 T^}Oy*2XL{ZDC>üݻ7'N  hB8| G[k-ܱ<=-]vf)+|OL-~^H)d?hM|QVNG??ˏ%DI[,]AjQ 0F!DI}}}Ϳ_[ L+1ؑ$v_ Ͼ.ݪ1 V{(ä#GGk"dxE@yn鉈 _ Vx/=w͗l$J*9X;:]LfqɘEbGWͻ-0!ŸsXj q#fn <d_ϯa$Pq@3oj:q(Y %:4(\H4#UpAgG]1,B^(  ]C;YWWoջQQфd?L#( ѭuV3Fk͝a0sy(ځ·K%)e#^XoA4S{S@\7whlFc  ,DAU\TÝ4z_[6"J Y&`nwxwUUyK ?VNK!k4+mbtKV~t˷.@ xem)x,r`%kKp Ӈ&Ќ{f,V*UOD9klRO+`Ct´=NZ;:4 :AHO͛TCa+{چo-{RZTɈxB@N]#ҷ-A&eX A#apV MU/d_M=`䴏V p}ܜ*eЋ cy7=h%{zHʓήnTI@S=ЪddAj^:f( kHk0ɷ0܎,)YVV9me*(,ZrGx=;Ⱥת?Vfr|dr[pU]6*JXJ4ƐQ@FCnnq]U~&L@V἟ ~;kkz^Y/w00ҋ j/XNUyysOx`mmVQ{RJ>H n#hi WၖpM0*;W*~95ΊT@1{aCQ  NC% գ >wTRFōug+*^=*_շt>Xk WyG>҈`垆V qڲh[T|!-iK0˵igOB|enAOiKG5 gZ:Fz Oma+eŔ@iy$:/P+.8vhei(-B.ri D%+ 4EX[mp8~PQ<miSάQn͍U1L+jq{_j{qhH9k3nJ㞨+R?tTF0 ԠFjg4>@F ]e_i H!ڈm_4ףyT!UjeE.aqrv~rw mGo/T޿#W .^ϹNJR @Jk4k(83=z? ͢:8mr?Y-Ձ*BS|eRCz=37Gc5ܤ+ǹG~VnH`>~yA>&-帩#B`At8FVQPӱ}~h2UjpgO{GQbi$+r`s(~8 dm~M-$=hs8n7֯bm SJ9)xjрb¥䨱ɴQAϞhSh4a.!\&`|ZE.i$ءC৵!|(T۶=hBYS_܆QEyM8^cK=gؿ6r#ȧ~M: "d)w7?vQ 6Ϗ;=('_߬ s]9I@7zoDtHù>zf,#lg^^;y$:vRD C0jh;*-BB (ZX\˫]E]tQQ)H U@IH ;c;eIfN3sgyscL6a}GC~ C@}쟛r)WxQ-,_U*2:BӠi>kfC;[p)t"%Q3NmCpJPRݭYחk9Z)#69 [fS9ɆvUN}D&?Z08W6[G…#{5u߫9 lx[ub?XE;W3<.)uG e_[^G'3v90v*Mz~̾4ʁeFq$-@]ڪ{x#4RG2NbFW^ eK疒Ic=x047G0̄o]#uv\qԿ+5!+O~?č1/t` }?w4龟_ {z|]KN+0,2\%\jw-۽|ܓBN7xv o}gqɬ[sVr:B0]g9:\f'L8 ]46 ~X#XG)^e[bRqYŤ%FްNM `y~:v}DJ܉ WA2!;_3NFPDinf,1lx2kOȎX܁aܕw8InRG4@#u{9%xff8/<@ RHDnzE$'-85ts6θgWQ{k6y3+64yM-Ft#R O3V5FR ?aFۋHƑk;vs@.ΌT;.=x,4^o8ڋf~*bm8sg7i=co둗;&ÂIR(83Ӱg›+1<8̢uZf\YJ vA+"*k!Y= qЎ5\& >PiW#Fv*ⴞ49D젊.O1֓ }OOM$cЈ.r =cKlv+'DiHSN?J ɧէKp,x"3l~[$HZ8|m ]_,Dg_ְ#VkzE;On<`J,m*w4(j}rf?=% l9PqO־ʿ_M°o(`!B; #.u׮L,ܾMa/_k#qQUȢ2vU=/Q xZQA7K+7@@cAЏZ>O8M5|u>u$7#h҅MZ]~G@lQ_)uԷ;)'9'xuH xy(/*RNFKKn^Qm )UgC ׹X4Ϥdafήu<&o192rjKMe.L>c (ƶmJ.n PLǨڳ) E 6'6b=ϘAEp/'~F$虙o'!ܩN#AGR:Z<=gei! B(y9rpdKvu:Qft7fafifFk'݉ 6߂٭f*s VY>>-ǖ=ڬtUPs%7xm.aj Oޜg&?WyNkKFfOJPKJvr+ؕ'+cv胿l5RiƠGxkOOtu+ Ph-j>|ӗ#HϵP6pRi q-ԆPPc ehO$1u}Uund:׿;ʂDyQDAfzl,od2v/Wvju'q".#ч]j\nbno:kwPAb0C%UrRvNK 1Mǚ1K:nvb MM;E˜^.# /-} huתO~C3?y? J@9QU[tK<i ^>~"g {׫Fco(~bW,(ջ^k S{*a%=k |VjcEg|`5!n3r,yBQIНkQA>Dog5˞Y̬.I<{uƇ =2}5_nS78N\src!wpo܏w2\J|5I)7rXc}ob1ֽ+1o~F8.vyߋ ҸRsΈd*M?sNū{>6~*C8wm6λ٭#,v:+DA9HH)T6p}oY4<Z=w'&NyJf.46f-óD/oOo=ߞR9NpfOUeL@G &Cc=l~'mELÖuaʬ~^B<:[haI0}z1o>ϾxEmͧ?&pgv1nEꝣ Mq0"hX[ohRUGzzr)3A\9XȈDlނl@X.?>,>7Si>z09jHX701}2hXE*aO@|0CG*oN#:a_WjLЉaY߇c%K6@+KJ@@^W~qa|G!_wŝ +!.ʚ'?~O?0xrO*Zz>2 0}:N;h ϕqx:6̯)b=._| xrzk 2=#W.)n`]6ӯopnvSWYd; i3tpӨo"vu"#1$c~</Qյ޹ɀdDtb.FuOɉ 1{k5MtR'A]q?|h{C#+6O^?>fjl^2FtM6SBnNkjG?1v98kƙ ߦ 6= `xONٟNf LjuZuMsaXz@K woTpFt<w}'WlZwlds:<;wzGEU.QA/<-#0)e@-K0JrKdm bqgvDU*HiA^j  s)'|IE~5+3緃uyPEOn w\CֱKdFގẢ"3* 97UQ|\e}*~ t$pԍK;MÏO\nӫ rZ>t;an=)>o|k )#q׍G kP^q\b#4{TQjQ=P$V¢oՑ9=9vW?ԦA[+^O"H&{i.5uM #\Φ$ySHv+Vz>/lxh rRLnQZS;95~[_=p}-;rcpXuK0B]=~iO_Ds 93{\tr bv:<e쓗›SoTmb?Vf3LKv놰jGݾΆk2k9w~L|c{18mqSkG!u#p'mrb9u7P?Vc^ﮮa -*w RFNjc(.JΧdk>I|aŸ6GlܕpXG*ؤy# qt7$']Hl Y)|螝T!ze{oS._8zy/9iSؗcxbq%#Sq%h_Ֆ=C<+юwXU 4Od9} E한 }CZˤWV%a"1vH$#\&A!d2mVL .V`C.uw >Ҝ zi@N2m\( k #]D}mT6ÉSq&q)x Z2+TS$ )pťF*Y /|u`*(O~~0=_`0ӗ ù'ڵ >F*SҒX+,W^[&u.ˇuN+$0 .5gEۯw- -#HTPY(N fK)9pUC S{sj׈*gOLθw?.Â.ep`rYwl\ xZ~3^prwLF-ा9O>Y>Ȉ^Ѧ wb抂oJh@|WHb\.Oa.1ӻjAϷ% 30yt T0yRWZYؿ,cG1ݍ]-ͶSX1.xq\zJ,\wڝl]i\@`NKKhB"pMq>blzo wˡOLwAR-mክL|-/w{& R 52Ϭb惆{wJ䥫2 B"Eďh `J "D7ۋRNRZ^]Fi5%w+L ~W%~g># aѶb*֗SRZj^ړ0KA;R~(BBp 'TK~f2I%Dv$bnblopf@r + pLn WjIJgwgɝmV( c¼#1` v]'?x+3 QQة FZv<8ܖ`q{X3MG~S<@aOe +kSYZJTAE ,)HhB /4\T5A:"&. L[9o3em@xbg[~NuӞvb7 D %RF?%@sPX HzurK_j@W73kjpH).VmV s׋,8v8 hX A"Kvr%SLJ70=Nk{\Kb2݆' *{nY_<<-W\ ,z)?H)륔eRmRʵRʅ/jGܤ A Y;w~2a@&ijH&f섳˰G3w.%v2Hk"/"X(nx}%3!*qKm?>NC-Ŀew%c'`rFi⇟ B|s槦ΕXĿo,y.>,7%3ÛSFIs;[^Ks{>9hdHsK,ZHhT߼&h\L7.:zasVpIq{.-n1dNQw1nr;=Zp>~ @Xj/@xSaBRsZoV%0 OJ*MUX77Zĭc:qk'@ Ӏ`%` ` ZgPe2^7* t4Y4z{ؼ .RW򿂟=BtD$ -} w}b!KzǞ) sǻ[* tN=9Wa 0oæ?h;Siok Wqzfޏ1nS/e`%|ȏۃ7?$뢝l h~%|7GB|h_G$.ƴS*Ta#J sK*-N۞{ʀd,DbhTx}УwD"@poSK?vW>37TU? hc: h;ۑ7~.iIqC{ )FF !_mA)S菉DU}sRZ`c?f*4ҽ툯gorqE) w\Qз0A"qѦ{X})_賫P ~[.LB$3qnsf= 5( =:=_vĮiQt$$˶UGXOAz~ՃbU~U7mCu\2C,8)m Aé -tRw)$9%cHG|Ǝ\VɅˆUx_,"@q3$'A& Nu vts۪B57Kv=IqkM^T%!%q7[+1w:OLx+σev!DL"} "\)O}dx1@3Bh[)l+Nopʡ}mBB`[m5[y]Х`t zu:>R^ĒU,Tɇkh۩aTvsF!`to ˍ;3"~&|i/|>; IIP ƾPSQQώJְL۴j==׏ep$,B_F` Y=,;焰blrb) m J!b[ѹP6OMCQC2mv;Uܷ]4%-g}| 0"5c|Te2fNҡY\8$ :bZ]+9 OfW}<p2i4z_AFNC b&t,0 +v0AVe:J6D[XukAɌ‰RLb7ˤclp9nq+2szZ::3B8 !ơ2]:i4BI%R@l@I'`%~`ͯm i=%{^@U~^i ZG,`G亴]^.\ i3p% bݾ[Y˵tCuzͩua&wYҏJ7;uU(5WACuPUpR^ w`[+6n^yuea-NKԁWܕu"5ydž"Y>|Rn s{?w1A;%m170W>'gX<ڧ&UAPg=,)ŲbC֣|YJy6~DBd280ѯ=} ӧ03ӎxs V?PVqM*X}?v`l(-sUoJ)7W m G !Dppz/7k)J(+v'Ǔ@FJ0512sXS߄?Pp=(?KbjvrP*~|hc?!z'Odr `1XF". }QÙ"]JkPw56?`h"MN lT'_YUqkJ_aͦI bk PH!(4aڲiDT7ˆ ! ̩(\Z=%oyhCN`Kb⋵Y:o:/V,ӥ~2BGW/SHsm;,ן<(o=qk@撱m?l0ouQnEx?Cݑo"A30o -7yCl)K(Ǿ6<%`&qdj$:PQrc3p+>gǂCWMh0B"|AJ lV{J׃kk#\0]0m5e%~c}?{kQtF,) x = !F$ʌSV]CeY1tL8kcdb6n2QEO5b:tLz 'MEA`sTPBA 4 Kd2/KEfy)Tq~9RH lGmKrY?=Hr9*'i:)IJm{ u=s)ۼL:BQNp !( j 'lN^et.Ӑ+]|Ƀ}ҚDI>ͷzڑA ;fqNc,* SwP.uիF.Ma|^wg-za@l_4O|vBy.1վczTx*,* [1bM7yw5!|Srio]lEAV>xP6M,. 9o;KRz%ƴͯxU/ %{h"_shX~ {nO%B>|ךm~dQ~ecr"΍qLǥ$ '+BRˣu]r_"lƔm5+ 2FD@흿}?Cz_rI IENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768 mcomix-3.1.0/share/icons/hicolor/32x32/mimetypes/0000755000175000017500000000000014553265237021166 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/icons/hicolor/32x32/mimetypes/application-x-cb7.png0000644000175000017500000000205314477111712025106 0ustar00moritzmoritzPNG  IHDR szzsBIT|dIDATX_U?ffgXf(1$D YT z,Y7 %*Tԥ e383߽=uVs9f1;Z/?x3 k<;KF-rYyƎD`ߖ%~:skRmDQ ?LL P: u/qъ}sWu jMe+:[-C ug&NpQӂтL'-d{]8ׅFsE˶T+]$]ݶzBX +(%DP 5/.OOrSNj˯zA3bUxa^2,濻'ݳz/,Yp.\dEv"_}E\x& lUDod͕b~EИyjDcb^) uq"^EJ/8;93  ݿ=7tǍ*b`_wCbQuQDy4F5v3ΈĶ.o(x~OVp v7mtc nF|:|mgw fL%8v^ݒ&JLB H6d%(m u!7܌6~,濻'ݳz/,Yp.\dEv"_}E\x& lUDod͕b~EИyjDcb^) uq"^EJ/8;93  ݿ=7tǍ*b`_wCbQuQDy4F5v3ΈĶ.o(x~OVp v7mtc nF|:|mgw fL%8v^ݒ&JLB H6d%(m u!7܌6~,濻'ݳz/,Yp.\dEv"_}E\x& lUDod͕b~EИyjDcb^) uq"^EJ/8;93  ݿ=7tǍ*b`_wCbQuQDy4F5v3ΈĶ.o(x~OVp v7mtc nF|:|mgw fL%8v^ݒ&JLB H6d%(m u!7܌6~,濻'ݳz/,Yp.\dEv"_}E\x& lUDod͕b~EИyjDcb^) uq"^EJ/8;93  ݿ=7tǍ*b`_wCbQuQDy4F5v3ΈĶ.o(x~OVp v7mtc nF|:|mgw fL%8v^ݒ&JLB H6d%(m u!7܌6~3B޿{u}t= P5  YHlG ,bcľo' 𭨛v׶cSh@S&ά(ibLwu$ `5ٰ<ջsl;\].ZX;>_Wֿu7f< OtO P0 oldDɌu{vf +E;N硅呵W[}G@<Úzz쭂tx^#3KO,˥=ܲ͟B\/FuAƅU` qPIV,m2\ +qHMx'WLQ&)4)n'NqrcB`DJi+ _{7&:K/k(+Ah>|Ot\C q$%7]6O]T_h /N"$8[vw<^mB0,LwfWʒ;TW:Ʀ7w5V}i~3ZxoQQ?CBh>ڇbrM4 EǖK<| TDB!y4dUÓ͹l0!f@663mǯr]kӤ eۻK82O薊 ߼O.C]1@0mZSȤ9 t=YXˤ:1Τs Rp?zTk/?|!@r mXl,D+ήUy0o # Ĺ"#,Fqmϸcf ld*VG6 곭{AX]z/19ǮIZ7͌njwet4ŽcETE|Ti@ zʵ=qlcq}:+ϧ$;^{'zGxD>!/Q4NBkCEǖ}H'iȘӔTdϤih( 5a ]x]G:qT _Ԇh}"Cj.};=y]w=pNԢ Q|"߼ 7%T\G84k2i=yuI91ƲZa ?|y/w_ZG.b(A#6wx.|B;j"+ +/]\V6߿#7,0=cBnY}bsBL(0"]T*GwpJVքE|\2<ゅeΎ;G[GjyLJiTB] ]TUJg Z` v]e4xuNOlqrT<1mr׳Guy ێnmJN)[?P?Q'ǛG&}Rʩ?lՔ߶zYraۈ.)XF')YSCeaC+UVqx`/˃k+i(!]=OlؼcqK?¬*7P[upb$Ŧ'/ 3?'` Mhfh*EyratQY]ݻwohybćBO9{|~4č S&&=))hNeĠ "5\]m?9wNt}m*ش0^H@ /4SŎA(Bڠle!VZXna h[ Shq3J Wj=px3b}akm<%@I)J %% @J$Jxgv,ɤ9h!~sdHrҥ4Z=m|g> EѴ}~N/3n$)0 Q#%" ^^B'Zf:6b=-g7 c=i6׬ jX|UQ'dO-Ё;fpǔT@6%8DZ%&#OE6i"mPD_Iйfб ٢o:;K kϧ[qÕhIg_;DW }7Gkyڐ1n H,Ak'U?,V`)(![DƯki0aN 5ny{}E{ak"89wG;KèOb)BkA}}Ckhm?({Et1G❧K bħ83-x!S8~%5^s~ :q¯O~lUEUpK|i$_ĔEϧP)z>EPH*0F#m .c:vk+A*B&GME#}NKe|c9wuzk6ntG~ jhǺ+,*ce3GtVf${멌h -]sf>,3B{n82t8{n1n0m'.\|PIy"W a}6مc*BR2ĸbc eZ0 ?{=.$/ޣ~0(#Xἱjvm6fr1& Yn~!|9oZ\eR5^#]mYR31'_~Q[:j-?rQ|@ɌH_za3f=D< mZ#83l_(wYeq?Ko (3m+xHz=kWvEZJa9n .KЋW߀eHw[+@I ۾\ZУH%I )?"Hea9W9)}#ЖB} IAYvb)U>r곕QBx(D!*ve"#-!D&Rb$R@БYDXK֔:õA{J5ДLov6എP㧞1&B UQ [V$FhpR*ׂ JND}c?Xm*;͝/%˥x͍}ǼW)Fpyj{=v ' >d(M H-t8]5*(@ so E# .vJdLݳ( WWQ z`5GIIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/icons/hicolor/48x48/mimetypes/application-x-cbr.png0000644000175000017500000000356514477111712025230 0ustar00moritzmoritzPNG  IHDR00WbKGDtIME  b;IDAThkU־993 N " %ZPR#mkD.mP0A@5"6P Be.ps33F&&wa0c!o[u)r4#%7A(}(ukC5rœ^>ش0^H@ /4SŎA(Bڠle!VZXna h[ Shq3J Wj=px3b}akm<%@I)J %% @J$Jxgv,ɤ9h!~sdHrҥ4Z=m|g> EѴ}~N/3n$)0 Q#%" ^^B'Zf:6b=-g7 c=i6׬ jX|UQ'dO-Ё;fpǔT@6%8DZ%&#OE6i"mPD_Iйfб ٢o:;K kϧ[qÕhIg_;DW }7Gkyڐ1n H,Ak'U?,V`)(![DƯki0aN 5ny{}E{ak"89wG;KèOb)BkA}}Ckhm?({Et1G❧K bħ83-x!S8~%5^s~ :q¯O~lUEUpK|i$_ĔEϧP)z>EPH*0F#m .c:vk+A*B&GME#}NKe|c9wuzk6ntG~ jhǺ+,*ce3GtVf${멌h -]sf>,3B{n82t8{n1n0m'.\|PIy"W a}6مc*BR2ĸbc eZ0 ?{=.$/ޣ~0(#Xἱjvm6fr1& Yn~!|9oZ\eR5^#]mYR31'_~Q[:j-?rQ|@ɌH_za3f=D< mZ#83l_(wYeq?Ko (3m+xHz=kWvEZJa9n .KЋW߀eHw[+@I ۾\ZУH%I )?"Hea9W9)}#ЖB} IAYvb)U>r곕QBx(D!*ve"#-!D&Rb$R@БYDXK֔:õA{J5ДLov6എP㧞1&B UQ [V$FhpR*ׂ JND}c?Xm*;͝/%˥x͍}ǼW)Fpyj{=v ' >d(M H-t8]5*(@ so E# .vJdLݳ( WWQ z`5GIIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/icons/hicolor/48x48/mimetypes/application-x-cbt.png0000644000175000017500000000356514477111712025232 0ustar00moritzmoritzPNG  IHDR00WbKGDtIME  b;IDAThkU־993 N " %ZPR#mkD.mP0A@5"6P Be.ps33F&&wa0c!o[u)r4#%7A(}(ukC5rœ^>ش0^H@ /4SŎA(Bڠle!VZXna h[ Shq3J Wj=px3b}akm<%@I)J %% @J$Jxgv,ɤ9h!~sdHrҥ4Z=m|g> EѴ}~N/3n$)0 Q#%" ^^B'Zf:6b=-g7 c=i6׬ jX|UQ'dO-Ё;fpǔT@6%8DZ%&#OE6i"mPD_Iйfб ٢o:;K kϧ[qÕhIg_;DW }7Gkyڐ1n H,Ak'U?,V`)(![DƯki0aN 5ny{}E{ak"89wG;KèOb)BkA}}Ckhm?({Et1G❧K bħ83-x!S8~%5^s~ :q¯O~lUEUpK|i$_ĔEϧP)z>EPH*0F#m .c:vk+A*B&GME#}NKe|c9wuzk6ntG~ jhǺ+,*ce3GtVf${멌h -]sf>,3B{n82t8{n1n0m'.\|PIy"W a}6مc*BR2ĸbc eZ0 ?{=.$/ޣ~0(#Xἱjvm6fr1& Yn~!|9oZ\eR5^#]mYR31'_~Q[:j-?rQ|@ɌH_za3f=D< mZ#83l_(wYeq?Ko (3m+xHz=kWvEZJa9n .KЋW߀eHw[+@I ۾\ZУH%I )?"Hea9W9)}#ЖB} IAYvb)U>r곕QBx(D!*ve"#-!D&Rb$R@БYDXK֔:õA{J5ДLov6എP㧞1&B UQ [V$FhpR*ׂ JND}c?Xm*;͝/%˥x͍}ǼW)Fpyj{=v ' >d(M H-t8]5*(@ so E# .vJdLݳ( WWQ z`5GIIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/icons/hicolor/48x48/mimetypes/application-x-cbz.png0000644000175000017500000000356514477111712025240 0ustar00moritzmoritzPNG  IHDR00WbKGDtIME  b;IDAThkU־993 N " %ZPR#mkD.mP0A@5"6P Be.ps33F&&wa0c!o[u)r4#%7A(}(ukC5rœ^>ش0^H@ /4SŎA(Bڠle!VZXna h[ Shq3J Wj=px3b}akm<%@I)J %% @J$Jxgv,ɤ9h!~sdHrҥ4Z=m|g> EѴ}~N/3n$)0 Q#%" ^^B'Zf:6b=-g7 c=i6׬ jX|UQ'dO-Ё;fpǔT@6%8DZ%&#OE6i"mPD_Iйfб ٢o:;K kϧ[qÕhIg_;DW }7Gkyڐ1n H,Ak'U?,V`)(![DƯki0aN 5ny{}E{ak"89wG;KèOb)BkA}}Ckhm?({Et1G❧K bħ83-x!S8~%5^s~ :q¯O~lUEUpK|i$_ĔEϧP)z>EPH*0F#m .c:vk+A*B&GME#}NKe|c9wuzk6ntG~ jhǺ+,*ce3GtVf${멌h -]sf>,3B{n82t8{n1n0m'.\|PIy"W a}6مc*BR2ĸbc eZ0 ?{=.$/ޣ~0(#Xἱjvm6fr1& Yn~!|9oZ\eR5^#]mYR31'_~Q[:j-?rQ|@ɌH_za3f=D< mZ#83l_(wYeq?Ko (3m+xHz=kWvEZJa9n .KЋW߀eHw[+@I ۾\ZУH%I )?"Hea9W9)}#ЖB} IAYvb)U>r곕QBx(D!*ve"#-!D&Rb$R@БYDXK֔:õA{J5ДLov6എP㧞1&B UQ [V$FhpR*ׂ JND}c?Xm*;͝/%˥x͍}ǼW)Fpyj{=v ' >d(M H-t8]5*(@ so E# .vJdLݳ( WWQ z`5GIIENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9674768 mcomix-3.1.0/share/icons/hicolor/scalable/0000755000175000017500000000000014553265237020137 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768 mcomix-3.1.0/share/icons/hicolor/scalable/apps/0000755000175000017500000000000014553265237021102 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/icons/hicolor/scalable/apps/mcomix.svg0000644000175000017500000003362314477111712023117 0ustar00moritzmoritz image/svg+xml ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9674768 mcomix-3.1.0/share/man/0000755000175000017500000000000014553265237014372 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768 mcomix-3.1.0/share/man/man1/0000755000175000017500000000000014553265237015226 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/man/man1/mcomix.1.gz0000644000175000017500000000141614477111712017216 0ustar00moritzmoritzdmcomix.1TMo0 W٥RavKɶFnX?MZeɐ|׏R7kb(Ȑ1\T7p/L6LoهN1L(>9Ew'p~w\q:8NdUlpiй$tn7ً 0h Bsv!k%#[^% abƌ#·q%qH:Ԅj& NJB~ E݉Ԃm,n ҴH#8e]u'[T˕.J#ZZX#ٽqU\6h{T *^rsjgr&ӈ򍴞Z&׾A窡ֻ;ޠ n༞B7JU.ZG2ӝZokN= Y8g(=~-1zDj )ۂT6~vqCd axANd4Ԃ7w#8BPMw֟]\_n&~5#$*"\\cf uUlYd4Ɉ!F@f4qr5Һ%a64Jkk3Yb,^؜5ཱn8G ST-,y [V^/씄i3uICs8o[Fu././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768 mcomix-3.1.0/share/metainfo/0000755000175000017500000000000014553265237015421 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1698566589.0 mcomix-3.1.0/share/metainfo/mcomix.metainfo.xml0000644000175000017500000000227614517410675021245 0ustar00moritzmoritz mcomix.desktop CC0-1.0 GPL-2.0+ MComix Comic and general purpose image viewer

MComix is a user-friendly, customizable image viewer. It is specifically designed to handle comic books (both Western comics and manga) and supports a variety of container formats (including CBR, CBZ, CB7, CBT, LHA and PDF).

MComix is a fork of the Comix project, and aims to add bug fixes and stability improvements after Comix development came to a halt in late 2009.

mcomix.desktop http://sourceforge.net/p/mcomix/screenshot/273797.jpg http://mcomix.sourceforge.net/ The MComix Team https://sourceforge.net/p/mcomix/bugs/ mcomix
././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9674768 mcomix-3.1.0/share/mime/0000755000175000017500000000000014553265237014546 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1705863838.9974768 mcomix-3.1.0/share/mime/packages/0000755000175000017500000000000014553265237016324 5ustar00moritzmoritz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694274506.0 mcomix-3.1.0/share/mime/packages/mcomix.xml0000644000175000017500000000215114477111712020332 0ustar00moritzmoritz Comic Book Archive (Zip compressed) Comic Book Archive (RAR compressed) Comic Book Archive (tar, possibly compressed) Comic Book Archive (7zip compressed)