pax_global_header00006660000000000000000000000064141757370070014525gustar00rootroot0000000000000052 comment=ea1b8b019019d422270bdb8cdc9c6cb7b776eabf paperwork-2.1.1/000077500000000000000000000000001417573700700135405ustar00rootroot00000000000000paperwork-2.1.1/.gitignore000066400000000000000000000017431417573700700155350ustar00rootroot00000000000000*~ *.tar.gz *.aux *.pyc build paperwork.conf paperwork_repo/ dist/ out/ paperwork.egg-info/ .idea *.traineddata *.app/ .flatpak-builder/ repo/ *.flatpak venv*/ *.pyo _version.py *.exe *.dll out.* rclone* AUTHORS.json AUTHORS.git.json paperwork_repo/ .buildozer/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Django stuff: *.log # Sphinx documentation _build/ # PyBuilder target/ #Ipython Notebook .ipynb_checkpoints paperwork-2.1.1/.gitlab-ci.yml000066400000000000000000000253731417573700700162060ustar00rootroot00000000000000image: debian:bullseye variables: GIT_STRATEGY: clone GIT_SUBMODULE_STRATEGY: none stages: # no point in waiting for the tests to end before generating the data files # or the development documentation - tests - data - deploy check: stage: tests only: - branches@World/OpenPaperwork/paperwork - tags@World/OpenPaperwork/paperwork tags: - linux - volatile script: - apt-get update - apt-get install -y flake8 - apt-get install -y make - apt-get install -y pycodestyle - apt-get install -y python3-flake8 - make check test: stage: tests only: - branches@World/OpenPaperwork/paperwork - tags@World/OpenPaperwork/paperwork tags: - linux - volatile script: - apt-get update - apt-get install -y build-essential - apt-get install -y gcc - apt-get install -y gettext - apt-get install -y git - apt-get install -y gobject-introspection - apt-get install -y gtk-doc-tools - apt-get install -y libcunit1-ncurses-dev - apt-get install -y libgirepository1.0-dev - apt-get install -y libjpeg-dev - apt-get install -y libsane-dev - apt-get install -y make - apt-get install -y meson - apt-get install -y python3-dev - apt-get install -y python3-gi - apt-get install -y python3-pil - apt-get install -y python3-scipy - apt-get install -y tox - apt-get install -y valac - apt-get install -y wget - apt-get install -y xvfb - apt-get install -y zlib1g-dev - source ./activate_test_env.sh && make install - source ./activate_test_env.sh && paperwork-gtk chkdeps -y - source ./activate_test_env.sh && paperwork-cli chkdeps -y - source ./activate_test_env.sh && make test test_chkdeps: stage: tests only: - branches@World/OpenPaperwork/paperwork - tags@World/OpenPaperwork/paperwork tags: - linux - volatile script: - apt-get update # bare minimum to install Paperwork from sources - apt-get install -y build-essential - apt-get install -y make - apt-get install -y python3 - apt-get install -y python3-dev - apt-get install -y python3-pip - apt-get install -y python3-setuptools - apt-get install -y wget - apt-get install -y gettext - make install - paperwork-json chkdeps generate_data: stage: data only: - branches@World/OpenPaperwork/paperwork - tags@World/OpenPaperwork/paperwork tags: - linux - volatile script: - echo deb http://deb.debian.org/debian buster-backports main > /etc/apt/sources.list.d/backports.list - apt-get update - apt-get install -y build-essential - apt-get install -y doxygen - apt-get install -y gcc - apt-get install -y gettext - apt-get install -y git - apt-get install -y gobject-introspection - apt-get install -y graphviz - apt-get install -y gtk-doc-tools - apt-get install -y imagemagick - apt-get install -y libcunit1-ncurses-dev - apt-get install -y libgirepository1.0-dev - apt-get install -y libjpeg-dev - apt-get install -y libsane-dev - apt-get install -y make - apt-get install -y meson - apt-get install -y po4a - apt-get install -y python3-dev - apt-get install -y python3-gi - apt-get install -y python3-pil - apt-get install -y python3-virtualenv - apt-get install -y rclone - apt-get install -y texlive - apt-get install -y texlive-lang-english - apt-get install -y texlive-lang-french - apt-get install -y texlive-lang-german - apt-get install -y texlive-latex-extra - apt-get install -y texlive-latex-recommended - apt-get install -y valac - apt-get install -y valgrind - apt-get install -y virtualenv - apt-get install -y wget - apt-get install -y xvfb - apt-get install -y zlib1g-dev - source ./activate_test_env.sh && make install - source ./activate_test_env.sh && xvfb-run paperwork-gtk chkdeps -y - source ./activate_test_env.sh && xvfb-run paperwork-cli chkdeps -y - source ./activate_test_env.sh && make data upload_data doc_devel: stage: data only: - branches@World/OpenPaperwork/paperwork - tags@World/OpenPaperwork/paperwork tags: - linux - volatile script: - apt-get update - apt-get install -y build-essential - apt-get install -y doxygen - apt-get install -y gettext - apt-get install -y git - apt-get install -y gobject-introspection - apt-get install -y graphviz - apt-get install -y gtk-doc-tools - apt-get install -y libcunit1-ncurses-dev - apt-get install -y libgirepository1.0-dev - apt-get install -y libsane-dev - apt-get install -y make - apt-get install -y meson - apt-get install -y openjdk-11-jre - apt-get install -y plantuml - apt-get install -y plantuml - apt-get install -y python3-dev - apt-get install -y python3-gi - apt-get install -y python3-pil - apt-get install -y python3-recommonmark - apt-get install -y python3-sphinx - apt-get install -y python3-sphinxcontrib.plantuml - apt-get install -y python3-virtualenv - apt-get install -y rclone - apt-get install -y valac - apt-get install -y valgrind - apt-get install -y virtualenv - apt-get install -y wget - apt-get install -y zlib1g-dev - source ./activate_test_env.sh && make doc - source ./activate_test_env.sh && make upload_doc linux_flatpak: stage: deploy timeout: 48h only: - branches@World/OpenPaperwork/paperwork - tags@World/OpenPaperwork/paperwork tags: - openpaper-flatpak script: # Running in from a gitlab-runner directly in a shell, as the user # 'gitlab-runner' # --> not running as root, so we cannot actually install anything # - apt-get update # - apt-get install -y -q rsync flatpak-builder make - ./ci/update_flatpak_repo.sh .windows: &windows variables: MSYSTEM: "MINGW64" CHERE_INVOKING: "yes" before_script: # Libinsane build dependencies - c:\msys64\usr\bin\pacman --needed --noconfirm -S make - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-cunit - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-doxygen - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-gcc - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-gobject-introspection - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-meson - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-python3-gobject - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-vala # Paperwork build dependencies - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-gdk-pixbuf2 - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-cairo - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-python3-cairo - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-gtk3 - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-python3-pillow - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-ca-certificates - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-python3-setuptools - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-python3-pip - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-libhandy - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-libnotify - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-poppler - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-python-psutil - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-gettext - c:\msys64\usr\bin\pacman --needed --noconfirm -S git # for 'make version' - c:\msys64\usr\bin\pacman --needed --noconfirm -S wget # for downloading data files - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-python3-cx_Freeze - c:\msys64\usr\bin\pacman --needed --noconfirm -S zip unzip - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-nsis - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-nsis-nsisunz - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-python-scikit-learn # Workaround for pycountry and Cx_freeze # See https://github.com/marcelotduarte/cx_Freeze/issues/930 - c:\msys64\usr\bin\bash -lc "pip3 uninstall -y pycountry" - c:\msys64\usr\bin\bash -lc "pip3 install --no-cache --use-pep517 pycountry==20.7.3" - git submodule init - git submodule update --recursive --remote - c:\msys64\usr\bin\bash -lc "make clean" - c:\msys64\usr\bin\bash -lc "make uninstall" - c:\msys64\usr\bin\bash -lc "make -C sub/libinsane uninstall || true" - c:\msys64\usr\bin\bash -lc "make -C sub/libpillowfight uninstall || true" - c:\msys64\usr\bin\bash -lc "make -C sub/pyocr uninstall || true" # a 2nd time just to be really sure # (that's the problem when can't use containers ..) - c:\msys64\usr\bin\bash -lc "make uninstall" - c:\msys64\usr\bin\bash -lc "make -C sub/libinsane uninstall || true" - c:\msys64\usr\bin\bash -lc "make -C sub/libpillowfight uninstall || true" - c:\msys64\usr\bin\bash -lc "make -C sub/pyocr uninstall || true" windows_tests: stage: tests only: - branches@World/OpenPaperwork/paperwork - tags@World/OpenPaperwork/paperwork tags: - windows - msys2 <<: *windows script: # Tesseract (required for unit tests) - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-libarchive # missing tesseract dependency - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-tesseract-ocr - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-tesseract-data-eng - c:\msys64\usr\bin\pacman --needed --noconfirm -S mingw-w64-x86_64-tesseract-data-fra # Build - c:\msys64\usr\bin\bash -lc "export TESSDATA_PREFIX=/mingw64/share/tessdata && make install test" - c:\msys64\usr\bin\bash -lc "make uninstall" windows_exe: stage: deploy only: - branches@World/OpenPaperwork/paperwork - tags@World/OpenPaperwork/paperwork tags: - windows - msys2 <<: *windows script: # We need rclone to upload the files on OVH object storage - c:\msys64\usr\bin\rm -f rclone-v1.53.3-windows-386.zip - c:\msys64\usr\bin\rm -rf rclone-v1.53.3-windows-386 - c:\msys64\usr\bin\wget -q https://github.com/rclone/rclone/releases/download/v1.53.3/rclone-v1.53.3-windows-386.zip - c:\msys64\usr\bin\unzip rclone-v1.53.3-windows-386.zip - c:\msys64\usr\bin\cp rclone-v1.53.3-windows-386/rclone.exe /usr/bin # Build - c:\msys64\usr\bin\bash -lc "make windows_exe" - c:\msys64\usr\bin\bash -lc "./ci/deliver.sh dist/paperwork.zip windows .zip" - c:\msys64\usr\bin\bash -lc "make uninstall" paperwork-2.1.1/.gitmodules000066400000000000000000000005371417573700700157220ustar00rootroot00000000000000[submodule "sub/libinsane"] path = sub/libinsane url = https://gitlab.gnome.org/World/OpenPaperwork/libinsane.git [submodule "sub/pyocr"] path = sub/pyocr url = https://gitlab.gnome.org/World/OpenPaperwork/pyocr.git [submodule "sub/libpillowfight"] path = sub/libpillowfight url = https://gitlab.gnome.org/World/OpenPaperwork/libpillowfight.git paperwork-2.1.1/AUTHORS.py000066400000000000000000000015151417573700700152410ustar00rootroot00000000000000# Code contributors is automatically completed by the Makefile patrons = """ Aszlig René Ribaud """ authors_ui = """ Mathieu Jourdan """ authors_translators = """ French: Jerome Flesch German: Tristan Kohl (Mirodin) Spanish: Iñigo Figuero Ukrainian: Daniel Korostil """ authors_documentation = """ Alain arthurlutz Christophe Delaere Christophe GERARDIN (RTDaemons) Daniel Hahler Davidbrcz Jehan lb1a Ludo Samuel Dorsaz Santiago Castro swap38 """ paperwork-2.1.1/AUTHORS.ui.json000066400000000000000000000002001417573700700161640ustar00rootroot00000000000000[ { "UI and UX": [ ["", "Mathieu Jourdan", 75], ["", "Jerome Flesch", 25] ] } ] paperwork-2.1.1/CONTRIBUTING.md000066400000000000000000000201731417573700700157740ustar00rootroot00000000000000# Reporting bug If you want open a ticket on Gitlab, the following information is needed: - Exact Paperwork version - Operation system (Windows ? Linux ?) - If you use Linux: how did you install Paperwork ? Using Flatpak ? - Logs of the session where the bug happened are strongly recommended. - If the bug is a UI bug, a screenshot is strongly recommended. # Other contributions [You can help in many ways](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/wikis/Contributing): - [Code contributions](doc/install.devel.markdown) - UX and UI designs ([example](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/issues/356#note_244099)) - Testing - [Translating](https://translations.openpaper.work) - Documentation (markdown files or [LyX](https://www.lyx.org/)/[PDF files](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/tree/master/paperwork-gtk/doc) integrated in Paperwork) For most tasks, being familiar with Git is really helpful. Most of the communication happens on the [bug tracker](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/issues) or on the [forum](https://forum.openpaper.work/) Sometimes [IRC](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/wikis/Contact#irc) is used too. # Code contribution [OpenPaperwork-core documentation](https://doc.openpaper.work/openpaperwork_core/latest/) Rules are: * All commits go to the branch `develop`. I (Jflesch) will cherry-pick them in the branches `master` (stable) or `testing` (release) if required. * Paperwork is made to be *simple* to use (think simple enough that your own mother could install and use it) * Paperwork is open-source software (GPLv3+) * Run `make check` and `make test`. If they fail, your changes will be rejected. * Consider adding automated tests. * Consider updating the user manual (paperwork-gtk/src/paperwork\_gtk/model/help/data/\*.tex) * Your changes must respect the [PEP8](https://www.python.org/dev/peps/pep-0008/): you can use the command `make check` to check your changes * You must not break existing features. * You're strongly encouraged to discuss the changes you want to make beforehand (on the bug tracker, on the forum or on IRC). * Your contribution must be maintainable: It must be clear enough so that somebody else can maintain it. If it is a complicated piece of code, please comment it as clearly as possible. * Your contribution must and will be reviewed (most likely by me, Jflesch) * If you make an important contribution, please try to maintain it (fix bugs reported by other users regarding features you added, etc). * Unmaintained and unmaintainable pieces of code will be removed, sooner or later. * [Please try to have one change per commit](https://www.freshconsulting.com/atomic-commits/). * If you see pieces of code that doesn't follow these rules, feel free to make a cleanup commit to fix it. Please do not mix cleanups with other changes in a same commit. * If you add new dependencies, please update: * setup.py scripts as required (beware of Windows support) * `chkdeps()` methods as required * Flatpak JSON files as required Same rules apply for all the libraries in Openpaperwork: PyOCR, Libinsane, etc. Regarding PEP-8, the following rules must be strictly followed: 1. Lines are at most 80 characters long 2. Indentation is done using 4 spaces # Continous Integration and Delivery There is a [Continous Integration and Delivery](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/pipelines) running. All changes must leave the CI/CD OK. You can have look at the file .gitlab-ci.yml to know what the CI/CD build and check. # Branches Paperwork follows a process similar to the [GitFlow branching model](http://nvie.com/posts/a-successful-git-branching-model/). Permanent branches: * `master` : always match the last released version of Paperwork + some extra bugfixes. Only documentation updates and **safe** bugfixes are allowed on this branch. Minor versions come from this branch. Please **do not** send me changes for the branch 'master'. Send them for the branch `develop`, and I will cherry-pick them on `master` if required. * `develop` : where new features, cleanup, etc go. * `testing` : the next version of Paperwork, during its testing phase. No new feature is allowed. Only bugfixes, translations and documentation updates. When no new version is being prepared, it simply matches the branch `master`. (Those pre-release branches should be called `release-xxx`, but here it's called `testing` to simplify CI/CD management) Temporary / feature development branches: * `wip-xxxx` Bug fixes and other contributions always go first in the branch `develop`. They may or may not be cherry-picked into the branches `testing` and `master` by Paperwork maintainer (Jerome Flesch). # Main dependencies Paperwork depends on various libraries: * GLib, Cairo, GTK, etc: GUI * Poppler: Reading PDF * Pillow: Reading images, basic operations on images, writing images * [PyOCR](https://gitlab.gnome.org/World/OpenPaperwork/pyocr): Wrapper for Tesseract + Parsing and writing of hOCR files * [Libpillowfight](https://gitlab.gnome.org/World/OpenPaperwork/libpillowfight): Various image processing algorithms * [Libinsane](https://gitlab.gnome.org/World/OpenPaperwork/libinsane): Crossplatform access to scanners # General code structure Paperwork is divided in many Python packages: * openpaperwork-core: - The [core](https://doc.openpaper.work/openpaperwork_core/latest) is the piece of code that manages the plugins. It's designed to be as minimal as possible. - Various plugins who could be useful in pretty much any other application, GUI or not (for instance, [Pythoness](https://framagit.org/OpenPaperwork/pythoness)). * openpaperwork-gtk: - Various plugins who could be useful in pretty much any Gnome/GTK application (for instance, [Pythoness](https://framagit.org/OpenPaperwork/pythoness)). * paperwork-backend: - Plugins for Paperwork independant from any type of frontends (plugins to manage the work directory, provide various features, access scanners, etc) * paperwork-gtk: - Plugins and bootstrap module that compose the GTK user interface of Paperwork * paperwork-shell: - Plugins and bootstrap module that compose the shell interface (CLI or JSON) # Tips ## Virtual env You can easily get a Python virtual environment that includes OpenPaperwork dependencies (Libinsane, ...) by using the script `activate_test_env.sh`: ```sh make clean # delete any previously existing virtual env source ./activate_test_env.sh # build and load a virtual env make install # install Paperwork and its Python dependencies in the virtual env paperwork-gtk ``` ## Debug On GNU/Linux, you can increase debug level by using the following command: ```sh paperwork-gtk config put log_level str debug ``` Or, if you use Flatpak: ```sh flatpak run --command=paperwork-gtk work.openpaper.Paperwork config put log_level str debug ``` You can revert the log level by setting it back to `info` instead of `debug`. ## Separate Paperwork configuration for development from your day-to-day configuration If you want to make changes, here is a tip that can help you: Paperwork looks for a file `paperwork2.conf` in the current work directory before looking for a `~/.config/paperwork2.conf` in your home directory. So if you want to use a different configuration and/or a different set of documents for development than for your real-life work, just copy your `~/.config/paperwork2.conf` to `./paperwork2.conf`. Note that the settings dialog will also take care of updating `./paperwork2.conf` instead of `~/.config/paperwork2.conf` if it exists. # Versionning Version have the following syntax: <M>[.<m>[.<U>[-<extra>]]] ## M = Major version Major changes made / product completed. On libraries, it means completely incompatible API with the previous version. ## m = minor version Minor changes made. On libraries, it means new major features have been added, but API should remain compatible. ## U = update version Only bugfixes or very minor features. ## Extra May match a Git tag done before a big change (for instance: before switching from Gtk 2 to Gtk 3). If extra == "git", indicates a version directly taken from the git repository. paperwork-2.1.1/LICENSE000066400000000000000000001045051417573700700145520ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {one line to give the program's name and a brief idea of what it does.} Copyright (C) {year} {name of author} This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: {project} Copyright (C) {year} {fullname} This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . paperwork-2.1.1/Makefile000066400000000000000000000131231417573700700152000ustar00rootroot00000000000000# order matters (dependencies) ALL_COMPONENTS = \ openpaperwork-core \ openpaperwork-gtk \ paperwork-backend \ paperwork-shell \ paperwork-gtk RELEASE ?= build: openpaperwork-core_install_py: echo "Installing openpaperwork-core" $(MAKE) -C openpaperwork-core install_py %_install_py: openpaperwork-core_install_py echo "Installing $(@:%_install_py=%)" $(MAKE) -C $(@:%_install_py=%) install_py clean: $(ALL_COMPONENTS:%=%_clean) rm -rf build dist rm -rf venv rm -f data.tar.gz make -C sub/libinsane clean || true make -C sub/libpillowfight clean || true make -C sub/pyocr clean || true install_py: download_data $(ALL_COMPONENTS:%=%_install_py) install: install_py uninstall: $(ALL_COMPONENTS:%=%_uninstall) uninstall_py: $(ALL_COMPONENTS:%=%_uninstall_py) uninstall_c: $(ALL_COMPONENTS:%=%_uninstall_c) version: $(ALL_COMPONENTS:%=%_version) check: $(ALL_COMPONENTS:%=%_check) test: $(ALL_COMPONENTS:%=%_test) data: $(ALL_COMPONENTS:%=%_data) upload_data: data tar -cvzf data.tar.gz \ paperwork-backend/src/paperwork_backend/authors/*.json \ paperwork-gtk/src/paperwork_gtk/model/help/out/*.pdf \ paperwork-gtk/src/paperwork_gtk/icon/out/*.png ci/deliver_data.sh data.tar.gz data.tar.gz: ci/download_data.sh data.tar.gz download_data: data.tar.gz tar -xvzf data.tar.gz doc: $(ALL_COMPONENTS:%=%_doc) upload_doc: $(ALL_COMPONENTS:%=%_upload_doc) release_pypi: version download_data l10n_compile $(MAKE) $(ALL_COMPONENTS:%=%_release_pypi) RELEASE=${RELEASE} release: $(ALL_COMPONENTS:%=%_release) ifeq (${RELEASE}, ) @echo "You must specify a release version (make release RELEASE=1.2.3)" @echo "Also makes sure to update:" @echo "- AUTHORS.ui.json" exit 1 else @echo "Will release: ${RELEASE}" git tag -a ${RELEASE} -m ${RELEASE} git push origin ${RELEASE} make clean make version make release_pypi @echo "All done" @echo "IMPORTANT: Don't forgot to add the latest release on Flathub !" endif linux_exe: $(ALL_COMPONENTS:%=%_linux_exe) libinsane_win64: ${MAKE} -C sub/libinsane clean ${MAKE} -C sub/libinsane install PREFIX=/mingw64 pyocr_win64: ${MAKE} -C sub/pyocr install libpillowfight_win64: ${MAKE} -C sub/libpillowfight install_py windows_exe: # dirty hack to make cx_freeze happy # Cx_freeze looks for a file sqlite3.dll whereas in MSYS2, it's called # libsqlite3-0.dll mkdir -p /mingw64/DLLs cp /mingw64/bin/libsqlite3-0.dll /mingw64/DLLs/sqlite3.dll rm -rf $(CURDIR)/build/exe $(MAKE) $(ALL_COMPONENTS:%=%_windows_exe) # a bunch of things are missing mkdir -p $(CURDIR)/build/exe/lib cp -Ra /mingw64/lib/gdk-pixbuf-2.0 $(CURDIR)/build/exe/lib # 2nd part of the dirty hack to make cx_freeze happy rm -f $(CURDIR)/build/exe/lib/sqlite3.dll mkdir -p $(CURDIR)/build/exe/share cp -Ra /mingw64/share/icons $(CURDIR)/build/exe/share cp -Ra /mingw64/share/locale $(CURDIR)/build/exe/share cp -Ra /mingw64/share/themes $(CURDIR)/build/exe/share cp -Ra /mingw64/share/fontconfig $(CURDIR)/build/exe/share cp -Ra /mingw64/share/poppler $(CURDIR)/build/exe/share cp -Ra /mingw64/share/glib-2.0 $(CURDIR)/build/exe/share mkdir -p dist (cd $(CURDIR)/build/exe ; zip -r ../../dist/paperwork.zip *) l10n_extract: $(ALL_COMPONENTS:%=%_l10n_extract) l10n_compile: $(ALL_COMPONENTS:%=%_l10n_compile) help: @echo "make build: run 'python3 ./setup.py build' in all components" @echo "make clean" @echo "make help: display this message" @echo "make install : run 'python3 ./setup.py install' on all components" @echo "make release" @echo "make uninstall : run 'pip3 uninstall -y (component)' on all components" @echo "make l10n_extract" @echo "make l10n_compile" @echo "Components:" ${ALL_COMPONENTS} %_version: echo "Making version file $(@:%_version=%)" $(MAKE) -C $(@:%_version=%) version %_check: echo "Checking $(@:%_check=%)" $(MAKE) -C $(@:%_check=%) check %_test: echo "Testing $(@:%_test=%)" $(MAKE) -C $(@:%_test=%) test %_upload_doc: echo "Uploading doc of $(@:%_upload_doc=%)" $(MAKE) -C $(@:%_upload_doc=%) upload_doc %_doc: echo "Generating doc of $(@:%_doc=%)" $(MAKE) -C $(@:%_doc=%) doc %_data: echo "Generating data files of $(@:%_data=%)" $(MAKE) -C $(@:%_data=%) data %_clean: echo "Cleaning $(@:%_clean=%)" $(MAKE) -C $(@:%_clean=%) clean %_uninstall: echo "Uninstalling $(@:%_uninstall=%)" $(MAKE) -C $(@:%_uninstall=%) uninstall %_uninstall_py: echo "Uninstalling $(@:%_uninstall_py=%)" $(MAKE) -C $(@:%_uninstall=%) uninstall_py %_uninstall_c: echo "Uninstalling $(@:%_uninstall_c=%)" $(MAKE) -C $(@:%_uninstall=%) uninstall_c %_release: echo "Releasing $(@:%_release=%)" $(MAKE) -C $(@:%_release=%) release RELEASE=$(RELEASE) %_release_pypi: echo "Releasing $(@:%_release_pypi=%)" $(MAKE) -C $(@:%_release_pypi=%) release_pypi %_linux_exe: echo "Building Linux exe for $(@:%_linux_exe=%)" $(MAKE) -C $(@:%_linux_exe=%) linux_exe %_windows_exe: version l10n_compile download_data libinsane_win64 pyocr_win64 libpillowfight_win64 echo "Building Windows exe for $(@:%_windows_exe=%)" $(MAKE) -C $(@:%_windows_exe=%) windows_exe %_l10n_extract: echo "Extracting translatable strings from $(@:%_l10n_extract=%)" $(MAKE) -C $(@:%_l10n_extract=%) l10n_extract %_l10n_compile: echo "Compiling translated strings for $(@:%_l10n_compile=%)" $(MAKE) -C $(@:%_l10n_compile=%) l10n_compile venv: echo "Building virtual env" make -C sub/libinsane build_c virtualenv -p python3 --system-site-packages venv .PHONY: help build clean test check install install_py install_c uninstall \ uninstall_c uninstall_py release release_pypi libinsane_win64 \ pyocr_win64 libpillowfight_win64 doc upload_doc data upload_data \ download_data l10n_extract l10n_compile paperwork-2.1.1/README.markdown000066400000000000000000000107161417573700700162460ustar00rootroot00000000000000# Paperwork - [openpaper.work](https://openpaper.work/) ## Description Paperwork is a personal document manager. It manages scanned documents and PDFs. It's designed to be easy and fast to use. The idea behind Paperwork is "scan & forget": You can just scan a new document and forget about it until the day you need it again. In other words, let the machine do most of the work for you. ## Screenshots ### Main Window ### Search Suggestions ### Labels ### Settings window ### Command line ![Command line](https://storage.sbg.cloud.ovh.net/v1/AUTH_6c4273c748b243c58df3f6942075e0c9/gitlab.gnome.org/paperwork-shell/search.gif) ## Main features * Scan * Automatic detection of page orientation * OCR * Document labels * Automatic guessing of the labels to apply on new documents * Search * Keyword suggestions * Quick edit of scans * PDF support * [Kick-ass command line interface](/paperwork-shell/README.markdown) Papers are organized into documents. Each document contains pages. ## Installation Note regarding updates: If you're upgrading from a previous version installed with pip, it is strongly recommended you uninstall it first before installing the new version. * GNU/Linux: * Distribution-specific methods: * [GNU/Linux Archlinux](doc/install.archlinux.markdown) * [GNU/Linux Debian](doc/install.debian.markdown) * [GNU/Linux Fedora](doc/install.fedora.markdown) * [GNU/Linux Gentoo](doc/install.gentoo.markdown) * [GNU/Linux Ubuntu](doc/install.debian.markdown) * [Using Flatpak](doc/install.flatpak.markdown) * [GNU/Linux Development](doc/install.devel.markdown) * Microsoft Windows: * [Installer](https://openpaper.work) * [Development](doc/devel.windows.markdown) ## Uninstallation ### GNU/Linux [Doc](doc/uninstall.linux.markdown) ### Windows If you used the installer from [OpenPaper](https://openpaper.work), Paperwork can be uninstalled like any other Windows application (something like Control Panel --> Applications --> Uninstall). If you installed it manually (for development), you can follow the same process than for [GNU/Linux](doc/uninstall.linux.markdown) ## Donate [Help us help you ! ;-)](https://www.patreon.com/openpaper) ## Contact/Help * [Extra documentation / FAQ / Tips / Wiki](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/wikis/) * [Forum](https://forum.openpaper.work/) * [IRC](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/wikis/Contact#irc) * [Bug tracker](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/wikis/Contact#bug-trackers) * [Contributing to Paperwork](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/wikis/Contributing) ## Details It mainly uses: * [Sane](http://www.sane-project.org/)/[Libinsane](https://gitlab.gnome.org/World/OpenPaperwork/libinsane#readme): To scan the pages * [Tesseract](https://github.com/tesseract-ocr)/[Pyocr](https://gitlab.gnome.org/World/OpenPaperwork/pyocr#readme): To extract the words from the pages (OCR) * [GTK](https://www.gtk.org/): For the user interface * [Whoosh](https://pypi.python.org/pypi/Whoosh/): To index and search documents, and provide keyword suggestions * [Scikit-learn](https://scikit-learn.org/): To guess the labels * [Pillow](https://pypi.python.org/pypi/Pillow/)/[Libpillowfight](https://gitlab.gnome.org/World/OpenPaperwork/libpillowfight): Image manipulation ## Licence GPLv3 or later. See COPYING. ## Development All the information can be found on [the wiki](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/wikis). paperwork-2.1.1/activate_test_env.sh000077500000000000000000000004601417573700700176060ustar00rootroot00000000000000if ! [ -e sub/libinsane/Makefile ] ; then git submodule init git submodule update --recursive --remote --init fi make venv && \ source venv/bin/activate && \ cd sub/libinsane && \ source ./activate_test_env.sh && \ cd ../.. && \ make -C sub/pyocr install_py && \ make -C sub/libpillowfight install_py paperwork-2.1.1/ci/000077500000000000000000000000001417573700700141335ustar00rootroot00000000000000paperwork-2.1.1/ci/deliver.sh000077500000000000000000000021551417573700700161270ustar00rootroot00000000000000#!/bin/sh binary="$1" os="$2" exe_suffix="$3" if [ "${os}" = "linux" ] ; then arch="amd64" else arch="x86" fi if [ "${os}" = "linux" ] ; then arch="amd64" else arch="x86" fi if [ -z "$RCLONE_CONFIG_OPENPAPERWORK_ACCESS_KEY_ID" ] ; then echo "Delivery: No rclone credentials provided." exit 0 fi if ! which rclone; then echo "rclone not available." exit 1 fi echo "Delivering: ${binary} (${CI_COMMIT_REF_NAME} - ${CI_COMMIT_SHORT_SHA})" echo "Destination: ${os}/${arch} (${exe_suffix})" out_name="paperwork-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHORT_SHA}${exe_suffix}" latest_name="paperwork-${CI_COMMIT_REF_NAME}-latest${exe_suffix}" echo "rclone: ${out_name}" if ! rclone --config ./ci/rclone.conf copyto "${binary}" "openpaperwork:openpaperwork-download/${os}/${arch}/${out_name}" ; then echo "rclone failed" exit 1 fi echo "rclone: ${latest_name}" if ! rclone --config ./ci/rclone.conf copyto \ "openpaperwork:openpaperwork-download/${os}/${arch}/${out_name}" \ "openpaperwork:openpaperwork-download/${os}/${arch}/${latest_name}" ; then echo "rclone failed" exit 1 fi echo Success exit 0 paperwork-2.1.1/ci/deliver_data.sh000077500000000000000000000015701417573700700171200ustar00rootroot00000000000000#!/bin/sh input_file="$1" if ! [ -f "${input_file}" ] ; then echo "You must specify an input file to upload" exit 1 fi if [ -z "$RCLONE_CONFIG_OPENPAPERWORK_ACCESS_KEY_ID" ] ; then echo "Delivery: No rclone credentials provided." exit 0 fi if ! which rclone; then echo "rclone not available." exit 1 fi echo "Delivering: ${input_file} (${CI_COMMIT_REF_NAME} - ${CI_COMMIT_SHORT_SHA})" out_name="${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHORT_SHA}" latest_name="${CI_COMMIT_REF_NAME}_latest" if ! rclone --config ./ci/rclone.conf copy \ "${input_file}" \ "openpaperwork:openpaperwork-download/data/paperwork/${out_name}/" ; then echo "rclone failed" exit 1 fi if ! rclone --config ./ci/rclone.conf sync \ "${input_file}" \ "openpaperwork:openpaperwork-download/data/paperwork/${latest_name}/" ; then echo "rclone failed" exit 1 fi echo Success exit 0 paperwork-2.1.1/ci/deliver_doc.sh000077500000000000000000000016521417573700700167550ustar00rootroot00000000000000#!/bin/sh directory="$1" destination="$2" if ! [ -d "${directory}" ] || [ -z "${destination}" ] ; then echo "You must specify a directory to upload and a destination directory" exit 1 fi if [ -z "$RCLONE_CONFIG_OPENPAPERWORK_ACCESS_KEY_ID" ] ; then echo "Delivery: No rclone credentials provided." exit 0 fi if ! which rclone; then echo "rclone not available." exit 1 fi echo "Delivering: ${directory} (${CI_COMMIT_REF_NAME} - ${CI_COMMIT_SHORT_SHA})" out_name="$(date "+%Y%m%d_%H%M%S")_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHORT_SHA}" latest_name="latest" if ! rclone --config ./ci/rclone.conf copy \ "${directory}/" \ "openpaperwork:openpaperwork-doc/${destination}/${out_name}" ; then echo "rclone failed" exit 1 fi if ! rclone --config ./ci/rclone.conf sync \ "${directory}/" \ "openpaperwork:openpaperwork-doc/${destination}/latest" ; then echo "rclone failed" exit 1 fi echo Success exit 0 paperwork-2.1.1/ci/download_data.sh000077500000000000000000000017651417573700700173030ustar00rootroot00000000000000#!/bin/sh WGET_OPTS="-q" if [ -n "${CI_COMMIT_REF_NAME}" ] ; then branch="${CI_COMMIT_REF_NAME}" else branch=$(git symbolic-ref -q HEAD) echo "Current ref: ${branch}" branch=${branch##refs/heads/} branch=${branch:-master} echo "Current branch: ${branch}" fi commit="$(git rev-parse --short HEAD)" echo "Current commit: ${commit}" download() { url="$1" out="$2" echo "${url} --> ${out} ..." if wget ${WGET_OPTS} "${url}" -O "${out}" ; then echo "OK" exit 0 fi rm -f "${out}" echo "FAILED" } filename="$1" if [ -f "${filename}" ] ; then echo "File ${filename} already downloaded" exit 0 fi download "https://download.openpaper.work/data/paperwork/${branch}_${commit}/${filename}" "${filename}" echo "[FALLBACK]" download "https://download.openpaper.work/data/paperwork/${branch}_latest/${filename}" "${filename}" echo "[FALLBACK]" download "https://download.openpaper.work/data/paperwork/master_latest/${filename}" "${filename}" echo "FAILED: Unable to download ${filename}" exit 1 paperwork-2.1.1/ci/rclone.conf000066400000000000000000000002301417573700700162570ustar00rootroot00000000000000[openpaperwork] type = s3 env_auth = false region = us-east-1 endpoint = https://objects.openpaper.work/ location_constraint = server_side_encryption = paperwork-2.1.1/ci/update_flatpak_repo.sh000077500000000000000000000043331417573700700205060ustar00rootroot00000000000000#!/usr/bin/bash echo $PWD git log -1 USER=gitlab-runner LOCKDIR=/tmp/build.lock.d PIDFILE=${LOCKDIR}/pid branch="${CI_COMMIT_REF_NAME}" echo "Branch: ${branch}" if [ "${branch}" != "master" ] && [ "${branch}" != "testing" ] && [ "${branch}" != "develop" ]; then echo Nothing to do exit 0 fi msg() { echo "#####" "$@" "######" } download() { url="$1" out="$2" echo "${url} --> ${out} ..." if ! wget -q "${url}" -O "${out}" ; then echo "FAILED" rm -f "${out}" exit 1 fi echo "OK" } export LANG=C if ! mkdir ${LOCKDIR} ; then pid=$(cat ${PIDFILE}) msg "Lock directory present (PID: ${pid})" if kill -0 ${pid} ; then msg "PID ${pid} alive" exit 1 fi fi cleanup() { msg "Cleaning up ${LOCKDIR}" rm -rf ${LOCKDIR} } # possible race condition if the other was stopping # -> re-mkdir mkdir -p ${LOCKDIR} msg "PID: $$ \> ${PIDFILE}" echo $$ > ${PIDFILE} # We make our own copy of the repository: there will be a big .flatpak-builder # created in it with a lot of cache files we want to reuse later. mkdir -p ~/git cd ~/git if ! [ -d paperwork ] ; then if ! git clone https://gitlab.gnome.org/World/OpenPaperwork/paperwork.git ; then echo "Clone failed !" exit 1 fi fi cd paperwork if ! git checkout "${branch}" || ! git pull ; then echo "Git pull failed !" exit 1 fi mkdir -p ~/flatpak # directory that contains the repository directory cd flatpak/ rm -f data.tar.gz download "https://download.openpaper.work/data/paperwork/${branch}_latest/data.tar.gz" ../data.tar.gz export EXPORT_ARGS="--gpg-sign=E5ACE6FEA7A6DD48" export REPO=/home/${USER}/flatpak/paperwork_repo for arch in x86_64 ; do msg "=== Architecture: ${arch} ===" export ARCH_ARGS=--arch=${arch} msg "Cleaning ..." if ! make clean ; then msg "Clean failed" cleanup exit 2 fi if [ -z "${branch}" ]; then msg "Building ..." if ! make ; then msg "Build failed" cleanup exit 2 fi else msg "Building branch ${branch} ..." if ! make ${branch}.app ; then msg "Build failed" cleanup exit 2 fi if ! make upd_repo ; then msg "Repo update failed" cleanup exit 2 fi fi msg "Cleaning ..." if ! make clean ; then msg "Clean failed" cleanup exit 2 fi done cd .. chmod -R a+rX ${HOME}/flatpak cleanup paperwork-2.1.1/doc/000077500000000000000000000000001417573700700143055ustar00rootroot00000000000000paperwork-2.1.1/doc/devel.windows.markdown000066400000000000000000000055311417573700700206450ustar00rootroot00000000000000# Paperwork development on Windows ## Build dependencies Paperwork build is based on [Msys2](https://www.msys2.org/). You must first compile and install [Libinsane](https://doc.openpaper.work/libinsane/latest/libinsane/install.html) in your MSYS2 environment. You can have a look at the [.gitlab-ci.yml](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/blob/develop/.gitlab-ci.yml) (target `windows_exe`) to have an exhaustive list of all the required MSYS2 packages. Some Python packages are automatically downloaded and installed by setuptools when running `make install` / `make install_py` / `python3 ./setup.py install` / etc. Once everything is installed: * `git clone https://gitlab.gnome.org/World/OpenPaperwork/paperwork.git` * You can run `make install` (GNU Makefile) to fetch all the Python dependencies not listed here and install Paperwork system-wide. However, it won't create any shortcut or anything (the installer creates them). Tesseract is not packaged in the same .zip than Paperwork and is not required to build Paperwork executable and .zip. It is only required for running it. Already-compiled version of Tesseract and its data files are available on [download.openpaper.work](https://download.openpaper.work/tesseract/) and are the files actually downloaded by the installer. ## Running Once installed system-wide, you can run `paperwork-gtk`. ## Packaging ```sh make windows_exe ``` It should create a directory 'paperwork' with all the required files, except Tesseract. This directory can be stored in a .zip file and deploy wherever you want. ## Adding Tesseract [PyOCR](https://gitlab.gnome.org/World/OpenPaperwork/pyocr) has 2 ways to call Tesseract. Either by running its executable (module ```pyocr.tesseract```), or using its library (module ```pyocr.libtesseract```). Currently, for convenience reasons, the packaged version of Paperwork uses only ```pyocr.tesseract```. By default, this module looks for tesseract in the PATH only, and let Tesseract look for its data files in the default location. However, when packaged with Pyinstaller, PyOCR will also look for tesseract in the subdirectory ```tesseract``` of the current directory (```os.path.join(os.getcwd(), 'tesseract')```). It will also set an environment variable so Tesseract looks for its data files in the subdirectory ```data\tessdata```. So in the end, you can put Paperwork in a directory with the following hierarchy: ``` C:\Program Files (x86)\OpenPaper\ (for example) |-- Paperwork\ (for example) | |-- Paperwork.exe |-- (...).dll | |-- Tesseract\ | |-- Tesseract.exe | |-- (...).dll | |-- Data | |-- paperwork.svg |-- (...) | |-- Tessdata\ |-- eng.traineddata |-- fra.traineddata ``` Note that it will only work if packaged with cx_freeze (`sys.frozen == True`). paperwork-2.1.1/doc/flatpak_saned_1.png000066400000000000000000002634701417573700700200430ustar00rootroot00000000000000PNG  IHDR #zTXtRaw profile type exifxڭg7c ,6`?A)Ra4kru?rV{??4|~?ݾ??w~}|O|O= >/`sgCl } ?IwB)~>]7//~F0vz)xRH> wA|TRz?o#;?GC|PT~Q??gy+P?B~I(;eWW񈋌m9Z. 90 }_a1Oѭg-Yqz.5rZs,ݷи /rBtCP0G{b\/ CEBƴ/3Qb,/̍~~.1Ks8uF!ޅD| h!F~#)IB)qwM,{swl,1B"JM#Cd\ˍMvZ˨ZjVQÒe+VͬYR˭ڬ' {.=8{p34,Nm9*.[m5vins R:SvZ[nvw7g--sy7kX~_Yc7eʙo!)gGD2F 3we?͛+osJ9Y1Jz R*>]ug'quۣۮcڭ<Խikq.mέ]S'䲈vgwpD1џctY3T;;3NgX fE!ĵ;3עtm}ߜ.zmc3_}Fmx0s.\o?j-5QXcm!D3va:Ff vl9|cvN&T~J=MFF)6NN7\^TܜA KVv;SQ|čg)v}l~6h(9կ2Dv@ dJsY~l-{Rá~lSWc7?#p:A㲪Y}(F1ߩ?ʷq8rϣO-V#g?bY?<>_[?šq5/V٨/CGkd5o~{/ Bmu)mz"!`M%٬84Bٜr|b'6 t>PeIWB{NWRz69|X5m~rqȣC<8zX`tp<ɇč6oEL+'cخռa2gK>׋"~uLnz ky@Msv @c1zh@ 2jM+F|za%⛈9NjH ehe*ާէuTߗ23S[yB4uF^ :2aJ- 䱬YP)p@>5p; i*f!m{2LJ#&sh¿3t`&e?(g 4وꨜ@'25. xOQRs DTHKhu@pٱ;x^В%L_s mwb: jHNSWmKc;IIp"nO@=pc>Vu]d#API5qd=]5 Sښ a@YBИމ\sFOjͧ#vm>.Aה5ބy^2Ǚ m";äASSH6 WEeDrX<0)]3nYx.j!!1M O_ qg:`#wY}x<if5tz4x%N(* 2#B݌M]&wQXx]: _}AiѡѠnN|0_ܗ葻cꑦD1!nT #B/= ׸C`Ks(;AK =ϒ_R~A#;U;Ck>~ =H1Ȣ~]^Q=o.v+BgPc>CT5HHbjWr wZ3v]EzXk{j3+@I$88O|\:ր(?(Dt6"郜"[@#jPĤfc`}O#uy~@BH(q"mU41s82xNz w&7O$`Gr9C iQ&)LV # tFhea wr%0{o 2c,AcZZ›s9@p^DJ#.95a!URU5x:V/FY[g A8g 1Y8k/NfsRd<0^yfʐklBŁdbvvTGMyZ %! F~F*LbT"GۚIޚ,qATʢVN?VD4}iF@G]5[ ]W-#Iq j&Dm72~pUŒ.DH`l.TNtii 5P֔bJ4:م@)RA!RFl<p ; AĐ?8D5"|C#xmQ[=R)Rxjʋ1C<͢3<Չ<|rJٚFl煝_T;p 0A8a)kDw-ܜѿT`3wy1A=-P,і{@'%%R10yӋ23.jMD2j17k67 +`H7b{H3䝀Aŭhp%@O.$~pԅ" .{jHڳ%@0 2hRUt }外|r jsD?oʅ@aXSX?D|3pabڨgp¨<xbSi21L~iס_J$u68x\ Г_qS{tWCkN[Ev"s,kg trV/$Rs;wZ$^t] y6:j>sX\/xexMxPgWi/kX2FXw0s_ TJv%M@"FV5'MAq)>X]QiëZRE,:8 txXs$6%2OFM.i1v6I[6!֤C7ٴ!1)HK}8 P^t8j&8*'i&"W͡#SWr#^vKhJm$i#6壕|1;8[oipju)d]%iIMT JJ)V9qrmBӪP߭)ߏ®[5Glth k7Z 6Ʌ8dƢoEЩ>7#ƛ @h(?OB{ʵ+GкV*Em+heO4D j5u`H-c\|CBX<~^6gY`yaD b%s)V(?=}p7eRVҒTf![q!Rp#3|$"6IPs@1? 3eliR |=O%gMxA@-Z5AU}N*PhOASr{{薁A!V4A(Pn.4zGJ|JW#VEk_VȂ/ukU7o !! V= +nGmݴ:aI>4, (JWKA&z lkDM'6=7ha|?k;ݏ4Ñ+Ji2'4ftM0 @l$ТQn3u@!gό $cŏT)9KP"F2 `Lj^)) ަܢbGSP Ckom`bkQq+fݛ4k(Rc5m$H?E!|ܔ54匪#e{M}ky "THߴYPHIEPbP&O;t)J~V'"l})S1IdHmyR>הc {8m]׎V0 1kef2QCcExѷQZЛ-[s=j%zeYq>'0/]XB]G譮Wv6{[<gLfͲȝk;I\Ҩ<̯՜wuN#V?bVo*KwKAUA{C `ęA(F8A GZ}T,.ANFT=) llY^j}m6}OMGj=5s'LH G h@QE:;66oK3`֋9$ b >66TS"QXVz8k{x2j+o>{;x:Rт#e43΅nsQg e(:mXRe N^;t&tTq)КP-./ 4>&g)w|ы]AiԚ2ƴu tW5]UKh !ǃi> z"HxsiN z|ŹYM uw!4/=A-)Zd*>iGLNZAIۋj\K裬$ :k HEj֊GK1 ʉs"ǒ6kgVN*+# iMl_H~AwyެYr-iF iW3=rI䦕/ێ緍F`DYAk<j )8Qs]R}W=煍M}q_+imco O=B8S3?ama kYQml4n{p#'c o&>3?lžbaMu ޶m|&Y?nނ\_ac;QkD]ﮃ0h(f2-8wWSsQ$o|1~DJ y(_|9ETڢUFv٬hQ lF1OH"k_ 2" OVآ RouZ+,Ls ߨq |?CF_0f1AAVb gE (~E?~f磑5QNERiu0~yF5v]nfm7#'(l}VXߣ?9 Wds֡E3Z`` O-P3\e?>}FDR?J}O3ʄHx6Q?Bڶ*(&)77͕8QH(mjp' 0&栛r4hZk A2)98ZP`)5qHHEPL j}^zͩ .43?ywTLk%Dms+mcFzu##}[*:2e6K#a|t)8A ht_)+u!]RC^HZT֑~5-eL]'.io]2#7f7u!`@3pwts >Kֱ)K5Yij Qg(#72Bj]Z+ox Cjǩ GFNH@Akoc:!!,~7uI"2h*LU#LY֥=!Ȕti K2|>K!`ƙ!+Bzg+w`gKd<,!˄\h%Z&"ҝSOV]O Z0" o`ޘ⢼h0KTM#RD)!%źgXnL+*(F$a)mX,0^̪ cIriBU  ..lư=~{T<(L?Y"43̰.*W 0 XnY8a#35p-X€ krN$`wSJ:%򴛥:~SD2׺  ?X&"2V买T }PH U ~ρPiP::ƙm$U:Q?,0%4xT9+að~7s>,Ve6CW=_k{HfFTpGN1K8 Ngv3V!ZK+~fg;64FmHq$TU+4/'l(cBѤ HSą&f倥%6))cW14bL2L #㍑0|>0!\8&k7Q#Z?zRuMDC }0 W]M|p~ސRVoF3k:.QRx ǶGa)]nn ^@ L#שWf,RjصA&.dT~w{|t\42f0aD3VR3.(GA 0 Ӛ׹YPNFpSW+Dˍ,k9HY;Eeԅ ȜJ܎sŅCFiYQSЃUP r*AIRYHPL yFT঴`ST&_\FQUf隮Tu<͗Qw .QwpIz#>q.HxuׅY9a@ Q! U7D5nF Ӻ>` P@KS;)i 9(-Wn:g@#C]Mb507R`?(1$-2 Dm :Ss[QnF ۬ ۜuQ]IbM}T/+K BߜtafXjCs5`[啨Q>0%*f0߷Ru`Uw;Uaصm`4M/!jt#9ni#7>F{؅{ݺΧg)e֞V"hd9ΐ_d0wx'tpuTEr⪩~u9u0hvV˾4dBG#GykFvcFnĬ ڎC 6#e%Fdآsx\T5{X5lA3 v7C:=:,b )d%F i`;~Wn*}&jcHu2Dft,l +_(%.s_zד;aw*_zz/LV:rwTJ_&|kz?o1(DXEBo.F(Ph|*R\rğZ-}.P猄&d "2/ʧ[>0INJ e˪4a>A jS>MStƚvE8q(TofS\2^+AVB}p C-?wqQ"!J$)"fO8ztT"A `BҦs(U/%c{JUUpl"PGbwwEL2,zȋ Q!A: $C?N!}8# ,u'8k,h߳=7inumߊ֪QYQLPՌUHRC+nuK+߶#WSORgjлw(P#CbFZkapcl&vUGע49XQ U*+HSE3O?~8 N(-Q¨j!1A#ůzM}@z4Ȓ2*łKB ]JX :-$A :jD3CWw<osc6Tre'CTEe 7ǶG Wj0yZ5} lD[~̝_z.8ֱ٬WS}V |Ԫr &%OnD#ZHETFeX\X K5\"˵dss(Y+..}|e @ⷆrj.qHWy.2mF9F7y*RJ)jʷ1|ueT_/rTØb3kq!5i|TJU_)l֔J5{poJ'T]TDXVVBз,30EIF0z$6V@cpF֕}UX1@>9* J S\5^gθmT0ڟ3FgsnLφ>r%r K+j욌-=[*|Tn,#sч%* BY'6+Tn5+}uDXmrVpcY?&]pсU8PJC}8mj C]\z84PΈK.[*c0w T,+G!~A)obkb$.U 5 !9aQ{t^_;Iq0TܶmB |j6e#di3)Q U"ܟCEضH8|V!]L@195%'[U݋Ȕޠ@KvtJVFSkvBLmuy]-"1ʗAQtZk jtFUΛٷL$UR04ބWpQNxyU^ː'cyrV`YQne IQ8SYw~y ac>ch= jx@C0^{3@"T ҁH- B~BY;5|_SIԣr>""W6X|K_XAypcH5"t;Ξ/OVAZ\~P9r*[uW] 'u$=e |9Q|]݅^4UѾ<z~rko jY%&k [I4>:*E+wUƉZ|~@жzr$YCRj$#l) te?/Wވ8Bg6uFy(Ң=ex 2bٞSܻL04r/'-[ӢMU˗ecP7Gt 21?CiC]T8xo1%Qkdk)|x2BJ:1ТMTEҺB:XH*`(p@2enMzZ4IdNBq\jt\@3l(%C뎕OfY?ePr $]bhb[+4SٽoRc~/̔#E{vhC \ZӀ`Ў~7'#.gN*NJ|A;ƎrX;:E{[I^˴=Βb`;'$Smtn); B@tS)ET(TfFwb1xb8CVˌdnm3Ť\.qz7m{f$s d>vb *sՃaFO>|?O:em~ͪJ3_ -l77[ˉM@w\*3e/ !DxկQ,yrk_uz QW{yRB[jIH!lۺ 9IU_slr9 &s/a5җ4G׳|{9?EGYos_:WV\.7>(CC̛3v F\_Tbܹ\p[iKdQG_ -_\+9yNŲ,~ R$g Y[n)Wbj;㰃04<-OOC W\pyo;=ebO:7q,fbY/=d. @w׼hxࡇͷr*z{{8%/᲋/׼3fxetvtĈ~+[l=( `ƍ(RG3xgo~L6/]-~ ΙͱGhoǶO,\Ǟ|v;RJ=x!e/㫘u2k֭'L./O.cqtGx[o/>@NKغm gޞJVϔ^^)}X"C_믔GO|[w_[&կ`;k?qn|y?p`F ~]/~#2 ߿~1ϓH&}0?r}L/zrTwUF>v͇b``>QR y4ٿ~[%nf2gM7sUZ⣏k?)⽜WyfrLLg}x.75wDž8eL\twe6׮ǟk?K~p=%r?~ښ8M͕'_DړO:7YG{3W; axtg#>E.`ds9V[=35OstuVj4RTp}#:C˃<ƃU.s P0^|w.V4nk@?H-YeY̝=TLh X%}3HTG tBC/xɤS8U3A,n/G< Op`%hT o)1hwZ$`Z )6~T*e[8J84 .L5C:G! "E}CL$ }_'-sQ|S988H<}0,&eJںm;߻6Mo`3xGk{7pg֭7mZt6\.sK/K/ {;rqBmٯb>㟒H$غ}K_XATz6<4,>H̚sKO|i}}ôg2y+I%̘>%^O~:eWw_Jߔ^1u6L:|: 1s.X⣎'>I˺ɤ۸5̹g3⣎>yo~Bpyd)@ʂuMwjebEU_D,!a!pR_[Div!7C>_१ W}L2M⣎qq6u*loO}o@+! {[718e/,Gi͡Ռ΃<[챴gxIRtutX`lw[Ƃ)/&bٟCH)>m*y8gZ0ԑ 7?ygشe }V3>qˏ3?5k=k&,}Szw?g6^n uCﻜW<ȣw2~<Ц_Ϧ-[ygغm;HC2{Bȴ}xҾF X*9gؘW;SJK4<?d۶qa|[DJI|/ߺ{\O8'xK8հ ozY<xKOӚCge_|w6_wشi3=s8Jq#>ٯ{-\wÛ̙5/g7'Ox56(n0jKluވ!mZT:G1fwW,cGD[Skwj1?O Mb>W UƶFNa/i&d|uW׿7:8CT@{1KA"0c\*z.wB(d ) lۢT,Զ=DQyrl1TxX,4VU}My[Hj@)ETĩnUs_+}z(m۬Y#x!ށa!ZU[TD,ϪpP:UT*ύe۶ll6ql6RC/@fNWs`].,@ZTtOΩ/} \vх|Zu6A~|]UZlָ iˠc/\ppiON'yнR_v /Yڰ?a\)=c*mU!omkfZԢ X.-,*Na'IQKգҡ,2̆uc4 5 +͜DMdBbwXjtu-VxtZ!ievSJwܶ& ʤPΰJ3/n1Nk&Hc8a^qX|TXI=%6={]b-Ŋ,0V sW+?I$4MQ$#KĥCLZSI֕`yfb<>-ddlnMV;e43 @nҴXEGg!MV{x?(ӀbMcfm'bbQrceH%<_BnczlpK_,KS` ߜO$Eb.5q,xg#cк"Zw7 XZ i+]!&y'ɩ89sQTO5Ey[$dY@E Y[&cH"j6_łi'ڰ1Rrdk>Qfz|`'dC*AACe,a)!4 D#D^ht5֢@!H\T$ Zbieji6mNeD(â5iQ \nmX,/OAJZ̋mq&`I1PIIB6 a6و2)*A(k6_ K9I19mm`ir_IIQؓ0UCm3B%tڍ¤aZOsGkR2PQر6LHH53ˉS({brŴva9 ;(%z#( =dR9O`ź(gc]0Z(o'^J-jQZyeb)$qm1~C?C)aى-&flƴERI 'cķsf.3Gwz)u47mƸiX&B"1C`9ct\27Z'|n]kh݊O*9JrlI%/[!qb2kL Iw$<˷5>cdvA6ީӛ۶7fɕ5/lUj] fS(+vldm6 ƿYrfgSLl.ԊLŢ4X^ ,Jm%n.1Xczɔvsu3xN1ChFwqc5Xw0Z` f G6fKR8qE+X;hљ45DnQriфEÃ˳I/[!h^;JbK6(c)`Nw}1LcHeyg|'ZlX?`j*I%חArrq7o>RHՍJ9a ߏQ&i_Eh֭wm;ضe(*LlYH!)KSǶ1ejZ c;؎mR M)e;!p,ږ4* !%xG?h0L{sa*c)nIjjel!pUg!JF aؖR?G+r*tj8EbRJʖE4, 4ʖNFOU)5DOR80$R`[6ibFBbi4 !1eL mڞ&} ]U:l$nl,R#q@w1nP=iy_]L\.KKD(ɜ ,fKN~X-77^)nЗiފ4 833[qbݎ 4CE["n*q$ FoܴitYnnqvXtCAс3p3I]^ }[dJ ZG+4[)nm{vh(AhBX~fUVǤ}Qm4JI8hC5qTU '6*V*i)=q8RT! *8U(n{;'B򞡕FJ] q7kjΙ-pٝv{7i3+F"˵Lq!&O+¶zϖ5Kil}iiftMas^iHd$|d 1)Y8 >8]qX?`b[~+ٚXa3/ZjLbX ] (\+@:XxZnU=VRʚ+%Qy@JkC,>%B|f=dBھk4R?5vFN]ԴEZ rCw`2G|8 yUZU_Ap\KO\8*ݟRg8WTDHɢɿ.jߗeY?.zӛ,n FW7P))v4 ƘЕ,"d4Cd^_ݵ-kr[MpIy96=ڱEbGN$r ַr>?{^ У쐯DkJ >k Ѱiϝ%Ph Nd<^T\Dv_n*OЇ Lv#xCpAy+ uA"xkr܆"\ps$hE%uƘc2K6(R ;z3&Cf$Y/ͲZ?d~h7iua0E`> +?ȸh08{g5A%TxzV"kyvOQR lڮPn<5`Gu qy \v,0WbTvUXgL5w3VhI+c[#H &6.5e?ș)ڛ,eYdذfSM5{8I)I$$I{56&y«5-xhْmW~rLPvf2@$_Wph2gYp!ݜuYZ<'x"Lc9{{'Nsr׆6ĶmZΝ˔)SKXp!/P`L6w]rv~:\.sUWqAƢE=o{ǹ eˊ9̘E)Uqe_sZi=0zx sR֍k6t%bJg:X'["-JlQ7J\S j;n}܉Md|w64^駟hvN8}Y^ka4mmmdY;8N=T;0{"eJSW]p<\ct%[ߜ,ݤFՉ i&{h<`$<Ͽ 4F1-?kvF2րAR̶m}Q8∦𲗽[n{w;ujO[`B?|4wy縘s?l۶/˜zp ] SpQ' yibuȍ?4 M mN?t:,Oh"~_W|_KÎB8?KOxLh#5De3mg 0CEͪ) r%L脥(O0Y264=em6/G6K3s 8M EJo =9rNwMN`;vy 9rqKc쓏r[vg iW܀S6ò4FGjE f03,K\0/*V֨M4)m6^pKa=^c=Ƃ x`Xy$l5 !8hZ,;a@7U/T]t2papSe<ٖFF'bLMAi#e֨-6Swϊ~ns󲗽7|~{,XQ$ĞW߫!VfҭXj[\hBf3Tʊ[c-l7ืn|s:w\r%{ %M7u.Xb]Af,Rθ,!dbH^-@S'ݠi5g "m}FF4]vcdK,GS<;2[ XWʿhw |t`QK}[ݠi֊ Ƴş0WaqF@)|vekz 0e2A$rmFMBҏ{ S{۩ =x4]I+U/pfwymfYZn3IQT~ޗ,ım:x+/al?0*OI@|YG&ƾ]҈waaaω:m O\i\JIS5'KQQYK*N=tE~Gj@lGլYwsgQ&"[%اF3%LhNLr.e~wn3-\ﺌC9gJ%Drv6vFYunH<0R/%Kɏ~Yf188O>IwWWWJ[*z^rʋ9p֠7Fl (m3Ul%S>V8']RNT&H؎BT"]|DTA]y0 ,Ǫܣ1vץ違TBoe7%q nl^e4ӣv? nt5 _,U 4XUT}k-ȁx}uǞ|ڱS\%R,ӦNαƎۻ{Gu==̟7w4KbkVO hHY5_ZhZۀ!cd%6lٶct['4Hy$r92 iJBf\R.Y3LsiE;IRyFU"HI&%x AhHd2(L2I*f+ Ãۙ>7ՅmY;3K%f$1aDͤqc(ttt0<4LggBx,F!ڛjk#͑Ng(6iVXL)-Tq*Ytv4 d˱l0<8D<0$\t;3hR>O&az'(ǡC" ! ̘IP@DU㔬2&c   ##نi4xFUJ9N BVDxrًӝL3m;mmrSZ1xnlc']•AX^Z>/tR:,Zҗ[)|O?pWJr\ ,0c|iSnk; o}z3~x˭^!W[6W|>sN>SU,XLMZb)}qxATB\{_=!"zbr(9NddH4J#:ϼ$$I2ffHY@ MNk\>kd3>X&I>baii8AܬԥV4Jlh8v:]D,bڶ+1#4h5,m$II,%dXcae-Ӻ!{EuCKO IzL[lB>0HD/n3gByܕ4 u]3ܴy31%{{V2Z{<ܧ<: =gyC.k߲:~D]ٝחlV.|4~T#&0G3֑th$2m`09$e4aX3#Y!kbRHzTe "SC" S'PIŖJhM ,"f"GO-+RbmM##qK(Y%W*C"J(5Ē .c t˂mZ+z_O):41'] uUiӘ{A{0 !*F"f^ J%+AGT!xT`'p~%!3~A=w\4~ʮq1ؾA΃N?%%shTwYd;lW)@~_W[ʛCǓ a{B"\-.{w 9^fWDu|;^έXyH]b"V5J[\sF(jKT"'1*UeB"ȩBȄ117\OV m5˄3jNZD贈95QeaS1ec&X T$ZYDOj6.DJBO?o ) #Ƅ̉R}۱b&)dV&FBJJlB00JcK*C cJB$MR e .;p2gzi*H2ACʹ)2FLQ4_4F寘2n=tw| ߍi}[: 'W;a};.O}ܶu!}MŘ] ܸ-W `XJTˮQGM|-} "/]"@L\ҠM&'O e;=z$H)1b!I*HeڢɔDjcD(UHpе9RI1û\Z b`PL4&<~"c*HFQkBq9ƞlSAXFT@Fg!6J[ T@Jh%1ZC[.7'v kF"MHBsh:e$麎q,0RG.R7wL}mۯ|\wܲe x׻'`_E||K_"+s1/^۶Ѷ횏H ~g{}߼Jo[m+/,.۶n:4 ֭;䯩!嘺 Śm˫e&zTx B{ 2߼ݟ:v9AS:7ŵ-)OzJɑ=n$oM7DO>Wxs~uk_FnٲK`V>!3o1?D N'!̧>57oܼ:XIG}pXb }gvZsFfBIXl;.KyyOoKB2j|mDŽ8##K;Jq9.Jm J9gd%HI|r&IK( x.{G0Uuҹ0|/qk+ ~)KOu_ϧ>Y1pgyOg9p 9VYv=ۊw5Vev+͸ A$⪛ "y  $ BF fbRR" @)a4B+,?\<ιԨ@}|@9,  &KcbJE@̀"2$L$4%+?mv#Dž!blh CPBrOB()I5uCT !K;J-%ӶEjEtm`V*I2B#]DHA{__9b5Y x @'Ӓδ˪*1+(M'ȴ$Bu}\8zpcW/M\Mp@bii̅o~ꬳyK^<<*(qpW"i(ɑsrDEZgymA)D 羉!G) T*z]>"DJ&Z;d@+tSX᠄(!x2YIc-vu}Vfw%HMVi0;jci4tCZ8ض Ǟ:\cF93 IDATU{vU_9B& $ne;7RY <Bs"eb*й0kǎ9892*JanIڒc%]Sk՚R+ھC+AHr.%S`PEvx)!@JAt[ IKG* rR`0wB(\ UB @E<ԆqtDB(@ 1Ő\%8x,j|͞rͮ.ɺ{Jg*fW7TB¨b=i3!njFK)zH&$Er+}(Ť)aAi ,2},[ĠHPd]U#dFxK5."tA]W"HS$ˈ)9 x! A=F5: {RUY[wĐѺBH̃v)j:VZ1j8"Dӵ&Bz`0 Ig`ݺ!]D ${iݶ, DJHȢjK$qRiU7l%pLTpBI]{ iw]⒫eWn?Xfs.%n6Vixʈ;2A]],wfWn'$i#$ 8RQ YB(U,;+׮feE)OrDT^apbQ4UM wtͯ[w,vWS| ]X}!-&0WJVˋqm&k2լȪ {޵xQR"aZkPIs$FTRbm&AZo+)%9,$hG IeBAt-=QHwQS>1kl!$%.RieKq芬s$\Vrs亶J )|1[\i0 LB`JB‡锺4$Y 4! 1UcL܇eC;k ¢u)Y9I =m 4t)M$JhƋfĸsh=IEkg.{>,>CZpxܸ#N7g&e-{fk5 Y2;*Gn""5a0,#C('|Β4TRc.BDlcd /|A( #JJt!!bu=)EV  }ڎ|WҎ-L>z#Oh6 knM-wz;x3/1>{xu>DcgjkgKj{kX-Y 4l[n'CB$͠aiq!zz5*҅V$D L&0TVv2̍p#*#)+ߔUXd2hH%K3Wj;Rt)K")%Lm,y9Rc)Dfn)bχ041=UT t2[ ETUjCHa1kO#: O`"xamEwJyKKRLd( IK%S1RѶ-2]Rɴ`lظ$?}Ra;W^~c99an] z՘ӎm?<"D3nO\c܆;x)sX#ߟ3i=ȾrYʙnN57G}7$"DƈB)RX.<;e{2 Mj4,ubm94gR%BPJS!ƙ o|)Q5mEkc$H9TB(.| a.@K=.řJ<9D($>Rc:",%9sb8hRԕAՆEl3@ZT#IgڮG;#PO cy SѥLyC.J(2 G6LfqFRs5C]!B.nwئ.L E0i{s\srl#m+Wou|蛋\ B|໷G.n/`W> ۮX!c͇]"[~{:ŝSЌ]K'{BOݔ >El"QI0QdڙR c+\Lﺎz*H-m !h]OzAJbJ(.B OBaULԄRW5u] ᰈAΚA"hi2 /GO=.\*i#e RJ锦iȢ4р)y=iT!hҰ"IG$hZT..&gvԢtYmh@t@C #RF bϜs43sJCdOE(Aگ/[?_Bs\-$'^wn7lwq.BIZ?Yƚ/'"w1d)O&(,RIw+r+BK}G|;<7pnŗvlXx>5z?| ߾Q|ʿg^5ooMԿ?YS1;vMkׅ~O}&QÊP6%(-E&ɼLEz7!,-[xaX kM!DT|=҄Jjd,Mj1BJY1 BL!ҵeDgT˄"h!hWL/RSOnŽA f@-MS&[ L1UQB+!R9OL>NQ4Ʀm[T_CH[jP}O;ZtFk*Y\A\G";' 躖oMQŀϑB7Pj@##BT]:Grh Q*Jc5.կ~5_袋x+^w?>Ossg/n38;-\r%wy4M9Ν;yså^^d2~闸 B?Wk?#Qߖ\#Ef6 wB|y&\6x;q҉w/=?uq 3.O'3gr4i!iae_ N;J1_ŵ@]AU (+1Ž(9 }eI4B i27ѶM9H?m)Kf0(&T2nP$H{kny(qV^ IȺ!BTƠ3$Qna+KA1UaFJlm+WptJ֪dGLuHHI=JJe5H6 rH<"` ufG k|PFc4hOD֚id=QҤ$つ"6JꙜPqQ3``xniBXhĤR tq̨iF s!yZk.ҕ xԧЇ>.^x!y^)UW]_z9mqǥ^ʩׅ:9#~(x_mċFc~pyZpucV"yoxSb˭^ċx;߾?z ?;2[ YiGJ.1Zq2{m*mZ@ľ% T -U+ !-K'LUN,Z1QW^It^l*Ԧ:⧢bf֥in ir̥ḰHe(Hڥ1N-eɘ)-̦"Q8*[XànAh5eR:6F,d֊~6}di׍] xO4kLlSSb@D-E3ΔcdOV>1YU% 9@Ip)l#zVE+L=9&,gJ8AK[tZ(Z2tHdBN" @JR*ס<3W$:~'ԧӞ4>ϳ}vN9714k~"߁]pH̛W]F>~M#.%ApZiF!sssSxo[lk9\+,R-_\7n ,VV=ZoߊTԶ"qv;:=.$BbfȰЩb:fBf.6Z9 mRACů}jF(T{s)W>2J d v 2"{I>VFb%ib#>b۶jETs8β S q]OS7HӒIM#(CDg>!RZɂ9R mA2~GO: UԮTUU 2PJ`}(CG(H"#b( "[.jn"gC9D$P!NQhpY#KZGyA]vesr6mSNmN; ͛7q;yF6n#H^jwA_ ߿]q򑖫~kn1P~@scG % NGe+zgpu&Jy?S|8y_sٗ=wؿsNy_ȱSr.Ǹuj>}dw>yy{{߿}ܶu+}_1'޽{?gkUJt}0x%_!Vj(E>~HJIC@yK[%Q5wX]!b,d]Y@!iiV9cF/hZas!ѭOEuEV2s e !Mb`4ҵ!Ce5=˳HUW,U*)a-9Ⱥ%5t ec"zxA5CH}@jzԦ&V` NF:"|#\y啜+w۶mc~~~֯_m۶ 񲗽}s\{]bӦM<1a<jxv8鸻X/cN4<#;zb˛#v$&nV;v'z}9mV|ß۶n%o_rÍ^~{vNyK^oW|s5o/R_n24 xc[/x |x˓is_XnvUûv!7ly/un. 7WkM7~e6H|Ǯ|ysd˜<Oj8y~ D+[Z-Ȼꋭ&ӎYP4̀d4֊1b\Δ|xL4i e0EEev\1\II:Z@'fVYB*etbT'8O Ƞn⃟)\X@i`8Dt4NU:ԠBHx+2ţ^ kv*BiE2쩈x)+bÄL4a:&%VN:bUmd9W=\^_ cGÿ\ <ܾF8GgR}.,N ˾o>X?ͧï>ݷugyݿd;?z?s ?ز>_fɬ{88z~.Wq#'w?SnzO>Ee8eWjG )7+_OsƏo|Y~=g?x-/y Vyʹp<.%'<x?q៽Sq^/g \/3 z&gu<鉜r{oK>i~,7sSO_|;|{~୩ ^rovB]Jӫ AܙAj ?-{ R(0@&|PcV"!+ z,`:4RMD3Ϛ, fC cjYsaAQ`D)%T. ln6/40n'hmQ8^HR1>$B- iPWd0h1YQڀE-EdLCWnKYWRw6E1;$a}``-)FR $.\ܰfki=CJ PJ‰L% jc%ڮ'⫣F%|r,..rgsgǮ::#8شiӁq.a^,8ؼmp:xҙ?y7X6o-b^|0WToKkβ5,Ƴ"H3cu; O}ܻ'oXcX?kM vڊR Mܴy3)%KS`0p T8`A7ܸ߽i_#̝g ]'507/ڃ2KZw}~Wjj1^ V"!LL匥C9VXf1lZ{֊A,s=1erc:]qSf? B*jU: c48KB IDAT|Zt- [!.ޕs(̬Ru=jn둶p9$.")S]NԶiH9S55Sח =;31ȘPVSUU9>0К޻[vuu>&EȅI! QalD힋-3*#3-[mq LP@Bȅ$Tٗ:9uT.\<9^g{~}`Qb`zFb6ֶT aF (0#bBfʘ'Kɸ(2pRCD 3HsO֊ m IA=YӋ2Z5ٌn#%L̇p/x hۖ_|;N^o>w+e']/~;iK}[2^x֓Jr |pZ ){1~yI,#^,x/!n[ɣ:g;'~,Sv ?p 7};Gͷu3H_|7IZMhFD90\f8~{۟-8W\z%Xyýc#GcωhBxVZQIһLB37B)ͨj8C/@f|\tD$U$d΅ʫϥ&S&9_a   Ap`b&bi $వ/0)U[UWt"R͹00x #KZyO[739Q j1W".'rUth1#J .-]sR& 1XKe,\ڊ9>!*1Iޡa:!eij+7fTlc5y _=I)1iRW׽u8y̻Zw00 fV~ի^ _B.vKG||EK,1}BJ1!mmrcH|0NDD"g54̨"zT Ԑ*U}9J$Y2uD{ZRŴ,2S`T5ĔhG5n^f>&P i5YJ5(m࢒J! C7g,,C4~CLt8Ȍv/3 ƚ&ւBsY*7%*k1b~@)ɐ~sK m4*kC)iw`@JOf.G=6B O:lĉöMi'H9 H(PYP7CrI>:6\r#DC㪫3_{\|Ŝvi\s5K.wwyӟ{#4nT$wy'm۲g^r7w^g򶷽mSdڴ~epeޝeGO0 qK|snkar=cɞ1\A[ yΫ~Y|s$"paۆJ&I9ڦa6 #HcBb6ȵAI*JO P輣T*"2FuڠOla6J Қ)S| iBD}O۶8YO%/p_|1W_}Q_;S7MMo톒i*c‰`,URXTO'FJk,nҢ CH V[D#'ʓwK ?ksڈWI'-b'=G ,x[~^fXEH_|.:$SF-:vϬ.s5xp )`g#Q D{#oZ 9$;VW|ur4XFB٨N{]l6SJ1IHbX4;B 2SWwXm \ 9e܂S"UJ2QW m5OfZHЄ</16b![C ef]O-U_00PيO""H7V,Y60TJ2w;,& X.Vs 9A!0P+[t ̉ }եmS*)j<*pʀ]iIZr' J%!af*=UeV)$hmM\J,9x4x'= uO-c{j+#gßۻ~GTǃ.X%N=LdX3C DhX˵`*quhaG>_*yC*)&hȉ${e%2Iv0dV)w)!D.d0"!@g ^I 52""gتЫsFW!PҐ4ԍZ[XAi ,QZdGMg90_O.2;XGbZ`MyB@.w-?C|H^.z[)LS7rrD1/*J ٣Uy1Tte[%Rz穫#~GHwE*?D %ňȚJ7H FSa" Dq#b Ȗ!'*%(4hE2) iߦ 5aQx_H =9:ҐiVT:2O.߶d>x%s$F%Է`V ܟhr}lM*iU *Jr*C!bJ$(Kb5I!waj+!V kd h ]BB@ظneVǨ+/K#LNHO3X*BC\GSR+5zh8ƐS!*99tmɁL̳^]b @ROBG Eg02(!+IH*JS*W:,!'LEYjU9a`=Lsi$Y dc9ѕE+R:\7ϼaY?=Wo^#]17P1Fcm`q G"ZE] V9)8f &D d "IOR]L=JHU<"y9q#v#RXjsQa IF J ))e3"M?=CUbR RUecIZH M6EK(UCX̺9Ҷ CD$*SY[7Vr y CșDksXsfmN7$sb,buŧH)cF|6RO.9,~v{">ÞS<Oob!dԌ5Yuk[Ǜ?1_ٵ %Vckx#dB.b&b@9*FRY1d)99LI)U,JB$_ԑFec2E K)rs K[6REXSBFC? ڒBֆ>Ri$qFjLI-ʧ0B 5TnN|mZ*D .!ۆn@LIZ [0*rm99G{l]ؐP.x?8-=i.\mE1"&C6tAkȥ S$vϜJinn #[2A F#ЏJw$}(;W$PiHL3bzp !4~drB H"0j[.lyP)wШ"@`KӴYe&IJ rJ01q}yZڤLS5s(ȗB2 =XT/uvVvrَtvB(Fܛ/BHFue!>F[dޥRhއcz5#R*6c4{bLĜdB(x.FT#I7KѾ.I`GuF$-kt6\c X@EUv۴P.dn U$RϦiXfʰ="`R"eJ$8 *b n 91/ѯOg$@R)4UE[@۶2} h-,膞Q!GT#b 29$($BA4~h`.۱[XGRZG>KE >sNx1 D PրXc1X=Ąd|8c̦ZI ! Rn!A9rθkPEKi"cXTF>; nRŖSs NTK;Mh-FkzaѺSizz)"*lWl'ÜNzEB2*A[7_Bيvy(\c;ѡ*#e\(cPAH )ERE+@ Rd}󞉈8#ǀE|"3G,A7m-Ye IONo剏;%  Jb*xL0C GNEl.fn5IM.9?KXN5 b}1aBYΙLBZjy15 9lR-5"GFaM"'Q"z;r쉃ÅxMKE5QcmBY5c sBOH^L$KβZQd"3+2 3I \G{:b(ĜQRN. ( IDATw?+~?w~eWo < @L<[[F1FzW?cN -ZjK%g$1Ma@JhQPC]E7M\ 4dPtf׊=;Xi+{eBCНGĐ"Sr]wi~mֻa=)HBaY,19Ùmcd|*O;x2Qdr J%L|hF{dp%!%!xv6;}@BXId\`F9Ƞ3? |Bvc;_yn+V*ShlMkv)黾Ȅ*P#$2fU6G:UE7#SB9jYZZf*Mk]@L Nq.?/tm\GǠI  jS(12 #n?㉗cͷ5LN>!c&sϘ']r!Vn!ZZ{:Цi#t#M1zyӪTFF$JN\v\ts 3zn qG՜̤2αO{,fxQEL+f}xylFJ끿1gy>n^dطdx?,-~žPr/Yǘ],(66y;hn f-cBts-[/jI{XL BGd̒ 6~:Bubt -%9Tͨ,\TE, 9FmQ*<{.ܳwZl!dǕO<_}5;*#i1n6$r+z 8Z ]ZKRcaKNVk<OB&U ]TԊ^z6O\>7ᅫ 4gЮsY<ӊhm,[ɹ>?\ۨa/8ʯ!L15""c$dŕq>՟dՌЩcE \GP,Tdrlm7sEL{Rՠt]OT*JTe;{~m;'c~M}tȫx3 7~&84~_矷`rv"ؔѿED&:KO]2nFeNe#I%C 4#'87hY]etNHe . VE3rCסnj ܻwZX[VDD5>WPw@LE!cdKsW~q@S!DkqIbY,$D-pa1 >Vw'+DNLY[#}K?1_5_E'^c= _'ڟ)~x; * qeRYhnm==JU#%!`c\# xkgYn٪*R*F2t=mӐb*g*v;":*-!UvpW\雹;ﺍ}y<{m'Y]]/?_wA}y]GAd9xdƒ|?=Eq) wVX$2<;5K+B`oSg%FIgHdTK(i[5pez^#rۭ \(ϱBncXjNca 0#^'l+_sc&kȕU he)bHhEzSTZ*تb9DȘJa!˯s| g;4Jz?ZESAKr4b!$si _܏Q\qQU)[Cb2ϼß1m"l%'tJQp%"Jî %2*:.94Z#ČAYxnc{6ڏ1vgg8u+%)+si|タc&ed7 qg~| F92yf,#1D꥝+{I2 aȈ"+d+ J%h)$3]lqbܶͷՕ!y ^ʗkz"ɤ0iߗq4&ڑheFh轧j<`bB lS1{F#r>gTBfA?  ^ \#5rAnIR.I|쟹1g[:@"Y(%I%HH23:dkW n`BgQ\rd5#_}de4*jZmٵK;t)+E$ȩG3s+rCv13#cQ&3xWa8Vcx!r8i^.\)q`ee/~a ox) /O<)ON,Ǫb6 6GLec DoKIMRÕu]ozDžKbe{X/~p4UEN $1DH(U`//i AJ .{37ų)Vg4Qjc*!I(||Ց @ F =ÀYiqG")a;i+1B1@ݞo_3?)bD&'?Or@u+t|\598suSWVc&RNE !٭4 YF G !M⤐.|cPY""Dӌpn |:9޹ʮݧd=rj& YHt' 1 Fv;l_CTuś/|];w|fry 7Lq?kWq< y/y|^_bcm A٢:B9:9ڶuv"X60>!Ԇ5@hZ-\74t. s]b h)Q+-9'S&.Ҋy`řU>z~>;/;s1bh7&w |FTP)$@tZ,{Z]Р>F[70ĄJQZ`Y؞ż%/>k_vĝ%/^8O}ʓyS|sOxʗ{|ȷX*[iV|m*3.xL?ved6>l.ּziЊny1n,@/2"ciI;t]ѻY>X:e7uݐRdm}֙:, EJc@-3b6v5ƄՆJSa)SY̩k\`VrT4rXwb,dX^^"ňAHǯOE0A2TwLE%_Ғޓ%#ULY$t]*\ϸ#]UEK$\ryW/q0})3MFLuAH𴣆48bt>Ф؎olH)6㍉#fa6};{:Y1t3< _Z=D_I+)nl4C?d9Fz]Vg݃l˯>;hA6$l!*v Py v]ʔĦ HC ĀKփAII#F<43v9gs;{gF`L{_o+?V7R)s&: 4xD4݊wR5Ehua Sw7Շ9Z%c1 [#(`tf;V,7]J:"M#vbW cܲp\H}&:< rdC$x&;si ˎmT3iiV0ua)3Jj[? ÆH{ %YnhDJWyWZ*n wrk=+ ~56 yǟd:J,뉙Q *n/[_Š"…').K+6Ǥ[4)& a`zӠ>Y4YP@,%B {wF%c%DmaZ"NA7x7r#j@/l֫;?N̄&rHUi(1g O.PjO \2+'JYv5>v& "J- KM=J3! , FƑ9G.Ln&s1glH֐R?3GTa zQ3+4~_cÌo5-|sO;@t-k~W^. :G4rpxvctWaV6oʟMn+~}pe|Og҉ z۸|WZS8瑴;5r -.ra๟>S!bhڲ[(G-MH.1+8(yFv|KI!'8':u='SyHN:qd27pO 7W? tiXh}}*/G^sm2xX S'J4s3ՑW|*_ki">]w[Au3m;q[]BsE{~oxx.>p#JPzZ3bqJΐppCJEƀjA;ۡK84+ȩw`?DŅT UV^1 D} rTx?o$:(j1[NIsb!-Ҩ3p9C< c ֹ?$>Fq>7[4R,0Tj8ÏP WRZV 0 `&@Pk'& aCϕ\Zn6D5Jie;?b:i 5b :q[>&(K#ce/~!{pte5+njM4ګ5"NDʹ cĉ+em (400r Mxe IDATo?&)<#.4ϡqsn%qmnJN4]ZwR1aiS T97 ;xcr}YzuXNdk\yZdX()iC4_a kuҀ*\ 6t`u1ޣ û?tj>i#IQ$S1*@ X8:~A q(Kk>rt ZȎK_IWߎneU*da$9K4pR7Ւ1~N$W+!.*Z .QAXM,#R`e;E3m3ivHk.Tr{q%L,Մ]H,ڥkl68'-f4%?REq :qȿ(`RQMrst(i(R̙iȝc.W?r]iY<:8Ĩ&n{``drN0WR}Sg>aWNR4-jl63%~}Ml*R2%{~&+yqttt 1$AMd]5۸FOaW^JNNNH9͑, aX²,4i,B+ yX4~[)J%5%gߗn(|v2mQ@ 64\(Ґ\U$P #`J;ϓyRi?Կ}u jÑ]||ݯkK pZMĀ Uw3 -4 g,:Zxz8"N0nKvOVjeΉP"JlhZ]λhpRUAfM`t$ %~Ps&+a`GϥT%L+R+Qq`.!,)($Ȟ_; $|]PZ#k}hmqeͿWwKm 3-T`宻/Sd=nLU.0F1o7V"bf'mN(kdԜIyjj ڏ$,:p4Wh!0@찫X2iF''ssx#፼/O/L7Ɠs02S^Nx%RklG+fk4Fz@[q'\a5 ?,(Uk߷5^#wp @,`, uxbj=aJ f>Dz+ &Ę jCp;e VRL4\<]ۍ+3yys5G3 v.C`*;P cmVZZْ2d_K^"rμO.Ot$_$OdB:: \8<ef3kE@)FgYJj8Yf|(g% ڨ.!kfQTY*u/˿= Ķl/ܗG PՊ⟁nU+e|'j*5(VqC3ʎ;PƼɼW#(RsRDwraeƇns]IX"ݿFzg,L*B΢DQJD;ޅHY+tm`a5`4a6OL2 051#L1%%%@D \Qkկ}_E_7,c^[ox#~W:֚|7~3/W}=gۿ_o~{|WC?/E/}'IYXkG9~j̣Zێ._(EtNL#<1Ft5P0owHi]iWEZkjI(J9w{eH32"׊t;VV%kdȑUIE-@mq&’Y[?"Ⱦ J A2fvb1\ k nvXQxmjZAmhl-WN.Sh A P VjUJjŽna. -2H`Kv;pdCt7*3rٝW.Ox[߆@_  zJTv3zp@8Y%ZJ Պs-l-8?) B'gjpUڢ'O)є"M}4UWj<ԆAA,1XKKmx5LKԆqCyUdyY R]z ʌ l37ؑ J.#-k1k[TLrsX؞lpmh8xnY3Y56gYb4Ԋ, > l{(.M+X2*Ag!B6aDJÖJ7Rs|s|Ƨ>gۧi3g?Mםu\(|-cZzRK7\"/X1hZE URJ~h2 zFgH9rHkE &:E %QOMeo3 - ND)z;^n`h'撉_y,U;hTIU K1$LkRĒ9a|'NiDLg+(4(,`ݫ8cξ=jGٝpF(KBmOێZkɎ8nႷ 1ݗGb- c'֤4, Msؓ2nnw''7/~ ^9bgOW/ڇ@9V=ʕ+\OS&q-(d9jC,3Y>ΰ/ 9#IʾuJxT?u{:H7΢>tZ)Ы=" kΊ4A爷QF.d&mBMhDž Hۙ2C%V03bp#APˎPZka@rA h곇R *+Z* GkC҅eJEpT2vO: ۔iXNqvn?0/KG2njXrf+n=8TOΖد/_䍯}C~Yɫ_ڏ}jg>yX~ロO:n&_/vmk-Z궚R]%g{KڷJWE+5<喛c$+ a 0p*s>si,tzۓF-P<&=KW0e-ՙ[sZi!}5]Ť3jIxzkRѵ b=ĠҙIߕDZ9ԒT+E=..cbTi99JRaEV+v Ve]m4 VT_]Y LΪFNH/չ-ER+~lZ"DŽ- 4shcQS1D<xʲ,\|{?~gMYo~ۿ=zzOCT]ZK兀%L2 xf'ns|p\J!. VcYK_=)K%Ri XlF͍ŢeCf’) pN~ߔB焧I*46PMPޡh=K}ܒ sr+6x魾& \ 1gZQRB~8;4 2Kz?xg. .]Gb?Z[ɧ!8mX{†Ӫ[!kk1JVә(e#SFi1g{Y|ҥau M %E$g*rH)hE[6frNXTm4$!V@A:tXP{h^exMR_XS45*ް+4]9}.3Z1s۫%-`E{nU``K^k8YfVs=CTW``![LU3n4z}D8;<:$ $)a֨%晢)'ZLZ2uVKbJ Kn*rβLi\ .,B+VJm5ziaƢ1vQ{ǥd_1`-ZNkLWZ8;$RIG99FtiOu) xehBS! chC晲$vT6jP{((XgTpUQ01E7dQ(V1D)4G5jTݵN._C˅8bTXiJ0i!6)QJ>\<谶K՜gW@VJuOu9C`킷vi ԫX"uւ{U]fҍ܍Rz0mc 9XRF[Aj h* hyfzpUPuvB`Y,VƐKF>mZd "d{ӊsKƤ+‚yrVkNQRG]VyqՆPDcuݳeXxep tU,RJWPpcuuL7[eNѐDQցQ 6Vl*aC&Clf4"ݑtyW.q B+}J|\)I\Kz?ZAi^Jcu|pGYͱR~p^+,9\rdOJ3MCRg, )%ێ3Jchm(yRQ%r>׆bJ()%syб*Igla#$~p q™}[zb#MC3HZ8Њitgvŧɕ ¬XﻔR)}fjGn-Ce9n`4!R멣؀I%Wb,bm_{I=IkRBm=R]+ʼے}`Gvjݳ&ngJYzԊZOZm#4e(BAJa0 UiZ+.^olkfuKD9C* UsZT-13x.vMc:Q_< \_8y)uqɹZ-W'{ՄIk'WWAJ%eR11sQy\,}esꊿ13ɂ0K!;MAKb{D7[jUL g=90hRXlO94MX@7b0"V;@Y -kb|v r Q %Y:C3QZ%hCʹZAsx:fo~V唳bѸp53j̵p̤V9,W= iE29\ IDATXSP.Lh(ql7;$5$VUQZgt7>*BV%Uj;SfXikυ+<q-}?j#ɪ3D#hXd,?ݦ(X ZitֽM6M8ﻼvKI 5tfp;mA*pƟ2ZSKAFZ 7H >uf퉃udKaEÕAȾ 癜3VϊwT vV(zY d'14Y:dώz`q 0OZD\8>>=驷pi^3;X&iA "ZT iMA'N v譹}-][c1B faqO|[,)s&G|>nϵְWU2a$R+ě޶rP : &E,3D8Tn:NՒOwB']ZE2Rh7R m46W֔yKPs@}/"8y!tဋRēZk.gjkNNNSdGE ئ0 {ƽ,Z'c]l:1?S ٠60^dN )UsX(0[]^arv|c 1FVes}n&70b62 8R,K$K*Ņją\vQT_rk]$V)rάUK3VOVH\#6x1aQZ{*e!ӨNIB._5@U=ZC /91yǟؼNVEL>ޖ<`FJ*(4y ~Ⱖ.]^?KgyatS7vl-B֝gbiHTbJ QR RN(E[(m^;#2=Fr{x;a {`(hT@{[AOE{ھ\O{}u8VAVtЫ:8"!@H! !Z~{i}+{yڿ;|>݌٦pPJI FT8tJR+d#fΕ$ŚLy೒TAo )i44{t&',hC( O5yɴdsJ2)*QJH*54YRIl)!ˠ2*H-sK&vPiy &5 (+dSt4Q'#A!DfHk*$FLSN5q3e9qccA/JgbF6zTLU3G,= VI֔ &'ADkd) LU8H!g֠F` qtA958|UP%ݢBcZbEg0 ]Y+ۄp,JݦPt-*(prGCBKdD Kԙ6 }(SID0a(/CYFJgDq$&PmPg.{(cpAؠ$M]bT2𝪢$bʪC$0cpy^M@Jscy_*LqaI*95Y3၁gv$%rڹah]En*rW%Drl0r^b/P&xGR@'CeU._p u, sܔXd]Y+ &Ո4EU6̍ 62ܵ8iC"[KVh$uai|{SzG.sDM4zOF=%\/oMi\d.ݓh^8JcE^#hLX̫(gehrE`o,<%DOB fZqݛ3AxZ9*/ETe5)DQ%݀\l {Z-2c$k4- %;%Հ}ɞ2Ŏ{ιA|FJ4XRtůYT*4m/C3(Q 6TR5C HԘ< T'pWZ\QktG2![2S<"@k\( clQh4.d.[.sMEG{z2'N*c>hTDRՋu16!(( BK4N,yN'HjOc6MS8Ga yY"I71[(LcÄ:0kS?Q!6(!{ +KVapBXatYjSqU܊XDIAFw3k?\G \3lBNDZm%"rA)4U^)B*(/2&O^ 6('77k FAAYzt#[AEn\5HA&h(Q֣B1Tx)BT ZmyȫDDyŒFD8>,i"V^QӘ(`u(JSĞ@jIו4BF):AGKJcE,[_z|i1E0+:T&R!@$+{֣D'i=IPU(-I@z$m"$3 ÄN5{13n, ;|b|S~ bדO3oޛ&P_hkbh6ة1*KhDI]wL8JUHY5q*YKEQ42%$1'ܐZH } @ ҡ AE:ŕ^UR8K^=X*'qLZ-AAYXkBo;+?"9D`*j>0UQg%T4u >+EE钢H On8SXRR4OCPX*bDd.ɕ} (a$+#t` |$I *pRl˔P&nM}蕍̟)Ic'oǞ,;o:Եgd}pl4PnJjXOQU(pSDBX ,}B"TuNEYV* DQ2W!BES d#jS*HmE%4sMYUyA N TcXȝ( 舕£Pjk쏛㤤nkqƅ]z[ QKz9R((mN1Ǭ(H+ʂK_iLW"d,w%*sFhܗ{O7NE BaMF n {5D:&uI1YY1u:AZ,BM1$li"0:NWx'pJTz&LSqu=(q<$Q8〨 >U?CD}+MU=~S]7\p""i& !uwvd{LWtQ@&RQs A43n0DYp(_TE`b}@އMrJ`'1 tFE_x *oQ:BCSyO@0^[$.TT/2<WY(y%pp]J"P;O!SERL~*4>mhX mƒdu ]0 T^=X0P0H a+\,)E+IfP mL^Ҕ"vޣj S/(cLh AXت[m^<3xG137/rآB1SXG$"QtPڊW4[ O$~)($؊[4L `ZUI) K&("pyc aÔi^!<4(h1I,|, Wh_ X&'ڈoRҥK޳mgltQ0u 0 UʂJ[b 3*Rxcdhw,LpQvr">4wƲlM aMa M,- n^E1e/ǤEhcnY!bM4%ͤHUNgaѕq T"C8h"VDXO"Z*8GJS@U߯c )^.5͌ R$Q\Omy"#ZE*L5 fnû8t"5u%d%Vvf,$JB!A+E5- au .C{ېe$IIoB5{gp[oXk1bBed0 , 2PaYx|$Pb{HFH$_U i\iQZ8}:p-XΑHƗ 38fG:GH3$zK+ƆATh@M ,l>1.j“߈k\QZ^)Mt]8&MEAKt q6-3zLY/ij1َ9Ds&[LWDx"AF"N K!i)\Yb*]&ҩ_anfDWhᬥ,+%KRYpKRChvHtD}9lNO۠`#MNJ$uv%⨃'0IL-R ]2]2qtt:-[%Sțɔ4SqkK9#R@QkVx R_@:XdpP !$R"CRbz,}@ƫX塐p^`A&Vhx@8B ㄒo<!%xX0J\[,W}ɘki CR9A0u8+J`mEk VRHPE^kQRD!FRE˱J!M C8\~_cxŞ?ãÆS Kvsn<8/zgMBr%Jx"𸾊 vpZpA WZP"<ZP sHԋiY+ FfV`)Cv" ">=zn{.=f[<)V..Ս>n sF^gBc$O?C՞[3a?R8,{:|[el4Y(闿Xl!_ȣf!^>%mftVOGq\L4t6-zr~4zM{xexԿ}-@{;|FwߣlBSo$4l/>Z.\Z&k޷? }g4k;qc?ZwϖZ=<6=zla/fjxl{10hlt<(,)N gv`;zђBtC{? {F}_f M FQ4qcZ_|QvQ~3d{og\I)jpB[B ÀȢBL>~1B^,.4y]s9$  5G,fOΞ{>غڼjž%yJ dƘy }0SyΔ {cSU`9)}2Q~zu{kY쳳P ph>exw..q= c~F?R| /vnt`glxd1;ѩB7>E]?XV߷UVf֮];X/E^pOnYzeUV4xc[cݺuH)fǓ;敘2'ܟZP=C86m<^y` ;vاi X,vcq9q,,&4GwûO蚥o 6;[/5D!. 1777:|8HBڝϩ믻BenZvI,LSgo,{v=$_v_)0X?wT{WsrXW{ hh{ǥ]J𳳎p#;v<\e:n6|b_W{拟CǼCA ˣ>1ÝwT{[S_{r)mocnn}s=ǷrsQG 2kя~[VN;q2q<_[0do/}yMļ-o0۶mcǎ =wq|Ν;ٹs's^v)o|wK#seٲxG9Ùf3oy ]z7ֵb!?mƣ>$OC?%\‘Go>6nE2=7nD)EQ\v٥88暁 ^~o//0⋹ 95kկ}K/zvyEz׻ꫯ=wy'/?|gwKu9쳹ٹs'Zk^Ws'smⲿ^z^~o33,_N8M6>~x6mu]*>sN|e\{r!uQu;/{?\1.Ǧ4ў|˼ B M6o}s=5k֐๧Tٰ> 6o5ky{_ٰa^N8V)~pgy&d͜x҉W^G#lܸo~}?oE!G#I'?LYi-_yfs\yYuャX|泬]78AUmo{Ub˖-8ƱCII'O[YLz({Sn[qC(Ji5pgrJaҥzw9C/XoG颤"2;Qc=z[pSO_Rb%鳧0 [c9a\p|3#x`kKҗˑGE^s9<1."6oL,{D+V8B𖷼.e,[^ZVZũ}MP*Yf ]v<'pqyh4hNɫG…_Ī{WqǯnC&eEY|+|y#^b>|el߶^x7E_y]{ݍO|hO`eƍXt)ӻyr,;`˖- jRI66l`ff;.ljzƒ_1~;?wŊ+8#nwʕ+sNn68N8z!/Xv wq;;vOgrr;wq뭷r1/caCif>(&&'Xt)?9cIℛ~xVSO=sȡ27񑍬o/?vaR;w;N:,;0 疟/9C9dyf~׼Kecpr?>́z{;7Lo1Q˅bOlሢhAǷ<'?)>=EyTd_HDsO?-7dXbe!aba e2qA$yu膯q\8wY,h6OY "[[@+rցc3m/`.-h 05YH5Rzjӳŏ7|u„Pҏr`)#ξ\Bk`|l1n^&]<[\f\h':z`7@XaV Xlݺ-m qcukֲ}y'BB^(g-޹]]/^7? :\7>{ h֥?km:+-Ң*7vp6 <' 4 @_ "!Q=Hi pPF҆sxm8#yY /?;8Ʊ3ͱۉ 3O1ìa=>PݾZXpya} pIh[Co37G/$ކ;8Ws%G:#r0Z+B6Tsss\r%R@gComb+V}wH?o#пX%W^y%[Zϐ8[~HB+{+_J{=m / \8;,dLX iF gZ78?_}?KXK_R׾l۶뮻7|H)7x#?gy&Ip%v93Yf ˗/7𲗽Ƿ>׿uWNG?[5!{XzO???bc[+y_ǯ8֭֟B3m?>ZO}o]ٰa]toy[o4M`zz?;o~?|t:\vɥyR<Ї>5k/w~~ZV߷O\yv+'t"(b箝s9<\tEW| q=\wM{wO~<\vel޼K.-msϥ*ο|5|_ k׮cW|(?C~s.??}5?ǼNxk׬e.w`my]'xGY>=OYlqwnww4MY25y  γ'z=w~'s~8{zl~Q>%t~dy,Y9 IDAT+_o(SrNzH($Z( 7z?wDJUJ`"]bVW[HHoL̙$Dux3p-JNH~grssTR6AK.?w},ˌ=onrɓ'OhgO#8\ 5@&Mܗ~_n}_`#Y=z5BS0ٕ$]Q_k-fވnޝg@& mеkWzE||<#ۏ(}pKzz:⣏>bCɓ'Yj%w͚5Vi}~bbbHLL4LZ4JKKiѢ1'2339r?#7p׵2ڵksz+Vп:uDӰaCn0C?l~t_n)jĕ}0I/K\*f&WZ볟.9VmF$^effZ9(s7އ8u@M}0\7Jq|̹a 56PWgx9Z,Ic>Q }]"5(!5+L"!:5G{e֭+6v{]v1B`KIJɲL0ޤnCPNQ6n؈(L:4MCQdY6^sle1kѵn=~Lqq1~aHF?s=-EеA<Ϳ?ݻuP3gC~XZZ%lav¯{^5@0|~ u8[Kߢo>VYgƍ\,@u~?}G!(e|~GΟ?,l߾4M۷NTU'Or{XpMӨ(8ȲLΜ9$IQVVi9ٳg\$QZZӧ-)Q)+-H~w̠A]L98L6$sT :CdfT)9>O^^Q%``Raj$IV 5KҳgZa |Y@ ba 57W#q%+u-}].sF>D9c&ƍcWHWQ㏹[3,^xYv-]tÏ>bٲ1m:%r's=4nܘcһwoo΢E8yUU^~eڦey&zV[իVO˖-1bNȒ%Kغy ral;no/5jO~7Xqxa֮[ZW^e˖$$$O0t <Ν>þ={Xr%O?4-b^/?m۶O㉌cڴٳ>CÆ yټy K/!""I&j0!HL}U p̘>eb=&=g":D^5C1w\DIdat֕OE4|Uz}^;9>`-ZY4,# "?TUV2{lvҥK576G;lar/JrҦMÃC~/~ܹMQehN?Yqc2fh; MU:e 3Og_־4iܘݻ2(nZ.X3Xj. MVVǎGUUϟOaa!k׮eż̝3P9}s [YBcWeWj0KV[ԇE1Tj,ˤz۷/IIIP4""<[HJzetZ`C<|'T]4ț+-+v#9$""" UVVgn84mUUѵ[W RoO?aӦP(c\Q"X$GEV Vц9TUXk鍴9s~u]*iپ};? ]IMMt0:: eT>}'zcׯ_]׉#SZZjBgST\LNN zk?o2rH~a`=[ ෪pg cV`ܹk#2d=ZLڷIMMOWF@ @QT*6.AySOڶmÇ n߾m۶18gϞfKԔOyY,СC&. :7nܘnݺqsA/ .bРA,Xz9Ñ#G:uM4!3ƍ Er)bbbB vWUUn7 PQQAQQUf0+"<]nRSS={&L~R}+!xHII7Ƃ78p 4rCÁFdXVZ1o:v숪i:bbcҵ+syٲu+SMOx^; >dknO<* SNrqϴiڴ)qqh3?/HJj ڽ_x˗5jlVYÀ#2*F6c:2q$DYyTt`Ȑ8Ybb9r$:QQGczi\.$]wœcBD^|EdY@VUQkn|w+?c߈yrՖSE-=gb(!,Ǣi & IyEQp:սmk!ʁ})K{ U]%bF~pjju36CL̹3%" %la'0sSZj5vi۶9`grpZqo>Ւ(&P[jir`hMaWh1?cC 9VI,|I4arckg^; [ *İ"p*u^ n밅ښBoh&$:Io1|w(.*ۗmY`ně6nٳu܂,_iE! ( Of-5rf`5U>FUyٮpY8$?cop!7lm^~!\-ԒY\kmR޺DfץVlx2uQzO]kBA^>0X>ҒM APAeχ3*4j)RQW*q 6EEAt |h * @UV~1"??ߒ*u h%qc(} aYF f#T #.6G`߬eYvub{Y1!+֮U樊?>}ODG[@@ 8T ]VP㞈h(%IuMCcC-l[uGySut>(iiiܹWL؉㔔rAK.dG6m*ZFV3w\p8̟?G?ٶm/q뭷2i$t$7b Q޻c6TUcYDGG[Ɂ*|_2#Ӛ{0hH;~lAw-~ k|F߾}Ca"UVx).)r1o_mbo>'1 4@t/ÇyXf 'NC 6.];w˨Qdݻ[6ӭ[7+( wwo`Ȑ!ؾe˖“O=Çٺu+?3:eGqq'Nlݺx`zINN<8~N?-[hּ 0:e3ŋͽ@LL 'N/˼>kEE :N;2?{999ߟ?^L]/8ul9(?GqLLLN &&qƲpB**۷ .d…L:*PYYYjwqK,SYQItt4׬ᕗ_aX@ @E_glڸ$ 8FEEh"&N7`ɛKx<"N$3˗-AÆCrr#$QD mv>M8~8M6EQ^Q2[ܨ|C 1Էtr9#I͛7'??CwȠ*7oNTT 99YQN8AnnծܹsZ]*[jb39|0m۶%""vڱ}vZhADDDDbb"QQQ(ܹsg\.M4… 2g\TU2G4>?EE2{l EAׯ&^fʔ)~uN?اr9r!iM?a ۵!IGN$``мUKƏ 8)+- EUҭ+tEbb" .RUUC@ k7nfV^IFF_A^~K|M;{*$Q^VΗ_|Ñ#GXr%z(>tQ?! y!WѪ2/>}++` E(.-vӨqcrssIKK4k B*++QTF!QÆq' 4*ü^/>7_(..F5BhYZlR7Ux##iʔiS o{^ddYfǎ !h*Á$$B7o qfFI]7DDDPQYW^&??8tA@ SU0=p8ۧ\i,\ -@#.6g\GϞ=nʹ_|,x<؊ DAuѝ3f9+ZЩ\pKgf̘իYx bС6KXbbbX2vڅ+6`֜TVVѣԔTf2l4IDATLAiY)m۶%5%A@T}u]̙=p:ۿ?>,=z`C*8 3jWG'1{l\n5~ a”uy)uI3}頨j@EbA, OSy Ţ( TVVr1q9]sFɍ#))ܹu\.̭qL `Wq0cM^gET5|>L8F b5엽{z4ܠA' ƔD6im v.]NQ/~4N.5~͸\"ec]R$V{Z ?z6mڐU~o{fo"V󼗖 vo,OyIENDB`paperwork-2.1.1/doc/flatpak_saned_2.png000066400000000000000000001441211417573700700200330ustar00rootroot00000000000000PNG  IHDR3G~dzTXtRaw profile type exifxڭi# >7qR-]Ӟ֔b$sϿreh)YΗgz/]Jg3( qC=—y}3=Rw}=c1\qN!yޛ͒Oދ Փvǟ}_9O/ (|\ˠyGFsV;g߻M\x}c麭R^-N"5] 9 aw|8cTIB '֤i9b$jpͫ|34f^1`,\qr?]'s,uqpBGawFpؼ\nnlq>r+]qNK#E-,&$"KHJ5~lħrD !6JE{jFe@H*M#B`,O͍$;)RJ/"Z zM5WڪRMZikNΤӝ;#zq2hCGSfuҢWYխt6]vm\;#zQ{5j[~D"q#j\e"Ōxb[9Z,f^#E!`#y('cI*rB=5n?DmQn*4t#s6 CMq͐fba )[3*}j~Z%wugt$:R.Ee֒sڲ&p$Uuٹ6 Տ˪.vp\A{A߲FʧmkPۭ(_ \Z77cDA -(?ak:i-&wR˗IB xp`L=wQJ>8iBZcb-Jže  {Vt5)s%ڳXn} RN,}&_)P$)@> @ED1g{q*?IgV*gI G(v^i> ;jdO9<[t ;DpR\=t#5i3QU<Yqd_M"S@X_%ϲt6MXw`D̓-^~uD0U|JB9wErN!W ILc\64?!KD"xqmb A.N+ {2)՟mqm}} 0{W{YwS>XTT|bk{) lZTG,#fUF7#7nD4Tǹڣ`JrreuVC؏jv*cRB2UQ R;\x*_rf%N-a[tji5P7A: +[A\QL[sG,r`WԘUtJܝYm+ؾdCEm_1u[o[^=)C:E8I&) ݆߰ ȈF>lIJX󷿻𧿻_M}AAGJAМeDߦ4d6eQ5*du1f7E5WYu7-,u5s]d$zЛD( \FPL%zLDao0IBAYl;VI;ZHۆ $mkޑpyݙ*D;Et]d6Xj, h4LM։Z,hPX"E*ghjk2CO8"tɠoWJ" ň;i2AbL(lڌϨŒԋFڣV57R%/B"AQւ>^3v+P sDh\)v24M?QM'|Nhh2=hrwA(uW@D (ZkoHS]ZMGU;-]&%9܁e,Tc>x>D`DzL[oc,~.mGL  $;*@) E*: 7ucFCMV4}L"b&{Pfz{dr`vݒlRZ&GQZۀZu c1cp,!j*i@++kPTº*UGn>"+~έӆiaI5H%}R]pӨL!˿x/K'&qBq~ LˠŞYkfg(.B3a^e_Ǵ5ݚ5f9!N"iCTBMb4VA P6b[K4r=jϚw #I=)b'3E0̂u@T"a-sQRހAtMvYk#?ѼXjuSL[C\v(m| 6?fAm*&k TVMs G h;e¬b`7ƤԄQ@UM!)Ry6U^홧BtGay> !ѢCR:F҂d@IE?X* B4}'T}0i(ڣői0eg;C#pAiI" ~;S!zʴB: m}#m5g7 FR~8X2X9g4(bbKGD pHYs.#.#x?vtIME:4xQ IDATxwe̜s$@HHHobE׎Du,ꪻյ{Y% "%P{93SsCUy$93o_6x=%{ku8ϕ)eǠx_p>?9WKIA=R;ȿ#{@L~>{ރǪ.[-Uy_{ yޗ^ oz[OE7Ly7J_{(~:qH~9?<>/ _R=pQpޣ{W]WVozZߣ:}O*?fq<ʾ=a ;,tVUU# }cxnk.58<5ST㣼Nk]z{gsToIQ3Y]s/}mjN8|LEKH)y9xQJbEJ!e9l~~O͹\J99W>VI<#9~}ogmPoQ_j|v(JUs7ϊ>"E5g[9痟)U) ~)ES}9k/9_ha!y&?Ou\}~Q t( ﱮ.fwCkax_cLՉU?[Ywm񛣔r>f|fVD -@o(Zиݳ:M%G5ւ,ZW.;zIEԔ-ɤ6lr#g=O RR޹\@潓2@mޏQOZsnV[VBD,7Bywk<}CW)ojJI~VHL\mP31 N u\>j^u% &'f R|42! B>y'9yż_(, la8B8UJy4z_.?kGV[c>럩Wmx} j͟/Ŭ%\Yhչ]{6'srV'slnFmwryZw~4lúP? ]X{`,EZ(|IYȵwԺy#-<]#jQd1ꖴ˵sI]ԴCZAeQeaֽ eU wshlͼϳqT&uߩYFUSM3J/ zs>PZBVDc{ֲ!V{mEVt}|υ?"/SHAל`cY TXG}Kn[xvcܶӶwB抷mibUk˾US3lZ!G)>\?mE`A_x,^ym X[h]}iY~_GɝYZ1.Ay YV?BTq$񣔪ں.sz{8g WM@2Br9=+Y$6v'Z9Rg~YI~9RIBVxZuʖ˚ hz.zH +5&<aKr/j9,ޛZ8A>7Zn[s}rx1x?THvEq>Ƙj–ge9;d̋w;drG%?RH[Ts@!#@W!U_Q%]Wja,omnFW[%Of[#*ǟ(^`D{Yy,ރP[]9--}uD-jB~5ʀj{Gy}qoKEJTJǿ V:ڡr9y{:jRáRFszJJ*.*U85=IU=ʝ3*~xwS FydڱdX?viu ro{ۂcY66ZT-rϗֹܫuczA0k5hש{U8Z]%^̶}1IȢ0fxl5!!&hXX$=UloϬ(#⒵*ZҞ#iv͚2oDIrE_sOQ9 %f8Qդ&kV~v\n\t#cwkT\K>{l5:剤#T)|HBb'ME3|+m؞97:tazC ZIkJ ,ǵ;E ٰp󢫆Sޅa4R {5gUW{\c32PVF4e.%j+-(Ш(0WW۲lˇTh MY&-ƕRJH:eodm-JF,2,yT+ƺ|^cCޢ2 Lv \˸Q9agm3q];#ʅX !. W9Y\\Uwi)2K{U+j]-T fU*MY7:;RXs1Y6Z 3R5|g Lz?I uf~(YN>YVy(lᒕV5-B1LxsP rqK-]}␢TCaLVe8ŁȪ;34wiVZnݭ%8u㦰&IV-E)-z–w#,vώJ1G~Ĭ|zuNj`ּu[S2mRܟ*(/#[r#R1F5MSTʇJEbdVJ7R \pʻ=jֹJw΁ʽU^@_=Lι\S*֚$t-9V؊)*jnu褤FZ|%/Ws5jɆf' _@]iCAv1sy-\ EvYUHûщo"[i&,"+=;ncg9 EwcE:.Fv.(ʼmfivI^=;W{9IFPO` ]FIc9G%<:aR_[4˚<7DVP*hCZ|n.~Cx/TW$fWp͚FVy]C0gJht-32sO-6] ΍LE-מ[EퟲeDqļʻF*IEHQQ.kQ9yz()ׅ(= X1-YJ 2so_Ilu @௝2Jz]-Xp)QB(E^R;A {{jv܃I]љsxʼ,;c-6$,# 8s@ =2}Rv5^q ,P ,,{UWx`R8oR#+-mл8}li29׮baV,-$;-][+YGPג{bkÖJ+Y++9c֚ǵ:*7Xo,@ Bcjj*_9tÊ-_+m{}͢omS ZDs f)n]_^Z}] pLOOc@ B$)Y+dD!֪*1xS;Z,#VF+\-p֒f)xZ@ 5Y ipղe@掑9U5>a^PZZ/](%IT%*XTd&w_[I1xXH#1"_q5*WS-7AsUV Z`sjo7#1u5֔+_6Y%B w c 䛺)=;\5%|pVg|^W f67iR,5hky!x={Hhp29XY Af-\ߗK|t=&3ÜQ+0Hu^ =}6߶5) Q2‹_WPbr+֘<X,6 Hjrύ9CtLD( _P&ԻTcåLͭ\L|U(7>jTu~z?Tr;?ܲ}j-Zç?FBOd\qj@gpÍ7A6v wdk{꭯</ʺκXjO鞭y~QИ.6˲z|Og&ũ>p Z2ex^ <_җ ϫxdOEUCe<ǰu/4ÕCs%"meyXOk[viVJEg8 V߲+A?)><'= zcy?fg>;iӦChB,囟$o߿>ǬB~a~R>o_:VBk&])M׾ o {J|wj5]g|U._ƘH9+d. /,c/)(Ϸ$ȅ曇 P&Si$)7Q@]_}W'=?-6v;?8_%2/$@@zYd_q&&9򰃹++82o˗-c0Ho߽ۧ<jvXxQk&0wLnD`@E1Z)tB)!@HU܇/|/)ר-'P-Bba>#)XqL8( iJ03$4 ^@>Bͷ.;}˖.SQdBvy/Ǫ}(x˻?oxFn4>1{3x^f̟ϱN}5z[. M3&yG_erqdK,Wss^sOx"??| s}Yf _[: ^cbl%N!C2?^+⾯3S~k @f-֘0H>IrrS A p1 HGJx(Ȗ-SkR2>>~4M㘙>i?WZ399Eزe  gf0| ~˷=r?V^@U@`C%*Lw\v,[nNKӡEQ^PxRP r`w=^z1|hqgajj(gջ21~ 4!I4{@ p xϏ/^SvWۏ[ E @ pe8!ޓfYˁTض}@ p4 7"N@ qs6@`+@ p6CEq .ܲ_ifnݸ1ܥ@ ʨB¶Y+sͷG?L@)@ mm<}wlR^{ru׳b9#IӔ8اKcǥ˪%JC Sl(7< GI!ȑWM6mkv㖍9aseڵm5_ @ Mf(z^cc=:ccc$IRlF ]żg=`ݺK%pgD 9!j]Yw\ze̟7cUk@-ɫVxuԹ /=4K+!n9.+VK{ȃwRoeҥ8ku!0pΡ*ԁmH9ټESSz]b-di)gfH<wG굡 $!KS͟o~S6nfvt]N(Z$^GQJ]8kv:DJ)qGI [ۢ?Ik-91c*C D8 @.`[B@))s_z ?{Xk1Ɛei$ , ` wt%{wBIdxHP !0}nº.kM7@ ;âE㈸84RI(F/(/ǂ\)(vE<"BB0%yGJ+@ (x#ҥKzt;<FkUU 52ΥQ |QBs(jƼ{ ? EH.+yE@`7|]+u{,JA@υ,RRHUzD!ԛem˿y"Pj(@ urn)Rjg`N~OOn_"=|͝oQB^)K윿}`EcL֕`AJR uu@333Eb ;7EClJYC7~iί{a^o{w)­ފ?Yz\s ?y+^k^`@ p/RV!prЎ@W ?_f]!oy[x`0կ~5癞 .-brr38-[wO?=^ =޹n(s$R$FY9XkGH)^x!/˱/|dYGͲeȲ=؃:/C@ 9{o[uaTX`w5Z@q|3s{,kZk^sON;FA@ pLbϝ{_6l;_>JWoO\|U`](-399YI ]6ޝ]`:ӹ>ƍsdt:^WrrUWq 'bИ@  _Jh-ܬw/FGGQ_`xDskײrJ43 s%1@ ؊/Eo/vs60J?W Jڮ @ ղ=As}о+weߨŀ@`-:@P]1+=_|!@ lwB@fחΏx@̊omQV<A$ ]wbcccEˆ@ l]03V)5Q9Vk#9Zk^qozxpJ)ԧ[I@ R&Ͻs]sh_=j]A퇾7,_tc]^hCR$ if)\9 m H!33SH+__]we˖233u_sd\n&ej -?33=w^BfffX`! ]$7l؀=v}Zc ynp1DR!N!#T/tnx' )qxR ^!%^ k ރzP(á9V_bd4%MI$ {`ɒ%%rӟ1},BJ.^;-'?;yXb'SNa|W?295͢ŋy_NkO~yoxO=[Nj^ V\Ɇ 89ǟC: 4 zWihPD2"E鈴75%`Bx 3)98" a,1@w&38 q[~,E.XVJ.V s=J)R0A`j87ӟp3}:'>ӎk7??[6ng^Vלv*|qx9#x̣O=?<wgEodN01YD6IcH1GA_ pA1Zzq -T2c2 }irUWs 7pǦL̛.o/6m⁇\zey /aժ]ǻ۸lz9 5q!/Wr+#6Xd/\4@`3J5=6IHrX;  ZVx9rY`-Kf2H'<>IRzH]Bdzf{!f/ۜ@9HvI%$ 0L Dk,*I YnA2 1uTp!qP/B+RƂ 8I<<@ ضC8#Qq-w0"pYlOg@H! ǰaa*5'=<!(3-\S$ʵil>^t#ǘv'uq1e бo z"93CWUttDb2r,썓&3dYd^o2%%`8xH@E,8kș :HPZc0 56HQ4ۥ? D+4R).!;+ (@ 3 t@GDZjhRv$2]1It"(QJ&d)z`6BљXQnzhMz++Z5c Y) !M4M@ KIN+"i4łH)8g!,NIn%" 1h Ea3CwI]F7?3QG d[ ցdyb•qxFh@ q-5*X,rb :Y">n`ÆК-lj6o"t@ J~R,qwXͩc'&ZTSi/"ŚU8sDؔ4MB2"RLZGODqIV (6zrYA_7>%8|?x q3,5Ź_s#]vݍ6u\.քQ#KS=[PXbe~5J wǧ;&AH<-9z.S[fJHYGIhR| shiFH3Ug˵PUljvu7zA%Kgf_nHA!3}ǘV) Bb@Xw$7#DB䶷 ft"u! Ìuʃ^-hp6/2WAo)@AB@C͛ ?X2dnD "5P(d818Yf,ːJad <>VHQc1t4 WX kZ< YaZO|,olX *%g<{_`.fg!S'ĺ@n0/pDR!y=#a^fQ-:d/x7,*H p2&ChR nCG)faL##2if wб@{?knlꏐ]8N8ݘd.Iպ[H;.z8{D]_ӥuw5 aHō9D$擐"H׉pGOX0iF41NHVY/d`4W$ɀ$IpذP pBfvn"BF))O?5NCˣޙg>jNGUz':?=>HH'?Nrƻ.f%l2)%$t.cZe.^&H=^| +49IRBH!0.OR=Hw 6?3t!oLv0ޑMbio"==wqvq 'xTC!g2:F $y:").6x@`; (cg9gmmS}Q:+u[ %yR@kYg]5}ޠ>_˩O Y#溛$Ct:8kQRFA2;t0N"14()6ne˖eZkxu^+민kb[oŋx?ϸ t3/u~8n]~{Ee%K, z0?+}nC;Îdbʕ+XOGxSؿvE/{9۞Ŏ.d %c#ރk NwS[>^#܁^2m=_t+oHXx*x%HGkpS]ԤiF !K '?K]ʁƍ𦷿׳vZNzwID۽^u:{u _7y3 T <-lcY{Fsע#](Ƣ^KYJY}\-93slw98<$ϒ%Kx^{8{ NjHeЛXW"#SxaQEEv$N΂T;`2{bn(&$Eyqq5pۋ.uzz{ỵ͍7s$*>jוzgc&=U'''9oÏ /_.}|?>Ou~+{G>eҥ+yKGq衇p/~ɏ1o<#]۔L);_2A!`+Y0k׬aW|=֬i=Gv(7^{pEq^9e`ծ+Yr~ xތswxޏ|;XdrzЁ<ᱏ-y7{]4{Ih=zO}/Ri'qI's`N;KYt)-}k|ʲ3>{qPFP߼dd/$>Ć,ـEt[(b->#@:AbY8 IDATDgӦMS=sXgq"lf _'^yʓu@am7\X_/~ŋ_O?1VsI'_)%x?𛟜X;ыW̢ Yre1z]+>D}- ^~ }/}/xsx~V{x /gN{)ޜt|?^jVXN;>s衇pe񪗿,t`,q?9?Ob:N:xNy 87!>\N88.1Ox'Gv_ַcN{yŎr9oOb=ODZ{8ƗoFkM$;._g>W]ɳzy odۿ] w^|wr2.?7v>sqI'-}oKv/K@d[~xg֯7?|st#努˪pKy_/tR%},{v`_?_oܷA^u<hy}N$K.T,wXpS5yF JysO~g-w^: },Nq[jo-j -iWa QCaJ!3\{tp?1wZ ׿xy/KqӘ5s!tU$Ӎ[l4~>~}7n_Zus(ׁzyCs-w˗.`6\fTlOn&,_Qla WkK?< Y3yGG;eʼYSx?~h?) &7glN8fyNp ]?7۷HL%#wYy^v/5漏=X73M3ӟd_ƗWH^3PR ;f) UX0[n?}[r^an Vd玌J9W6s9 )*+5 e]b`O#q==>2xs9?:;b?U^:>?rT =* hr1w?݇55IF s!guZ(zΒ;[Rʇ^ B5i!$KŶi\)hHÚZ'BG ĉF(2rbc4RtZhb!H-XMqMW< +k~~ / &+1Xb'q/5"RX81NE!Z2G)S Y¸d9R` ($@hhzכϡ7 2C@1esaeMJ@!hk%I^"&" FIjvA kW<Βpϋx^TJ2`A$) & hwPVTH" is$I )$Na)PBI(l-%qEZizrsI,:$Zq*q M5qJQ"EH@ %H!J\\"&&FK(еU TT!ф*<9rw.6E ZhMǔ+dFH*3(0bfj0Y,,( $)U, a!R x<PRT@ h"h % 4[8A&ub$K!ek!ka@M' W-S :+x<#ekcPJ$ $Ȝ%BlCNS *J51;!FKR)A b,4;hbFKH%QBLyx<#Ðj1DQiZ j@(IXcD . ha@"':oD$aRMS!(Hć'j6Mcs뢤$i] )1B"C 1L#€0 h$$ty! M%ØԂ Dæ9Ǥ)ۻ_ vlH_1@!. Bd5FB&ŀ@HT`U@IqLlA jCj1&OOKò,MlZêLʞ;ٷg (5ׯ!B&AEJ4E \ $P !.$iB.Z-(.! 2 q$,D!HӔ$MPAf@9<8kQC[ s,ټ~xJwPjjzAVKK%3 e@aW T HIb `, m8Dq!cMb# Y$֐VkDH&hk來_sEq?Z RbTJBE"qBjEH\G,.IQVb@`EN4--E$CCJ B(x<sT"bљF8b(! $KSb0΁Rhk1PԐ J`k2@!$Z'H\>S@$@xXK(*Y68ZG(sHBtqXi(IԴFY*5pXfU dY5YZ<<@ @k5azx<#F 88&2$\k4&sԪ56ybQK(A`95TBKA8biJ$ͥ&B(D(D$h[#Kj^x<H#" "X((#@4RHTB) =@ "MݨCjNF8 0S!x<Xk56o+*3XEå $&&cx#N qU 8%s4Q!"lib,KTbkUL%x<sdB`D/.@P;XAM aQS$d  P`BI(%NB9aE\F2KH!2!)5a x<s΁(%QA@VC"QBETHa@D 5@d-u@d::̢Z?3s x<#E4%ւQRa(DhsH+TpTցdB*t#J""@ƒLf&P+ PKR*J P459K!)N! L()qP*`#N 򶾑TAĹG!t:pXwfR,ր*x<sh. RsjUB$Rq:T m BhF\RJp֐毦T28R &c5x<s$+F8"b2L4`X, H3D!$L@P U tF`4zhbL0A;Lbx<$RB(TH?Y AsK\$-Qc-q!FYGR Nkd1FE!*4 r5*FD i,qx<a6Y4!$Q((KU B EHe $C\MHphE"JHԤ5GhVjV!A$@x "° "*Urb3a`@8t!8K6h(1(FM($!! $ V1}%AX+$B2)pVքI3@^x<Hݰ6@G475ce>'P R!D O vlР'Ycq!c  !IZ'<)I-K"/- xd(U`) C'a-x<Hh#jX@ HbQ.E X0EYJ p{ʕ?00v2o#Ęc+sZ>˖~#P$ּFֹ5З8٤TiH= 4~ 꺵_iFB<Œ5#? bpxV_l C˪X0E^4GL;SZym0q55\079)RѤHTz_&+ cF@ xx*r6nTOi(8BI&Hv Z5±{Cٳ ȅCI5NGw]7<$]k~@9 g@|[U=*͂DŽLk=IzǙ&ؐM# E^9pGj7Z=H苃_׭4cJ yx&87fƸ3 'MU='8(V $='`dE6FW3gcFf2O ac̸R@11k ry\?!do!oiJRR+3ytG]=s:>I]fP5ļz,kxiV\7Mܿ~CdYuc}c Y|Y}}}}<|>?i 5}<gB"<]P#p,EW85Aw`t/w硷XSs` Zsnb\c>ʪUPJqȭފvj*FT[}kL6?nK/}W"߿kx!2N>d6oƍw̘=3314*ӦY z 1c Znh2UaF:'~ !{w^ٲq#={Xx|֭y-6 rfvlh׿{'VdiL]ƛ#lټݻwX䱇! zinifxL4vm鐇򯔇OSKRxuA;hjn9[ IDAT`tICy ‘&%^*_ڵk9wǫ_}%/s!I4% C={ ڔ)SXjv=K({q W^qsΥĞ=P,e$Ix<MoK9Zޝ;{:&_];~vn#Mj̟<wpss Xt)G[[+s,sΡ{23fLg괩\qTk54e֬ټ実_OT" C/^ĺug?KhimcPJ1sLO=ŋ=Y`Bk7gl;º '˲F&a$j5jI+W2瘃nxnJ|#Ȅn8[%zMkrT5~!2~omL4\O%k֬a}۷kn9z{{yƇCttt   !>re!y{/v+?+)%A4z8kY|u0UPyQJ)EER`z`C OA<^utH)ٶeKNgG' ,d|dڴ_9wqC him?wrofv8$M(W'ӻϿ Pl۶w]twO+/s5B>pYn׾M\ظi(=lQ#qӡ^p/kαt=Ǎ}1y_cS܋1stuu,+_IRA(W[a]bHXd<̱e疾Vd<#w1̙=Y~.rv܉@NͮVqI'iima`pg @ hhj 8q"d>9MS Bcs= F6h8&IosWҋɓgN/HE8``E E( B5:D1AW\Nw$6L6j8o{֭c㦍sg( wu1m 駟N,Z=ykɕOP,hmis 7rJ^Fsizp*n`tz}bՓ|Cf}|'غc'{yϟ1Z8),w??`ђ rӧ>scm? /ȇ./כٵу2o= F{Ň?f7J)N:$LGxsH094!*6}q@E :(yE@K6*yOKK &M"Fŋ{IJ rZW0$ΝC k-'w1k,cٝw3^?pirI)J _ ޿D)pcKN?3,ᘙ`,:q!}UlظGko5,Z hcf1@qg,GjT 7}]waF)Ųe(Ҙg2 c I`dpfYXkQBeZl@Xkiֱc6k/28(SN>i^s(Jv C1|߾@gg'C} ):y KN;> o~+fLzS&O?]$a_cZ4Y.|[>cpiŲ_ђƘǞDUsqŧ-BOO7l`T*<4|B}:qFH)*O 0Df۶1kރ6IS&~::BPTشa# OۑaiӨVB'x'xox<D9h/Hv x8;^d¾1EuF1P B{ 7?])EG{x00 DJɤI]N$3!ԕ'9)健RN> g-=aGCZZZ 3y4|LY4f)`kyӅ} I/&#|;)%<8 q: cɜskx<CS] owz A1 r=VZ? SƽΟk-Z1x<)rp[~SooFf?Cv\Zd믱XkQJ!tUՃ^L `ojŽ7I[)wojĵBcXUc>f> uBݛP $@@t7 hgX8Nv V!fj ϬZyǡׄ_`MEtH/jÎpLآ4˘|! !(W4ƹ-~nk(uX`챔R Bx< z$GBo7\0/8(!nEwM""ݹ}D8@Jɤ)inig}Ÿo,3IRń({.}՘xR*P !]u Ǻ낾N]o_ǁx<X8qb۠E[M:8+$Ziji=l\q:Zjc,& kk&:)ݸC)cU,ML4+t?0Pu, +azaϡWq?>p@#X ׇ( GDQwMy<`|#p?6. aO!%B ֯_㙈Qgs͚r:kסbcJn֍40}#k-/[+^q.RH֮Y$eŃdcSV;ٿ yzS(׮gӺdnΆuҬ1o 0<0Ț'&߻kR[ ԇ?w'@JR %A ,38gYvbٴwo񼬬M*1`1.aאeZ"<`)-tk-qs~lg.YBZ+_ AiSG%׭}{\W:Y=twvOmۘ>sdYƭ?szaڵ|d|_! ݁p΂3XV@]I\ y8,Qa G_uܵQ7J3`b `̝7A;vrYgs…ٵU 622}4-ZLlڴ'ȌY3GpB=w2e ?9|_fҤnN:$ݱ~8ctFJ+x*_2y9$8sfɊٝ;[[c 53$&|' " #tƹV=K/=%%* ]ɤS?Akv$M) Xxҥ'ig}?mRKSFkd" ZpL)Nװ3)O^/^ʹ4@y{N3N; 8kv ൅h7Ojq!&uQ,)JQD剓 ) ~ pe֚0 qaAHר2[vܨimA{!2X 03jQ_ԡz 8)S:$9`{54s=$51T^:~aj5k{R7sΥwW/.{}qgqᩧo~UW^sv/ZʦMXh+V@J0.+,Mٵs!1<˚cU^5 {`%s<;DcY}=btXaS&3q G^J 0{XI]]tw'=ǜ@x< T4lؗQN-qnZ:#ɱ18牏YFZ1BPNUk.11441?@wgtXRTPRkNL/`ZeߞݸM{G;SMZÅ63s9! EdQt\:OncΙ`c(8Xd Z͗fN[r:'z*2P_yk;ԧ> OEJMd3'~2y2Ckk+3ݓ'Ap1ӧO/̘>T (_xRG9'L%hYRHIPP'ܺSkx@2 JDaLDaHEH! $QQ(Jc~!FJNh*(ԄԄ3c-ZTV|/3HR ^?+E!Nq+pHJsx}: aРA%$HJj s =$V\ɀc%++w{.YRp 9DFO=7@E>su6pa\QwEY iǠc5_V''a RiCL0q9]?MӢ }zd})B$AMCHZŽ!.8FEQ(^gA:5f-Qd-0pc:mGgWkDt B8=X8l!d"i4,Bk_G@EQEߧ4=3 ^8dxy`(۶  g0 P+dHۦxeeHX²v|UX ²CؖPEQAuNy B~s&'; 9r$}vvʕ+Bτ3d޼yн{w,ZDƍdffGAAU$gORpጋ( RJ*WP[w,Zod6~DR[W.r2`6m:,\`08t(} tڕy,[6~zI-~p:hlhKlluQdS--N8dG*(0 QFם2$5+W}6ewo];rqΝ|2ؼe ERr`iλ%,tӤelJ ^a#{/Cow ;wGwb 5cWќ6j &_,zxTϫA4ʣ֭3>vɄs&˯D'cMM?yu|M|?Wss3`Pnf3p\tEQ]]OLL 1 8(ksDgQ?g8cY\wX}_?g섉,^]^fЉ'SYUE([ncԸבRy&Oc᥿V3jlݶ +fܤI/Xns5y*ƌ 8y 3Ɩ=~~U0vD)*_əg/P?N?Q3ϲ{b!3.W_ocOmH)cW?a8>qq,$j!h`v4K@5jkk#v;\ߏeYZ}~P0IJ达ނtu~ YY %_.[Ƅ3DZO? X;n۶E ,tp7rUWG2rp k֮wṧbM7G  ;inicgw>ċ='BSS3 08>_1.]c,_8x-/6|SO>/|ySyѿ=|FIClV~YY\w\{H)غ};N㍗_/cQ,3s ̼<'1>u?x~?FJɫssx'q~51z/>{3‹xOGX,SxSz3鬉\:s?Na v)^ov5W]Q$.. rn3~y$&$`}F4KJᇘ4:yM!3Nl[-11x^ 7o_߾ 9-_᧓٩=zt`}d?۶iku7݄Ȩ.?/{1lq#\ E],FJISSu̺446DeW]yӧq̙Ǿ zpe:Yҽ;ii'*k+IHpk!>rWx:PÀhhh`Ϟbmm{ :nPtzB?O~} TUU{%=#;vp]wqꩧң{oFcC˗`ڳ_׼;G `0w@CPb?{iiuǯ4Þ?ziѭkîE' EkDvTZ?_߾lڲ=hh$&ij2xc}#q:]2md~IÇOݬLn8zN)l|~?1n7RfwE|v(4ZZ?$x)߷F1سg{$"exVf? 8c=6f,V,'dn׷{?NKc3p3s >"%5 WL,} (//@22ٺm+' 9˲Bl2tCgE\qj*Xx o6&M)0jZ3ƌb.I'r8 OTF")ill.↛o .D4mwEqI 7l2ӧ^=0yߩem$&&rKo>Xyǎo0MiXl96n`c="+ul< ,sJJv ڶux&%{׿Ev}8~xgirW{Bdg>@ }E--tҥsg{AJziDJ&B(dϞbnRSx<޽!tqz?X'6.0?XOFFƿ֮ŒM( Ya j[IKOrvp8N\#\@)i{Hh 0Bt?)et_xЍ5M©vwL8՜2EQE9"Qyزu @ښpî)e{“=xۼ#jkkiii]Yn^7FX $mc  'Cc|!j!@EQE9%KiLM] V4753yݻ֖V}L0d AV\)++d4zy7zAZېBYe[=ӄwCM j^(YP@ll,--|d֯S=C`뉍hN;wwe츱ģĩJ jx .d%qCey7ofSZ\9S$y,{,̄dhmrCEQ_uzc:LٵkŻvvqlٺjzӽ{wl&3+RЧ?ϐ!C6m*7m5!uya[`r2+*Rٲe SNV={EQEXG| 7^O-YJjR2'p--ҿN9d>!W[[KFL C чae0$O{¨aHMMEZ[z$&&ILL`֭tL^^ߟYyT1#T(!* YZZHOrv1 8Lp HHL  "ؘx<IYi]td/z" u۷#*S'vxux.J~g|2/_An9466LRR23n؀ LU~^/ ||0hii敗J!?a<*xq , PQEQ4LRSO @2<ԟG||]veѢ <]PSSC(" m6ƎKYi)@4Wavnx^zM'NtCMSEQ@" X!RW_Ock n} Xn-!) ΩܹsyMEEsdf/C2BT #5C`S#x}>4=݈qѣ7~ۦQ((QK.!!!8,ƛn,n^=IH' 8N|>/(]DIxSUYi }̣hzr~Κ8fҒSihn2p q8ˠc/T( BHJJ0ABYٽ8+ =q(Ce93 !nx.q;ҳg/,XG3qDBV$O(++G<XVv:)/KK[ uL2Y(rR "|~ &egwb: 0M8Bmm왏i>IuP(Dkk ˅'11} f7x^233ywY?HH#1%LÁnڳY_M')mXmYAB` @0`M\:u$]4@(OJ\b ;*0\.|0u5b&R 0 4MCJyo O`p x[ZZд'?|ELL C#&Ob0[lrsV R[[K'K/6o_m;<)0YFڙBz]^{}%x}YEK{`Aǟ:?ªU_p㭷GI~8/›o}cz}/[ ?>F{M3B1hnibAUk8XSOр;>z!« _224M-`<8;Y\MzZ!t"[I||<---ضvG'455EGڈM63zbcc_u ׉NHH￵5zT\\?#x z۶)ڵUUuG3}eWpcN;˸F("|=7~||P@RW]K$xqL;o:?0$8ٷ7/  w)9Sii ^/ii|wرs'yA[_|"֬[//Py338OĐᣨoh/=ڼ^FrNo=P?]}-vK/gǎH)9e8V^NS\LuMaPWWg}qgb?뿳c//m;v >˖3yᡇk[Zd[6%%5}PR&Z}>*++!rc Th_ B4M'7;%_R:l!߲|B ]gʕH)ٳgL:nݢnN6\/%|irEYIKM%kWbccXz CdwʉC<̄#?n,}PÊVrĉB`B$FOKK˶HNI9=5:reç(99У9P]1--IDAT$q(nޜC|\,|8EG\LӤUt|99XJg~u>2RRo[vxko3+o7G>j_3s{Gn.ͭ呝iɴ)y׸p:IMN)% 9xl[DSJ<\hkkӻKF;|A|+T'MQ~ )iimŲ,hnnsμWRXj5)))tܙvhWkג_^=Yh1^oР`0IKKSHNœRb ;ױ_mê~`NJeBsJEl/*p3w[fet;XK,]JKK >Lൗ_ⶻ!G.}za1P\RO|V=wIϞ,^GP]S QYg4ԣ `jR y?@lL iir n~uMLb<0kyyz Lx xݬ7bfp#$%- PR^UH}x<_FH}AC'@HMMӐHt݈6P]"ul[Fi&Z֡\tBY%J!R@ߏ'PX#5W-V}<Ѓ?y?K3 _}Iy}#q1z<ܱ'c.b&Srai&Bk 5S|B-1pCBf %$@tEQ/O*? NdNw°1Ama@sr#ma 6NgL젆0v5S/@QEn9 $ރm:!c߻ $7Ig_e,:)Fr7z8scK?ҼC\\o~m̤tOgז5gfoPZ.Ngtx[[RJ\. EQoR5g] V>ىdJjF*wŝ b9ceTGRm5LG&`Df(\wg%'5^#شCFA"9+Yt@QEQJ_bKZ!:J/(Bfv@wƁ4@^x]doVvmAL F2}Yyǐkl&^6 A28|((>]1NFcMp%~p%EFzB8zIK~L}gȸ4WB6]4MFQg' = vhYF}A5((Gĺ]X +ƙLqCؿc%)"l bEN!k*kpw&&-E݉m%!%ӱvqЗ-޽~lhfx ]W( L{tJ ֌!ndb iK4-r5$$&)ɴY1t+8 !q0h$1ش<4acI gw1d]ڠ똦-u @QEQJ 1dp lʼnMat.MM\|H$6A8X2H'h&t) # Ѽo(((Gic[wh́hkBf9##Dfxfk` 4!44Mq>((ъd&!í8;پ5Cڗ#5Xr,':R.((Q#Dɡ Cm?{_ʀ EQ v=wWu:>GӴhy }t@ףA]PEQDiڇ#nߩoow)P8@(rF4hoK4ȀoG 7nRZ=_C2<{ B)/+IdNu0EQ"ڄ$u;$:H:.Li`` V_xU䀌\- 04ɾL7''`# p#tphD I Y"Q)., ˲A~?@` @s{h=Ӡ'(Jľb=@D"xF83 hiU8aq̝r1n$&^ۇ|̌K. Ie]~A4&˶aY&m \Bڱ++% BY6-Οp: @C(·>$7/ ,%>+M=+6Ndƭd?KZ~JYh8.|liI3&̝m87"( i |c221M t3~䧢((G2a݅$NT@kk | .? F I}hlF4ܶo]{>v5塸˶q8|hl[K73ذVm*:(rMޛiT j%i t޻܌9ϡ6^?4.AZÁix[[q:hDCcٹpLxydu`tJw̉` #%<@Ψ(! n^ÊVз_?N?4 9x hFNYcFr(-+eU8M'[ 7i^v %ewĸ: 7H<㣔йsg:,y]0tN'@Vpn.xJ8?sh=^ @ +(ʏk:n> ݻ7q]믻$JJ;@‰CN$11E߂; &ĭʋ/H0$ЧOo>Mp\<Ѓhip8hni<@\|:LDp-ʫspB ` mT(q;}z:g- euz}t%N$&6`0mx}^MIiϙÄKHKOcmy]l߹-lZ t51c4660 #2p⩒OOzY86\.`lx.r8d½/ BM_%+O}:L+lVQVBZF0 +27 a.К@Bb455m =a:L111475KSSnKJr uuun à b.aܸdv팮0ؖMzttM亩pp>9^7\&~P8`]lWoEQHho_@JOzF:11AΝq8aiZ44M#!14ҵK8O$k.HӕĄt]'ѓ]߿] 7 ġ,+|D*(F0*094Dyy9M=.g[B* ]AF,C04i 4 #'!..a )R(rTH-lliSUU]iiiUH˦*y}Rc>])MMM܏eY;H('|GCAZx}U£Fmm-X!/i%8&vx ?=$ӦM[NοǺ!S'u@E9b>CޣW]~>/P@8XwzvlNrB֭禛nh.JKKinnb)+-gI'b~{iSh"6nd֬Y<RQQA[]EE2y 7j*tMo߾d#,͎ܬo Gox!@cC#@@LEQzZ}y9lnfzOh:hGQ__OFNϿ%Ջ$`44]#11n9t8Bt:1 ^yXȠAp8Cvv6[7o!>./.WokHڿ\/meYXV`0D0$l6rO n,[Ƙ#rrNS]]K!7x#O"Ͻݧ7W_}ӧ(^{<@S}B,]JϞ=KЍï|Ջ.i̛1#3ӧNUщ9sc#=@cK3?vþ}躎bͤ1 8UWOrl}d UTDjj K.sϦ$]~՚՘Y )Ʀ}}BBe%0ϙ3zNsfE3#vR*4jeBjSNpRu x,/ 4 G;紘 Z墅}[K,QD%q%ET Z.^@I pYpqz ;v@+ssM@lu~=\es1 61YNV;$##֝/&&5 q ܻ9<n`EpؑeCDd,V+UUHD0$-- ]q\tuw*sfQT\D0D)|$ v!׳x7氲Bnn.RSSq:$CLwR% ԧ_8\z}VJb|>* o޾#>BH4d͖Lp$Wmzf@ Fϣ'S3lԞSALqv(iv)11m6~h"Ccvcl/{[@UUB˅#Iz.u͛@alhx؝N  POnIENDB`paperwork-2.1.1/doc/flatpak_saned_3.png000066400000000000000000001245331417573700700200410ustar00rootroot00000000000000PNG  IHDRl(|V"zTXtRaw profile type exifxڭu\;# ބ?FDJ4Iuڦf4磉r-6ǟOg~ͽ33{o^.tR9Mz09}(sNӝe^G D0i<`qknqFcz^)މ@lv!l8R20֜#Fr!'t:>R \}XۣϵaA4lۙk%:C.%F^ד}uuÚ%]6Q3 M̒WtE8[6tu߃8V]|-wf- N%3{׳}e]<|9{8cv0ͷu(m;ˑfB0/f-Q\uǃ G泫ō5A N$j߾,37;TXW=L Rj`uWc_cP6i@ij 4 7sZQЫ J`#Y͚i>8@lxCO>=~@17.X}l8Oi'v O3Om87EkOn5s;QNǢ33AılkITJ#!JeD)m,bꄵ{^:2>}Q!5@ѐ("`,H:]^ oux#Y7aYd >ˤb0{9y*pl(4`Qd8ܺ͡&R%VR!WzሊaYZ\63 zmInB m;\ا1T;=Zc3E dtn  6j Hǔ"WMy|?k!>gDQ}Nb~U%< 2Cw4rSjy 9b!Y$ Eت8@*:21wr 4/0u0}( ywyj71-rFYmѫ PjvZ zIMҭirDu!:x`%3'Qdq-N\DF/Ҹ7/"L~2P)F EdRq ]m(V_Vniq@DccvnUL20$El뼸,) s$²b eG.ߙrg`y؀I½ND]ĒGiNC7n5)AH܋M'PC\9ͺCxjF*1 F['sO/ %̘< 3 2~gRexT>[ A &H FCɎd:ȚS?L ָ'92ϝa-"ÂIW9%Bf! 8`)C4$:`yIPV *PIOD/aT~ӅeaN hJTX8P\U0B"D`An[~G;u ^Y>Y9[,-,{g@9)HBHNR4XP1!5*rT|%9&`j"]up;Iҏ9bbO;u&b4ΓsZCJ2h) ).I,l OO&xRZvj }%ΰOAZcDT0yTD RA3k5EW n⌻O4N l3\.K{SxwȨ˯q[€ m\N)QNa@x& $.$O$"?zZ U X y@$ :y1b@H9s"9`ʺP[$U 6uL+ - ;gzBJr4 ̈ >wj73" <@0EW7=JBFscl[!$'D~'w%P00F7hCXJ-N*)*Z@A`4Ɖ͢c<"EE>- 3 ! āa<qB!)_,D.95bi>UI nȸkb)Wzq3F&, ܁ 3;%D (ԲW^IBK$iՉ$2k=ZIg7RC kX8$l>@@kť0o7GU7F$frd5zֱ_EͯE͟<\|wRHysՊ=C߼'x^C'mZ&w U;X*y]զ]`.0jڥTiXPzf?@c>('HEsc(#]Cbd\&hu0hyoGc*AUVRw~\p,h-[,8d8F`씟@4YrQ50D%AS ZYEZȝӡBТ91ɸuQ--P˱ um_NpR,(BeDs>vjaBp6DŽό 3؃8z<-:UßP+*S|75TN̝\©d{D僪H:pnK)P,("B[W(ѩ ͑Ei͓a0HT4-@e4$SU8#z9b6&T@=O :h1Bh { 0L$z{ sAh)Z*2ښ jy\_0̄+]r$[j< oӹ0Ů/dD-"&wx+{bFhǥD a ,F4tMo|5E _ኁŠZ۴>>gZ<2O ⸡S!kI8^<WAHa <Ut Xm[1D~7{ {崫l"#J܈)ƍh1=h@2&G0$ FA'0ɨMAv5Ƃs12zbD҇sźZHDC<ö!LA;^b3jCmUaI8Qў ͣG@\HN5Z-@=(%T':oPXZCtx>HgS1L%@+ =x @} 4I?C=nēR7!81y7\]:d"تWcܟͶ` |wO\Ӻ_B+jxoihW (NӻKccNsK@HADGŒ K9j:iXHZ׺V[һkǡ9lYmEjDŽ{2ikY9\LW8kUkAHċcM9e@zB{QHYM#~<LNq?BQӅ,zSoՅxa ӎ Ywvb_# PQ>"|L@h Q! q1jZ5| $] >c6/cᮀw6!,սẃLEۅDbdᏮ [kZ$S.O.MT#>?]SI͕ފRCPdo)f=jz'= 8oz8A(I=lЇ*u;sF*'- -]T7]5Ue=jOP =""@F5Uږe)V+{ܬl Keyƈ-|rM^]KjmqH"jjFwQOpSxJaԖԋjjᏬ|`c-Tmmhe|x,ag-h$AsZ1duM(ot1SiZsnP>LXߴKK3\w1Ȟ`[`l7AC -O,ԤLŠZ٢$+h)vSiX)8 -euic9VwYE ; I6lVd x ܃QO"Rw(.jKdЖ'eh/|^O^Ѷmha6A ]P" ԄiW2DXܑQa|)w3Xh[wƬ5- )SBO:͕Ya͈SK.G2P=5H]|$hGUFQN&E뵒1[wiZDɻ@`D2٦ua+la.9!݃QۅRmP1)!ڹ:%Ě%nV$VP+3 o([mڬCD %]Pd(ZE8I?I@?yZ'eH9c\^ dFiK@{O3ij'WԺhƌ/uCN-ޚ:2ͳ5cOP2!9&2rrq)oHEiA*N@W} 7?ͧopϠ [^mu!aC& j!m5 % Tn%Նt,Ҭ/ӦggPȤta.A}筍ۀZ'hNgR}}a`Hn' q6ʑBZ{H z}X%Omĩk#Nݑ\cEQ(˩`I(ɾ[3E!ӺQ J&.8{@vvY۶@Dc>aܣOΉ7\"3>_f7Cկ!\RGIVhܩS[T#T/#Eϻ9TWꒁd wdME@ Ii )gVB&I D \D; Զ_|,];eO/2+% d\,9G (ikQ،}>Ukj!;7vܹLjg6N͊V}c.N58j5!`jq3{5GXZ4. nP-iI3J5Z/:#26eo 6f۪OQZ~ZQ̺ۓ!]E Z7%0h Z*"2PM9ZVdhvyuMpHX X炜EL(*WN+=HjZm5u G1RuLNE?to<.Ɩn/n]mŸ|vaГu6''EZTzYHBZ>JX[@wtoj~NWۈ5RzGw5V>vq̈}W}SԻih,(|8|u)ګY_Nz#G$Se^@RU&m8/>ݾSaR29m2_7)xꕗ \C VM$U&xpK!@H*?X<{IKؐ怙6qj~Q4`K@( v/ ieBW@YcILi > N$3Gm6,f^OSև0Gjߊ%)9c*]]TDjŧ֖&ފBx,~|<z*d*!{kyC{a{ݜ?̒yϳd^Y2Y2o,q̛7io,u^@u< `nُ~TEqaC Tn̗g+ۇ_f?}`8WލaGw?5?qֆ6_mXW!bKGD pHYs.#.#x?vtIME;ok IDATxw|TnO;D("TDT@(|4Az{I!M]nX{ٙ9̙s{fFZb Iyyb* ';v/i9u,׶hFֲk>,+N:( ]Юu+N>ͳF̚ojr=ШQCEA hD0nXZmGFt҅׭gvZܳc4:|Pt:Y9 FUQzCgEE qZ$%5ooϜ9{LoOdĈ^R@ nlق}e6z=ݍ LFF#'Oڶ-`0椥] b㈊DQ8DBD8NW &:: EQHJN!@atF+:^@ ,bbu-]nB&4 &IҼysPUWD+VrG޴kӚ~GCٿ7>CUTTU%.6 )LB@@UEo 4+)(IY :{oZJmcXXJLkŴ | >Agw˯!2͚6EUUΝObToLbBb4ߨ)'OB7zugx˯&B|}.Sv=22eԨUAb-T[Obӣ[WڶEQQ'%+|!I^`ԭW{ӴU[|%_זì>nݺ9{mZsCqN> @Νhټˆ%~.}ы>O0)tОoV?f_y3l ]'xlL|CxCd^EQ0 HiX>Z͚Pn]f|SO8x(99$T3`Гz!oϡmVF?!:q/fLxq$izɓ]^ [R}u*V E()٣FQ㈎r>9=D]V4lPEu=~|>BC9P0"( iߴkw株NWS:U$$$Yob28]AO:Yշ8NŨQ0Q=6 k!+=@^}).)asq?ᙙ7U5|GpGP>]9+֬yqcNX6r߆o=<Դ4|x(>~?111;{v"/;/>CXKe9l PJ"#>QT]:n_H4Kt-璒bbӄrI!03YjZ4oƣ@`2h44:-#MDD8{4 DoU,E Ղ8U*/'Uo۫JpvqƢdUU+q/o(שGo"#"ʪ߶?z󰅻U;ضPa4pFJLTv*\Gǫt:/ ^r9@ (=F*-I6C AU|JG9kJ[`:pu(cDO%eV[}  T a @8l@ [ a @8l@ [ a @8l@ [ a @8l@ [ a ګѨlfNeHOO'** YCrssju]vYIvNNI $((/y$Addd$B̅g7EE =Gԩ*$& ;;Ʉh6Ndd$Zj$뉊lªUFR\\,ڳ9K;kY~=$IWtXm6:cGPXXV^ϰߊyIbYHWvGSTRBaQEEfdYOpqȲ̦[غ};e*2 >k<:?ꝝi̙7ɤgqǿ&6ytf-fAAA6~~.;…|}(DDc0&((@aANpSQ  44L$YpY]u7ɩPV%''͆N#:*jJg  !00If 00Ln 80ƍѨAz='O-Ϊ Ff&$a %0 nf&v@q8X(.)& BXX8:tZ-vl6AAAE k4D>ՔW LH ?/hSR;DGECRE$J,% z= 1njD,Q#6DQR{QjHj;;˫7kFXh(",022 V[ɲ,\HK#̌WUU2PN0L>ZNL&Syn' p`6aZHtZ-T\:fpCśVGzSxrss+=77Y@xx,ftZ.v8.i4p8':* p=I*>$#Td$:Χ+LJ牰( YYYIfʋvixs,Zv3GղhRCG 1̚h4صlO]l#)6iEEz>'%55c1gΞcdc{dYPvv6)̝?>NKؽwz={9>#Bӱk^^:<9zEUUN'q1hd >~u;gΞe蓣em_PcQXh&77z[~+Wrz&O峥tص̬,EE<7fɲy~z>l RZϗɎ9DvvE~ZN-ɟ> E2Qt .P }W&rJVQQUȈpNVWZ-~wf^"E ʫΜپsc8xN~VF.^d;K~_zz>cמ=~Wlݱ%%%gWYd4#ڴoӉjeՋ[{Vb vރh*=.`˪122{KhѬ)A$4k°Ӯm ~խCY̰:W,s1#opzf\5Mܩ#k )9˿dėi}̚1\lPQ z{Sn]"}u ݺtu~VZ#$*27vȌ ӛG~ؘhٗvS\RB׮]8}  6Ajj*C?]S'˕zK 2NJmiڤ QQQHHH@y>Oezғk71/p=ЦU+?pӣ]&՗m;wz= IĪ5kyqX^=/\u^nP`0Bxx8QQ(ʝ{.4v ߭rHە5HCغ}Ə0v;]}oSԬHtBC=DDr*y3;v` ϱi32+ڳ,*x>"(0KOO|RNg~W}wчV-[2sT|%)p_CBw][ki݊o#X6v=͹ٺmr3V?\ 5Ν?ϗ+VzbΞ=% ZڵGn<8?Nmiy5_2(nՒMc2$$ǣjQUX @rSէMk& DQt8PUBs!5+jV_QڶnA^䍙oWDUU:-EEddSQ>g6"BC5c!!~wE!:*\v' Խ[W<Gqˏج6:w@ڵJmq.)5p8HDB|Nӣ/w[~$>>1@$ṖJQUATc'\Etҿݼ<<3I ?t:8 NT,',ɜOv,$I"% hKC$ky;)?eu88Kۑ$ l=t:/VQT$7v],գf  EQH٧FztP~:v耹LA~>~MہF.]k'nUշ?ł=>Fq1=qh4ND2}dÏeMULE LUUas9rM`4E-}Q?tr!>.TU%F Χ!2񄅅S%Wwp=K~G$H پsFҸQC Be)4q:[$N9nj$$ᨊq,9{gyY).YK@JJ*V sQ99,t:9w7$IJB|>B^^`٪ײT~+'5+س/]|ݠ^}ԮEdi?x}6-iN:uٴGbcb$Lns}X|9m۴Ʈ]bcU?l5mk|HD $!>_!;'IQq ">4y+WUVWt,3ʹ!K2_/&(0G !>LM c=ھ];1둒u׵C$9>6/OB5\ðyf|;r >#ddztenC$O$tȑ zb$qQs_8) җbNh~hPO +S~:({n$ѩs'.돪3|slf蓣̜1A/R#>7NᑡO͗_xbYߕ-j$ē_PONԫ[bÞ 0(:k{ R͆|抪ұ~i-P^UUn;ɹI<9lSx| 9} uԩP$ f4̔I f t:^"@6MWW8n;]{|R2O ̀LJU\a2+Ip9MJZ:F˅E+VUK>|yhJNOeOnSӡpGz=A,޳ȈpڰZtP^~V}cy2A܏z4Vu=Ffٮ->fC vq/gr|^Y:3e\Z֫z${be˕ "y}^w=:gyJrJ ktÊ햕`0xuRz-o'*6ݏټWUUF#Vէ=ם)S9t2ЩOwx+c7e*vu7kjyݶj_\"eQm돦'~ >՝`(o$ύW8£m6RiBNn ̝zk;g*VYeU2{TM}V++sYu~ke_yL8۬^*WV\OoVaa+&[%Ǜk\J5>vA, l6# %<<Ặ^,AՒ'>.vEWk |䉹-ȣTu)5h4^U\&L`X|}Qvn0y!̚1*un8Wa; ٙ4o^O6=\3Nd9  4`]^r޻k!,鿰]{˯Æ &:* ̜( ##<,Uk".>'C2glVny3]:u_\E\7_M頓egUh߷f>],zty#Ӡ~=/R:왳<@fDΪm~ز4AԯGNNNAg>|:^Q^A̚=N~qξ\H3;FѰs[5m0UG>1Erq^0gEpPk|1 ЃʯJd"l6+nɉ8ݺ28q޽zc~7Z ka!FGwrWe,K{] BkLFCZxy8رk7 $%'3q\|R2/y6fLy Ͻ@zYO  >C1l(Ҙ"5l+ej,Y9;`47a"=Ȩ#BZ\ }Fqq ώK01L;~YaI30Meg7}OHp0όmnecr}p8:}KiNnn^vW^mѡ}{ό|{@uUֿdUݺvͷfjs <4gGCדv"~=O>1L2sۯ s8,[$$jH F j&&2زmk3,I"9%B]E 쓏aQ|hdڵmï`ƍ8NlV+*jp8qv D`@Qߗ> fj&Sv-"۸vVp[.;UpiڣϤdΝ;WAeFʛȦtmӚ5_jLRZ >!c7ujp:Zmsqvi9q:A7fR#!F[}{`~&V+O-yq8tࡏiS7]ޜ1ZyoP/X6e62HFiX*t*[0>%(e/kűINI^ݺ>5.^Mכ]Uc'NP+1v3Xϧn^r%%%c;{ԽL$I摁QˬRUr6);wѼYS8r !#3e퉈de6בS^i5ڵzN:UeBQ";n߁fc>ߵg͚6INNrx%OQoJTD%w䲲&,$봚3Iɜ=wl.\L.7"6Տx}e7qI f={`ɣFBii^L&}fyvLN?|,I Hh4z}II ,sQUvL&;^}Bp8*؍f:IѣML, OzYY6ٖmٴyikW&I.]pZ3|uڵ^B7JZ%&5iԠ!$jbҨaC6i%Kٹk%**{b9x ͚6mVzTUqÆl۱{qm%K(..a  G ڽ_A#kh԰!Kؼe 6VD5i⑫~zbjVXnԉ,36/>4j7OMp8݋e_.gϞ̜6ĵ׶dm[Sv-4 4nkFƂOyګYzz}޺q-GiӺnwUk\7kīdϾ,]9Na' ciҵѽDZJYYx  "6&g9[z}JqI [[زe+͚5vZ$ő3gqVޘ20Xj-iN>}Eqظ:=CBP^]X%kۆ&*_&h];~7|56$11$ڴi^'56 (08zrsrЏ$ Фq#b(**f5ܩT /YJ֭1TFde߽ rvH-`<?|x iii\۲GMJCjj*m۶EUT22ܱ6l$,,zu)y?\ZXT)>|Kt=EQ\3*U v)|+WQ|fbcc+=O S\XQ.K*/ _ E*K_^~_rέKc=WUGxOҧ9]Ke鲒$/֭<lXSQi}]S=όS|V~џZqGyJ͇\?$_UeJ]wۯ%_jU-9/SU뼜Uu>e.՞?/Gws*2U֦?}\N}#G|=zwu%}s>y2&o-6 bٞ0@oCQ>?בLƿxH9]P&"TP}>`Jdwb+|b?*ն+響-kyܼj1\΅ j7+;\$\>sVipհl &5C8 Nom6[#=H!I|!䞑HD\lLʷ[82_ml6,+w|KIgVmJaVF=s$Vo@h|VFa֭ܭ+}aаN-$dY򊭻}-e뽎\$H&OnAYՅ;X6+X);ݸl:v4e@Q?ls˸n Wbxs^Yc<˥ǟQxÖeEKR_CGg Yh1;vLR26uؾHL.7qh,1XVO|6mʓÆr9y} ydٓWL#0(SNJޘCiaگΰ*K>_o Cp=x1\X1g ڶnͱyhZ1_\gv_EEueT6jCd} )' ?e߾e{ihJ,%>-qm擗[LVV6O#AY>'7ϗl6矙[|~h԰Ͼ,Xxq˘ dʫG3sdxhѢyGOzu;!_yW'OAՓBʬקc2|Ohh(V|FZ9wo~S#GЬInyzŋ.<ʼn'HHH`?1lag%tcFs.f~P{}z:wPXd!KZ_{miO=btɞcGs1ry7n=vt:^um_^rsrh" [ z?H>a+½fؓ#y(yyԓ$%%c2Y1X׫S;xdYfy4o֔NǤSy1 aasЃppGw}@&Mؿ'o/>3g0QN]?7Ȁq*NJ@iޜ1?HH ((ysfSTTczZ-N睷 0 ^۸>/<AAA 6YoALL o~\$Y"22_BZ'Ld0ɧfl6s?s? $8k\hk$\}Ϗ9Kg ^}0Ν?M?Ѓp8*ɓhղO?9_0~Qa> C"#GѮMk/=x0ڴEDD[يi ;005X,|:թñҹ2. >~tV_]۶HD`@))ԩU0F#mZSH@P>D|\~Kqq1VKTDDz}^2 FV%&: ¹Iԯ]p:A^2GJsaIؽB_=;w@UUE!95mʤL: zPUUQ=KNU^^}/'OV[ʡ#Ghۦ $Q^]~ |ul6z Yl̙3WO`5(((ٳtI }Twqoy:=@xzL5Q=z DѐX MF: !;0 Tӿ'gZ=e>x x *(*Dsyl6{oA«:j}ڶnE~w=Wƍ%'7nš.GP58 z=,{u]tŗZ^N/[~iu K2ZPRRSBjJ$$8[I1y&6~ܶ-ZT}BLt'Nfb 4eצDiv4S-oyV_׏H_Vف(޻7t4y*oLZ iW7JTrj*˿\ɔI),4 $Unحpl~*z]8V;+oY Ĺ0i :^FCZy烹ܽ3{h2qmع{r^/.N&NʩSnzkYl߱ڴiFa' {~i"kljiܰ+Vaگ)*2{ =FAAiݚUkְg~Yk^j޻",,O,ȑ#lںb\~9x0tzO#?>[SN͞{'mԙ3ȲTNb:ڃt~ ,,Tt:l{W_𑣼2y 뮻ϖ.# q."&Pb<ɼ \gdD۰Ju*DqIeuGvv6 |yղ^"NO>Ŭo ]`h5bbb\ovr=occc%22q Pll12^;ЬYSZ4o` ,,̳+߸_fλxN4q b h\oCBQLxxJTTl\(HDDDOeSVd`0h뉈@$)))!:* FCVFpMFF*Iaaa9 QTsH~4.'''H@ ݎj#88@xs|uu:QT0L& 22Y&,, VK~A& 33HTU%;;XfAxx8f,VWVSDxx$223=}>vٹXJ,V9 |ŕ[DzzG$UI/J`` YY^豏h-q5%rv^~>^Gsf)@,3z:+-o#/?;;%KT~_999̙3\z-ϿEwptR{.+VyfϿG.\Xw ,`U҇?A a߾};3fЩS'wNJJvN8QۣX,6mNY'*;wDUUN7n%[FF "++]ff&&''JeKA V%((^z~z^uz-F_wǎ_dܸqpIN'YYYL0QFzjOI&1c FΝ;+Ȱl27o^eykz)/^(deeaY`v oYd 3f̠g97lٲ8GvXrUVѸqcɓ8s= /@ZZ~-#GdݺuW@$I OaG{Ef{B"WC)ҥ"^"ED H (E"4ׂLwgGe"c 2sL}>}:;wfԩiEQ,qppAc?~3g`458wv]\\xg>|8&M"//j93EEE߿ ]veǎSTT7|Cxxh`0o3yd:uĴi K.Ynݺq?Krs;p$[շ\>PQFZxƏOTTdDGGuuuEQV\I^^> ~1f\yFFZw Zl+o6 .D_ɢc20 W\矯pZj=իWrJFUC LJ={ryTUEQ-w ̧1|(nܔ4/ʶO7cs5k|@RR}||G Y9}4瞣HKL&$a2KTTY+gfӦM̚5q@hh($ѨQ#x is-V)~Ȳys7q2&EC'U:oUU(^z%^z%t¤IPUX-"9s&8pz9[nQzu**̙3t☇?86m";;/=zo)**bҥ9EQ7o|Wڞ=׮]d2~܏?Suűw?>LvvEdXx1,_<>#~IƆf0Yml6*w!!!̞=qw߱g<<<CӦM1b۷o`0YbfìY@;t7>}:mڴzt<#c,..ѣ4k.Ndž vSN%==ɓ'Dj7n5GLJwyǣhРvmmmy(..7j9xڴiSaRV-֭K``f666zׯ `0i&/^̎;~/TUU%-- EQHMMJwPZy|q[K.t&';6;;O>#Gs8pT?{T?^U/vرgg?̓6nnnڵ@pD@ @ PAl(wa{.f5 z[;@ 襣_5n@pE:IBnU4KDssv5ug&-9PM}hΞoUBGGgjz?5ϧZu|Uc/;L&P:U2$!IUya+~"r;U W]؈!DT F @ -*R"X9#þx@8n@ H&7$CZ1Jsקa[D@1vO{_:dggWi KܺuK]l@ x0MjٳgeqlDGGzʒƪU7o'O'033yi9| {Z8`???pvvޞ䐗^(4L$''*feeQPP; rss)**B$)))a׮][.СC}$ ш+...iѧ666HEllJֶꨜrssemDɄ5{1899iaQqdذaxzzn:\\\(((-{/[$_ɓ'ѣGϯ\޽{III!''mǜ9sA3o<:ıc ?""cǎɬYPta=.ZPzuǥKhٲEŋ$../ZLSej(tޝN>>1l0^y,X`o? JJP]v%??h '!!(C+BӦMٸq#&\$Inݺl޼4F#111PPP%KXhi^rHQQ888`4KLU->vX֭[hKJJ:ʦIL&ر_|k׮qfϞMhh3͚5ѣNۛ׳fƏEIM^|/#NǙ3:u*tGGG^}Uׯ=z 00 |IPNǮ]֭5j`ܸqԪUKKM1I [s?m۶%::8t:f"<<_gylHLL"T={6 cZ=Fbwpp`4lؐӱcG$##uF||}#=z(׿2e cǎtޝ˗dfΜ'|#KN0`=zd21~x"""ͭ`kew,aÆV?KKKX8U٘DGG[m ?ewzWƸqkիWILLKo~;R1* +W 66@&NȆ Сk7ۛÇsNԩ-.,,dܹ1rHԩ?|r ={t:ΝKnn.>,[M 7o4n(qYTUđ,X?Lhh(={رclݺz-z=/QF 6 [[[;oߎ+o@Ҵ4x {~Wj߅yK.p>C ܹs3g֮]Kݵ322k׮L6dm>$I[\~]*aҤIcQq39rjժYy摕رc6m%**-[bkkKTT_|...L4 %KʢcǎWq`m˲d._Ww}kY^g&Mݝ+Wryڶmˀd޽>}Y7nǎcӱcG5kF^^׶eYǹPU%K?pyZh. )))!993g}v=JLL z`>S [[[E᭷b̘1QXXhjժEj0䩧b޼yԬY[[[ᘗ|Ϙ1ƍ3fzMpp0$;i&5U=777&LiӈÃ3fm6ȑ#Yn$4 :T*v͚5ܹsiQ(kN~8w-"I[e={sҰaC^~e\\\*=^ؿJqԩ\rEرc<ӥKI ^zٓG}ڵk3}t?N`` ժUd21k,5jdѾfEP9s0g y*\`^g,YȻرciҤ >>>?~'N0m4֮]K߾}Yl<«HGX777ǖ-[ AHll,k׮LJ~Liݺ5M6%((Yfg۶m#66iذ!~~~3k׮Uj_zz:AAA4oޜjժb._̑#G?~,M]m3.]"99k.\>jޒKN0bUFZZȸ1UU),,x dRSSiРW8qFít֏4jȢdYQceyek3x`>srss f6mڄ,W5jXYvvvq!==Y$k6?<9sÇ3h ´_,SXXh} A+s޵UV/{ML&-~G'+MaKDJj*,3f;L&M4!<3}YtW_}E6mpvvgϞlٲSNSF s,Zo .pEZh9&MXʮ9uziذ!Vu֌3FGygҮ];\\\eÆ رիӱcGٿ?qԯ_5kV֭KΝ+o4'8z(ԯ_z,5kҼys+ZnuOСmڴL6m PԮ];n޼c=;&Ǘsakk[%ODL&uvu,_nw/xpOw8G ЦFݾ@GK@8l@ -/a޵)Jɷ}gqSNGŮ],ݍv+Wp*K$$IE׮]So7z k'_{*vܩ+˲ 0N۩(A9 j֬y[;<3_@gD .]0h $Iִ>S1Ltޝ'xdYŋtޝ6miQᩧbXYf˛%IТoڵkyRI{222otڕ-[ӻwo^|Em~H ի|ZeuIdgg4iF7oN>}$Gмysh֬2k,K޽+3YLFFFqXt)mڴޞJ9s }[ntl^^Ӈ Kˎϖ-[gaa!aaaۗ)Sh7>b-;NYYYڵh^yk+~厦t:N:ʹix'H[[zloo,$&&ɞ={ᥗ^Be^xkxiذ!קEO> 2228~8Ghޞ`ggСCcǎL>3gΰ`M,ӻwo6l@͚5y7wzݻwSXX3}uV8q"{ۛ5k֐*P.]JFFÆ cǎDEEƵkضmG`00rHÉ'ؼy3aaaY8ydq:t(#GDe6lH=*/m6fΜ_|AQQڵĉ̜9+Vɓ'-"]kTƍi&&OL͵/;v믿O?0NgΜ9tԉ˗@ "(7fՋ7nTcbbӧ,d"..vၽ=!!!OZ/Ӳ IDATeKϟOII 8::RXXʕ+U=X;;w+W2b>SM^f͚@ݺu5=ƍ#2?#O?4:NKS}zJs@?EQth|~eF1cׯ_'%%lll*_Fѹsg^~xzz;[ 0rpp͍W^yE[6 dQޞK.i&֬YCnn&9ZNYiԨ&ISܳVw~ؽ{7< GֶR%EEQ[.,^XSL&z!.;>GfѢEƳUVxzzVAL:^{-[T:NFQ2/TE-:8tɘL&L0AS>{9x xxxиqc֯_OTT<#Yj?3ǎQFHD5 m۶p… 7hЀ}q%غu+wZ~611Q;( 5cǎ8qSRRBÆ zmmm-vxx׹tipfRSSaݺu>}{O %IbժUt֍cǢ( = 춢ϲ^xYرcDFF2k,E!==ooo>3Sݻɓ'),,$33>+WбcG2/iٲt={0c hݺ5Vǣ,ȑ#Z?WO666ܹsbccÔ)Sx"Nw@ "RQ I&@΄t:dY&!!e˖Ѿ}{:t@^(((ѣL8Y9~8SN N8yv~7:u*qqqL4WVh ;;;|}}9~8ˋW^ywNڵ ٙM6oȂ (..&))wyӹs ^GGGjԨ+/YYYt:4i¤I0acƌ"XGGG +=͛7N~~'vڄL.]z*[ne8::2b ȰdqUU 6n:<==_Xr% O$?p%6nHNN.\`ӻwo}]w9V6mh p&Lt:._LݺuczxիE[W_}Epp0_~% h4pBh۶ooVS y*/̩ 777t:gϞQEe9s&j/ʧV9r$#;;7o2tP~eƆ|`II aaaTVxx۵kW_ccc4?B?ɲL\\jJH (&vJ~~>`ooOBBYYY(BӦMٸq#&\$Inݺl޼4M.88آcDz~n݊(IPPk.uF57n f͚ō7طo۷oפN}-p)f̘a~XX~~~?+WҬY3:w$IL8nݺEf}1+-[pNTV ~:wzV\- N  bڴibmw%r&K -))O>ٳ綾jnkzPY #CZ"1;;Bvn0s쁸c%@pwoX/ھ@O*RR D-ᰡTo*+ճz?q/qM~ތ3w@ 6Gu8kX[x?a0e'|6n;wM7* +W 66@&NȆ Сk7ۛÇsNԩ-.,,dܹ1rHԩվg< 6L[zn<Ν(|̜9?[š5k,X`?3ٓtoh۶sR\\\5;ٻw/OFex a٨ɓE/K,xyyѵkW9tH5!&(U |vXxqvعs'+VÃE裏jYYY_@pGتƾ}pwwgȐ!V鈍eڵOaiݺ5M6JWٳ>}о}{nJ||<999X͛<<kfioQQ/_&55(EAeN)ޙ)_ޚEEE 2z￯@fϞ` ))I{bꫯ>|8ׯ_hsڴiϛokYȠgϞQTT?"##vnܸO?Dzz:G֤P~aM/o߾"Ԯ]kג-$I˯EQI(..?t:RAĘ1c4'eM|̚Rn] {(,,d|SFJϹ|ys*cVWWW}Y-[ƪUPUU;SkMʾTuuu.+U̺&cʛoҥKYbvc޽@ $IgR=PXXHDD&&MNrr2͍ ֬YÉ'HJJbʔ)t]s:999|8'O`ԩ0n8 5;SYnwfڵI&QZ5yƏNAZ|hoˋG`}]-MFÆ E:D oği$%%b fΜyGF#!!!9sFt@7p7ğpZۨjUw8rNNN\z@pc#b'#77y@8l]B˹*:P xO@ u|}}DZ=}!%%+Whm:t vRVeqpp@׋+R TÖt A6l$>S1Ltޝ'xdYŋtޝ6mh˦,]6m`ooODD@={ӫW/>Ct:Æ `GFFPXHHdҤIFn޼ ?l*_^$v޼yp$I=z3gзo_uƉ'R`Cpp0666Yaaaۗ)S*͛7',,f͚i"cǎ^g%$$?dѣ1c5k… W"#KGNǩS6mO<񄦡NYILLӓ={K/,˼ |888ӰaCmСC9r$,ӰaCzAFFz BTT :Ԫz'OVwĉݻooo֬YC꯫7ӉV;gk)2,] ^|Em̙3/(**]v8q3gb BBB8yVoe͚5ɓ'Ӽys $(ܸv۶mcѸҮ];8z(gΜAm'AAA\zU imT3|zō7*u111Yqwwd2Gvޞ~'\ff&#GW^dTs:t耧'ΎP {b1XAh7)[N^dddp"##9r$&LEJtt4-[De|||,,3 |hYqvveȐ!L2T-]cv$''#me~Z x"lPquq!22$Ǒ#GtZ4QU5krizRgo4QNGNN@Tho^zLJ9{,FQ[ 2aaoTT+j 0._~<39rDEݺu `Ś=&z*˷_SzuRRRJ_Tnڴ5k֐ˢE*jOc@ x"lYǡCHNNd2aooO˖-?>Of„ 駟{xxxAƍY~=QQQ㏗AllhӦ VbӦMzL&zhڴ)k׮o?DUUZjeaN… :991h ^u.]DDDyyyjuK򞞞jN,[>99F… |2_|,3p@̙3g4hED_=777K/Dll,}[ӧO{iK_ UU5۱|y@ppGZ"cNSQ3QU\]]QU$́ʢ[[[TUDJJ c撛,t:<==tVQEKHFII YYYB-t:|||0LTXRPPP5;˗ONN:)))(F;;;prr=@:zt:^^^cgg3Bξ-ʖwC4y@ X( a p@ EݨǼCHII& q a8Jz&NHff}9?~\;--UV+T h)=씔 6663hF999J$''*kAƾYyuIHOONGZZꜜpww`0Oaa!xxxh/oN& }EQMW[䓒Mi^N ίIOOGe /y-IׯɓЄfΜɕ+Wػw/)))жm[3g6z޼y:tcǎjQDDǎ#22Yf(J̞=s)6oތ,̚5ŽT+ػgf̘AZZ[&;;tlVޚqqq,X@#mvY!IX؛Opp0GJSf_|rr2˗/z--[@i|^ڼkN֭[Jrr2zW@ R"u:Μ9ԩSqvvk׮8::ꫯH~ѣdddO)ڵnݺQF ƍGZ$Ii#$ҡCBBBؼy3-W_% @#44SN19HIDATc ^'''&NHٻw/[duI~~~vZ?hŋӠA6mSNˋ~QTTرcr^FaggǪU/qpp`ڵܡ)L61*NjOst,l߱gҮ(BXP7nJVv6Sfk%~ZDDR\6k(JII 4jbeOw7F1˗A0|]PUJJ L&PB'r9t:]AKˮ~aN<Re EQ+`ȳ0}\{"J~PQuQ(<ӿ?L#wlaRLȒ 6|Dϒw"p3[ 9_[S& |T>Z[L0\]طm ;%66*n f5!R$L~IQU8_{4qw,s,W7ߌ0:ݬLE&i/*Ŋ+M%~r/sSog H4pTQgss6$v6{ͤ8sGw̔$<. ΰ]q!0&z9ȇeZp#ZPnëXJMVGnݓW闗?tz2V0ҼJO`yh݇޼~ĒB4כVE]f&tG̔ʆV,qN7y|_j߿<:i2h:.'c j#[p*H%dkg$Id&UF7ϰLA(ŵnq*"vZփ\L+#W.=0Y@YfBq~ؾi^hLTou[:|&8fs)BUkkî _:lQ}Po l3S}W8VF1|L'tK%%6M@l 7'J5~1IENDB`paperwork-2.1.1/doc/install.archlinux.markdown000066400000000000000000000022361417573700700215160ustar00rootroot00000000000000# Paperwork installation on GNU/Linux ArchLinux ## Packages This is the recommended method of installation. A package is available in [AUR](https://www.archlinux.org/packages/community/any/paperwork/). Once installed, you can run `paperwork-cli chkdeps` and `paperwork-gtk chkdeps` to make sure all the required depencies are installed. You can start Paperwork with the command `paperwork-gtk`. ## Flatpak You can get more up-to-date versions of Paperwork [using Flatpak](install.flatpak.markdown). Just beware that those versions of Paperwork come directly from Paperwork developers themselves and haven't been reviewed by the ArchLinux package maintainer(s). ## Reporting a bug If you find a bug in the version of Paperwork packaged in GNU/Linux ArchLinux: - First try to reproduce it with the version of Paperwork in Flatpak. - If you can reproduce it with the Flatpak version, please [report it on Paperwork bug tracker](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/-/issues) - If you can't reproduce it with the Flatpak version, please [report it to the ArchLinux package maintainer(s)](https://wiki.archlinux.org/index.php/Bug_reporting_guidelines) paperwork-2.1.1/doc/install.debian.markdown000066400000000000000000000030541417573700700207420ustar00rootroot00000000000000# Paperwork installation on GNU/Linux Debian or GNU/Linux Ubuntu ## Packages This is the recommended method of installation. A package is [available in GNU/Linux Debian](https://packages.debian.org/search?keywords=paperwork-gtk&searchon=names&suite=all§ion=all). Since GNU/Linux Ubuntu is based on GNU/Linux Debian, [Paperwork is also available in it](https://packages.ubuntu.com/search?keywords=paperwork-gtk&searchon=names&suite=all§ion=all). ```sh # For example: sudo apt install paperwork-gtk paperwork-gtk-l10n-fr ``` Once installed, you can run `paperwork-cli chkdeps` and `paperwork-gtk chkdeps` to make sure all the required depencies are installed. You can start Paperwork with the command `paperwork-gtk`. ## Flatpak If packages are not yet available for your version of GNU/Linux Debian/Ubuntu/Mint/… or if you want more up-to-date versions of Paperwork, you can install it [using Flatpak](install.flatpak.markdown). Just beware that those versions of Paperwork come directly from Paperwork developers themselves and haven't been reviewed by the Debian package maintainer(s). ## Reporting a bug If you find a bug in the version of Paperwork packaged in GNU/Linux Debian: - First try to reproduce it with the version of Paperwork in Flatpak. - If you can reproduce it with the Flatpak version, please [report it on Paperwork bug tracker](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/-/issues) - If you can't reproduce it with the Flatpak version, please [report it to the Debian package maintainer(s)](https://www.debian.org/Bugs/) paperwork-2.1.1/doc/install.devel.markdown000066400000000000000000000140521417573700700206170ustar00rootroot00000000000000## Contributing Please read [CONTRIBUTING.md](CONTRIBUTING.md) carefully before submitting any merge request. In this document, it is assumed you are already familiar with the basics of Git. ### Paperwork system-wide Those instructions are useful if you just want to install Paperwork and don't plan on contributing. #### Installation ```sh sudo apt install \ python3-virtualenv virtualenv python3-dev \ gettext \ git \ make \ meson \ build-essential \ libsane-dev \ libgirepository1.0-dev gobject-introspection \ python3-gi \ valac \ gtk-doc-tools cd /tmp git clone git@gitlab.gnome.org:World/OpenPaperwork/paperwork.git cd paperwork # switch to the stable branch ('master') # other possible branches are 'testing' and 'develop' git checkout master sudo make install cd - rm -rf paperwork # Install missing dependencies paperwork-gtk chkdeps # Add icon to menus (you may have to log out and log back in to see them) sudo paperwork-gtk install ``` Later, if you want to update Paperwork, uninstall it (see below) and reinstall it (see above). #### Uninstallation ```sh cd /tmp git clone git@gitlab.gnome.org:World/OpenPaperwork/paperwork.git cd paperwork sudo make uninstall cd - rm -rf paperwork ``` Uninstallation never deletes your work directory nor your documents. Uninstallation doesn't delete Paperwork icons. ## Paperwork in a Virtualenv This is the recommended approach for development. If you intend to work on Paperwork (or just try it), this is probably the most convenient way to install safely a development version of Paperwork. Virtualenv allows to run Paperwork in a specific environment, with the latest versions of most of its dependencies. It also make it easier to remove it (you just have to delete the directory containing the virtualenv). However the user that did the installation will be the only one able to run Paperwork. No shortcut will be installed in the menus of your window manager. Paperwork won't be available directly on your PATH. Paperwork depends on various libraries. Development should be done against the latest versions of the OpenPaper.work's librairies (Libinsane, Libpillowfight, PyOCR, etc). To make things simpler, Paperwork repository includes a script to create and load a Python virtualenv (`source ./activate_test_env.sh`). It will automatically get OpenPaper.work's librairies fresh from Git and install them in this virtualenv. Then the command `make install` will install Paperwork components using Python setuptools. Python setuptools will automatically fetch and install pure-Python dependencies. Finally, `paperwork-cli chkdeps` and `paperwork-gtk chkdeps` will take care of installing the remaining dependencies system-wide using the package manager of your Linux distribution (APT, Dnf, Pacman, etc). ### Requirements You will have to install [python3-virtualenv](https://pypi.python.org/pypi/virtualenv): ```sh sudo apt install python3-virtualenv virtualenv python3-dev ``` [Libinsane](https://gitlab.gnome.org/World/OpenPaperwork/libinsane/-/blob/master/README.markdown) will also be built. This build requires various dependencies: ```sh sudo apt install \ gettext \ make \ meson \ build-essential \ libsane-dev \ libgirepository1.0-dev gobject-introspection \ python3-gi \ valac \ gtk-doc-tools ``` Paperwork requires some data files (documentation, etc) that require a lot of dependencies and time to build. By default, if you don't have those data files, Makefiles and scripts will download the latest version made by the CI/CD builds. If you want to generate the data file (documentation, etc) yourself, you are going to need a whole lot of other dependencies: ```sh sudo apt install \ imagemagick \ po4a \ texlive \ texlive-lang-english \ texlive-lang-french \ texlive-lang-german \ texlive-latex-extra \ texlive-latex-recommended \ xvfb ``` ### Setting up a virtualenv ```sh mkdir -p ~/git cd ~/git git clone https://gitlab.gnome.org/World/OpenPaperwork/paperwork.git cd paperwork # - 'develop' is the branch where any new features, bug fixes, etc should go by # default. # - 'testing' is only for bug fixes, translations and documentations during # the testing phase *only* # - 'master' is the latest stable version of Paperwork + some bug fixes (only # the project maintainer add commits in this branch). git checkout develop # Delete existing Python virtualenv if there is one. # Delete all Paperwork data files if they have been generated or downloaded. make clean # Will create the Python virtualenv if it doesn't exist. # It will compile Libinsane and some other librairies. It will then set the # correct environment variables to use them without installing them # system-wide. source ./activate_test_env.sh # you're now in the virtualenv # 'make install' will install Paperwork in the virtualenv make install # takes care of the dependencies that cannot be installed in the virtualenv # (Gtk, Tesseract, etc) paperwork-cli chkdeps paperwork-gtk chkdeps ``` ### Running Paperwork from the virtualenv ```sh cd ~/git/paperwork source ./activate_test_env.sh # you're now in a virtualenv # Running Paperwork paperwork-gtk # or paperwork-cli --help # or paperwork-json --help ``` ### Updating ```sh cd ~/git/paperwork git pull make clean git submodule update --recursive --remote source ./activate_test_env.sh # you're now in a virtualenv make install ``` ### Generating data files Data files are: - Documentation files (.tex turned into .pdf) - Screenshots (taken with xvfb-run + a script ; used in the documentation files) - Icons at various sizes To generate the screnshots, [the script](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/-/blob/develop/paperwork-gtk/src/paperwork_gtk/model/help/screenshot.sh) will automatically fetch test documents from here: https://gitlab.gnome.org/World/OpenPaperwork/paperwork-test-documents ```sh cd ~/git/paperwork source ./activate_test_env.sh make install make data ``` paperwork-2.1.1/doc/install.fedora.markdown000066400000000000000000000022451417573700700207610ustar00rootroot00000000000000# Paperwork installation on GNU/Linux Fedora ## Packages This is the recommended method of installation. A package is available in [Fedora](https://apps.fedoraproject.org/packages/). ```sh sudo dnf install paperwork ``` Once installed, you can run `paperwork-cli chkdeps` and `paperwork-gtk chkdeps` to make sure all the required depencies are installed. You can start Paperwork with the command `paperwork-gtk`. ## Flatpak If you want more up-to-date versions of Paperwork, you can install it [using Flatpak](install.flatpak.markdown). Just beware that those versions of Paperwork come directly from Paperwork developers themselves and haven't been reviewed by the Fedora package maintainer(s). ## Reporting a bug If you find a bug in the version of Paperwork packaged in GNU/Linux Fedora: - First try to reproduce it with the version of Paperwork in Flatpak. - If you can reproduce it with the Flatpak version, please [report it on Paperwork bug tracker](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/-/issues) - If you can't reproduce it with the Flatpak version, please [report it to the Fedora package maintainer(s)](https://fedoraproject.org/wiki/Bugzilla) paperwork-2.1.1/doc/install.flatpak.markdown000066400000000000000000000172741417573700700211530ustar00rootroot00000000000000# Introduction Flatpak is a package manager for Linux. It is available on all major GNU/Linux distributions. It also keeps applications and all their dependencies inside containers, making them easy to update and uninstall. Its advantages: - You get the latest version of Paperwork, directly from its developers. - Paperwork remains nicely packaged. It won't make a mess on your system. Its drawback: - Using Flatpak, Paperwork comes directly from its developers. It has not been reviewed by your distribution maintainers. It may not include some changes that your distribution maintainers would have added. # Quick start ## Installing Flatpak * GNU/Linux Debian: `sudo apt install flatpak` * GNU/Linux Fedora: `sudo dnf install flatpak` ## Installing Paperwork ```sh # Install Paperwork (for the current user only) # can be: # - 'master': stable branch (latest release + some bug fixes) # - 'testing': stabilization branch # - 'develop': development branch (new untested features) flatpak --user install https://builder.openpaper.work/paperwork_.flatpakref # For example: flatpak --user install https://builder.openpaper.work/paperwork_master.flatpakref ``` Flatpak will add a Paperwork icon in your menus. You may have to log out and log back in to see it. Alternatively, you can start Paperwork from a terminal: ```sh flatpak run work.openpaper.Paperwork ``` ## Allowing Paperwork to access scanners IMPORTANT: Paperwork in Flatpak uses Saned to access scanners, and Saned gives access only to local scanners (non-network scanner). If you want to use a network scanner, you will have to install Paperwork from your Linux distribution packages or [from sources](install.devel.markdown). When installed using Flatpak, Paperwork runs in a container. This container prevents Paperwork from accessing devices directly. Therefore the scanning daemon [Saned](https://linux.die.net/man/1/saned) must be enabled on the host system, and connection must be allowed from 127.0.0.1. Instructions can be found in the settings of Paperwork: ![Flatpak + Saned instructions: Step 1](flatpak_saned_1.png) ![Flatpak + Saned instructions: Step 2](flatpak_saned_2.png) ![Flatpak + Saned instructions: Step 3](flatpak_saned_3.png) If after a reboot your scanner is still not found, please see the FAQ below. ## Updating Paperwork ```sh flatpak --user update work.openpaper.Paperwork ``` # FAQ ## Even after following the integrated instructions, my scanner is still not found For some scanners, extra work is required to make them available to Paperwork in Flatpak. You must add specific udev rules. For example, with a Canon Lide 30: ```console $ lsusb (...) Bus 003 Device 008: ID 04a9:220e Canon, Inc. CanoScan N1240U/LiDE 30 (...) ``` `04a9` is the vendor ID. `220e` is the product ID. The following command will add and enable the required udev rule. You must just change the idVendor and the idProduct in it. ```sh sudo sh -c "echo 'ATTRS{idVendor}==\"04a9\", ATTRS{idProduct}==\"220e\", MODE=\"0666\"' > /lib/udev/rules.d/10-my-scanner.rules" sudo systemctl restart udev ``` Then, you can either disconnect or reconnect your scanner, or reboot your computer. Beware the udev rule above is quite large and will allow any user on your computer to use your scanner. ## How to allow Paperwork to access files outside of your home directory ? When running with Flatpak, Paperwork runs in a container. By default, the only directory outside of the container that Paperwork can access is your home directory. You can allow access to other directories with the following command: ```sh flatpak override --user work.openpaper.Paperwork --filesystem= ``` For example: ```sh flatpak override --user work.openpaper.Paperwork --filesystem=/media/paul/raid ``` ## No text appears when rendering PDF files. What do I do ? If you run Paperwork from a terminal, you can see the message `some font thing has failed` every time you open a PDF file from Paperwork. This issue is related to fontconfig cache. To fix it: - Stop Paperwork - Run: `flatpak run --command=fc-cache work.openpaper.Paperwork -f` ## How do I run paperwork-cli / paperwork-json ? When using Flatpak, paperwork-cli is also available. Note that it will run inside Paperwork's container and cannot access files outside your home directory. ```sh flatpak run --command=paperwork-cli work.openpaper.Paperwork [args] flatpak run --command=paperwork-cli work.openpaper.Paperwork --help ``` Examples: ```sh flatpak run --command=paperwork-cli work.openpaper.Paperwork help import flatpak run --command=paperwork-cli work.openpaper.Paperwork -bq import ~/tmp/pdf ``` ## Installing support for additional languages By default, Flatpak installs support for English and the language of your system. If you want support for additional languages, you can use the following commands: ```sh flatpak config --user --set languages "en;fr;de" flatpak update --user ``` To get support for all the languages supported by Tesseract (OCR): ```sh flatpak install --reinstall --user work.openpaper.Paperwork.Locale ``` ## What about i386 and ARM architectures ? [Continous integration](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/pipelines) only builds Paperwork for amd64 (aka x86\_64). If you want to run Paperwork on i386 or ARM systems, you can use the [Flathub version](https://flathub.org/apps/details/work.openpaper.Paperwork). If you don't know what architecture your computer is based and if your computer has more than 2GB of RAM, it is probably compatible with amd64. ## How do I update The GPG public key of the Flatpak repository ? Paperwork repository GPG key expires every 2 years. When that happens, when you try updating Paperwork, you will get an output similar to the following one: ``` $ flatpak --user update Looking for updates… F: Error updating remote metadata for 'paperwork-origin': GPG signatures found, but none are in trusted keyring F: Warning: Treating remote fetch error as non-fatal since runtime/work.openpaper.Paperwork.Locale/x86_64/master is already installed: Unable to load summary from remote paperwork-origin: GPG signatures found, but none are in trusted keyring F: Warning: Can't find runtime/work.openpaper.Paperwork.Locale/x86_64/master metadata for dependencies: Unable to load metadata from remote paperwork-origin: summary fetch error: GPG signatures found, but none are in trusted keyring F: Warning: Treating remote fetch error as non-fatal since app/work.openpaper.Paperwork/x86_64/master is already installed: Unable to load summary from remote paperwork-origin: GPG signatures found, but none are in trusted keyring F: Warning: Can't find app/work.openpaper.Paperwork/x86_64/master metadata for dependencies: Unable to load metadata from remote paperwork-origin: summary fetch error: GPG signatures found, but none are in trusted keyring (...) Warning: org.freedesktop.Platform.openh264 needs a later flatpak version Error: GPG signatures found, but none are in trusted keyring Error: GPG signatures found, but none are in trusted keyring Changes complete. error: There were one or more errors ``` The simplest way to fix that is to reinstall Paperwork. Uninstalling Paperwork will never delete your documents. ``` flatpak --user remove work.openpaper.Paperwork flatpak --user remote-delete paperwork-origin flatpak --user install https://builder.openpaper.work/paperwork_master.flatpakref ``` # Build Here are the instructions if you want to try to build the Flatpak version yourself. ```sh git clone https://gitlab.gnome.org/World/OpenPaperwork/paperwork.git cd paperwork/flatpak flatpak --user remote-add --if-not-exists gnome https://sdk.gnome.org/gnome.flatpakrepo flatpak --user install gnome org.gnome.Sdk//3.26 flatpak --user install gnome org.gnome.Platform//3.26 make ``` paperwork-2.1.1/doc/install.gentoo.markdown000066400000000000000000000023071417573700700210130ustar00rootroot00000000000000# Paperwork installation on GNU/Linux Gentoo ## Packages This is the recommended method of installation. A package is [available in GNU/Linux Gentoo](https://packages.gentoo.org/packages/app-text/paperwork). ```sh sudo emerge paperwork ``` Once installed, you can run `paperwork-cli chkdeps` and `paperwork-gtk chkdeps` to make sure all the required depencies are installed. You can start Paperwork with the command `paperwork-gtk`. ## Flatpak If you want more up-to-date versions of Paperwork, you can install it [using Flatpak](install.flatpak.markdown). Just beware that those versions of Paperwork come directly from Paperwork developers themselves and haven't been reviewed by the Gentoo package maintainer(s). ## Reporting a bug If you find a bug in the version of Paperwork packaged in GNU/Linux Gentoo: - First try to reproduce it with the version of Paperwork in Flatpak. - If you can reproduce it with the Flatpak version, please [report it on Paperwork bug tracker](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/-/issues) - If you can't reproduce it with the Flatpak version, please [report it to the Gentoo package maintainer(s)](https://wiki.gentoo.org/wiki/Bugzilla/Bug_report_guide) paperwork-2.1.1/doc/uninstall.linux.markdown000066400000000000000000000006761417573700700212310ustar00rootroot00000000000000Uninstallation *won't* delete your work directory nor your documents. # Paperwork uninstallation using Flatpak If you installed Paperwork using Flatpak, you can uninstall it with: ```sh flatpak --user uninstall work.openpaper.Paperwork ``` # Paperwork uninstallation using virtual-env If you installed Paperwork using Python's virtualenv (`source ./active_test_env.sh`), you can simply delete the Git repository (`rm -rf ~/git/paperwork`). paperwork-2.1.1/flatpak/000077500000000000000000000000001417573700700151625ustar00rootroot00000000000000paperwork-2.1.1/flatpak/1.2.2.json000066400000000000000000000416141417573700700165230ustar00rootroot00000000000000{ "app-id": "work.openpaper.Paperwork", "branch": "1.2.2", "runtime": "org.gnome.Platform", "runtime-version": "3.26", "sdk": "org.gnome.Sdk", "command": "paperwork", "copy-icon": true, "finish-args": [ "--share=ipc", "--share=network", "--socket=x11", "--socket=wayland", "--filesystem=home", "--talk-name=org.freedesktop.Notifications", "--talk-name=org.freedesktop.DBus", "--socket=session-bus", "--own-name=work.openpaper.paperwork" ], "modules": [ "shared-modules/tesseract-3.05.01.json", "shared-modules/sane-backends-1.0.27.json", { "name": "python-six", "no-autogen": true, "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/b3/b2/238e2590826bfdd113244a40d9d3eb26918bd798fc187e2360a8367068db/six-1.10.0.tar.gz", "sha256": "105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-dateutil", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/51/fc/39a3fbde6864942e8bb24c93663734b74e281b984d1b8c4f95d64b0c21f6/python-dateutil-2.6.0.tar.gz", "sha256": "62a2f8df3d66f878373fd0072eacf4ee52194ba302e00082828e0d263b0418d2" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-Levenshtein", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/42/a9/d1785c85ebf9b7dfacd08938dd028209c34a0ea3b1bcdb895208bd40a67d/python-Levenshtein-0.12.0.tar.gz", "sha256": "033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pillow", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/93/73/66854f63b1941aad9af18a1de59f9cf95ad1a87c801540222e332f6688d7/Pillow-4.1.1.tar.gz", "sha256": "00b6a5f28d00f720235a937ebc2f50f4292a5c7e2d6ab9a8b26153b625c4f431" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" }, { "type": "patch", "path": "python-pillow-disable-multithreaded-compilation.diff", "strip-components": 0, "dest": ".", "use-git": false } ], "modules": [ { "name": "python-olefile", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/35/17/c15d41d5a8f8b98cc3df25eb00c5cee76193114c78e5674df6ef4ac92647/olefile-0.44.zip", "sha256": "61f2ca0cd0aa77279eb943c07f607438edf374096b66332fae1ee64a6f0f73ad" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] }, { "name": "python-pycountry", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/4b/51/9155a48faed108db64a0ff45227c752fda8126f3585475cef30b7abaa536/pycountry-17.1.8.tar.gz", "sha256": "c5ccad49e47caee92779bf83da81565159b1fe3d8f48b063068ac118b73dd1f8" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-nose", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/58/a5/0dc93c3ec33f4e281849523a5a913fa1eea9a3068acfa754d44d88107a44/nose-1.3.7.tar.gz", "sha256": "f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyinsane2", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/89/02/d516f6676ce668626275207e1bb4567404c900854157ea2d1b9b1f2bc2b6/pyinsane2-2.0.9.tar.gz", "sha256": "879cecc7679acac0129b5e3e297236197cbd6991a89faa6dadfae89ce10f8abc" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyocr", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/6b/5e/0eaa5c939426b0f6a51f9fc883a1d756ad54ac9568faab129440a1dbca24/pyocr-0.4.6.tar.gz", "sha256": "3626ea30ca3d52c8282da672692b216f28cca62fe0e5f97e58f57b7b1d38d56f" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pypillowfight", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/c6/89/a32d817e56314ca8c1532bce4553ba2ca8c93d75766bfd748730841f2cf5/pypillowfight-0.2.1.tar.gz", "sha256": "57bb003ff66979b9b3d5e1e32189f6cd23bb63f2a659f3b97c84ad05ba07992a" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyxdg", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/26/28/ee953bd2c030ae5a9e9a0ff68e5912bd90ee50ae766871151cd2572ca570/pyxdg-0.25.tar.gz", "sha256": "81e883e0b9517d624e8b0499eb267b82a815c0b7146d5269f364988ae031279d" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pydbus", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/58/56/3e84f2c1f2e39b9ea132460183f123af41e3b9c8befe222a35636baa6a5a/pydbus-0.6.0.tar.gz", "sha256": "4207162eff54223822c185da06c1ba8a34137a9602f3da5a528eedf3f78d0f2c" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-paperwork-backend", "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/05/36/08c1f5ff9ade9611be788455ed6cac50b3779353d0e8c5594b73df2afe57/paperwork-backend-1.2.2.tar.gz", "sha256": "2d7a957b33592c9bf3107103ed129c81188ca4a1c9052e38cbe583452f0fee0a" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ], "modules": [ { "name": "python-natsort", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/8e/6b/a4e3031e573ef29a251984ac0a6bd26cedac6f5e67a7607c9746bd64b3fe/natsort-5.0.3.tar.gz", "sha256": "d57b7a0156f16f49c6c010c9ce97e2125956697846f31bba7cd544cd24b007c1" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyenchant", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/73/73/49f95fe636ab3deed0ef1e3b9087902413bcdf74ec00298c3059e660cfbb/pyenchant-1.6.8.tar.gz", "sha256": "7ead2ee74f1a4fc2a7199b3d6012eaaaceea03fbcadcb5df67d2f9d0d51f050a" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-simplebayes", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/b9/73/764578df72934940d95a8941cbd374b56319562dda72630fc8bfeaefc350/simplebayes-1.5.8.tar.gz", "sha256": "363418c0ef185ac2158ebbd6d8afb45aa997254fcb809a73ed20a7d5dccf8b85" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-whoosh", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/25/2b/6beed2107b148edc1321da0d489afc4617b9ed317ef7b72d4993cad9b684/Whoosh-2.7.4.tar.gz", "sha256": "7ca5633dbfa9e0e0fa400d3151a8a0c4bec53bd2ecedc0a67705b17565c31a83" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-termcolor", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/8a/48/a76be51647d0eb9f10e2a4511bf3ffb8cc1e6b14e9e4fab46173aa79f981/termcolor-1.1.0.tar.gz", "sha256": "1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-setuptools", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/a9/23/720c7558ba6ad3e0f5ad01e0d6ea2288b486da32f053c73e259f7c392042/setuptools-36.0.1.zip", "sha256": "e17c4687fddd6d70a6604ac0ad25e33324cec71b5137267dd5c45e103c4b288a" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] }, { "name": "poppler-data", "buildsystem": "cmake", "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-data-0.4.7.tar.gz", "sha256": "e752b0d88a7aba54574152143e7bf76436a7ef51977c55d6bd9a48dccde3a7de" } ] }, { "name": "poppler", "buildsystem": "autotools", "config-opts": [ "--enable-libopenjpeg=none", "--enable-xpdf-headers" ], "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-0.55.0.tar.xz", "sha256": "537f2bc60d796525705ad9ca8e46899dcc99c2e9480b80051808bae265cdc658" } ] }, { "name": "python-paperwork", "make-install-args": ["prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "post-install": ["paperwork-shell install_system /app/share/icons /app/share"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/11/c9/7027fd39165666236faffa3a5a6ebc9d9c4732df36684b0c5e9b6a675e56/paperwork-1.2.2.tar.gz", "sha256": "18fe3bccd5f9ad16e920baa88985b7e9ba36858b83cdf43cb3591be59cbfc50f" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] } paperwork-2.1.1/flatpak/1.2.3.json000066400000000000000000000420501417573700700165170ustar00rootroot00000000000000{ "app-id": "work.openpaper.Paperwork", "branch": "1.2.3", "runtime": "org.gnome.Platform", "runtime-version": "3.26", "sdk": "org.gnome.Sdk", "command": "paperwork", "copy-icon": true, "finish-args": [ "--share=ipc", "--share=network", "--socket=x11", "--socket=wayland", "--filesystem=home", "--talk-name=org.freedesktop.Notifications", "--talk-name=org.freedesktop.DBus", "--socket=session-bus", "--own-name=work.openpaper.paperwork", "--filesystem=xdg-run/dconf", "--filesystem=~/.config/dconf:ro", "--talk-name=ca.desrt.dconf", "--env=DCONF_USER_CONFIG_DIR=.config/dconf" ], "modules": [ "shared-modules/tesseract-3.05.01.json", "shared-modules/sane-backends-1.0.27.json", { "name": "python-six", "no-autogen": true, "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/b3/b2/238e2590826bfdd113244a40d9d3eb26918bd798fc187e2360a8367068db/six-1.10.0.tar.gz", "sha256": "105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-dateutil", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/51/fc/39a3fbde6864942e8bb24c93663734b74e281b984d1b8c4f95d64b0c21f6/python-dateutil-2.6.0.tar.gz", "sha256": "62a2f8df3d66f878373fd0072eacf4ee52194ba302e00082828e0d263b0418d2" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-Levenshtein", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/42/a9/d1785c85ebf9b7dfacd08938dd028209c34a0ea3b1bcdb895208bd40a67d/python-Levenshtein-0.12.0.tar.gz", "sha256": "033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pillow", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/93/73/66854f63b1941aad9af18a1de59f9cf95ad1a87c801540222e332f6688d7/Pillow-4.1.1.tar.gz", "sha256": "00b6a5f28d00f720235a937ebc2f50f4292a5c7e2d6ab9a8b26153b625c4f431" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" }, { "type": "patch", "path": "python-pillow-disable-multithreaded-compilation.diff", "strip-components": 0, "dest": ".", "use-git": false } ], "modules": [ { "name": "python-olefile", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/35/17/c15d41d5a8f8b98cc3df25eb00c5cee76193114c78e5674df6ef4ac92647/olefile-0.44.zip", "sha256": "61f2ca0cd0aa77279eb943c07f607438edf374096b66332fae1ee64a6f0f73ad" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] }, { "name": "python-pycountry", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/4b/51/9155a48faed108db64a0ff45227c752fda8126f3585475cef30b7abaa536/pycountry-17.1.8.tar.gz", "sha256": "c5ccad49e47caee92779bf83da81565159b1fe3d8f48b063068ac118b73dd1f8" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-nose", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/58/a5/0dc93c3ec33f4e281849523a5a913fa1eea9a3068acfa754d44d88107a44/nose-1.3.7.tar.gz", "sha256": "f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyinsane2", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/89/02/d516f6676ce668626275207e1bb4567404c900854157ea2d1b9b1f2bc2b6/pyinsane2-2.0.9.tar.gz", "sha256": "879cecc7679acac0129b5e3e297236197cbd6991a89faa6dadfae89ce10f8abc" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyocr", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/6b/5e/0eaa5c939426b0f6a51f9fc883a1d756ad54ac9568faab129440a1dbca24/pyocr-0.4.6.tar.gz", "sha256": "3626ea30ca3d52c8282da672692b216f28cca62fe0e5f97e58f57b7b1d38d56f" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pypillowfight", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/c6/89/a32d817e56314ca8c1532bce4553ba2ca8c93d75766bfd748730841f2cf5/pypillowfight-0.2.1.tar.gz", "sha256": "57bb003ff66979b9b3d5e1e32189f6cd23bb63f2a659f3b97c84ad05ba07992a" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyxdg", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/26/28/ee953bd2c030ae5a9e9a0ff68e5912bd90ee50ae766871151cd2572ca570/pyxdg-0.25.tar.gz", "sha256": "81e883e0b9517d624e8b0499eb267b82a815c0b7146d5269f364988ae031279d" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pydbus", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/58/56/3e84f2c1f2e39b9ea132460183f123af41e3b9c8befe222a35636baa6a5a/pydbus-0.6.0.tar.gz", "sha256": "4207162eff54223822c185da06c1ba8a34137a9602f3da5a528eedf3f78d0f2c" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-paperwork-backend", "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/3f/b9/8cae7c26a9a56a06fa3b1b3c5fc97f633a9a3f543a2a92f9a96edb7df8c0/paperwork-backend-1.2.3.tar.gz", "sha256": "b25aacb754f123465f77ebfbd68adf083025fbc3b240bf8f6cd4019dd9a91bd1" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ], "modules": [ { "name": "python-natsort", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/8e/6b/a4e3031e573ef29a251984ac0a6bd26cedac6f5e67a7607c9746bd64b3fe/natsort-5.0.3.tar.gz", "sha256": "d57b7a0156f16f49c6c010c9ce97e2125956697846f31bba7cd544cd24b007c1" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyenchant", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/73/73/49f95fe636ab3deed0ef1e3b9087902413bcdf74ec00298c3059e660cfbb/pyenchant-1.6.8.tar.gz", "sha256": "7ead2ee74f1a4fc2a7199b3d6012eaaaceea03fbcadcb5df67d2f9d0d51f050a" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-simplebayes", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/b9/73/764578df72934940d95a8941cbd374b56319562dda72630fc8bfeaefc350/simplebayes-1.5.8.tar.gz", "sha256": "363418c0ef185ac2158ebbd6d8afb45aa997254fcb809a73ed20a7d5dccf8b85" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-whoosh", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/25/2b/6beed2107b148edc1321da0d489afc4617b9ed317ef7b72d4993cad9b684/Whoosh-2.7.4.tar.gz", "sha256": "7ca5633dbfa9e0e0fa400d3151a8a0c4bec53bd2ecedc0a67705b17565c31a83" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-termcolor", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/8a/48/a76be51647d0eb9f10e2a4511bf3ffb8cc1e6b14e9e4fab46173aa79f981/termcolor-1.1.0.tar.gz", "sha256": "1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-setuptools", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/a9/23/720c7558ba6ad3e0f5ad01e0d6ea2288b486da32f053c73e259f7c392042/setuptools-36.0.1.zip", "sha256": "e17c4687fddd6d70a6604ac0ad25e33324cec71b5137267dd5c45e103c4b288a" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] }, { "name": "poppler-data", "buildsystem": "cmake", "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-data-0.4.7.tar.gz", "sha256": "e752b0d88a7aba54574152143e7bf76436a7ef51977c55d6bd9a48dccde3a7de" } ] }, { "name": "poppler", "buildsystem": "autotools", "config-opts": [ "--enable-libopenjpeg=none", "--enable-xpdf-headers" ], "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-0.55.0.tar.xz", "sha256": "537f2bc60d796525705ad9ca8e46899dcc99c2e9480b80051808bae265cdc658" } ] }, { "name": "python-paperwork", "make-install-args": ["prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "post-install": ["paperwork-shell install_system /app/share/icons /app/share"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/a2/4f/dbc66c0338307528a8ca95a875aeb5bbd12c353723fcd43852cebaa8c715/paperwork-1.2.3.tar.gz", "sha256": "0a715c4aa292d2da73f1a1840d59b628f4907553f30a8557f30041625bab7883" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] } paperwork-2.1.1/flatpak/1.2.4.json000066400000000000000000000416651417573700700165330ustar00rootroot00000000000000{ "app-id": "work.openpaper.Paperwork", "branch": "1.2.4", "runtime": "org.gnome.Platform", "runtime-version": "3.26", "sdk": "org.gnome.Sdk", "command": "paperwork", "copy-icon": true, "finish-args": [ "--share=ipc", "--share=network", "--socket=x11", "--socket=wayland", "--filesystem=home", "--talk-name=org.freedesktop.Notifications", "--own-name=work.openpaper.paperwork", "--filesystem=xdg-run/dconf", "--filesystem=~/.config/dconf:ro", "--talk-name=ca.desrt.dconf", "--env=DCONF_USER_CONFIG_DIR=.config/dconf" ], "modules": [ "shared-modules/tesseract-3.05.01.json", "shared-modules/sane-backends-1.0.27.json", { "name": "python-six", "no-autogen": true, "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/b3/b2/238e2590826bfdd113244a40d9d3eb26918bd798fc187e2360a8367068db/six-1.10.0.tar.gz", "sha256": "105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-dateutil", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/51/fc/39a3fbde6864942e8bb24c93663734b74e281b984d1b8c4f95d64b0c21f6/python-dateutil-2.6.0.tar.gz", "sha256": "62a2f8df3d66f878373fd0072eacf4ee52194ba302e00082828e0d263b0418d2" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-Levenshtein", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/42/a9/d1785c85ebf9b7dfacd08938dd028209c34a0ea3b1bcdb895208bd40a67d/python-Levenshtein-0.12.0.tar.gz", "sha256": "033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pillow", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/93/73/66854f63b1941aad9af18a1de59f9cf95ad1a87c801540222e332f6688d7/Pillow-4.1.1.tar.gz", "sha256": "00b6a5f28d00f720235a937ebc2f50f4292a5c7e2d6ab9a8b26153b625c4f431" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" }, { "type": "patch", "path": "python-pillow-disable-multithreaded-compilation.diff", "strip-components": 0, "dest": ".", "use-git": false } ], "modules": [ { "name": "python-olefile", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/35/17/c15d41d5a8f8b98cc3df25eb00c5cee76193114c78e5674df6ef4ac92647/olefile-0.44.zip", "sha256": "61f2ca0cd0aa77279eb943c07f607438edf374096b66332fae1ee64a6f0f73ad" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] }, { "name": "python-pycountry", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/4b/51/9155a48faed108db64a0ff45227c752fda8126f3585475cef30b7abaa536/pycountry-17.1.8.tar.gz", "sha256": "c5ccad49e47caee92779bf83da81565159b1fe3d8f48b063068ac118b73dd1f8" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-nose", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/58/a5/0dc93c3ec33f4e281849523a5a913fa1eea9a3068acfa754d44d88107a44/nose-1.3.7.tar.gz", "sha256": "f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyinsane2", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/89/02/d516f6676ce668626275207e1bb4567404c900854157ea2d1b9b1f2bc2b6/pyinsane2-2.0.9.tar.gz", "sha256": "879cecc7679acac0129b5e3e297236197cbd6991a89faa6dadfae89ce10f8abc" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyocr", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/08/ba/2f3a9f05db05826831b7df5d3c5b3b6d0705819414b2bb5ecfb10c737cf1/pyocr-0.5.1.tar.gz", "sha256": "9ee8b5f38dd966ca531115fc5fe4715f7fa8961a9f14cd5109c2d938c17a2043" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pypillowfight", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/fb/bf/1716de65f4ef463f9297c61426c0fcba8193a529074450babc407412ccd8/pypillowfight-0.2.3.tar.gz", "sha256": "83653eecf4d58278ab3e01c5188d9a2b33dcaf9603c613d3c8160b31e542d8c9" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyxdg", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/26/28/ee953bd2c030ae5a9e9a0ff68e5912bd90ee50ae766871151cd2572ca570/pyxdg-0.25.tar.gz", "sha256": "81e883e0b9517d624e8b0499eb267b82a815c0b7146d5269f364988ae031279d" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pydbus", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/58/56/3e84f2c1f2e39b9ea132460183f123af41e3b9c8befe222a35636baa6a5a/pydbus-0.6.0.tar.gz", "sha256": "4207162eff54223822c185da06c1ba8a34137a9602f3da5a528eedf3f78d0f2c" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-natsort", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/8e/6b/a4e3031e573ef29a251984ac0a6bd26cedac6f5e67a7607c9746bd64b3fe/natsort-5.0.3.tar.gz", "sha256": "d57b7a0156f16f49c6c010c9ce97e2125956697846f31bba7cd544cd24b007c1" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyenchant", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/73/73/49f95fe636ab3deed0ef1e3b9087902413bcdf74ec00298c3059e660cfbb/pyenchant-1.6.8.tar.gz", "sha256": "7ead2ee74f1a4fc2a7199b3d6012eaaaceea03fbcadcb5df67d2f9d0d51f050a" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-simplebayes", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/b9/73/764578df72934940d95a8941cbd374b56319562dda72630fc8bfeaefc350/simplebayes-1.5.8.tar.gz", "sha256": "363418c0ef185ac2158ebbd6d8afb45aa997254fcb809a73ed20a7d5dccf8b85" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-whoosh", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/25/2b/6beed2107b148edc1321da0d489afc4617b9ed317ef7b72d4993cad9b684/Whoosh-2.7.4.tar.gz", "sha256": "7ca5633dbfa9e0e0fa400d3151a8a0c4bec53bd2ecedc0a67705b17565c31a83" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-termcolor", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/8a/48/a76be51647d0eb9f10e2a4511bf3ffb8cc1e6b14e9e4fab46173aa79f981/termcolor-1.1.0.tar.gz", "sha256": "1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-setuptools", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/a9/23/720c7558ba6ad3e0f5ad01e0d6ea2288b486da32f053c73e259f7c392042/setuptools-36.0.1.zip", "sha256": "e17c4687fddd6d70a6604ac0ad25e33324cec71b5137267dd5c45e103c4b288a" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "poppler-data", "buildsystem": "cmake", "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-data-0.4.7.tar.gz", "sha256": "e752b0d88a7aba54574152143e7bf76436a7ef51977c55d6bd9a48dccde3a7de" } ] }, { "name": "poppler", "buildsystem": "autotools", "config-opts": [ "--enable-libopenjpeg=none", "--enable-xpdf-headers" ], "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-0.55.0.tar.xz", "sha256": "537f2bc60d796525705ad9ca8e46899dcc99c2e9480b80051808bae265cdc658" } ] }, { "name": "python-paperwork-backend", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/e6/3c/ba77e8c087e32e2ef1f34b2eff28f7ce0e3f04989e390bfad0a411530345/paperwork-backend-1.2.4.tar.gz", "sha256": "cfd54f739e1fb8d7acc5ae4f7ff484cb03be8b2c73be185ec3ea30dc1219c0bb" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-paperwork", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "post-install": ["paperwork-shell install_system /app/share/icons /app/share"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/e1/ee/83ed1ebe896ce4f135a204c8a632a80a931840603f2321ab76287f78db65/paperwork-1.2.4.tar.gz", "sha256": "9045d297076dfc219b62bb994ca46fff53b5a990b2f077946d279f366ae3ab4d" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] } paperwork-2.1.1/flatpak/1.3.0.json000066400000000000000000000432541417573700700165240ustar00rootroot00000000000000{ "app-id": "work.openpaper.Paperwork", "branch": "1.3.0", "runtime": "org.gnome.Platform", "runtime-version": "3.30", "sdk": "org.gnome.Sdk", "command": "paperwork", "copy-icon": true, "finish-args": [ "--share=ipc", "--share=network", "--socket=fallback-x11", "--socket=wayland", "--filesystem=home", "--persist=.python-eggs", "--talk-name=org.freedesktop.Notifications", "--talk-name=org.freedesktop.FileManager1", "--own-name=work.openpaper.paperwork", "--filesystem=xdg-run/dconf", "--filesystem=~/.config/dconf:ro", "--talk-name=ca.desrt.dconf", "--env=DCONF_USER_CONFIG_DIR=.config/dconf", "--device=all" ], "modules": [ "shared-modules/tesseract-4.0.0.json", "shared-modules/sane-backends-1.0.27.json", { "name": "python-setuptools", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/c2/f7/c7b501b783e5a74cf1768bc174ee4fb0a8a6ee5af6afa92274ff964703e0/setuptools-40.8.0.zip", "sha256": "6e4eec90337e849ade7103723b9a99631c1f0d19990d6e8412dc42f5ae8b304d" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-setuptools-scm", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/54/85/514ba3ca2a022bddd68819f187ae826986051d130ec5b972076e4f58a9f3/setuptools_scm-3.2.0.tar.gz", "sha256": "52ab47715fa0fc7d8e6cd15168d1a69ba995feb1505131c3e814eb7087b57358" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-setuptools-scm-git-archive", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/7e/2c/0c15b29a1b5940250bfdc4a4f53272e35cd7cf8a34159291b6b4ec9eb291/setuptools_scm_git_archive-1.1.tar.gz", "sha256": "6026f61089b73fa1b5ee737e95314f41cb512609b393530385ed281d0b46c062" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-distro", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/ca/e3/78443d739d7efeea86cbbe0216511d29b2f5ca8dbf51a6f2898432738987/distro-1.4.0.tar.gz", "sha256": "362dde65d846d23baee4b5c058c8586f219b5a54be1cf5fc6ff55c4578392f57" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-dateutil", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/ad/99/5b2e99737edeb28c71bcbec5b5dda19d0d9ef3ca3e92e3e925e7c0bb364c/python-dateutil-2.8.0.tar.gz", "sha256": "c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-Levenshtein", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/42/a9/d1785c85ebf9b7dfacd08938dd028209c34a0ea3b1bcdb895208bd40a67d/python-Levenshtein-0.12.0.tar.gz", "sha256": "033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pillow", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/3c/7e/443be24431324bd34d22dd9d11cc845d995bcd3b500676bcf23142756975/Pillow-5.4.1.tar.gz", "sha256": "5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ], "modules": [ { "name": "python-olefile", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/34/81/e1ac43c6b45b4c5f8d9352396a14144bba52c8fec72a80f425f6a4d653ad/olefile-0.46.zip", "sha256": "133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] }, { "name": "python-pycountry", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/87/c7/c2c76c3ae4ac79c74c1871ae775ed97b70d475dd90d1e824b1d2fc0cd54f/pycountry-18.12.8.tar.gz", "sha256": "8ec4020b2b15cd410893d573820d42ee12fe50365332e58c0975c953b60a16de" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-nose", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/58/a5/0dc93c3ec33f4e281849523a5a913fa1eea9a3068acfa754d44d88107a44/nose-1.3.7.tar.gz", "sha256": "f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "libinsane", "buildsystem": "meson", "sources": [ { "type": "git", "url": "https://gitlab.gnome.org/World/OpenPaperwork/libinsane.git", "tag": "1.0.1", "disable-shallow-clone": true } ] }, { "name": "python-pyocr", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/86/17/5fa0edc8da817a7da0198f03319850cb36cf2f20a38b6c7616fcb36211ef/pyocr-0.7.2.tar.gz", "sha256": "fa15adc7e1cf0d345a2990495fe125a947c6e09a60ddba0256a1c14b2e603179" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pypillowfight", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/90/70/575e3d04d581e04dccefd52cbb75e26aa07934147b2e85f3fa2896e61eed/pypillowfight-0.2.4.tar.gz", "sha256": "9208518494df900b8842b3d826c55ff673127634bdb2d2c85cca93b5017fd061" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyxdg", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/47/6e/311d5f22e2b76381719b5d0c6e9dc39cd33999adae67db71d7279a6d70f4/pyxdg-0.26.tar.gz", "sha256": "fe2928d3f532ed32b39c32a482b54136fe766d19936afc96c8f00645f9da1a06" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pydbus", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/58/56/3e84f2c1f2e39b9ea132460183f123af41e3b9c8befe222a35636baa6a5a/pydbus-0.6.0.tar.gz", "sha256": "4207162eff54223822c185da06c1ba8a34137a9602f3da5a528eedf3f78d0f2c" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-natsort", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/b3/5d/c0fbee4ed688fe2ed6533dd4a0124e1470d6692bc29e1da06bc0861ed4ab/natsort-6.0.0.tar.gz", "sha256": "ff3effb5618232866de8d26e5af4081a4daa9bb0dfed49ac65170e28e45f2776" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-simplebayes", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/b9/73/764578df72934940d95a8941cbd374b56319562dda72630fc8bfeaefc350/simplebayes-1.5.8.tar.gz", "sha256": "363418c0ef185ac2158ebbd6d8afb45aa997254fcb809a73ed20a7d5dccf8b85" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-whoosh", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/25/2b/6beed2107b148edc1321da0d489afc4617b9ed317ef7b72d4993cad9b684/Whoosh-2.7.4.tar.gz", "sha256": "7ca5633dbfa9e0e0fa400d3151a8a0c4bec53bd2ecedc0a67705b17565c31a83" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-termcolor", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/8a/48/a76be51647d0eb9f10e2a4511bf3ffb8cc1e6b14e9e4fab46173aa79f981/termcolor-1.1.0.tar.gz", "sha256": "1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "poppler-data", "buildsystem": "cmake-ninja", "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-data-0.4.9.tar.gz", "sha256": "1f9c7e7de9ecd0db6ab287349e31bf815ca108a5a175cf906a90163bdbe32012" } ] }, { "name": "poppler", "buildsystem": "cmake-ninja", "config-opts": [ "-DENABLE_LIBOPENJPEG:STRING=none" ], "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-0.74.0.tar.xz", "sha256": "92e09fd3302567fd36146b36bb707db43ce436e8841219025a82ea9fb0076b2f" } ] }, { "name": "python-paperwork-backend", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/38/55/3776279e0f725ab739ba09f6e7db241cde251eb79dcc5681415cf94a13f9/paperwork-backend-1.3.0.tar.gz", "sha256": "08262a62c948b1c75f6dce30f290e181ba7f98b212dcbb05f46d71744b50408a" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-paperwork", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "post-install": ["paperwork-shell install_system /app/share/icons /app/share"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/1d/d3/b7ffd1ae7eb5c29f8587145a9cb908e3971fff3d79a6f0322e81c2663089/paperwork-1.3.0.tar.gz", "sha256": "36c32cad431b0119f34ade74b01168df87731e0967028af509ba95f751dcd2fd" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] } paperwork-2.1.1/flatpak/1.3.1.json000066400000000000000000000432541417573700700165250ustar00rootroot00000000000000{ "app-id": "work.openpaper.Paperwork", "branch": "1.3.1", "runtime": "org.gnome.Platform", "runtime-version": "3.34", "sdk": "org.gnome.Sdk", "command": "paperwork", "copy-icon": true, "finish-args": [ "--share=ipc", "--share=network", "--socket=fallback-x11", "--socket=wayland", "--filesystem=home", "--persist=.python-eggs", "--talk-name=org.freedesktop.Notifications", "--talk-name=org.freedesktop.FileManager1", "--own-name=work.openpaper.paperwork", "--filesystem=xdg-run/dconf", "--filesystem=~/.config/dconf:ro", "--talk-name=ca.desrt.dconf", "--env=DCONF_USER_CONFIG_DIR=.config/dconf", "--device=all" ], "modules": [ "shared-modules/tesseract-4.0.0.json", "shared-modules/sane-backends-1.0.27.json", { "name": "python-setuptools", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/c2/f7/c7b501b783e5a74cf1768bc174ee4fb0a8a6ee5af6afa92274ff964703e0/setuptools-40.8.0.zip", "sha256": "6e4eec90337e849ade7103723b9a99631c1f0d19990d6e8412dc42f5ae8b304d" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-setuptools-scm", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/54/85/514ba3ca2a022bddd68819f187ae826986051d130ec5b972076e4f58a9f3/setuptools_scm-3.2.0.tar.gz", "sha256": "52ab47715fa0fc7d8e6cd15168d1a69ba995feb1505131c3e814eb7087b57358" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-setuptools-scm-git-archive", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/7e/2c/0c15b29a1b5940250bfdc4a4f53272e35cd7cf8a34159291b6b4ec9eb291/setuptools_scm_git_archive-1.1.tar.gz", "sha256": "6026f61089b73fa1b5ee737e95314f41cb512609b393530385ed281d0b46c062" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-distro", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/ca/e3/78443d739d7efeea86cbbe0216511d29b2f5ca8dbf51a6f2898432738987/distro-1.4.0.tar.gz", "sha256": "362dde65d846d23baee4b5c058c8586f219b5a54be1cf5fc6ff55c4578392f57" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-dateutil", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/ad/99/5b2e99737edeb28c71bcbec5b5dda19d0d9ef3ca3e92e3e925e7c0bb364c/python-dateutil-2.8.0.tar.gz", "sha256": "c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-Levenshtein", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/42/a9/d1785c85ebf9b7dfacd08938dd028209c34a0ea3b1bcdb895208bd40a67d/python-Levenshtein-0.12.0.tar.gz", "sha256": "033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pillow", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/3c/7e/443be24431324bd34d22dd9d11cc845d995bcd3b500676bcf23142756975/Pillow-5.4.1.tar.gz", "sha256": "5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ], "modules": [ { "name": "python-olefile", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/34/81/e1ac43c6b45b4c5f8d9352396a14144bba52c8fec72a80f425f6a4d653ad/olefile-0.46.zip", "sha256": "133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] }, { "name": "python-pycountry", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/87/c7/c2c76c3ae4ac79c74c1871ae775ed97b70d475dd90d1e824b1d2fc0cd54f/pycountry-18.12.8.tar.gz", "sha256": "8ec4020b2b15cd410893d573820d42ee12fe50365332e58c0975c953b60a16de" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-nose", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/58/a5/0dc93c3ec33f4e281849523a5a913fa1eea9a3068acfa754d44d88107a44/nose-1.3.7.tar.gz", "sha256": "f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "libinsane", "buildsystem": "meson", "sources": [ { "type": "git", "url": "https://gitlab.gnome.org/World/OpenPaperwork/libinsane.git", "tag": "1.0.3", "disable-shallow-clone": true } ] }, { "name": "python-pyocr", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/86/17/5fa0edc8da817a7da0198f03319850cb36cf2f20a38b6c7616fcb36211ef/pyocr-0.7.2.tar.gz", "sha256": "fa15adc7e1cf0d345a2990495fe125a947c6e09a60ddba0256a1c14b2e603179" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pypillowfight", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/90/70/575e3d04d581e04dccefd52cbb75e26aa07934147b2e85f3fa2896e61eed/pypillowfight-0.2.4.tar.gz", "sha256": "9208518494df900b8842b3d826c55ff673127634bdb2d2c85cca93b5017fd061" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyxdg", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/47/6e/311d5f22e2b76381719b5d0c6e9dc39cd33999adae67db71d7279a6d70f4/pyxdg-0.26.tar.gz", "sha256": "fe2928d3f532ed32b39c32a482b54136fe766d19936afc96c8f00645f9da1a06" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pydbus", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/58/56/3e84f2c1f2e39b9ea132460183f123af41e3b9c8befe222a35636baa6a5a/pydbus-0.6.0.tar.gz", "sha256": "4207162eff54223822c185da06c1ba8a34137a9602f3da5a528eedf3f78d0f2c" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-natsort", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/b3/5d/c0fbee4ed688fe2ed6533dd4a0124e1470d6692bc29e1da06bc0861ed4ab/natsort-6.0.0.tar.gz", "sha256": "ff3effb5618232866de8d26e5af4081a4daa9bb0dfed49ac65170e28e45f2776" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-simplebayes", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/b9/73/764578df72934940d95a8941cbd374b56319562dda72630fc8bfeaefc350/simplebayes-1.5.8.tar.gz", "sha256": "363418c0ef185ac2158ebbd6d8afb45aa997254fcb809a73ed20a7d5dccf8b85" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-whoosh", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/25/2b/6beed2107b148edc1321da0d489afc4617b9ed317ef7b72d4993cad9b684/Whoosh-2.7.4.tar.gz", "sha256": "7ca5633dbfa9e0e0fa400d3151a8a0c4bec53bd2ecedc0a67705b17565c31a83" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-termcolor", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/8a/48/a76be51647d0eb9f10e2a4511bf3ffb8cc1e6b14e9e4fab46173aa79f981/termcolor-1.1.0.tar.gz", "sha256": "1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "poppler-data", "buildsystem": "cmake-ninja", "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-data-0.4.9.tar.gz", "sha256": "1f9c7e7de9ecd0db6ab287349e31bf815ca108a5a175cf906a90163bdbe32012" } ] }, { "name": "poppler", "buildsystem": "cmake-ninja", "config-opts": [ "-DENABLE_LIBOPENJPEG:STRING=none" ], "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-0.74.0.tar.xz", "sha256": "92e09fd3302567fd36146b36bb707db43ce436e8841219025a82ea9fb0076b2f" } ] }, { "name": "python-paperwork-backend", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/8d/80/293051b9fb7da187ee4dd77b387312c319099f760472ff9b5aedbea27d07/paperwork-backend-1.3.1.tar.gz", "sha256": "7d0ef35bac1904a981ae40693eb8da99ea65ee644c7def084194824808489fa4" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-paperwork", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "post-install": ["paperwork-shell install_system /app/share/icons /app/share"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/d8/ee/429bb189358a558968a7b01148cd4936e1e0734fc35d1640ba75b7ef2b0c/paperwork-1.3.1.tar.gz", "sha256": "d8d229cfa4a0fb9118c93f2d3e9d8f8c5b9ec41397ae4864d8ceeefd141743a2" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] } paperwork-2.1.1/flatpak/2.0.1.json000066400000000000000000000475131417573700700165250ustar00rootroot00000000000000{ "app-id": "work.openpaper.Paperwork", "branch": "master", "runtime": "org.gnome.Platform", "runtime-version": "3.36", "sdk": "org.gnome.Sdk", "command": "paperwork-gtk", "copy-icon": true, "finish-args": [ "--share=ipc", "--share=network", "--socket=fallback-x11", "--socket=wayland", "--filesystem=home", "--persist=.python-eggs", "--talk-name=org.freedesktop.Notifications", "--talk-name=org.freedesktop.FileManager1", "--talk-name=org.gtk.vfs", "--talk-name=org.gtk.vfs.*", "--own-name=work.openpaper.paperwork" ], "modules": [ "shared-modules/tesseract-4.1.1.json", "shared-modules/sane-backends-1.0.27.json", { "name": "python-setuptools", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/c2/f7/c7b501b783e5a74cf1768bc174ee4fb0a8a6ee5af6afa92274ff964703e0/setuptools-40.8.0.zip", "sha256": "6e4eec90337e849ade7103723b9a99631c1f0d19990d6e8412dc42f5ae8b304d" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-setuptools-scm", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/54/85/514ba3ca2a022bddd68819f187ae826986051d130ec5b972076e4f58a9f3/setuptools_scm-3.2.0.tar.gz", "sha256": "52ab47715fa0fc7d8e6cd15168d1a69ba995feb1505131c3e814eb7087b57358" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-setuptools-scm-git-archive", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/7e/2c/0c15b29a1b5940250bfdc4a4f53272e35cd7cf8a34159291b6b4ec9eb291/setuptools_scm_git_archive-1.1.tar.gz", "sha256": "6026f61089b73fa1b5ee737e95314f41cb512609b393530385ed281d0b46c062" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-distro", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/ca/e3/78443d739d7efeea86cbbe0216511d29b2f5ca8dbf51a6f2898432738987/distro-1.4.0.tar.gz", "sha256": "362dde65d846d23baee4b5c058c8586f219b5a54be1cf5fc6ff55c4578392f57" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-dateutil", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/ad/99/5b2e99737edeb28c71bcbec5b5dda19d0d9ef3ca3e92e3e925e7c0bb364c/python-dateutil-2.8.0.tar.gz", "sha256": "c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-Levenshtein", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/42/a9/d1785c85ebf9b7dfacd08938dd028209c34a0ea3b1bcdb895208bd40a67d/python-Levenshtein-0.12.0.tar.gz", "sha256": "033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-getkey", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/74/f2/3312ea94369f410967667eeca61d261cdf3037df6ea827078ac7c5321150/getkey-0.6.5.tar.gz", "sha256": "68c7c702c3b34deacf427f6c0f1fd66c5c2aa12d7801aa32442fc1a71c8ce059" }, { "type": "patch", "path": "getkey-setup.diff" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-fabulous", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/53/2d/5750798dbb1cd3029c17b6f7456f79948b15f63e4781ffa0be8cf35cfc22/fabulous-0.3.0.tar.gz", "sha256": "54040da01d7ce1e937fc4b61d265e872b007463bea411a3a5762f4d6ee55c312" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pillow", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/3c/7e/443be24431324bd34d22dd9d11cc845d995bcd3b500676bcf23142756975/Pillow-5.4.1.tar.gz", "sha256": "5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ], "modules": [ { "name": "python-olefile", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/34/81/e1ac43c6b45b4c5f8d9352396a14144bba52c8fec72a80f425f6a4d653ad/olefile-0.46.zip", "sha256": "133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] }, { "name": "python-pycountry", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/87/c7/c2c76c3ae4ac79c74c1871ae775ed97b70d475dd90d1e824b1d2fc0cd54f/pycountry-18.12.8.tar.gz", "sha256": "8ec4020b2b15cd410893d573820d42ee12fe50365332e58c0975c953b60a16de" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-nose", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/58/a5/0dc93c3ec33f4e281849523a5a913fa1eea9a3068acfa754d44d88107a44/nose-1.3.7.tar.gz", "sha256": "f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyxdg", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/47/6e/311d5f22e2b76381719b5d0c6e9dc39cd33999adae67db71d7279a6d70f4/pyxdg-0.26.tar.gz", "sha256": "fe2928d3f532ed32b39c32a482b54136fe766d19936afc96c8f00645f9da1a06" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pydbus", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/58/56/3e84f2c1f2e39b9ea132460183f123af41e3b9c8befe222a35636baa6a5a/pydbus-0.6.0.tar.gz", "sha256": "4207162eff54223822c185da06c1ba8a34137a9602f3da5a528eedf3f78d0f2c" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-simplebayes", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/b9/73/764578df72934940d95a8941cbd374b56319562dda72630fc8bfeaefc350/simplebayes-1.5.8.tar.gz", "sha256": "363418c0ef185ac2158ebbd6d8afb45aa997254fcb809a73ed20a7d5dccf8b85" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-whoosh", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/25/2b/6beed2107b148edc1321da0d489afc4617b9ed317ef7b72d4993cad9b684/Whoosh-2.7.4.tar.gz", "sha256": "7ca5633dbfa9e0e0fa400d3151a8a0c4bec53bd2ecedc0a67705b17565c31a83" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "poppler-data", "buildsystem": "cmake-ninja", "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-data-0.4.9.tar.gz", "sha256": "1f9c7e7de9ecd0db6ab287349e31bf815ca108a5a175cf906a90163bdbe32012" } ] }, { "name": "poppler", "buildsystem": "cmake-ninja", "config-opts": [ "-DENABLE_LIBOPENJPEG:STRING=none" ], "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-0.74.0.tar.xz", "sha256": "92e09fd3302567fd36146b36bb707db43ce436e8841219025a82ea9fb0076b2f" } ] }, { "name": "libinsane", "buildsystem": "meson", "sources": [ { "type": "git", "url": "https://gitlab.gnome.org/World/OpenPaperwork/libinsane.git", "tag": "1.0.8", "disable-shallow-clone": true } ] }, { "name": "python-pyocr", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/86/17/5fa0edc8da817a7da0198f03319850cb36cf2f20a38b6c7616fcb36211ef/pyocr-0.7.2.tar.gz", "sha256": "fa15adc7e1cf0d345a2990495fe125a947c6e09a60ddba0256a1c14b2e603179" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pypillowfight", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/76/73/ce51023006387551b37b286d918e1a4e467f754374dec98d253aaa9ea121/pypillowfight-0.3.0.tar.gz", "sha256": "ec5bcf4b935f3b6e49b327c17f5a804d41ab72966c2b0edfbd45220fb7ad9316" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-openpaperwork-core", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/67/d4/ce1835b729d5e0cf8abbe373d3f2b3140fa42ed92c1df6c69545f6beaba6/openpaperwork-core-2.0.1.tar.gz", "sha256": "5eb92bacb672bae25f85f1012da3ad084ea951f5ef252f38a6bad2f4c7b5da08" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-openpaperwork-gtk", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/9a/71/5ef7c4c7c39a181a715748fa1f59efb5068781fb114697afc8b4cddc4190/openpaperwork-gtk-2.0.1.tar.gz", "sha256": "a137a4a327075125a9b14e510b7cc387af18c5bc7ab6f57f87512f0510b0213f" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-paperwork-backend", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/64/7d/b3bc8ba5efd1e316989e7a0955e8db58906a119e97d7773e485efc01d66c/paperwork-backend-2.0.1.tar.gz", "sha256": "4a813edf5e517a01b76e9fa7703a0b255241e585e899745d0133c5b655e5c6f4" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-paperwork-shell", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/c0/5e/a3a243025198aff48bc8dc4143e4ed35656d00639feacf40cee13c945a4c/paperwork-shell-2.0.1.tar.gz", "sha256": "bab9baf3199d79fe789ead8e01e5a5c0313b4ef1726fa06f43543c702230e1eb" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-paperwork-gtk", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "post-install": ["paperwork-gtk install --icon_base_dir=/app/share/icons --data_base_dir=/app/share"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/cd/42/9434bfc27a03a65fca505b9ed92313081bb29a915a3cb498a44ade11465a/paperwork-2.0.1.tar.gz", "sha256": "b8c376b1afe206436042f5c64b56c3160cb3144887f841d4706271e08e61394b" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] } paperwork-2.1.1/flatpak/2.0.2.json000066400000000000000000000510661417573700700165240ustar00rootroot00000000000000{ "app-id": "work.openpaper.Paperwork", "branch": "master", "runtime": "org.gnome.Platform", "runtime-version": "3.36", "sdk": "org.gnome.Sdk", "command": "paperwork-gtk", "copy-icon": true, "finish-args": [ "--share=ipc", "--share=network", "--socket=fallback-x11", "--socket=wayland", "--filesystem=home", "--persist=.python-eggs", "--talk-name=org.freedesktop.Notifications", "--talk-name=org.freedesktop.FileManager1", "--talk-name=org.gtk.vfs", "--talk-name=org.gtk.vfs.*", "--own-name=work.openpaper.paperwork" ], "modules": [ "shared-modules/tesseract-4.1.1.json", "shared-modules/sane-backends-1.0.27.json", { "name": "python-setuptools", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/c2/f7/c7b501b783e5a74cf1768bc174ee4fb0a8a6ee5af6afa92274ff964703e0/setuptools-40.8.0.zip", "sha256": "6e4eec90337e849ade7103723b9a99631c1f0d19990d6e8412dc42f5ae8b304d" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-setuptools-scm", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/54/85/514ba3ca2a022bddd68819f187ae826986051d130ec5b972076e4f58a9f3/setuptools_scm-3.2.0.tar.gz", "sha256": "52ab47715fa0fc7d8e6cd15168d1a69ba995feb1505131c3e814eb7087b57358" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-setuptools-scm-git-archive", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/7e/2c/0c15b29a1b5940250bfdc4a4f53272e35cd7cf8a34159291b6b4ec9eb291/setuptools_scm_git_archive-1.1.tar.gz", "sha256": "6026f61089b73fa1b5ee737e95314f41cb512609b393530385ed281d0b46c062" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-distro", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/ca/e3/78443d739d7efeea86cbbe0216511d29b2f5ca8dbf51a6f2898432738987/distro-1.4.0.tar.gz", "sha256": "362dde65d846d23baee4b5c058c8586f219b5a54be1cf5fc6ff55c4578392f57" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-dateutil", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/ad/99/5b2e99737edeb28c71bcbec5b5dda19d0d9ef3ca3e92e3e925e7c0bb364c/python-dateutil-2.8.0.tar.gz", "sha256": "c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-Levenshtein", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/42/a9/d1785c85ebf9b7dfacd08938dd028209c34a0ea3b1bcdb895208bd40a67d/python-Levenshtein-0.12.0.tar.gz", "sha256": "033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-getkey", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/74/f2/3312ea94369f410967667eeca61d261cdf3037df6ea827078ac7c5321150/getkey-0.6.5.tar.gz", "sha256": "68c7c702c3b34deacf427f6c0f1fd66c5c2aa12d7801aa32442fc1a71c8ce059" }, { "type": "patch", "path": "getkey-setup.diff" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-fabulous", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/53/2d/5750798dbb1cd3029c17b6f7456f79948b15f63e4781ffa0be8cf35cfc22/fabulous-0.3.0.tar.gz", "sha256": "54040da01d7ce1e937fc4b61d265e872b007463bea411a3a5762f4d6ee55c312" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pillow", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/3c/7e/443be24431324bd34d22dd9d11cc845d995bcd3b500676bcf23142756975/Pillow-5.4.1.tar.gz", "sha256": "5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ], "modules": [ { "name": "python-olefile", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/34/81/e1ac43c6b45b4c5f8d9352396a14144bba52c8fec72a80f425f6a4d653ad/olefile-0.46.zip", "sha256": "133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] }, { "name": "python-pycountry", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/87/c7/c2c76c3ae4ac79c74c1871ae775ed97b70d475dd90d1e824b1d2fc0cd54f/pycountry-18.12.8.tar.gz", "sha256": "8ec4020b2b15cd410893d573820d42ee12fe50365332e58c0975c953b60a16de" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-nose", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/58/a5/0dc93c3ec33f4e281849523a5a913fa1eea9a3068acfa754d44d88107a44/nose-1.3.7.tar.gz", "sha256": "f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyxdg", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/47/6e/311d5f22e2b76381719b5d0c6e9dc39cd33999adae67db71d7279a6d70f4/pyxdg-0.26.tar.gz", "sha256": "fe2928d3f532ed32b39c32a482b54136fe766d19936afc96c8f00645f9da1a06" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pydbus", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/58/56/3e84f2c1f2e39b9ea132460183f123af41e3b9c8befe222a35636baa6a5a/pydbus-0.6.0.tar.gz", "sha256": "4207162eff54223822c185da06c1ba8a34137a9602f3da5a528eedf3f78d0f2c" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-simplebayes", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/b9/73/764578df72934940d95a8941cbd374b56319562dda72630fc8bfeaefc350/simplebayes-1.5.8.tar.gz", "sha256": "363418c0ef185ac2158ebbd6d8afb45aa997254fcb809a73ed20a7d5dccf8b85" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-whoosh", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/25/2b/6beed2107b148edc1321da0d489afc4617b9ed317ef7b72d4993cad9b684/Whoosh-2.7.4.tar.gz", "sha256": "7ca5633dbfa9e0e0fa400d3151a8a0c4bec53bd2ecedc0a67705b17565c31a83" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-psutil", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/e1/b0/7276de53321c12981717490516b7e612364f2cb372ee8901bd4a66a000d7/psutil-5.8.0.tar.gz", "sha256": "0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "poppler-data", "buildsystem": "cmake-ninja", "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-data-0.4.9.tar.gz", "sha256": "1f9c7e7de9ecd0db6ab287349e31bf815ca108a5a175cf906a90163bdbe32012" } ] }, { "name": "poppler", "buildsystem": "cmake-ninja", "config-opts": [ "-DENABLE_LIBOPENJPEG:STRING=none" ], "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-0.74.0.tar.xz", "sha256": "92e09fd3302567fd36146b36bb707db43ce436e8841219025a82ea9fb0076b2f" } ] }, { "name": "libinsane", "buildsystem": "meson", "sources": [ { "type": "git", "url": "https://gitlab.gnome.org/World/OpenPaperwork/libinsane.git", "tag": "1.0.9", "disable-shallow-clone": true } ] }, { "name": "python-pyocr", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/86/17/5fa0edc8da817a7da0198f03319850cb36cf2f20a38b6c7616fcb36211ef/pyocr-0.7.2.tar.gz", "sha256": "fa15adc7e1cf0d345a2990495fe125a947c6e09a60ddba0256a1c14b2e603179" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pypillowfight", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/76/73/ce51023006387551b37b286d918e1a4e467f754374dec98d253aaa9ea121/pypillowfight-0.3.0.tar.gz", "sha256": "ec5bcf4b935f3b6e49b327c17f5a804d41ab72966c2b0edfbd45220fb7ad9316" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-openpaperwork-core", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/1b/12/3238d0226b9b276cb6cda94facc82d55f4efe6c1b218c8c255d7858fb02c/openpaperwork-core-2.0.2.tar.gz", "sha256": "c60ddafcecd03466ebad2448b3764d9e168b956c484e3030357ba31d28c6b13a" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-openpaperwork-gtk", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/05/76/89335123b672a4e577ff474591bf1f7080e942be366ef110a538a35a892f/openpaperwork-gtk-2.0.2.tar.gz", "sha256": "2491a506c6d966096a744a5a274898c3f297d228121fcc6f0e5db394d621c508" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-paperwork-backend", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/0f/43/bc1f8ddf1a80fc3514a6544258e566057bc12b963a8ca9aac1f7acd0f79a/paperwork-backend-2.0.2.tar.gz", "sha256": "b72914e1ff8d7742f4a07db5db0d0e2ec3cf4bcb9b3bf46dc9fe3dc8199cb8d8" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-paperwork-shell", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/50/b7/6d4029ed9fbb3702f6687c235b1769cf23351ba4b191888735e9a34053f6/paperwork-shell-2.0.2.tar.gz", "sha256": "9c80590b81a475c94cbb49adef64e0f75c84ea6da814908e6b2b98a70583687a" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-paperwork-gtk", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "post-install": ["paperwork-gtk install --icon_base_dir=/app/share/icons --data_base_dir=/app/share"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/09/57/f9f60dd2d724bc7a4be2352d37a9a33e1177acb0c8a962344ca5eb061b29/paperwork-2.0.2.tar.gz", "sha256": "1a0a161acdd4d8f0741729460b39813a5f1bbbd5ecb032244706f9bcc0189de5" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] } paperwork-2.1.1/flatpak/2.0.json000066400000000000000000000475011417573700700163630ustar00rootroot00000000000000{ "app-id": "work.openpaper.Paperwork", "branch": "master", "runtime": "org.gnome.Platform", "runtime-version": "3.36", "sdk": "org.gnome.Sdk", "command": "paperwork-gtk", "copy-icon": true, "finish-args": [ "--share=ipc", "--share=network", "--socket=fallback-x11", "--socket=wayland", "--filesystem=home", "--persist=.python-eggs", "--talk-name=org.freedesktop.Notifications", "--talk-name=org.freedesktop.FileManager1", "--talk-name=org.gtk.vfs", "--talk-name=org.gtk.vfs.*", "--own-name=work.openpaper.paperwork" ], "modules": [ "shared-modules/tesseract-4.1.1.json", "shared-modules/sane-backends-1.0.27.json", { "name": "python-setuptools", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/c2/f7/c7b501b783e5a74cf1768bc174ee4fb0a8a6ee5af6afa92274ff964703e0/setuptools-40.8.0.zip", "sha256": "6e4eec90337e849ade7103723b9a99631c1f0d19990d6e8412dc42f5ae8b304d" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-setuptools-scm", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/54/85/514ba3ca2a022bddd68819f187ae826986051d130ec5b972076e4f58a9f3/setuptools_scm-3.2.0.tar.gz", "sha256": "52ab47715fa0fc7d8e6cd15168d1a69ba995feb1505131c3e814eb7087b57358" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-setuptools-scm-git-archive", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/7e/2c/0c15b29a1b5940250bfdc4a4f53272e35cd7cf8a34159291b6b4ec9eb291/setuptools_scm_git_archive-1.1.tar.gz", "sha256": "6026f61089b73fa1b5ee737e95314f41cb512609b393530385ed281d0b46c062" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-distro", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/ca/e3/78443d739d7efeea86cbbe0216511d29b2f5ca8dbf51a6f2898432738987/distro-1.4.0.tar.gz", "sha256": "362dde65d846d23baee4b5c058c8586f219b5a54be1cf5fc6ff55c4578392f57" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-dateutil", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/ad/99/5b2e99737edeb28c71bcbec5b5dda19d0d9ef3ca3e92e3e925e7c0bb364c/python-dateutil-2.8.0.tar.gz", "sha256": "c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-Levenshtein", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/42/a9/d1785c85ebf9b7dfacd08938dd028209c34a0ea3b1bcdb895208bd40a67d/python-Levenshtein-0.12.0.tar.gz", "sha256": "033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-getkey", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/74/f2/3312ea94369f410967667eeca61d261cdf3037df6ea827078ac7c5321150/getkey-0.6.5.tar.gz", "sha256": "68c7c702c3b34deacf427f6c0f1fd66c5c2aa12d7801aa32442fc1a71c8ce059" }, { "type": "patch", "path": "getkey-setup.diff" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-fabulous", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/53/2d/5750798dbb1cd3029c17b6f7456f79948b15f63e4781ffa0be8cf35cfc22/fabulous-0.3.0.tar.gz", "sha256": "54040da01d7ce1e937fc4b61d265e872b007463bea411a3a5762f4d6ee55c312" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pillow", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/3c/7e/443be24431324bd34d22dd9d11cc845d995bcd3b500676bcf23142756975/Pillow-5.4.1.tar.gz", "sha256": "5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ], "modules": [ { "name": "python-olefile", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/34/81/e1ac43c6b45b4c5f8d9352396a14144bba52c8fec72a80f425f6a4d653ad/olefile-0.46.zip", "sha256": "133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] }, { "name": "python-pycountry", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/87/c7/c2c76c3ae4ac79c74c1871ae775ed97b70d475dd90d1e824b1d2fc0cd54f/pycountry-18.12.8.tar.gz", "sha256": "8ec4020b2b15cd410893d573820d42ee12fe50365332e58c0975c953b60a16de" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-nose", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/58/a5/0dc93c3ec33f4e281849523a5a913fa1eea9a3068acfa754d44d88107a44/nose-1.3.7.tar.gz", "sha256": "f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyxdg", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/47/6e/311d5f22e2b76381719b5d0c6e9dc39cd33999adae67db71d7279a6d70f4/pyxdg-0.26.tar.gz", "sha256": "fe2928d3f532ed32b39c32a482b54136fe766d19936afc96c8f00645f9da1a06" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pydbus", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/58/56/3e84f2c1f2e39b9ea132460183f123af41e3b9c8befe222a35636baa6a5a/pydbus-0.6.0.tar.gz", "sha256": "4207162eff54223822c185da06c1ba8a34137a9602f3da5a528eedf3f78d0f2c" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-simplebayes", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/b9/73/764578df72934940d95a8941cbd374b56319562dda72630fc8bfeaefc350/simplebayes-1.5.8.tar.gz", "sha256": "363418c0ef185ac2158ebbd6d8afb45aa997254fcb809a73ed20a7d5dccf8b85" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-whoosh", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/25/2b/6beed2107b148edc1321da0d489afc4617b9ed317ef7b72d4993cad9b684/Whoosh-2.7.4.tar.gz", "sha256": "7ca5633dbfa9e0e0fa400d3151a8a0c4bec53bd2ecedc0a67705b17565c31a83" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "poppler-data", "buildsystem": "cmake-ninja", "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-data-0.4.9.tar.gz", "sha256": "1f9c7e7de9ecd0db6ab287349e31bf815ca108a5a175cf906a90163bdbe32012" } ] }, { "name": "poppler", "buildsystem": "cmake-ninja", "config-opts": [ "-DENABLE_LIBOPENJPEG:STRING=none" ], "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-0.74.0.tar.xz", "sha256": "92e09fd3302567fd36146b36bb707db43ce436e8841219025a82ea9fb0076b2f" } ] }, { "name": "libinsane", "buildsystem": "meson", "sources": [ { "type": "git", "url": "https://gitlab.gnome.org/World/OpenPaperwork/libinsane.git", "tag": "1.0.8", "disable-shallow-clone": true } ] }, { "name": "python-pyocr", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/86/17/5fa0edc8da817a7da0198f03319850cb36cf2f20a38b6c7616fcb36211ef/pyocr-0.7.2.tar.gz", "sha256": "fa15adc7e1cf0d345a2990495fe125a947c6e09a60ddba0256a1c14b2e603179" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pypillowfight", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/76/73/ce51023006387551b37b286d918e1a4e467f754374dec98d253aaa9ea121/pypillowfight-0.3.0.tar.gz", "sha256": "ec5bcf4b935f3b6e49b327c17f5a804d41ab72966c2b0edfbd45220fb7ad9316" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-openpaperwork-core", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/f8/1b/9735c4835429086e7d6b732a678fa4dedfb7347c0a723326d9416c8d683b/openpaperwork-core-2.0.tar.gz", "sha256": "65492f937f1e166f0aeac99e8488526d0cf30e30ea1ba1621d83b8cfb21b0514" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-openpaperwork-gtk", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/ea/9a/b9f8bea49b99d78b87a9d9bfcef6bf2b5b54d66ee207da8aadc0d582de6a/openpaperwork-gtk-2.0.tar.gz", "sha256": "062052779defa9979069e8eef9e858f2870937d47e2477b14be8a03e03f456d7" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-paperwork-backend", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/5e/9d/7b8986069c54777460db75a1882bcab19cd7d1e4f9f7017140ccde4fc1c4/paperwork-backend-2.0.tar.gz", "sha256": "bfe938cf0dd17a91b10e2449da979d608cf3635e721cd1bebc5cf5b29ae87fd9" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-paperwork-shell", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/77/24/56696480a4509aca5fde27ec50045b103e9fc5fb21aa76393ff70da902c7/paperwork-shell-2.0.tar.gz", "sha256": "8940974d2f23e942c6beec40c1052625515ab14150d470e72ed539f6b546fe6a" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-paperwork-gtk", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "post-install": ["paperwork-gtk install --icon_base_dir=/app/share/icons --data_base_dir=/app/share"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/67/ee/1232f268d06b08cdda5d32ad1ef29a0a45ada23fb0b9381f16c818e86555/paperwork-2.0.tar.gz", "sha256": "252f8f13375c2e8590561ed876939bdf5a0365e1bd93f17226c964df21d2fa41" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] } paperwork-2.1.1/flatpak/Makefile000066400000000000000000000030301417573700700166160ustar00rootroot00000000000000REPO ?= $(abspath paperwork_repo) APP = work.openpaper.Paperwork ALL_TARGETS = $(patsubst %.json,%,$(wildcard *.json)) ALL_BRANCHES = master testing develop all: upd_branches # clean: remove all intermediate files clean: rm -rf *.app *.flatpak # dist-clean: remove *everything* that is not stored in Git dist-clean: clean rm -rf $(REPO) .flatpak-builder # Build a specific branch / tag # Will export to the Flatpak repository $(REPO) %.app: ../work.openpaper.Paperwork.json @echo flatpak-builder $< --\> $@ \($(REPO) ${EXPORT_ARGS} ${ARCH_ARGS}\) cd ../ && flatpak-builder --repo=$(REPO) -s "Build of Paperwork $(@:%.app=%) `date`" ${EXPORT_ARGS} ${ARCH_ARGS} flatpak/$@ work.openpaper.Paperwork.json upd_repo: @echo flatpak build-update-repo $(REPO) \($(REPO) ${EXPORT_ARGS} ${ARCH_ARGS}\) flatpak build-update-repo --generate-static-deltas ${EXPORT_ARGS} $(REPO) # Build all branches and tags and update the Flatpak repository $(REPO) mk_all: $(patsubst %,%.app,$(ALL_TARGETS)) $(MAKE) upd_repo # Build only the branches and update the Flatpak repository $(REPO) upd_branches: $(patsubst %,%.app,$(ALL_BRANCHES)) $(MAKE upd_repo) # Can be used if you call %.app manually first # Build a Flatpak bundle %.flatpak: %.app $(MAKE) upd_repo @echo flatpak build-bundle $(REPO) --\> $@ \(${EXPORT_ARGS} ${ARCH_ARGS} branch: $(@:%.flatpak=%)\) flatpak -v build-bundle ${ARCH_ARGS} $(REPO) $@ $(APP) $(@:%.flatpak=%) # Build all Flatpak bundles bundles: $(patsubst %,%.flatpak,$(ALL_TARGETS)) .PHONY: all clean dist-clean mkrepo bundles upd_repo paperwork-2.1.1/flatpak/getkey-setup.diff000066400000000000000000000003141417573700700204400ustar00rootroot00000000000000--- a/setup.py 2020-06-28 23:11:43.217511302 +0200 +++ b/setup.py 2020-06-28 23:11:53.353700987 +0200 @@ -71,6 +71,5 @@ install_requires=[ ], setup_requires=[ - 'flake8', ], ) paperwork-2.1.1/flatpak/pip-Makefile000066400000000000000000000001271417573700700174100ustar00rootroot00000000000000all: python3 setup.py build install: python3 setup.py install --prefix=/app ${ARGS} paperwork-2.1.1/flatpak/pypillowfight-Makefile000066400000000000000000000040161417573700700215220ustar00rootroot00000000000000# If you want to build in MSYS2 on Windows # export CMAKE_OPTS=-G "MSYS Makefiles" VERSION_FILE = src/pillowfight/_version.h PYTHON = python3 build: build_c build_py install: ${VERSION_FILE} python3 setup.py install --prefix=/app ${ARGS} uninstall: uninstall_py build_py: ${VERSION_FILE} ${PYTHON} ./setup.py build build_c: ${VERSION_FILE} build/libpillowfight.so build/libpillowfight.so: ${VERSION_FILE} build/Makefile (cd build && make -j4) build/Makefile: mkdir -p build (cd build && cmake ${CMAKE_OPTS} ..) ${VERSION_FILE}: echo -n "#define INTERNAL_PILLOWFIGHT_VERSION \"" >| $@ echo -n $(shell git describe --always) >> $@ echo "\"" >> $@ version: ${VERSION_FILE} doc: install_py (cd doc && make html) doxygen doc/doxygen.conf cp doc/index.html doc/build/index.html check: flake8 # pydocstyle src test: build_py tox linux_exe: windows_exe: release: ifeq (${RELEASE}, ) @echo "You must specify a release version (make release RELEASE=1.2.3)" else @echo "Will release: ${RELEASE}" @echo "Checking release is in ChangeLog ..." grep ${RELEASE} ChangeLog | grep -v "/xx" @echo "Releasing ..." git tag -a ${RELEASE} -m ${RELEASE} git push origin ${RELEASE} make clean make version ${PYTHON} ./setup.py sdist upload @echo "All done" endif clean: rm -rf doc/build rm -rf build dist *.egg-info rm -f ${VERSION_FILE} install_py: ${VERSION_FILE} ${PYTHON} ./setup.py install ${PIP_ARGS} install_c: build/Makefile ${VERSION_FILE} (cd build && make install) uninstall_py: pip3 uninstall -y pypillowfight uninstall_c: echo "Can't uninstall C library. Sorry" help: @echo "make build || make build_c || make build_py" @echo "make check" @echo "make doc" @echo "make help: display this message" @echo "make install || make install_c || make install_py" @echo "make release" @echo "make test" @echo "make uninstall || make uninstall_py" .PHONY: \ build \ build_c \ build_py \ check \ doc \ exe \ exe \ help \ install \ install_c \ install_py \ release \ test \ uninstall \ uninstall_c \ version paperwork-2.1.1/flatpak/python-pillow-disable-multithreaded-compilation.diff000066400000000000000000000005511417573700700274100ustar00rootroot00000000000000--- setup.py 2017-11-16 21:02:44.002328167 +0100 +++ setup.py 2017-11-16 21:03:48.353489990 +0100 @@ -22,7 +22,7 @@ # monkey patch import hook. Even though flake8 says it's not used, it is. # comment this out to disable multi threaded builds. -import mp_compile +# import mp_compile _IMAGING = ("decode", "encode", "map", "display", "outline", "path") paperwork-2.1.1/flatpak/release.json000066400000000000000000000432561417573700700175070ustar00rootroot00000000000000{ "app-id": "work.openpaper.Paperwork", "branch": "release", "runtime": "org.gnome.Platform", "runtime-version": "3.34", "sdk": "org.gnome.Sdk", "command": "paperwork", "copy-icon": true, "finish-args": [ "--share=ipc", "--share=network", "--socket=fallback-x11", "--socket=wayland", "--filesystem=home", "--persist=.python-eggs", "--talk-name=org.freedesktop.Notifications", "--talk-name=org.freedesktop.FileManager1", "--own-name=work.openpaper.paperwork", "--filesystem=xdg-run/dconf", "--filesystem=~/.config/dconf:ro", "--talk-name=ca.desrt.dconf", "--env=DCONF_USER_CONFIG_DIR=.config/dconf", "--device=all" ], "modules": [ "shared-modules/tesseract-4.0.0.json", "shared-modules/sane-backends-1.0.27.json", { "name": "python-setuptools", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/c2/f7/c7b501b783e5a74cf1768bc174ee4fb0a8a6ee5af6afa92274ff964703e0/setuptools-40.8.0.zip", "sha256": "6e4eec90337e849ade7103723b9a99631c1f0d19990d6e8412dc42f5ae8b304d" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-setuptools-scm", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/54/85/514ba3ca2a022bddd68819f187ae826986051d130ec5b972076e4f58a9f3/setuptools_scm-3.2.0.tar.gz", "sha256": "52ab47715fa0fc7d8e6cd15168d1a69ba995feb1505131c3e814eb7087b57358" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-setuptools-scm-git-archive", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/7e/2c/0c15b29a1b5940250bfdc4a4f53272e35cd7cf8a34159291b6b4ec9eb291/setuptools_scm_git_archive-1.1.tar.gz", "sha256": "6026f61089b73fa1b5ee737e95314f41cb512609b393530385ed281d0b46c062" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-distro", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/ca/e3/78443d739d7efeea86cbbe0216511d29b2f5ca8dbf51a6f2898432738987/distro-1.4.0.tar.gz", "sha256": "362dde65d846d23baee4b5c058c8586f219b5a54be1cf5fc6ff55c4578392f57" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-dateutil", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/ad/99/5b2e99737edeb28c71bcbec5b5dda19d0d9ef3ca3e92e3e925e7c0bb364c/python-dateutil-2.8.0.tar.gz", "sha256": "c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-Levenshtein", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/42/a9/d1785c85ebf9b7dfacd08938dd028209c34a0ea3b1bcdb895208bd40a67d/python-Levenshtein-0.12.0.tar.gz", "sha256": "033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pillow", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/3c/7e/443be24431324bd34d22dd9d11cc845d995bcd3b500676bcf23142756975/Pillow-5.4.1.tar.gz", "sha256": "5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ], "modules": [ { "name": "python-olefile", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/34/81/e1ac43c6b45b4c5f8d9352396a14144bba52c8fec72a80f425f6a4d653ad/olefile-0.46.zip", "sha256": "133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] }, { "name": "python-pycountry", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/87/c7/c2c76c3ae4ac79c74c1871ae775ed97b70d475dd90d1e824b1d2fc0cd54f/pycountry-18.12.8.tar.gz", "sha256": "8ec4020b2b15cd410893d573820d42ee12fe50365332e58c0975c953b60a16de" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-nose", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/58/a5/0dc93c3ec33f4e281849523a5a913fa1eea9a3068acfa754d44d88107a44/nose-1.3.7.tar.gz", "sha256": "f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "libinsane", "buildsystem": "meson", "sources": [ { "type": "git", "url": "https://gitlab.gnome.org/World/OpenPaperwork/libinsane.git", "tag": "1.0.3", "disable-shallow-clone": true } ] }, { "name": "python-pyocr", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/86/17/5fa0edc8da817a7da0198f03319850cb36cf2f20a38b6c7616fcb36211ef/pyocr-0.7.2.tar.gz", "sha256": "fa15adc7e1cf0d345a2990495fe125a947c6e09a60ddba0256a1c14b2e603179" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pypillowfight", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/90/70/575e3d04d581e04dccefd52cbb75e26aa07934147b2e85f3fa2896e61eed/pypillowfight-0.2.4.tar.gz", "sha256": "9208518494df900b8842b3d826c55ff673127634bdb2d2c85cca93b5017fd061" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pyxdg", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/47/6e/311d5f22e2b76381719b5d0c6e9dc39cd33999adae67db71d7279a6d70f4/pyxdg-0.26.tar.gz", "sha256": "fe2928d3f532ed32b39c32a482b54136fe766d19936afc96c8f00645f9da1a06" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-pydbus", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/58/56/3e84f2c1f2e39b9ea132460183f123af41e3b9c8befe222a35636baa6a5a/pydbus-0.6.0.tar.gz", "sha256": "4207162eff54223822c185da06c1ba8a34137a9602f3da5a528eedf3f78d0f2c" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-natsort", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/b3/5d/c0fbee4ed688fe2ed6533dd4a0124e1470d6692bc29e1da06bc0861ed4ab/natsort-6.0.0.tar.gz", "sha256": "ff3effb5618232866de8d26e5af4081a4daa9bb0dfed49ac65170e28e45f2776" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-simplebayes", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/b9/73/764578df72934940d95a8941cbd374b56319562dda72630fc8bfeaefc350/simplebayes-1.5.8.tar.gz", "sha256": "363418c0ef185ac2158ebbd6d8afb45aa997254fcb809a73ed20a7d5dccf8b85" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-whoosh", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/25/2b/6beed2107b148edc1321da0d489afc4617b9ed317ef7b72d4993cad9b684/Whoosh-2.7.4.tar.gz", "sha256": "7ca5633dbfa9e0e0fa400d3151a8a0c4bec53bd2ecedc0a67705b17565c31a83" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-termcolor", "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/8a/48/a76be51647d0eb9f10e2a4511bf3ffb8cc1e6b14e9e4fab46173aa79f981/termcolor-1.1.0.tar.gz", "sha256": "1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "poppler-data", "buildsystem": "cmake-ninja", "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-data-0.4.9.tar.gz", "sha256": "1f9c7e7de9ecd0db6ab287349e31bf815ca108a5a175cf906a90163bdbe32012" } ] }, { "name": "poppler", "buildsystem": "cmake-ninja", "config-opts": [ "-DENABLE_LIBOPENJPEG:STRING=none" ], "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-0.74.0.tar.xz", "sha256": "92e09fd3302567fd36146b36bb707db43ce436e8841219025a82ea9fb0076b2f" } ] }, { "name": "python-paperwork-backend", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/8d/80/293051b9fb7da187ee4dd77b387312c319099f760472ff9b5aedbea27d07/paperwork-backend-1.3.1.tar.gz", "sha256": "7d0ef35bac1904a981ae40693eb8da99ea65ee644c7def084194824808489fa4" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-paperwork", "make-install-args": ["PIP_ARGS=--prefix=/app"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "post-install": ["paperwork-shell install_system /app/share/icons /app/share"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/d8/ee/429bb189358a558968a7b01148cd4936e1e0734fc35d1640ba75b7ef2b0c/paperwork-1.3.1.tar.gz", "sha256": "d8d229cfa4a0fb9118c93f2d3e9d8f8c5b9ec41397ae4864d8ceeefd141743a2" }, { "type": "file", "path": "pip-Makefile", "dest-filename": "Makefile" } ] } ] } paperwork-2.1.1/flatpak/shared-modules/000077500000000000000000000000001417573700700200765ustar00rootroot00000000000000paperwork-2.1.1/flatpak/shared-modules/libreoffice-7.0.4.2.json000066400000000000000000001020061417573700700240450ustar00rootroot00000000000000{ "_comment": "Copy pasta from https://raw.githubusercontent.com/flathub/org.libreoffice.LibreOffice/master/org.libreoffice.LibreOffice.json", "name": "libreoffice-complete", "modules": [ { "name": "openjdk", "buildsystem": "simple", "build-commands": [ "/usr/lib/sdk/openjdk11/install.sh" ] }, { "name": "libreoffice", "sources": [ { "type": "git", "url": "https://gerrit.libreoffice.org/core", "branch": "libreoffice-7.0.4.2", "disable-fsckobjects": true }, { "type": "archive", "url": "https://archive.apache.org/dist/ant/binaries/apache-ant-1.10.5-bin.tar.xz", "sha256": "cebb705dbbe26a41d359b8be08ec066caba4e8686670070ce44bbf2b57ae113f", "dest": "ant" }, { "commands": [ "mkdir external/tarballs" ], "type": "shell" }, { "url": "https://dev-www.libreoffice.org/src/pdfium-4306.tar.bz2", "sha256": "eca406d47ac7e2a84dcc86f93c08f96e591d409589e881477fa75e488e4851d8", "type": "file", "dest-filename": "external/tarballs/pdfium-4306.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/0168229624cfac409e766913506961a8-ucpp-1.3.2.tar.gz", "sha256": "983941d31ee8d366085cadf28db75eb1f5cb03ba1e5853b98f12f7f51c63b776", "type": "file", "dest-filename": "external/tarballs/0168229624cfac409e766913506961a8-ucpp-1.3.2.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/xmlsec1-1.2.30.tar.gz", "sha256": "2d84360b03042178def1d9ff538acacaed2b3a27411db7b2874f1612ed71abc8", "type": "file", "dest-filename": "external/tarballs/xmlsec1-1.2.30.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/368f114c078f94214a308a74c7e991bc-crosextrafonts-20130214.tar.gz", "sha256": "c48d1c2fd613c9c06c959c34da7b8388059e2408d2bb19845dc3ed35f76e4d09", "type": "file", "dest-filename": "external/tarballs/368f114c078f94214a308a74c7e991bc-crosextrafonts-20130214.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/c74b7223abe75949b4af367942d96c7a-crosextrafonts-carlito-20130920.tar.gz", "sha256": "4bd12b6cbc321c1cf16da76e2c585c925ce956a08067ae6f6c64eff6ccfdaf5a", "type": "file", "dest-filename": "external/tarballs/c74b7223abe75949b4af367942d96c7a-crosextrafonts-carlito-20130920.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/33e1e61fab06a547851ed308b4ffef42-dejavu-fonts-ttf-2.37.zip", "sha256": "7576310b219e04159d35ff61dd4a4ec4cdba4f35c00e002a136f00e96a908b0a", "type": "file", "dest-filename": "external/tarballs/33e1e61fab06a547851ed308b4ffef42-dejavu-fonts-ttf-2.37.zip" }, { "url": "https://dev-www.libreoffice.org/src/1725634df4bb3dcb1b2c91a6175f8789-GentiumBasic_1102.zip", "sha256": "2f1a2c5491d7305dffd3520c6375d2f3e14931ee35c6d8ae1e8f098bf1a7b3cc", "type": "file", "dest-filename": "external/tarballs/1725634df4bb3dcb1b2c91a6175f8789-GentiumBasic_1102.zip" }, { "url": "https://dev-www.libreoffice.org/src/liberation-narrow-fonts-ttf-1.07.6.tar.gz", "sha256": "8879d89b5ff7b506c9fc28efc31a5c0b954bbe9333e66e5283d27d20a8519ea3", "type": "file", "dest-filename": "external/tarballs/liberation-narrow-fonts-ttf-1.07.6.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/liberation-fonts-ttf-2.00.4.tar.gz", "sha256": "c40e95fc5e0ecb73d4be565ae2afc1114e2bc7dc5253e00ee92d8fd6cc4adf45", "type": "file", "dest-filename": "external/tarballs/liberation-fonts-ttf-2.00.4.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/e7a384790b13c29113e22e596ade9687-LinLibertineG-20120116.zip", "sha256": "54adcb2bc8cac0927a647fbd9362f45eff48130ce6e2379dc3867643019e08c5", "type": "file", "dest-filename": "external/tarballs/e7a384790b13c29113e22e596ade9687-LinLibertineG-20120116.zip" }, { "url": "https://dev-www.libreoffice.org/src/907d6e99f241876695c19ff3db0b8923-source-code-pro-2.030R-ro-1.050R-it.tar.gz", "sha256": "09466dce87653333f189acd8358c60c6736dcd95f042dee0b644bdcf65b6ae2f", "type": "file", "dest-filename": "external/tarballs/907d6e99f241876695c19ff3db0b8923-source-code-pro-2.030R-ro-1.050R-it.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/edc4d741888bc0d38e32dbaa17149596-source-sans-pro-2.010R-ro-1.065R-it.tar.gz", "sha256": "e7bc9a1fec787a529e49f5a26b93dcdcf41506449dfc70f92cdef6d17eb6fb61", "type": "file", "dest-filename": "external/tarballs/edc4d741888bc0d38e32dbaa17149596-source-sans-pro-2.010R-ro-1.065R-it.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/source-serif-pro-3.000R.tar.gz", "sha256": "826a2b784d5cdb4c2bbc7830eb62871528360a61a52689c102a101623f1928e3", "type": "file", "dest-filename": "external/tarballs/source-serif-pro-3.000R.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/EmojiOneColor-SVGinOT-1.3.tar.gz", "sha256": "d1a08f7c10589f22740231017694af0a7a270760c8dec33d8d1c038e2be0a0c7", "type": "file", "dest-filename": "external/tarballs/EmojiOneColor-SVGinOT-1.3.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/boost_1_71_0.tar.xz", "sha256": "35e06a3bd7cd8f66be822c7d64e80c2b6051a181e9e897006917cb8e7988a543", "type": "file", "dest-filename": "external/tarballs/boost_1_71_0.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/48d647fbd8ef8889e5a7f422c1bfda94-clucene-core-2.3.3.4.tar.gz", "sha256": "ddfdc433dd8ad31b5c5819cc4404a8d2127472a3b720d3e744e8c51d79732eab", "type": "file", "dest-filename": "external/tarballs/48d647fbd8ef8889e5a7f422c1bfda94-clucene-core-2.3.3.4.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/CoinMP-1.7.6.tgz", "sha256": "86c798780b9e1f5921fe4efe651a93cb420623b45aa1fdff57af8c37f116113f", "type": "file", "dest-filename": "external/tarballs/CoinMP-1.7.6.tgz" }, { "url": "https://dev-www.libreoffice.org/src/cppunit-1.15.1.tar.gz", "sha256": "89c5c6665337f56fd2db36bc3805a5619709d51fb136e51937072f63fcc717a7", "type": "file", "dest-filename": "external/tarballs/cppunit-1.15.1.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/Firebird-3.0.0.32483-0.tar.bz2", "sha256": "6994be3555e23226630c587444be19d309b25b0fcf1f87df3b4e3f88943e5860", "type": "file", "dest-filename": "external/tarballs/Firebird-3.0.0.32483-0.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/glm-0.9.9.7.zip", "sha256": "c5e167c042afd2d7ad642ace6b643863baeb33880781983563e1ab68a30d3e95", "type": "file", "dest-filename": "external/tarballs/glm-0.9.9.7.zip" }, { "url": "https://dev-www.libreoffice.org/src/gpgme-1.9.0.tar.bz2", "sha256": "1b29fedb8bfad775e70eafac5b0590621683b2d9869db994568e6401f4034ceb", "type": "file", "dest-filename": "external/tarballs/gpgme-1.9.0.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/libassuan-2.5.1.tar.bz2", "sha256": "47f96c37b4f2aac289f0bc1bacfa8bd8b4b209a488d3d15e2229cb6cc9b26449", "type": "file", "dest-filename": "external/tarballs/libassuan-2.5.1.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/libgpg-error-1.27.tar.bz2", "sha256": "4f93aac6fecb7da2b92871bb9ee33032be6a87b174f54abf8ddf0911a22d29d2", "type": "file", "dest-filename": "external/tarballs/libgpg-error-1.27.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/libabw-0.1.3.tar.xz", "sha256": "e763a9dc21c3d2667402d66e202e3f8ef4db51b34b79ef41f56cacb86dcd6eed", "type": "file", "dest-filename": "external/tarballs/libabw-0.1.3.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libcdr-0.1.6.tar.xz", "sha256": "01cd00b04a030977e544433c2d127c997205332cd9b8e35ec0ee17110da7f861", "type": "file", "dest-filename": "external/tarballs/libcdr-0.1.6.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libcmis-0.5.2.tar.xz", "sha256": "d7b18d9602190e10d437f8a964a32e983afd57e2db316a07d87477a79f5000a2", "type": "file", "dest-filename": "external/tarballs/libcmis-0.5.2.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libe-book-0.1.3.tar.xz", "sha256": "7e8d8ff34f27831aca3bc6f9cc532c2f90d2057c778963b884ff3d1e34dfe1f9", "type": "file", "dest-filename": "external/tarballs/libe-book-0.1.3.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libetonyek-0.1.9.tar.xz", "sha256": "e61677e8799ce6e55b25afc11aa5339113f6a49cff031f336e32fa58635b1a4a", "type": "file", "dest-filename": "external/tarballs/libetonyek-0.1.9.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libexttextcat-3.4.5.tar.xz", "sha256": "13fdbc9d4c489a4d0519e51933a1aa21fe3fb9eb7da191b87f7a63e82797dac8", "type": "file", "dest-filename": "external/tarballs/libexttextcat-3.4.5.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libfreehand-0.1.2.tar.xz", "sha256": "0e422d1564a6dbf22a9af598535425271e583514c0f7ba7d9091676420de34ac", "type": "file", "dest-filename": "external/tarballs/libfreehand-0.1.2.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/language-subtag-registry-2020-09-29.tar.bz2", "sha256": "cbe9fca811a37056560aab73e9fc9d3522b46b6785cb02db165f521bf42c230f", "type": "file", "dest-filename": "external/tarballs/language-subtag-registry-2020-09-29.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/liblangtag-0.6.2.tar.bz2", "sha256": "d6242790324f1432fb0a6fae71b6851f520b2c5a87675497cf8ea14c2924d52e", "type": "file", "dest-filename": "external/tarballs/liblangtag-0.6.2.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/libmspub-0.1.4.tar.xz", "sha256": "ef36c1a1aabb2ba3b0bedaaafe717bf4480be2ba8de6f3894be5fd3702b013ba", "type": "file", "dest-filename": "external/tarballs/libmspub-0.1.4.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libmwaw-0.3.16.tar.xz", "sha256": "0c639edba5297bde5575193bf5b5f2f469956beaff5c0206d91ce9df6bde1868", "type": "file", "dest-filename": "external/tarballs/libmwaw-0.3.16.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libodfgen-0.1.6.tar.bz2", "sha256": "2c7b21892f84a4c67546f84611eccdad6259875c971e98ddb027da66ea0ac9c2", "type": "file", "dest-filename": "external/tarballs/libodfgen-0.1.6.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/libpagemaker-0.0.4.tar.xz", "sha256": "66adacd705a7d19895e08eac46d1e851332adf2e736c566bef1164e7a442519d", "type": "file", "dest-filename": "external/tarballs/libpagemaker-0.0.4.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/librevenge-0.0.4.tar.bz2", "sha256": "c51601cd08320b75702812c64aae0653409164da7825fd0f451ac2c5dbe77cbf", "type": "file", "dest-filename": "external/tarballs/librevenge-0.0.4.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/libstaroffice-0.0.7.tar.xz", "sha256": "f94fb0ad8216f97127bedef163a45886b43c62deac5e5b0f5e628e234220c8db", "type": "file", "dest-filename": "external/tarballs/libstaroffice-0.0.7.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/ltm-1.0.zip", "sha256": "083daa92d8ee6f4af96a6143b12d7fc8fe1a547e14f862304f7281f8f7347483", "type": "file", "dest-filename": "external/tarballs/ltm-1.0.zip" }, { "url": "https://dev-www.libreoffice.org/src/libvisio-0.1.7.tar.xz", "sha256": "8faf8df870cb27b09a787a1959d6c646faa44d0d8ab151883df408b7166bea4c", "type": "file", "dest-filename": "external/tarballs/libvisio-0.1.7.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libwpd-0.10.3.tar.xz", "sha256": "2465b0b662fdc5d4e3bebcdc9a79027713fb629ca2bff04a3c9251fdec42dd09", "type": "file", "dest-filename": "external/tarballs/libwpd-0.10.3.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libwpg-0.3.3.tar.xz", "sha256": "99b3f7f8832385748582ab8130fbb9e5607bd5179bebf9751ac1d51a53099d1c", "type": "file", "dest-filename": "external/tarballs/libwpg-0.3.3.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libwps-0.4.11.tar.xz", "sha256": "a8fdaabc28654a975fa78c81873ac503ba18f0d1cdbb942f470a21d29284b4d1", "type": "file", "dest-filename": "external/tarballs/libwps-0.4.11.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libzmf-0.0.2.tar.xz", "sha256": "27051a30cb057fdb5d5de65a1f165c7153dc76e27fe62251cbb86639eb2caf22", "type": "file", "dest-filename": "external/tarballs/libzmf-0.0.2.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/26b3e95ddf3d9c077c480ea45874b3b8-lp_solve_5.5.tar.gz", "sha256": "171816288f14215c69e730f7a4f1c325739873e21f946ff83884b350574e6695", "type": "file", "dest-filename": "external/tarballs/26b3e95ddf3d9c077c480ea45874b3b8-lp_solve_5.5.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/mariadb-connector-c-3.1.8-src.tar.gz", "sha256": "431434d3926f4bcce2e5c97240609983f60d7ff50df5a72083934759bb863f7b", "type": "file", "dest-filename": "external/tarballs/mariadb-connector-c-3.1.8-src.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/mdds-1.6.0.tar.bz2", "sha256": "f1585c9cbd12f83a6d43d395ac1ab6a9d9d5d77f062c7b5f704e24ed72dae07d", "type": "file", "dest-filename": "external/tarballs/mdds-1.6.0.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/neon-0.30.2.tar.gz", "sha256": "db0bd8cdec329b48f53a6f00199c92d5ba40b0f015b153718d1b15d3d967fbca", "type": "file", "dest-filename": "external/tarballs/neon-0.30.2.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/noto-fonts-20171024.tar.gz", "sha256": "29acc15a4c4d6b51201ba5d60f303dfbc2e5acbfdb70413c9ae1ed34fa259994", "type": "file", "dest-filename": "external/tarballs/noto-fonts-20171024.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/openldap-2.4.45.tgz", "sha256": "cdd6cffdebcd95161a73305ec13fc7a78e9707b46ca9f84fb897cd5626df3824", "type": "file", "dest-filename": "external/tarballs/openldap-2.4.45.tgz" }, { "url": "https://dev-www.libreoffice.org/src/liborcus-0.15.4.tar.bz2", "sha256": "cfb2aa60825f2a78589ed030c07f46a1ee16ef8a2d1bf2279192fbc1ae5a5f61", "type": "file", "dest-filename": "external/tarballs/liborcus-0.15.4.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/poppler-0.82.0.tar.xz", "sha256": "234f8e573ea57fb6a008e7c1e56bfae1af5d1adf0e65f47555e1ae103874e4df", "type": "file", "dest-filename": "external/tarballs/poppler-0.82.0.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/postgresql-9.2.24.tar.bz2", "sha256": "a754c02f7051c2f21e52f8669a421b50485afcde9a581674d6106326b189d126", "type": "file", "dest-filename": "external/tarballs/postgresql-9.2.24.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/QR-Code-generator-1.4.0.tar.gz", "sha256": "fcdf9fd69fde07ae4dca2351d84271a9de8093002f733b77c70f52f1630f6e4a", "type": "file", "dest-filename": "external/tarballs/QR-Code-generator-1.4.0.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/a39f6c07ddb20d7dd2ff1f95fa21e2cd-raptor2-2.0.15.tar.gz", "sha256": "ada7f0ba54787b33485d090d3d2680533520cd4426d2f7fb4782dd4a6a1480ed", "type": "file", "dest-filename": "external/tarballs/a39f6c07ddb20d7dd2ff1f95fa21e2cd-raptor2-2.0.15.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/1f5def51ca0026cd192958ef07228b52-rasqal-0.9.33.tar.gz", "sha256": "6924c9ac6570bd241a9669f83b467c728a322470bf34f4b2da4f69492ccfd97c", "type": "file", "dest-filename": "external/tarballs/1f5def51ca0026cd192958ef07228b52-rasqal-0.9.33.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/e5be03eda13ef68aabab6e42aa67715e-redland-1.0.17.tar.gz", "sha256": "de1847f7b59021c16bdc72abb4d8e2d9187cd6124d69156f3326dd34ee043681", "type": "file", "dest-filename": "external/tarballs/e5be03eda13ef68aabab6e42aa67715e-redland-1.0.17.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/ReemKufi-0.7.zip", "sha256": "f60c6508d209ce4236d2d7324256c2ffddd480be7e3d6023770b93dc391a605f", "type": "file", "dest-filename": "external/tarballs/ReemKufi-0.7.zip" }, { "url": "https://dev-www.libreoffice.org/src/libepubgen-0.1.1.tar.xz", "sha256": "03e084b994cbeffc8c3dd13303b2cb805f44d8f2c3b79f7690d7e3fc7f6215ad", "type": "file", "dest-filename": "external/tarballs/libepubgen-0.1.1.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libqxp-0.0.2.tar.xz", "sha256": "e137b6b110120a52c98edd02ebdc4095ee08d0d5295a94316a981750095a945c", "type": "file", "dest-filename": "external/tarballs/libqxp-0.0.2.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/alef-1.001.tar.gz", "sha256": "b98b67602a2c8880a1770f0b9e37c190f29a7e2ade5616784f0b89fbdb75bf52", "type": "file", "dest-filename": "external/tarballs/alef-1.001.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/Amiri-0.111.zip", "sha256": "1fbfccced6348b5db2c1c21d5b319cd488e14d055702fa817a0f6cb83d882166", "type": "file", "dest-filename": "external/tarballs/Amiri-0.111.zip" }, { "url": "https://dev-www.libreoffice.org/src/culmus-0.131.tar.gz", "sha256": "dcf112cfcccb76328dcfc095f4d7c7f4d2f7e48d0eed5e78b100d1d77ce2ed1b", "type": "file", "dest-filename": "external/tarballs/culmus-0.131.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/libre-hebrew-1.0.tar.gz", "sha256": "f596257c1db706ce35795b18d7f66a4db99d427725f20e9384914b534142579a", "type": "file", "dest-filename": "external/tarballs/libre-hebrew-1.0.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/Scheherazade-2.100.zip", "sha256": "251c8817ceb87d9b661ce1d5b49e732a0116add10abc046be4b8ba5196e149b5", "type": "file", "dest-filename": "external/tarballs/Scheherazade-2.100.zip" }, { "url": "https://dev-www.libreoffice.org/src/ttf-kacst_2.01+mry.tar.gz", "sha256": "dca00f5e655f2f217a766faa73a81f542c5c204aa3a47017c3c2be0b31d00a56", "type": "file", "dest-filename": "external/tarballs/ttf-kacst_2.01+mry.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/beeca87be45ec87d241ddd0e1bad80c1-bsh-2.0b6-src.zip", "sha256": "9e93c73e23aff644b17dfff656444474c14150e7f3b38b19635e622235e01c96", "type": "file", "dest-filename": "external/tarballs/beeca87be45ec87d241ddd0e1bad80c1-bsh-2.0b6-src.zip" }, { "url": "https://dev-www.libreoffice.org/src/commons-logging-1.2-src.tar.gz", "sha256": "49665da5a60d033e6dff40fe0a7f9173e886ae859ce6096c1afe34c48b677c81", "type": "file", "dest-filename": "external/tarballs/commons-logging-1.2-src.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/ba2930200c9f019c2d93a8c88c651a0f-flow-engine-0.9.4.zip", "sha256": "233f66e8d25c5dd971716d4200203a612a407649686ef3b52075d04b4c9df0dd", "type": "file", "dest-filename": "external/tarballs/ba2930200c9f019c2d93a8c88c651a0f-flow-engine-0.9.4.zip" }, { "url": "https://dev-www.libreoffice.org/src/d8bd5eed178db6e2b18eeed243f85aa8-flute-1.1.6.zip", "sha256": "1b5b24f7bc543c0362b667692f78db8bab4ed6dafc6172f104d0bd3757d8a133", "type": "file", "dest-filename": "external/tarballs/d8bd5eed178db6e2b18eeed243f85aa8-flute-1.1.6.zip" }, { "url": "https://dev-www.libreoffice.org/src/17410483b5b5f267aa18b7e00b65e6e0-hsqldb_1_8_0.zip", "sha256": "d30b13f4ba2e3b6a2d4f020c0dee0a9fb9fc6fbcc2d561f36b78da4bf3802370", "type": "file", "dest-filename": "external/tarballs/17410483b5b5f267aa18b7e00b65e6e0-hsqldb_1_8_0.zip" }, { "url": "https://dev-www.libreoffice.org/src/eeb2c7ddf0d302fba4bfc6e97eac9624-libbase-1.1.6.zip", "sha256": "75c80359c9ce343c20aab8a36a45cb3b9ee7c61cf92c13ae45399d854423a9ba", "type": "file", "dest-filename": "external/tarballs/eeb2c7ddf0d302fba4bfc6e97eac9624-libbase-1.1.6.zip" }, { "url": "https://dev-www.libreoffice.org/src/3bdf40c0d199af31923e900d082ca2dd-libfonts-1.1.6.zip", "sha256": "e0531091787c0f16c83965fdcbc49162c059d7f0c64669e7f119699321549743", "type": "file", "dest-filename": "external/tarballs/3bdf40c0d199af31923e900d082ca2dd-libfonts-1.1.6.zip" }, { "url": "https://dev-www.libreoffice.org/src/3404ab6b1792ae5f16bbd603bd1e1d03-libformula-1.1.7.zip", "sha256": "5826d1551bf599b85742545f6e01a0079b93c1b2c8434bf409eddb3a29e4726b", "type": "file", "dest-filename": "external/tarballs/3404ab6b1792ae5f16bbd603bd1e1d03-libformula-1.1.7.zip" }, { "url": "https://dev-www.libreoffice.org/src/db60e4fde8dd6d6807523deb71ee34dc-liblayout-0.2.10.zip", "sha256": "e1fb87f3f7b980d33414473279615c4644027e013012d156efa538bc2b031772", "type": "file", "dest-filename": "external/tarballs/db60e4fde8dd6d6807523deb71ee34dc-liblayout-0.2.10.zip" }, { "url": "https://dev-www.libreoffice.org/src/97b2d4dba862397f446b217e2b623e71-libloader-1.1.6.zip", "sha256": "3d853b19b1d94a6efa69e7af90f7f2b09ecf302913bee3da796c15ecfebcfac8", "type": "file", "dest-filename": "external/tarballs/97b2d4dba862397f446b217e2b623e71-libloader-1.1.6.zip" }, { "url": "https://dev-www.libreoffice.org/src/8ce2fcd72becf06c41f7201d15373ed9-librepository-1.1.6.zip", "sha256": "abe2c57ac12ba45d83563b02e240fa95d973376de2f720aab8fe11f2e621c095", "type": "file", "dest-filename": "external/tarballs/8ce2fcd72becf06c41f7201d15373ed9-librepository-1.1.6.zip" }, { "url": "https://dev-www.libreoffice.org/src/f94d9870737518e3b597f9265f4e9803-libserializer-1.1.6.zip", "sha256": "05640a1f6805b2b2d7e2cb9c50db9a5cb084e3c52ab1a71ce015239b4a1d4343", "type": "file", "dest-filename": "external/tarballs/f94d9870737518e3b597f9265f4e9803-libserializer-1.1.6.zip" }, { "url": "https://dev-www.libreoffice.org/src/ace6ab49184e329db254e454a010f56d-libxml-1.1.7.zip", "sha256": "7d2797fe9f79a77009721e3f14fa4a1dec17a6d706bdc93f85f1f01d124fab66", "type": "file", "dest-filename": "external/tarballs/ace6ab49184e329db254e454a010f56d-libxml-1.1.7.zip" }, { "url": "https://dev-www.libreoffice.org/src/798b2ffdc8bcfe7bca2cf92b62caf685-rhino1_5R5.zip", "sha256": "1fb458d6aab06932693cc8a9b6e4e70944ee1ff052fa63606e3131df34e21753", "type": "file", "dest-filename": "external/tarballs/798b2ffdc8bcfe7bca2cf92b62caf685-rhino1_5R5.zip" }, { "url": "https://dev-www.libreoffice.org/src/39bb3fcea1514f1369fcfc87542390fd-sacjava-1.3.zip", "sha256": "085f2112c51fa8c1783fac12fbd452650596415121348393bb51f0f7e85a9045", "type": "file", "dest-filename": "external/tarballs/39bb3fcea1514f1369fcfc87542390fd-sacjava-1.3.zip" }, { "url": "https://dev-www.libreoffice.org/src/35c94d2df8893241173de1d16b6034c0-swingExSrc.zip", "sha256": "64585ac36a81291a58269ec5347e7e3e2e8596dbacb9221015c208191333c6e1", "type": "file", "dest-filename": "external/tarballs/35c94d2df8893241173de1d16b6034c0-swingExSrc.zip" }, { "url": "https://dev-www.libreoffice.org/src/libnumbertext-1.0.6.tar.xz", "sha256": "739f220b34bf7cb731c09de2921771d644d37dfd276c45564401e5759f10ae57", "type": "file", "dest-filename": "external/tarballs/libnumbertext-1.0.6.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libatomic_ops-7.6.8.tar.gz", "sha256": "1d6a279edf81767e74d2ad2c9fce09459bc65f12c6525a40b0cb3e53c089f665", "type": "file", "dest-filename": "external/tarballs/libatomic_ops-7.6.8.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/dtoa-20180411.tgz", "sha256": "0082d0684f7db6f62361b76c4b7faba19e0c7ce5cb8e36c4b65fea8281e711b4", "type": "file", "dest-filename": "external/tarballs/dtoa-20180411.tgz" }, { "url": "https://dev-www.libreoffice.org/extern/884ed41809687c3e168fc7c19b16585149ff058eca79acbf3ee784f6630704cc-opens___.ttf", "sha256": "884ed41809687c3e168fc7c19b16585149ff058eca79acbf3ee784f6630704cc", "type": "file", "dest-filename": "external/tarballs/884ed41809687c3e168fc7c19b16585149ff058eca79acbf3ee784f6630704cc-opens___.ttf" } ], "buildsystem": "simple", "build-commands": [ "./autogen.sh --prefix=/run/build/libreoffice/inst --with-distro=LibreOfficeFlatpak --with-gdrive-client-id=280867422816-9jc83t6phkfb2q8p94dtk8kr5fa8af3r.apps.googleusercontent.com --with-gdrive-client-secret=Hnzikj7HqdqsUYLru4jmFo1p --disable-pdfimport --disable-crashdump --disable-odk --disable-dbus --disable-gui --disable-sdremote --disable-sdremote-bluetooth --disable-cups --disable-extension-update --disable-dconf --disable-skia --without-helppack-integration", "make $(if test \"$FLATPAK_ARCH\" = i386; then printf build-nocheck; fi)", "make distro-pack-install", "make cmd cmd='$(SRCDIR)/solenv/bin/assemble-flatpak.sh'", "printf '\\nfalse' >/app/libreoffice/share/registry/flatpak.xcd" ] } ] } paperwork-2.1.1/flatpak/shared-modules/libreoffice-7.2.3.2.json000066400000000000000000001106701417573700700240540ustar00rootroot00000000000000{ "name": "libreoffice-complete", "modules": [ { "name": "openjdk", "buildsystem": "simple", "build-commands": [ "/usr/lib/sdk/openjdk11/install.sh" ] }, { "name": "krb5", "subdir": "src", "config-opts": [ "--disable-static", "--disable-rpath", "--sbindir=/app/bin" ], "cleanup": [ "/include", "/lib/pkgconfig", "/var" ], "sources": [ { "type": "archive", "url": "https://kerberos.org/dist/krb5/1.16/krb5-1.16.2.tar.gz", "sha256": "9f721e1fe593c219174740c71de514c7228a97d23eb7be7597b2ae14e487f027" } ] }, { "name": "libreoffice", "sources": [ { "type": "git", "url": "https://gerrit.libreoffice.org/core", "tag": "libreoffice-7.2.3.2", "disable-fsckobjects": true }, { "type": "archive", "url": "https://archive.apache.org/dist/ant/binaries/apache-ant-1.10.5-bin.tar.xz", "sha256": "cebb705dbbe26a41d359b8be08ec066caba4e8686670070ce44bbf2b57ae113f", "dest": "ant" }, { "commands": [ "mkdir external/tarballs" ], "type": "shell" }, { "url": "https://dev-www.libreoffice.org/src/pdfium-4500.tar.bz2", "sha256": "26a03dd60e5ed0979cdaba9cc848242895110ddfdf347d40989ce2f14020f304", "type": "file", "dest": "external/tarballs", "dest-filename": "pdfium-4500.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/0168229624cfac409e766913506961a8-ucpp-1.3.2.tar.gz", "sha256": "983941d31ee8d366085cadf28db75eb1f5cb03ba1e5853b98f12f7f51c63b776", "type": "file", "dest": "external/tarballs", "dest-filename": "0168229624cfac409e766913506961a8-ucpp-1.3.2.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/xmlsec1-1.2.32.tar.gz", "sha256": "e383702853236004e5b08e424b8afe9b53fe9f31aaa7a5382f39d9533eb7c043", "type": "file", "dest": "external/tarballs", "dest-filename": "xmlsec1-1.2.32.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/368f114c078f94214a308a74c7e991bc-crosextrafonts-20130214.tar.gz", "sha256": "c48d1c2fd613c9c06c959c34da7b8388059e2408d2bb19845dc3ed35f76e4d09", "type": "file", "dest": "external/tarballs", "dest-filename": "368f114c078f94214a308a74c7e991bc-crosextrafonts-20130214.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/c74b7223abe75949b4af367942d96c7a-crosextrafonts-carlito-20130920.tar.gz", "sha256": "4bd12b6cbc321c1cf16da76e2c585c925ce956a08067ae6f6c64eff6ccfdaf5a", "type": "file", "dest": "external/tarballs", "dest-filename": "c74b7223abe75949b4af367942d96c7a-crosextrafonts-carlito-20130920.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/33e1e61fab06a547851ed308b4ffef42-dejavu-fonts-ttf-2.37.zip", "sha256": "7576310b219e04159d35ff61dd4a4ec4cdba4f35c00e002a136f00e96a908b0a", "type": "file", "dest": "external/tarballs", "dest-filename": "33e1e61fab06a547851ed308b4ffef42-dejavu-fonts-ttf-2.37.zip" }, { "url": "https://dev-www.libreoffice.org/src/1725634df4bb3dcb1b2c91a6175f8789-GentiumBasic_1102.zip", "sha256": "2f1a2c5491d7305dffd3520c6375d2f3e14931ee35c6d8ae1e8f098bf1a7b3cc", "type": "file", "dest": "external/tarballs", "dest-filename": "1725634df4bb3dcb1b2c91a6175f8789-GentiumBasic_1102.zip" }, { "url": "https://dev-www.libreoffice.org/src/liberation-narrow-fonts-ttf-1.07.6.tar.gz", "sha256": "8879d89b5ff7b506c9fc28efc31a5c0b954bbe9333e66e5283d27d20a8519ea3", "type": "file", "dest": "external/tarballs", "dest-filename": "liberation-narrow-fonts-ttf-1.07.6.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/liberation-fonts-ttf-2.1.4.tar.gz", "sha256": "26f85412dd0aa9d061504a1cc8aaf0aa12a70710e8d47d8b65a1251757c1a5ef", "type": "file", "dest": "external/tarballs", "dest-filename": "liberation-fonts-ttf-2.1.4.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/e7a384790b13c29113e22e596ade9687-LinLibertineG-20120116.zip", "sha256": "54adcb2bc8cac0927a647fbd9362f45eff48130ce6e2379dc3867643019e08c5", "type": "file", "dest": "external/tarballs", "dest-filename": "e7a384790b13c29113e22e596ade9687-LinLibertineG-20120116.zip" }, { "url": "https://dev-www.libreoffice.org/src/907d6e99f241876695c19ff3db0b8923-source-code-pro-2.030R-ro-1.050R-it.tar.gz", "sha256": "09466dce87653333f189acd8358c60c6736dcd95f042dee0b644bdcf65b6ae2f", "type": "file", "dest": "external/tarballs", "dest-filename": "907d6e99f241876695c19ff3db0b8923-source-code-pro-2.030R-ro-1.050R-it.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/edc4d741888bc0d38e32dbaa17149596-source-sans-pro-2.010R-ro-1.065R-it.tar.gz", "sha256": "e7bc9a1fec787a529e49f5a26b93dcdcf41506449dfc70f92cdef6d17eb6fb61", "type": "file", "dest": "external/tarballs", "dest-filename": "edc4d741888bc0d38e32dbaa17149596-source-sans-pro-2.010R-ro-1.065R-it.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/source-serif-pro-3.000R.tar.gz", "sha256": "826a2b784d5cdb4c2bbc7830eb62871528360a61a52689c102a101623f1928e3", "type": "file", "dest": "external/tarballs", "dest-filename": "source-serif-pro-3.000R.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/EmojiOneColor-SVGinOT-1.3.tar.gz", "sha256": "d1a08f7c10589f22740231017694af0a7a270760c8dec33d8d1c038e2be0a0c7", "type": "file", "dest": "external/tarballs", "dest-filename": "EmojiOneColor-SVGinOT-1.3.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/boost_1_75_0.tar.xz", "sha256": "cc378a036a1cfd3af289f3da24deeb8dba7a729f61ab104c7b018a622e22d21b", "type": "file", "dest": "external/tarballs", "dest-filename": "boost_1_75_0.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/48d647fbd8ef8889e5a7f422c1bfda94-clucene-core-2.3.3.4.tar.gz", "sha256": "ddfdc433dd8ad31b5c5819cc4404a8d2127472a3b720d3e744e8c51d79732eab", "type": "file", "dest": "external/tarballs", "dest-filename": "48d647fbd8ef8889e5a7f422c1bfda94-clucene-core-2.3.3.4.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/CoinMP-1.7.6.tgz", "sha256": "86c798780b9e1f5921fe4efe651a93cb420623b45aa1fdff57af8c37f116113f", "type": "file", "dest": "external/tarballs", "dest-filename": "CoinMP-1.7.6.tgz" }, { "url": "https://dev-www.libreoffice.org/src/cppunit-1.15.1.tar.gz", "sha256": "89c5c6665337f56fd2db36bc3805a5619709d51fb136e51937072f63fcc717a7", "type": "file", "dest": "external/tarballs", "dest-filename": "cppunit-1.15.1.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/Firebird-3.0.7.33374-0.tar.bz2", "sha256": "acb85cedafa10ce106b1823fb236b1b3e5d942a5741e8f8435cc8ccfec0afe76", "type": "file", "dest": "external/tarballs", "dest-filename": "Firebird-3.0.7.33374-0.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/glm-0.9.9.7.zip", "sha256": "c5e167c042afd2d7ad642ace6b643863baeb33880781983563e1ab68a30d3e95", "type": "file", "dest": "external/tarballs", "dest-filename": "glm-0.9.9.7.zip" }, { "url": "https://dev-www.libreoffice.org/src/gpgme-1.13.1.tar.bz2", "sha256": "c4e30b227682374c23cddc7fdb9324a99694d907e79242a25a4deeedb393be46", "type": "file", "dest": "external/tarballs", "dest-filename": "gpgme-1.13.1.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/libassuan-2.5.3.tar.bz2", "sha256": "91bcb0403866b4e7c4bc1cc52ed4c364a9b5414b3994f718c70303f7f765e702", "type": "file", "dest": "external/tarballs", "dest-filename": "libassuan-2.5.3.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/libgpg-error-1.37.tar.bz2", "sha256": "b32d6ff72a73cf79797f7f2d039e95e9c6f92f0c1450215410840ab62aea9763", "type": "file", "dest": "external/tarballs", "dest-filename": "libgpg-error-1.37.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/libabw-0.1.3.tar.xz", "sha256": "e763a9dc21c3d2667402d66e202e3f8ef4db51b34b79ef41f56cacb86dcd6eed", "type": "file", "dest": "external/tarballs", "dest-filename": "libabw-0.1.3.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libcdr-0.1.7.tar.xz", "sha256": "5666249d613466b9aa1e987ea4109c04365866e9277d80f6cd9663e86b8ecdd4", "type": "file", "dest": "external/tarballs", "dest-filename": "libcdr-0.1.7.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libcmis-0.5.2.tar.xz", "sha256": "d7b18d9602190e10d437f8a964a32e983afd57e2db316a07d87477a79f5000a2", "type": "file", "dest": "external/tarballs", "dest-filename": "libcmis-0.5.2.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libe-book-0.1.3.tar.xz", "sha256": "7e8d8ff34f27831aca3bc6f9cc532c2f90d2057c778963b884ff3d1e34dfe1f9", "type": "file", "dest": "external/tarballs", "dest-filename": "libe-book-0.1.3.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libetonyek-0.1.10.tar.xz", "sha256": "b430435a6e8487888b761dc848b7981626eb814884963ffe25eb26a139301e9a", "type": "file", "dest": "external/tarballs", "dest-filename": "libetonyek-0.1.10.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libexttextcat-3.4.5.tar.xz", "sha256": "13fdbc9d4c489a4d0519e51933a1aa21fe3fb9eb7da191b87f7a63e82797dac8", "type": "file", "dest": "external/tarballs", "dest-filename": "libexttextcat-3.4.5.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libfreehand-0.1.2.tar.xz", "sha256": "0e422d1564a6dbf22a9af598535425271e583514c0f7ba7d9091676420de34ac", "type": "file", "dest": "external/tarballs", "dest-filename": "libfreehand-0.1.2.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/language-subtag-registry-2021-08-06.tar.bz2", "sha256": "08452d3997c78e21f2d81e31409dc46557707be6dc1df3129674019659e5ff9b", "type": "file", "dest": "external/tarballs", "dest-filename": "language-subtag-registry-2021-08-06.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/liblangtag-0.6.2.tar.bz2", "sha256": "d6242790324f1432fb0a6fae71b6851f520b2c5a87675497cf8ea14c2924d52e", "type": "file", "dest": "external/tarballs", "dest-filename": "liblangtag-0.6.2.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/libmspub-0.1.4.tar.xz", "sha256": "ef36c1a1aabb2ba3b0bedaaafe717bf4480be2ba8de6f3894be5fd3702b013ba", "type": "file", "dest": "external/tarballs", "dest-filename": "libmspub-0.1.4.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libmwaw-0.3.19.tar.xz", "sha256": "b272e234eefc828c4bb8344af0f047a62e070f530e9e2fba11b04c8db8eda5af", "type": "file", "dest": "external/tarballs", "dest-filename": "libmwaw-0.3.19.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libodfgen-0.1.8.tar.xz", "sha256": "55200027fd46623b9bdddd38d275e7452d1b0ff8aeddcad6f9ae6dc25f610625", "type": "file", "dest": "external/tarballs", "dest-filename": "libodfgen-0.1.8.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libpagemaker-0.0.4.tar.xz", "sha256": "66adacd705a7d19895e08eac46d1e851332adf2e736c566bef1164e7a442519d", "type": "file", "dest": "external/tarballs", "dest-filename": "libpagemaker-0.0.4.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/librevenge-0.0.4.tar.bz2", "sha256": "c51601cd08320b75702812c64aae0653409164da7825fd0f451ac2c5dbe77cbf", "type": "file", "dest": "external/tarballs", "dest-filename": "librevenge-0.0.4.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/libstaroffice-0.0.7.tar.xz", "sha256": "f94fb0ad8216f97127bedef163a45886b43c62deac5e5b0f5e628e234220c8db", "type": "file", "dest": "external/tarballs", "dest-filename": "libstaroffice-0.0.7.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/ltm-1.0.zip", "sha256": "083daa92d8ee6f4af96a6143b12d7fc8fe1a547e14f862304f7281f8f7347483", "type": "file", "dest": "external/tarballs", "dest-filename": "ltm-1.0.zip" }, { "url": "https://dev-www.libreoffice.org/src/libvisio-0.1.7.tar.xz", "sha256": "8faf8df870cb27b09a787a1959d6c646faa44d0d8ab151883df408b7166bea4c", "type": "file", "dest": "external/tarballs", "dest-filename": "libvisio-0.1.7.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libwpd-0.10.3.tar.xz", "sha256": "2465b0b662fdc5d4e3bebcdc9a79027713fb629ca2bff04a3c9251fdec42dd09", "type": "file", "dest": "external/tarballs", "dest-filename": "libwpd-0.10.3.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libwpg-0.3.3.tar.xz", "sha256": "99b3f7f8832385748582ab8130fbb9e5607bd5179bebf9751ac1d51a53099d1c", "type": "file", "dest": "external/tarballs", "dest-filename": "libwpg-0.3.3.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libwps-0.4.12.tar.xz", "sha256": "e21afb52a06d03b774c5a8c72679687ab64891b91ce0c3bdf2d3e97231534edb", "type": "file", "dest": "external/tarballs", "dest-filename": "libwps-0.4.12.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libzmf-0.0.2.tar.xz", "sha256": "27051a30cb057fdb5d5de65a1f165c7153dc76e27fe62251cbb86639eb2caf22", "type": "file", "dest": "external/tarballs", "dest-filename": "libzmf-0.0.2.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/26b3e95ddf3d9c077c480ea45874b3b8-lp_solve_5.5.tar.gz", "sha256": "171816288f14215c69e730f7a4f1c325739873e21f946ff83884b350574e6695", "type": "file", "dest": "external/tarballs", "dest-filename": "26b3e95ddf3d9c077c480ea45874b3b8-lp_solve_5.5.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/mariadb-connector-c-3.1.8-src.tar.gz", "sha256": "431434d3926f4bcce2e5c97240609983f60d7ff50df5a72083934759bb863f7b", "type": "file", "dest": "external/tarballs", "dest-filename": "mariadb-connector-c-3.1.8-src.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/mdds-1.7.0.tar.bz2", "sha256": "a66a2a8293a3abc6cd9baff7c236156e2666935cbfb69a15d64d38141638fecf", "type": "file", "dest": "external/tarballs", "dest-filename": "mdds-1.7.0.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/neon-0.31.2.tar.gz", "sha256": "cf1ee3ac27a215814a9c80803fcee4f0ede8466ebead40267a9bd115e16a8678", "type": "file", "dest": "external/tarballs", "dest-filename": "neon-0.31.2.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/noto-fonts-20171024.tar.gz", "sha256": "29acc15a4c4d6b51201ba5d60f303dfbc2e5acbfdb70413c9ae1ed34fa259994", "type": "file", "dest": "external/tarballs", "dest-filename": "noto-fonts-20171024.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/openldap-2.4.59.tgz", "sha256": "99f37d6747d88206c470067eda624d5e48c1011e943ec0ab217bae8712e22f34", "type": "file", "dest": "external/tarballs", "dest-filename": "openldap-2.4.59.tgz" }, { "url": "https://dev-www.libreoffice.org/src/liborcus-0.16.1.tar.bz2", "sha256": "c700d1325f744104d9fca0d5a019434901e9d51a16eedfb05792f90a298587a4", "type": "file", "dest": "external/tarballs", "dest-filename": "liborcus-0.16.1.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/poppler-21.01.0.tar.xz", "sha256": "016dde34e5f868ea98a32ca99b643325a9682281500942b7113f4ec88d20e2f3", "type": "file", "dest": "external/tarballs", "dest-filename": "poppler-21.01.0.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/poppler-data-0.4.10.tar.gz", "sha256": "6e2fcef66ec8c44625f94292ccf8af9f1d918b410d5aa69c274ce67387967b30", "type": "file", "dest": "external/tarballs", "dest-filename": "poppler-data-0.4.10.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/postgresql-13.5.tar.bz2", "sha256": "9b81067a55edbaabc418aacef457dd8477642827499560b00615a6ea6c13f6b3", "type": "file", "dest": "external/tarballs", "dest-filename": "postgresql-13.5.tar.bz2" }, { "url": "https://dev-www.libreoffice.org/src/a39f6c07ddb20d7dd2ff1f95fa21e2cd-raptor2-2.0.15.tar.gz", "sha256": "ada7f0ba54787b33485d090d3d2680533520cd4426d2f7fb4782dd4a6a1480ed", "type": "file", "dest": "external/tarballs", "dest-filename": "a39f6c07ddb20d7dd2ff1f95fa21e2cd-raptor2-2.0.15.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/1f5def51ca0026cd192958ef07228b52-rasqal-0.9.33.tar.gz", "sha256": "6924c9ac6570bd241a9669f83b467c728a322470bf34f4b2da4f69492ccfd97c", "type": "file", "dest": "external/tarballs", "dest-filename": "1f5def51ca0026cd192958ef07228b52-rasqal-0.9.33.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/e5be03eda13ef68aabab6e42aa67715e-redland-1.0.17.tar.gz", "sha256": "de1847f7b59021c16bdc72abb4d8e2d9187cd6124d69156f3326dd34ee043681", "type": "file", "dest": "external/tarballs", "dest-filename": "e5be03eda13ef68aabab6e42aa67715e-redland-1.0.17.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/ReemKufi-0.7.zip", "sha256": "f60c6508d209ce4236d2d7324256c2ffddd480be7e3d6023770b93dc391a605f", "type": "file", "dest": "external/tarballs", "dest-filename": "ReemKufi-0.7.zip" }, { "url": "https://dev-www.libreoffice.org/src/libepubgen-0.1.1.tar.xz", "sha256": "03e084b994cbeffc8c3dd13303b2cb805f44d8f2c3b79f7690d7e3fc7f6215ad", "type": "file", "dest": "external/tarballs", "dest-filename": "libepubgen-0.1.1.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libqxp-0.0.2.tar.xz", "sha256": "e137b6b110120a52c98edd02ebdc4095ee08d0d5295a94316a981750095a945c", "type": "file", "dest": "external/tarballs", "dest-filename": "libqxp-0.0.2.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/alef-1.001.tar.gz", "sha256": "b98b67602a2c8880a1770f0b9e37c190f29a7e2ade5616784f0b89fbdb75bf52", "type": "file", "dest": "external/tarballs", "dest-filename": "alef-1.001.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/Amiri-0.111.zip", "sha256": "1fbfccced6348b5db2c1c21d5b319cd488e14d055702fa817a0f6cb83d882166", "type": "file", "dest": "external/tarballs", "dest-filename": "Amiri-0.111.zip" }, { "url": "https://dev-www.libreoffice.org/src/culmus-0.133.tar.gz", "sha256": "c0c6873742d07544f6bacf2ad52eb9cb392974d56427938dc1dfbc8399c64d05", "type": "file", "dest": "external/tarballs", "dest-filename": "culmus-0.133.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/libre-hebrew-1.0.tar.gz", "sha256": "f596257c1db706ce35795b18d7f66a4db99d427725f20e9384914b534142579a", "type": "file", "dest": "external/tarballs", "dest-filename": "libre-hebrew-1.0.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/Scheherazade-2.100.zip", "sha256": "251c8817ceb87d9b661ce1d5b49e732a0116add10abc046be4b8ba5196e149b5", "type": "file", "dest": "external/tarballs", "dest-filename": "Scheherazade-2.100.zip" }, { "url": "https://dev-www.libreoffice.org/src/ttf-kacst_2.01+mry.tar.gz", "sha256": "dca00f5e655f2f217a766faa73a81f542c5c204aa3a47017c3c2be0b31d00a56", "type": "file", "dest": "external/tarballs", "dest-filename": "ttf-kacst_2.01+mry.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/beeca87be45ec87d241ddd0e1bad80c1-bsh-2.0b6-src.zip", "sha256": "9e93c73e23aff644b17dfff656444474c14150e7f3b38b19635e622235e01c96", "type": "file", "dest": "external/tarballs", "dest-filename": "beeca87be45ec87d241ddd0e1bad80c1-bsh-2.0b6-src.zip" }, { "url": "https://dev-www.libreoffice.org/src/ba2930200c9f019c2d93a8c88c651a0f-flow-engine-0.9.4.zip", "sha256": "233f66e8d25c5dd971716d4200203a612a407649686ef3b52075d04b4c9df0dd", "type": "file", "dest": "external/tarballs", "dest-filename": "ba2930200c9f019c2d93a8c88c651a0f-flow-engine-0.9.4.zip" }, { "url": "https://dev-www.libreoffice.org/src/d8bd5eed178db6e2b18eeed243f85aa8-flute-1.1.6.zip", "sha256": "1b5b24f7bc543c0362b667692f78db8bab4ed6dafc6172f104d0bd3757d8a133", "type": "file", "dest": "external/tarballs", "dest-filename": "d8bd5eed178db6e2b18eeed243f85aa8-flute-1.1.6.zip" }, { "url": "https://dev-www.libreoffice.org/src/17410483b5b5f267aa18b7e00b65e6e0-hsqldb_1_8_0.zip", "sha256": "d30b13f4ba2e3b6a2d4f020c0dee0a9fb9fc6fbcc2d561f36b78da4bf3802370", "type": "file", "dest": "external/tarballs", "dest-filename": "17410483b5b5f267aa18b7e00b65e6e0-hsqldb_1_8_0.zip" }, { "url": "https://dev-www.libreoffice.org/src/eeb2c7ddf0d302fba4bfc6e97eac9624-libbase-1.1.6.zip", "sha256": "75c80359c9ce343c20aab8a36a45cb3b9ee7c61cf92c13ae45399d854423a9ba", "type": "file", "dest": "external/tarballs", "dest-filename": "eeb2c7ddf0d302fba4bfc6e97eac9624-libbase-1.1.6.zip" }, { "url": "https://dev-www.libreoffice.org/src/3bdf40c0d199af31923e900d082ca2dd-libfonts-1.1.6.zip", "sha256": "e0531091787c0f16c83965fdcbc49162c059d7f0c64669e7f119699321549743", "type": "file", "dest": "external/tarballs", "dest-filename": "3bdf40c0d199af31923e900d082ca2dd-libfonts-1.1.6.zip" }, { "url": "https://dev-www.libreoffice.org/src/3404ab6b1792ae5f16bbd603bd1e1d03-libformula-1.1.7.zip", "sha256": "5826d1551bf599b85742545f6e01a0079b93c1b2c8434bf409eddb3a29e4726b", "type": "file", "dest": "external/tarballs", "dest-filename": "3404ab6b1792ae5f16bbd603bd1e1d03-libformula-1.1.7.zip" }, { "url": "https://dev-www.libreoffice.org/src/db60e4fde8dd6d6807523deb71ee34dc-liblayout-0.2.10.zip", "sha256": "e1fb87f3f7b980d33414473279615c4644027e013012d156efa538bc2b031772", "type": "file", "dest": "external/tarballs", "dest-filename": "db60e4fde8dd6d6807523deb71ee34dc-liblayout-0.2.10.zip" }, { "url": "https://dev-www.libreoffice.org/src/97b2d4dba862397f446b217e2b623e71-libloader-1.1.6.zip", "sha256": "3d853b19b1d94a6efa69e7af90f7f2b09ecf302913bee3da796c15ecfebcfac8", "type": "file", "dest": "external/tarballs", "dest-filename": "97b2d4dba862397f446b217e2b623e71-libloader-1.1.6.zip" }, { "url": "https://dev-www.libreoffice.org/src/8ce2fcd72becf06c41f7201d15373ed9-librepository-1.1.6.zip", "sha256": "abe2c57ac12ba45d83563b02e240fa95d973376de2f720aab8fe11f2e621c095", "type": "file", "dest": "external/tarballs", "dest-filename": "8ce2fcd72becf06c41f7201d15373ed9-librepository-1.1.6.zip" }, { "url": "https://dev-www.libreoffice.org/src/f94d9870737518e3b597f9265f4e9803-libserializer-1.1.6.zip", "sha256": "05640a1f6805b2b2d7e2cb9c50db9a5cb084e3c52ab1a71ce015239b4a1d4343", "type": "file", "dest": "external/tarballs", "dest-filename": "f94d9870737518e3b597f9265f4e9803-libserializer-1.1.6.zip" }, { "url": "https://dev-www.libreoffice.org/src/ace6ab49184e329db254e454a010f56d-libxml-1.1.7.zip", "sha256": "7d2797fe9f79a77009721e3f14fa4a1dec17a6d706bdc93f85f1f01d124fab66", "type": "file", "dest": "external/tarballs", "dest-filename": "ace6ab49184e329db254e454a010f56d-libxml-1.1.7.zip" }, { "url": "https://dev-www.libreoffice.org/src/798b2ffdc8bcfe7bca2cf92b62caf685-rhino1_5R5.zip", "sha256": "1fb458d6aab06932693cc8a9b6e4e70944ee1ff052fa63606e3131df34e21753", "type": "file", "dest": "external/tarballs", "dest-filename": "798b2ffdc8bcfe7bca2cf92b62caf685-rhino1_5R5.zip" }, { "url": "https://dev-www.libreoffice.org/src/39bb3fcea1514f1369fcfc87542390fd-sacjava-1.3.zip", "sha256": "085f2112c51fa8c1783fac12fbd452650596415121348393bb51f0f7e85a9045", "type": "file", "dest": "external/tarballs", "dest-filename": "39bb3fcea1514f1369fcfc87542390fd-sacjava-1.3.zip" }, { "url": "https://dev-www.libreoffice.org/src/35c94d2df8893241173de1d16b6034c0-swingExSrc.zip", "sha256": "64585ac36a81291a58269ec5347e7e3e2e8596dbacb9221015c208191333c6e1", "type": "file", "dest": "external/tarballs", "dest-filename": "35c94d2df8893241173de1d16b6034c0-swingExSrc.zip" }, { "url": "https://dev-www.libreoffice.org/src/libnumbertext-1.0.7.tar.xz", "sha256": "17b8249cb89ae11ae15a85612d2665626c0e0e3e56b35654363ba6566d8b61fc", "type": "file", "dest": "external/tarballs", "dest-filename": "libnumbertext-1.0.7.tar.xz" }, { "url": "https://dev-www.libreoffice.org/src/libatomic_ops-7.6.8.tar.gz", "sha256": "1d6a279edf81767e74d2ad2c9fce09459bc65f12c6525a40b0cb3e53c089f665", "type": "file", "dest": "external/tarballs", "dest-filename": "libatomic_ops-7.6.8.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/dtoa-20180411.tgz", "sha256": "0082d0684f7db6f62361b76c4b7faba19e0c7ce5cb8e36c4b65fea8281e711b4", "type": "file", "dest": "external/tarballs", "dest-filename": "dtoa-20180411.tgz" }, { "url": "https://dev-www.libreoffice.org/src/box2d-2.3.1.tar.gz", "sha256": "58ffc8475a8650aadc351345aef696937747b40501ab78d72c197c5ff5b3035c", "type": "file", "dest": "external/tarballs", "dest-filename": "box2d-2.3.1.tar.gz" }, { "url": "https://dev-www.libreoffice.org/src/zxing-cpp-1.1.1.tar.gz", "sha256": "e595b3fa2ec320beb0b28f6af56b1141853257c2611686685639cebb3b248c86", "type": "file", "dest": "external/tarballs", "dest-filename": "zxing-cpp-1.1.1.tar.gz" }, { "url": "https://dev-www.libreoffice.org/extern/f543e6e2d7275557a839a164941c0a86e5f2c3f2a0042bfc434c88c6dde9e140-opens___.ttf", "sha256": "f543e6e2d7275557a839a164941c0a86e5f2c3f2a0042bfc434c88c6dde9e140", "type": "file", "dest": "external/tarballs", "dest-filename": "f543e6e2d7275557a839a164941c0a86e5f2c3f2a0042bfc434c88c6dde9e140-opens___.ttf" } ], "buildsystem": "simple", "build-commands": [ "./autogen.sh --prefix=/run/build/libreoffice/inst --with-distro=LibreOfficeFlatpak --with-gdrive-client-id=280867422816-9jc83t6phkfb2q8p94dtk8kr5fa8af3r.apps.googleusercontent.com --with-gdrive-client-secret=Hnzikj7HqdqsUYLru4jmFo1p --disable-pdfimport --disable-crashdump --disable-odk --disable-dbus --disable-gui --disable-sdremote --disable-sdremote-bluetooth --disable-cups --disable-extension-update --disable-dconf --disable-skia --without-helppack-integration", "make $(if test \"$FLATPAK_ARCH\" = i386; then printf build-nocheck; fi)", "make distro-pack-install", "make cmd cmd='$(SRCDIR)/solenv/bin/assemble-flatpak.sh'", "printf '\\nfalse' >/app/libreoffice/share/registry/flatpak.xcd" ] } ] } paperwork-2.1.1/flatpak/shared-modules/noop-Makefile000066400000000000000000000000661417573700700225110ustar00rootroot00000000000000all: @echo no-op install: @echo no-op .PHONY: all paperwork-2.1.1/flatpak/shared-modules/sane-backends-1.0.27.json000066400000000000000000000011261417573700700242120ustar00rootroot00000000000000{ "name": "sane-backends", "buildsystem": "autotools", "config-opts": [ "--disable-saned" ], "sources": [ { "type": "archive", "url": "https://ftp.osuosl.org/.1/blfs/conglomeration/sane-backends/sane-backends-1.0.27.tar.gz", "sha256": "293747bf37275c424ebb2c833f8588601a60b2f9653945d5a3194875355e36c9" } ], "post-install": [ "rm /app/etc/sane.d/dll.conf", "rm /app/etc/sane.d/net.conf", "echo 127.0.0.1 > /app/etc/sane.d/net.conf", "echo net > /app/etc/sane.d/dll.conf" ] } paperwork-2.1.1/flatpak/shared-modules/sane-backends-1.0.29.json000066400000000000000000000012571417573700700242210ustar00rootroot00000000000000{ "name": "sane-backends", "buildsystem": "autotools", "config-opts": [ "--disable-saned" ], "sources": [ { "type": "archive", "url": "https://ftp.osuosl.org/.1/blfs/conglomeration/sane-backends/sane-backends-1.0.29.tar.gz", "sha256": "aa027b4e5f59849cd41b8c26d54584cf31fffd986049019be6ad4140e11ea8ed" }, { "type": "patch", "path": "sanei_usb.c.diff" } ], "post-install": [ "rm /app/etc/sane.d/dll.conf", "rm /app/etc/sane.d/net.conf", "echo 127.0.0.1 > /app/etc/sane.d/net.conf", "echo net > /app/etc/sane.d/dll.conf" ] } paperwork-2.1.1/flatpak/shared-modules/sane-backends-1.0.32.json000066400000000000000000000013211417573700700242030ustar00rootroot00000000000000{ "name": "sane-backends", "buildsystem": "autotools", "config-opts": [ "--disable-saned" ], "sources": [ { "type": "archive", "url": "https://gitlab.com/sane-project/backends/uploads/104f09c07d35519cc8e72e604f11643f/sane-backends-1.0.32.tar.gz", "sha256": "3a28c237c0a72767086202379f6dc92dbb63ec08dfbab22312cba80e238bb114" } ], "post-install": [ "rm /app/etc/sane.d/dll.conf", "rm /app/etc/sane.d/net.conf", "echo 127.0.0.1 > /app/etc/sane.d/net.conf", "echo net > /app/etc/sane.d/dll.conf", "echo escl >> /app/etc/sane.d/dll.conf", "echo airscan >> /app/etc/sane.d/dll.conf" ] } paperwork-2.1.1/flatpak/shared-modules/sanei_usb.c.diff000066400000000000000000000004111417573700700231150ustar00rootroot00000000000000--- a/sanei/sanei_usb.c 2021-01-04 22:01:10.399478905 +0100 +++ b/sanei/sanei_usb.c 2021-01-04 22:01:13.091463360 +0100 @@ -48,6 +48,7 @@ #include "../include/sane/config.h" +#include #include #include #include paperwork-2.1.1/flatpak/shared-modules/scikit-learn-0.24.0.json000066400000000000000000000147141417573700700241040ustar00rootroot00000000000000{ "name": "python-scikit-sklearn", "buildsystem": "simple", "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "build-commands": [ "python3 setup.py build -j 0", "python3 setup.py install --prefix=/app --root=/ --optimize=1" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/db/e2/9c0bde5f81394b627f623557690536b12017b84988a4a1f98ec826edab9e/scikit-learn-0.24.0.tar.gz", "sha256": "076369634ee72b5a5941440661e2f306ff4ac30903802dc52031c7e9199ac640" } ], "modules": [ { "name": "python-cython", "buildsystem": "simple", "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "build-commands": [ "python3 setup.py build -j 0", "python3 setup.py install --prefix=/app --root=/ --optimize=1" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/6c/9f/f501ba9d178aeb1f5bf7da1ad5619b207c90ac235d9859961c11829d0160/Cython-0.29.21.tar.gz", "sha256": "e57acb89bd55943c8d8bf813763d20b9099cc7165c0f16b707631a7654be9cad" } ] }, { "name": "python-pybind11", "buildsystem": "simple", "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "build-commands": [ "python3 setup.py build -j 0", "python3 setup.py install --prefix=/app --root=/ --optimize=1" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/d8/47/2eb4be23fa8cc1a08c855c012c1aa4348d06ab1a5527f876515bbf689644/pybind11-2.6.1.tar.gz", "sha256": "ab7e60a520fe6ae25eca939191bb2ac416cd58478ce754740238a8bf1af18934" } ] }, { "name": "lapack", "buildsystem": "cmake", "builddir": true, "buildsystem": "cmake", "builddir": true, "config-opts": [ "-DCMAKE_INSTALL_prefix=/app --root=/ --optimize=1", "-DCMAKE_INSTALL_LIBDIR=lib", "-DCMAKE_BUILD_TYPE=Release", "-DBUILD_SHARED_LIBS=ON", "-DBUILD_TESTING=OFF", "-DLAPACKE=ON", "-DCBLAS=ON" ], "sources": [ { "type": "archive", "url": "https://github.com/Reference-LAPACK/lapack/archive/v3.9.0.tar.gz", "sha256": "106087f1bb5f46afdfba7f569d0cbe23dacb9a07cd24733765a0e89dbe1ad573" } ], "cleanup": [ "/lib/cmake" ] }, { "name": "python-threadpoolctl", "buildsystem": "simple", "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "build-commands": [ "python3 setup.py build -j 0", "python3 setup.py install --prefix=/app --root=/ --optimize=1" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/78/e8/e39dc842f512ab5be11efe83160ddb7ad3c0cc1b8d42ce8c0469a0d2b926/threadpoolctl-2.1.0.tar.gz", "sha256": "ddc57c96a38beb63db45d6c159b5ab07b6bced12c45a1f07b2b92f272aebfa6b" } ] }, { "name": "python-joblib", "buildsystem": "simple", "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "build-commands": [ "python3 setup.py build -j 0", "python3 setup.py install --prefix=/app --root=/ --optimize=1" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/69/a1/68266edcfabe8dc240d7c9e06972a121b011b232f0d1796a016192438435/joblib-1.0.0.tar.gz", "sha256": "7ad866067ac1fdec27d51c8678ea760601b70e32ff1881d4dc8e1171f2b64b24" } ] }, { "name": "python-numpy", "buildsystem": "simple", "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "build-commands": [ "python3 setup.py build -j 0", "python3 setup.py install --prefix=/app --root=/ --optimize=1" ], "build-options": { "env": { "ATLAS": "None", "BLAS": "/app/lib", "LAPACK": "/app/lib" } }, "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/c5/63/a48648ebc57711348420670bb074998f79828291f68aebfff1642be212ec/numpy-1.19.4.zip", "sha256": "141ec3a3300ab89c7f2b0775289954d193cc8edb621ea05f99db9cb181530512" } ] }, { "name": "python-scipy", "build-options": { "env": { "ATLAS": "None", "BLAS": "/app/lib", "LAPACK": "/app/lib", "LDFLAGS": "-shared" } }, "buildsystem": "simple", "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "build-commands": [ "python3 setup.py build -j 0", "python3 setup.py install --prefix=/app --root=/ --optimize=1" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/aa/d5/dd06fe0e274e579e1dff21aa021219c039df40e39709fabe559faed072a5/scipy-1.5.4.tar.gz", "sha256": "4a453d5e5689de62e5d38edf40af3f17560bfd63c9c5bd228c18c1f99afa155b" } ] } ] } paperwork-2.1.1/flatpak/shared-modules/setuptools-40.8.0.json000066400000000000000000000044221417573700700237410ustar00rootroot00000000000000{ "name": "python-setuptools-all", "modules": [ { "name": "python-setuptools", "buildsystem": "simple", "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "build-commands": [ "python3 setup.py build", "python3 setup.py install --prefix=/app" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/c2/f7/c7b501b783e5a74cf1768bc174ee4fb0a8a6ee5af6afa92274ff964703e0/setuptools-40.8.0.zip", "sha256": "6e4eec90337e849ade7103723b9a99631c1f0d19990d6e8412dc42f5ae8b304d" } ] }, { "name": "python-setuptools-scm", "buildsystem": "simple", "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "build-commands": [ "python3 setup.py build", "python3 setup.py install --prefix=/app" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/54/85/514ba3ca2a022bddd68819f187ae826986051d130ec5b972076e4f58a9f3/setuptools_scm-3.2.0.tar.gz", "sha256": "52ab47715fa0fc7d8e6cd15168d1a69ba995feb1505131c3e814eb7087b57358" } ] }, { "name": "python-setuptools-scm-git-archive", "buildsystem": "simple", "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "build-commands": [ "python3 setup.py build", "python3 setup.py install --prefix=/app" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/7e/2c/0c15b29a1b5940250bfdc4a4f53272e35cd7cf8a34159291b6b4ec9eb291/setuptools_scm_git_archive-1.1.tar.gz", "sha256": "6026f61089b73fa1b5ee737e95314f41cb512609b393530385ed281d0b46c062" } ] } ] } paperwork-2.1.1/flatpak/shared-modules/setuptools-59.5.0.json000066400000000000000000000014061417573700700237470ustar00rootroot00000000000000{ "name": "python-setuptools-all", "modules": [ { "name": "python-setuptools", "buildsystem": "simple", "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "build-commands": [ "python3 ./setup.py install --prefix=/app --root=/" ], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/e6/e2/f2bfdf364e016f7a464db709ea40d1101c4c5a463dd7019dae0a42dbd1c6/setuptools-59.5.0.tar.gz", "sha256": "d144f85102f999444d06f9c0e8c737fd0194f10f2f7e5fdb77573f6e2fa4fad0" } ] } ] } paperwork-2.1.1/flatpak/shared-modules/tesseract-3.05.01.json000066400000000000000000000073131417573700700235740ustar00rootroot00000000000000{ "name": "tesseract-bundle", "no-autogen": true, "sources": [ { "type": "file", "path": "noop-Makefile", "dest-filename": "Makefile" } ], "modules": [ { "name": "libleptonica", "buildsystem": "autotools", "config-opts": [ ], "sources": [ { "type": "archive", "url": "https://github.com/DanBloomberg/leptonica/releases/download/1.74.4/leptonica-1.74.4.tar.gz", "sha256": "29c35426a416bf454413c6fec24c24a0b633e26144a17e98351b6dffaa4a833b" } ] }, { "name": "tesseract", "buildsystem": "autotools", "config-opts": [ "--disable-graphics" ], "sources": [ { "type": "archive", "url": "https://github.com/tesseract-ocr/tesseract/archive/3.05.01.tar.gz", "sha256": "05898f93c5d057fada49b9a116fc86ad9310ff1726a0f499c3e5211b3af47ec1" } ] }, { "name": "tessdata", "no-autogen": true, "sources": [ { "type": "archive", "url": "https://github.com/tesseract-ocr/tessdata/archive/3.04.00.tar.gz", "sha256": "5dcb37198336b6953843b461ee535df1401b41008d550fc9e43d0edabca7adb1" }, { "type": "file", "path": "noop-Makefile", "dest-filename": "Makefile" } ], "post-install": [ "mkdir -p /app/share/tessdata", "cp eng.traineddata /app/share/tessdata", "cp osd.traineddata /app/share/tessdata", "mkdir -p /app/share/locale/bg", "cp bul.traineddata /app/share/locale/bg", "mkdir -p /app/share/locale/da", "cp dan.traineddata /app/share/locale/da", "mkdir -p /app/share/locale/de", "cp deu.traineddata /app/share/locale/de", "mkdir -p /app/share/locale/eo", "cp epo.traineddata /app/share/locale/eo", "mkdir -p /app/share/locale/fi", "cp fin.traineddata /app/share/locale/fi", "mkdir -p /app/share/locale/fr", "cp fra.traineddata /app/share/locale/fr", "mkdir -p /app/share/locale/gl", "cp glg.traineddata /app/share/locale/gl", "mkdir -p /app/share/locale/hu", "cp hun.traineddata /app/share/locale/hu", "mkdir -p /app/share/locale/it", "cp ita.traineddata /app/share/locale/it", "mkdir -p /app/share/locale/ja", "cp jpn.traineddata /app/share/locale/ja", "mkdir -p /app/share/locale/nl", "cp nld.traineddata /app/share/locale/nl", "mkdir -p /app/share/locale/nb", "cp nor.traineddata /app/share/locale/nb", "mkdir -p /app/share/locale/pt", "cp por.traineddata /app/share/locale/pt", "mkdir -p /app/share/locale/ru", "cp rus.traineddata /app/share/locale/ru", "mkdir -p /app/share/locale/es", "cp spa.traineddata /app/share/locale/es", "mkdir -p /app/share/locale/sv", "cp swe.traineddata /app/share/locale/sv", "mkdir -p /app/share/locale/uk", "cp ukr.traineddata /app/share/locale/uk" ] } ] }paperwork-2.1.1/flatpak/shared-modules/tesseract-4.0.0.json000066400000000000000000000062741417573700700234340ustar00rootroot00000000000000{ "name": "tesseract", "buildsystem": "autotools", "config-opts": [ "--disable-graphics" ], "sources": [ { "type": "archive", "url": "https://github.com/tesseract-ocr/tesseract/archive/4.0.0.tar.gz", "sha256": "a1f5422ca49a32e5f35c54dee5112b11b99928fc9f4ee6695cdc6768d69f61dd" } ], "modules": [ { "name": "libleptonica", "buildsystem": "autotools", "sources": [ { "type": "archive", "url": "https://github.com/DanBloomberg/leptonica/releases/download/1.77.0/leptonica-1.77.0.tar.gz", "sha256": "161d0b368091986b6c60990edf257460bdc7da8dd18d48d4179e297bcdca5eb7" } ] }, { "name": "tessdata", "buildsystem": "simple", "sources": [ { "type": "archive", "url": "https://github.com/tesseract-ocr/tessdata/archive/4.0.0.tar.gz", "sha256": "38c637d3a1763f6c3d32e8f1d979f045668676ec5feb8ee1869ee77cedd31b08" } ], "build-commands": [ "mkdir -p /app/share/tessdata", "cp eng.traineddata /app/share/tessdata", "cp osd.traineddata /app/share/tessdata", "mkdir -p /app/share/locale/bg", "cp bul.traineddata /app/share/locale/bg", "mkdir -p /app/share/locale/da", "cp dan.traineddata /app/share/locale/da", "mkdir -p /app/share/locale/de", "cp deu.traineddata /app/share/locale/de", "mkdir -p /app/share/locale/eo", "cp epo.traineddata /app/share/locale/eo", "mkdir -p /app/share/locale/fi", "cp fin.traineddata /app/share/locale/fi", "mkdir -p /app/share/locale/fr", "cp fra.traineddata /app/share/locale/fr", "mkdir -p /app/share/locale/gl", "cp glg.traineddata /app/share/locale/gl", "mkdir -p /app/share/locale/hu", "cp hun.traineddata /app/share/locale/hu", "mkdir -p /app/share/locale/it", "cp ita.traineddata /app/share/locale/it", "mkdir -p /app/share/locale/ja", "cp jpn.traineddata /app/share/locale/ja", "mkdir -p /app/share/locale/nl", "cp nld.traineddata /app/share/locale/nl", "mkdir -p /app/share/locale/nb", "cp nor.traineddata /app/share/locale/nb", "mkdir -p /app/share/locale/pt", "cp por.traineddata /app/share/locale/pt", "mkdir -p /app/share/locale/ru", "cp rus.traineddata /app/share/locale/ru", "mkdir -p /app/share/locale/es", "cp spa.traineddata /app/share/locale/es", "mkdir -p /app/share/locale/sv", "cp swe.traineddata /app/share/locale/sv", "mkdir -p /app/share/locale/uk", "cp ukr.traineddata /app/share/locale/uk" ] } ] } paperwork-2.1.1/flatpak/shared-modules/tesseract-4.1.1.json000066400000000000000000000062741417573700700234360ustar00rootroot00000000000000{ "name": "tesseract", "buildsystem": "autotools", "config-opts": [ "--disable-graphics" ], "sources": [ { "type": "archive", "url": "https://github.com/tesseract-ocr/tesseract/archive/4.1.1.tar.gz", "sha256": "2a66ff0d8595bff8f04032165e6c936389b1e5727c3ce5a27b3e059d218db1cb" } ], "modules": [ { "name": "libleptonica", "buildsystem": "autotools", "sources": [ { "type": "archive", "url": "https://github.com/DanBloomberg/leptonica/releases/download/1.79.0/leptonica-1.79.0.tar.gz", "sha256": "045966c9c5d60ebded314a9931007a56d9d2f7a6ac39cb5cc077c816f62300d8" } ] }, { "name": "tessdata", "buildsystem": "simple", "sources": [ { "type": "archive", "url": "https://github.com/tesseract-ocr/tessdata/archive/4.0.0.tar.gz", "sha256": "38c637d3a1763f6c3d32e8f1d979f045668676ec5feb8ee1869ee77cedd31b08" } ], "build-commands": [ "mkdir -p /app/share/tessdata", "cp eng.traineddata /app/share/tessdata", "cp osd.traineddata /app/share/tessdata", "mkdir -p /app/share/locale/bg", "cp bul.traineddata /app/share/locale/bg", "mkdir -p /app/share/locale/da", "cp dan.traineddata /app/share/locale/da", "mkdir -p /app/share/locale/de", "cp deu.traineddata /app/share/locale/de", "mkdir -p /app/share/locale/eo", "cp epo.traineddata /app/share/locale/eo", "mkdir -p /app/share/locale/fi", "cp fin.traineddata /app/share/locale/fi", "mkdir -p /app/share/locale/fr", "cp fra.traineddata /app/share/locale/fr", "mkdir -p /app/share/locale/gl", "cp glg.traineddata /app/share/locale/gl", "mkdir -p /app/share/locale/hu", "cp hun.traineddata /app/share/locale/hu", "mkdir -p /app/share/locale/it", "cp ita.traineddata /app/share/locale/it", "mkdir -p /app/share/locale/ja", "cp jpn.traineddata /app/share/locale/ja", "mkdir -p /app/share/locale/nl", "cp nld.traineddata /app/share/locale/nl", "mkdir -p /app/share/locale/nb", "cp nor.traineddata /app/share/locale/nb", "mkdir -p /app/share/locale/pt", "cp por.traineddata /app/share/locale/pt", "mkdir -p /app/share/locale/ru", "cp rus.traineddata /app/share/locale/ru", "mkdir -p /app/share/locale/es", "cp spa.traineddata /app/share/locale/es", "mkdir -p /app/share/locale/sv", "cp swe.traineddata /app/share/locale/sv", "mkdir -p /app/share/locale/uk", "cp ukr.traineddata /app/share/locale/uk" ] } ] } paperwork-2.1.1/flatpak/stacktrace.sh000077500000000000000000000047661417573700700176620ustar00rootroot00000000000000#!/usr/bin/bash # Needed: # flatpak install --user https://builder.openpaper.work/paperwork_master.flatpakref # flatpak install --user paperwork-origin work.openpaper.Paperwork.Debug//master # flatpak install --user https://builder.openpaper.work/paperwork_testing.flatpakref # flatpak install --user paperwork-origin work.openpaper.Paperwork.Debug//testing # flatpak install --user https://builder.openpaper.work/paperwork_develop.flatpakref # flatpak install --user paperwork-origin work.openpaper.Paperwork.Debug//develop # The stack trace file must contain something similar to: # /app/lib/libinsane.so.1(+0x1aa14)[0x7f2ad73b5a14] # /usr/lib/x86_64-linux-gnu/libc.so.6(+0x39690)[0x7f2adf6e7690] # /usr/lib/x86_64-linux-gnu/libc.so.6(+0x9cc2a)[0x7f2adf74ac2a] # /usr/lib/x86_64-linux-gnu/libc.so.6(+0x6a716)[0x7f2adf718716] # /usr/lib/x86_64-linux-gnu/libc.so.6(+0x7c78a)[0x7f2adf72a78a] # /app/lib/libinsane.so.1(lis_log+0x125)[0x7f2ad73a3fb5] # /app/lib/libinsane.so.1(+0x1b7ba)[0x7f2ad73b67ba] # /app/lib/libinsane.so.1(+0x1b9f8)[0x7f2ad73b69f8] # /app/lib/libinsane.so.1(lis_worker_main+0x23a)[0x7f2ad73b6c6a] # /app/lib/libinsane.so.1(lis_api_workaround_dedicated_process+0x3a8)[0x7f2ad73b4518] # /app/lib/libinsane.so.1(lis_safebet+0x1b2)[0x7f2ad73ab502] # /app/lib/libinsane_gobject.so.1(libinsane_api_new_safebet+0x5a)[0x7f2ad766cf5a] # /usr/lib/x86_64-linux-gnu/libffi.so.6(ffi_call_unix64+0x4c)[0x7f2ade490b78] # /usr/lib/x86_64-linux-gnu/libffi.so.6(ffi_call+0x1d4)[0x7f2ade490374] # /usr/lib/python3.7/site-packages/gi/_gi.cpython-37m-x86_64-linux-gnu.so(+0x2a12d)[0x7f2addfdf12d] if [ $# -lt 2 ] ; then echo "Syntax:" echo " $0 " exit 1 fi stacktrace_file="$1" nb_commits="$2" for branch in master testing develop ; do for commit in $(flatpak remote-info \ --log -c paperwork-origin \ work.openpaper.Paperwork//${branch} \ | head -n ${nb_commits}) ; do echo "===================================" echo "Branch: ${branch}" echo "Flatpak Commit: ${commit}" echo for line in $(< ${stacktrace_file}) ; do if [ -z "${line}" ] || [ "${line}" = "-" ] ; then echo "(...)" echo continue fi filename=$(echo "${line}"|cut -d'(' -f1) addr=$(echo "${line}"|cut -d'(' -f2|cut -d')' -f1) result=$(flatpak run --devel --command=addr2line \ work.openpaper.Paperwork//${branch} \ -i -p -f -e "${filename}" "${addr}") echo "IN: ${filename}(${addr})" echo "ADDR2LINE: ${result}" echo done echo echo done done paperwork-2.1.1/nsis/000077500000000000000000000000001417573700700145145ustar00rootroot00000000000000paperwork-2.1.1/nsis/Makefile000066400000000000000000000034421417573700700161570ustar00rootroot00000000000000all: paperwork_stable_installer.exe paperwork_testing_installer.exe paperwork_develop_installer.exe dll/INetC.dll: mkdir -p dll rm -rf tmp mkdir tmp cd tmp ; wget http://nsis.sourceforge.net/mediawiki/images/c/c9/Inetc.zip cd tmp ; unzip Inetc.zip cd tmp ; mv Plugins/x86-ansi/INetC.dll ../dll rm -rf tmp dll/nsisunz.dll: mkdir -p dll rm -rf tmp mkdir tmp cd tmp ; wget http://nsis.sourceforge.net/mediawiki/images/1/1c/Nsisunz.zip cd tmp ; unzip Nsisunz.zip cd tmp ; mv nsisunz/Release/nsisunz.dll ../dll rm -rf tmp out.nsi: gen_installer_nsi.py if [ -z "${RELEASE}" ] ; then echo "Syntax: make RELEASE=x.y.z DOWNLOAD_URI=https://pouet" ; exit 1 ; fi echo Release: ${RELEASE} echo Download URI: ${DOWNLOAD_URI} python3 ./gen_installer_nsi.py ${RELEASE} ${DOWNLOAD_URI} paperwork_stable_installer.exe: dll/INetC.dll dll/nsisunz.dll rm -f out.nsi $(MAKE) out.nsi RELEASE=stable DOWNLOAD_URI=https://download.openpaper.work/windows/x86/paperwork-master-latest.zip makensis ./out.nsi mv -f paperwork_installer.exe paperwork_stable_installer.exe paperwork_testing_installer.exe: dll/INetC.dll dll/nsisunz.dll rm -f out.nsi $(MAKE) out.nsi RELEASE=testing DOWNLOAD_URI=https://download.openpaper.work/windows/x86/paperwork-testing-latest.zip makensis ./out.nsi mv -f paperwork_installer.exe paperwork_testing_installer.exe paperwork_develop_installer.exe: dll/INetC.dll dll/nsisunz.dll rm -f out.nsi $(MAKE) out.nsi RELEASE=develop DOWNLOAD_URI=https://download.openpaper.work/windows/x86/paperwork-develop-latest.zip makensis ./out.nsi mv -f paperwork_installer.exe paperwork_develop_installer.exe clean: rm -rf dll rm -f out.nsi rm -f paperwork_installer.exe rm -f paperwork_stable_installer.exe rm -f paperwork_testing_installer.exe rm -f paperwork_develop_installer.exe .PHONY = all paperwork-2.1.1/nsis/data/000077500000000000000000000000001417573700700154255ustar00rootroot00000000000000paperwork-2.1.1/nsis/data/gpl-3.0.txt000066400000000000000000001045131417573700700172520ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . paperwork-2.1.1/nsis/data/licences.txt000066400000000000000000001076121417573700700177620ustar00rootroot00000000000000Paperwork is released under the licence GPL v3+. However, other components are used by Paperwork and must be installed. All of them are licensed under GPLv3+ or less restrictive licences. It's hard (if not impossible) to list them all, but here is a non-exhaustive list with their corresponding licences: - PyOCR : https://gitlab.gnome.org/World/OpenPaperwork/pyocr (GPL v3+) - Libinsane : https://gitlab.gnome.org/World/OpenPaperwork/libinsane (LGPL v3+) - Libpillowfight : https://gitlab.gnome.org/World/OpenPaperwork/libpillowfight (GPL v3+) - Python interpreter : https://python.org/ (PSF) - GTK+ : https://www.gtk.org/ (LGPL v2.1) - Poppler : https://poppler.freedesktop.org/ (GPL v2 / GPL v3) - Pillow : http://pillow.readthedocs.io/en/3.1.x/about.html (MIT-like) - Whoosh : https://pypi.python.org/pypi/Whoosh/ (Two-clause BSD) - Simplebayes : https://github.com/hickeroar/simplebayes (MIT) - Pycountry : https://pypi.python.org/pypi/pycountry/ (LGPL v2.1) Translated versions of the GPL v3 are available here: https://www.gnu.org/licenses/gpl-3.0.html https://www.gnu.org/licenses/quick-guide-gplv3.html PSF licence is available here: https://docs.python.org/3/license.html Translated versions of the LGPL v3 are available here: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html Translated versions of the GPL v2 are available here: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html A copy of the MIT licence is available here: https://opensource.org/licenses/MIT A copy of the two-clause BSD licence is available here: https://opensource.org/licenses/BSD-2-Clause ----- GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . paperwork-2.1.1/nsis/data/paperwork_64.ico000066400000000000000000000410761417573700700204540ustar00rootroot00000000000000@@ (B(@ @iiik@@@666!~~~wiiiGGG]]]UgggORRR5aaa7 \\\V gggc+++0SSSS~~~qXXX:XXX1eeeLʪ\\\E}}}hEEE;}⫫;;;111NNNlll\山gggHhhhBDDD-ᵵ999􉉉SSS+tttgⷷiii\􌌌vvvaYYY<===AAA+;;;bbbFssshhhh[~~~yVVV;NNN$CCC*@@@w쐐oooZqqqh333rrrU쎎RRR>___;DDD)蟟汱 ~~~wݷspppi疖靝KKK"cccMߔQQQ<邂cccUDDD)~~~椤 ~~~wխ{{{ooojwwwZZZ3ZZZRLLLPPP222g-??????paperwork-2.1.1/nsis/gen_installer_nsi.py000077500000000000000000000375461417573700700206070ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # TODO(Jflesch): PEP8 ... # flake8: noqa import pycountry import re import sys DEFAULT_DOWNLOAD_URI = ( "https://download.openpaper.work/windows/x86/paperwork-master-latest.zip" ) ALL_LANGUAGES = [ "eng", # English (always first) "afr", "amh", "ara", "asm", "aze", {"lower": "aze_cyrl", "upper": "AZECYRL", "long": "Azerbaijani - Cyrilic"}, "bel", "ben", "bod", # Tibetan "bos", "bre", "bul", "cat", "ceb", "ces", # Czech {"lower": "chi_sim", "upper": "CHISIM", "long": "Chinese (simplified)"}, { "lower": "chi_sim_vert", "upper": "CHISIMVERT", "long": "Chinese (simplified, vertical)" }, {"lower": "chi_tra", "upper": "CHITRA", "long": "Chinese (traditional)"}, { "lower": "chi_tra_vert", "upper": "CHITRAVERT", "long": "Chinese (traditional, vertical)" }, "chr", "cos", "cym", # Welsh "dan", "deu", # German "div", "dzo", {"lower": "ell", "upper": "ELL", "long": "Greek (modern)"}, "enm", "epo", # Esperanto "est", "eus", # Basque "fao", {"lower": "fas", "upper": "FAS", "long": "Persian"}, "fil", "fin", "fra", # French "frk", # Frankish "frm", "fry", "gla", "gle", # Irish "glg", {"lower": "grc", "upper": "GRC", "long": "Greek (ancient)"}, "guj", "hat", "heb", "hin", "hrv", # Croatian "hun", "hye", "iku", # Inuktitut "ind", "isl", # Icelandic "ita", {"lower": "ita_old", "upper": "ITAOLD", "long": "Italian (old)"}, "jav", "jpn", # Japanese "kan", "kat", # Georgian "khm", "kir", "kor", "lao", "lat", "lav", "lit", "ltz", "mal", "mar", "mkd", # Macedonian "mlt", # Maltese "mon", "mri", "msa", # Malay "mya", # Burmese "nep", "nld", # Dutch "nor", "oci", "ori", "pan", "pol", "por", "pus", "que", {"lower": "ron", "upper": "RON", "long": "Romanian"}, "rus", "san", "sin", "slk", "slv", "spa", # Spanish "sqi", # Albanian "srp", # Serbian {"lower": "srp_latn", "upper": "SRPLATN", "long": "Serbian (Latin)"}, "swa", "swe", "syr", "tam", "tat", "tel", "tgk", # Tajik {"lower": "tha", "upper": "THA", "long": "Thai"}, "tir", "ton", "tur", "uig", "ukr", "urd", "uzb", {"lower": "uzb_cyrl", "upper": "UZBCYRL", "long": "Uzbek - Cyrilic"}, "vie", "yid", "yor", ] UNKNOWN_LANGUAGE = { 'download_section': """ Section /o "{long}" SEC_{upper} inetc::get "https://download.openpaper.work/tesseract/4.0.0/tessdata/{lower}.traineddata" "$INSTDIR\\Data\\Tessdata\\{lower}.traineddata" /END Pop $0 StrCmp $0 "OK" +3 MessageBox MB_OK "Download of {lower}.traineddata failed: $0" Quit SectionEnd """, 'lang_strings': """ LangString DESC_SEC_{upper} ${{LANG_ENGLISH}} "Data files required to run OCR on {long} documents" LangString DESC_SEC_{upper} ${{LANG_FRENCH}} "Data files required to run OCR on {long} documents" LangString DESC_SEC_{upper} ${{LANG_GERMAN}} "Data files required to run OCR on {long} documents" """, } KNOWN_LANGUAGES = { 'deu': { "download_section": """ Section /o "German / Deutsch" SEC_DEU inetc::get "https://download.openpaper.work/tesseract/4.0.0/tessdata/{lower}.traineddata" "$INSTDIR\\Data\\Tessdata\\{lower}.traineddata" /END Pop $0 StrCmp $0 "OK" +3 MessageBox MB_OK "Download of {lower}.traineddata failed: $0" Quit SectionEnd """, "lang_strings": """ LangString DESC_SEC_DEU ${{LANG_ENGLISH}} "Data files required to run OCR on German documents" LangString DESC_SEC_DEU ${{LANG_FRENCH}} "Fichiers requis pour la reconnaissance de caractères sur les documents en allemand" LangString DESC_SEC_DEU ${{LANG_GERMAN}} "Data files required to run OCR on German documents" ; TODO """, }, 'eng': { "download_section": """ Section "English / English" SEC_ENG SectionIn RO ; Mandatory section inetc::get "https://download.openpaper.work/tesseract/4.0.0/tessdata_eng_4_0_0.zip" "$PLUGINSDIR\\tess_eng.zip" /END Pop $0 StrCmp $0 "OK" +3 MessageBox MB_OK "Download of {lower}.traineddata failed: $0" Quit nsisunz::UnzipToLog "$PLUGINSDIR\\tess_eng.zip" "$INSTDIR\\Data\\Tessdata" SectionEnd """, "lang_strings": """ LangString DESC_SEC_ENG ${{LANG_ENGLISH}} "Data files required to run OCR on English documents" LangString DESC_SEC_ENG ${{LANG_FRENCH}} "Fichiers requis pour la reconnaissance de caractères sur les documents en anglais" LangString DESC_SEC_ENG ${{LANG_GERMAN}} "Data files required to run OCR on English documents" ; TODO """, }, 'fra': { "download_section": """ Section /o "French / Français" SEC_FRA inetc::get "https://download.openpaper.work/tesseract/4.0.0/tessdata/{lower}.traineddata" "$INSTDIR\\Data\\Tessdata\\{lower}.traineddata" /END Pop $0 StrCmp $0 "OK" +3 MessageBox MB_OK "Download of {lower}.traineddata failed: $0" Quit SectionEnd """, "lang_strings": """ LangString DESC_SEC_FRA ${{LANG_ENGLISH}} "Data files required to run OCR on French documents" LangString DESC_SEC_FRA ${{LANG_FRENCH}} "Fichiers requis pour la reconnaissance de caractères sur les documents en français" LangString DESC_SEC_FRA ${{LANG_GERMAN}} "Data files required to run OCR on French documents" ; TODO """, }, } VERSION = """ !define PRODUCT_VERSION "{version}" !define PRODUCT_SHORT_VERSION "{short_version}" !define PRODUCT_DOWNLOAD_URI "{download_uri}" """ HEADER = """ !define PRODUCT_NAME "Paperwork" !define PRODUCT_PUBLISHER "Openpaper.work" !define PRODUCT_WEB_SITE "https://openpaper.work" !define PRODUCT_UNINST_KEY "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${PRODUCT_NAME}" !define PRODUCT_UNINST_ROOT_KEY "HKLM" !addplugindir ".\dll" ; MUI 1.67 compatible ------ !include "MUI.nsh" !include "Sections.nsh" !include "LogicLib.nsh" ; MUI Settings !define MUI_ABORTWARNING !define MUI_ICON "data\\paperwork_64.ico" !define MUI_UNICON "data\\paperwork_64.ico" ; Language Selection Dialog Settings !define MUI_LANGDLL_REGISTRY_ROOT "${PRODUCT_UNINST_ROOT_KEY}" !define MUI_LANGDLL_REGISTRY_KEY "${PRODUCT_UNINST_KEY}" !define MUI_LANGDLL_REGISTRY_VALUENAME "NSIS:Language" ; Welcome page !insertmacro MUI_PAGE_WELCOME ; License page !insertmacro MUI_PAGE_LICENSE "data\\licences.txt" ; Components page !insertmacro MUI_PAGE_COMPONENTS ; Directory page !insertmacro MUI_PAGE_DIRECTORY ; Instfiles page !insertmacro MUI_PAGE_INSTFILES ; Finish page !define MUI_FINISHPAGE_RUN "$INSTDIR\\paperwork.exe" !insertmacro MUI_PAGE_FINISH ; Uninstaller pages !insertmacro MUI_UNPAGE_INSTFILES ; Language files !insertmacro MUI_LANGUAGE "English" !insertmacro MUI_LANGUAGE "French" !insertmacro MUI_LANGUAGE "German" ; MUI end ------ Name "${PRODUCT_NAME} ${PRODUCT_VERSION}" OutFile "paperwork_installer.exe" InstallDir "$PROGRAMFILES\\Paperwork" ShowInstDetails hide ShowUnInstDetails hide BrandingText "OpenPaper.work" Section "Paperwork" SEC_PAPERWORK SectionIn RO ; Mandatory section SetOutPath "$INSTDIR" SetOverwrite on inetc::get "${PRODUCT_DOWNLOAD_URI}" "$PLUGINSDIR\\paperwork.zip" /END Pop $0 StrCmp $0 "OK" +3 MessageBox MB_OK "Download of ${PRODUCT_DOWNLOAD_URI} failed: $0" Quit inetc::get "https://download.openpaper.work/tesseract/4.0.0/tesseract_4_0_0.zip" "$PLUGINSDIR\\tesseract.zip" /END Pop $0 StrCmp $0 "OK" +3 MessageBox MB_OK "Download failed: $0" Quit inetc::get "https://download.openpaper.work/tesseract/4.0.0/tessconfig_4_0_0.zip" "$PLUGINSDIR\\tessconfig.zip" /END Pop $0 StrCmp $0 "OK" +3 MessageBox MB_OK "Download failed: $0" Quit CreateDirectory "$INSTDIR" nsisunz::UnzipToLog "$PLUGINSDIR\\paperwork.zip" "$INSTDIR" ; CreateShortCut "$DESKTOP.lnk" "$INSTDIR\\paperwork.exe" ; CreateShortCut "$STARTMENU.lnk" "$INSTDIR\\paperwork.exe" SetOutPath "$INSTDIR\\Tesseract" CreateDirectory "$INSTDIR\\Tesseract" nsisunz::UnzipToLog "$PLUGINSDIR\\tesseract.zip" "$INSTDIR" SetOutPath "$INSTDIR\\Data\\Tessdata" CreateDirectory "$INSTDIR\\Data\\Tessdata" nsisunz::UnzipToLog "$PLUGINSDIR\\tessconfig.zip" "$INSTDIR\\Data\\Tessdata" SectionEnd Section "Desktop icon" SEC_DESKTOP_ICON CreateShortCut "$DESKTOP\\Paperwork.lnk" "$INSTDIR\\paperwork.exe" "" "$INSTDIR\\Data\\paperwork_64.ico" 0 SW_SHOWNORMAL "" "Paperwork" SectionEnd """ MIDDLE = """ !macro SecSelect SecId Push $0 SectionSetFlags ${SecId} ${SF_SELECTED} SectionSetInstTypes ${SecId} 1 Pop $0 !macroend !define SelectSection '!insertmacro SecSelect' Function .onInit InitPluginsDir !insertmacro MUI_LANGDLL_DISPLAY StrCmp $LANGUAGE ${LANG_FRENCH} french maybegerman french: ${SelectSection} ${SEC_FRA} Goto end maybegerman: StrCmp $LANGUAGE ${LANG_GERMAN} german end german: ${SelectSection} ${SEC_DEU} end: FunctionEnd Section -AdditionalIcons SetOutPath $INSTDIR WriteIniStr "$INSTDIR\${PRODUCT_NAME}.url" "InternetShortcut" "URL" "${PRODUCT_WEB_SITE}" CreateDirectory "$SMPROGRAMS\\Paperwork" CreateShortCut "$SMPROGRAMS\\Paperwork\\Paperwork.lnk" "$INSTDIR\\paperwork.exe" "" "$INSTDIR\\Data\\paperwork_64.ico" 0 SW_SHOWNORMAL "" "Paperwork" CreateShortCut "$SMPROGRAMS\\Paperwork\\Website.lnk" "$INSTDIR\\${PRODUCT_NAME}.url" CreateShortCut "$SMPROGRAMS\\Paperwork\\Uninstall.lnk" "$INSTDIR\\uninst.exe" SectionEnd Section -Post WriteUninstaller "$INSTDIR\\uninst.exe" WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)" WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\\uninst.exe" WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}" WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}" WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}" SectionEnd LangString DESC_SEC_PAPERWORK ${LANG_ENGLISH} "Paperwork and all the required libriaires (Tesseract, GTK, etc)" LangString DESC_SEC_PAPERWORK ${LANG_FRENCH} "Paperwork et toutes les librairies requises (Tesseract, GTK, etc)" LangString DESC_SEC_PAPERWORK ${LANG_GERMAN} "Paperwork and all the required libriaires (Tesseract, GTK, etc)" ; TODO LangString DESC_SEC_OCR_FILES ${LANG_ENGLISH} "Data files required to run OCR" LangString DESC_SEC_OCR_FILES ${LANG_FRENCH} "Fichiers de données nécessaires pour la reconnaissance de caractères" LangString DESC_SEC_OCR_FILES ${LANG_GERMAN} "Data files required to run OCR" ; TODO """ FOOTER = """ LangString DESC_SEC_DESKTOP_ICON ${LANG_ENGLISH} "Icon on the desktop to launch Paperwork" LangString DESC_SEC_DESKTOP_ICON ${LANG_FRENCH} "Icône sur le bureau pour lancer Paperwork" LangString DESC_SEC_DESKTOP_ICON ${LANG_GERMAN} "Icon on the desktop to launch Paperwork" ; TODO Function un.onUninstSuccess HideWindow MessageBox MB_ICONINFORMATION|MB_OK "$(^Name) has been deleted successfully" FunctionEnd Function un.onInit !insertmacro MUI_UNGETLANGUAGE MessageBox MB_ICONQUESTION|MB_YESNO|MB_DEFBUTTON2 "Are you sure you want to uninstall $(^Name) ? (your documents won't be deleted)" IDYES +2 Abort FunctionEnd Section Uninstall Delete "$SMPROGRAMS\\Paperwork\\Paperwork.lnk" Delete "$SMPROGRAMS\\Paperwork\\Uninstall.lnk" Delete "$SMPROGRAMS\\Paperwork\\Website.lnk" Delete "$DESKTOP\\Paperwork.lnk" ; Delete "$STARTMENU.lnk" ; Delete "$DESKTOP.lnk" RMDir /r "$INSTDIR\\data" RMDir /r "$INSTDIR\\etc" RMDir /r "$INSTDIR\\gi_typelibs" RMDir /r "$INSTDIR\\include" RMDir /r "$INSTDIR\\lib2to3" RMDir /r "$INSTDIR\\pycountry" RMDir /r "$INSTDIR\\share" RMDir /r "$INSTDIR\\tcl" RMDir /r "$INSTDIR\\tesseract" RMDir /r "$INSTDIR\\tk" RMDir /r "$INSTDIR\\*.*" Delete "$INSTDIR\\*.*" RMDir "$INSTDIR" RMDir "$SMPROGRAMS\\Paperwork" RMDir "" DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" SetAutoClose true SectionEnd """ def find_language(lang_str): lang_str = lang_str.lower() if "_" in lang_str: lang_str = lang_str.split("_")[0] print("System language: {}".format(lang_str)) attrs = ( 'iso_639_3_code', 'iso639_3_code', 'iso639_2T_code', 'iso639_1_code', 'terminology', 'bibliographic', 'alpha_3', 'alpha_2', 'alpha2', 'name', ) for attr in attrs: try: r = pycountry.pycountry.languages.get(**{attr: lang_str}) if r is not None: return r except (KeyError, UnicodeDecodeError): pass raise Exception("Unable to find language !") def get_lang_infos(lang_name): if isinstance(lang_name, dict): return lang_name lang = lang_name.split("_") lang_name = lang[0] suffix = "" if len(lang) <= 1 else lang[1] lang = find_language(lang_name) if not suffix: long_name = lang.name else: long_name = "{} ({})".format(lang.name, suffix) return { "lower": lang_name.lower() + suffix.lower(), "upper": lang_name.upper() + suffix.upper(), "long": long_name, } def main(args): if (len(args) < 2): print ("ARGS: {} []".format(args[0])) return download_uri = DEFAULT_DOWNLOAD_URI if len(args) == 3: version = short_version = args[1] download_uri = args[2] else: version = args[1] m = re.match(r"([\d\.]+)", version) # match everything but the suffix short_version = m.string[m.start():m.end()] download_uri = DEFAULT_DOWNLOAD_URI with open("out.nsi", "w") as out_fd: out_fd.write(VERSION.format(version=version, short_version=short_version, download_uri=download_uri)) out_fd.write(HEADER) out_fd.write(""" SectionGroup /e "Tesseract OCR data files" SEC_OCR_FILES """) langs = {} for lang_name in ALL_LANGUAGES: print ("Adding download section {}".format(lang_name)) lang = UNKNOWN_LANGUAGE if isinstance(lang_name, str) and lang_name in KNOWN_LANGUAGES: lang = KNOWN_LANGUAGES[lang_name] txt = lang['download_section'] infos = get_lang_infos(lang_name) txt = txt.format(**infos) langs[infos['long']] = txt lang_sorted = sorted(langs.keys()) for lang_name in lang_sorted: out_fd.write(langs[lang_name]) out_fd.write(""" SectionGroupEnd """) out_fd.write(MIDDLE) for lang_name in ALL_LANGUAGES: print ("Adding strings section {}".format(lang_name)) lang = UNKNOWN_LANGUAGE if isinstance(lang_name, str) and lang_name in KNOWN_LANGUAGES: lang = KNOWN_LANGUAGES[lang_name] txt = lang['lang_strings'] txt = txt.format(**get_lang_infos(lang_name)) out_fd.write(txt) out_fd.write(""" !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN !insertmacro MUI_DESCRIPTION_TEXT ${SEC_PAPERWORK} $(DESC_SEC_PAPERWORK) """) for lang_name in ALL_LANGUAGES: print ("Adding MUI section {}".format(lang_name)) infos = get_lang_infos(lang_name) txt = " !insertmacro MUI_DESCRIPTION_TEXT ${{SEC_{upper}}} $(DESC_SEC_{upper})\n".format(upper=infos['upper']) out_fd.write(txt) out_fd.write(""" !insertmacro MUI_DESCRIPTION_TEXT ${SEC_DESKTOP_ICON} $(DESC_SEC_DESKTOP_ICON) !insertmacro MUI_FUNCTION_DESCRIPTION_END """) out_fd.write(FOOTER) print ("out.nsi written") if __name__ == "__main__": main(sys.argv) paperwork-2.1.1/openpaperwork-core/000077500000000000000000000000001417573700700173625ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/ChangeLog000066400000000000000000000031041417573700700211320ustar00rootroot000000000000002022/01/31 - 2.1.1: - Fix tests of plugins cmd.config and cmd.plugins - thread.pool: Fix: take into account that the main loop can started and stopped many times in a Paperwork instance lifetime (broke some paperwork-shell commands in some cases) 2021/12/05 - 2.1.0: - Bug report censoring: Take into account that some strings in the logs may be URL-encoded - Version data files: If the version changes, rebuild them all - Handle gracefully copies from fake in-memory files (memory://) to non-fake files (thanks to Benjamin Li) - If unable to load or init a plugin, don't hide the problem anymore ; instead clearly fail 2021/05/24 - 2.0.3: - Add LICENSE file in pypi package 2021/01/01 - 2.0.2: - Commands "config": When parsing boolean value, accepts "false" and "0" as input for False - bug_report censoring: Do not censor text files that have already been censored (avoid renaming the file again and making the file name even longer) 2020/11/15 - 2.0.1: - Bug report censoring: Take into account that some strings in the logs may be URL-encoded and censor them too (for instance, path to the user home directory) - fault handler: Prefer dumping the output of faulthandler in the log file instead of stderr (can't do both inforunately) - Logs archive + bug report: by default, report the logs of the last 2 previous sessions too - fs.python: Text files must be encoded in UTF-8 - Windows packaging: Fix HTTPS support: Use the certifi module to provide root certificates - Include tests in Pypi package (thanks to Elliott Sales de Andrade) 2020/10/17 - 2.0: - Initial release paperwork-2.1.1/openpaperwork-core/LICENSE000066400000000000000000001045051417573700700203740ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {one line to give the program's name and a brief idea of what it does.} Copyright (C) {year} {name of author} This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: {project} Copyright (C) {year} {fullname} This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . paperwork-2.1.1/openpaperwork-core/MANIFEST.in000066400000000000000000000001271417573700700211200ustar00rootroot00000000000000recursive-include src *.py *.mo recursive-include tests * include *.md include LICENSE paperwork-2.1.1/openpaperwork-core/Makefile000066400000000000000000000044621417573700700210300ustar00rootroot00000000000000VERSION_FILE = src/openpaperwork_core/_version.py PYTHON ?= python3 build: build_c build_py install: install_py install_c uninstall: uninstall_py build_py: ${VERSION_FILE} l10n_compile ${PYTHON} ./setup.py build build_c: version: ${VERSION_FILE} ${VERSION_FILE}: echo -n "version = \"" >| $@ echo -n $(shell git describe --always) >> $@ echo "\"" >> $@ doc: install_py $(MAKE) -C doc html doc/_build/html/index.html: doc upload_doc: doc/_build/html/index.html cd .. && ./ci/deliver_doc.sh ${CURDIR}/doc/_build/html openpaperwork_core data: check: flake8 src/openpaperwork_core test: install python3 -m unittest discover --verbose -s tests linux_exe: windows_exe: ${PYTHON} /mingw64/bin/pip3-script.py install . # ugly, but "import pkg_resources" doesn't work in frozen environments # and I don't want to have to patch the build machine to fix it every # time. mkdir -p $(CURDIR)/../build/exe/data (cd $(CURDIR)/src && find . -name '*.mo' -exec cp --parents \{\} $(CURDIR)/../build/exe/data \; ) release: ifeq (${RELEASE}, ) @echo "You must specify a release version (make release RELEASE=1.2.3)" exit 1 else @echo "Will release: ${RELEASE}" @echo "Checking release is in ChangeLog ..." grep ${RELEASE} ChangeLog | grep -v "/xx" endif release_pypi: @echo "Releasing paperwork-backend ..." ${PYTHON} ./setup.py sdist twine upload $(CURDIR)/dist/openpaperwork-core-${RELEASE}.tar.gz @echo "All done" clean: rm -rf doc/_build rm -f ${VERSION_FILE} rm -rf build dist *.egg-info # PIP_ARGS is used by Flatpak build install_py: ${VERSION_FILE} l10n_compile ${PYTHON} ./setup.py install ${PIP_ARGS} install_c: uninstall_py: pip3 uninstall -y openpaperwork-core uninstall_c: l10n_extract: $(CURDIR)/../tools/l10n_extract.sh "$(CURDIR)/src" "$(CURDIR)/l10n" l10n_compile: $(CURDIR)/../tools/l10n_compile.sh \ "$(CURDIR)/l10n" \ "$(CURDIR)/src/openpaperwork_core/l10n" \ "openpaperwork_core" help: @echo "make build || make build_py" @echo "make check" @echo "make help: display this message" @echo "make install || make install_py" @echo "make uninstall || make uninstall_py" @echo "make release" .PHONY: \ build \ build_c \ build_py \ check \ doc \ exe \ help \ install \ install_c \ install_py \ l10n_extract \ l10n_compile \ release \ test \ uninstall \ uninstall_c \ version paperwork-2.1.1/openpaperwork-core/README.md000066400000000000000000000003541417573700700206430ustar00rootroot00000000000000# OpenPaperwork Core The core manages Plugins, Callbacks and Interfaces. This package also provide some basic plugins that may be used in any kind of application. [Documentation](https://doc.openpaper.work/openpaperwork_core/latest/) paperwork-2.1.1/openpaperwork-core/doc/000077500000000000000000000000001417573700700201275ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/doc/Makefile000066400000000000000000000011111417573700700215610ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = python3 -msphinx SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) paperwork-2.1.1/openpaperwork-core/doc/conf.py000066400000000000000000000130401417573700700214240ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- project = 'Openpaperwork-core' copyright = '2019, Jerome Flesch' author = 'Jerome Flesch' # The short X.Y version version = '' # The full version, including alpha/beta/rc tags release = '' # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinxcontrib.plantuml', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = 'Openpaperwork-coredoc' # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'Openpaperwork-core.tex', 'Openpaperwork-core Documentation', 'Jerome Flesch', 'manual'), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'openpaperwork-core', 'Openpaperwork-core Documentation', [author], 1) ] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'Openpaperwork-core', 'Openpaperwork-core Documentation', author, 'Openpaperwork-core', 'One line description of project.', 'Miscellaneous'), ] # -- Options for Epub output ------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # -- Extension configuration ------------------------------------------------- # -- Options for todo extension ---------------------------------------------- # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True autodoc_inherit_docstrings = False paperwork-2.1.1/openpaperwork-core/doc/config.rst000066400000000000000000000022231417573700700221250ustar00rootroot00000000000000Configuration plugin ==================== Configuration management plugins provide a way to store an application configuration. Openpaperwork_core provides the plugin `openpaperwork_core.config` that acts a frontend for backend plugins (`openpaperowkr_core.config.backend.*`). It provides some high level operations (like registering options and their default value). Other plugins and applications should use this frontend only. When initialized, plugins are expected to register any setting they need. Only the plugin responsible for a setting register it ; Plugins depending on settings registered by one of their dependency do not need to register them. Backends provide access to the configuration storage (Python's ConfigParser, Windows registry, Android Content Provider, etc). Reference implementation for backends is `openpaperwork_core.config.backend.configparser` (based on Python's ConfigParser). ---- Frontend plugin ~~~~~~~~~~~~~~~ .. automodule:: openpaperwork_core.config :members: :undoc-members: ---- Backend plugin: File ~~~~~~~~~~~~~~~~~~~~ .. automodule:: openpaperwork_core.config.backend.configparser :members: :undoc-members: paperwork-2.1.1/openpaperwork-core/doc/core.rst000066400000000000000000000034701417573700700216150ustar00rootroot00000000000000Core & Plugins ============== Basic concepts -------------- The idea behind OpenPaperwork's plugins is similar to `Python duck typing `_: When you request something, it does not matter who does the job as long as it's done. To that end, when the calling code wants something done, it uses the core. It gives it a callback name and some arguments. It does not know which plugin will handle this call (nor if one will) and it doesn't matter to them as long as the job is done. Many plugins can provide the same callback names but with different implementations. Calling code can: * call all the callbacks with a given name one after the other: :py:meth:`~openpaperwork_core.Core.call_all`, * call them until one of them reply with a value != `None`: :py:meth:`~openpaperwork_core.Core.call_success`, * or call just one of them semi-randomly: :py:meth:`~openpaperwork_core.Core.call_one`. A plugin is a Python module containing a class named `Plugin` (subclassing :py:class:`~openpaperwork_core.PluginBase`). This class must be instantiable without arguments. Callbacks are all the methods provided by this class `Plugin` (with some exceptions, like methods starting with `_` and those coming from :py:class:`~openpaperwork_core.PluginBase`). Each plugin can implement many interfaces. Those interfaces are used to define dependencies and are simply conventions: Plugins pretending to implement some interfaces should implement the corresponding methods but no check is done to ensure they do. Examples -------- .. toctree:: example_plugin example_app API --- You're strongly advised to read the documentation of :py:meth:`~openpaperwork_core.Core.call_all`, :py:meth:`~openpaperwork_core.Core.call_success`, :py:meth:`~openpaperwork_core.Core.call_one`. .. toctree:: core_api paperwork-2.1.1/openpaperwork-core/doc/core_api.rst000066400000000000000000000001261417573700700224410ustar00rootroot00000000000000Core API ======== .. automodule:: openpaperwork_core :members: :undoc-members: paperwork-2.1.1/openpaperwork-core/doc/example_app.rst000066400000000000000000000016321417573700700231560ustar00rootroot00000000000000Calling Code Example ==================== .. code-block:: python import openpaperwork_core core = openpaperwork_core.Core() # Load mandatory plugins core.load("openpaperwork_plugin_a") core.load("mandatory_plugin_a") core.load("mandatory_plugin_b") # `init()` will load dependencies and call method `init()` on all the plugins # You can safely call `core.init()` many times core.init() # Load plugins requested by your user if any. # You can used previously loaded to get the plugin to load if you want. # For instance, you can load and initialize `openpaperwork_core.config` # and then use it to get a plugin list from a configuration file. core.load(...) core.init() nb_called = core.call_all('some_method_a', "random_argument") assert(nb_called > 0) return_value = core.call_success('some_method_a', "random_argument") assert(return_value is not None) paperwork-2.1.1/openpaperwork-core/doc/example_plugin.rst000066400000000000000000000043721417573700700237000ustar00rootroot00000000000000Plugin Example ============== .. code-block:: python import openpaperwork_core class Plugin(openpaperwork_core.PluginBase): # callbacks will always be run before those of priority <= 21 # but after those of priority >= 23 PRIORITY = 22 def __init__(self): # do something, but the least possible # cannot rely on any dependencies here. pass def get_interfaces(self): # indicates which interfaces this plugin satisfies. # note that this is only used for controlling that dependencies are # satisfied (see get_deps()). There are no checks at all to ensure # that the methods corresponding to each interface are actually # implemented. return ['interface_name_toto', 'interface_name_tutu'] def get_deps(self): # specify that we are looking for plugins implementing the # specified interface(s). # Provide also some default plugins to load if no plugins provide # the requested interface yet. # Note that plugins may be loaded in any order. Dependencies may # not be satisfied yet when they are loaded. # When initializing plugins, the core will make sure that # all dependencies are satisfied, loaded and initialized before # calling the method `init()` of this plugin. return [ { 'interface': 'interface_name_a', 'defaults': [ 'suggested_default_plugin_a', 'suggested_default_plugin_b', ], }, { 'interface': 'inteface_name_b', 'defaults': [ 'suggested_default_plugin_d', 'suggested_default_plugin_e', ], } ] def init(self, core): # all the dependencies have loaded and initialized. # we can safely rely on them here. super().init(core) def some_method_a(self, arg_a): # do something self.core.call_all("some_method_of_other_plugins", "arg_a", 22) paperwork-2.1.1/openpaperwork-core/doc/fs.rst000066400000000000000000000014651417573700700212770ustar00rootroot00000000000000File system plugins =================== Those plugins provide methods to access files on various storages. For instance, 'python' plugin uses the python API to access local files (`file://`), so does the GIO implementation (openpaperwork-gtk). Memory plugin allow storing data in filesystem-like manner (`memory://`). Later other plugins could provide access to other storages (MariaDB, HTTP, etc). The reference implementation is 'python'. A fake/mock implementation is provided for testing. It behaves in a way similar to the memory plugin. ---- Python-based implementation ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. automodule:: openpaperwork_gtk.fs.python :members: :undoc-members: ---- In-Memory implementation ~~~~~~~~~~~~~~~~~~~~~~~~ .. automodule:: openpaperwork_core.fs.memory :members: :undoc-members: paperwork-2.1.1/openpaperwork-core/doc/index.rst000066400000000000000000000006271417573700700217750ustar00rootroot00000000000000.. Openpaperwork-core documentation master file, created by sphinx-quickstart on Thu Nov 21 12:27:52 2019. Welcome to Openpaperwork-core's documentation! ============================================== .. toctree:: :maxdepth: 3 :caption: Contents: core config logs fs mainloop promise Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` paperwork-2.1.1/openpaperwork-core/doc/logs.rst000066400000000000000000000016601417573700700216300ustar00rootroot00000000000000Log management plugins ====================== Log management plugins catch Python logs (see `logging`), format them and send them somewhere. `uncaught_exception` --------------------- Broadcast uncaught exceptions (see `sys.excepthook`) by calling `self.core.call_all("on_uncaught_exception", exc_info)`. `logs.print` ------------ Send the logs to stderr, stdout or a file. It can send to many outputs at the same time. Configuration entries are as follow: .. code-block:: ini [logging] level = str:info # none, critical, error, warn, warning, info, debug files = str:stderr,temp,/tmp/test.txt # 'stderr', 'temp', or a file path format = str:[%(levelname)-6s] [%(name)-30s] %(message)s It monitors the configuration. So to change its settings, you can just update the configuration using the plugin `openpaperwork_core.config`. ---- .. automodule:: openpaperwork_core.log_collector :members: :undoc-members: paperwork-2.1.1/openpaperwork-core/doc/mainloop.rst000066400000000000000000000015221417573700700224770ustar00rootroot00000000000000Mainloop plugins ================ Most GUI applications need a main loop. A main loop is a thread dedicated to running callbacks provided by other threads (or stacked before the main loop is started). Depending on the environment in which you're working, you may need different mainloop implementations. For instance, Python 3 provide main loop support through `asyncio`. If you're working in a GTK environment, you will have to use the GLib mainloop instead. Openpaperwork_core provides an implementation that uses Python 3's `asyncio` module. More advanced main loops may provide features such as waiting for an event to occur on a file descriptor. The main loop interface provided here is kept simple so it can easily be implemented for any other platforms. .. automodule:: openpaperwork_core.mainloop_asyncio :members: :undoc-members: paperwork-2.1.1/openpaperwork-core/doc/promise.rst000066400000000000000000000001361417573700700223370ustar00rootroot00000000000000Promises ======== .. automodule:: openpaperwork_core.promise :members: :undoc-members: paperwork-2.1.1/openpaperwork-core/examples/000077500000000000000000000000001417573700700212005ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/examples/core_plugins.py000077500000000000000000000010231417573700700242420ustar00rootroot00000000000000#!/usr/bin/env python3 import logging import openpaperwork_core LOGGER = logging.getLogger(__name__) def main(): LOGGER.info("Start") core = openpaperwork_core.Core() core.load("openpaperwork_core.logs.print") core.load("openpaperwork_core.uncaught_exception") core.load('openpaperwork_core.config') core.load('openpaperwork_core.config.backend.configparser') core.init() core.init_logs("some_app_name", default_log_level="info") LOGGER.info("End") if __name__ == "__main__": main() paperwork-2.1.1/openpaperwork-core/l10n/000077500000000000000000000000001417573700700201345ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/l10n/de.po000066400000000000000000000176461417573700700211020ustar00rootroot00000000000000# German translations for PACKAGE package. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2021-10-25 05:49+0000\n" "Last-Translator: Andreas Forster \n" "Language-Team: German \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.4\n" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:38 msgid "Today" msgstr "Heute" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:39 msgid "Yesterday" msgstr "Gestern" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:41 #, python-format msgid "%3.1f bytes" msgstr "%3.1f Bytes" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:42 #, python-format msgid "%3.1f KiB" msgstr "%3.1f KiB" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:43 #, python-format msgid "%3.1f MiB" msgstr "%3.1f MiB" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:44 #, python-format msgid "%3.1f GiB" msgstr "%3.1f GiB" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:45 #, python-format msgid "%3.1f TiB" msgstr "%3.1f TiB" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:52 msgid "Check that all required dependencies are installed" msgstr "Überprüfe, dass alle benötigten Abhängigkeiten installiert sind" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:73 msgid "Missing dependencies:" msgstr "Fehlende Abhängigkeiten:" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:75 #, python-brace-format msgid "- {dep_name} (package: {pkg_name})" msgstr "-{dep_name} (Paket: {pkg_name})" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:80 msgid "UNKNOWN" msgstr "Unbekannt" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:100 msgid "Suggested command:" msgstr "Vorgeschlagener Befehl:" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:105 msgid "Do you want to run this command now ?" msgstr "Möchtest du dieses Kommando jetzt ausführen?" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:119 msgid "Don't know how to install missing dependencies. Sorry." msgstr "" "Die Installationsroutine für unbekannte Abhängigkeiten konnte nicht erkannt " "werden." #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:122 msgid "Nothing to do." msgstr "Es gibt nichts zu tun." #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:58 msgid "Manage Paperwork configuration" msgstr "Verwalte die Konfiguration von Paperwork" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:62 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:49 msgid "sub-command" msgstr "Unterkommando" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:66 msgid "Get a value from Paperwork's configuration" msgstr "Gebe einen spezifischen Eintrag der Konfiguration aus" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:71 msgid "Set a value in Paperwork's configuration" msgstr "Setze einen spezifischen Eintrag der Konfiguration" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:74 msgid "See 'config list_type'" msgstr "Siehe 'config list_type'" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:78 msgid "Show Paperwork's configuration" msgstr "Gebe die Konfiguration von Paperwork aus" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:83 msgid "Show value types you can use from command line" msgstr "" "Zeige die möglichen Werte, die über die Kommandozeile verwendet werden können" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:45 #, python-format msgid "Manage %s plugins" msgstr "Verwalte Erweiterungen für %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:54 #, python-format msgid "Show plugins enabled for %s" msgstr "Zeige alle aktivierten Erweiterungen für %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:60 #, python-format msgid "Add plugin in %s" msgstr "Aktiviere Erweiterung für %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:66 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:77 msgid "Do not correct dependencies automatically" msgstr "Korrigiere Abhängigkeiten nicht automatisch" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:71 #, python-format msgid "Remove plugin from %s" msgstr "Deaktiviere Erweiterung für %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:82 msgid "Clean up your mess by reseting the plugin list to its default value" msgstr "Setze die Liste an Erweiterungen zurück" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:89 msgid "Show information regarding a plugin (must be enabled)" msgstr "Zeige Informationen über eine aktivierte Erweiterung" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:125 #, python-format msgid "Error: Plugin '%s' not found" msgstr "Fehler: Plugin '%s' nicht gefunden" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:143 #, python-brace-format msgid "" "Adding plugin '{plugin_name}' to satisfy dependency of '{other_plugin_name}' " "on interface '{interface}'" msgstr "" "Füge Plugin '{plugin_name}' hinzu, um die Abhängigeit von " "'{other_plugin_name}' am Interface '{interface}' zu erfüllen" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:162 msgid "Plugin {} added" msgstr "Plugin {} hinzugefügt" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:184 #, python-brace-format msgid "Removing plugin '{plugin_name}' due to missing dependency '{interface}'" msgstr "" "Entferne das Plugin '{plugin_name}' wegen der fehlenden Abhängigkeit " "'{interface}'" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:201 msgid "Plugin {} removed" msgstr "Plugin {} entfernt" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:207 msgid "Active plugins:" msgstr "Aktive Erweiterungen:" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:293 #, python-format msgid "Plugin '%s' not enabled." msgstr "Plugin '%s' nicht aktiviert." #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:298 #, python-format msgid "Plugin '%s':" msgstr "Plugin '%s':" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:300 msgid "Implements:" msgstr "Implementiert:" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:303 msgid "Depends on:" msgstr "Hängt ab von:" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:309 #, python-format msgid "suggested: %s" msgstr "vorgeschlagen: %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:317 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:330 msgid "Plugin name" msgstr "Plugin Name" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:318 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:331 msgid "Interface" msgstr "Schnittstelle" #: openpaperwork-core/src/openpaperwork_core/cmd/util.py:10 msgid "Y/n" msgstr "Y/n" #: openpaperwork-core/src/openpaperwork_core/cmd/util.py:13 msgid "y/N" msgstr "y/N" #: openpaperwork-core/src/openpaperwork_core/logs/archives.py:134 #: openpaperwork-core/src/openpaperwork_core/logs/archives.py:145 msgid "Log file" msgstr "Logdatei" #: openpaperwork-core/src/openpaperwork_core/config/backend/configparser.py:333 msgid "App. config." msgstr "App. Konfiguration." #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:126 msgid "App. & system info." msgstr "App. & System info." #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:127 msgid "Select to generate" msgstr "Wähle zum Erzeugen" #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:149 msgid "Collecting statistics ..." msgstr "Sammle Statistiken ..." paperwork-2.1.1/openpaperwork-core/l10n/es.po000066400000000000000000000145631417573700700211140ustar00rootroot00000000000000# Spanish translations for PACKAGE package. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2020-05-03 15:35+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=ASCII\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:38 msgid "Today" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:39 msgid "Yesterday" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:41 #, python-format msgid "%3.1f bytes" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:42 #, python-format msgid "%3.1f KiB" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:43 #, python-format msgid "%3.1f MiB" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:44 #, python-format msgid "%3.1f GiB" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:45 #, python-format msgid "%3.1f TiB" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:52 msgid "Check that all required dependencies are installed" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:73 msgid "Missing dependencies:" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:75 #, python-brace-format msgid "- {dep_name} (package: {pkg_name})" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:80 msgid "UNKNOWN" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:100 msgid "Suggested command:" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:105 msgid "Do you want to run this command now ?" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:119 msgid "Don't know how to install missing dependencies. Sorry." msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:122 msgid "Nothing to do." msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:58 msgid "Manage Paperwork configuration" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:62 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:49 msgid "sub-command" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:66 msgid "Get a value from Paperwork's configuration" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:71 msgid "Set a value in Paperwork's configuration" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:74 msgid "See 'config list_type'" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:78 msgid "Show Paperwork's configuration" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:83 msgid "Show value types you can use from command line" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:45 #, python-format msgid "Manage %s plugins" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:54 #, python-format msgid "Show plugins enabled for %s" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:60 #, python-format msgid "Add plugin in %s" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:66 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:77 msgid "Do not correct dependencies automatically" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:71 #, python-format msgid "Remove plugin from %s" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:82 msgid "Clean up your mess by reseting the plugin list to its default value" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:89 msgid "Show information regarding a plugin (must be enabled)" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:125 #, python-format msgid "Error: Plugin '%s' not found" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:143 #, python-brace-format msgid "" "Adding plugin '{plugin_name}' to satisfy dependency of '{other_plugin_name}' " "on interface '{interface}'" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:162 msgid "Plugin {} added" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:184 #, python-brace-format msgid "Removing plugin '{plugin_name}' due to missing dependency '{interface}'" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:201 msgid "Plugin {} removed" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:207 msgid "Active plugins:" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:293 #, python-format msgid "Plugin '%s' not enabled." msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:298 #, python-format msgid "Plugin '%s':" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:300 msgid "Implements:" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:303 msgid "Depends on:" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:309 #, python-format msgid "suggested: %s" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:317 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:330 msgid "Plugin name" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:318 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:331 msgid "Interface" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/util.py:10 msgid "Y/n" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/util.py:13 msgid "y/N" msgstr "" #: openpaperwork-core/src/openpaperwork_core/logs/archives.py:134 #: openpaperwork-core/src/openpaperwork_core/logs/archives.py:145 msgid "Log file" msgstr "" #: openpaperwork-core/src/openpaperwork_core/config/backend/configparser.py:333 msgid "App. config." msgstr "" #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:126 msgid "App. & system info." msgstr "" #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:127 msgid "Select to generate" msgstr "" #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:149 msgid "Collecting statistics ..." msgstr "" paperwork-2.1.1/openpaperwork-core/l10n/fr.po000066400000000000000000000176551417573700700211210ustar00rootroot00000000000000# French translations for PACKAGE package # Traductions françaises du paquet PACKAGE. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2021-11-16 17:11+0000\n" "Last-Translator: Jerome Flesch \n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 4.9\n" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:38 msgid "Today" msgstr "Aujourd'hui" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:39 msgid "Yesterday" msgstr "Hier" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:41 #, python-format msgid "%3.1f bytes" msgstr "%3.1f octets" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:42 #, python-format msgid "%3.1f KiB" msgstr "%3.1f Kio" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:43 #, python-format msgid "%3.1f MiB" msgstr "%3.1f MiO" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:44 #, python-format msgid "%3.1f GiB" msgstr "%3.1f Gio" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:45 #, python-format msgid "%3.1f TiB" msgstr "%3.1f Tio" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:52 msgid "Check that all required dependencies are installed" msgstr "Vérifie que toutes les dépendances requises sont installées" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:73 msgid "Missing dependencies:" msgstr "Dépendances manquantes :" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:75 #, python-brace-format msgid "- {dep_name} (package: {pkg_name})" msgstr "- {dep_name} (paquet : {pkg_name})" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:80 msgid "UNKNOWN" msgstr "INCONNU" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:100 msgid "Suggested command:" msgstr "Commande suggérée :" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:105 msgid "Do you want to run this command now ?" msgstr "Voulez-vous exécuter cette commande ?" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:119 msgid "Don't know how to install missing dependencies. Sorry." msgstr "Je ne sais pas comment installer ces dépendances. Désolé." #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:122 msgid "Nothing to do." msgstr "Rien à faire." #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:58 msgid "Manage Paperwork configuration" msgstr "Gérer la configuration de Paperwork" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:62 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:49 msgid "sub-command" msgstr "sous-commande" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:66 msgid "Get a value from Paperwork's configuration" msgstr "Renvoi une valeur depuis la configuration de Paperwork" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:71 msgid "Set a value in Paperwork's configuration" msgstr "Défini une valeur dans la configuration de Paperwork" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:74 msgid "See 'config list_type'" msgstr "Voir 'config list_type'" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:78 msgid "Show Paperwork's configuration" msgstr "Affiche la configuration de Paperwork" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:83 msgid "Show value types you can use from command line" msgstr "" "Affiche les types de valeurs que vous pouvez utiliser depuis la ligne de " "commande" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:45 #, python-format msgid "Manage %s plugins" msgstr "Gérer les plugins de %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:54 #, python-format msgid "Show plugins enabled for %s" msgstr "Montre les plugins activés pour %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:60 #, python-format msgid "Add plugin in %s" msgstr "Ajoute un plugin dans %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:66 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:77 msgid "Do not correct dependencies automatically" msgstr "Ne pas corriger les dépendances automatiquement" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:71 #, python-format msgid "Remove plugin from %s" msgstr "Retire le plugin de %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:82 msgid "Clean up your mess by reseting the plugin list to its default value" msgstr "Réinitialise la liste des plugins à sa valeur par défaut" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:89 msgid "Show information regarding a plugin (must be enabled)" msgstr "Montre les informations concernant un plugin (doit être activé)" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:125 #, python-format msgid "Error: Plugin '%s' not found" msgstr "Erreur : Plugin '%s' non-trouvé" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:143 #, python-brace-format msgid "" "Adding plugin '{plugin_name}' to satisfy dependency of '{other_plugin_name}' " "on interface '{interface}'" msgstr "" "Ajoute le plugin '{plugin_name}' pour satisfaire la dépendance de " "'{other_plugin_name}' sur l'interface '{interface}'" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:162 msgid "Plugin {} added" msgstr "Plugin {} ajouté" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:184 #, python-brace-format msgid "Removing plugin '{plugin_name}' due to missing dependency '{interface}'" msgstr "" "Retire le plugin '{plugin_name}' à cause de la dépendance manquante " "'{interface}'" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:201 msgid "Plugin {} removed" msgstr "Plugin {} retiré" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:207 msgid "Active plugins:" msgstr "Plugins actifs :" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:293 #, python-format msgid "Plugin '%s' not enabled." msgstr "Plugin '%s' non-activé." #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:298 #, python-format msgid "Plugin '%s':" msgstr "Plugin '%s' :" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:300 msgid "Implements:" msgstr "Implémente :" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:303 msgid "Depends on:" msgstr "Dépend de :" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:309 #, python-format msgid "suggested: %s" msgstr "Suggéré : %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:317 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:330 msgid "Plugin name" msgstr "Nom du plugin" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:318 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:331 msgid "Interface" msgstr "Interface" #: openpaperwork-core/src/openpaperwork_core/cmd/util.py:10 msgid "Y/n" msgstr "O/n" #: openpaperwork-core/src/openpaperwork_core/cmd/util.py:13 msgid "y/N" msgstr "o/N" #: openpaperwork-core/src/openpaperwork_core/logs/archives.py:134 #: openpaperwork-core/src/openpaperwork_core/logs/archives.py:145 msgid "Log file" msgstr "Traces" #: openpaperwork-core/src/openpaperwork_core/config/backend/configparser.py:333 msgid "App. config." msgstr "Config. de l'app." #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:126 msgid "App. & system info." msgstr "App. & info. système" #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:127 msgid "Select to generate" msgstr "Sélectionnez pour générer" #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:149 msgid "Collecting statistics ..." msgstr "Collecte de statistiques en cours …" paperwork-2.1.1/openpaperwork-core/l10n/messages.pot000066400000000000000000000145221417573700700224730ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:38 msgid "Today" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:39 msgid "Yesterday" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:41 #, python-format msgid "%3.1f bytes" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:42 #, python-format msgid "%3.1f KiB" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:43 #, python-format msgid "%3.1f MiB" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:44 #, python-format msgid "%3.1f GiB" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:45 #, python-format msgid "%3.1f TiB" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:52 msgid "Check that all required dependencies are installed" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:73 msgid "Missing dependencies:" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:75 #, python-brace-format msgid "- {dep_name} (package: {pkg_name})" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:80 msgid "UNKNOWN" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:100 msgid "Suggested command:" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:105 msgid "Do you want to run this command now ?" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:119 msgid "Don't know how to install missing dependencies. Sorry." msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:122 msgid "Nothing to do." msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:58 msgid "Manage Paperwork configuration" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:62 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:49 msgid "sub-command" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:66 msgid "Get a value from Paperwork's configuration" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:71 msgid "Set a value in Paperwork's configuration" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:74 msgid "See 'config list_type'" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:78 msgid "Show Paperwork's configuration" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:83 msgid "Show value types you can use from command line" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:45 #, python-format msgid "Manage %s plugins" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:54 #, python-format msgid "Show plugins enabled for %s" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:60 #, python-format msgid "Add plugin in %s" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:66 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:77 msgid "Do not correct dependencies automatically" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:71 #, python-format msgid "Remove plugin from %s" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:82 msgid "Clean up your mess by reseting the plugin list to its default value" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:89 msgid "Show information regarding a plugin (must be enabled)" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:125 #, python-format msgid "Error: Plugin '%s' not found" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:143 #, python-brace-format msgid "" "Adding plugin '{plugin_name}' to satisfy dependency of '{other_plugin_name}' " "on interface '{interface}'" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:162 msgid "Plugin {} added" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:184 #, python-brace-format msgid "Removing plugin '{plugin_name}' due to missing dependency '{interface}'" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:201 msgid "Plugin {} removed" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:207 msgid "Active plugins:" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:293 #, python-format msgid "Plugin '%s' not enabled." msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:298 #, python-format msgid "Plugin '%s':" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:300 msgid "Implements:" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:303 msgid "Depends on:" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:309 #, python-format msgid "suggested: %s" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:317 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:330 msgid "Plugin name" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:318 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:331 msgid "Interface" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/util.py:10 msgid "Y/n" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/util.py:13 msgid "y/N" msgstr "" #: openpaperwork-core/src/openpaperwork_core/logs/archives.py:134 #: openpaperwork-core/src/openpaperwork_core/logs/archives.py:145 msgid "Log file" msgstr "" #: openpaperwork-core/src/openpaperwork_core/config/backend/configparser.py:333 msgid "App. config." msgstr "" #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:126 msgid "App. & system info." msgstr "" #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:127 msgid "Select to generate" msgstr "" #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:149 msgid "Collecting statistics ..." msgstr "" paperwork-2.1.1/openpaperwork-core/l10n/oc.po000066400000000000000000000176431417573700700211100ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2021-10-23 15:04+0000\n" "Last-Translator: Quentin PAGÈS \n" "Language-Team: Occitan \n" "Language: oc\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 4.4\n" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:38 msgid "Today" msgstr "Uèi" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:39 msgid "Yesterday" msgstr "Ièr" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:41 #, python-format msgid "%3.1f bytes" msgstr "%3.1f octets" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:42 #, python-format msgid "%3.1f KiB" msgstr "%3.1f Kio" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:43 #, python-format msgid "%3.1f MiB" msgstr "%3.1f MiO" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:44 #, python-format msgid "%3.1f GiB" msgstr "%3.1f Gio" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:45 #, python-format msgid "%3.1f TiB" msgstr "%3.1f Tio" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:52 msgid "Check that all required dependencies are installed" msgstr "Verificar que totas las dependéncias requeridas son installadas" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:73 msgid "Missing dependencies:" msgstr "Dependéncias mancantas :" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:75 #, python-brace-format msgid "- {dep_name} (package: {pkg_name})" msgstr "- {dep_name} (paquet : {pkg_name})" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:80 msgid "UNKNOWN" msgstr "DESCONEGUT" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:100 msgid "Suggested command:" msgstr "Comanda suggerida :" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:105 msgid "Do you want to run this command now ?" msgstr "Volètz executar aquesta comanda ara ?" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:119 msgid "Don't know how to install missing dependencies. Sorry." msgstr "Sabi pas cossí installar aquestas dependéncias. Desconsolat." #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:122 msgid "Nothing to do." msgstr "Res a far." #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:58 msgid "Manage Paperwork configuration" msgstr "Gerir la configuracion de Paperwork" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:62 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:49 msgid "sub-command" msgstr "jos-comanda" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:66 msgid "Get a value from Paperwork's configuration" msgstr "Obténer una valor a partir de la configuracion de Paperwork" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:71 msgid "Set a value in Paperwork's configuration" msgstr "Definir una valor dins la configuracion de Paperwork" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:74 msgid "See 'config list_type'" msgstr "Veire « config list_type »" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:78 msgid "Show Paperwork's configuration" msgstr "Mostrar la configuracion de Paperwork" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:83 msgid "Show value types you can use from command line" msgstr "" "Mostrar los tipes de valors que podètz utilizar a partir de la linha de " "comanda" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:45 #, python-format msgid "Manage %s plugins" msgstr "Gerir lo modul %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:54 #, python-format msgid "Show plugins enabled for %s" msgstr "Mostrar los moduls activats per %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:60 #, python-format msgid "Add plugin in %s" msgstr "Ajustar un modul dins %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:66 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:77 msgid "Do not correct dependencies automatically" msgstr "Corregir pas automaticament las dependéncias" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:71 #, python-format msgid "Remove plugin from %s" msgstr "Tirar lo modul de %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:82 msgid "Clean up your mess by reseting the plugin list to its default value" msgstr "Reïnicializar la lista dels moduls a la valor de defaut" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:89 msgid "Show information regarding a plugin (must be enabled)" msgstr "Mostrar las informacions que concernisson un modul (deu èsser activat)" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:125 #, python-format msgid "Error: Plugin '%s' not found" msgstr "Error : modul « %s » pas trobat" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:143 #, python-brace-format msgid "" "Adding plugin '{plugin_name}' to satisfy dependency of '{other_plugin_name}' " "on interface '{interface}'" msgstr "" "Ajusta l modul « {plugin_name} » per satisfar la dependéncia de " "« {other_plugin_name} » sus l’interfàcia « {interface} »" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:162 msgid "Plugin {} added" msgstr "Modul {} ajustat" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:184 #, python-brace-format msgid "Removing plugin '{plugin_name}' due to missing dependency '{interface}'" msgstr "" "Tira lo modul « {plugin_name} » a causa de la dependéncia mancanta " "« {interface} »" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:201 msgid "Plugin {} removed" msgstr "Modul {} tirat" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:207 msgid "Active plugins:" msgstr "Moduls actius :" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:293 #, python-format msgid "Plugin '%s' not enabled." msgstr "Modul « %s » pas activat." #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:298 #, python-format msgid "Plugin '%s':" msgstr "Modul « %s » :" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:300 msgid "Implements:" msgstr "Implementa :" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:303 msgid "Depends on:" msgstr "Depend de :" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:309 #, python-format msgid "suggested: %s" msgstr "Suggerit : %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:317 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:330 msgid "Plugin name" msgstr "Nom modul" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:318 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:331 msgid "Interface" msgstr "Interfàcia" #: openpaperwork-core/src/openpaperwork_core/cmd/util.py:10 msgid "Y/n" msgstr "O/n" #: openpaperwork-core/src/openpaperwork_core/cmd/util.py:13 msgid "y/N" msgstr "o/N" #: openpaperwork-core/src/openpaperwork_core/logs/archives.py:134 #: openpaperwork-core/src/openpaperwork_core/logs/archives.py:145 msgid "Log file" msgstr "Fichièr d'istoric" #: openpaperwork-core/src/openpaperwork_core/config/backend/configparser.py:333 msgid "App. config." msgstr "Config. de l'ap." #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:126 msgid "App. & system info." msgstr "Aplicacion e info sustèma" #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:127 msgid "Select to generate" msgstr "Seleccionar per generar" #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:149 msgid "Collecting statistics ..." msgstr "Reculhida d’estatisticas..." paperwork-2.1.1/openpaperwork-core/l10n/sv.po000066400000000000000000000167461417573700700211420ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-09-01 22:01+0200\n" "PO-Revision-Date: 2021-01-04 15:31+0000\n" "Last-Translator: Åke Engelbrektson \n" "Language-Team: Swedish \n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.4\n" #: openpaperwork-core/src/openpaperwork_core/logs/archives.py:123 #: openpaperwork-core/src/openpaperwork_core/logs/archives.py:134 msgid "Log file" msgstr "Loggfil" #: openpaperwork-core/src/openpaperwork_core/cmd/util.py:10 msgid "Y/n" msgstr "J/n" #: openpaperwork-core/src/openpaperwork_core/cmd/util.py:13 msgid "y/N" msgstr "j/N" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:50 msgid "Manage Paperwork configuration" msgstr "Hantera Paperwork-konfigurationen" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:54 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:49 msgid "sub-command" msgstr "underkommando" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:58 msgid "Get a value from Paperwork's configuration" msgstr "Hämta ett värde från Paperworks konfiguration" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:63 msgid "Set a value in Paperwork's configuration" msgstr "Ange ett värde i Paperworks konfiguration" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:66 msgid "See 'config list_type'" msgstr "See \"config list_type\"" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:70 msgid "Show Paperwork's configuration" msgstr "Visa Paperworks konfiguration" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:75 msgid "Show value types you can use from command line" msgstr "Visa värdetyper du kan använda i kommandoraden" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:52 msgid "Check that all required dependencies are installed" msgstr "Kolla om alla beroenden är installerade" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:73 msgid "Missing dependencies:" msgstr "Saknade beroenden:" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:75 #, python-brace-format msgid "- {dep_name} (package: {pkg_name})" msgstr "- {dep_name} (package: {pkg_name})" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:80 msgid "UNKNOWN" msgstr "OKÄND" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:100 msgid "Suggested command:" msgstr "Föreslaget kommando:" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:105 msgid "Do you want to run this command now ?" msgstr "Vill du köra det här kommandot?" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:119 msgid "Don't know how to install missing dependencies. Sorry." msgstr "Vet inte hur man installerar saknade beroenden, tyvärr." #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:122 msgid "Nothing to do." msgstr "Inget att göra." #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:45 #, python-format msgid "Manage %s plugins" msgstr "Hantera %s insticksmoduler" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:54 #, python-format msgid "Show plugins enabled for %s" msgstr "Visa insticksmoduler aktiverade för %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:60 #, python-format msgid "Add plugin in %s" msgstr "Lägg till insticksmodul %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:66 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:77 msgid "Do not correct dependencies automatically" msgstr "Korrigera inte beroenden automatiskt" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:71 #, python-format msgid "Remove plugin from %s" msgstr "Ta bort insticksmodul från %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:82 msgid "Clean up your mess by reseting the plugin list to its default value" msgstr "" "Städa upp din röra genom att återställa instickslistan till dess " "standardvärde" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:89 msgid "Show information regarding a plugin (must be enabled)" msgstr "Visa information rörande en insticksmodul (måste vara aktiverat)" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:125 #, python-format msgid "Error: Plugin '%s' not found" msgstr "Fel: Insticksmodul \"%s\" hittades inte" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:143 #, python-brace-format msgid "" "Adding plugin '{plugin_name}' to satisfy dependency of '{other_plugin_name}' " "on interface '{interface}'" msgstr "" "Lägger till insticksmodul \"{plugin_name}\" för att tillfredsställa beroende " "för \"{other_plugin_name}\" eller gränssnittet \"{interface}\"" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:162 msgid "Plugin {} added" msgstr "Insticksmodul {} tillagd" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:184 #, python-brace-format msgid "Removing plugin '{plugin_name}' due to missing dependency '{interface}'" msgstr "" "Tar bort insticksmodul \"{plugin_name}\" på grund av saknat beroende \"" "{interface}\"" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:201 msgid "Plugin {} removed" msgstr "Insticksmodul {} borttagen" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:207 msgid "Active plugins:" msgstr "Aktiva insticksmoduler:" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:293 #, python-format msgid "Plugin '%s' not enabled." msgstr "Insticksmodul \"%s\" är inte aktiverad." #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:298 #, python-format msgid "Plugin '%s':" msgstr "Insticksmodul \"%s\":" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:300 msgid "Implements:" msgstr "Implementerar:" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:303 msgid "Depends on:" msgstr "Beroende av:" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:309 #, python-format msgid "suggested: %s" msgstr "föreslaget: %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:317 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:330 msgid "Plugin name" msgstr "Insticksnamn" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:318 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:331 msgid "Interface" msgstr "Gränssnitt" #: openpaperwork-core/src/openpaperwork_core/config/backend/configparser.py:333 msgid "App. config." msgstr "App. config." #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:31 msgid "Today" msgstr "Idag" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:32 msgid "Yesterday" msgstr "I går" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:34 #, python-format msgid "%3.1f bytes" msgstr "%3.1f bytes" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:35 #, python-format msgid "%3.1f KiB" msgstr "%3.1f KiB" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:36 #, python-format msgid "%3.1f MiB" msgstr "%3.1f MiB" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:37 #, python-format msgid "%3.1f GiB" msgstr "%3.1f GiB" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:38 #, python-format msgid "%3.1f TiB" msgstr "%3.1f TiB" paperwork-2.1.1/openpaperwork-core/l10n/uk.po000066400000000000000000000147021417573700700211170ustar00rootroot00000000000000# Ukrainian translations for PACKAGE package. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2020-05-03 15:35+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: uk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=ASCII\n" "Content-Transfer-Encoding: 8bit\n" "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);\n" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:38 msgid "Today" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:39 msgid "Yesterday" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:41 #, python-format msgid "%3.1f bytes" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:42 #, python-format msgid "%3.1f KiB" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:43 #, python-format msgid "%3.1f MiB" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:44 #, python-format msgid "%3.1f GiB" msgstr "" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:45 #, python-format msgid "%3.1f TiB" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:52 msgid "Check that all required dependencies are installed" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:73 msgid "Missing dependencies:" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:75 #, python-brace-format msgid "- {dep_name} (package: {pkg_name})" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:80 msgid "UNKNOWN" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:100 msgid "Suggested command:" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:105 msgid "Do you want to run this command now ?" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:119 msgid "Don't know how to install missing dependencies. Sorry." msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:122 msgid "Nothing to do." msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:58 msgid "Manage Paperwork configuration" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:62 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:49 msgid "sub-command" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:66 msgid "Get a value from Paperwork's configuration" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:71 msgid "Set a value in Paperwork's configuration" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:74 msgid "See 'config list_type'" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:78 msgid "Show Paperwork's configuration" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:83 msgid "Show value types you can use from command line" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:45 #, python-format msgid "Manage %s plugins" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:54 #, python-format msgid "Show plugins enabled for %s" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:60 #, python-format msgid "Add plugin in %s" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:66 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:77 msgid "Do not correct dependencies automatically" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:71 #, python-format msgid "Remove plugin from %s" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:82 msgid "Clean up your mess by reseting the plugin list to its default value" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:89 msgid "Show information regarding a plugin (must be enabled)" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:125 #, python-format msgid "Error: Plugin '%s' not found" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:143 #, python-brace-format msgid "" "Adding plugin '{plugin_name}' to satisfy dependency of '{other_plugin_name}' " "on interface '{interface}'" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:162 msgid "Plugin {} added" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:184 #, python-brace-format msgid "Removing plugin '{plugin_name}' due to missing dependency '{interface}'" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:201 msgid "Plugin {} removed" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:207 msgid "Active plugins:" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:293 #, python-format msgid "Plugin '%s' not enabled." msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:298 #, python-format msgid "Plugin '%s':" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:300 msgid "Implements:" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:303 msgid "Depends on:" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:309 #, python-format msgid "suggested: %s" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:317 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:330 msgid "Plugin name" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:318 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:331 msgid "Interface" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/util.py:10 msgid "Y/n" msgstr "" #: openpaperwork-core/src/openpaperwork_core/cmd/util.py:13 msgid "y/N" msgstr "" #: openpaperwork-core/src/openpaperwork_core/logs/archives.py:134 #: openpaperwork-core/src/openpaperwork_core/logs/archives.py:145 msgid "Log file" msgstr "" #: openpaperwork-core/src/openpaperwork_core/config/backend/configparser.py:333 msgid "App. config." msgstr "" #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:126 msgid "App. & system info." msgstr "" #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:127 msgid "Select to generate" msgstr "" #: openpaperwork-core/src/openpaperwork_core/beacon/stats.py:149 msgid "Collecting statistics ..." msgstr "" paperwork-2.1.1/openpaperwork-core/l10n/zh_Hans.po000066400000000000000000000163041417573700700220720ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-09-01 22:01+0200\n" "PO-Revision-Date: 2021-02-06 09:20+0000\n" "Last-Translator: 玉堂白鹤 \n" "Language-Team: Chinese (Simplified) \n" "Language: zh_Hans\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Weblate 4.4\n" #: openpaperwork-core/src/openpaperwork_core/logs/archives.py:123 #: openpaperwork-core/src/openpaperwork_core/logs/archives.py:134 msgid "Log file" msgstr "日志文件" #: openpaperwork-core/src/openpaperwork_core/cmd/util.py:10 msgid "Y/n" msgstr "Y/n" #: openpaperwork-core/src/openpaperwork_core/cmd/util.py:13 msgid "y/N" msgstr "y/N" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:50 msgid "Manage Paperwork configuration" msgstr "管理 Paperwork 配置" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:54 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:49 msgid "sub-command" msgstr "子命令" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:58 msgid "Get a value from Paperwork's configuration" msgstr "从 Paperwork 的配置中获取值" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:63 msgid "Set a value in Paperwork's configuration" msgstr "设置一个值到 Paperwork 的配置中" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:66 msgid "See 'config list_type'" msgstr "查看 'config list_type'" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:70 msgid "Show Paperwork's configuration" msgstr "显示 Paperwork 配置" #: openpaperwork-core/src/openpaperwork_core/cmd/config.py:75 msgid "Show value types you can use from command line" msgstr "显示可以从命令行使用的值类型" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:52 msgid "Check that all required dependencies are installed" msgstr "检查是否已安装所有必需的依赖项" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:73 msgid "Missing dependencies:" msgstr "缺少依赖项:" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:75 #, python-brace-format msgid "- {dep_name} (package: {pkg_name})" msgstr "- {dep_name} (软件包: {pkg_name})" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:80 msgid "UNKNOWN" msgstr "未知" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:100 msgid "Suggested command:" msgstr "建议的命令:" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:105 msgid "Do you want to run this command now ?" msgstr "您是否要立即运行此命令 ?" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:119 msgid "Don't know how to install missing dependencies. Sorry." msgstr "不知道如何安装缺少的依赖项。抱歉。" #: openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py:122 msgid "Nothing to do." msgstr "无事可做。" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:45 #, python-format msgid "Manage %s plugins" msgstr "管理%s插件" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:54 #, python-format msgid "Show plugins enabled for %s" msgstr "显示为 %s 启用的插件" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:60 #, python-format msgid "Add plugin in %s" msgstr "添加插件至 %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:66 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:77 msgid "Do not correct dependencies automatically" msgstr "不要自动更正依赖关系" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:71 #, python-format msgid "Remove plugin from %s" msgstr "从 %s 移除插件" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:82 msgid "Clean up your mess by reseting the plugin list to its default value" msgstr "通过将插件列表重置为默认值来清理混乱" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:89 msgid "Show information regarding a plugin (must be enabled)" msgstr "显示有关插件的信息 (必须启用)" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:125 #, python-format msgid "Error: Plugin '%s' not found" msgstr "错误: 找不到插件 %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:143 #, python-brace-format msgid "" "Adding plugin '{plugin_name}' to satisfy dependency of '{other_plugin_name}' " "on interface '{interface}'" msgstr "添加插件 '{plugin_name}' 以满足接口 '{interface}' 上 '{other_plugin_name}' 的依赖" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:162 msgid "Plugin {} added" msgstr "插件 {} 已添加" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:184 #, python-brace-format msgid "Removing plugin '{plugin_name}' due to missing dependency '{interface}'" msgstr "由于缺少依赖项 “{interface}”,正在删除插件“{plugin_name}”" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:201 msgid "Plugin {} removed" msgstr "插件 {} 已移除" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:207 msgid "Active plugins:" msgstr "活动插件:" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:293 #, python-format msgid "Plugin '%s' not enabled." msgstr "插件 '%s' 未启用。" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:298 #, python-format msgid "Plugin '%s':" msgstr "插件 '%s':" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:300 msgid "Implements:" msgstr "工具:" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:303 msgid "Depends on:" msgstr "依赖于:" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:309 #, python-format msgid "suggested: %s" msgstr "建议: %s" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:317 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:330 msgid "Plugin name" msgstr "插件名称" #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:318 #: openpaperwork-core/src/openpaperwork_core/cmd/plugins.py:331 msgid "Interface" msgstr "接口" #: openpaperwork-core/src/openpaperwork_core/config/backend/configparser.py:333 msgid "App. config." msgstr "App. 配置." #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:31 msgid "Today" msgstr "今天" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:32 msgid "Yesterday" msgstr "昨天" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:34 #, python-format msgid "%3.1f bytes" msgstr "%3.1f 字节" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:35 #, python-format msgid "%3.1f KiB" msgstr "%3.1f KiB" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:36 #, python-format msgid "%3.1f MiB" msgstr "%3.1f MiB" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:37 #, python-format msgid "%3.1f GiB" msgstr "%3.1f GiB" #: openpaperwork-core/src/openpaperwork_core/i18n/python.py:38 #, python-format msgid "%3.1f TiB" msgstr "%3.1f TiB" paperwork-2.1.1/openpaperwork-core/setup.cfg000066400000000000000000000000661417573700700212050ustar00rootroot00000000000000[tool:pytest] addopts = -ra python_files = tests_*.py paperwork-2.1.1/openpaperwork-core/setup.py000077500000000000000000000035731417573700700211070ustar00rootroot00000000000000#!/usr/bin/env python3 import os import sys from setuptools import setup, find_packages quiet = '--quiet' in sys.argv or '-q' in sys.argv try: with open("src/openpaperwork_core/_version.py", "r") as file_descriptor: version = file_descriptor.read().strip() version = version.split(" ")[2][1:-1] if not quiet: print("OpenPaperwork-core version: {}".format(version)) if "-" in version: version = version.split("-")[0] except FileNotFoundError: print("ERROR: _version.py file is missing") print("ERROR: Please run 'make version' first") sys.exit(1) if os.name == "nt": install_requires = [ "certifi", ] else: install_requires = [ "distro", # chkdeps ] setup( name="openpaperwork-core", version=version, description=( "OpenPaperwork's core" ), long_description="""Paperwork is a GUI to make papers searchable. This is the core part of Paperwork. It manages plugins. There is no GUI here. The GUI is . """, url=( "https://gitlab.gnome.org/World/OpenPaperwork/paperwork/tree/master/" "openpaperwork-core" ), download_url=( "https://gitlab.gnome.org/World/OpenPaperwork/paperwork/-" "/archive/{}/paperwork-{}.tar.gz".format(version, version) ), classifiers=[ "Development Status :: 5 - Production/Stable", ("License :: OSI Approved ::" " GNU General Public License v3 or later (GPLv3+)"), "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", ], license="GPLv3+", author="Jerome Flesch", author_email="jflesch@openpaper.work", packages=find_packages('src'), include_package_data=True, package_dir={'': 'src'}, zip_safe=(os.name != 'nt'), install_requires=install_requires, ) paperwork-2.1.1/openpaperwork-core/src/000077500000000000000000000000001417573700700201515ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/000077500000000000000000000000001417573700700240555ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/__init__.py000066400000000000000000000442321417573700700261730ustar00rootroot00000000000000import collections import gettext import importlib import itertools import logging import os import time LOGGER = logging.getLogger(__name__) MINIMUM_CONFIG_PLUGINS = [ # You also have to provide a plugin providing the interface 'app' 'openpaperwork_core.archives', 'openpaperwork_core.cmd.config', 'openpaperwork_core.cmd.plugins', 'openpaperwork_core.config', 'openpaperwork_core.config.automatic_plugin_reset', 'openpaperwork_core.config.backend.configparser', 'openpaperwork_core.data_versioning', 'openpaperwork_core.display.print', 'openpaperwork_core.frozen', 'openpaperwork_core.fs.python', 'openpaperwork_core.logs.archives', 'openpaperwork_core.logs.print', 'openpaperwork_core.mainloop.asyncio', 'openpaperwork_core.paths.xdg', 'openpaperwork_core.uncaught_exception', ] RECOMMENDED_PLUGINS = [ 'openpaperwork_core.external_apps.dbus', 'openpaperwork_core.external_apps.windows', 'openpaperwork_core.external_apps.xdg', 'openpaperwork_core.flatpak', 'openpaperwork_core.fs.memory', 'openpaperwork_core.http', 'openpaperwork_core.i18n.python', 'openpaperwork_core.l10n.python', 'openpaperwork_core.perfcheck.log', 'openpaperwork_core.resources.frozen', 'openpaperwork_core.resources.setuptools', 'openpaperwork_core.thread.pool', 'openpaperwork_core.urls', 'openpaperwork_core.work_queue.default', ] if os.name != 'nt': RECOMMENDED_PLUGINS += [ 'openpaperwork_core.cmd.chkdeps', ] def _(s): return gettext.dgettext('openpaperwork_core', s) class DependencyException(Exception): """ Failed to satisfy dependencies. """ pass class PluginBase(object): """ Indicates all the methods that must be implemented by any plugin managed by OpenPaperwork core. Also provides default implementations for each method. """ # Priority defines in which order callbacks will be called. # Plugins with higher priorities will have their callbacks called first. PRIORITY = 0 # Convenience for the applications: Indicates if users should be able # to enable/disable this plugin in the UI. USER_VISIBLE = False def __init__(self): """ Called as soon as the module is loaded. Should be as minimal as possible. Most of the work should be done in `init()`. You *must* *not* rely on any dependencies here. """ self.core = None def get_interfaces(self): """ Indicates the list of interfaces implemented by this plugin. Interface names are arbitrarily defined. Methods provided by each interface are arbitrarily defined (and no checks are done). Returns a list of string. """ return [] def get_deps(self): """ Return the dependencies required by this plugin. Example: .. code-block:: python [ { "interface": "some_interface_name", # required "defaults": ['plugin_a', 'plugin_b'], # required "expected_already_satisfied": False, # optional, default: True }, ] """ return [] def init(self, core): """ Plugins can initialize whatever they want here. When called, all dependencies have been loaded and initialized, so using them is safe. Does nothing by default. """ self.core = core class Core(object): """ Manage plugins and their callbacks. """ def __init__(self, auto_load_dependencies=False): """ `auto_load_dependencies=True` means that missing dependencies will be loaded automatically based on the default plugin list provided by plugins. This should be only used for testing. """ self.plugins = {} self.initialized = False self._to_initialize = set() self._initialized = set() # avoid double-init self.interfaces = collections.defaultdict(list) self.callbacks = collections.defaultdict(list) self.auto_load_dependencies = auto_load_dependencies self.log_all = bool(os.getenv("CORE_LOG_ALL", 0)) self.count_limit_per_second = int(os.getenv("CORE_CALL_LIMIT", 0)) self.counters_last_reset = 0 self.counters = collections.defaultdict(lambda: 0) def load(self, module_name): """ - Load the specified module - Instantiate the class 'Plugin()' of this module - Register all the methods of this plugin object (except those starting by '_' and those from the class PluginBase) as callbacks BEWARE of dependency loops ! Arguments: - module_name: name of the Python module to load """ if module_name in self.plugins: return self.plugins[module_name] LOGGER.info("Loading plugin '%s' ...", module_name) module = importlib.import_module(module_name) return self._load_module(module_name, module) def _load_module(self, module_name, module): """ should be called from outside for testing only """ if module_name in self.plugins: LOGGER.debug("Module %s already loaded", module_name) return self.plugins[module_name] self.initialized = False plugin = module.Plugin() self.plugins[module_name] = plugin for interface in plugin.get_interfaces(): LOGGER.debug("- '%s' provides '%s'", str(type(plugin)), interface) self.interfaces[interface].append(plugin) self._to_initialize.add(plugin) LOGGER.info("Plugin '%s' loaded", module_name) return plugin def _check_deps(self): to_examine = [ (plugin_name, plugin) for (plugin_name, plugin) in self.plugins.items() ] while len(to_examine) > 0: (plugin_name, plugin) = to_examine[0] to_examine = to_examine[1:] LOGGER.info("Examining dependencies of '%s' ...", plugin_name) deps = plugin.get_deps() for dep in deps: interface = dep['interface'] if len(self.interfaces[interface]) > 0: LOGGER.debug( "- Interface '%s' already provided by %d plugins", interface, len(self.interfaces[interface]) ) continue defaults = dep['defaults'] if len(defaults) <= 0: continue if (not self.auto_load_dependencies and ( 'expected_already_satisfied' not in dep or dep['expected_already_satisfied'] )): LOGGER.warning( "Plugin '{}' requires interface '{}' but no plugins" " provide this interface (suggested: {}). Plugin '{}'" " will not be initialized.".format( plugin_name, interface, defaults, plugin_name ) ) plugin = self.plugins.pop(plugin_name) self._to_initialize.remove(plugin) # return False to indicate we actually dropped a plugin # and need to reevaluate all the dependencies again. return False else: LOGGER.info( "Loading plugins %s to satisfy dependency." " Required by '%s' for interface '%s'", defaults, type(plugin), interface ) for default in defaults: to_examine.append((default, self.load(default))) return True def _register_plugin(self, plugin): for attr_name in dir(plugin): if attr_name[0] == "_": continue if attr_name in dir(PluginBase): # ignore base methods of plugins continue callback = getattr(plugin, attr_name) if not hasattr(callback, '__call__'): continue LOGGER.debug("- %s.%s()", str(type(plugin)), attr_name) self.callbacks[attr_name].append(( plugin.PRIORITY, str(type(plugin)), callback )) self.callbacks[attr_name].sort(reverse=True) def _init(self, plugin, stack=list()): nb = 0 if plugin in self._initialized: return nb if plugin in stack: LOGGER.error("Dependency loop:") for p in itertools.chain(stack, [plugin]): LOGGER.error( "- %s %s depends on %s", p, p.get_interfaces(), [d['interface'] for d in p.get_deps()] ) raise DependencyException("Dependency loop: %s" % str(stack)) stack.append(plugin) self.initialized = True deps = plugin.get_deps() for dep in deps: dep_plugins = self.interfaces[dep['interface']] for dep_plugin in dep_plugins: nb += self._init(dep_plugin, stack) LOGGER.info("Initializing plugin '%s' ...", type(plugin)) stack.remove(plugin) plugin.init(self) self._register_plugin(plugin) nb += 1 self._initialized.add(plugin) return nb def init(self): """ - Make sure all the dependencies of all the plugins are satisfied. - Call the method init() of each plugin following the dependency order (those without dependencies are called first). BEWARE of dependency loops ! """ LOGGER.info("Initializing all plugins") while not self._check_deps(): pass nb = 0 for plugin in self._to_initialize: nb += self._init(plugin) self._to_initialize = set() LOGGER.info("%d plugins initialized", nb) def get_by_name(self, module_name): """ Returns a Plugin instance based on the corresponding module name (assuming it has been loaded). You shouldn't use this function, except for: - unit tests - configuration (see cmd.plugins) """ return self.plugins[module_name] def get_by_interface(self, interface_name): return self.interfaces[interface_name] def get_plugins(self): """ You shouldn't use this function, except for: - unit tests - configuration (see cmd.plugins) """ return dict(self.plugins) def _check_call_limit(self, callback_name): if self.count_limit_per_second <= 0: return now = time.time() if now - self.counters_last_reset >= 1.0: self.counters = collections.defaultdict(lambda: 0) self.counters_last_reset = now self.counters[callback_name] += 1 if self.counters[callback_name] >= self.count_limit_per_second: raise Exception( "Too many calls to '{}' (>= {}) in one second".format( callback_name, self.count_limit_per_second ) ) def call_all(self, callback_name, *args, **kwargs): """ Call all the methods of all the plugins that have `callback_name` as name. Arguments are passed as is. Returned values are dropped (use callbacks for return values if required) Method call order is defined by the plugin priorities: Plugins with a higher priority get their methods called first. When we need a return value from callbacks called with `call_all()`, we need a way to get the results from all of them. The usual way to do that is to instantiate an empty `list` or `set`, and pass it as first argument of the callbacks (argument `out`). Callbacks can then complete this list or set using `list.append()` or `set.add()`. .. uml:: Caller -> Core: call "func" Core -> "Plugin A": plugin.func() Core <- "Plugin A": returns "something_a" Core -> "Plugin B": plugin.func() Core <- "Plugin B": returns "something_b" Core -> "Plugin C": plugin.func() Core <- "Plugin C": returns "something_c" Caller <- Core: returns 3 """ if self.log_all: print( "[{}] call_all({}, args={}, kwargs={})".format( time.time(), callback_name, args, kwargs ) ) assert \ self.initialized, \ "A plugin has been loaded without being initialized." \ " Call core.init() first" self._check_call_limit(callback_name) callbacks = self.callbacks[callback_name] if len(callbacks) <= 0: if callback_name.startswith("on_"): # those are 'observer' callback. If nobody is observing, # it's usually fine. log_method = LOGGER.debug else: log_method = LOGGER.warning log_method("No method '%s' found", callback_name) return 0 for (priority, plugin, callback) in callbacks: if self.log_all: print( "[{}] call_all({}, args={}, kwargs={}) -> {}:{}".format( time.time(), callback_name, args, kwargs, priority, callback ) ) callback(*args, **kwargs) return len(callbacks) def call_one(self, callback_name, *args, **kwargs): """ Look for a plugin method called `callback_name` and calls it. Raises an error if no such method exists. If many exists, call one at random. Returns the value return by the callback. Method call order is defined by the plugin priorities: Plugins with a higher priority get their methods called first. .. uml:: Caller -> Core: call "func" Core -> "Plugin A": plugin.func() Core <- "Plugin A": returns X Caller <- Core: returns X You're advised to use `call_all()` or `call_success()` instead whenever possible. This method is only provided as convenience for when you're fairly sure there should be only one plugin with such callback (example: mainloop plugins). """ assert \ self.initialized, \ "A plugin has been loaded without being initialized." \ " Call core.init() first" self._check_call_limit(callback_name) if self.log_all: print( "[{}] call_one({}, args={}, kwargs={})".format( time.time(), callback_name, args, kwargs ) ) callbacks = self.callbacks[callback_name] if len(callbacks) <= 0: raise IndexError( "No method '{}' found !".format(callback_name) ) if self.log_all: print( "[{}] call_one({}, args={}, kwargs={}) -> {}:{}".format( time.time(), callback_name, args, kwargs, callbacks[0][0], callbacks[0][2] ) ) return callbacks[0][2](*args, **kwargs) def call_success(self, callback_name, *args, **kwargs): """ Call methods of all the plugins that have `callback_name` as name until one of them return a value that is not None. Arguments are passed as is. First value to be different from None is returned. If none of the callbacks returned a value different from None or if no callback has the specified name, this method will return None. Method call order is defined by the plugin priorities: Plugins with a higher priority get their methods called first. Callbacks should never raise any exception. .. uml:: Caller -> Core: call "func" Core -> "Plugin A": plugin.func() Core <- "Plugin A": returns None Core -> "Plugin B": plugin.func() Core <- "Plugin B": returns None Core -> "Plugin C": plugin.func() Core <- "Plugin C": returns "something" Caller <- Core: returns "something" """ assert \ self.initialized, \ "A plugin has been loaded without being initialized." \ " Call core.init() first" self._check_call_limit(callback_name) if self.log_all: print( "[{}] call_one({}, args={}, kwargs={})".format( time.time(), callback_name, args, kwargs ) ) callbacks = self.callbacks[callback_name] if len(callbacks) <= 0: LOGGER.warning("No method '%s' found", callback_name) for (priority, plugin, callback) in callbacks: if self.log_all: msg = "[{}] call_success({}, args={}, kwargs={}) -> {}:{}" print(msg.format( time.time(), callback_name, args, kwargs, priority, callback )) r = callback(*args, **kwargs) if r is not None: return r return None def get_deps(self, plugin_name): plugin = self.plugins[plugin_name] for dep in plugin.get_deps(): if 'interface' not in dep: raise KeyError( "Missing interface in dependency list of plugin" " '{}'".format(plugin_name) ) if 'defaults' not in dep: raise KeyError( "Missing default plugins in dependency list of plugin" " '{}' (interface={})".format( plugin_name, dep['interface'] ) ) yield { 'interface': dep['interface'], 'actives': { x.__module__ for x in self.get_by_interface(dep['interface']) }, 'defaults': set(dep['defaults']), } def get_active_plugins(self): return self.plugins.keys() paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/app.py000066400000000000000000000006711417573700700252130ustar00rootroot00000000000000from . import PluginBase class Plugin(PluginBase): """ Plugin implementing the interface 'app' just provide some very basic information regarding the application we are building. """ def get_interfaces(self): return ['app'] def app_get_name(self): return "OpenPaperwork Core" def app_get_fs_name(self): return "openpaperwork_core" def app_get_version(self): return "0.0" paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/archives.py000066400000000000000000000070331417573700700262360ustar00rootroot00000000000000""" Used to archive logs, screenshots, etc. Do not depends on 'fs' plugins (this plugin is used for logging and therefore is critical --> has minimum dependencies). """ import datetime import logging from . import PluginBase LOGGER = logging.getLogger(__name__) ARCHIVE_FILE_DATE_FORMAT = "%Y%m%d_%H%M_%S" MAX_DAYS = 31 class ArchiveHandler(object): def __init__(self, core, storage_name, storage_dir, file_extension): self.core = core self.storage_name = storage_name self.storage_dir = storage_dir self.file_extension = file_extension def get_new(self, name=None): if name is None: name = self.storage_name out_file_name = datetime.datetime.now().strftime( ARCHIVE_FILE_DATE_FORMAT ) out_file_name += "_{}.{}".format(name, self.file_extension) out_file_path = self.core.call_success( "fs_join", self.storage_dir, out_file_name ) return out_file_path def get_archived(self): for f in self.core.call_success("fs_listdir", self.storage_dir): f = self.core.call_success("fs_basename", f) if not f.lower().endswith(".{}".format(self.file_extension)): continue short_f = "_".join(f.split("_", 3)[:3]) try: date = datetime.datetime.strptime( short_f, ARCHIVE_FILE_DATE_FORMAT ) except ValueError as exc: LOGGER.warning( "Unexpected filename: %s. Ignoring it", f, exc_info=exc ) continue yield ( date, self.core.call_success("fs_join", self.storage_dir, f) ) def delete_obsoletes(self): now = datetime.datetime.now() for (date, file_path) in self.get_archived(): if (now - date).days <= MAX_DAYS: continue LOGGER.info("Deleting obsolete log file: %s", file_path) self.core.call_success("fs_unlink", file_path, trash=False) class Plugin(PluginBase): def __init__(self): super().__init__() self.storage_dirs = [] def get_interfaces(self): return ['file_archives'] def get_deps(self): return [ { 'interface': 'data_versioning', 'defaults': ['openpaperwork_core.data_versioning'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_core.fs.python'], }, { 'interface': 'paths', 'defaults': ['openpaperwork_core.paths.xdg'], }, ] def init(self, core): super().init(core) data_dir = self.core.call_success("paths_get_data_dir") self.base_archive_dir = self.core.call_success( "fs_join", data_dir, "openpaperwork" ) LOGGER.info("Archiving to %s", self.base_archive_dir) def file_archive_get(self, storage_name, file_extension): self.core.call_success("fs_mkdir_p", self.base_archive_dir) storage_dir = self.core.call_success( "fs_join", self.base_archive_dir, storage_name ) self.storage_dirs.append(storage_dir) LOGGER.info("Archiving '%s' to %s", storage_name, storage_dir) self.core.call_success("fs_mkdir_p", storage_dir) archiver = ArchiveHandler( self.core, storage_name, storage_dir, file_extension ) archiver.delete_obsoletes() return archiver paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/beacon/000077500000000000000000000000001417573700700253045ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/beacon/__init__.py000066400000000000000000000040261417573700700274170ustar00rootroot00000000000000""" openpaperwork_core.beacon contains everything related to the communication with the website https://openpaper.work/ """ import datetime import logging LOGGER = logging.getLogger(__name__) class PeriodicTask(object): def __init__( self, config_section_name, min_delay: datetime.timedelta, periodic_callback, else_callback=lambda: None ): self.config_section_name = config_section_name self.min_delay = min_delay self.periodic_callback = periodic_callback self.else_callback = else_callback def register_config(self, core): setting = core.call_success( "config_build_simple", self.config_section_name, "last_run", lambda: datetime.date(year=1970, month=1, day=1) ) core.call_all( "config_register", self.config_section_name + "_last_run", setting ) def do(self, core): now = datetime.date.today() last_run = core.call_success( "config_get", self.config_section_name + "_last_run" ) if hasattr(last_run, 'value'): # ConfigDate object from # openpaperwork_core.config.backend.configparser last_run = last_run.value LOGGER.info( "[%s] Last run: %s ; Now: %s", self.config_section_name, last_run, now ) if now - last_run < self.min_delay: LOGGER.info( "[%s] Nothing to do (%s < %s)", self.config_section_name, now - last_run, self.min_delay ) self.else_callback() return LOGGER.info( "[%s] Running %s (%s >= %s)", self.config_section_name, self.periodic_callback, now - last_run, self.min_delay ) self.periodic_callback() LOGGER.info("[%s] Updating last run date", self.config_section_name) core.call_all( "config_put", self.config_section_name + "_last_run", now ) core.call_all("config_save") paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/beacon/stats.py000066400000000000000000000117171417573700700270230ustar00rootroot00000000000000import datetime import json import logging import uuid import openpaperwork_core import openpaperwork_core.promise from . import PeriodicTask from .. import _ LOGGER = logging.getLogger(__name__) POST_STATS_INTERVAL = datetime.timedelta(days=7) POST_STATS_PATH = "/beacon/post_statistics" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.periodic = None self.http = None def get_interfaces(self): return [ "bug_report_attachments", "stats_post", ] def get_deps(self): return [ { 'interface': 'app', 'defaults': ['paperwork_backend.app'], }, { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'fs', 'defaults': [ 'openpaperwork_core.fs.memory', 'openpaperwork_core.fs.python', ], }, { 'interface': 'http_json', 'defaults': ['openpaperwork_core.http'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_core.mainloop.asyncio'], }, { 'interface': 'thread', 'defaults': ['openpaperwork_core.thread.simple'], }, ] def init(self, core): super().init(core) self.periodic = PeriodicTask( "statistics", datetime.timedelta(days=7), self.stats_send ) self.http = self.core.call_success( "http_json_get_client", "statistics" ) self._register_config(core) self.periodic.register_config(core) if self.core.call_success("config_get", "send_statistics"): self.periodic.do(core) def _register_config(self, core): setting = self.core.call_success( "config_build_simple", "statistics", "enabled", lambda: False ) self.core.call_all( "config_register", "send_statistics", setting ) setting = self.core.call_success( "config_build_simple", "statistics", "uuid", lambda: uuid.getnode() ) self.core.call_all("config_register", "uuid", setting) def _collect_stats(self, node_uuid): stats = { 'uuid': node_uuid, 'paperwork_version': self.core.call_success("app_get_version"), 'nb_documents': 0, 'os_name': '', 'platform_architecture': '', 'platform_processor': '', 'platform_distribution': '', 'cpu_count': 0, } self.core.call_all("stats_get", stats) return {'statistics': stats} def stats_send(self): node_uuid = self.core.call_success("config_get", "uuid") promise = openpaperwork_core.promise.ThreadedPromise( self.core, self._collect_stats, args=(node_uuid,) ) promise = promise.then(self.http.get_request_promise(POST_STATS_PATH)) def on_request_done(reply): LOGGER.info("Statistics posted. Reply: {}".format(reply)) self.core.call_all('on_stats_sent') promise = promise.then(on_request_done) promise.schedule() def bug_report_get_attachments(self, out: dict): out['stats'] = { 'include_by_default': True, 'date': None, 'file_type': _("App. & system info."), 'file_url': _("Select to generate"), 'file_size': 0, } def _write_stats_to_tmp_file(self, stats): stats = json.dumps( stats, indent=4, separators=(",", ": "), sort_keys=True ) (file_url, fd) = self.core.call_success( "fs_mktemp", prefix="statistics_", suffix=".json", mode="w", on_disk=True ) with fd: fd.write(stats) return file_url def on_bug_report_attachment_selected(self, attachment_id, *args): if attachment_id != 'stats': return self.core.call_all( "bug_report_update_attachment", attachment_id, {"file_url": _("Collecting statistics ...")}, *args ) node_uuid = self.core.call_success("config_get", "uuid") promise = openpaperwork_core.promise.ThreadedPromise( self.core, self._collect_stats, args=(node_uuid,) ) promise = promise.then(self._write_stats_to_tmp_file) promise = promise.then( lambda file_url: self.core.call_all( "bug_report_update_attachment", attachment_id, { 'file_url': file_url, 'file_size': self.core.call_success( 'fs_getsize', file_url ), }, *args ) ) promise.schedule() paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/beacon/sysinfo.py000066400000000000000000000027701417573700700273560ustar00rootroot00000000000000import os import multiprocessing import platform import psutil import sys try: import distro except (ImportError, ValueError): assert(os.name == "nt") import openpaperwork_core class Plugin(openpaperwork_core.PluginBase): PRIORITY = 1000 def get_interfaces(self): return ['stats'] def stats_get(self, out: dict): if os.name == 'nt': distribution = str(platform.win32_ver()) else: distribution = str(distro.linux_distribution( full_distribution_name=False )) processor = "" os_name = os.name if os_name != 'nt': # processor contains too much infos on Windows processor = str(platform.processor()) cpu_freq = None if hasattr(psutil, 'cpu_freq'): cpu_freq = psutil.cpu_freq() if cpu_freq is not None: cpu_freq = int(cpu_freq.max) if cpu_freq is not None: out['cpu_freq'] = cpu_freq out['cpu_count'] = multiprocessing.cpu_count() out['os_name'] = os_name out['platform_architecture'] = str(platform.architecture()) out['platform_distribution'] = distribution out['platform_machine'] = platform.machine() out['platform_mem'] = int(psutil.virtual_memory().total) out['platform_processor'] = processor out['software_python'] = sys.version out['software_release'] = platform.release() out['software_system'] = platform.system() return out paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/bug_report/000077500000000000000000000000001417573700700262255ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/bug_report/__init__.py000066400000000000000000000000001417573700700303240ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/bug_report/censor.py000066400000000000000000000035121417573700700300710ustar00rootroot00000000000000import logging from .. import PluginBase LOGGER = logging.getLogger(__name__) class Plugin(PluginBase): """ Censor bug report attachments """ PRIORITY = -10000 def get_interfaces(self): return ['bug_report_attachments'] def get_deps(self): return [ { 'interface': 'censor', 'defaults': ['openpaperwork_core.censor'], }, ] def _censor_attachment(self, attachment_id, args): url = self.core.call_success( "bug_report_get_attachment_file_url", attachment_id, *args ) if url is None: LOGGER.info( "Attachment %s has no URL yet. Can't censor", attachment_id ) return if not url.endswith(".conf") and not url.endswith(".txt"): LOGGER.info( "Unknown file type: %s:%s. Can't censor", attachment_id, url ) return basename = self.core.call_success("fs_basename", url) if basename.startswith("censored_"): LOGGER.info("Attachmnent %s appears to be already censored", url) return LOGGER.info("Censoring %s:%s", attachment_id, url) censored = self.core.call_success( "censor_txt_file", url, tmp_on_disk=True ) self.core.call_all( "bug_report_update_attachment", attachment_id, { "censored": True, "file_url": censored, }, *args ) def on_bug_report_attachment_selected(self, attachment_id, *args): self._censor_attachment(attachment_id, args) def bug_report_update_attachment(self, attachment_id, infos: dict, *args): if 'censored' in infos: return self._censor_attachment(attachment_id, args) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/censor.py000066400000000000000000000047541417573700700257320ustar00rootroot00000000000000import getpass import logging import os import socket import urllib.parse from . import PluginBase LOGGER = logging.getLogger(__name__) class Plugin(PluginBase): def __init__(self): self.replacements = ( (os.path.expanduser("~"), "###HOME_DIR###"), (urllib.parse.quote(os.path.expanduser("~")), "###HOME_DIR###"), (getpass.getuser(), "###USER_NAME###"), (urllib.parse.quote(getpass.getuser()), "###USER_NAME###"), (socket.gethostname(), "###HOST_NAME###"), (urllib.parse.quote(socket.gethostname()), "###HOST_NAME###"), ) def get_interfaces(self): return ['censor'] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_core.fs.python'], }, ] def censor_string(self, string): """ Rewrite a string by censoring anything close to personal information (username, home directory path, host name, ...). """ for r in self.replacements: string = string.replace(*r) return string def censor_txt_file( self, input_url, output_url=None, tmp_on_disk=False): """ Rewrite a file but censor anything close to personal information (username, home directory path, host name, ...). Arguments: - file_input_url: file to censor - file_output_url: output file. If None, a temporary file will be created - tmp_on_disk: only used if file_output_url is None. If true, the temporary file will be written on disk. If false, we may return a memory:// URI (won't work outside of Paperwork). Returns: censored file URL """ if output_url is not None: fd_out = self.core.call_success("fs_open", output_url, 'w') else: basename = self.core.call_success("fs_basename", input_url) (output_url, fd_out) = self.core.call_success( "fs_mktemp", prefix="censored_", suffix="_" + basename, mode="w", on_disk=tmp_on_disk ) with fd_out: with self.core.call_success("fs_open", input_url, 'r') as fd_in: while True: line = fd_in.readline() if line == '': break line = self.censor_string(line) fd_out.write(line) return output_url paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/cmd/000077500000000000000000000000001417573700700246205ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/cmd/__init__.py000066400000000000000000000000001417573700700267170ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/cmd/chkdeps.py000066400000000000000000000075331417573700700266230ustar00rootroot00000000000000import collections import distro import logging import os import sys from . import util from .. import (_, PluginBase) LOGGER = logging.getLogger(__name__) PACKAGE_TOOLS = { 'debian': 'apt-get install -y', 'fedora': 'dnf install', 'gentoo': 'emerge', 'linuxmint': 'apt-get install -y', 'raspbian': 'apt-get install -y', 'suse': 'zypper in', 'ubuntu': 'apt-get install -y', } class Plugin(PluginBase): def get_interfaces(self): return ['shell'] def get_deps(self): # will call method from interface 'chkdeps', but we can still # work if no other plugin implement 'chkdeps'. return [] def _get_distribution(self): distribution = distro.linux_distribution(full_distribution_name=False) if self.interactive: print("Detected system: {}".format(" ".join(distribution))) distribution = distribution[0].lower() if distribution not in PACKAGE_TOOLS: LOGGER.warning( "WARNING: Unknown distribution." " Can't suggest packages to install" ) return distribution def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): p = parser.add_parser( 'chkdeps', help=_("Check that all required dependencies are installed") ) p.add_argument( "--yes", "-y", required=False, default=False, action='store_true' ) def cmd_run(self, args): if args.command != 'chkdeps': return None auto = args.yes if auto: LOGGER.warning("Confirmation disabled") distribution = self._get_distribution() missing = collections.defaultdict(dict) self.core.call_all("chkdeps", missing) if self.interactive and len(missing) > 0: print(_("Missing dependencies:")) for (dep_name, distrib_packages) in missing.items(): print(_("- {dep_name} (package: {pkg_name})").format( dep_name=dep_name, pkg_name=( distrib_packages[distribution] if distribution in distrib_packages else _("UNKNOWN") ) )) if distribution in PACKAGE_TOOLS: command = PACKAGE_TOOLS[distribution] if os.getuid() != 0: command = "sudo {}".format(command) else: command = None has_pkg = False for (dep_name, distrib_packages) in missing.items(): if distribution in distrib_packages: if command is not None: command += " " + distrib_packages[distribution] has_pkg = True if self.interactive: if has_pkg and command is not None: print("") print(_("Suggested command:")) print(" " + command) print("") if not auto: r = util.ask_confirmation( _("Do you want to run this command now ?") ) if r != 'y': return { "missing": missing, "command": command, } print("Running command ...") r = os.system(command) print("Command returned {}".format(r)) if r != 0: sys.exit(r) elif len(missing) > 0: print( _("Don't know how to install missing dependencies. Sorry.") ) else: print(_("Nothing to do.")) return { "missing": missing, "command": command, } paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/cmd/config.py000066400000000000000000000105401417573700700264370ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import logging from .. import (_, PluginBase) LOGGER = logging.getLogger(__name__) def bool_from_str(s): s = s.strip() if s == "" or s == "0" or s.lower() == "false": return False return True # Only basic types are handled by shell commands CMD_VALUE_TYPES = { 'str': str, 'int': int, 'float': float, 'bool': bool_from_str, } class Plugin(PluginBase): def __init__(self): super().__init__() self.interactive = True def get_interfaces(self): return ['shell'] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, ] def cmd_complete_argparse(self, parser): config_parser = parser.add_parser( 'config', help=_("Manage Paperwork configuration") ) subparser = config_parser.add_subparsers( help=_("sub-command"), dest='subcommand', required=True ) get_parser = subparser.add_parser( 'get', help=_("Get a value from Paperwork's configuration") ) get_parser.add_argument('opt_name') put_parser = subparser.add_parser( 'put', help=_("Set a value in Paperwork's configuration") ) put_parser.add_argument('opt_name') put_parser.add_argument('type', help=_("See 'config list_type'")) put_parser.add_argument('value') subparser.add_parser( 'show', help=_("Show Paperwork's configuration") ) subparser.add_parser( 'list_types', help=_( "Show value types you can use from command line" ) ) def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_run(self, args): if args.command != 'config': return None if args.subcommand == "get": return self._cmd_get(args.opt_name) elif args.subcommand == "put": return self._cmd_put(args.opt_name, args.type, args.value) elif args.subcommand == "show": return self._cmd_show() elif args.subcommand == "list_types": return self._cmd_list_types() else: return None def _cmd_get(self, opt_name): v = self.core.call_success("config_get", opt_name) if v is None: LOGGER.warning("No such option '%s'", opt_name) return None if self.interactive: self.core.call_all("print", "{} = {}\n".format(opt_name, v)) self.core.call_all("print_flush") return {opt_name: v} def _cmd_put(self, opt_name, vtype, value): value = CMD_VALUE_TYPES[vtype](value) if self.interactive: self.core.call_all("print", "{} = {}\n".format(opt_name, value)) self.core.call_all("print_flush") self.core.call_all("config_put", opt_name, value) self.core.call_all("config_save") return {opt_name: value} def _cmd_show(self): opts = self.core.call_success("config_list_options") out = {} opts.sort() for opt in opts: out[opt] = self.core.call_success("config_get", opt) if self.interactive: self.core.call_all("print", "{} = {}\n".format(opt, out[opt])) if self.interactive: self.core.call_all("print_flush") return out def _cmd_list_types(self): r = list(CMD_VALUE_TYPES.keys()) if self.interactive: self.core.call_all("print", str(r) + "\n") self.core.call_all("print_flush") return r paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/cmd/plugins.py000066400000000000000000000276351417573700700266700ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import importlib import logging from .. import (_, PluginBase) LOGGER = logging.getLogger(__name__) class Plugin(PluginBase): def __init__(self): super().__init__() self.interactive = True def get_interfaces(self): return ['shell'] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, ] def cmd_complete_argparse(self, parser): application = self.core.call_success("config_get_plugin_list_name") config_parser = parser.add_parser( 'plugins', help=(_("Manage %s plugins") % application) ) subparser = config_parser.add_subparsers( help=_("sub-command"), dest='subcommand', required=True ) subparser.add_parser( 'list', help=( _("Show plugins enabled for %s") % application ) ) p = subparser.add_parser( 'add', help=( _("Add plugin in %s") % application ) ) p.add_argument('plugin_name') p.add_argument( '--no_auto', '-n', action="store_true", help=_("Do not correct dependencies automatically") ) p = subparser.add_parser( 'remove', help=( _("Remove plugin from %s") % application ) ) p.add_argument('plugin_name') p.add_argument( '--no_auto', '-n', action="store_true", help=_("Do not correct dependencies automatically") ) subparser.add_parser( 'reset', help=(_( "Clean up your mess by reseting the plugin list to its" " default value" )) ) p = subparser.add_parser( 'show', help=( _("Show information regarding a plugin (must be enabled)") ) ) p.add_argument('plugin_name') def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_run(self, args): if args.command != 'plugins': return None elif args.subcommand == "list": return self._cmd_list_plugins() elif args.subcommand == "add": return self._cmd_add_plugin(args.plugin_name, not args.no_auto) elif args.subcommand == "remove": return self._cmd_remove_plugin(args.plugin_name, not args.no_auto) elif args.subcommand == "reset": return self._cmd_reset_plugins() elif args.subcommand == "show": return self._cmd_show_plugin(args.plugin_name) else: return None def _cmd_add_plugin(self, plugin_name, auto=True, added=set(), save=True): added.add(plugin_name) if auto: # Load the plugin, and check that all its dependencies are # satisfied. # Do not use the core to load it, otherwise we won't be able # to use 'call_success()' or 'call_all(). try: module = importlib.import_module(plugin_name) except ModuleNotFoundError: if self.interactive: print(_("Error: Plugin '%s' not found") % plugin_name) else: LOGGER.error("Plugin '%s' not found", plugin_name) return False plugin = module.Plugin() deps = plugin.get_deps() for dep in deps: try: actives = self.core.get_by_interface(dep['interface']) if len(actives) > 0: continue except KeyError: pass for default in dep['defaults']: if default in added: continue print( _( "Adding plugin '{plugin_name}' to satisfy" " dependency of '{other_plugin_name}' on interface" " '{interface}'" ).format( plugin_name=default, other_plugin_name=plugin_name, interface=dep['interface'] ) ) r = self._cmd_add_plugin( default, auto=True, added=added, save=False ) if not r: return r self.core.call_all("config_add_plugin", plugin_name) if save: self.core.call_all("config_save") if self.interactive: print(_("Plugin {} added").format(plugin_name)) return True def _cmd_remove_plugin( self, plugin_name, auto=True, removed=set(), save=True): removed.add(plugin_name) if auto: # look for plugins depending on this one # if they have no other plugin satisfying their dependency, # remove them too. for other_plugin in self.core.get_active_plugins(): if other_plugin in removed: continue deps = self.core.get_deps(other_plugin) for dep in deps: actives = dep['actives'] actives = actives.difference(removed) if len(actives) <= 0: if self.interactive: print( _( "Removing plugin '{plugin_name}' due to" " missing dependency '{interface}'" ).format( plugin_name=other_plugin, interface=dep['interface'] ) ) self._cmd_remove_plugin( other_plugin, auto=auto, removed=removed, save=False ) break self.core.call_all("config_remove_plugin", plugin_name) if save: self.core.call_all("config_save") if self.interactive: print(_("Plugin {} removed").format(plugin_name)) return True def _cmd_list_plugins(self): plugins = self.core.call_success("config_list_plugins") if self.interactive: self.core.call_all("print", " " + _("Active plugins:") + "\n") for plugin in plugins: self.core.call_all("print", plugin + "\n") self.core.call_all("print_flush") return list(plugins) def _cmd_reset_plugins(self): self.core.call_success("config_reset_plugins") self.core.call_all("config_save") if self.interactive: print("Plugin list reseted") return True def _print_columns(self, columns): out = "" for (column_size, string) in columns: out += "| " out += ("{:" + str(column_size) + "}").format(string) out = out[1:] self.core.call_all("print", out + "\n") def _get_printable_deps( self, plugin_name, parents_requirements=set(), depth=0, already_printed=set()): header = "| " * (depth - 1) try: plugin = self.core.get_by_name(plugin_name) except KeyError: # Plugin not loaded --> can't get the info yield ( header + "|-- " + plugin_name, "(not loaded)", ) return str_plugin_name = plugin_name if plugin_name in already_printed: str_plugin_name = plugin_name + " (dup)" interfaces = plugin.get_interfaces() interfaces = [i for i in interfaces if i in parents_requirements] if len(interfaces) <= 0: interfaces = [""] for (idx, interface) in enumerate(interfaces): if idx == 0: yield ( header + "|-- " + str_plugin_name if depth > 0 else str_plugin_name, interface, ) else: yield ( header + "| |", interface, ) if plugin_name in already_printed: return [] already_printed.add(plugin_name) deps = plugin.get_deps() requirements = {d['interface'] for d in deps} requirements.update(parents_requirements) plugin_names = set() for (idx, dep) in enumerate(deps): plugins = self.core.get_by_interface(dep['interface']) plugin_names.update({plugin.__module__ for plugin in plugins}) plugin_names.update(dep['defaults']) plugin_names = list(plugin_names) plugin_names.sort() for (idx, plugin_name) in enumerate(plugin_names): for line in self._get_printable_deps( plugin_name, requirements, depth + 1, already_printed): yield line def _cmd_show_plugin(self, plugin_name): try: plugin = self.core.get_by_name(plugin_name) except KeyError: if self.interactive: print(_("Plugin '%s' not enabled.") % plugin_name) return {} if self.interactive: self.core.call_all( "print", (_("Plugin '%s':") % plugin_name) + "\n" ) self.core.call_all("print", "* " + _("Implements:") + "\n") for interf in plugin.get_interfaces(): self.core.call_all("print", " + " + interf + "\n") self.core.call_all("print", "* " + _("Depends on:") + "\n") for dep in plugin.get_deps(): self.core.call_all("print", " + " + dep['interface'] + "\n") for default in dep['defaults']: self.core.call_all( "print", " - " + (_("suggested: %s") % default) + "\n" ) self.core.call_all("print", "\n") deps = list(self._get_printable_deps(plugin_name)) column_headers = ( _("Plugin name"), _("Interface"), ) column_sizes = [len(c) + 1 for c in column_headers] for d in deps: for (idx, column_value) in enumerate(d): column_sizes[idx] = max( column_sizes[idx], len(column_value) + 1 ) total = sum(column_sizes) + (2 * len(column_sizes)) self._print_columns(( (column_sizes[0], _("Plugin name")), (column_sizes[1], _("Interface")), )) self.core.call_all("print", ("-" * total) + "\n") for d in deps: self._print_columns([ (column_sizes[idx], column_value) for (idx, column_value) in enumerate(d) ]) self.core.call_all("print_flush") return { 'interface': plugin.get_interfaces(), 'deps': plugin.get_deps() } paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/cmd/util.py000066400000000000000000000007231417573700700261510ustar00rootroot00000000000000import sys from .. import _ def ask_confirmation(question, default='y'): sys.stdout.write(question) if default == 'y': yesno = _("Y/n") default = yesno[0].lower() else: yesno = _("y/N") default = yesno[-1].lower() sys.stdout.write(" [{}] ".format(yesno)) reply = input() if reply == "": reply = default else: reply = reply.lower() return 'y' if reply == yesno[0].lower() else 'n' paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/config/000077500000000000000000000000001417573700700253225ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/config/__init__.py000066400000000000000000000156061417573700700274430ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2014 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . """ Paperwork configuration management code """ import collections import logging from .. import PluginBase LOGGER = logging.getLogger(__name__) class Setting(object): def __init__(self, core, section, token, default_value_func): self.core = core self.section = section self.token = token self.default_value_func = default_value_func def get(self): value = self.core.call_success( "config_backend_get", self.section, self.token, None ) if value is None: return self.default_value_func() else: return value def put(self, value): self.core.call_all( "config_backend_put", self.section, self.token, value ) class Plugin(PluginBase): """ Translate values from the configuration into more usable ones. Provides default values (except for plugins). """ def __init__(self): self.core = None self.settings = {} self.values = {} # applicatiom here is a bit more specific: paperwork-gtk, # paperwork-shell, etc. # It is used to known which plugin list must be loaded self.plugin_list_name = None self.observers = collections.defaultdict(set) def get_interfaces(self): return ['config'] def get_deps(self): return [ { 'interface': 'app', 'defaults': ['openpaperwork_core.app'], }, { 'interface': 'config_backend', 'defaults': ['openpaperwork_core.config.backend.configparser'], }, ] def init(self, core): self.core = core self.settings = {} def config_load(self): application = self.core.call_success("app_get_fs_name") LOGGER.info("Loading configuration for %s", application) self.values = {} self.core.call_all('config_backend_load', application) for observers in self.observers.values(): for observer in observers: observer() def config_load_plugins(self, plugin_list_name, default_plugins=[]): LOGGER.info("Loading plugins for '%s'", plugin_list_name) self.plugin_list_name = plugin_list_name self.core.call_all( 'config_backend_load_plugins', plugin_list_name, default_plugins ) def config_get_plugin_list_name(self): return self.plugin_list_name def config_save(self): LOGGER.info("Saving configuration") self.core.call_all('config_backend_save') def config_build_simple( self, section, token, default_value_func ): """ Provide a default simple implementation for a new setting that can be registered using 'config_register'. Arguments: - section: Section in which option must be stored (see ConfigParser) - token: token name for the option (see ConfigParser) - default_value_func: function to call to get the default value if none is stored in the file. """ return Setting(self.core, section, token, default_value_func) def config_register(self, key: str, setting): """ Add another setting to manage. Make this setting available to other components. Arguments: - key: configuration key - setting: Setting must be an object providing a method `get()` and a method `put(value)`. See :func:`config_build_simple` to get quickly a default implementation. """ LOGGER.debug("Registering configuration: %s", key) self.settings[key] = setting def config_list_options(self): return list(self.settings.keys()) def config_get_setting(self, key: str): return self.settings[key] def config_get(self, key: str): LOGGER.debug("Config get: %s", key) if key not in self.settings: return None if key not in self.values: self.values[key] = self.settings[key].get() return self.values[key] def config_get_default(self, key: str): return self.settings[key].default_value_func() def config_put(self, key: str, value): """ Store a setting value. Warning: You must call :func:`config_save` so the changes are actually saved. Arguments: - key: configuration key, - value: can be of many types (`str`, `int`, etc). """ LOGGER.debug("Config put: %s", key) self.settings[key].put(value) self.values[key] = value if key in self.observers: for callback in self.observers[key]: callback() def config_add_plugin(self, plugin, plugin_list_name=None): if plugin_list_name is None: plugin_list_name = self.plugin_list_name LOGGER.debug("Config add plugin: %s -> %s", plugin, plugin_list_name) self.core.call_all( 'config_backend_add_plugin', plugin_list_name, plugin ) def config_remove_plugin(self, plugin, plugin_list_name=None): if plugin_list_name is None: plugin_list_name = self.plugin_list_name LOGGER.debug( "Config remove plugin: %s -> %s", plugin, plugin_list_name ) self.core.call_all( 'config_backend_remove_plugin', plugin_list_name, plugin ) def config_list_plugins(self, plugin_list_name=None): if plugin_list_name is None: plugin_list_name = self.plugin_list_name return self.core.call_success( "config_backend_list_active_plugins", plugin_list_name ) def config_reset_plugins(self, plugin_list_name=None): if plugin_list_name is None: plugin_list_name = self.plugin_list_name LOGGER.debug("Config reset plugin: %s", plugin_list_name) return self.core.call_success( "config_backend_reset_plugins", plugin_list_name ) def config_add_observer(self, key: str, callback): self.observers[key].add(callback) def config_remove_observer(self, key: str, callback): self.observers[key].remove(callback) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/config/automatic_plugin_reset.py000066400000000000000000000027211417573700700324440ustar00rootroot00000000000000import logging from .. import PluginBase LOGGER = logging.getLogger(__name__) class Plugin(PluginBase): PRIORITY = 1000 def config_load_plugins(self, plugin_list_name, default_plugins=[]): old_default = self.core.call_success( "config_backend_get", "default_plugins", plugin_list_name, None ) if old_default is None: # no previously known list of default plugins. Weird but not much # we can do about it. self.core.call_all( "config_backend_put", "default_plugins", plugin_list_name, default_plugins ) self.core.call_all("config_backend_save") return old_default = sorted(old_default) default_plugins = sorted(default_plugins) if old_default == default_plugins: # default list hasn't changed. So we assume the custom list # is still fine. return LOGGER.warning( "Default plugin list has changed." " Reseting plugin list to its new default." ) LOGGER.warning("Old default list: %s", old_default) LOGGER.warning("New default list: %s", default_plugins) self.core.call_all("config_backend_reset_plugins", plugin_list_name) self.core.call_all( "config_backend_put", "default_plugins", plugin_list_name, default_plugins ) self.core.call_all("config_backend_save") paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/config/backend/000077500000000000000000000000001417573700700267115ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/config/backend/__init__.py000066400000000000000000000000001417573700700310100ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/config/backend/configparser.py000066400000000000000000000240251417573700700317500ustar00rootroot00000000000000""" Manages a configuration file using configparser. """ import collections import configparser import datetime import logging from ... import (_, PluginBase) LOGGER = logging.getLogger(__name__) class ConfigBool(object): def __init__(self, value=False): if isinstance(value, str): self.value = (value.lower() == 'true') else: self.value = value def __eq__(self, o): return (self.value == o) def __bool__(self): return self.value def __str__(self): return str(self.value) class ConfigDate(object): DATE_FORMAT = "%Y-%m-%d" def __init__(self, value=datetime.datetime(year=1971, month=1, day=1)): if isinstance(value, str): self.value = ( datetime.datetime .strptime(value, self.DATE_FORMAT) .date() ) else: self.value = value def __eq__(self, o): return (self.value == o) def __str__(self): return self.value.strftime(self.DATE_FORMAT) class ConfigList(object): SEPARATOR = ";" def __init__(self, value=None, elements=None): if elements is None: elements = [] self.elements = elements if value is not None: if isinstance(value, str): if value != '': elements = value.split(self.SEPARATOR) for i in elements: (t, v) = i.split(_TYPE_SEPARATOR, 1) self.elements.append(_STR_TO_TYPE[t](v)) elif hasattr(value, 'elements'): self.elements = value.elements[:] else: self.elements = list(value) def __iter__(self): return iter(self.elements) def __contains__(self, o): return o in self.elements def __getitem__(self, *args, **kwargs): return self.elements.__getitem__(*args, **kwargs) def __setitem__(self, *args, **kwargs): return self.elements.__setitem__(*args, **kwargs) def __len__(self): return len(self.elements) def append(self, value): self.elements.append(value) def remove(self, value): self.elements.remove(value) def __str__(self): out = [] for e in self.elements: out.append("{}{}{}".format( _TYPE_TO_STR[type(e)], _TYPE_SEPARATOR, str(e) )) return self.SEPARATOR.join(out) class ConfigDict(object): SEPARATOR_ITEMS = ";" SEPARATOR_KEYVALS = "=" def __init__(self, value=None, elements={}): self.elements = elements if value is not None: if isinstance(value, str): elements = value.split(self.SEPARATOR_ITEMS) for i in elements: (k, v) = i.split(self.SEPARATOR_KEYVALS, 1) (t, v) = v.split(_TYPE_SEPARATOR, 1) self.elements[k] = _STR_TO_TYPE[t](v) elif hasattr(value, 'elements'): self.elements = value.elements[:] else: self.elements = dict(value) def __iter__(self): return iter(self.elements) def __contains__(self, o): return o in self.elements def __getitem__(self, *args, **kwargs): return self.elements.__getitem__(*args, **kwargs) def __setitem__(self, *args, **kwargs): return self.elements.__setitem__(*args, **kwargs) def __len__(self): return len(self.elements) def __str__(self): out = [] for (k, v) in self.elements.items(): out.append("{}{}{}{}{}".format( k, self.SEPARATOR_KEYVALS, _TYPE_TO_STR[type(v)], _TYPE_SEPARATOR, str(v) )) return self.SEPARATOR_ITEMS.join(out) _TYPE_TO_STR = { bool: "bool", ConfigBool: "bool", ConfigDate: "date", ConfigDict: "dict", ConfigList: "list", datetime.date: "date", dict: "dict", float: "float", int: "int", list: "list", str: "str", tuple: "list", } _STR_TO_TYPE = { "bool": ConfigBool, "date": ConfigDate, "dict": ConfigDict, "list": ConfigList, "float": float, "int": int, "str": str, } _TYPE_SEPARATOR = ":" class Plugin(PluginBase): def __init__(self): self.config = configparser.RawConfigParser() self.base_path = None self.application_name = None self.config_path = None self.observers = collections.defaultdict(set) self.core = None self.default_plugins = [] def get_interfaces(self): return ['config_backend'] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_core.fs.python'], }, { 'interface': 'paths', 'defaults': ['openpaperwork_core.paths.xdg'], }, ] def init(self, core): self.core = core if self.base_path is None: self.base_path = self.core.call_success("paths_get_config_dir") def _get_filepath(self): return self.core.call_success( "fs_join", self.base_path, self.application_name + ".conf" ) def config_backend_load(self, application_name): self.application_name = application_name self.config_path = self._get_filepath() self.config = configparser.RawConfigParser() LOGGER.info("Loading configuration '%s' ...", self.config_path) if self.core.call_success("fs_exists", self.config_path) is not None: fd = self.core.call_success("fs_open", self.config_path, 'r') with fd: self.config.read_file(fd) else: LOGGER.warning( "Cannot load configuration '%s'. File does not exist", self.config_path ) for observers in self.observers.values(): for observer in observers: observer() def config_backend_save(self, application_name=None): if application_name is not None: self.application_name = application_name config_path = self._get_filepath() LOGGER.info("Writing configuration '%s' ...", config_path) with self.core.call_success("fs_open", config_path, 'w') as fd: self.config.write(fd) def config_backend_load_plugins(self, opt_name, default=[]): """ Load and init the plugin list from the configuration. """ self.default_plugins = default modules = self.config_backend_get( "plugins", opt_name, ConfigList(None, self.default_plugins) ) LOGGER.info( "Loading and initializing plugins from configuration: %s", str(modules) ) for module in modules: self.core.load(module) self.core.init() def config_backend_list_active_plugins(self, opt_name): return self.config_backend_get( "plugins", opt_name, ConfigList(None, self.default_plugins) ) def config_backend_reset_plugins(self, opt_name): self.config_backend_del("plugins", opt_name) def config_backend_add_plugin(self, opt_name, module_name): LOGGER.info("Adding plugin '%s' to configuration", module_name) modules = self.config_backend_list_active_plugins(opt_name) modules.elements.append(module_name) self.config_backend_put("plugins", opt_name, modules) def config_backend_remove_plugin(self, opt_name, module_name): LOGGER.info("Removing plugin '%s' from configuration", module_name) modules = self.config_backend_list_active_plugins(opt_name) try: modules.elements.remove(module_name) except ValueError: LOGGER.warning("Plugin '%s' not found", module_name) self.config_backend_put("plugins", opt_name, modules) def config_backend_put(self, section, key, value): """ Section must be a string. Key must be a string. """ LOGGER.debug("Configuration: %s:%s <-- %s", section, key, str(value)) if value is None: if section not in self.config: return if key not in self.config[section]: return self.config[section].pop(key) return t_str = _TYPE_TO_STR[type(value)] t = _STR_TO_TYPE[t_str] value = t(value) value = "{}{}{}".format(t_str, _TYPE_SEPARATOR, str(value)) if section not in self.config: self.config[section] = {key: value} else: self.config[section][key] = value for observer in self.observers[section]: observer() def config_backend_del(self, section, key): if section not in self.config: return if key not in self.config[section]: return self.config.remove_option(section, key) def config_backend_get(self, section, key, default=None): try: value = self.config[section][key] (t, value) = value.split(_TYPE_SEPARATOR, 1) r = _STR_TO_TYPE[t](value) LOGGER.debug("Configuration: %s:%s --> %s", section, key, str(r)) return r except KeyError: LOGGER.debug( "Configuration: %s:%s --> %s (default value)", section, key, str(default) ) return default def config_backend_add_observer(self, section, callback): self.observers[section].add(callback) def config_backend_remove_observer(self, section, callback): self.observers[section].remove(callback) def bug_report_get_attachments(self, out: dict): if self.config_path is None: return if self.core.call_success("fs_exists", self.config_path) is None: return out['config'] = { 'include_by_default': True, 'date': None, 'file_type': _("App. config."), 'file_url': self.config_path, 'file_size': self.core.call_success("fs_getsize", self.config_path) } paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/config/fake.py000066400000000000000000000045101417573700700266020ustar00rootroot00000000000000from .. import PluginBase class Setting(object): def __init__(self, value, default_func): self.value = value self.default_value_func = default_func self.observers = [] def get(self): if self.value is None: return self.default_value_func() return self.value def put(self, v): self.value = v for obs in self.observers: obs() def add_observer(self, obs): self.observers.append(obs) def remove_observer(self, obs): self.observers.remove(obs) class Plugin(PluginBase): """ Translate values from the configuration into more usable ones. Provides default values (except for plugins). """ def __init__(self): super().__init__() self._settings = {} def __get_settings(self): return {k: v.get() for (k, v) in self._settings.items()} def __set_settings(self, new_settings): self._settings = { k: Setting(v, lambda: None) for (k, v) in new_settings.items() } settings = property(__get_settings, __set_settings) def get_interfaces(self): return ['config'] def config_load(self): pass def config_load_plugins(self, plugin_list_name, default_plugins=[]): pass def config_save(self): pass def config_build_simple(self, section, token, default): return Setting(None, default) def config_register(self, key, setting): if key not in self._settings: # don't smash test settings self._settings[key] = setting def config_get_setting(self, key): return self._settings[key] def config_get(self, key): return self._settings[key].get() def config_get_default(self, key): return self._settings[key].default_value_func() def config_put(self, key, value): self._settings[key].put(value) def config_add_plugin(self, plugin): raise NotImplementedError() def config_remove_plugin(self, plugin): raise NotImplementedError() def config_list_plugins(self): raise NotImplementedError() def config_add_observer(self, key: str, callback): self._settings[key].add_observer(callback) def config_remove_observer(self, key: str, callback): self._settings[key].remove_observer(callback) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/data_versioning.py000066400000000000000000000037531417573700700276130ustar00rootroot00000000000000import logging import openpaperwork_core LOGGER = logging.getLogger(__name__) # TODO(Jflesch): this version shouldn't be common to all applications using # Openpaperwork-core DATA_VERSION = 2 class Plugin(openpaperwork_core.PluginBase): """ Keep a version number in ~/.local/share/paperwork2/data_version. If the version doesn't match 'DATA_VERSION', right before syncing, delete ~/.local/share/paperwork2 to force a full synchronisation. """ def get_interfaces(self): return ['data_versioning'] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_core.fs.python'], }, { 'interface': 'paths', 'defaults': ['openpaperwork_core.paths.xdg'], }, ] def init(self, core): super().init(core) data_dir = self.core.call_success("paths_get_data_dir") data_ver_file = self.core.call_success( "fs_join", data_dir, "data_version" ) data_version = -1 if self.core.call_success("fs_exists", data_ver_file): with self.core.call_success("fs_open", data_ver_file, 'r') as fd: data_version = int(fd.read().strip()) LOGGER.info( "Expected data version: %d ; Current data version: %d", DATA_VERSION, data_version ) if data_version != DATA_VERSION: LOGGER.warning( "Data version doesn't match (%d != %d). Forcing full sync", data_version, DATA_VERSION ) self.core.call_success("fs_rm_rf", data_dir, trash=False) # call 'paths_get_data_dir' again to recreate the data directory data_dir = self.core.call_success("paths_get_data_dir") with self.core.call_success("fs_open", data_ver_file, 'w') as fd: fd.write(str(DATA_VERSION)) self.core.call_all("on_data_files_deleted") paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/debug/000077500000000000000000000000001417573700700251435ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/debug/__init__.py000066400000000000000000000000001417573700700272420ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/debug/objgraph.py000066400000000000000000000024531417573700700273150ustar00rootroot00000000000000import gc import logging import tempfile import weakref import objgraph from .. import PluginBase LOGGER = logging.getLogger(__name__) class Plugin(PluginBase): def __init__(self): super().__init__() self.objs = [] self.graph_path = tempfile.mktemp(suffix='.png') def get_interfaces(self): return ['memleak_detector'] def on_objref_track(self, obj): assert(obj is not None) self.objs.append((str(type(obj)), weakref.ref(obj))) def on_objref_graph(self): to_graph = [] objs = self.objs.copy() gc.collect() for (idx, (type_name, wref)) in reversed(list(enumerate(objs))): obj = wref() if obj is None: LOGGER.info("Object of type %s has disappeared", type_name) self.objs.pop(idx) continue to_graph.append(obj) if len(to_graph) <= 0: LOGGER.info("Nothing to graph") return LOGGER.info( "Making reference graph for %d objects to %s", len(to_graph), self.graph_path ) objgraph.show_backrefs(to_graph, filename=self.graph_path) def on_memleak_track_stop(self): LOGGER.info("Most common object types:") objgraph.show_most_common_types() paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/debug/pympler.py000066400000000000000000000014571417573700700272140ustar00rootroot00000000000000import gc import logging import pympler.tracker from .. import PluginBase LOGGER = logging.getLogger(__name__) class Plugin(PluginBase): def __init__(self): super().__init__() self.tracker = None def get_interfaces(self): return ['memleak_detector'] def on_memleak_track_start(self): self.tracker = None LOGGER.info("Garbage collecting ...") gc.collect() LOGGER.info("Starting memory leaks tracking ...") self.tracker = pympler.tracker.SummaryTracker() def on_memleak_track_stop(self): if self.tracker is not None: LOGGER.info("Garbage collecting ...") gc.collect() LOGGER.info("Stopping memory leaks tracking ...") self.tracker.print_diff() self.tracker = None paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/deps.py000066400000000000000000000024161417573700700253650ustar00rootroot00000000000000CAIRO = { 'debian': 'python3-gi-cairo', 'fedora': 'python3-pycairo', 'gentoo': 'dev-python/pycairo', # Python 3 ? 'linuxmint': 'python-gi-cairo', # Python 3 ? 'raspbian': 'python3-gi-cairo', 'suse': 'python-cairo', # Python 3 ? 'ubuntu': 'python3-gi-cairo', } GDK = { 'debian': 'gir1.2-gdkpixbuf-2.0', 'linuxmint': 'gir1.2-gdkpixbuf-2.0', 'raspbian': 'gir1.2-gdkpixbuf-2.0', 'ubuntu': 'gir1.2-gdkpixbuf-2.0', } GI = { 'debian': 'python3-gi', 'fedora': 'python3-gobject-base', 'gentoo': 'dev-python/pygobject', # Python 3 ? 'linuxmint': 'python3-gi', 'raspbian': 'python3-gi', 'suse': 'python-gobject', # Python 3 ? 'ubuntu': 'python3-gi', } GLIB = { 'debian': 'gir1.2-glib-2.0', 'linuxmint': 'gir1.2-glib-2.0', 'raspbian': 'gir1.2-glib-2.0', 'ubuntu': 'gir1.2-glib-2.0', } PANGO = { 'debian': 'gir1.2-pango-1.0', 'linuxmint': 'gir1.2-pango-1.0', 'raspbian': 'gir1.2-glib-2.0', 'ubuntu': 'gir1.2-pango-1.0', } POPPLER = { 'debian': 'gir1.2-poppler-0.18', 'fedora': 'poppler-glib', 'gentoo': 'app-text/poppler', 'linuxmint': 'gir1.2-poppler-0.18', 'raspbian': 'gir1.2-poppler-0.18', 'suse': 'typelib-1_0-Poppler-0_18', 'ubuntu': 'gir1.2-poppler-0.18', } paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/display/000077500000000000000000000000001417573700700255225ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/display/__init__.py000066400000000000000000000000001417573700700276210ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/display/print.py000066400000000000000000000035421417573700700272340ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import os import sys import subprocess import openpaperwork_core class Plugin(openpaperwork_core.PluginBase): MIN_LINES_FOR_PAGING = 25 def __init__(self): self.output = [] def get_interfaces(self): return ['print'] def print(self, txt): self.output.append(txt) def print_isatty(self): return os.isatty(sys.stdout.fileno()) def print_flush(self): output = "".join(self.output) self.output = [] nb_lines = output.count("\n") isatty = os.isatty(sys.stdout.fileno()) if nb_lines < self.MIN_LINES_FOR_PAGING or not isatty: sys.stdout.write(output) else: # we always use 'less -R' because it's the only one we are sure # that handles correctly our ANSI colors process = subprocess.Popen(('less', '-R'), stdin=subprocess.PIPE) # TODO(Jflesch): Charset. For now we assume the system is UTF-8 try: process.stdin.write(output.encode("utf-8")) process.communicate() except BrokenPipeError: pass paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/external_apps/000077500000000000000000000000001417573700700267225ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/external_apps/__init__.py000066400000000000000000000000001417573700700310210ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/external_apps/dbus.py000066400000000000000000000033711417573700700302350ustar00rootroot00000000000000""" This plugin requires pydbus. If Pydbus is not installed. It just does nothing. """ import logging import os import socket import time try: import pydbus PYDBUS_AVAILABLE = True except ImportError: PYDBUS_AVAILABLE = False from .. import PluginBase LOGGER = logging.getLogger(__name__) class Plugin(PluginBase): PRIORITY = 50 def get_interfaces(self): return ['external_apps'] def get_deps(self): return [] def _get_file_manager_proxy(self): bus = pydbus.SessionBus() proxy = bus.get( "org.freedesktop.FileManager1", "/org/freedesktop/FileManager1" ) iface = proxy['org.freedesktop.FileManager1'] return iface def _open_url(self, func_name, url): if not PYDBUS_AVAILABLE: LOGGER.info("Pydbus is not available") return None try: uid = "{}{}_TIME{}".format( socket.gethostname(), os.getpid(), int(time.time()) ) iface = self._get_file_manager_proxy() LOGGER.info("Opening %s using Dbus", url) getattr(iface, func_name)([url], uid) except Exception as exc: LOGGER.error( "Failed to open %s using Dbus", url, exc_info=exc ) return None return True def external_app_open_file(self, file_url): # doesn't really open the file, but close enough. return self._open_url('ShowItems', file_url) def external_app_open_folder(self, folder_url): return self._open_url('ShowFolders', folder_url) def external_app_can_send_as_attachment(self) -> None: return None def external_app_send_as_attachment(self, uri) -> None: return None paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/external_apps/windows.py000066400000000000000000000020101417573700700307570ustar00rootroot00000000000000import logging import os from .. import PluginBase LOGGER = logging.getLogger(__name__) class Plugin(PluginBase): PRIORITY = 100 def get_interfaces(self): return ['external_apps'] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_core.fs.python'], }, ] def external_app_open_file(self, file_url): if not hasattr(os, 'startfile'): return None # os.startfile() is Windows-only. LOGGER.info("Opening %s with os.startfile()", file_url) assert(file_url.startswith("file://")) file_path = self.core.call_success("fs_safe", file_url) os.startfile(file_path) return True def external_app_open_folder(self, folder_url): return self.external_app_open_file(folder_url) def external_app_can_send_as_attachment(self) -> None: return None def external_app_send_as_attachment(self, uri) -> None: return None paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/external_apps/xdg.py000066400000000000000000000031571417573700700300640ustar00rootroot00000000000000import logging import os import subprocess import shutil from typing import Optional from .. import PluginBase LOGGER = logging.getLogger(__name__) class Plugin(PluginBase): def get_interfaces(self): return ['external_apps'] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_core.fs.python'], }, ] def external_app_open_file(self, file_url): if shutil.which("xdg-open") is None: return None LOGGER.info("Opening %s with xdg-open", file_url) os.spawnlp(os.P_NOWAIT, 'xdg-open', 'xdg-open', file_url) return True def external_app_open_folder(self, folder_url): return self.external_app_open_file(folder_url) def external_app_can_send_as_attachment(self) -> bool: return shutil.which("xdg-email") is not None def external_app_send_as_attachment(self, file_url: str) -> Optional[bool]: if not self.external_app_can_send_as_attachment(): return None LOGGER.info("Sending %s as attachment with xdg-email", file_url) assert(file_url.startswith("file://")) file_path = self.core.call_success("fs_unsafe", file_url) # xdg-email returns immediately, and we are interested in whether is # raises an exception try: subprocess.run(['xdg-email', '--attach', file_path], check=True) except subprocess.CalledProcessError as e: raise OSError( f"Failed to run xdg-email to send {file_path} as attachment" ) from e return True paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/flatpak.py000066400000000000000000000052171417573700700260560ustar00rootroot00000000000000import logging import os import shutil import tempfile from . import PluginBase LOGGER = logging.getLogger(__name__) class Plugin(PluginBase): PRIORITY = 75 def __init__(self): super().__init__() self.tmp_files = set() self.flatpak = False self.tmp_dir = None def get_interfaces(self): return [ 'flatpak', 'stats', ] def get_deps(self): return [ { 'interface': 'data_versioning', 'defaults': ['openpaperwork_core.data_versioning'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_core.fs.python'], }, { 'interface': 'paths', 'defaults': ['openpaperwork_core.paths.xdg'], }, ] def init(self, core): super().init(core) self.flatpak = (os.name == 'posix') and bool(self.core.call_success( "fs_isdir", "file:///app" )) LOGGER.info("Flatpak environment: %s", self.flatpak) self.tmp_dir = self.core.call_success( "fs_join", self.core.call_success("paths_get_data_dir"), "tmp" ) def is_in_flatpak(self): if self.flatpak: return True return None def fs_mktemp(self, prefix=None, suffix=None, mode='w+b', **kwargs): """ Modifies slightly the behaviour of fs_mktemp(): When making a bug report, we use temporary files. We need the user to be able to access those temporary files with external applications. With Flatpak, the easiest way for that is to place the temporary files somewhere in the user home directory. """ if not self.flatpak: return None self.core.call_success("fs_mkdir_p", self.tmp_dir) tmp = tempfile.NamedTemporaryFile( prefix=prefix, suffix=suffix, delete=False, mode=mode, dir=self.core.call_success("fs_unsafe", self.tmp_dir) ) LOGGER.info("Flatpak tmp file: %s --> %s", self.tmp_dir, tmp.name) self.tmp_files.add(tmp.name) return (self.core.call_success("fs_safe", tmp.name), tmp) def on_quit(self): if not self.flatpak: return if self.core.call_success("fs_exists", self.tmp_dir) is not None: shutil.rmtree(self.core.call_success("fs_unsafe", self.tmp_dir)) def stats_get(self, out: dict): if not self.flatpak: return if 'os_name' in out: out['os_name'] += ' (flatpak)' else: out['os_name'] = 'GNU/Linux (flatpak)' paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/frozen.py000066400000000000000000000020211417573700700257250ustar00rootroot00000000000000""" Takes care of the few things that must be done if we are running in an executable created with cx_freeze. It must be loaded as early as possible. """ import multiprocessing import os import sys from . import PluginBase class Plugin(PluginBase): def __init__(self): if not getattr(sys, 'frozen', False): return self._set_meipass() multiprocessing.freeze_support() def get_interfaces(self): return ['frozen'] def _set_meipass(self): # If sys.frozen, then Pyocr (and possibly others) needs MEIPASS to be # set *before* importing it. if getattr(sys, '_MEIPASS', False): # Pyinstaller case return # Cx_Freeze case if "python" not in sys.executable or '__file__' not in globals(): sys._MEIPASS = os.path.dirname(os.path.realpath(sys.executable)) else: sys._MEIPASS = os.path.realpath(os.path.join( os.path.dirname(os.path.realpath(__file__)), "..", ".." )) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/fs/000077500000000000000000000000001417573700700244655ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/fs/__init__.py000066400000000000000000000051561417573700700266050ustar00rootroot00000000000000import hashlib import logging import os import pathlib import urllib import urllib.parse import openpaperwork_core LOGGER = logging.getLogger(__name__) class CommonFsPluginBase(openpaperwork_core.PluginBase): def __init__(self): """ Should be used only by sub-classes """ super().__init__() def get_interfaces(self): return ['fs'] @staticmethod def fs_safe(uri): """ Make sure the specified URI is actually an URI and not a Unix path. Returns: - An URI """ LOGGER.debug("safe: %s", uri) if uri[:2] == "\\\\" or "://" in uri: LOGGER.debug("safe: --> %s", uri) return uri return pathlib.Path(uri).as_uri() @staticmethod def fs_unsafe(uri): """ Turn an URI into an Unix path, whenever possible. Shouldn't be used at all. """ LOGGER.debug("unsafe: %s", uri) if "://" not in uri and uri[:2] != "\\\\": LOGGER.debug("unsafe: --> %s", uri) return uri if not uri.startswith("file://"): LOGGER.debug("unsafe: --> EXC") raise Exception("TARGET URI SHOULD BE A LOCAL FILE") uri = uri[len("file://"):] if os.name == 'nt' and uri[0] == '/': # for some reason, URIs on Windows start with # "file:///C:\..." instead of "file://C:\..." uri = uri[1:] uri = urllib.parse.unquote(uri) return uri def fs_join(self, base, url): if not base.endswith("/"): base += "/" if base.startswith('smb'): return base + url return urllib.parse.urljoin(base, url) def fs_basename(self, url): parsed_url = urllib.parse.urlparse(url) path = parsed_url.path if path == "": path = parsed_url.hostname if path is None: return None basename = os.path.basename(path) # Base name can be safely unquoted return urllib.parse.unquote(basename) def fs_dirname(self, url): # dir name should not be unquoted. It could mess up the URI return os.path.dirname(url) def fs_hash(self, url): with self.core.call_success("fs_open", url, 'rb') as fd: content = fd.read() return int(hashlib.sha256(content).hexdigest(), 16) def fs_copy(self, origin_url, dest_url): """ default generic implementation """ with open(origin_url, 'rb') as fd: content = fd.read() with open(dest_url, 'wb') as fd: fd.write(content) return True paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/fs/fake.py000066400000000000000000000140721417573700700257510ustar00rootroot00000000000000""" Mock implementation of the plugin interface 'fs'. Useful for tests only. """ import io import logging import os from . import CommonFsPluginBase from . import memory LOGGER = logging.getLogger(__name__) class FakeFileAdapter(io.RawIOBase): def __init__(self, fs_plugin, path, content, mode='r'): super().__init__() self.fs_plugin = fs_plugin self.path = path self.content = content if 'w' not in mode else None self.pos = len(self.content) if 'a' in mode else 0 self.mode = mode if self.content is None: self.content = b'' if 'b' in mode else '' def readable(self): return True def writable(self): return 'w' in self.mode or 'a' in self.mode def read(self, size=-1): if size < 0: r = len(self.content[self.pos:]) else: r = min(size, len(self.content[self.pos:])) out = self.content[self.pos:self.pos + r] self.pos += r return out def readall(self): return self.content def readinto(self, b): raise NotImplementedError() def readline(self, size=-1): r = self.content[self.pos:] r = r.split("\n", 1) self.pos += len(r[0]) + 1 return r[0] + "\n" def readlines(self, hint=-1): self.pos = len(self.content) return [ r + "\n" for r in self.content.split("\n") ] def seek(self, offset, whence=os.SEEK_SET): if whence == os.SEEK_CUR: self.pos += offset elif whence == os.SEEK_SET: self.pos = offset else: raise NotImplementedError() def seekable(self): return True def tell(self): return self.pos def flush(self): pass def truncate(self, size=None): raise NotImplementedError() def isatty(self): return False def write(self, b): self.content = self.content[:self.pos] self.content += b self.pos += len(b) return len(b) def writelines(self, lines): raise NotImplementedError() def close(self): if 'a' in self.mode or 'w' in self.mode: d = self.fs_plugin.fs for p in self.path[:-1]: d = d[p] d[self.path[-1]] = self.content self.flush() super().close() def __enter__(self): return self def __exit__(self, type, value, traceback): self.close() class Plugin(CommonFsPluginBase): def __init__(self, fs=None): """ Test implementation. `fs` should have the following format: { 'base_dir': { 'sub_dir_a': {}, 'sub_dir_b': { 'file_a': "content_file_a", 'file_b': "content_file_b", } } } """ super().__init__() if fs is None: self.fs = {} else: self.fs = fs @staticmethod def _get_path(url): assert(url.lower().startswith("file:///")) url = url[len('file:///'):] return url.split('/') def fs_open(self, url, mode='r', **kwargs): path = self._get_path(url) f = self.fs for p in path[:-1]: f = f[p] if path[-1] in f: f = f[path[-1]] assert(isinstance(f, str) or isinstance(f, bytes)) elif 'b' in mode: f = b"" else: f = "" return FakeFileAdapter(self, path, f, mode) def fs_exists(self, url): path = self._get_path(url) f = self.fs for p in path: if p not in f: return None f = f[p] return True def fs_listdir(self, url): path = self._get_path(url) f = self.fs for p in path: try: f = f[p] except KeyError: return None assert(isinstance(f, dict)) return [url + "/" + k for k in f.keys()] def fs_rename(self, old_url, new_url): old_path = self._get_path(old_url) old_dir = self.fs for p in old_path[:-1]: old_dir = old_dir[p] new_path = self._get_path(new_url) new_dir = self.fs for p in new_path[:-1]: new_dir = new_dir[p] old_file = old_dir.pop(old_path[-1]) new_dir[new_path[-1]] = old_file def fs_unlink(self, url, **kwargs): self.fs_rm_rf(url) def fs_rm_rf(self, url, **kwargs): path = self._get_path(url) f = self.fs for p in path[:-1]: if p not in f: return None f = f[p] assert(isinstance(f, dict)) filename = url.split("/")[-1] if filename in f: f.pop(filename) return True return None def fs_get_mtime(self, url): return 0 def fs_getsize(self, url): path = self._get_path(url) f = self.fs for p in path: f = f[p] return len(f) def fs_isdir(self, url): path = self._get_path(url) f = self.fs for p in path: if p not in f: return None f = f[p] if isinstance(f, dict): return True return None def fs_copy(self, old_url, new_url): raise NotImplementedError() def fs_mkdir_p(self, url): path = self._get_path(url) f = self.fs for p in path[:-1]: if p not in f: f[p] = {} f = f[p] name = url.split("/")[-1] if name not in f: f[name] = {} return True def fs_recurse(self, parent_uri, dir_included=False): raise NotImplementedError() def fs_hide(self, uri): pass def fs_get_mime(self, uri): pass def fs_iswritable(self, url): return True def fs_mktemp(self, prefix=None, suffix=None, mode='w+b', **kwargs): name = "file://tmp/temporary_file" + suffix return (name, memory.MemoryFileAdapter(self, name, mode)) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/fs/memory.py000066400000000000000000000170211417573700700263500ustar00rootroot00000000000000""" Provides support for URIs "memory://". Those files are actually stored in memory. It is only useful for temporary files. It has been made as a plugin so it can easily be disabled on low-memory systems (will fall back on gio.fs --> real on-disk files). """ import io import itertools import logging import time from . import CommonFsPluginBase LOGGER = logging.getLogger(__name__) class _MemoryFileAdapter(io.RawIOBase): def __init__(self, plugin, key, mode='r'): super().__init__() self.plugin = plugin self.mode = mode self.key = key self.io = None self.io_cls = io.BytesIO if 'b' in mode else io.StringIO if 'r' in mode or 'a' in mode: if key not in self.plugin.fs: raise FileNotFoundError(key) data = self.plugin.fs[key][1] if 'b' in mode and isinstance(data, str): data = data.encode("utf-8") elif 'b' not in mode and isinstance(data, bytes): data = data.decode("utf-8") self.io = self.io_cls(data) elif 'w' in mode: self.io = self.io_cls() self.plugin.fs[self.key] = ( time.time(), b"" if 'b' in mode else "" ) self.get_content = ( self.io.getbuffer if 'b' in mode else self.io.getvalue ) self.read = self.io.read if hasattr(self.io, 'read') else None self.readall = self.io.readall if hasattr(self.io, 'readall') else None self.readinto = ( self.io.readinto if hasattr(self.io, 'readinto') else None ) self.readline = ( self.io.readline if hasattr(self.io, 'readline') else None ) self.readlines = ( self.io.readlines if hasattr(self.io, 'readlines') else None ) self.seek = self.io.seek if hasattr(self.io, 'seek') else None self.tell = self.io.tell if hasattr(self.io, 'tell') else None self.truncate = ( self.io.truncate if hasattr(self.io, 'truncate') else None ) self.write = self.io.write if hasattr(self.io, 'write') else None self.writelines = ( self.io.writelines if hasattr(self.io, 'writelines') else None ) def readable(self): return 'r' in self.mode or '+' in self.mode def writable(self): return 'w' in self.mode or 'a' in self.mode def seekable(self): return True def fileno(self): raise io.UnsupportedOperation("fileno() called on memory object") def isatty(self): return False def writelines(self, lines): if 'b' in self.mode: self.write(b"".join(lines)) else: self.write("".join(lines)) def flush(self): super().flush() if hasattr(self.io, 'flush'): self.io.flush() if 'w' not in self.mode and 'a' not in self.mode: return if 'b' in self.mode: self.plugin.fs[self.key] = (time.time(), bytes(self.get_content())) else: self.plugin.fs[self.key] = (time.time(), str(self.get_content())) def close(self): self.flush() super().close() def __enter__(self): return self def __exit__(self, type, value, traceback): self.close() class Plugin(CommonFsPluginBase): PRIORITY = 100 # to be called first for fs_mktemp def __init__(self): super().__init__() # self.fs = {id: (mtime, content), id: (mtime, content), ...} self.fs = {} self.id_gen = itertools.count() @staticmethod def _get_memory_id(uri): if not uri.startswith("memory://"): return None return uri[len("memory://"):] def fs_open(self, uri, mode='r', needs_fileno=False, **kwargs): if needs_fileno: return None mem_id = self._get_memory_id(uri) if mem_id is None: return None return _MemoryFileAdapter(self, mem_id, mode) def fs_exists(self, url): mem_id = self._get_memory_id(url) if mem_id is None: return None return mem_id in self.fs def fs_listdir(self, url): mem_id = self._get_memory_id(url) if mem_id is None: return mem_id += '/' out = [] for k in self.fs.keys(): if k.startswith(mem_id) and '/' not in k[len(mem_id):]: out.append(k) return out def fs_rename(self, old_url, new_url): old_mem_id = self._get_memory_id(old_url) new_mem_id = self._get_memory_id(old_url) if old_mem_id is None or new_mem_id is None: return f = self.fs.pop(old_mem_id) self.fs[new_mem_id] = f return True def fs_unlink(self, url, **kwargs): mem_id = self._get_memory_id(url) if mem_id is None: return self.fs.pop(mem_id) return True def fs_rm_rf(self, url, **kwargs): mem_id = self._get_memory_id(url) if mem_id is None: return if mem_id in self.fs: self.fs.pop(mem_id) mem_id += '/' for k in list(self.fs.keys()): if k.startswith(mem_id): self.fs.pop(k) return True def fs_get_mtime(self, url): mem_id = self._get_memory_id(url) if mem_id is None: return return self.fs[mem_id][0] def fs_getsize(self, url): mem_id = self._get_memory_id(url) if mem_id is None: return return len(self.fs[mem_id][1]) def fs_isdir(self, url): mem_id = self._get_memory_id(url) if mem_id is None: return None mem_id += '/' for k in list(self.fs.keys()): if k.startswith(mem_id): return True return None def fs_copy(self, old_url, new_url): old_mem_id = self._get_memory_id(old_url) new_mem_id = self._get_memory_id(old_url) if old_mem_id is None and new_mem_id is None: return None if old_mem_id is None or new_mem_id is None: # One memory url, one local filesystem url # use the more generic and cross-FS method return super().fs_copy(old_url, new_url) self.fs[new_mem_id] = self.fs[old_mem_id] return new_url def fs_mkdir_p(self, url): mem_id = self._get_memory_id(url) if mem_id is None: return None # nothing to do actually return True def fs_recurse(self, parent_url, dir_included=False): mem_id = self._get_memory_id(parent_url) if mem_id is None: return mem_id += '/' out = [] for k in self.fs.keys(): if k.startswith(mem_id): out.append(k) return out def fs_hide(self, url): return None def fs_get_mime(self, url): return None def fs_iswritable(self, url): mem_id = self._get_memory_id(url) if mem_id is None: return None return True def fs_mktemp( self, prefix=None, suffix=None, mode='w+b', on_disk=False, **kwargs ): assert(prefix is None or '/' not in prefix) assert(suffix is None or '/' not in suffix) if on_disk: return name = "{}{}{}".format( prefix if prefix is not None else "", next(self.id_gen), suffix if suffix is not None else "" ) return ("memory://" + name, _MemoryFileAdapter(self, name, mode)) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/fs/python.py000066400000000000000000000120141417573700700263560ustar00rootroot00000000000000""" Provides support for URIs "file://". """ import codecs import logging import os import os.path import shutil import tempfile from . import CommonFsPluginBase LOGGER = logging.getLogger(__name__) class Plugin(CommonFsPluginBase): def __init__(self): super().__init__() self.tmp_files = set() def _uri_to_path(self, uri): if not uri.lower().startswith("file://"): return None return self.fs_unsafe(uri) def fs_open(self, uri, mode='r', **kwargs): path = self._uri_to_path(uri) if path is None: return None if 'w' not in mode and 'a' not in mode and not os.path.exists(path): return None if 'b' in mode: return open(path, mode) return codecs.open(path, mode, encoding='utf-8') def fs_exists(self, uri): path = self._uri_to_path(uri) if path is None: return None r = os.path.exists(path) if not r: return None return r def fs_listdir(self, uri): path = self._uri_to_path(uri) if path is None: return None if not os.path.exists(path): return None for f in os.listdir(path): yield self.fs_safe(os.path.join(path, f)) def fs_rename(self, old_uri, new_uri): old_path = self._uri_to_path(old_uri) new_path = self._uri_to_path(new_uri) if old_path is None or new_path is None: return None if not os.path.exists(old_path): return None os.rename(old_path, new_path) return True def fs_unlink(self, uri, **kwargs): path = self._uri_to_path(uri) if path is None: return if not os.path.exists(path): return None os.unlink(path) return True def fs_rm_rf(self, uri, **kwargs): path = self._uri_to_path(uri) if path is None: return shutil.rmtree(path, ignore_errors=True) return True def fs_get_mtime(self, uri): path = self._uri_to_path(uri) if path is None: return return os.stat(path).st_mtime def fs_getsize(self, uri): path = self._uri_to_path(uri) if path is None: return return os.stat(path).st_size def fs_isdir(self, uri): path = self._uri_to_path(uri) if path is None: return if os.path.isdir(path): return True return None def fs_copy(self, old_uri, new_uri): old_path = self._uri_to_path(old_uri) new_path = self._uri_to_path(new_uri) if old_path is None or new_path is None: return None shutil.copy(old_path, new_path) return True def fs_mkdir_p(self, uri): path = self._uri_to_path(uri) if path is None: return os.makedirs(path, mode=0o700, exist_ok=True) return True def fs_recurse(self, parent_uri, dir_included=False): path = self._uri_to_path(parent_uri) if path is None: return for (root, dirs, files) in os.walk(path): if dir_included: for d in dirs: p = os.path.join(path, root, d) yield self.fs_safe(p) for f in files: p = os.path.join(path, root, f) yield self.fs_safe(p) def fs_hide(self, uri): pass def fs_get_mime(self, uri): path = self._uri_to_path(uri) if path is None: return None # should use 'magic', but Core can't have any dependency on it. # other we would pull it on all platforms. path = path.lower() if path.endswith(".pdf"): return "application/pdf" if path.endswith(".png"): return "image/png" if path.endswith(".tiff"): return "image/tiff" if path.endswith(".bmp"): return "image/x-ms-bmp" if path.endswith(".jpeg") or path.endswith(".jpg"): return "image/jpeg" if path.endswith(".txt"): return "text/plain" return None def fs_iswritable(self, uri): path = self._uri_to_path(uri) if path is None: return None return os.access(path, os.W_OK) def fs_mktemp( self, prefix=None, suffix=None, mode='w+b', on_disk=False, **kwargs ): if 'b' not in mode: tmp = tempfile.NamedTemporaryFile( prefix=prefix, suffix=suffix, delete=False, mode=mode, encoding='utf-8' ) else: tmp = tempfile.NamedTemporaryFile( prefix=prefix, suffix=suffix, delete=False, mode=mode ) self.tmp_files.add(tmp.name) return (self.fs_safe(tmp.name), tmp) def on_quit(self): for tmp_file in self.tmp_files: try: os.unlink(tmp_file) except FileNotFoundError: pass paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/http.py000066400000000000000000000076751417573700700254250ustar00rootroot00000000000000""" module 'http' contains all the code to communicate using HTTP+JSON with https://openpaper.work/: - update notification - anonymous statistics - etc """ import base64 import http import http.client import json import logging import os import ssl import urllib from . import PluginBase from . import promise if os.name == "nt": import certifi LOGGER = logging.getLogger(__name__) DEFAULT_SERVER = "openpaper.work" DEFAULT_PROTOCOL = "https" class JsonHttp(object): def __init__(self, core, module_name): self.core = core self.user_agent = "{} {}".format( core.call_success("app_get_name"), core.call_success("app_get_version") ) self.config_section_name = module_name settings = { self.config_section_name + "_protocol": core.call_success( "config_build_simple", self.config_section_name, "protocol", lambda: DEFAULT_PROTOCOL ), self.config_section_name + "_server": core.call_success( "config_build_simple", self.config_section_name, "server", lambda: DEFAULT_SERVER ), } for (k, setting) in settings.items(): core.call_all("config_register", k, setting) def _convert(self, data): if isinstance(data, str): return data if isinstance(data, bytes): return base64.encodebytes(data).decode("utf-8").strip() return json.dumps(data) def _request(self, data, protocol, server, path): if protocol == "http": h = http.client.HTTPConnection(host=server) elif os.name == "nt": # On Windows, when frozen, we must specify a cafile cafile = certifi.where() ssl_context = ssl.create_default_context(cafile=cafile) h = http.client.HTTPSConnection(host=server, context=ssl_context) else: h = http.client.HTTPSConnection(host=server) if data is None or (isinstance(data, str) and data == ""): LOGGER.info("Sending GET %s/%s", server, path) h.request('GET', url=path, headers={'User-Agent': self.user_agent}) else: body = urllib.parse.urlencode({ k: self._convert(v) for (k, v) in data.items() }) LOGGER.info("Sending POST %s/%s", server, path) h.request( 'POST', url=path, headers={ "Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain", 'User-Agent': self.user_agent, }, body=body ) r = h.getresponse() reply = r.read().decode('utf-8') LOGGER.info("Got HTTP %s: %s - %s", r.status, r, reply) if r.status != http.client.OK: raise ConnectionError("HTTP {}: {} - {}".format( r.status, r, reply )) if reply[0] != '[' and reply[0] != '{': return reply return json.loads(reply) def get_request_promise(self, path): protocol = self.core.call_success( "config_get", self.config_section_name + "_protocol" ) server = self.core.call_success( "config_get", self.config_section_name + "_server" ) return promise.ThreadedPromise( self.core, self._request, args=(protocol, server, path) ) class Plugin(PluginBase): def get_interfaces(self): return ['http_json'] def get_deps(self): return [ { 'interface': 'app', 'defaults': ['openpaperwork_core.app'], }, { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, ] def http_json_get_client(self, module_name): return JsonHttp(self.core, module_name) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/i18n/000077500000000000000000000000001417573700700246345ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/i18n/__init__.py000066400000000000000000000000001417573700700267330ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/i18n/python.py000066400000000000000000000062351417573700700265350ustar00rootroot00000000000000import collections import datetime import locale import unicodedata from .. import (_, PluginBase) class Plugin(PluginBase): MONTH_FORMATS = collections.defaultdict( lambda: "%B", oc="%b", # Occitan ) def __init__(self): self.today = datetime.date.today() self.yesterday = self.today - datetime.timedelta(days=1) # Need the l10n plugin to be loaded first before getting the # translations self.i18n_today = None self.i18n_yesterday = None self.i18n_sizes = () def get_interfaces(self): return ['i18n'] def get_deps(self): return [ { 'interface': 'l10n', 'defaults': ['openpaperwork_core.l10n.python'], }, ] def init(self, core): super().init(core) self.i18n_today = _("Today") self.i18n_yesterday = _("Yesterday") self.i18n_sizes = ( _('%3.1f bytes'), _('%3.1f KiB'), _('%3.1f MiB'), _('%3.1f GiB'), _('%3.1f TiB'), ) def i18n_date_short(self, date): if hasattr(date, 'date'): date = date.date() # datetime --> date if date == self.today: return self.i18n_today elif date == self.yesterday: return self.i18n_yesterday else: return date.strftime("%x") def i18n_parse_date_short(self, txt): if txt == self.i18n_today: return self.today elif txt == self.i18n_yesterday: return self.yesterday else: try: return datetime.datetime.strptime(txt, "%x").date() except ValueError: return None def i18n_date_long_year(self, date): if hasattr(date, 'date'): date = date.date() # datetime --> date return date.strftime("%Y") def i18n_date_long_month(self, date): if hasattr(date, 'date'): date = date.date() # datetime --> date locale_msg = None if hasattr(locale, 'LC_MESSAGES'): locale_msg = locale.getlocale(locale.LC_MESSAGES) elif hasattr(locale, 'LC_ALL'): locale_msg = locale.getlocale(locale.LC_ALL) if locale_msg is None or locale_msg[0] is None: locale_msg = None else: locale_msg = locale_msg[0].split("_", 1)[0] return date.strftime(self.MONTH_FORMATS[locale_msg]) def i18n_file_size(self, num): for string in self.i18n_sizes: if num < 1024.0: return string % (num) num /= 1024.0 return self.i18n_sizes[-1] % (num) def i18n_strip_accents(self, string): """ Strip all the accents from the string """ return u''.join( ( character for character in unicodedata.normalize('NFD', string) if unicodedata.category(character) != 'Mn' ) ) def i18n_sort(self, string_list): t = [ (self.i18n_strip_accents(str(e).lower()), e) for e in string_list ] t.sort() return [e[1] for e in t] paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/interactive.py000066400000000000000000000147561417573700700267610ustar00rootroot00000000000000import atexit import code import os import os.path import readline import rlcompleter # noqa: F401 import time import threading from . import PluginBase class HistoryConsole(code.InteractiveConsole): def __init__( self, locals=None, filename="", histfile=os.path.expanduser("~/.console-history")): super().__init__(locals, filename) self.init_history(histfile) def init_history(self, histfile): readline.parse_and_bind("tab: complete") if hasattr(readline, "read_history_file"): try: readline.read_history_file(histfile) except FileNotFoundError: pass atexit.register(self.save_history, histfile) def save_history(self, histfile): readline.set_history_length(1000) readline.write_history_file(histfile) class ProxyCore(object): def __init__(self, core): self.core = core def call_all(self, *args, **kwargs): return self.core.call_one( "mainloop_execute", self.core.call_all, *args, **kwargs ) def call_success(self, *args, **kwargs): return self.core.call_one( "mainloop_execute", self.core.call_success, *args, **kwargs ) def call_one(self, *args, **kwargs): return self.core.call_one( "mainloop_execute", self.core.call_one, *args, **kwargs ) class Plugin(PluginBase): def __init__(self): super().__init__() self.has_quit = False # used for wait(): self.progress_condition = threading.Condition() self.progresses = {} self._previous_progresses = {} self.nb_windows_to_realize = 0 def get_interfaces(self): return [ 'gtk_window_listener' ] def get_deps(self): return [ { 'interface': 'data_versioning', 'defaults': ['openpaperwork_core.data_versioning'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_core.fs.python'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_core.mainloop.asyncio'], }, { 'interface': 'paths', 'defaults': ['openpaperwork_core.paths.xdg'], }, ] def init(self, core): super().init(core) data_dir = self.core.call_success("paths_get_data_dir") base_hist_dir = self.core.call_success( "fs_join", data_dir, "openpaperwork" ) self.core.call_success("fs_mkdir_p", base_hist_dir) histfile = self.core.call_success( "fs_join", base_hist_dir, "interactive_history" ) print("Objects provided:") print(" core : reference to OpenPaperwork's core") print(" stop(): shut down the application (but not this shell)") print(" exit(): stops this shell (but not the application)") print(" wait(): wait for all background tasks to end") print(" Ctrl-D / EOF: stops this shell and the application") console = HistoryConsole({ "core": ProxyCore(core), "stop": self._stop, "wait": self._wait, }, histfile=self.core.call_success("fs_unsafe", histfile)) threading.Thread(target=self._interact, args=(console,)).start() def on_gtk_window_opened(self, window): with self.progress_condition: if window.get_window() is not None: return self.nb_windows_to_realize += 1 realize_handler_id = None def on_realize(): self.nb_windows_to_realize -= 1 window.disconnect(realize_handler_id) self.progress_condition.notify_all() realize_handler_id = window.connect("realize", on_realize) def on_gtk_window_closed(self, window): pass def _interact(self, console): console.interact() self._stop() def on_quit(self): self.has_quit = True def _stop(self): if self.has_quit: return print("Quitting") self.core.call_one("mainloop_execute", self.core.call_all, "on_quit") self.core.call_one( "mainloop_execute", self.core.call_all, "mainloop_quit_graceful" ) def on_progress(self, upd_type, progress, description=None): progress = int(progress * 100) with self.progress_condition: if progress >= 100: if upd_type in self.progresses: self.progresses.pop(upd_type) self.progress_condition.notify_all() elif (upd_type not in self.progresses or progress != self.progresses[upd_type]): self.progresses[upd_type] = progress self.progress_condition.notify_all() def _wait(self): # WORKAROUND(Jflesch): sometimes, for some reason, we never get the # notification for the end of boxes loading MAX_TIME = 30 # seconds start = time.time() time_diff = 0 # wait a little, in case the call to _wait() came slightly before # the background task creation. print("Waiting for all background tasks to end") time.sleep(3.0) with self.progress_condition: while ((len(self.progresses) > 0 or self.nb_windows_to_realize > 0) and time_diff <= MAX_TIME): while ((len(self.progresses) > 0 or self.nb_windows_to_realize > 0) and time_diff <= MAX_TIME): print("Waiting for all background tasks to end") print("Remaining background tasks:") for (upd_type, progress) in self.progresses.items(): print(" {}: {}%".format(upd_type, int(progress))) self.progress_condition.wait(1.0) time_diff = time.time() - start if time_diff > MAX_TIME: break # wait again a little ; sometime background tasks are # sequentially removed and added self.progress_condition.release() try: time.sleep(3) finally: self.progress_condition.acquire() if time_diff <= MAX_TIME: print("All background tasks have ended") else: print("TIMEOUT") paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/l10n/000077500000000000000000000000001417573700700246275ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/l10n/__init__.py000066400000000000000000000000001417573700700267260ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/l10n/python.py000066400000000000000000000055311417573700700265260ustar00rootroot00000000000000import ctypes import gettext import locale import logging import os import sys from .. import PluginBase LOGGER = logging.getLogger(__name__) class Plugin(PluginBase): def __init__(self): super().__init__() self.libintl = None def get_interfaces(self): return [ 'l10n', 'l10n_init', ] def get_deps(self): return [ { # if frozen, we need sys._MEIPASS to be set correctly 'interface': 'frozen', 'defaults': ['openpaperwork_core.frozen'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_core.fs.python'], }, { 'interface': 'resources', 'defaults': ['openpaperwork_core.resources.setuptools'], }, ] def init(self, core): super().init(core) if os.name == "nt" and os.getenv('LANG') is None: (lang, enc) = locale.getdefaultlocale() os.environ['LANG'] = lang try: locale.setlocale(locale.LC_ALL, '') except locale.Error: # happens, for instance when LC_ALL is set to a nonexisting locale LOGGER.warning( "Failed to set localization. Localization will be disabled" ) return self.libintl = None if getattr(sys, 'frozen', False): libintl_path = os.path.abspath(os.path.join( sys._MEIPASS, "libintl-8.dll" )) self.libintl = ctypes.cdll.LoadLibrary(libintl_path) self.l10n_load('openpaperwork_core.l10n', 'openpaperwork_core') def l10n_load(self, python_package, text_domain): path = self.core.call_success( "resources_get_dir", python_package, 'out' ) if path is None: LOGGER.error( "Failed to access ressources '%s/out'", python_package ) return None path = self.core.call_success("fs_unsafe", path) mo_file = gettext.find(text_domain, path) if mo_file is None: # expected if we don't have translation for the user language LOGGER.info( "Failed to find valid locale for '%s' (path=%s)", text_domain, path ) # we still try to keep going LOGGER.info("Binding text domain %s to '%s'", text_domain, path) if self.libintl is not None: self.libintl.bindtextdomain(text_domain, path) self.libintl.bind_textdomain_codeset(text_domain, 'UTF-8') for module in (gettext, locale): if hasattr(module, 'bindtextdomain'): module.bindtextdomain(text_domain, path) if hasattr(module, 'textdomain'): module.textdomain(text_domain) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/logs/000077500000000000000000000000001417573700700250215ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/logs/__init__.py000066400000000000000000000000001417573700700271200ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/logs/archives.py000066400000000000000000000105431417573700700272020ustar00rootroot00000000000000import faulthandler import logging from .. import (_, PluginBase) LOGGER = logging.getLogger(__name__) LOG_DATE_FORMAT = "%Y%m%d_%H%M_%S" MAX_DAYS = 31 class LogLine(object): def __init__(self, line): self.line = line self.next = None class LogHandler(logging.Handler): LOG_FORMAT = '[%(levelname)-6s] [%(name)-30s] %(message)s' MAX_LINES = 5000 MAX_UNCAUGHT_LOGGUED = 20 def __init__(self, core, archiver): super().__init__() self.core = core self.archiver = archiver self.first_line = None self.last_line = None self.nb_lines = 0 self.nb_uncaught_loggued = 0 self.out_file_url = self.archiver.get_new() self.formatter = logging.Formatter(self.LOG_FORMAT) self.out_fd = self.core.call_success( "fs_open", self.out_file_url, 'w', needs_fileno=True ) faulthandler.disable() faulthandler.enable(file=self.out_fd) def emit(self, record): line = self.formatter.format(record) + "\n" self.out_fd.write(line) line = LogLine(line) if self.last_line is not None: self.last_line.next = line self.last_line = line if self.first_line is None: self.first_line = line if self.nb_lines >= self.MAX_LINES: self.last_line = self.last_line.next else: self.nb_lines += 1 def log_uncaught_exception(self): if self.nb_uncaught_loggued >= self.MAX_UNCAUGHT_LOGGUED: # avoid logging too much return self.nb_uncaught_loggued += 1 out_file_url = self.archiver.get_new( name="uncaught_exception_logs" ) self.formatter = logging.Formatter(self.LOG_FORMAT) with self.core.call_success("fs_open", out_file_url, 'w') as fd: line = self.first_line while line is not None: fd.write(line.line) line = line.next class Plugin(PluginBase): PRIORITY = -10000 def __init__(self): super().__init__() logging.getLogger().setLevel(logging.INFO) self.archiver = None def get_interfaces(self): return [ 'bug_report_attachments', 'log_archiver', 'uncaught_exception_listener', ] def get_deps(self): return [ { 'interface': 'file_archives', 'defaults': ['openpaperwork_core.archives'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_core.fs.python'], }, { 'interface': 'uncaught_exception', 'defaults': ['openpaperwork_core.uncaught_exception'], }, ] def init(self, core): super().init(core) self.archiver = self.core.call_success( "file_archive_get", storage_name="logs", file_extension="txt" ) self.log_handler = LogHandler(core, self.archiver) logging.getLogger().addHandler(self.log_handler) def on_uncaught_exception(self, exc_info): self.log_handler.log_uncaught_exception() def bug_report_get_attachments(self, inputs: dict): archived = list(self.archiver.get_archived()) archived.sort(key=lambda x: x[0], reverse=True) for (nb, (date, file_url)) in enumerate(archived): inputs[file_url] = { 'date': date, # Users tend to send only the pre-selected elements in the bug # reports. However, when they report a sudden crash, we need # the logs of the previous session, not just the current one. # --> Include the logs of the last 2 previous sessions too. 'include_by_default': nb <= 2, 'file_type': _("Log file"), 'file_url': file_url, 'file_size': self.core.call_success("fs_getsize", file_url), } LOGGER.info("Flushing logs to disk") self.log_handler.out_fd.flush() file_url = self.log_handler.out_file_url inputs[file_url] = { 'date': None, # now 'include_by_default': True, 'file_type': _("Log file"), 'file_url': file_url, 'file_size': self.core.call_success("fs_getsize", file_url), } paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/logs/print.py000066400000000000000000000122511417573700700265300ustar00rootroot00000000000000import datetime import logging import sys import tempfile from .. import PluginBase LOGGER = logging.getLogger(__name__) g_tmp_file = None def _get_tmp_file(): global g_tmp_file if g_tmp_file is not None: return g_tmp_file date = datetime.datetime.now() date = date.strftime("%Y%m%d_%H%M_%S") t = tempfile.NamedTemporaryFile( mode='w', suffix=".txt", prefix="openpaperwork_{}_".format(date), encoding='utf-8' ) if sys.stderr is not None: sys.stderr.write("Temporary file = {}\n".format(t.name)) g_tmp_file = t return t class _LogHandler(logging.Handler): def __init__(self): super().__init__() self.log_level = logging.DEBUG self.formatter = logging.Formatter(Plugin.DEFAULT_LOG_FORMAT) self.out_fds = set() if sys.stderr is not None: self.out_fds.add(sys.stderr) def emit(self, record): if record.levelno < self.log_level: return line = self.formatter.format(record) for fd in self.out_fds: fd.write(line + "\n") class Plugin(PluginBase): CONFIG_FILE_SEPARATOR = "," DEFAULT_LOG_FILES = 'stderr' DEFAULT_LOG_FORMAT = '[%(levelname)-6s] [%(name)-30s] %(message)s' LOG_LEVELS = { 'none': logging.CRITICAL, 'critical': logging.CRITICAL, 'error': logging.ERROR, 'warn': logging.WARN, 'warning': logging.WARNING, 'info': logging.INFO, 'debug': logging.DEBUG, } SPECIAL_FILES = { 'stdout': lambda: sys.stdout, 'stderr': lambda: sys.stderr, 'temp': _get_tmp_file, } def __init__(self): super().__init__() self.log_file_paths = set() self.log_handler = None def get_interfaces(self): return [ 'logs', 'uncaught_exception_listener', ] def get_deps(self): return [ { 'interface': 'uncaught_exception', 'defaults': ['openpaperwork_core.uncaught_exception'], }, { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, ] def init_logs(self, app_name, default_log_level): section_name = "logging:" + app_name s = self.core.call_success( "config_build_simple", section_name, "level", lambda: default_log_level ) self.core.call_all("config_register", "log_level", s) self.core.call_all( 'config_add_observer', "log_level", self._reload_config ) s = self.core.call_success( "config_build_simple", section_name, "files", lambda: self.DEFAULT_LOG_FILES ) self.core.call_all("config_register", "log_files", s) self.core.call_all( 'config_add_observer', "log_files", self._reload_config ) s = self.core.call_success( "config_build_simple", section_name, "format", lambda: self.DEFAULT_LOG_FORMAT ) self.core.call_all("config_register", "log_format", s) self.core.call_all( 'config_add_observer', "log_format", self._reload_config ) self.log_handler = _LogHandler() logging.getLogger().addHandler(self.log_handler) self._reload_config() def _disable_logging(self): for fd in self.log_handler.out_fds: if fd != sys.stdout and fd != sys.stderr and fd != g_tmp_file: fd.close() self.log_handler.out_fds = set() def _enable_logging(self): self.log_handler.out_fds = set() first = None for file_path in self.log_file_paths: if file_path.lower() not in self.SPECIAL_FILES: fd = open(file_path, 'a') self.log_handler.out_fds.add(fd) else: fd = self.SPECIAL_FILES[file_path.lower()]() if fd is not None: # stderr doesn't exist when frozen self.log_handler.out_fds.add(fd) if first is None and fd is not None: first = fd def _reload_config(self, *args, **kwargs): self._disable_logging() LOGGER.info("Reloading logging configuration") try: log_level = self.core.call_success('config_get', "log_level") self.set_log_level(log_level) self.log_file_paths = self.core.call_success( 'config_get', "log_files", ).split(self.CONFIG_FILE_SEPARATOR) self.log_handler.formatter = logging.Formatter( self.core.call_success('config_get', "log_format") ) finally: self._enable_logging() def set_log_level(self, log_level): lvl = self.LOG_LEVELS[log_level] self.log_handler.log_level = lvl if lvl > logging.INFO: # Never disable info level ; it may be used by other plugins lvl = logging.INFO logging.getLogger().setLevel(lvl) def on_uncaught_exception(self, exc_info): LOGGER.error("=== UNCAUGHT EXCEPTION ===", exc_info=exc_info) LOGGER.error("==========================") paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/mainloop/000077500000000000000000000000001417573700700256735ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/mainloop/__init__.py000066400000000000000000000000001417573700700277720ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/mainloop/asyncio.py000066400000000000000000000135551417573700700277230ustar00rootroot00000000000000import asyncio import logging import threading import openpaperwork_core LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): """ A main loop based on asyncio. Not as complete as GLib main loop, but good enough for shell commands. """ def __init__(self): super().__init__() self.halt_on_uncaught_exception = True self.loop = None self.loop_ident = None self.halt_cause = None self.task_count = 0 self.log_uncaught = True def _check_mainloop_instantiated(self): if self.loop is None: self.loop = asyncio.get_event_loop() self.task_count = 0 def get_interfaces(self): return [ "mainloop", ] def mainloop(self, halt_on_uncaught_exception=True, log_uncaught=True): """ Wait for callbacks to be scheduled and execute them. This method is blocking and will block until `mainloop_quit*()` is called. """ self._check_mainloop_instantiated() self.log_uncaught = log_uncaught self.halt_on_uncaught_exception = halt_on_uncaught_exception self.mainloop_schedule(self.core.call_all, "on_mainloop_start") self.loop_ident = threading.current_thread().ident try: self.loop.run_forever() finally: self.loop_ident = None self.core.call_all("on_mainloop_quit") if self.halt_cause is not None: halt_cause = self.halt_cause self.halt_cause = None LOGGER.error("Main loop stopped because %s", str(halt_cause)) raise halt_cause self.loop = None return True def mainloop_get_thread_id(self): """ Gets the ID of the thread running the main loop. `None` if no thread is running it. """ return self.loop_ident def mainloop_quit_graceful(self): """ Wait for all the scheduled callbacks to be executed and then stops the main loop. """ self.mainloop_schedule(self._mainloop_quit_graceful) return True def _mainloop_quit_graceful(self): if self.task_count > 1: # keep in mind this function is in a task too LOGGER.info( "Quit graceful: Remaining tasks: %d", self.task_count - 1 ) self.mainloop_schedule(self._mainloop_quit_graceful, delay_s=0.2) return LOGGER.info("Quit graceful: Quitting") self.mainloop_quit_now() self.task_count = 1 # we are actually the one task still running def mainloop_quit_now(self): """ Stops the main loop right now. Note that it cannot interrupt a callback being executed, but no callback scheduled after this one will be executed. """ if self.loop is None: return None self.loop.stop() self.loop = None self.task_count = 0 def mainloop_ref(self, obj): """ If you run a task independently from the main loop, you may want to increment the reference counter of the main loop so `mainloop_quit_graceful` does not interrupt the main loop while your task is still running. ThreadedPromise already takes care of incrementing and decrementing this reference counter. """ self.task_count += 1 def mainloop_unref(self, obj): self.task_count -= 1 def mainloop_schedule(self, func, *args, delay_s=0, **kwargs): """ Request that the main loop executes the callback `func`. Will return immediately. """ assert(hasattr(func, '__call__')) self._check_mainloop_instantiated() self.task_count += 1 async def decorator(args, kwargs): try: func(*args, **kwargs) except Exception as exc: if self.halt_on_uncaught_exception: LOGGER.error( "Main loop: Uncaught exception (%s) ! Quitting", func, exc_info=exc ) self.halt_cause = exc self.mainloop_quit_now() elif self.log_uncaught: LOGGER.error( "Main loop: Uncaught exception (%s) !", func, exc_info=exc ) finally: self.task_count -= 1 coroutine = decorator(args, kwargs) args = (self.loop.create_task, coroutine) if delay_s != 0: args = (self.loop.call_later, delay_s) + args self.loop.call_soon_threadsafe(args[0], *(args[1:])) return True def mainloop_execute(self, func, *args, **kwargs): """ Ensure a function is run on the main loop, even if called from a thread. Will return only once the callback `func` has been executed. Will return the value returned by `func`. This method makes it easier to work with non-thread-safe modules (sqlite3 for instance). """ current = threading.current_thread().ident # XXX(Jflesch): # if self.loop_ident is None, it means the mainloop hasn't been started # yet --> we cannot run the function on the mainloop anyway, so # we assume we are on the same thread that will later run the main # loop. if self.loop_ident is None or current == self.loop_ident: return func(*args, **kwargs) event = threading.Event() out = [None] exc = [None] def get_result(): try: out[0] = func(*args, **kwargs) except Exception as e: exc[0] = e event.set() self.mainloop_schedule(get_result) event.wait() if exc[0] is not None: raise exc[0] return out[0] paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/mainloop/tests.py000066400000000000000000000061571417573700700274200ustar00rootroot00000000000000import unittest from .. import (Core, promise) class AbstractTestCallback(unittest.TestCase): def get_plugin_name(self): """ Must be overloaded by subclasses """ assert() def setUp(self): self.core = Core(auto_load_dependencies=True) self.core.load(self.get_plugin_name()) self.core.init() self.val = None def test_basic(self): def set_val(value): self.val = value # queue some calls self.core.call_all("mainloop_schedule", set_val, 22) self.core.call_all("mainloop_quit_graceful") self.core.call_one('mainloop') self.assertEqual(self.val, 22) class AbstractTestPromise(unittest.TestCase): def get_plugin_name(self): """ Must be overloaded by subclasses """ assert() def setUp(self): self.core = Core(auto_load_dependencies=True) self.core.load(self.get_plugin_name()) self.core.init() self.alpha_called = False self.beta_called = False self.stop_called = False self.exc_raised = False self.idx = 0 def test_single(self): self.stop_called = False def stop(): self.stop_called = True p = promise.Promise(self.core, stop) p.schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertTrue(self.stop_called) def test_chain(self): self.alpha_called = -1 self.beta_called = -1 self.stop_called = -1 self.idx = 0 def alpha(): self.alpha_called = self.idx self.idx += 1 return "alpha" def beta(previous): self.assertEqual(previous, "alpha") self.beta_called = self.idx self.idx += 1 def stop(): self.stop_called = self.idx self.idx += 1 p = promise.Promise(self.core, alpha) p = p.then(beta) p = p.then(stop) p.schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertEqual(self.alpha_called, 0) self.assertEqual(self.beta_called, 1) self.assertEqual(self.stop_called, 2) def test_catch(self): self.alpha_called = False self.beta_called = False self.stop_called = False self.exc_raised = False def alpha(): self.alpha_called = True def beta(): self.beta_called = True raise Exception("paf") def stop(): self.stop_called = True def on_exc(exc): self.exc_raised = True p = promise.Promise(self.core, alpha) p = p.then(beta) p = p.then(stop) p = p.catch(on_exc) p.hide_caught_exceptions = True p.schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertTrue(self.alpha_called) self.assertTrue(self.beta_called) self.assertFalse(self.stop_called) self.assertTrue(self.exc_raised) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/paths/000077500000000000000000000000001417573700700251745ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/paths/__init__.py000066400000000000000000000000001417573700700272730ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/paths/xdg.py000066400000000000000000000027451417573700700263400ustar00rootroot00000000000000import os from .. import PluginBase class Plugin(PluginBase): def get_interfaces(self): return ['paths'] def get_deps(self): return [ { 'interface': 'app', 'defaults': ['openpaperwork_core.app'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_core.fs.python'] }, ] def paths_get_config_dir(self): config_dir = self.core.call_success( "fs_safe", os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) ) self.core.call_success("fs_mkdir_p", config_dir) if os.name == 'nt': # hide ~/.local on Windows self.core.call_success("fs_hide", config_dir) return config_dir def paths_get_data_dir(self): local_dir = os.path.expanduser("~/.local") data_dir = os.getenv("XDG_DATA_HOME", os.path.join(local_dir, "share")) data_dir = self.core.call_success("fs_safe", data_dir) app_name = self.core.call_success("app_get_fs_name") app_dir = self.core.call_success("fs_join", data_dir, app_name) self.core.call_success("fs_mkdir_p", app_dir) if os.name == 'nt': # hide ~/.local on Windows local_dir = self.core.call_success("fs_safe", local_dir) if self.core.call_success("fs_isdir", local_dir) is not None: self.core.call_success("fs_hide", local_dir) return app_dir paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/perfcheck/000077500000000000000000000000001417573700700260075ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/perfcheck/__init__.py000066400000000000000000000000001417573700700301060ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/perfcheck/log.py000066400000000000000000000014461417573700700271470ustar00rootroot00000000000000import logging import threading import time from .. import PluginBase LOGGER = logging.getLogger(__name__) MIN_TIME_MS = 200 class Plugin(PluginBase): def __init__(self): super().__init__() self.pending = {} def get_interfaces(self): return ['perfcheck'] def on_perfcheck_start(self, task_name): k = (task_name, threading.get_ident()) self.pending[k] = time.time() def on_perfcheck_stop(self, task_name, **extras): stop = time.time() k = (task_name, threading.get_ident()) start = self.pending.pop(k) if (stop - start) * 1000 >= MIN_TIME_MS: LOGGER.warning( "Task '%s' took %dms (> %dms) ! (%s)", task_name, (stop - start) * 1000, MIN_TIME_MS, extras ) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/pillow/000077500000000000000000000000001417573700700253635ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/pillow/__init__.py000066400000000000000000000000001417573700700274620ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/pillow/img.py000066400000000000000000000050531417573700700265140ustar00rootroot00000000000000import logging import PIL import PIL.Image from .. import ( PluginBase, promise ) LOGGER = logging.getLogger(__name__) class Plugin(PluginBase): FILE_EXTENSIONS = [ "bmp", "gif", "jpeg", "jpg", "png", "tiff", ] def get_interfaces(self): return [ 'page_img_size', 'pillow', ] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_core.fs.python'] }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_core.mainloop.asyncio'], }, { 'interface': 'thread', 'defaults': ['openpaperwork_core.thread.simple'], }, ] def _check_is_img(self, file_url): return file_url.rsplit(".", 1)[-1].lower() in self.FILE_EXTENSIONS def url_to_img_size(self, file_url): if not self._check_is_img(file_url): return None task = "url_to_img_size({})".format(file_url) self.core.call_all("on_perfcheck_start", task) with self.core.call_success("fs_open", file_url, mode='rb') as fd: img = PIL.Image.open(fd) size = img.size self.core.call_all("on_perfcheck_stop", task, size=size) return size def url_to_img_size_promise(self, file_url): if not self._check_is_img(file_url): return None return promise.ThreadedPromise( self.core, self.url_to_img_size, args=(file_url,) ) def url_to_pillow(self, file_url): if not self._check_is_img(file_url): return None task = "url_to_pillow({})".format(file_url) self.core.call_all("on_perfcheck_start", task) with self.core.call_success("fs_open", file_url, mode='rb') as fd: img = PIL.Image.open(fd) img.load() self.core.call_all("on_objref_track", img) size = img.size self.core.call_all("on_perfcheck_stop", task, size=size) return img def url_to_pillow_promise(self, file_url): return promise.ThreadedPromise( self.core, self.url_to_pillow, args=(file_url,) ) def pillow_to_url(self, img, file_url, format='JPEG', quality=0.75): if format != 'PNG': img = img.convert("RGB") with self.core.call_success("fs_open", file_url, mode='wb') as fd: return img.save(fd, format=format, quality=int(quality * 100)) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/pillow/util.py000066400000000000000000000012541417573700700267140ustar00rootroot00000000000000import PIL.ImageDraw from .. import PluginBase class Plugin(PluginBase): def get_interfaces(self): return ['pillow_util'] def pillow_add_border(self, img, color="#a6a5a4", width=1): """ Add a border of the specified color and width around a PIL image """ img_draw = PIL.ImageDraw.Draw(img) for line in range(0, width): img_draw.rectangle( [ (line, line), ( img.size[0] - 1 - line, img.size[1] - 1 - line ) ], outline=color ) return img paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/promise.py000066400000000000000000000174001417573700700261070ustar00rootroot00000000000000import logging import threading import traceback LOGGER = logging.getLogger(__name__) class BasePromise(object): def __init__( self, core, func=None, args=None, kwargs=None, parent=None, hide_caught_exceptions=False ): # we allow dummy Promise with not function provided. It allows to # write cleaner code in some cases. self.core = core self.func = func self.hide_caught_exceptions = hide_caught_exceptions if args is None: self.args = () else: self.args = args if kwargs is None: self.kwargs = {} else: self.kwargs = kwargs self.parent = parent self._then = [] self._catch = [] self.scheduled = False self.parent_promise_return = None self.created_by = traceback.extract_stack() def __str__(self): return "Promise<{}>({})".format(str(self.func), id(self)) def __repr__(self): return str(self) def then(self, callback, *args, **kwargs): if isinstance(callback, BasePromise): assert(args is None or len(args) <= 0) assert(kwargs is None or len(kwargs) <= 0) last_promise = callback while callback.parent is not None: callback = callback.parent next_promise = callback next_promise.parent = self else: next_promise = Promise( self.core, callback, args, kwargs, parent=self ) last_promise = next_promise self._then.append(next_promise) return last_promise def catch(self, callback, *args, **kwargs): self._catch.append((callback, args, kwargs)) return self def on_error(self, exc, hide_caught_exceptions=False): self.scheduled = False hide_caught_exceptions = ( hide_caught_exceptions or self.hide_caught_exceptions ) if len(self._catch) > 0: if hide_caught_exceptions: trace = lambda *args, **kwargs: None # NOQA: E731 else: trace = LOGGER.warning caught = "caught" elif len(self._then) > 0: for t in self._then: t.scheduled = False t.on_error(exc, hide_caught_exceptions) return else: trace = LOGGER.error caught = "uncaught" trace("=== %s exception in promise ===", caught, exc_info=exc) trace("promise.func=%s", self.func) trace("promise.args=%s", self.args) trace("promise.kwargs=%s", self.kwargs) trace("promise.parent=%s", self.parent) trace( "promise.parent_promise_return=%s", self.parent_promise_return ) trace("=== Promise was created by ===") for (idx, stack_el) in enumerate(self.created_by): trace( "%2d: %20s: L%5d: %s", idx, stack_el[0], stack_el[1], stack_el[2] ) if len(self._catch) > 0: for (c, args, kwargs) in self._catch: self.core.call_one( "mainloop_schedule", c, exc, *args, **kwargs ) return raise exc def _do(self, args): try: self.do(args) finally: self.scheduled = False def schedule(self, *args): scheduled = self.scheduled s = self while s.parent is not None: scheduled = scheduled or s.scheduled s.scheduled = True s = s.parent s.scheduled = True if scheduled: # a parent promise already scheduled --> we made sure to mark # all the children promises as scheduled, but there is no # need to actually schedule the parent. return if args == (): args = None self.core.call_one("mainloop_schedule", s._do, args) def wait(self): assert( # must never be called from main loop. threading.current_thread().ident != self.core.call_success("mainloop_get_thread_id") ) event = threading.Event() out = [None] def wakeup(r=None): out[0] = r event.set() self.then(event.set) event.wait() return out[0] class Promise(BasePromise): """ Executed in the main loop thread. Requires a plugin implementing the interface 'mainloop'. """ def do(self, parent_r=None): self.parent_promise_return = parent_r try: if self.func is None: our_r = None else: if parent_r is None: args = self.args else: args = (parent_r,) + self.args LOGGER.debug( "Promise: Begin: %s(%s, %s)", self.func, args, self.kwargs ) our_r = self.func(*args, **self.kwargs) LOGGER.debug( "Promise: End: %s(%s, %s)", self.func, args, self.kwargs ) for t in self._then: self.core.call_one("mainloop_schedule", t._do, our_r) except Exception as exc: self.on_error(exc) class DelayPromise(BasePromise): """ Promise adding delay between 2 other promises. Requires a plugin implementing the interface 'mainloop' """ def __init__(self, core, delay_s): super().__init__(core) self.delay_s = delay_s def __str__(self): return "DelayPromise({})".format(self.delay_s, id(self)) def _call_then(self, parent_r): for t in self._then: self.core.call_one("mainloop_schedule", t._do, parent_r) def do(self, parent_r=None): LOGGER.debug("Promise: delay: %fs", self.delay_s) self.core.call_one( "mainloop_schedule", self._call_then, parent_r, delay_s=self.delay_s ) class ThreadedPromise(BasePromise): """ Promise for which the provided callback will be run in another thread, leaving the main loop thread free to do other things. Requires a plugin implementing the interface 'mainloop' and a plugin implementing the interface 'thread'. IMPORTANT: This should ONLY be used for long-lasting tasks that cannot be split in small tasks (image processing, OCR, etc). The callback provided must be really careful regarding thread-safety. """ def __str__(self): return "ThreadedPromise<{}>({})".format(str(self.func), id(self)) def _threaded_do(self, parent_r): try: if parent_r is None: args = self.args else: args = (parent_r,) + self.args LOGGER.debug( "Threaded promise: Begin: %s(%s, %s)", self.func, args, self.kwargs ) our_r = self.func(*args, **self.kwargs) LOGGER.debug( "Threaded promise: end: %s(%s, %s)", self.func, args, self.kwargs ) for t in self._then: self.core.call_one("mainloop_schedule", t._do, our_r) except Exception as exc: self.core.call_one("mainloop_schedule", self.on_error, exc) def do(self, parent_r=None): self.parent_promise_return = parent_r try: if self.func is None: # no thread, we immediately schedule the next promises for t in self._then: self.core.call_one("mainloop_schedule", t._do, None) return self.core.call_one("thread_start", self._threaded_do, parent_r) except Exception as exc: self.on_error(exc) return paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/resources/000077500000000000000000000000001417573700700260675ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/resources/__init__.py000066400000000000000000000000001417573700700301660ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/resources/frozen.py000066400000000000000000000023541417573700700277500ustar00rootroot00000000000000""" Workaround for the fact that "import pkg_resources" never works when frozen with Msys2 + cx_freeze ... """ import logging import os import sys from .. import PluginBase LOGGER = logging.getLogger(__name__) class Plugin(PluginBase): PRIORITY = 100 def get_interfaces(self): return ['resources'] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_core.fs.python'], }, ] def resources_get_file(self, pkg, filename): if not getattr(sys, 'frozen', False): return None # cx_freeze: pkg_resources doesn't work (can't import it), so we keep # the data files beside the executable. path = os.path.join( os.path.dirname(sys.executable), "data", pkg.replace(".", os.path.sep), filename ) if not os.path.exists(path): LOGGER.warning( "Failed to find %s/%s (tried %s)", pkg, filename, path ) return None return self.core.call_success("fs_safe", path) def resources_get_dir(self, pkg, dirname): return self.resources_get_file(pkg, dirname) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/resources/setuptools.py000066400000000000000000000024501417573700700306630ustar00rootroot00000000000000import logging import os try: import pkg_resources PKG_RESOURCES_AVAILABLE = True except Exception: PKG_RESOURCES_AVAILABLE = False from .. import PluginBase LOGGER = logging.getLogger(__name__) class Plugin(PluginBase): def get_interfaces(self): return ['resources'] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_core.fs.python'], }, ] def resources_get_file(self, pkg, filename): if not PKG_RESOURCES_AVAILABLE: return try: path = pkg_resources.resource_filename(pkg, filename) except KeyError: LOGGER.warning( "Failed to find resource file %s/%s," " unknown to pkg_resources.", pkg, filename ) return None if not os.access(path, os.R_OK): LOGGER.warning( "Failed to find resource file %s/%s," " tried at path %s.", pkg, filename, path ) return None LOGGER.debug("%s:%s --> %s", pkg, filename, path) return self.core.call_success("fs_safe", path) def resources_get_dir(self, pkg, dirname): return self.resources_get_file(pkg, dirname) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/spatial/000077500000000000000000000000001417573700700255125ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/spatial/__init__.py000066400000000000000000000000001417573700700276110ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/spatial/rtree.py000066400000000000000000000163741417573700700272200ustar00rootroot00000000000000""" Spatial indexer that works in a way very similar to R-tree. Except for the way the nodes are split because I'm a lazy bastard and it's good enough. """ import math import logging import openpaperwork_core LOGGER = logging.getLogger(__name__) NB_CHILDREN_PER_NODES = 4 # must be a multiple of 2 because I'm a lazy bastard # Node: R-tree page (leaf or not) # Non-leaf: children are other nodes # Leaf: last R-tree node: children are tuples (box, obj) def compute_area(box): return abs((box[1][0] - box[0][0]) * (box[1][1] - box[0][1])) class Node(object): def __init__(self, box=None): self.leaf = True self.children = [] self.box = None self.area = 0 self.set_box(box) self.parent = None def compute_enlargment(self, box): enlarged = ( ( min(self.box[0][0], box[0][0]), min(self.box[0][1], box[0][1]), ), ( max(self.box[1][0], box[1][0]), max(self.box[1][1], box[1][1]), ), ) enlarged = compute_area(enlarged) return enlarged - self.area def set_box(self, box): self.box = box if box is None: # root node self.area = math.inf else: self.area = compute_area(box) def recompute_box(self, recurse=True): depth = 0 s = self while True: depth += 1 assert(depth < 512) if s.leaf: box = ( ( min(x[0][0][0] for x in s.children), min(x[0][0][1] for x in s.children), ), ( max(x[0][1][0] for x in s.children), max(x[0][1][1] for x in s.children), ), ) else: box = ( ( min(x.box[0][0] for x in s.children), min(x.box[0][1] for x in s.children), ), ( max(x.box[1][0] for x in s.children), max(x.box[1][1] for x in s.children), ), ) s.set_box(box) if not recurse or s.parent is None: break s = s.parent def is_full(self): assert(self.leaf) return len(self.children) >= NB_CHILDREN_PER_NODES @staticmethod def _pick_seed(boxes): """ Returns a pair of nodes that would be the most wasteful. (see Guttman Quadratic Split algorithm) """ assert(len(boxes) >= 2) if len(boxes) == 2: # minor optim return tuple(boxes) max_waste = -1 r = None for (a_idx, a) in enumerate(boxes): for b in boxes[a_idx + 1:]: new_rect = ( ( min(a[0][0][0], b[0][0][0]), min(a[0][0][1], b[0][0][1]), ), ( max(a[0][1][0], b[0][1][0]), max(a[0][1][1], b[0][1][1]), ) ) new_area = ( (new_rect[1][0] - new_rect[0][0]) * (new_rect[1][1] - new_rect[0][1]) ) waste = new_area - a[2] - b[2] if waste > max_waste or r is None: r = (a, b) assert(r is not None) return r def split(self): assert(self.leaf) our_children = self.children picked = [] while len(our_children) > 0: p = self._pick_seed(our_children) picked.append(p) for c in p: our_children.remove(c) node_a = Node() node_a.parent = self node_a.children = [b[0] for b in picked] node_a.recompute_box(recurse=False) node_b = Node() node_b.parent = self node_b.children = [b[1] for b in picked] node_b.recompute_box(recurse=False) self.children = [node_a, node_b] self.leaf = False def insert_box(self, box): assert(self.leaf) self.children.append(box) self.recompute_box() def __gt__(self, o): return False class RTreeSpatialIndexer(object): def __init__(self, boxes): self.root = Node() boxes = list(boxes) LOGGER.info("Loading %d boxes in rtree", len(boxes)) for (pos, obj) in boxes: self.insert(pos, obj) # self.print_stats() def print_stats(self): """ Not used at the moment, but useful to make sure the tree looks OK when debugging. """ nb_boxes = 0 nb_nodes = 0 max_depth = 0 to_examine = [(0, self.root)] while len(to_examine) > 0: nb_nodes += 1 (depth, n) = to_examine.pop(0) max_depth = max(max_depth, depth) if n.leaf: nb_boxes += len(n.children) else: for c in n.children: to_examine.append((depth + 1, c)) LOGGER.info( "Rtree: %d boxes, %d nodes, max depth: %d", nb_boxes, nb_nodes, max_depth ) def _choose_leaf(self, box_position, node): # keep picking up the child node that overlap as much of # box_position as possible return min({ (c.compute_enlargment(box_position), c) for c in node.children })[1] def _find_leaf(self, box_position, root=None): if root is None: n = self.root else: n = root while not n.leaf: n = self._choose_leaf(box_position, n) return n def insert(self, box_position, obj): new_child = (box_position, obj, compute_area(box_position)) leaf = self._find_leaf(box_position) if leaf.is_full(): leaf.split() leaf = self._find_leaf(box_position, leaf) leaf.insert_box(new_child) @staticmethod def is_in(box_position, pt_x, pt_y): if box_position[0][0] > pt_x: return False if box_position[0][1] > pt_y: return False if box_position[1][0] < pt_x: return False if box_position[1][1] < pt_y: return False return True def get_boxes(self, pt_x, pt_y): """ Returns all the boxes that contains the point (pt_x, pt_y) """ examined = 0 to_examine = [self.root] while len(to_examine) > 0: n = to_examine.pop(0) examined += 1 if not n.leaf: for c in n.children: if self.is_in(c.box, pt_x, pt_y): to_examine.append(c) else: for c in n.children: if self.is_in(c[0], pt_x, pt_y): yield (c[0], c[1]) # LOGGER.debug("Examined nodes: %d", examined) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'spatial_index' ] def spatial_indexer_get(self, boxes): return RTreeSpatialIndexer(boxes) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/spatial/simple.py000066400000000000000000000012371417573700700273600ustar00rootroot00000000000000import openpaperwork_core class SpatialIndexer(object): def __init__(self, boxes): self.boxes = list(boxes) def get_boxes(self, pt_x, pt_y): for (pos, obj) in self.boxes: if pos[0][0] > pt_x: continue if pos[0][1] > pt_y: continue if pos[1][0] < pt_x: continue if pos[1][1] < pt_y: continue yield (pos, obj) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'spatial_index' ] def spatial_indexer_get(self, boxes): return SpatialIndexer(boxes) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/spatial/tests.py000066400000000000000000000036441417573700700272350ustar00rootroot00000000000000import unittest from .. import Core class AbstractTest(unittest.TestCase): def get_plugin_name(self): """ Must be overloaded by subclasses """ assert() def setUp(self): self.core = Core(auto_load_dependencies=True) self.core.load(self.get_plugin_name()) self.core.init() def test_basic(self): boxes = [ (((0, 0), (10, 10)), "a"), (((10, 10), (50, 40)), "b"), (((25, 25), (100, 100)), "c"), ] indexer = self.core.call_success("spatial_indexer_get", boxes) self.assertEqual( list(indexer.get_boxes(5, 5)), [ (((0, 0), (10, 10)), "a"), ] ) self.assertEqual( list(indexer.get_boxes(5, 10)), [ (((0, 0), (10, 10)), "a"), ] ) self.assertEqual( list(indexer.get_boxes(10, 10)), [ (((0, 0), (10, 10)), "a"), (((10, 10), (50, 40)), "b"), ] ) self.assertEqual( list(indexer.get_boxes(25, 20)), [ (((10, 10), (50, 40)), "b"), ] ) self.assertEqual( list(indexer.get_boxes(75, 75)), [ (((25, 25), (100, 100)), "c"), ] ) def test_real(self): boxes = [ (((584, 360), (1097, 415)), "CROIX-ROUGE"), (((1126, 361), (1540, 428)), "FRANCAISE"), (((865, 430), (1006, 464)), "EQUIPES"), (((1028, 437), (1258, 462)), "SECOURISTES"), (((724, 675), (911, 718)), "Madame"), ] indexer = self.core.call_success("spatial_indexer_get", boxes) self.assertEqual( list(indexer.get_boxes(836, 701)), [ (((724, 675), (911, 718)), "Madame"), ] ) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/tests/000077500000000000000000000000001417573700700252175ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/tests/__init__.py000066400000000000000000000000001417573700700273160ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/tests/local_file.py000066400000000000000000000377201417573700700276730ustar00rootroot00000000000000import os import tempfile import time import unittest import openpaperwork_core class AbstractTestSafe(unittest.TestCase): def get_plugin_name(self): """ must be subclassed """ assert() def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load(self.get_plugin_name()) self.core.init() @unittest.skipUnless(os.name == 'posix', reason="Linux only") def test_linux(self): v = self.core.call_one('fs_safe', '/home/flesch jerome') self.assertEqual(v, "file:///home/flesch%20jerome") v = self.core.call_one('fs_safe', 'file:///home/flesch%20jerome') self.assertEqual(v, "file:///home/flesch%20jerome") @unittest.skipUnless(os.name == 'nt', reason="Windows only") def test_windows(self): v = self.core.call_one('fs_safe', 'c:\\Users\\flesch jerome') self.assertEqual(v, "file:///c:/Users/flesch%20jerome") v = self.core.call_one('fs_safe', '\\\\someserver\\someshare') self.assertEqual(v, "\\\\someserver\\someshare") class AbstractTestUnsafe(unittest.TestCase): def get_plugin_name(self): """ must be subclassed """ assert() def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load(self.get_plugin_name()) self.core.init() @unittest.skipUnless(os.name == 'posix', reason="Linux only") def test_linux(self): v = self.core.call_one('fs_unsafe', 'file:///home/flesch%20jerome') self.assertEqual(v, "/home/flesch jerome") v = self.core.call_one('fs_unsafe', 'file:///home/flesch jerome') self.assertEqual(v, "/home/flesch jerome") v = self.core.call_one('fs_unsafe', '/home/flesch jerome') self.assertEqual(v, "/home/flesch jerome") @unittest.skipUnless(os.name == 'nt', reason="Windows only") def test_windows(self): v = self.core.call_success( 'fs_unsafe', 'file://c:\\Users\\flesch%20jerome' ) self.assertEqual(v, "c:\\Users\\flesch jerome") v = self.core.call_one('fs_safe', '\\\\someserver\\someshare') self.assertEqual(v, "\\\\someserver\\someshare") class AbstractTestOpen(unittest.TestCase): def get_plugin_name(self): """ must be subclassed """ assert() def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load(self.get_plugin_name()) self.core.init() def test_read_binary(self): file_name = None with tempfile.NamedTemporaryFile(mode='wb', delete=False) as fd: file_name = fd.name fd.write(b"content") try: safe_file_name = self.core.call_one('fs_safe', file_name) with self.core.call_one( 'fs_open', safe_file_name, mode='rb' ) as fd: content = fd.read() self.assertEqual(content, b"content") finally: os.unlink(file_name) def test_write_binary(self): file_name = None with tempfile.NamedTemporaryFile(mode='wb', delete=False) as fd: file_name = fd.name try: safe_file_name = self.core.call_one('fs_safe', file_name) with self.core.call_one( 'fs_open', safe_file_name, mode='wb' ) as fd: fd.write(b"content\n") with open(file_name, 'rb') as fd: content = fd.read() self.assertEqual(content, b"content\n") finally: os.unlink(file_name) def test_read(self): file_name = None with tempfile.NamedTemporaryFile(mode='w', delete=False) as fd: file_name = fd.name fd.write("content") try: safe_file_name = self.core.call_one('fs_safe', file_name) with self.core.call_one('fs_open', safe_file_name, mode='r') as fd: content = fd.read() self.assertEqual(content, "content") finally: os.unlink(file_name) def test_write(self): file_name = None with tempfile.NamedTemporaryFile(mode='w', delete=False) as fd: file_name = fd.name try: safe_file_name = self.core.call_one('fs_safe', file_name) with self.core.call_one('fs_open', safe_file_name, mode='w') as fd: fd.write("content\n") with open(file_name, 'r') as fd: content = fd.read() self.assertEqual(content, "content\n") finally: os.unlink(file_name) class AbstractTestExists(unittest.TestCase): def get_plugin_name(self): """ must be subclassed """ assert() def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load(self.get_plugin_name()) self.core.init() def test_not_exist(self): self.assertEqual(self.core.call_one('fs_exists', "/whatever"), None) def test_exists(self): file_name = None with tempfile.NamedTemporaryFile(mode='w', delete=False) as fd: file_name = fd.name fd.write("content") try: safe_file_name = self.core.call_one('fs_safe', file_name) self.assertTrue(self.core.call_one('fs_exists', safe_file_name)) finally: os.unlink(file_name) class AbstractTestListDir(unittest.TestCase): def get_plugin_name(self): """ must be subclassed """ assert() def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load(self.get_plugin_name()) self.core.init() self.dirname = tempfile.mkdtemp() self.uri_dirname = self.core.call_one('fs_safe', self.dirname) with open(os.path.join(self.dirname, 'test1.txt'), 'w') as fd: fd.write('test1') with open(os.path.join(self.dirname, 'test2.txt'), 'w') as fd: fd.write('test2') os.mkdir(os.path.join(self.dirname, 'test3')) with open(os.path.join(self.dirname, 'test3', 'test4.txt'), 'w') as fd: fd.write('test4') def tearDown(self): # check fs_rm_rf at the same time self.core.call_one("fs_rm_rf", self.uri_dirname, trash=False) self.assertFalse(os.path.exists(self.dirname)) def test_listdir(self): files = list(self.core.call_one('fs_listdir', self.uri_dirname)) files.sort() self.assertEqual(files, [ self.uri_dirname + '/test1.txt', self.uri_dirname + '/test2.txt', self.uri_dirname + '/test3' ]) def test_recurse(self): files = list(self.core.call_one('fs_recurse', self.uri_dirname)) files.sort() self.assertEqual(files, [ self.uri_dirname + '/test1.txt', self.uri_dirname + '/test2.txt', self.uri_dirname + '/test3/test4.txt' ]) class AbstractTestRename(unittest.TestCase): def get_plugin_name(self): """ must be subclassed """ assert() def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load(self.get_plugin_name()) self.core.init() def test_rename(self): src_file_name = None dst_file_name = None with tempfile.NamedTemporaryFile(mode='w', delete=False) as fd: src_file_name = fd.name fd.write("content") with tempfile.NamedTemporaryFile(mode='w', delete=False) as fd: dst_file_name = fd.name fd.write("content") os.unlink(dst_file_name) try: safe_src_file_name = self.core.call_one('fs_safe', src_file_name) safe_dst_file_name = self.core.call_one('fs_safe', dst_file_name) self.assertTrue(os.path.exists(src_file_name)) self.assertFalse(os.path.exists(dst_file_name)) self.core.call_one( 'fs_rename', safe_src_file_name, safe_dst_file_name ) self.assertFalse(os.path.exists(src_file_name)) self.assertTrue(os.path.exists(dst_file_name)) finally: if os.path.exists(src_file_name): os.unlink(src_file_name) if os.path.exists(dst_file_name): os.unlink(dst_file_name) class AbstractTestUnlink(unittest.TestCase): def get_plugin_name(self): """ must be subclassed """ assert() def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load(self.get_plugin_name()) self.core.init() def test_unlink(self): file_name = None with tempfile.NamedTemporaryFile(mode='w', delete=False) as fd: file_name = fd.name fd.write("content") try: safe_file_name = self.core.call_one('fs_safe', file_name) self.assertTrue(os.path.exists(file_name)) self.core.call_one('fs_unlink', safe_file_name, trash=False) self.assertFalse(os.path.exists(file_name)) finally: if os.path.exists(file_name): os.unlink(file_name) class AbstractTestGetMtime(unittest.TestCase): def get_plugin_name(self): """ must be subclassed """ assert() def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load(self.get_plugin_name()) self.core.init() def test_get_mtime(self): now = time.time() file_name = None with tempfile.NamedTemporaryFile(mode='w', delete=False) as fd: file_name = fd.name fd.write("content") try: safe_file_name = self.core.call_one('fs_safe', file_name) mtime = self.core.call_one('fs_get_mtime', safe_file_name) self.assertTrue(int(now) <= mtime) self.assertTrue(mtime <= now + 2) time.sleep(2) # :-( with open(file_name, 'a') as fd: fd.write("content") new_mtime = self.core.call_one('fs_get_mtime', safe_file_name) self.assertTrue(int(now) <= new_mtime) self.assertTrue(mtime < new_mtime) finally: os.unlink(file_name) class AbstractTestGetsize(unittest.TestCase): def get_plugin_name(self): """ must be subclassed """ assert() def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load(self.get_plugin_name()) self.core.init() def test_getsize(self): file_name = None with tempfile.NamedTemporaryFile(mode='w', delete=False) as fd: file_name = fd.name fd.write("content") # 7 bytes try: safe_file_name = self.core.call_one('fs_safe', file_name) s = self.core.call_one('fs_getsize', safe_file_name) self.assertEqual(s, 7) finally: os.unlink(file_name) class AbstractTestIsdir(unittest.TestCase): def get_plugin_name(self): """ must be subclassed """ assert() def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load(self.get_plugin_name()) self.core.init() def test_isdir_true(self): dirname = tempfile.mkdtemp() uri_dirname = self.core.call_one('fs_safe', dirname) try: self.assertTrue(self.core.call_one("fs_isdir", uri_dirname)) finally: # check fs_rm_rf at the same time self.core.call_one("fs_rm_rf", uri_dirname, trash=False) self.assertFalse(os.path.exists(dirname)) def test_isdir_false(self): file_name = None with tempfile.NamedTemporaryFile(mode='w', delete=False) as fd: file_name = fd.name fd.write("content") try: safe_file_name = self.core.call_one('fs_safe', file_name) self.assertFalse(self.core.call_one('fs_isdir', safe_file_name)) finally: os.unlink(file_name) class AbstractTestCopy(unittest.TestCase): def get_plugin_name(self): """ must be subclassed """ assert() def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load(self.get_plugin_name()) self.core.init() def test_copy(self): src_file_name = None dst_file_name = None with tempfile.NamedTemporaryFile(mode='w', delete=False) as fd: src_file_name = fd.name fd.write("content") with tempfile.NamedTemporaryFile(mode='w', delete=False) as fd: dst_file_name = fd.name fd.write("xxxxxxx") os.unlink(dst_file_name) try: safe_src_file_name = self.core.call_one('fs_safe', src_file_name) safe_dst_file_name = self.core.call_one('fs_safe', dst_file_name) self.assertTrue(os.path.exists(src_file_name)) self.assertFalse(os.path.exists(dst_file_name)) self.core.call_one( 'fs_copy', safe_src_file_name, safe_dst_file_name ) self.assertTrue(os.path.exists(src_file_name)) self.assertTrue(os.path.exists(dst_file_name)) with open(dst_file_name, 'r') as fd: content = fd.read() self.assertEqual(content, 'content') finally: if os.path.exists(src_file_name): os.unlink(src_file_name) if os.path.exists(dst_file_name): os.unlink(dst_file_name) class AbstractTestMkdirP(unittest.TestCase): def get_plugin_name(self): """ must be subclassed """ assert() def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load(self.get_plugin_name()) self.core.init() def test_mkdir_p(self): dirname = tempfile.mkdtemp() uri_dirname = self.core.call_one('fs_safe', dirname) try: new_dirs = uri_dirname + "/testA/testB/testC" self.core.call_one("fs_mkdir_p", new_dirs) self.assertTrue(os.path.exists(os.path.join( dirname, "testA", "testB", "testC" ))) finally: # check fs_rm_rf at the same time self.core.call_one("fs_rm_rf", uri_dirname, trash=False) self.assertFalse(os.path.exists(dirname)) class AbstractTestBasename(unittest.TestCase): def get_plugin_name(self): """ must be subclassed """ assert() def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load(self.get_plugin_name()) self.core.init() def test_basename(self): out = self.core.call_success("fs_basename", "file:///a/b/c.txt") self.assertEqual(out, "c.txt") out = self.core.call_success("fs_basename", "file:///c.txt") self.assertEqual(out, "c.txt") out = self.core.call_success("fs_basename", "memory://camion.txt") self.assertEqual(out, "camion.txt") class AbstractTestTemp(unittest.TestCase): def get_plugin_name(self): """ must be subclassed """ assert() def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load(self.get_plugin_name()) self.core.init() def test_mktemp(self): (tmp_url, tmp_fd) = self.core.call_success( "fs_mktemp", prefix="test", suffix=".txt", mode="w" ) with tmp_fd: tmp_fd.write("TEST\n") self.assertNotEqual( self.core.call_success("fs_exists", tmp_url), None ) self.core.call_all("on_quit") self.assertEqual( self.core.call_success("fs_exists", tmp_url), None ) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/thread/000077500000000000000000000000001417573700700253245ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/thread/__init__.py000066400000000000000000000016711417573700700274420ustar00rootroot00000000000000import logging LOGGER = logging.getLogger(__name__) class Task(object): def __init__(self, core, func, args, kwargs): self.core = core self.func = func self.args = args self.kwargs = kwargs # The mainloop can't track other threads, but if there is # a graceful shutdown waiting, we don't want it to stop the main # loop before our thread is done. # --> increment mainloop ref counter before core.call_all("mainloop_ref", self) def __str__(self): return "Task<{}>({}, {})".format(self.func, self.args, self.kwargs) def __repr__(self): return str(self) def do(self): try: self.func(*self.args, **self.kwargs) except Exception as exc: LOGGER.error( "==== UNCAUGHT EXCEPTION IN THREAD ===", exc_info=exc ) finally: self.core.call_all("mainloop_unref", self) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/thread/pool.py000066400000000000000000000034011417573700700266450ustar00rootroot00000000000000import logging import multiprocessing import queue import threading from . import Task from .. import PluginBase LOGGER = logging.getLogger(__name__) class Thread(threading.Thread): def __init__(self, plugin, thread_id): super().__init__(name="paperwork_thread_{}".format(thread_id)) self.daemon = True self.task_queue = plugin.queue self.core = plugin.core self.running = True def run(self): LOGGER.info("Thread %s ready", self.name) while True: task = self.task_queue.get() if task is None: break task.do() LOGGER.info("Thread %s stopped", self.name) class Plugin(PluginBase): def __init__(self): super().__init__() self.queue = queue.Queue() self.pool = None def get_interfaces(self): return ['thread'] def get_deps(self): return [ { 'interface': 'mainloop', 'defaults': ['openpaperwork_core.mainloop.asyncio'], }, ] def on_mainloop_start(self): if self.pool is None: self.pool = [ Thread(self, x) for x in range(0, max(4, multiprocessing.cpu_count())) ] for t in self.pool: t.start() def on_mainloop_quit(self): if self.pool is not None: for t in self.pool: self.queue.put(None) # in case the mainloop is restarted later: self.queue = queue.Queue() self.pool = None def thread_start(self, func, *args, **kwargs): if self.pool is None: self.on_mainloop_start() task = Task(self.core, func, args, kwargs) self.queue.put(task) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/thread/simple.py000066400000000000000000000013071417573700700271700ustar00rootroot00000000000000import threading from . import Task from .. import PluginBase class Plugin(PluginBase): """ Simply create a thread for each task to run. Less efficient than a thread pool, but may be useful for testing or debugging. """ def get_interfaces(self): return ['thread'] def get_deps(self): return [ { 'interface': 'mainloop', 'defaults': ['openpaperwork_core.mainloop.asyncio'], }, ] def thread_start(self, func, *args, **kwargs): task = Task(self.core, func, args, kwargs) thread = threading.Thread(target=task.do) thread.daemon = True thread.start() return True paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/thread/tests.py000066400000000000000000000042631417573700700270450ustar00rootroot00000000000000import threading import unittest from .. import (Core, PluginBase) class AbstractTestThread(unittest.TestCase): def get_plugin_name(self): """ Must be overloaded by subclasses """ assert() def setUp(self): class DummyMainloop(object): class Plugin(PluginBase): def get_interfaces(s): return ['mainloop'] def mainloop_ref(s, r): pass def mainloop_unref(s, r): pass self.core = Core(auto_load_dependencies=True) self.core._load_module("dummy_mainloop", DummyMainloop()) self.core.load(self.get_plugin_name()) self.core.init() def test_basic(self): out = { 'task_a_done': False, 'task_b_done': False, } sem = threading.Semaphore(value=0) def task_a(): out['task_a_done'] = True sem.release() def task_b(): out['task_b_done'] = True sem.release() self.core.call_all("on_mainloop_start") self.core.call_one("thread_start", task_a) self.core.call_one("thread_start", task_b) for _ in range(0, 2): sem.acquire() self.assertTrue(out['task_a_done']) self.assertTrue(out['task_b_done']) self.core.call_all("on_mainloop_quit") def test_mainloop_restart(self): # mainloop can be stopped and started again many times out = {} sem = threading.Semaphore(value=0) def task_a(): out['task_a_done'] = True sem.release() out['task_a_done'] = False self.core.call_all("on_mainloop_start") self.core.call_one("thread_start", task_a) for _ in range(0, 1): sem.acquire() self.assertTrue(out['task_a_done']) self.core.call_all("on_mainloop_quit") out['task_a_done'] = False self.core.call_all("on_mainloop_start") self.core.call_one("thread_start", task_a) for _ in range(0, 1): sem.acquire() self.assertTrue(out['task_a_done']) self.core.call_all("on_mainloop_quit") paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/uncaught_exception.py000066400000000000000000000026501417573700700303260ustar00rootroot00000000000000import sys from . import PluginBase class Plugin(PluginBase): def __init__(self): super().__init__() self.original_hook = sys.excepthook def get_interfaces(self): return ['uncaught_exception'] def get_deps(self): return [ { 'interface': 'mainloop', 'defaults': ['openpaperwork_core.mainloop.asyncio'], }, ] def init(self, core): super().init(core) sys.excepthook = self._on_uncaught_exception def _on_uncaught_exception(self, exc_type, exc_value, exc_tb): exc_info = (exc_type, exc_value, exc_tb) try: self.core.call_one( "mainloop_execute", self._broadcast_exception, exc_info ) finally: if getattr(sys, 'frozen', False): # Assumes that cx_freeze has put a specific handler # for uncatched exceptions (popup and stuff) self.original_hook(exc_type, exc_value, exc_tb) def _broadcast_exception(self, exc_info): # make sure we don't loop sys.excepthook = self.original_hook try: nb = self.core.call_all("on_uncaught_exception", exc_info) if nb <= 0: # no log handler yet --> switch back to default self.original_hook(*exc_info) finally: sys.excepthook = self._on_uncaught_exception paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/urls.py000066400000000000000000000015201417573700700254120ustar00rootroot00000000000000from . import PluginBase class Plugin(PluginBase): def get_interfaces(self): return ['urls'] def url_args_join(self, *args, **kwargs): out = "".join(args) if len(kwargs) <= 0: return out first = True for (k, v) in sorted(list(kwargs.items())): if v is None: continue if first: out += "#" first = False else: out += "&" out += "{}={}".format(k, v) return out def url_args_split(self, url): if "#" not in url: return (url, {}) (base, kwargs) = url.split("#", 1) kwargs = kwargs.split("&") out = {} for kwarg in kwargs: (k, v) = kwarg.split("=", 1) out[k] = v return (base, out) paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/work_queue/000077500000000000000000000000001417573700700262435ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/work_queue/__init__.py000066400000000000000000000000001417573700700303420ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/src/openpaperwork_core/work_queue/default.py000066400000000000000000000110361417573700700302420ustar00rootroot00000000000000import logging import heapq import threading import traceback from .. import PluginBase LOGGER = logging.getLogger(__name__) class Task(object): def __init__(self, work_queue, priority, insert_number, promise): self.work_queue = work_queue self.priority = priority self.insert_number = insert_number self.promise = promise self.active = True self.created_by = traceback.extract_stack() def _on_error(self, exc, hide_error): if not hide_error: LOGGER.error("=== Promise was queued by ===") for (idx, stack_el) in enumerate(self.created_by): LOGGER.error( "%2d: %20s: L%5d: %s", idx, stack_el[0], stack_el[1], stack_el[2] ) self.work_queue._run_next_promise_locked() raise exc def __lt__(self, o): if self.priority < o.priority: return True if self.priority > o.priority: return False if self.insert_number < o.insert_number: return True return False class WorkQueue(object): def __init__(self, name, stop_on_quit, hide_uncatched): self.insert_number = 0 self.name = name self.lock = threading.RLock() self.queue = [] self.all_tasks = {} self.running = False self.stop_on_quit = stop_on_quit self.hide_uncatched = hide_uncatched def add_promise(self, promise, priority=0): self.insert_number += 1 task = Task(self, -1 * priority, self.insert_number, promise) with self.lock: heapq.heappush(self.queue, task) assert( promise not in self.all_tasks or not self.all_tasks[promise].active ) self.all_tasks[promise] = task if not self.running: self._run_next_promise() def _run_next_promise(self): self.running = True try: task = None while task is None or not task.active: task = heapq.heappop(self.queue) if task.active: self.all_tasks.pop(task.promise) except IndexError: self.running = False return promise = task.promise.then(self._run_next_promise_locked) promise.catch(task._on_error, self.hide_uncatched) promise.schedule() def _run_next_promise_locked(self, *args, **kwargs): with self.lock: self._run_next_promise() def cancel(self, promise): try: with self.lock: task = self.all_tasks[promise] task.active = False except KeyError: LOGGER.debug( "Cannot cancel promise [%s]. (already running ?)", promise ) def cancel_all(self): # reset the queue with self.lock: self.queue = [] self.all_tasks = {} class Plugin(PluginBase): def __init__(self): self.queues = {} def get_interfaces(self): return ['work_queue'] def get_deps(self): return [ { 'interface': 'mainloop', 'defaults': ['openpaperwork_core.mainloop.asyncio'], }, ] def work_queue_create( self, queue_name, stop_on_quit=False, hide_uncatched=False): LOGGER.debug( "Creating work queue [%s] (stop_on_quit=%s)", queue_name, stop_on_quit ) self.queues[queue_name] = WorkQueue( queue_name, stop_on_quit, hide_uncatched ) return True def work_queue_add_promise(self, queue_name, promise, priority=0): if queue_name not in self.queues: return None self.queues[queue_name].add_promise(promise, priority) return True def work_queue_cancel(self, queue_name, promise): if queue_name not in self.queues: return None self.queues[queue_name].cancel(promise) return True def work_queue_cancel_all(self, queue_name): if queue_name not in self.queues: return None self.queues[queue_name].cancel_all() return True def mainloop_quit(self): # violent quit (does it ever happen ?) for queue in self.queues.values(): queue.cancel_all() def mainloop_quit_graceful(self): for queue in self.queues.values(): if queue.stop_on_quit: queue.cancel_all() def on_quit(self): self.mainloop_quit_graceful() paperwork-2.1.1/openpaperwork-core/tests/000077500000000000000000000000001417573700700205245ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/__init__.py000066400000000000000000000000001417573700700226230ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/beacon/000077500000000000000000000000001417573700700217535ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/beacon/__init__.py000066400000000000000000000000001417573700700240520ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/beacon/tests_stats.py000066400000000000000000000065711417573700700247160ustar00rootroot00000000000000import cgi import datetime import http import http.server import json import unittest import threading import time import urllib import openpaperwork_core class TestStats(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) class FakeAppModule(object): class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['app'] def app_get_name(self): return "Paperwork" def app_get_fs_name(self): return "paperwork2" def app_get_version(self): return "2.1" self.core._load_module("fake_app", FakeAppModule()) self.core.load("openpaperwork_core.config.fake") self.core.load("openpaperwork_core.beacon.stats") self.config = self.core.get_by_name("openpaperwork_core.config.fake") self.received = [] self.stats_sent = False def test_send_stats(self): self.received = [] class TestRequestHandler(http.server.BaseHTTPRequestHandler): def do_POST(s): (ctype, pdict) = cgi.parse_header( s.headers['Content-Type'] ) if ctype == 'multipart/form-data': data = cgi.parse_multipart(s.rfile, pdict) elif ctype == 'application/x-www-form-urlencoded': length = int(s.headers['Content-Length']) data = urllib.parse.parse_qs( s.rfile.read(length), keep_blank_values=1 ) else: data = {} self.received.append( (s.path, json.loads(data[b'statistics'][0])) ) s.send_response(200) s.send_header('Content-type', 'text/html') s.end_headers() s.wfile.write(b"

OK

") with http.server.HTTPServer(('', 0), TestRequestHandler) as h: self.config.settings = { "send_statistics": True, "uuid": 1245, "statistics_last_run": datetime.date(1995, 1, 1), "statistics_protocol": "http", "statistics_server": "127.0.0.1:{}".format(h.server_port), } threading.Thread(target=h.handle_request).start() time.sleep(0.1) class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): def stats_get(self, stats): stats['nb_documents'] += 122 stats['truck'] = 42 def on_stats_sent(s): self.stats_sent = True self.core._load_module( "fake_module", FakeModule() ) self.core.init() self.core.call_all("mainloop_quit_graceful") self.core.call_one('mainloop') self.assertTrue(self.stats_sent) self.assertEqual(len(self.received), 1) self.assertEqual(self.received[0][0], "/beacon/post_statistics") self.assertEqual(self.received[0][1]['uuid'], 1245) self.assertEqual(self.received[0][1]['nb_documents'], 122) self.assertEqual(self.received[0][1]['truck'], 42) paperwork-2.1.1/openpaperwork-core/tests/beacon/tests_sysinfo.py000066400000000000000000000022541417573700700252440ustar00rootroot00000000000000import unittest import openpaperwork_core class TestSysinfo(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) class FakeAppModule(object): class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['app'] def app_get_name(self): return "Paperwork" def app_get_fs_name(self): return "paperwork2" def app_get_version(self): return "2.1" self.core._load_module("fake_app", FakeAppModule()) self.core.load("openpaperwork_core.beacon.sysinfo") self.core.init() def test_get(self): # just go through the code to make sure it actually runs correctly # (we cannot check the output since it's system-dependant) out = {} self.core.call_all("stats_get", out) self.assertIn('os_name', out) self.assertIn('platform_architecture', out) self.assertIn('platform_processor', out) self.assertIn('platform_distribution', out) self.assertIn('cpu_count', out) paperwork-2.1.1/openpaperwork-core/tests/cmd/000077500000000000000000000000001417573700700212675ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/cmd/__init__.py000066400000000000000000000000001417573700700233660ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/cmd/tests_config.py000066400000000000000000000117511417573700700243350ustar00rootroot00000000000000import argparse import unittest import openpaperwork_core class MockConfigBackendModule(object): """ Plugin paperwork_backend.config uses openpaperwork.config_file. This mock mocks openpaperwork.config_file so we can test paperwork_backend.config.file """ class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.calls = [] self.returns = {} self.config_backend_load = ( lambda *args, **kwargs: self._handle_call('config_backend_load', args, kwargs) ) self.config_backend_save = ( lambda *args, **kwargs: self._handle_call('config_backend_save', args, kwargs) ) self.config_backend_load_plugins = ( lambda *args, **kwargs: self._handle_call('config_backend_load_plugins', args, kwargs) ) self.config_backend_add_plugin = ( lambda *args, **kwargs: self._handle_call('config_backend_add_plugin', args, kwargs) ) self.config_backend_remove_plugin = ( lambda *args, **kwargs: self._handle_call('config_backend_remove_plugin', args, kwargs) ) self.config_backend_put = ( lambda *args, **kwargs: self._handle_call('config_backend_put', args, kwargs) ) self.config_backend_get = ( lambda *args, **kwargs: self._handle_call('config_backend_get', args, kwargs) ) self.config_backend_add_observer = ( lambda *args, **kwargs: self._handle_call('config_backend_add_observer', args, kwargs) ) self.config_backend_remove_observer = ( lambda *args, **kwargs: self._handle_call( 'config_backend_remove_observer', args, kwargs ) ) self.config_backend_list_active_plugins = ( lambda *args, **kwargs: self._handle_call( 'config_backend_list_active_plugins', args, kwargs ) ) def _handle_call(self, func, args, kwargs): self.calls.append((func, args, kwargs)) if func not in self.returns: return None r = self.returns[func].pop(0) if len(self.returns[func]) <= 0: self.returns.pop(func) return r def get_interfaces(self): return ["config_backend"] class TestConfig(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core._load_module( "openpaperwork_core.config.backend.configparser", MockConfigBackendModule() ) self.core.load("openpaperwork_core.cmd.config") self.core.init() setting = self.core.call_success( "config_build_simple", "Global", "WorkDirectory", lambda: "file:///home/toto/papers" ) self.core.call_all("config_register", "workdir", setting) def test_get_put(self): self.core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).returns = { 'config_backend_put': [None], 'config_backend_save': [None], 'config_backend_get': ['file:///pouet/path'] } parser = argparse.ArgumentParser() cmd_parser = parser.add_subparsers( help='command', dest='command', required=True ) self.core.call_all("cmd_complete_argparse", cmd_parser) args = parser.parse_args( ['config', 'put', 'workdir', 'str', 'file:///pouet/path'] ) self.core.call_all("cmd_set_interactive", False) r = self.core.call_success("cmd_run", args) self.assertTrue(r) self.assertEqual( self.core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).calls, [ ( 'config_backend_put', ('Global', "WorkDirectory", "file:///pouet/path"), {} ), ('config_backend_save', (), {}), ] ) args = parser.parse_args(['config', 'get', 'workdir']) r = self.core.call_success("cmd_run", args) self.assertEqual(r, {"workdir": "file:///pouet/path"}) self.assertEqual( self.core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).calls, [ ( 'config_backend_put', ('Global', "WorkDirectory", "file:///pouet/path"), {} ), ('config_backend_save', (), {}), # it uses the cache for the get # --> no call to config_backend_get ] ) paperwork-2.1.1/openpaperwork-core/tests/cmd/tests_plugins.py000066400000000000000000000201041417573700700245410ustar00rootroot00000000000000import argparse import unittest import openpaperwork_core class MockConfigBackendModule(object): """ Plugin paperwork_backend.config uses openpaperwork.config_file. This mock mocks openpaperwork.config_file so we can test paperwork_backend.config.file """ class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.calls = [] self.returns = {} self.config_backend_load = ( lambda *args, **kwargs: self._handle_call('config_backend_load', args, kwargs) ) self.config_backend_save = ( lambda *args, **kwargs: self._handle_call('config_backend_save', args, kwargs) ) self.config_backend_load_plugins = ( lambda *args, **kwargs: self._handle_call('config_backend_load_plugins', args, kwargs) ) self.config_backend_add_plugin = ( lambda *args, **kwargs: self._handle_call('config_backend_add_plugin', args, kwargs) ) self.config_backend_remove_plugin = ( lambda *args, **kwargs: self._handle_call('config_backend_remove_plugin', args, kwargs) ) self.config_backend_put = ( lambda *args, **kwargs: self._handle_call('config_backend_put', args, kwargs) ) self.config_backend_get = ( lambda *args, **kwargs: self._handle_call('config_backend_get', args, kwargs) ) self.config_backend_add_observer = ( lambda *args, **kwargs: self._handle_call('config_backend_add_observer', args, kwargs) ) self.config_backend_remove_observer = ( lambda *args, **kwargs: self._handle_call( 'config_backend_remove_observer', args, kwargs ) ) self.config_backend_list_active_plugins = ( lambda *args, **kwargs: self._handle_call( 'config_backend_list_active_plugins', args, kwargs ) ) def _handle_call(self, func, args, kwargs): self.calls.append((func, args, kwargs)) if func not in self.returns: return None r = self.returns[func].pop(0) if len(self.returns[func]) <= 0: self.returns.pop(func) return r def get_interfaces(self): return ["config_backend"] class TestConfig(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core._load_module( "openpaperwork_core.config.backend.configparser", MockConfigBackendModule() ) self.core.load("openpaperwork_core.cmd.plugins") self.core.init() setting = self.core.call_success( "config_build_simple", "Global", "WorkDirectory", lambda: "file:///home/toto/papers" ) self.core.call_all("config_register", "workdir", setting) def test_add_remove_list_plugin(self): self.core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).returns = { 'config_backend_load': [None], 'config_backend_load_plugins': [None], 'config_backend_add_plugin': [None], 'config_backend_list_active_plugins': [ ['plugin_a', 'plugin_b', 'plugin_c'], ['plugin_a', 'plugin_c'] ], 'config_backend_remove_plugin': [None], 'config_backend_save': [None], } self.core.call_all('config_load') self.core.call_all( 'config_load_plugins', 'paperwork-shell', default_plugins=['pouet'] ) parser = argparse.ArgumentParser() cmd_parser = parser.add_subparsers( help='command', dest='command', required=True ) self.core.call_all("cmd_complete_argparse", cmd_parser) args = parser.parse_args( ['plugins', 'add', 'plugin_c', '--no_auto'] ) self.core.call_all("cmd_set_interactive", False) r = self.core.call_success("cmd_run", args) self.assertTrue(r) self.assertEqual( self.core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).calls, [ ('config_backend_load', ('openpaperwork_core',), {}), ( 'config_backend_load_plugins', ('paperwork-shell', ['pouet']), {} ), ( 'config_backend_add_plugin', ('paperwork-shell', 'plugin_c'), {} ), ('config_backend_save', (), {}), ] ) args = parser.parse_args(['plugins', 'list']) r = self.core.call_success("cmd_run", args) self.assertEqual(sorted(r), ['plugin_a', 'plugin_b', 'plugin_c']) self.assertEqual( self.core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).calls, [ ('config_backend_load', ('openpaperwork_core',), {}), ( 'config_backend_load_plugins', ('paperwork-shell', ['pouet']), {} ), ( 'config_backend_add_plugin', ('paperwork-shell', 'plugin_c'), {} ), ('config_backend_save', (), {}), ( 'config_backend_list_active_plugins', ('paperwork-shell',), {} ) ] ) args = parser.parse_args( ['plugins', 'remove', 'plugin_b', '--no_auto'] ) r = self.core.call_success("cmd_run", args) self.assertTrue(r) self.assertEqual( self.core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).calls, [ ('config_backend_load', ('openpaperwork_core',), {}), ( 'config_backend_load_plugins', ('paperwork-shell', ['pouet']), {} ), ( 'config_backend_add_plugin', ('paperwork-shell', 'plugin_c'), {} ), ('config_backend_save', (), {}), ( 'config_backend_list_active_plugins', ('paperwork-shell',), {} ), ( 'config_backend_remove_plugin', ('paperwork-shell', 'plugin_b'), {} ), ('config_backend_save', (), {}), ] ) args = parser.parse_args(['plugins', 'list']) r = self.core.call_success("cmd_run", args) self.assertEqual(sorted(r), ['plugin_a', 'plugin_c']) self.assertEqual( self.core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).calls, [ ('config_backend_load', ('openpaperwork_core',), {}), ( 'config_backend_load_plugins', ('paperwork-shell', ['pouet']), {} ), ( 'config_backend_add_plugin', ('paperwork-shell', 'plugin_c'), {} ), ('config_backend_save', (), {}), ( 'config_backend_list_active_plugins', ('paperwork-shell',), {} ), ( 'config_backend_remove_plugin', ('paperwork-shell', 'plugin_b'), {} ), ('config_backend_save', (), {}), ( 'config_backend_list_active_plugins', ('paperwork-shell',), {} ) ] ) paperwork-2.1.1/openpaperwork-core/tests/config/000077500000000000000000000000001417573700700217715ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/config/__init__.py000066400000000000000000000000001417573700700240700ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/config/backend/000077500000000000000000000000001417573700700233605ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/config/backend/__init__.py000066400000000000000000000000001417573700700254570ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/config/backend/tests_configparser.py000066400000000000000000000230331417573700700276370ustar00rootroot00000000000000import datetime import tempfile import unittest import unittest.mock import openpaperwork_core class TestReadWrite(unittest.TestCase): def test_simple_getset(self): core = openpaperwork_core.Core(auto_load_dependencies=True) core.load('openpaperwork_core.config.backend.configparser') core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).base_path = "file://" + tempfile.mkdtemp( prefix='openpaperwork_core_config_tests' ) try: core.init() core.call_all( 'config_backend_put', 'test_section', 'test_key', 'test_value' ) v = core.call_one('config_backend_get', 'test_section', 'test_key') self.assertEqual(v, 'test_value') v = core.call_one( 'config_backend_get', 'wrong_section', 'test_key', 'default' ) self.assertEqual(v, 'default') self.assertIsNone( core.call_success( 'config_backend_get', 'test_section', 'wrong_key' ) ) core.call_all('config_add_plugin', 'some_opt', 'some_test_module') finally: core.call_success("fs_rm_rf", core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).base_path, trash=False) def test_no_config_file(self): core = openpaperwork_core.Core(auto_load_dependencies=True) core.load('openpaperwork_core.config.backend.configparser') core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).base_path = "file://" + tempfile.mkdtemp( prefix='openpaperwork_core_config_tests' ) try: core.init() # must not throw an exception core.call_all('config_backend_load', 'openpaperwork_test') finally: core.call_success("fs_rm_rf", core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).base_path, trash=False) def test_simple_readwrite(self): core = openpaperwork_core.Core(auto_load_dependencies=True) core.load('openpaperwork_core.config.backend.configparser') core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).base_path = "file://" + tempfile.mkdtemp( prefix='openpaperwork_core_config_tests' ) try: core.init() core.call_all( 'config_backend_put', 'test_section', 'test_key', 'test_value' ) core.call_all( 'config_backend_add_plugin', 'some_opt', 'some_test_module' ) core.call_all('config_backend_save', 'openpaperwork_test') core.call_all('config_backend_load', 'openpaperwork_test') v = core.call_one('config_backend_get', 'test_section', 'test_key') self.assertEqual(v, 'test_value') v = core.call_one( 'config_backend_get', 'wrong_section', 'test_key', 'default' ) self.assertEqual(v, 'default') self.assertIsNone( core.call_success( 'config_backend_get', 'test_section', 'wrong_key' ) ) finally: core.call_success("fs_rm_rf", core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).base_path, trash=False) def test_observers(self): core = openpaperwork_core.Core(auto_load_dependencies=True) core.load('openpaperwork_core.config.backend.configparser') core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).base_path = "file://" + tempfile.mkdtemp( prefix='openpaperwork_core_config_tests' ) class Observer(object): def __init__(self): self.count = 0 def obs(self): self.count += 1 try: core.init() obs = Observer() core.call_all( 'config_backend_add_observer', 'test_section', obs.obs ) core.call_all( 'config_backend_put', 'other_section', 'test_key', 'test_value' ) self.assertEqual(obs.count, 0) core.call_all( 'config_backend_put', 'test_section', 'test_key', 'test_value' ) self.assertEqual(obs.count, 1) core.call_all( 'config_backend_add_plugin', 'some_opt', 'some_test_module' ) self.assertEqual(obs.count, 1) core.call_all('config_backend_save', 'openpaperwork_test') self.assertEqual(obs.count, 1) core.call_all('config_backend_load', 'openpaperwork_test') self.assertEqual(obs.count, 2) finally: core.call_success("fs_rm_rf", core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).base_path, trash=False) def test_simple_readwrite_list(self): core = openpaperwork_core.Core(auto_load_dependencies=True) core.load('openpaperwork_core.config.backend.configparser') core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).base_path = "file://" + tempfile.mkdtemp( prefix='openpaperwork_core_config_tests' ) try: core.init() v = core.call_success( 'config_backend_get', 'test_section', 'test_key', default=["test_value_a", "test_value_b"] ) self.assertNotEqual(v, None) self.assertEqual(len(v), 2) v[1] = 'test_value_c' core.call_all('config_backend_put', 'test_section', 'test_key', v) v = core.call_success( 'config_backend_get', 'test_section', 'test_key', default=["test_value_a", "test_value_b"] ) self.assertEqual(len(v), 2) self.assertEqual(v[1], "test_value_c") core.call_all('config_backend_save', 'openpaperwork_test') core.call_all('config_backend_load', 'openpaperwork_test') v = core.call_success( 'config_backend_get', 'test_section', 'test_key', default=["test_value_a", "test_value_b"] ) self.assertEqual(len(v), 2) self.assertEqual(v[1], "test_value_c") finally: core.call_success("fs_rm_rf", core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).base_path, trash=False) def test_simple_readwrite_dict(self): core = openpaperwork_core.Core(auto_load_dependencies=True) core.load('openpaperwork_core.config.backend.configparser') core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).base_path = "file://" + tempfile.mkdtemp( prefix='openpaperwork_core_config_tests' ) try: core.init() v = core.call_success( 'config_backend_get', 'test_section', 'test_key', default={ "test_key_a": "test_key_b", "test_key_b": "test_value_b" } ) self.assertNotEqual(v, None) self.assertEqual(len(v), 2) v['test_key_b'] = 'test_value_c' core.call_all('config_backend_put', 'test_section', 'test_key', v) v = core.call_success( 'config_backend_get', 'test_section', 'test_key', default={ "test_key_a": "test_key_b", "test_key_b": "test_value_b" } ) self.assertEqual(len(v), 2) self.assertEqual(v['test_key_b'], "test_value_c") core.call_all('config_backend_save', 'openpaperwork_test') core.call_all('config_backend_load', 'openpaperwork_test') v = core.call_success( 'config_backend_get', 'test_section', 'test_key', default={ "test_key_a": "test_key_b", "test_key_b": "test_value_b" } ) self.assertEqual(len(v), 2) self.assertEqual(v['test_key_b'], "test_value_c") finally: core.call_success("fs_rm_rf", core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).base_path, trash=False) def test_getset_date(self): core = openpaperwork_core.Core(auto_load_dependencies=True) core.load('openpaperwork_core.config.backend.configparser') core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).base_path = "file://" + tempfile.mkdtemp( prefix='openpaperwork_core_config_tests' ) try: core.init() core.call_all( 'config_backend_put', 'test_section', 'test_key', datetime.date(year=1985, month=1, day=1) ) core.call_all('config_backend_save', 'openpaperwork_test') core.call_all('config_backend_load', 'openpaperwork_test') v = core.call_one('config_backend_get', 'test_section', 'test_key') self.assertEqual(v, datetime.date(year=1985, month=1, day=1)) finally: core.call_success("fs_rm_rf", core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).base_path, trash=False) paperwork-2.1.1/openpaperwork-core/tests/config/tests_config.py000066400000000000000000000123501417573700700250330ustar00rootroot00000000000000import unittest import openpaperwork_core class MockConfigBackendModule(object): """ Plugin paperwork_backend.config uses openpaperwork.config_file. This mock mocks openpaperwork.config_file so we can test paperwork_backend.config.file """ class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.calls = [] self.rets = {} self.config_backend_load = ( lambda *args, **kwargs: self._handle_call('config_backend_load', args, kwargs) ) self.config_backend_save = ( lambda *args, **kwargs: self._handle_call('config_backend_save', args, kwargs) ) self.config_backend_load_plugins = ( lambda *args, **kwargs: self._handle_call('config_backend_load_plugins', args, kwargs) ) self.config_backend_add_plugin = ( lambda *args, **kwargs: self._handle_call('config_backend_add_plugin', args, kwargs) ) self.config_backend_remove_plugin = ( lambda *args, **kwargs: self._handle_call('config_backend_remove_plugin', args, kwargs) ) self.config_backend_put = ( lambda *args, **kwargs: self._handle_call('config_backend_put', args, kwargs) ) self.config_backend_get = ( lambda *args, **kwargs: self._handle_call('config_backend_get', args, kwargs) ) self.config_backend_add_observer = ( lambda *args, **kwargs: self._handle_call('config_backend_add_observer', args, kwargs) ) self.config_backend_remove_observer = ( lambda *args, **kwargs: self._handle_call( 'config_backend_remove_observer', args, kwargs ) ) def _handle_call(self, func, args, kwargs): self.calls.append((func, args, kwargs)) if func not in self.rets: return None r = self.rets[func].pop(0) if len(self.rets[func]) <= 0: self.rets.pop(func) return r def get_interfaces(self): return ["config_backend"] class TestConfig(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core._load_module( "openpaperwork_core.config.backend.configparser", MockConfigBackendModule() ) self.core.load("openpaperwork_core.config") self.core.init() setting = self.core.call_success( "config_build_simple", "Global", "WorkDirectory", lambda: "file:///home/toto/papers" ) self.core.call_all("config_register", "workdir", setting) def test_config_load(self): self.core.call_all('config_load') self.core.call_all( 'config_load_plugins', 'some_plugin_list_name', default_plugins=['pouet'] ) self.assertEqual( self.core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).calls, [ ('config_backend_load', ('openpaperwork_core',), {}), ( 'config_backend_load_plugins', ('some_plugin_list_name', ['pouet'],), {} ), ] ) def test_get_default(self): default = self.core.call_success("config_get", "workdir") self.assertEqual( self.core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).calls, [ ('config_backend_get', ('Global', "WorkDirectory", None), {}), ] ) self.assertEqual(default, "file:///home/toto/papers") def test_get_nondefault(self): self.core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).rets = { 'config_backend_get': ['file:///pouet/path'] } val = self.core.call_success( "config_get", "workdir" ) self.assertEqual( self.core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).calls, [ ('config_backend_get', ('Global', "WorkDirectory", None), {}), ] ) self.assertEqual(val, "file:///pouet/path") def test_get_cache(self): self.core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).rets = { # only one call expected 'config_backend_get': ['file:///pouet/path'] } val1 = self.core.call_success("config_get", "workdir") val2 = self.core.call_success("config_get", "workdir") self.assertEqual( self.core.get_by_name( 'openpaperwork_core.config.backend.configparser' ).calls, [ ('config_backend_get', ('Global', "WorkDirectory", None), {}), ] ) self.assertEqual(val1, "file:///pouet/path") self.assertEqual(val1, val2) paperwork-2.1.1/openpaperwork-core/tests/fs/000077500000000000000000000000001417573700700211345ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/fs/__init__.py000066400000000000000000000000001417573700700232330ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/fs/tests_memory.py000066400000000000000000000037161417573700700242470ustar00rootroot00000000000000import unittest import openpaperwork_core class TestSafe(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.fs.memory") self.core.init() def test_write_read_bytes(self): with self.core.call_success( "fs_open", "memory://test_file", mode="wb" ) as file_desc: file_desc.write(b"abcdef") file_desc.write(b"ghijf") with self.core.call_success( "fs_open", "memory://test_file", mode="rb" ) as file_desc: r = file_desc.read() self.assertEqual(r, b'abcdefghijf') self.core.call_success("fs_unlink", "memory://test_file", trash=False) def test_write_read_string(self): with self.core.call_success( "fs_open", "memory://test_file", mode="w" ) as file_desc: file_desc.write("abcdef\n") file_desc.write("ghijf\n") with self.core.call_success( "fs_open", "memory://test_file", mode="r" ) as file_desc: r = file_desc.readlines() self.assertEqual(r, [ 'abcdef\n', 'ghijf\n' ]) self.core.call_success("fs_unlink", "memory://test_file", trash=False) def test_write_read_tempfile(self): (name, file_desc) = self.core.call_success( "fs_mktemp", "camion", "tulipe", mode="w" ) with file_desc: self.assertTrue(name.startswith("memory://")) file_desc.write("abcdef\n") file_desc.write("ghijf\n") with self.core.call_success("fs_open", name, mode="r") as file_desc: r = file_desc.readlines() self.assertEqual(r, [ 'abcdef\n', 'ghijf\n' ]) self.core.call_success("fs_unlink", name, trash=False) paperwork-2.1.1/openpaperwork-core/tests/fs/tests_python.py000066400000000000000000000036501417573700700242550ustar00rootroot00000000000000import openpaperwork_core.tests.local_file PLUGIN_NAME = "openpaperwork_core.fs.python" class TestSafe(openpaperwork_core.tests.local_file.AbstractTestSafe): def get_plugin_name(self): return PLUGIN_NAME class TestUnsafe(openpaperwork_core.tests.local_file.AbstractTestUnsafe): def get_plugin_name(self): return PLUGIN_NAME class TestOpen(openpaperwork_core.tests.local_file.AbstractTestOpen): def get_plugin_name(self): return PLUGIN_NAME class TestExists(openpaperwork_core.tests.local_file.AbstractTestExists): def get_plugin_name(self): return PLUGIN_NAME class TestListDir(openpaperwork_core.tests.local_file.AbstractTestListDir): def get_plugin_name(self): return PLUGIN_NAME class TestRename(openpaperwork_core.tests.local_file.AbstractTestRename): def get_plugin_name(self): return PLUGIN_NAME class TestUnlink(openpaperwork_core.tests.local_file.AbstractTestUnlink): def get_plugin_name(self): return PLUGIN_NAME class TestGetMtime(openpaperwork_core.tests.local_file.AbstractTestGetMtime): def get_plugin_name(self): return PLUGIN_NAME class TestGetsize(openpaperwork_core.tests.local_file.AbstractTestGetsize): def get_plugin_name(self): return PLUGIN_NAME class TestIsdir(openpaperwork_core.tests.local_file.AbstractTestIsdir): def get_plugin_name(self): return PLUGIN_NAME class TestCopy(openpaperwork_core.tests.local_file.AbstractTestCopy): def get_plugin_name(self): return PLUGIN_NAME class TestMkdirP(openpaperwork_core.tests.local_file.AbstractTestMkdirP): def get_plugin_name(self): return PLUGIN_NAME class TestBasename(openpaperwork_core.tests.local_file.AbstractTestBasename): def get_plugin_name(self): return PLUGIN_NAME class TestTemp(openpaperwork_core.tests.local_file.AbstractTestTemp): def get_plugin_name(self): return PLUGIN_NAME paperwork-2.1.1/openpaperwork-core/tests/mainloop/000077500000000000000000000000001417573700700223425ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/mainloop/__init__.py000066400000000000000000000000001417573700700244410ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/mainloop/tests_asyncio.py000066400000000000000000000005531417573700700256060ustar00rootroot00000000000000import openpaperwork_core.mainloop.tests class TestCallback(openpaperwork_core.mainloop.tests.AbstractTestCallback): def get_plugin_name(self): return "openpaperwork_core.mainloop.asyncio" class TestPromise(openpaperwork_core.mainloop.tests.AbstractTestPromise): def get_plugin_name(self): return "openpaperwork_core.mainloop.asyncio" paperwork-2.1.1/openpaperwork-core/tests/pillow/000077500000000000000000000000001417573700700220325ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/pillow/test_doc.png000066400000000000000000001263371417573700700243600ustar00rootroot00000000000000PNG  IHDR L $IDATxgU5^DT"RR4^4L +*1sQc49(VD;⨈H3( "~?kãkg2wD#' 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"&'i+Yf:tUo/҄ .]ӬYݻ_~eymQQQ~~w߶mVL!-HT<÷zuR7瞫)Uh7|S7֪Ukƍey :o(u{TDɕ|a/Bk׮^{5\3waÆ|Ur+Vu]gѣ_V% nT1YYYuԩ[ߦMއ,GKJKKmԨ^{պuܪժU뢋.^ۿ7{oaΙ3gͳg:twQT!0,]+cڵkZZZ˖-w^%L$~ (9r5kn*9rXs5{J_~w{Qɣ[juG~={ɩi^z|ӧOEQGC%7r!?eae{n߾}׮]+<*vamڴ(##y{gÆ sss222222?9s$y晉Db˖-EEE6lXnڵkWXt?~|\:̙ӺuxsΟ}YEtՄ9P}W=w>u]W%?7n /0yDsO<1p>}Tr%zwN;+Ųe~r۶mSw]y 4Wg}W%ՠAoɇ5iI's`׫W\0iҤ~z޽u]7iҤ:*޵lٲ?OS&p@%nעE? P"!kqJtWάY:wNTk}iiiquOd~zO>dj$+l;>o뮻ۯVZ9´it^݉GHZhQ=[>SׯSR簳mذYlWuM/;;w]_N0J׿Ox뭷;JKOOoժUe˙;1O͈|!---##e˖Ur-[fdd$oGx≥KV,w';Ux_uUjQQoۭ[V)J߱sU5i7@MЪU [wrԻRc=xqx;wn1bŋӧO>*ĶO>KOO/zD"1qO?[~}nnn&M~pڵjgΜx+VX"==aÆ:tڵkvvves͟??\TTZm322Wj Ǎ7eʔKnڴQFmڴ96mZAUlzU%--^z+VHU{Zjx7pC6կ_7L$7oW}W良*{!ˋhҤIO=K/|Խ]wb%]P}}J<˨QnYf7##W^9묳50UW]5bĈ%Kq7pM7ݔQ~ʈ#>xq:f'tRnذVZUlWZZڹ{wo9pe*<*[▖vڵkWo9rG}4ve˖:b# 333R.-XN/qoQQߟ׿5uywN:u/r駸~GQ_裏{W^=}!C̟?hÆ yMC>K<3رcKaÆv%G6jhڴi{ϟ>$6mtٲe{+A}ze׿+eԩSS{+sԳ(+^޴iS{$dɒY͛Cf4hP⮌xhg=zN:moX˖-)S$j—U._|E^x˽{eee&^Xkv۾I&2+`W;CƏEˏ=k׮?9}YooXӦMŋ5ڼys[n]z… 'L0ux{a͚5kslR3>%\FQ͚5[n'xe]֫WvD"7īж+W5ڰaW_]׿[hSPWnR.ի믿^3&_ꫯƌ1cƌ3vڽ{K>cׯϟ2eܹs5klܸqӦMiYf֭j׮]wKxyƍrMJyg$322ԩS^-[v֭K.{b*XG-VLN|ZZZïO[vܽ{AB9_;x֮]ܒ޸q:uي]jZԩ/ׄ/D{ǫOZ̙3SW>J7dee 8k4hЋ/tԽ'N<#zYg8kwqSL(==^xaΝ1ۋU+37cƌf͚ 5˪O/y䑕<`8}٧ǩ.U{(ti^hј1c Pnx͛ppw& _E=c=va]5RMhMj-[L<9^m׮]oټy_ZyU{!YFF '0p tMk>#͡𩧞J.>]vYUN򲲲˗W5˪!~gӧ'SCT "uֽ{x{e˖%?jG͛WU _Vzg弼Tdj{ؖںuJ{ 7^ŋ7.^nܸqV:v/O8/ _V 4nܸaÆū]vYչ{ D>իWU_q' nEEE͛+6*t '/raaa^^f͚5O$u];7<ӥݴiSranM4gΜYyUFFFbŊ ;ssss|ͣ>ZU*bŊO>9fԩ3|ڵkWjފn͛7o;ܺukUM|P zw\rҤI=z7:dee%'M4ymL:\,QÆ O>d{֭{Wǫr /PU*H$^|.]L0!ycƌI} =_.\p?ȄO`Æ guܹs~饗&׬Y+"93H$zSO}׳g7n?ܭ[.]dggWrݻwK7nܲeˢEl;:t\޼ys~z5r'_o?#;Q>O&(**9s-[f͚M6o_߮]_WKSOϺuV*{1bD+k '-~g̙&MMf͚䮌Ν;ڶm{qǕx'Æ Kniݺ^z衇vڵ~Qmݺu̙SL=zt|]~7`ݺuUּy˂>W_MiժU߾}۵k׶m۶m&ۖ|p]FF)rG4mڴ𫯾zW(7nvܹ\HMCE_~塇~ <'|7xavᇧ^Ck׮mӦ͒%K- ׬Yze˖yǏ/vgȐ!j?rM˖-{7f̘1cƌO?tʕ:u۴is'h\PP_N4)>Z٥릛n:ꨣ*6;hbKSOoYyرcGkеkN:o߾}:tw}2w'6f̘7|39m۶~ĕ|P=6nXPP0w~ݺu 4h߾\[Yք9}3fXn]F{9sdff>:ujٲe/&Nt+VԪUaÆ WJ|PޮTTTJEQ:u2 &L3Ϥ]xn lI #@`D> 0"F|#@`D> 0"F|#@`D>LfuOzx1dggTŋK&LXtiNNNfͺw~%o;x}m۶d՚={yml֬Yc:AJK$=২e˖+OŋkҮ7poykժbŊ:o-**JտvU[׭[o߾=\uM 8# O>daaac.첌ʜeҥÇ/eyרQvŊwuٳ{q嗧Ubofڵ3fׯߜ9s{:1s̱cǖ>u'tRޛo9k7n|(rss̙yٳg:;Wkk;waÒ%?O/.e@&M.JFEr-+W,qoZ=FD"qO0!#GYo$w'k>ׯ_)S\{6mހO?1:ꡇJn֡C:={vrʕ+SI銊>#<_.}dݺu;vرc>iӦyÆ 333lْ\s=+vjVZjK}\>}Jye~r۶mSw]y 4I`J>ӧOxO?]`A_o,^xԨQ˗//7[Q{.|Q5ho[zzzEM4+|(v-ZP $"oEQ":th_OFQiӦ_|,y睵kF7+V_W_;fܹs)#?x,HjiӦ˹8%:Cj#ߒ%K|ÇGQٻwO֭[r)/ĕ ~#VSx93m(7^7qy%O?t|wI&}߲e˨Q(:7n\O;_ H-6mz>Ν;s9DKK=EEE*v7nʔ)K.ݴiSFڴis7mtEEEDb{os֭SL7n܂ 222ڴisgb[dرc+Vdgg7iҤcǎsLNNζ/)s~\lNH$6oJ_X&N駟~wׯmҤ~8]veyv?/bٲek֬c=֭[~-Zp)r޺uɓǏpD"Ѳe=zhѢ̟O@t}gϞ]~m77hG5kl{A%.^oРAr 7ܐH$}m_ҷos-YdРA'tR)]gjwuWzvymm~D"1mڴw޼VZ~e8Cʚ5k9Sb ͽ~b/GDQt衇~2p̙3硇:wx&ԿRP;6r8`{SxWK9uAAA>}J3;;;???Uޫ*HL8+lذD"J>ԧOlСrK#W\+DQt駏3&駟ѣG/7nܒ%K(xcڵI& B-[~;GݰaCwyxԩSG/x{(1777jd+ؒ%KN=ӧGQs{999SN}Ǘ-[/3fԨQGqD'Fz饗[^{ķiӦ?k׾KPvEEErƍ(J^q˗' 6<Æ {۶m/nժ[oZ>={r!:uJ=o~nݺ͜9?OW^^x^o0###0//qež/y9lzz/ˣ>z^zӇ 2m᱇z覛nJ뗿eN6lzYfq"ؼys-т N8엪_Z`7T*#;vްA%s9KjժGۺk(^r{S#ĉ'ئMrF::uD{%_/)v%_,cǎ-南q ڵkmڴi{ϟ>$6mtٲe͆ ;|W>裙3gN<jҤI<~֭+eڥ䋢o;vlz?c=z[ӦM.]q9x̯~O[oEQ6w܊M^ɗOG~\W{ɬ6l8(zK<OVZ혂(zaÆ}3f z3ج%KqDl0/O6m֬Y%{'(jݺ{E7m4dȐ_2bĈ(N;o{jĉsÿe˖}_Fvn +5EQԼy;,P~a?s9G}t=k?JYrѣ+?kJ[cƍ>g?Y<l+"^>|+J9O?E'{<2_ߟH$(ٳ綗r_Zw1Buy>-fS?/NKK;ӒKJfbLF^sg>g}{rW-Zj۟}ێݻwڵ йs߫W-[nD"qW'aZZ#,^7iӦŋ+p R'c:u:/5k(KwA%~˗/?c~M'|/wر'tRFxpJ' r)j%OӦM{왺7MhѢw}7^>|xEyyy^D"Il͍Akݺurum;+m5|c9w,wݩ|֬Yݺu;餓^zחr¯*^MjD>–z΂3f$̙FQtdffWUZ;v&o?سg!Zf֭[ծ]ގo=m CuժUێӧO +WL֭[{h7__\j3oڴo~cǖx+V$x~U(uY'w|'"* ykiܹSN¿W5kRWsN:WA+䤮nذa1^xaZ쎝cƌY`A=ܝ4]_wZZ zk׳g1t'x":uwy%$##_~ ǏEQ>}v|wxu87nlbR/{駓O:tuֱcǝ:ϼzsUםq ' 8p7tS}ڵ$^>KuǫO>9s>ڵkZvԟ.Pɯ{ݺukŋSw. ;8S4h堃aho~SlnpΤN8!^~ q2UwM.pD_~yzOȑ#(Sɧծ];5RwWUɯ^z$8":s_}ҥ:/!;:sR~_矟-4hp'W&###^^bE%G{oG9V#F|wO>u֥oѢE|QUܫsݺucǎv{PڵkqX7bĈW^yegFIK$=o Eoߧt[no.\ظqŋgff2`ĉaÒ[Zjշovڵm۶m۶ŞX6o޼/࣏>zWKڵk۴idɒqFZfMڵ;rٲegy㋽xK>} \;wqF/'nK/=CvZ~(lO~/v[E999-* /\=غuN6?{ov)HƎ;r5k$7vڵSN۷o߾}ws|cƌy7۶m{ǿ(L8S8∦M~WJ7~w;w\?qn{ _zzz"Hi[oE%5m3ۺu̙3L2zo?ϻuVVN|={?KVv'5o޼쯺[(K'yNjd}^2~f͚ӪU{;*-[ c nZPPPPPdɒ 6pG}t^^^^U*uoܸ`ܹu벳4hо}>8zJ~8?U4hpo>77*vM0gIKK uSP|,??N8 T{(uq'|v[/nѢEVVւ 4hP*J>`uo޼.R͈|@wG8uAժUoY#/}- ,86mt51I<^ڵ~#8bƍo߾x;?UVuOv֭+}G{ォeJS]'՜/| +WۼyW_}hѢk6mڴC 4IN$@`ܮ#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|=; u;!3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$@  `F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`FjHݎ@oH>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`FiIENDB`paperwork-2.1.1/openpaperwork-core/tests/pillow/tests_img.py000066400000000000000000000023411417573700700244020ustar00rootroot00000000000000import os import unittest import openpaperwork_core class TestPillowImg(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.fs.python") self.core.load("openpaperwork_core.fs.memory") self.core.load("openpaperwork_core.pillow.img") self.core.init() self.img_path = self.core.call_success( "fs_safe", os.path.join( os.path.dirname(os.path.abspath(__file__)), "test_doc.png" ) ) def test_img_url_to_pillow(self): img = self.core.call_success("url_to_pillow", self.img_path) self.assertIsNotNone(img) self.assertEqual(img.size, (2380, 3364)) def test_pillow_to_url(self): img = self.core.call_success("url_to_pillow", self.img_path) (img_url, fd) = self.core.call_success( "fs_mktemp", prefix="paperwork-test", suffix=".png" ) fd.close() self.core.call_success("pillow_to_url", img, img_url, format="PNG") # no real way to make sure the image was correctly written I guess self.core.call_success("fs_unlink", img_url, trash=False) paperwork-2.1.1/openpaperwork-core/tests/spatial/000077500000000000000000000000001417573700700221615ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/spatial/__init__.py000066400000000000000000000000001417573700700242600ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/spatial/tests_rtree.py000066400000000000000000000002751417573700700251020ustar00rootroot00000000000000import openpaperwork_core.spatial.tests class TestSpatial(openpaperwork_core.spatial.tests.AbstractTest): def get_plugin_name(self): return "openpaperwork_core.spatial.rtree" paperwork-2.1.1/openpaperwork-core/tests/spatial/tests_simple.py000066400000000000000000000002751417573700700252520ustar00rootroot00000000000000import openpaperwork_core.spatial.tests class TestSimple(openpaperwork_core.spatial.tests.AbstractTest): def get_plugin_name(self): return "openpaperwork_core.spatial.simple" paperwork-2.1.1/openpaperwork-core/tests/tests_core.py000066400000000000000000000310531417573700700232520ustar00rootroot00000000000000import unittest import unittest.mock import openpaperwork_core class TestLoading(unittest.TestCase): @unittest.mock.patch("importlib.import_module") def test_simple_loading(self, import_module): class TestModule(object): class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.init_called = False self.test_method_called = False def init(self, core): self.init_called = True def test_method(self): self.test_method_called = True core = openpaperwork_core.Core(auto_load_dependencies=True) import_module.return_value = TestModule() core.load('whatever_module') import_module.assert_called_once_with('whatever_module') core.init() self.assertTrue(core.get_by_name('whatever_module').init_called) core.call_all('test_method') self.assertTrue(core.get_by_name('whatever_module').test_method_called) class TestInit(unittest.TestCase): @unittest.mock.patch("importlib.import_module") def test_init_order(self, import_module): global g_idx g_idx = 0 class TestModuleA(object): class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.init_called_a = -1 def get_interfaces(self): return ['module_a'] def init(self, core): global g_idx self.init_called_a = g_idx g_idx += 1 class TestModuleB(object): class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.init_called_b = -1 def get_interfaces(self): return ['module_b'] def get_deps(self): return [ { 'interface': 'module_a', 'defaults': ['module_a'], } ] def init(self, core): global g_idx self.init_called_b = g_idx g_idx += 1 class TestModuleC(object): class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.init_called_c = -1 def get_deps(self): return [ { 'interface': 'module_b', 'defaults': ['module_b'], 'expected_already_satisfied': False, } ] def init(self, core): global g_idx self.init_called_c = g_idx g_idx += 1 core = openpaperwork_core.Core() import_module.return_value = TestModuleA() core.load('module_a') import_module.assert_called_once_with('module_a') import_module.reset_mock() import_module.return_value = TestModuleC() core.load('module_c') import_module.assert_called_once_with('module_c') import_module.reset_mock() import_module.return_value = TestModuleB() core.init() # will load 'module_b' because of dependencies import_module.assert_called_once_with('module_b') self.assertEqual(core.get_by_name('module_a').init_called_a, 0) self.assertEqual(core.get_by_name('module_b').init_called_b, 1) self.assertEqual(core.get_by_name('module_c').init_called_c, 2) class TestCall(unittest.TestCase): @unittest.mock.patch("importlib.import_module") def test_default_interface(self, import_module): class TestModuleB(object): class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.init_called_b = False self.test_method_called_b = False def get_interfaces(self): return ["test_interface"] def init(self, core): self.init_called_b = True def test_method(self): self.test_method_called_b = True class TestModuleC(object): class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.init_called_c = False self.test_method_called_c = False def get_deps(self): return [ { 'interface': 'test_interface', 'defaults': ['module_a', 'module_b'] }, ] def init(self, core): self.init_called_c = True def test_method(self): self.test_method_called_c = True core = openpaperwork_core.Core(auto_load_dependencies=True) import_module.return_value = TestModuleC() core.load('module_c') import_module.assert_called_once_with('module_c') import_module.reset_mock() import_module.return_value = TestModuleB() core.load('module_b') import_module.assert_called_once_with('module_b') import_module.reset_mock() # interface already satisfied --> won't load 'module_a' core.init() self.assertTrue(core.get_by_name('module_b').init_called_b) self.assertTrue(core.get_by_name('module_c').init_called_c) core.call_all('test_method') self.assertTrue(core.get_by_name('module_b').test_method_called_b) self.assertTrue(core.get_by_name('module_c').test_method_called_c) @unittest.mock.patch("importlib.import_module") def test_call_success_priority(self, import_module): class TestModuleB(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 33 def __init__(self): self.test_method_called_b = False def test_method(self): self.test_method_called_b = True return None class TestModuleC(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 22 def __init__(self): self.test_method_called_c = False def test_method(self): self.test_method_called_c = True return "value" class TestModuleD(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 11 def __init__(self): self.test_method_called_d = False def test_method(self): self.test_method_called_d = True return None core = openpaperwork_core.Core(auto_load_dependencies=True) import_module.return_value = TestModuleB() core.load('module_b') import_module.assert_called_once_with('module_b') import_module.reset_mock() import_module.return_value = TestModuleC() core.load('module_c') import_module.assert_called_once_with('module_c') import_module.reset_mock() import_module.return_value = TestModuleD() core.load('module_d') import_module.assert_called_once_with('module_d') import_module.reset_mock() # interface already satisfied --> won't load 'module_a' core.init() r = core.call_success('test_method') self.assertEqual(r, "value") self.assertTrue(core.get_by_name('module_b').test_method_called_b) self.assertTrue(core.get_by_name('module_c').test_method_called_c) self.assertFalse(core.get_by_name('module_d').test_method_called_d) @unittest.mock.patch("importlib.import_module") def test_priority(self, import_module): class TestModuleA(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 22 def test_method(self): return "A" class TestModuleB(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 33 def test_method(self): return "B" core = openpaperwork_core.Core(auto_load_dependencies=True) import_module.return_value = TestModuleA() core.load('module_a') import_module.assert_called_once_with('module_a') import_module.reset_mock() import_module.return_value = TestModuleB() core.load('module_b') import_module.assert_called_once_with('module_b') import_module.reset_mock() core.init() r = core.call_success('test_method') self.assertEqual(r, "B") class TestDependencies(unittest.TestCase): @unittest.mock.patch("importlib.import_module") def test_default_interface(self, import_module): class TestModuleA(object): class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.init_called = False self.test_method_called = False def get_interfaces(self): return [ "test_interface", "some_interface", ] def init(self, core): self.init_called = True def test_method(self): self.test_method_called = True class TestModuleB(object): class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.init_called = False def get_interfaces(self): return ['some_interface'] def get_deps(self): return [ { 'interface': 'test_interface', 'defaults': ['module_a'], } ] def init(self, core): self.init_called = True core = openpaperwork_core.Core(auto_load_dependencies=True) import_module.return_value = TestModuleB() core.load('module_b') import_module.assert_called_once_with('module_b') import_module.reset_mock() import_module.return_value = TestModuleA() core.init() # will load 'module_a' because of dependencies import_module.assert_called_once_with('module_a') self.assertTrue(core.get_by_name('module_a').init_called) self.assertTrue(core.get_by_name('module_b').init_called) core.call_all('test_method') self.assertTrue(core.get_by_name('module_a').test_method_called) self.assertEqual( core.get_by_interface('some_interface'), [ core.get_by_name('module_b'), core.get_by_name('module_a'), ] ) self.assertEqual(core.get_by_interface('unknown_interface'), []) @unittest.mock.patch("importlib.import_module") def test_no_init_if_dropped(self, import_module): self.init_called = False class TestModuleA(object): class Plugin(openpaperwork_core.PluginBase): def get_interfaces(s): return [ "test_interface", "some_interface", ] def init(s, core): self.init_called = True class TestModuleB(object): class Plugin(openpaperwork_core.PluginBase): def __init__(s): s.init_called = False def get_interfaces(s): return ['some_interface'] def get_deps(s): return [ { 'interface': 'test_interface', 'defaults': ['module_a'], } ] def init(s, core): self.init_called = True core = openpaperwork_core.Core(auto_load_dependencies=False) import_module.return_value = TestModuleB() core.load('module_b') import_module.assert_called_once_with('module_b') import_module.reset_mock() import_module.return_value = TestModuleA() core.init() # will NOT load 'module_a' and will drop 'module_b' self.assertFalse(self.init_called) self.assertRaises(KeyError, core.get_by_name, 'module_a') self.assertRaises(KeyError, core.get_by_name, 'module_b') paperwork-2.1.1/openpaperwork-core/tests/tests_url.py000066400000000000000000000027551417573700700231330ustar00rootroot00000000000000import unittest import openpaperwork_core class TestUrl(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.urls") self.core.init() def test_url_join(self): self.assertEqual( self.core.call_success("url_args_join", "file://something.txt"), "file://something.txt" ) self.assertEqual( self.core.call_success( "url_args_join", "file://something.txt", page=1 ), "file://something.txt#page=1" ) self.assertEqual( self.core.call_success( "url_args_join", "file://something.txt", page=1, password="test1234" ), "file://something.txt#page=1&password=test1234" ) def test_url_split(self): self.assertEqual( self.core.call_success("url_args_split", "file://something.txt"), ("file://something.txt", {}) ) self.assertEqual( self.core.call_success( "url_args_split", "file://something.txt#page=1" ), ("file://something.txt", {"page": "1"}) ) self.assertEqual( self.core.call_success( "url_args_split", "file://something.txt#page=1&password=test1234" ), ("file://something.txt", {"page": "1", "password": "test1234"}) ) paperwork-2.1.1/openpaperwork-core/tests/thread/000077500000000000000000000000001417573700700217735ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/thread/__init__.py000066400000000000000000000000001417573700700240720ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/thread/tests_pool.py000066400000000000000000000002761417573700700245450ustar00rootroot00000000000000import openpaperwork_core.thread.tests class TestThread(openpaperwork_core.thread.tests.AbstractTestThread): def get_plugin_name(self): return "openpaperwork_core.thread.pool" paperwork-2.1.1/openpaperwork-core/tests/thread/tests_simple.py000066400000000000000000000003001417573700700250510ustar00rootroot00000000000000import openpaperwork_core.thread.tests class TestThread(openpaperwork_core.thread.tests.AbstractTestThread): def get_plugin_name(self): return "openpaperwork_core.thread.simple" paperwork-2.1.1/openpaperwork-core/tests/work_queue/000077500000000000000000000000001417573700700227125ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/work_queue/__init__.py000066400000000000000000000000001417573700700250110ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-core/tests/work_queue/tests_default.py000066400000000000000000000140761417573700700261420ustar00rootroot00000000000000import unittest import openpaperwork_core class TestQueue(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.work_queue.default") self.core.init() def test_single_task(self): self.task_done = False def do_task(): self.task_done = True self.core.call_all("work_queue_create", "some_work_queue") self.core.call_one( "work_queue_add_promise", "some_work_queue", openpaperwork_core.promise.Promise(self.core, do_task) ) self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertTrue(self.task_done) def test_many_tasks(self): self.task_a_done = False self.task_b_done = False def do_task_a(): self.assertFalse(self.task_a_done) self.assertFalse(self.task_b_done) self.task_a_done = True def do_task_b(): self.assertTrue(self.task_a_done) self.assertFalse(self.task_b_done) self.task_b_done = True self.core.call_all("work_queue_create", "some_work_queue") self.core.call_one( "work_queue_add_promise", "some_work_queue", openpaperwork_core.promise.Promise(self.core, do_task_a) ) self.core.call_one( "work_queue_add_promise", "some_work_queue", openpaperwork_core.promise.Promise(self.core, do_task_b) ) self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertTrue(self.task_a_done) self.assertTrue(self.task_b_done) def test_uncaught(self): # make sure the work queue doesn't stop if an exception is raised # by one of the task/promise. self.task_a_done = False self.task_b_done = False def do_task_a(): self.assertFalse(self.task_a_done) self.assertFalse(self.task_b_done) self.task_a_done = True raise Exception("Test exception. May be normal. Do not panic :-)") def do_task_b(): self.assertTrue(self.task_a_done) self.assertFalse(self.task_b_done) self.task_b_done = True self.core.call_all( "work_queue_create", "some_work_queue", hide_uncatched=True ) self.core.call_one( "work_queue_add_promise", "some_work_queue", openpaperwork_core.promise.Promise( self.core, do_task_a, hide_caught_exceptions=True ) ) self.core.call_one( "work_queue_add_promise", "some_work_queue", openpaperwork_core.promise.Promise(self.core, do_task_b) ) self.core.call_all("mainloop_quit_graceful") self.core.call_one( "mainloop", halt_on_uncaught_exception=False, log_uncaught=False ) self.assertTrue(self.task_a_done) self.assertTrue(self.task_b_done) def test_cancel_all(self): self.task_a_done = False self.task_b_done = False def do_task_a(): self.assertFalse(self.task_a_done) self.assertFalse(self.task_b_done) self.task_a_done = True return "some_crap" def do_task_b(): self.assertTrue(self.task_a_done) self.assertFalse(self.task_b_done) self.task_b_done = True self.core.call_all("work_queue_cancel_all", "some_work_queue") def do_task_c(): self.assertTrue(False) self.core.call_all("work_queue_create", "some_work_queue") self.core.call_one( "work_queue_add_promise", "some_work_queue", openpaperwork_core.promise.Promise(self.core, do_task_a) ) self.core.call_one( "work_queue_add_promise", "some_work_queue", openpaperwork_core.promise.Promise(self.core, do_task_b) ) self.core.call_one( "work_queue_add_promise", "some_work_queue", openpaperwork_core.promise.Promise(self.core, do_task_c) ) self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertTrue(self.task_a_done) self.assertTrue(self.task_b_done) def test_cancel(self): self.task_a_done = False self.task_b_done = False self.task_d_done = False def do_task_a(): self.assertFalse(self.task_a_done) self.assertFalse(self.task_b_done) self.task_a_done = True return "some_crap" def do_task_b(): self.assertTrue(self.task_a_done) self.assertFalse(self.task_b_done) self.task_b_done = True self.core.call_all( "work_queue_cancel", "some_work_queue", task_c ) def do_task_c(): self.assertTrue(False) def do_task_d(): self.assertTrue(self.task_a_done) self.assertTrue(self.task_b_done) self.task_d_done = True return "some_crap" task_c = openpaperwork_core.promise.Promise(self.core, do_task_c) self.core.call_all("work_queue_create", "some_work_queue") self.core.call_one( "work_queue_add_promise", "some_work_queue", openpaperwork_core.promise.Promise(self.core, do_task_a) ) self.core.call_one( "work_queue_add_promise", "some_work_queue", openpaperwork_core.promise.Promise(self.core, do_task_b) ) self.core.call_one( "work_queue_add_promise", "some_work_queue", task_c ) self.core.call_one( "work_queue_add_promise", "some_work_queue", openpaperwork_core.promise.Promise(self.core, do_task_d) ) self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertTrue(self.task_a_done) self.assertTrue(self.task_b_done) self.assertTrue(self.task_d_done) paperwork-2.1.1/openpaperwork-core/tox.ini000066400000000000000000000002641417573700700206770ustar00rootroot00000000000000[tox] envlist=py3 [testenv] deps= pytest setuptools >= 9.0.1 commands=pytest {posargs} [flake8] exclude = .tox, build, dist, venv*, *.egg*, .git, paperwork-2.1.1/openpaperwork-gtk/000077500000000000000000000000001417573700700172175ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/ChangeLog000066400000000000000000000007141417573700700207730ustar00rootroot000000000000002022/01/31 - 2.1.1: - Fix: take into account that Cursor.new_for_display() may fail with Wayland 2021/12/05 - 2.1.0: - Bug report: Clarify the use of the author field 2021/05/24 - 2.0.3: - Swedish translations added - Add LICENSE file in pypi package 2021/01/01 - 2.0.2: - No changes 2020/11/15 - 2.0.1: - fs.gio: Text files must be encoded in UTF-8 - Include tests in Pypi package (thanks to Elliott Sales de Andrade) 2020/10/17 - 2.0: - Initial release paperwork-2.1.1/openpaperwork-gtk/LICENSE000066400000000000000000001045051417573700700202310ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {one line to give the program's name and a brief idea of what it does.} Copyright (C) {year} {name of author} This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: {project} Copyright (C) {year} {fullname} This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . paperwork-2.1.1/openpaperwork-gtk/MANIFEST.in000066400000000000000000000001671417573700700207610ustar00rootroot00000000000000recursive-include src *.py *.glade *.xml *.css *.svg *.png *.mo recursive-include tests * include *.md include LICENSE paperwork-2.1.1/openpaperwork-gtk/Makefile000066400000000000000000000047651417573700700206730ustar00rootroot00000000000000VERSION_FILE = src/openpaperwork_gtk/_version.py PYTHON ?= python3 build: build_c build_py install: install_py install_c uninstall: uninstall_py build_py: ${VERSION_FILE} l10n_compile ${PYTHON} ./setup.py build build_c: version: ${VERSION_FILE} ${VERSION_FILE}: echo -n "version = \"" >| $@ echo -n $(shell git describe --always) >> $@ echo "\"" >> $@ doc: install_py $(MAKE) -C doc html doc/_build/html/index.html: doc data: upload_doc: doc/_build/html/index.html cd .. && ./ci/deliver_doc.sh ${CURDIR}/doc/_build/html openpaperwork_gtk check: flake8 src/openpaperwork_gtk test: install python3 -m unittest discover --verbose -s tests linux_exe: windows_exe: ${PYTHON} /mingw64/bin/pip3-script.py install . # ugly, but "import pkg_resources" doesn't work in frozen environments # and I don't want to have to patch the build machine to fix it every # time. mkdir -p $(CURDIR)/../build/exe/data (cd $(CURDIR)/src && find . -name '*.css' -exec cp --parents \{\} $(CURDIR)/../build/exe/data \; ) (cd $(CURDIR)/src && find . -name '*.glade' -exec cp --parents \{\} $(CURDIR)/../build/exe/data \; ) (cd $(CURDIR)/src && find . -name '*.mo' -exec cp --parents \{\} $(CURDIR)/../build/exe/data \; ) release: ifeq (${RELEASE}, ) @echo "You must specify a release version (make release RELEASE=1.2.3)" exit 1 else @echo "Will release: ${RELEASE}" @echo "Checking release is in ChangeLog ..." grep ${RELEASE} ChangeLog | grep -v "/xx" endif release_pypi: @echo "Releasing paperwork-backend ..." ${PYTHON} ./setup.py sdist twine upload $(CURDIR)/dist/openpaperwork-gtk-${RELEASE}.tar.gz @echo "All done" clean: rm -rf doc/_build rm -f ${VERSION_FILE} rm -rf build dist *.egg-info # PIP_ARGS is used by Flatpak build install_py: ${VERSION_FILE} l10n_compile ${PYTHON} ./setup.py install ${PIP_ARGS} install_c: uninstall_py: pip3 uninstall -y openpaperwork-gtk uninstall_c: l10n_extract: $(CURDIR)/../tools/l10n_extract.sh "$(CURDIR)/src" "$(CURDIR)/l10n" l10n_compile: $(CURDIR)/../tools/l10n_compile.sh \ "$(CURDIR)/l10n" \ "$(CURDIR)/src/openpaperwork_gtk/l10n" \ "openpaperwork_gtk" help: @echo "make build || make build_py" @echo "make check" @echo "make help: display this message" @echo "make install || make install_py" @echo "make uninstall || make uninstall_py" @echo "make release" .PHONY: \ build \ build_c \ build_py \ check \ doc \ exe \ help \ install \ install_c \ install_py \ l10n_compile \ l10n_extract \ release \ test \ uninstall \ uninstall_c \ version paperwork-2.1.1/openpaperwork-gtk/README.md000066400000000000000000000001561417573700700205000ustar00rootroot00000000000000# OpenPaperwork GTK GLib/GTK plugins. [Documentation](https://doc.openpaper.work/openpaperwork_gtk/latest/) paperwork-2.1.1/openpaperwork-gtk/doc/000077500000000000000000000000001417573700700177645ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/doc/Makefile000066400000000000000000000011041417573700700214200ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)paperwork-2.1.1/openpaperwork-gtk/doc/conf.py000066400000000000000000000130271417573700700212660ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- project = 'OpenPaperwork-GTK' copyright = '2019, Jerome Flesch' author = 'Jerome Flesch' # The short X.Y version version = '' # The full version, including alpha/beta/rc tags release = '' # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinxcontrib.plantuml', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = 'OpenPaperwork-GTKdoc' # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'OpenPaperwork-GTK.tex', 'OpenPaperwork-GTK Documentation', 'Jerome Flesch', 'manual'), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'openpaperwork-gtk', 'OpenPaperwork-GTK Documentation', [author], 1) ] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'OpenPaperwork-GTK', 'OpenPaperwork-GTK Documentation', author, 'OpenPaperwork-GTK', 'One line description of project.', 'Miscellaneous'), ] # -- Options for Epub output ------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # -- Extension configuration ------------------------------------------------- # -- Options for todo extension ---------------------------------------------- # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True autodoc_inherit_docstrings = False paperwork-2.1.1/openpaperwork-gtk/doc/index.rst000066400000000000000000000007231417573700700216270ustar00rootroot00000000000000.. OpenPaperwork-GTK documentation master file, created by sphinx-quickstart on Sat Dec 14 13:12:28 2019. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to OpenPaperwork-GTK's documentation! ============================================= .. toctree:: :maxdepth: 2 :caption: Contents: Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` paperwork-2.1.1/openpaperwork-gtk/l10n/000077500000000000000000000000001417573700700177715ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/l10n/de.po000066400000000000000000000140431417573700700207230ustar00rootroot00000000000000# German translations for PACKAGE package. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2021-11-28 20:35+0000\n" "Last-Translator: LAZIC Anna <0.0.0.0.0.ffff.255.255.255.255@gmail.com>\n" "Language-Team: German \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.9\n" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/zip.py:56 msgid "ZIP file" msgstr "ZIP Datei" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/zip.py:61 #, python-format msgid "" "Build a ZIP file containing all the attachments.\n" " If you want, you can then submit a bug report manually on Paperwork's bug tracker and attach this ZIP file to the ticket." msgstr "" "Erstelle eine ZIP Datei mit allen Anhängen.\n" "Die ZIP Datei kann als Anhang für einen Fehlerbericht im Bug " "Tracker von Paperwork verwendet werden." #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:40 msgid "Creating bug report ..." msgstr "Erstelle Fehlerbericht ..." #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:42 msgid "Sending bug report attachment ..." msgstr "Sende Fehlerberichte als Anhänge ..." #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:161 msgid "Send automatically" msgstr "Automatisch senden" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:162 msgid "Send the bug report automatically to OpenPaper.work" msgstr "Sende den Fehlerbericht automatisch an OpenPaper.work" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:262 msgid "Success" msgstr "Erfolg" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:278 #, python-format msgid "" "Transfer failed:\n" "\n" "%s" msgstr "" "Übertragung fehlgeschlagen:\n" "\n" "%s" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:284 msgid "FAILED" msgstr "FEHLGESCHLAGEN" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/__init__.py:77 msgid "Now" msgstr "Jetzt" #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:258 #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:266 msgid "App. screenshots" msgstr "Bildschirmfotos" #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:267 msgid "Select to generate" msgstr "Wähle zum Erzeugen" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/lines.py:417 msgid "Total: {}" msgstr "Gesamt: {}" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/lines.py:427 msgid "Total" msgstr "Gesamt" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:1 msgid "Unexpected Error" msgstr "Unerwarteter Fehler" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:2 msgid "Report" msgstr "Bericht" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:3 msgid "An unexpected error occured." msgstr "Ein unerwarteter Fehler ist aufgetreten." #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:3 msgid "Date" msgstr "Datum" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:5 msgid "File" msgstr "Datei" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:6 msgid "Size" msgstr "Dateigröße" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:7 msgid "Open file" msgstr "Datei öffnen" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:8 msgid "For your privacy, attachments may be partially censored when selected" msgstr "Aus Datenschutzgründen könnten die Anhänge teilweise zensiert werden" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:9 msgid "Attachments to include in the bug report" msgstr "Anhänge für den Fehlerbericht" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:1 msgid "Bug description" msgstr "Fehlerbeschreibung" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:2 msgid "Required if you agree to be contacted regarding this bug report" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:3 msgid "Your email address" msgstr "Ihre Email Adresse" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:4 msgid "" "Bug report successfully submitted.\n" "\n" "You can find below the private link to your bug report.\n" "\n" "Be careful before sharing it. Bug report attachments (logs, " "screenshots, …) do not contain any private information. Still you're " "strongly advised to check them all before sharing this link.\n" "\n" "You can request deletion of your bug report by sending an email to jflesch@openpaper.work." msgstr "" "Die Fehlerbeschreibung wurde erfolgreich übermittelt.\n" "\n" "Weiter unten erhältst einen geheimen Link zu deinem Fehlerbericht.\n" "\n" "Sei vorsichtig, mit wem du diesen Link teilst. Die Anhänge für den " "Fehlerbericht beinhalten keine privaten Informationen. Trotzdem solltest du " "sie prüfen, bevor du den Link weitergibst.\n" "\n" "Du kannst jederzeit eine Löschung deines Fehlerberichts verlangen. Schreibe " "dafür eine E-Mail an jflesch@openpaper.work." #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_zip.glade.h:1 msgid "Please select where to save the ZIP file" msgstr "Bitte wähle einen Ablageort für die ZIP Datei aus" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/progress/progress_button.glade.h:1 msgid "Show background tasks" msgstr "Zeige Hintergrundaktivitäten" #~ msgid "Anonymous" #~ msgstr "Anonym" #~ msgid "Bug Report Author" #~ msgstr "Fehlerbericht Author" paperwork-2.1.1/openpaperwork-gtk/l10n/es.po000066400000000000000000000110541417573700700207410ustar00rootroot00000000000000# Spanish translations for PACKAGE package. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2020-05-03 15:37+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/zip.py:56 msgid "ZIP file" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/zip.py:61 #, python-format msgid "" "Build a ZIP file containing all the attachments.\n" " If you want, you can then submit a bug report manually on Paperwork's bug tracker and attach this ZIP file to the ticket." msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:40 msgid "Creating bug report ..." msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:42 msgid "Sending bug report attachment ..." msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:161 msgid "Send automatically" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:162 msgid "Send the bug report automatically to OpenPaper.work" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:262 msgid "Success" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:278 #, python-format msgid "" "Transfer failed:\n" "\n" "%s" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:284 msgid "FAILED" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/__init__.py:77 msgid "Now" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:258 #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:266 msgid "App. screenshots" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:267 msgid "Select to generate" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/lines.py:417 msgid "Total: {}" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/lines.py:427 msgid "Total" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:1 msgid "Unexpected Error" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:2 msgid "Report" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:3 msgid "An unexpected error occured." msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:3 msgid "Date" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:5 msgid "File" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:6 msgid "Size" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:7 msgid "Open file" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:8 msgid "For your privacy, attachments may be partially censored when selected" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:9 msgid "Attachments to include in the bug report" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:1 msgid "Bug description" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:2 msgid "Required if you agree to be contacted regarding this bug report" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:3 msgid "Your email address" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:4 msgid "" "Bug report successfully submitted.\n" "\n" "You can find below the private link to your bug report.\n" "\n" "Be careful before sharing it. Bug report attachments (logs, " "screenshots, …) do not contain any private information. Still you're " "strongly advised to check them all before sharing this link.\n" "\n" "You can request deletion of your bug report by sending an email to jflesch@openpaper.work." msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_zip.glade.h:1 msgid "Please select where to save the ZIP file" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/progress/progress_button.glade.h:1 msgid "Show background tasks" msgstr "" paperwork-2.1.1/openpaperwork-gtk/l10n/fr.po000066400000000000000000000145751417573700700207540ustar00rootroot00000000000000# French translations for PACKAGE package # Traductions françaises du paquet PACKAGE. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2021-11-28 20:35+0000\n" "Last-Translator: Jerome Flesch \n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 4.9\n" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/zip.py:56 msgid "ZIP file" msgstr "Fichier ZIP" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/zip.py:61 #, python-format msgid "" "Build a ZIP file containing all the attachments.\n" " If you want, you can then submit a bug report manually on Paperwork's bug tracker and attach this ZIP file to the ticket." msgstr "" "Crée un fichier ZIP qui contient tous les documents.\n" "Si vous le souhaitez, vous pouvez soumettre un rapport de bogue manuellement " "sur le suivi de bogue de paperwork et joignez ce fichier " "ZIP au ticket." #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:40 msgid "Creating bug report ..." msgstr "Création d'un rapport de bogue…" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:42 msgid "Sending bug report attachment ..." msgstr "Envoi de la pièce jointe du rapport de bogue…" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:161 msgid "Send automatically" msgstr "Envoyer automatiquement" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:162 msgid "Send the bug report automatically to OpenPaper.work" msgstr "Envoyer le rapport de bug automatiquement à OpenPaper.work" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:262 msgid "Success" msgstr "Succès" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:278 #, python-format msgid "" "Transfer failed:\n" "\n" "%s" msgstr "" "Le transfert a échoué :\n" "\n" "%s" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:284 msgid "FAILED" msgstr "ÉCHEC" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/__init__.py:77 msgid "Now" msgstr "Maintenant" #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:258 #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:266 msgid "App. screenshots" msgstr "Captures d'écran de l'app." #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:267 msgid "Select to generate" msgstr "Sélectionnez pour générer" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/lines.py:417 msgid "Total: {}" msgstr "Total : {}" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/lines.py:427 msgid "Total" msgstr "Total" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:1 msgid "Unexpected Error" msgstr "Erreur inattendue" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:2 msgid "Report" msgstr "Signaler" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:3 msgid "An unexpected error occured." msgstr "Une erreur inattendue s'est produite." #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:3 msgid "Date" msgstr "Date" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:5 msgid "File" msgstr "Fichier" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:6 msgid "Size" msgstr "Taille" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:7 msgid "Open file" msgstr "Ouvrir un fichier" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:8 msgid "For your privacy, attachments may be partially censored when selected" msgstr "" "Pour protéger votre vie privée, les fichiers joints peuvent être " "partiellement censurés quand vous les sélectionnez" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:9 msgid "Attachments to include in the bug report" msgstr "Fichiers à joindre à votre rapport de bogue" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:1 msgid "Bug description" msgstr "Description du bogue" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:2 msgid "Required if you agree to be contacted regarding this bug report" msgstr "Requis si vous acceptez d'être contacté au sujet de ce rapport de bug" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:3 msgid "Your email address" msgstr "Votre adresse email" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:4 msgid "" "Bug report successfully submitted.\n" "\n" "You can find below the private link to your bug report.\n" "\n" "Be careful before sharing it. Bug report attachments (logs, " "screenshots, …) do not contain any private information. Still you're " "strongly advised to check them all before sharing this link.\n" "\n" "You can request deletion of your bug report by sending an email to jflesch@openpaper.work." msgstr "" "Le rapport de bogue a été envoyé avec succès.\n" "\n" "Vous pouvez trouver le lien privé de votre rapport de bogue ci-dessous.\n" "\n" "Soyez prudent en partageant ce lien. Les fichiers joints au rapport " "de bogue (journaux, captures d'écran,…) ne contiennent pas d'informations " "privées. Mais il est vivement conseillé de les vérifier avant de partager ce " "lien.\n" "\n" "Vous pouvez demander la suppression de votre rapport de bogue en envoyant un " "courriel à cette adresse jflesch@openpaper.work." #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_zip.glade.h:1 msgid "Please select where to save the ZIP file" msgstr "Veuillez sélectionner la destination où sauvegarder le fichier ZIP" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/progress/progress_button.glade.h:1 msgid "Show background tasks" msgstr "Afficher les tâches de fond" #~ msgid "Anonymous" #~ msgstr "Anonyme" #~ msgid "Bug Report Author" #~ msgstr "Auteur du rapport de bug" paperwork-2.1.1/openpaperwork-gtk/l10n/messages.pot000066400000000000000000000110111417573700700223160ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/zip.py:56 msgid "ZIP file" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/zip.py:61 #, python-format msgid "" "Build a ZIP file containing all the attachments.\n" " If you want, you can then submit a bug report manually on Paperwork's bug tracker and attach this ZIP file to the ticket." msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:40 msgid "Creating bug report ..." msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:42 msgid "Sending bug report attachment ..." msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:161 msgid "Send automatically" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:162 msgid "Send the bug report automatically to OpenPaper.work" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:262 msgid "Success" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:278 #, python-format msgid "" "Transfer failed:\n" "\n" "%s" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:284 msgid "FAILED" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/__init__.py:77 msgid "Now" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:258 #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:266 msgid "App. screenshots" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:267 msgid "Select to generate" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/lines.py:417 msgid "Total: {}" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/lines.py:427 msgid "Total" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:1 msgid "Unexpected Error" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:2 msgid "Report" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:3 msgid "An unexpected error occured." msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:3 msgid "Date" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:5 msgid "File" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:6 msgid "Size" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:7 msgid "Open file" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:8 msgid "For your privacy, attachments may be partially censored when selected" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:9 msgid "Attachments to include in the bug report" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:1 msgid "Bug description" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:2 msgid "Required if you agree to be contacted regarding this bug report" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:3 msgid "Your email address" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:4 msgid "" "Bug report successfully submitted.\n" "\n" "You can find below the private link to your bug report.\n" "\n" "Be careful before sharing it. Bug report attachments (logs, " "screenshots, …) do not contain any private information. Still you're " "strongly advised to check them all before sharing this link.\n" "\n" "You can request deletion of your bug report by sending an email to jflesch@openpaper.work." msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_zip.glade.h:1 msgid "Please select where to save the ZIP file" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/progress/progress_button.glade.h:1 msgid "Show background tasks" msgstr "" paperwork-2.1.1/openpaperwork-gtk/l10n/oc.po000066400000000000000000000143561417573700700207430ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2020-09-04 01:05+0000\n" "Last-Translator: Quentin PAGÈS \n" "Language-Team: Occitan \n" "Language: oc\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 4.1.1\n" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/zip.py:56 msgid "ZIP file" msgstr "Fichièr ZIP" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/zip.py:61 #, python-format msgid "" "Build a ZIP file containing all the attachments.\n" " If you want, you can then submit a bug report manually on Paperwork's bug tracker and attach this ZIP file to the ticket." msgstr "" "Crear un fichièr ZIP que conten totes los documents.\n" "Se volètz, podètz enviar un senhalament de bug manualament sul seguiment de bugs de paperwork e ajustar aqueste fichièr ZIP al " "ticket." #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:40 msgid "Creating bug report ..." msgstr "Creacion d’un senhalament de bug…" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:42 msgid "Sending bug report attachment ..." msgstr "Mandadís de la pèça-junta del senhalament de bug…" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:161 msgid "Send automatically" msgstr "Enviar automaticament" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:162 msgid "Send the bug report automatically to OpenPaper.work" msgstr "Enviar lo senhalament de bug automaticament a OpenPaper.work" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:262 msgid "Success" msgstr "Succès" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:278 #, python-format msgid "" "Transfer failed:\n" "\n" "%s" msgstr "" "Lo transferiment a pas reüssit :\n" "\n" "%s" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:284 msgid "FAILED" msgstr "FRACÀS" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/__init__.py:77 msgid "Now" msgstr "Ara" #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:258 #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:266 msgid "App. screenshots" msgstr "Captura d’ecran de l’aplicacion" #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:267 msgid "Select to generate" msgstr "Seleccionar per generar" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/lines.py:417 msgid "Total: {}" msgstr "Total : {}" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/lines.py:427 msgid "Total" msgstr "Total" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:1 msgid "Unexpected Error" msgstr "Error inesperada" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:2 msgid "Report" msgstr "Senhalar" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:3 msgid "An unexpected error occured." msgstr "Una error inesperada s’es producha." #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:3 msgid "Date" msgstr "Data" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:5 msgid "File" msgstr "Fichièr" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:6 msgid "Size" msgstr "Talha" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:7 msgid "Open file" msgstr "Dobrís lo fichièr" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:8 msgid "For your privacy, attachments may be partially censored when selected" msgstr "" "Per protegir vòstra vida privada, los fichièrs junts pòdon èsser " "parcialament censurat quand los seleccionatz" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:9 msgid "Attachments to include in the bug report" msgstr "Fichièrs de juntar al senhalament de bug" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:1 msgid "Bug description" msgstr "Descripcion del bug" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:2 msgid "Required if you agree to be contacted regarding this bug report" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:3 msgid "Your email address" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:4 msgid "" "Bug report successfully submitted.\n" "\n" "You can find below the private link to your bug report.\n" "\n" "Be careful before sharing it. Bug report attachments (logs, " "screenshots, …) do not contain any private information. Still you're " "strongly advised to check them all before sharing this link.\n" "\n" "You can request deletion of your bug report by sending an email to jflesch@openpaper.work." msgstr "" "Lo senhalament de bug es corrèctament estat enviat.\n" "\n" "Traparetz çai-jos lo ligam privat de vòstre senhalament de bug.\n" "\n" "Siatz prudents en partejant aqueste ligam. Las pèças juntas del " "senhalament de bug (jornals, capturas d’ecran, …) contenon pas cap " "d’informacions privadas mas seriatz ben avisats de las verificar abans de " "partejar aqueste ligam.\n" "\n" "Podètz demandar la supression de vòstre senhalament de bug en escrivent a " "l’adreça seguenta jflesch@openpaper.work." #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_zip.glade.h:1 msgid "Please select where to save the ZIP file" msgstr "Mercés de seleccionar la destinacion de la salvagarda del fichièr ZIP" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/progress/progress_button.glade.h:1 msgid "Show background tasks" msgstr "Afichar los prètzfaches de fons" #~ msgid "Anonymous" #~ msgstr "Anonim" #~ msgid "Bug Report Author" #~ msgstr "Autor del senhalament del bug" paperwork-2.1.1/openpaperwork-gtk/l10n/sv.po000066400000000000000000000134231417573700700207640ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-09-01 22:01+0200\n" "PO-Revision-Date: 2021-01-04 15:31+0000\n" "Last-Translator: Åke Engelbrektson \n" "Language-Team: Swedish \n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.4\n" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/zip.py:56 msgid "ZIP file" msgstr "ZIP-fil" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/zip.py:61 #, python-format msgid "" "Build a ZIP file containing all the attachments.\n" " If you want, you can then submit a bug report manually on Paperwork's bug tracker and attach this ZIP file to the ticket." msgstr "" "Skapa en ZIP-fil innehållande alla bilagor.\n" "Om du vill kan du sedan manuellt skicka in en felrapport påPaperwork's bug tracker och bifoga ZIP-filen i ärendet." #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:40 msgid "Creating bug report ..." msgstr "Skapar felrapport..." #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:42 msgid "Sending bug report attachment ..." msgstr "Skickar felrapportbilagor..." #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:161 msgid "Send automatically" msgstr "Skicka automatiskt" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:162 msgid "Send the bug report automatically to OpenPaper.work" msgstr "Skicka felrapporten automatiskt till OpenPaper.work" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:262 msgid "Success" msgstr "Klart" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:278 #, python-format msgid "" "Transfer failed:\n" "\n" "%s" msgstr "" "Överföring misslyckades:\n" "\n" "%s" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:284 msgid "FAILED" msgstr "MISSLYCKADES" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/__init__.py:77 msgid "Now" msgstr "Nu" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/lines.py:417 msgid "Total: {}" msgstr "Totalt: {}" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/lines.py:427 msgid "Total" msgstr "Totalt" #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:258 #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:266 msgid "App. screenshots" msgstr "App. skärmklipp" #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:267 msgid "Select to generate" msgstr "Välj för att generera" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_zip.glade.h:1 msgid "Please select where to save the ZIP file" msgstr "Välj var ZIP-filen skall sparas" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:1 msgid "Bug description" msgstr "Felbeskrivning" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:2 msgid "Anonymous" msgstr "Anonymt" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:3 msgid "Bug Report Author" msgstr "Felrapportförfattare" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:4 msgid "" "Bug report successfully submitted.\n" "\n" "You can find below the private link to your bug report.\n" "\n" "Be careful before sharing it. Bug report attachments (logs, " "screenshots, …) do not contain any private information. Still you're " "strongly advised to check them all before sharing this link.\n" "\n" "You can request deletion of your bug report by sending an email to jflesch@openpaper.work." msgstr "" "Felrapport skickad.\n" "\n" "Du hittar den privata länken till felrapporten nedan.\n" "\n" "Var försiktig med att dela den. Felrapportbilagor (loggar, skärmklipp " "m.m.) innehåller ingen personlig information. Ändå rekommenderas du starkt " "att kontrollera dem alla innan du delar denna länk.\n" "\n" "Du kan begära radering av din felrapport genom att skicka ett e-" "postmeddelande till jflesch@openpaper.work." #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:3 msgid "Date" msgstr "Datum" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:5 msgid "File" msgstr "Fil" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:6 msgid "Size" msgstr "Storlek" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:7 msgid "Open file" msgstr "Öppna fil" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:8 msgid "For your privacy, attachments may be partially censored when selected" msgstr "För din integritets skull, kan bilagor delvis censureras när de väljs" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:9 msgid "Attachments to include in the bug report" msgstr "Bilagor att inkludera i felrapporten" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:1 msgid "Unexpected Error" msgstr "Oväntat fel" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:2 msgid "Report" msgstr "Rapport" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:3 msgid "An unexpected error occured." msgstr "Ett oväntat fel inträffade." #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/progress/progress_button.glade.h:1 msgid "Show background tasks" msgstr "Visa bakgrundsåtgärder" paperwork-2.1.1/openpaperwork-gtk/l10n/uk.po000066400000000000000000000111731417573700700207530ustar00rootroot00000000000000# Ukrainian translations for PACKAGE package. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2020-05-03 15:37+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: uk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "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);\n" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/zip.py:56 msgid "ZIP file" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/zip.py:61 #, python-format msgid "" "Build a ZIP file containing all the attachments.\n" " If you want, you can then submit a bug report manually on Paperwork's bug tracker and attach this ZIP file to the ticket." msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:40 msgid "Creating bug report ..." msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:42 msgid "Sending bug report attachment ..." msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:161 msgid "Send automatically" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:162 msgid "Send the bug report automatically to OpenPaper.work" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:262 msgid "Success" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:278 #, python-format msgid "" "Transfer failed:\n" "\n" "%s" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:284 msgid "FAILED" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/__init__.py:77 msgid "Now" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:258 #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:266 msgid "App. screenshots" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:267 msgid "Select to generate" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/lines.py:417 msgid "Total: {}" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/lines.py:427 msgid "Total" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:1 msgid "Unexpected Error" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:2 msgid "Report" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:3 msgid "An unexpected error occured." msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:3 msgid "Date" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:5 msgid "File" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:6 msgid "Size" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:7 msgid "Open file" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:8 msgid "For your privacy, attachments may be partially censored when selected" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:9 msgid "Attachments to include in the bug report" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:1 msgid "Bug description" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:2 msgid "Required if you agree to be contacted regarding this bug report" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:3 msgid "Your email address" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:4 msgid "" "Bug report successfully submitted.\n" "\n" "You can find below the private link to your bug report.\n" "\n" "Be careful before sharing it. Bug report attachments (logs, " "screenshots, …) do not contain any private information. Still you're " "strongly advised to check them all before sharing this link.\n" "\n" "You can request deletion of your bug report by sending an email to jflesch@openpaper.work." msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_zip.glade.h:1 msgid "Please select where to save the ZIP file" msgstr "" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/progress/progress_button.glade.h:1 msgid "Show background tasks" msgstr "" paperwork-2.1.1/openpaperwork-gtk/l10n/zh_Hans.po000066400000000000000000000132301417573700700217220ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-09-01 22:01+0200\n" "PO-Revision-Date: 2021-02-06 07:20+0000\n" "Last-Translator: 玉堂白鹤 \n" "Language-Team: Chinese (Simplified) \n" "Language: zh_Hans\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Weblate 4.4\n" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/zip.py:56 msgid "ZIP file" msgstr "ZIP 文件" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/zip.py:61 #, python-format msgid "" "Build a ZIP file containing all the attachments.\n" " If you want, you can then submit a bug report manually on Paperwork's bug tracker and attach this ZIP file to the ticket." msgstr "" "构建包含所有附件的ZIP文件。\n" "如果需要,您可以在Paperwork 追踪系统上手动提交 bug 报告,并将此 ZIP 文件附加到报告。" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:40 msgid "Creating bug report ..." msgstr "创建 bug 报告..." #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:42 msgid "Sending bug report attachment ..." msgstr "发送错误报告附件 ..." #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:161 msgid "Send automatically" msgstr "自动发送" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:162 msgid "Send the bug report automatically to OpenPaper.work" msgstr "将错误报告自动发送 OpenPaper.work" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:262 msgid "Success" msgstr "成功" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:278 #, python-format msgid "" "Transfer failed:\n" "\n" "%s" msgstr "" "传输失败:\n" "\n" "%s" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py:284 msgid "FAILED" msgstr "失败" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/__init__.py:77 msgid "Now" msgstr "现在" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/lines.py:417 msgid "Total: {}" msgstr "总计: {}" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/lines.py:427 msgid "Total" msgstr "总计" #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:258 #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:266 msgid "App. screenshots" msgstr "App. 截屏" #: openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py:267 msgid "Select to generate" msgstr "选择生成" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_zip.glade.h:1 msgid "Please select where to save the ZIP file" msgstr "请选择要保存 ZIP 文件的位置" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:1 msgid "Bug description" msgstr "Bug 详情" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:2 msgid "Anonymous" msgstr "匿名" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:3 msgid "Bug Report Author" msgstr "Bug 报告者" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade.h:4 msgid "" "Bug report successfully submitted.\n" "\n" "You can find below the private link to your bug report.\n" "\n" "Be careful before sharing it. Bug report attachments (logs, " "screenshots, …) do not contain any private information. Still you're " "strongly advised to check them all before sharing this link.\n" "\n" "You can request deletion of your bug report by sending an email to jflesch@openpaper.work." msgstr "" "Bug 报告已成功提交。\n" "\n" "您可以在下面找到您的 bug 报告的私人链接。\n" "\n" "分享前请小心。Bug 报告附件(日志、屏幕截图等…)不要包含任何私人信息。强烈建议您在共享此链接之前检查所有链接。\n" "\n" "您可以通过向jflesch@openpaper.work发送电子邮件请求删除此 bug 报告。" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:3 msgid "Date" msgstr "日期" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:5 msgid "File" msgstr "文件" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:6 msgid "Size" msgstr "大小" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:7 msgid "Open file" msgstr "打开文件" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:8 msgid "For your privacy, attachments may be partially censored when selected" msgstr "为保护您的隐私,选择附件时可能会对其进行部分审查" #: openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade.h:9 msgid "Attachments to include in the bug report" msgstr "要包含在 bug 报告中的附件" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:1 msgid "Unexpected Error" msgstr "意外错误" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:2 msgid "Report" msgstr "报告" #: openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade.h:3 msgid "An unexpected error occured." msgstr "发生意外错误。" #: openpaperwork-gtk/src/openpaperwork_gtk/widgets/progress/progress_button.glade.h:1 msgid "Show background tasks" msgstr "显示后台任务" paperwork-2.1.1/openpaperwork-gtk/setup.cfg000066400000000000000000000000661417573700700210420ustar00rootroot00000000000000[tool:pytest] addopts = -ra python_files = tests_*.py paperwork-2.1.1/openpaperwork-gtk/setup.py000066400000000000000000000032021417573700700207260ustar00rootroot00000000000000#!/usr/bin/env python3 import os import sys from setuptools import setup, find_packages quiet = '--quiet' in sys.argv or '-q' in sys.argv try: with open("src/openpaperwork_gtk/_version.py", "r") as file_descriptor: version = file_descriptor.read().strip() version = version.split(" ")[2][1:-1] if not quiet: print("OpenPaperwork-gtk version: {}".format(version)) if "-" in version: version = version.split("-")[0] except FileNotFoundError: print("ERROR: _version.py file is missing") print("ERROR: Please run 'make version' first") sys.exit(1) setup( name="openpaperwork-gtk", version=version, description=( "OpenPaperwork GTK plugins" ), long_description="""Paperwork is a GUI to make papers searchable. A bunch of plugins for Paperwork related to GLib and GTK. """, url=( "https://gitlab.gnome.org/World/OpenPaperwork/paperwork/tree/master/" "openpaperwork-gtk" ), download_url=( "https://gitlab.gnome.org/World/OpenPaperwork/paperwork/-" "/archive/{}/paperwork-{}.tar.gz".format(version, version) ), classifiers=[ "Development Status :: 5 - Production/Stable", ("License :: OSI Approved ::" " GNU General Public License v3 or later (GPLv3+)"), "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", ], license="GPLv3+", author="Jerome Flesch", author_email="jflesch@openpaper.work", packages=find_packages('src'), package_dir={'': 'src'}, include_package_data=True, zip_safe=(os.name != 'nt'), install_requires=[] ) paperwork-2.1.1/openpaperwork-gtk/src/000077500000000000000000000000001417573700700200065ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/000077500000000000000000000000001417573700700235475ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/__init__.py000066400000000000000000000015111417573700700256560ustar00rootroot00000000000000import gettext def _(s): return gettext.dgettext('openpaperwork_gtk', s) CLI_PLUGINS = [ # plugins that can make sense to use even in the CLI application 'openpaperwork_gtk.external_apps.gio', 'openpaperwork_gtk.fs.gio', 'openpaperwork_gtk.l10n', 'openpaperwork_gtk.mainloop.glib', ] GUI_PLUGINS = [ 'openpaperwork_gtk.bug_report', 'openpaperwork_gtk.bug_report.http', 'openpaperwork_gtk.bug_report.zip', 'openpaperwork_gtk.busy.mouse', 'openpaperwork_gtk.colors', 'openpaperwork_gtk.dialogs.single_entry', 'openpaperwork_gtk.dialogs.yes_no', 'openpaperwork_gtk.pixbuf.pillow', 'openpaperwork_gtk.resources', 'openpaperwork_gtk.screenshots', 'openpaperwork_gtk.uncaught_exception', 'openpaperwork_gtk.widgets.calendar', 'openpaperwork_gtk.widgets.progress', ] paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/bug_report/000077500000000000000000000000001417573700700257175ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/bug_report/__init__.py000066400000000000000000000243621417573700700300370ustar00rootroot00000000000000import datetime import logging import openpaperwork_core import openpaperwork_core.deps from .. import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = -100 def __init__(self): super().__init__() self.url_selected = None self.method_radio = None self.method_set_file_urls_callbacks = {} self.windows = [] self.widget_tree = None def get_interfaces(self): return [ 'gtk_bug_report_dialog', 'gtk_window_listener', 'screenshot_provider', ] def get_deps(self): return [ { 'interface': 'external_apps', 'defaults': [ 'openpaperwork_core.external_apps.dbus', 'openpaperwork_core.external_apps.windows', 'openpaperwork_core.external_apps.xdg', ], }, { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, { 'interface': 'screenshot', 'defaults': ['openpaperwork_gtk.screenshots'], }, ] def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def _shorten_url(self, url): if "://" not in url: return url dir_path = self.core.call_success("fs_dirname", url) dir_name = self.core.call_success("fs_basename", dir_path) if dir_name is None or dir_name == "": return url file_name = self.core.call_success("fs_basename", url) return dir_name + "/" + file_name def _format_date(self, date): if date is None: return _("Now") return date.strftime("%c") def _format_timestamp(self, date): if date is None: date = datetime.datetime.now() return int(date.timestamp()) def _refresh_attachment_page_complete(self, widget_tree): # check we have at least one entry toggled model = widget_tree.get_object("bug_report_model") assistant = widget_tree.get_object("bug_report_dialog") page = widget_tree.get_object("bug_report_attachment_selector") enabled = False for row in model: if row[0]: enabled = True if row[0] and "://" not in row[4]: enabled = False break assistant.set_page_complete(page, enabled) def _i18n_file_size(self, file_size): if isinstance(file_size, str) or file_size <= 0: return "" return self.core.call_success("i18n_file_size", file_size) def open_bug_report(self): self.method_radio = None self.url_selected = None self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "openpaperwork_gtk.bug_report", "bug_report.glade" ) dialog = self.widget_tree.get_object("bug_report_dialog") self.core.call_all("bug_report_complete", self.widget_tree) self.widget_tree.get_object("bug_report_toggle_renderer").connect( "toggled", self._on_attachment_toggle, self.widget_tree ) self.widget_tree.get_object("bug_report_treeview").connect( "row-activated", self._on_row_selected, self.widget_tree ) self.widget_tree.get_object("bug_report_open_file").connect( "clicked", self._open_selected ) model = self.widget_tree.get_object("bug_report_model") model.clear() inputs = {} self.core.call_all("bug_report_get_attachments", inputs) now = datetime.datetime.now() for (k, v) in inputs.items(): v['id'] = k v['sort_date'] = ( v['date'].timestamp() if v['date'] is not None else now.timestamp() ) inputs = list(inputs.values()) inputs.sort( key=lambda i: (-i['sort_date'], i['file_type'], i['file_url']), ) for i in inputs: model.append( [ i['include_by_default'], i['sort_date'], self._format_date(i['date']), i['file_type'], i['file_url'], self._shorten_url(i['file_url']), i['file_size'], self._i18n_file_size(i['file_size']), i['id'], ] ) for i in inputs: if not i['include_by_default']: continue self.core.call_all( "on_bug_report_attachment_selected", i['id'], self.widget_tree ) if len(self.windows) > 0: dialog.set_transient_for(self.windows[-1]) dialog.connect("close", self._on_close) dialog.connect("cancel", self._on_close) dialog.connect("prepare", self._on_prepare, self.widget_tree) dialog.set_visible(True) self.core.call_all("on_gtk_window_opened", dialog) self._refresh_attachment_page_complete(self.widget_tree) def close_bug_report(self): dialog = self.widget_tree.get_object("bug_report_dialog") dialog.destroy() def _on_attachment_toggle(self, cell_renderer, row_path, widget_tree): model = widget_tree.get_object("bug_report_model") val = not model[row_path][0] model[row_path][0] = val if val: self.core.call_all( "on_bug_report_attachment_selected", model[row_path][-1], widget_tree ) else: self.core.call_all( "on_bug_report_attachment_unselected", model[row_path][-1], widget_tree ) self._refresh_attachment_page_complete(widget_tree) def _on_row_selected(self, treeview, row_path, column, widget_tree): self._on_url_selected(widget_tree) def _on_url_selected(self, widget_tree): button = widget_tree.get_object("bug_report_open_file") treeview = widget_tree.get_object("bug_report_treeview") model = widget_tree.get_object("bug_report_model") (has_selected, model_iter) = ( treeview.get_selection().get_selected() ) if not has_selected or model_iter is None: button.set_sensitive(False) return self.url_selected = model[model_iter][4] button.set_sensitive("://" in self.url_selected) def _open_selected(self, button): self.core.call_success("external_app_open_file", self.url_selected) def bug_report_update_attachment( self, attachment_id, infos: dict, widget_tree): model = widget_tree.get_object("bug_report_model") for row in model: if row[-1] != attachment_id: continue if 'date' in infos: row[1] = self._format_timestamp(infos['date']) row[2] = self._format_date(infos['date']) if 'file_type' in infos: row[3] = infos['file_type'] if 'file_url' in infos: row[4] = infos['file_url'] row[5] = self._shorten_url(infos['file_url']) if 'file_size' in infos: row[6] = infos['file_size'] row[7] = self._i18n_file_size(infos['file_size']) break self._on_url_selected(widget_tree) self._refresh_attachment_page_complete(widget_tree) def bug_report_get_attachment_file_url(self, attachment_id, widget_tree): model = widget_tree.get_object("bug_report_model") for row in model: if row[-1] != attachment_id: continue if "://" not in row[4]: return None return row[4] return None def _on_close(self, dialog): dialog.set_visible(False) self.core.call_all("on_gtk_window_closed", dialog) def bug_report_add_method( self, title, description, enable_callback, disable_callback, widget_tree): widget_tree_method = self.core.call_success( "gtk_load_widget_tree", "openpaperwork_gtk.bug_report", "bug_report_method.glade" ) widget_tree_method.get_object("bug_report_method_title").set_text( title ) widget_tree_method.get_object( "bug_report_method_description" ).set_markup(description) widget_tree.get_object("bug_report_methods").pack_start( widget_tree_method.get_object("bug_report_method"), expand=False, fill=False, padding=20 ) radio = widget_tree_method.get_object("bug_report_method_radio") radio.connect( "toggled", self._on_method_toggled, widget_tree, enable_callback, disable_callback ) if self.method_radio is None: self.method_radio = radio enable_callback(widget_tree) else: radio.join_group(self.method_radio) self.method_radio.set_active(True) def _on_method_toggled( self, radio, widget_tree, enable_callback, disable_callback): if radio.get_active(): enable_callback(widget_tree) else: disable_callback(widget_tree) def _on_prepare(self, assistant, page, widget_tree): self._set_file_urls(widget_tree) def _set_file_urls(self, widget_tree): model = widget_tree.get_object("bug_report_model") file_urls = [row[4] for row in model if row[0]] self.core.call_all("bug_report_set_file_urls_to_send", file_urls) def screenshot_snap_all_doc_widgets(self, out_dir): if self.widget_tree is None: return self.core.call_success( "screenshot_snap_widget", self.widget_tree.get_object("bug_report_dialog"), self.core.call_success("fs_join", out_dir, "bug_report.png") ) paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report.glade000066400000000000000000000247761417573700700311050ustar00rootroot00000000000000 False 12 Image /home/jflesch/.local/share/camion/21211212_crash_screenshot.png True 1 Text /home/jflesch/camion/20200101_logs.txt False Bug Spray 1000 520 True dialog 1 True False 20 20 20 20 0 none True False 12 True False vertical True True in True True bug_report_model 3 True True 0 Date 1 2 Type 3 3 File 4 5 Size 6 7 True True 0 True False Open file True False True True False True end 0 True False For your privacy, attachments may be partially censored when selected True False True 1 False True 1 True False 10 Attachments to include in the bug report False True False 20 20 20 20 vertical True True False paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_http.glade000066400000000000000000000157761417573700700321440ustar00rootroot00000000000000 True False vertical bug_report_http_description_page True False 20 20 0 none True False 12 True True in True True word bug_report_http_description_buffer GTK_INPUT_HINT_SPELLCHECK | GTK_INPUT_HINT_NONE True False 10 Bug description True True 0 True False 0 none True False 12 True True Required if you agree to be contacted regarding this bug report GTK_INPUT_HINT_NO_SPELLCHECK | GTK_INPUT_HINT_NONE True False 10 Your email address False True 1 True False center 20 20 20 20 True True True True False vertical True True center True Bug report successfully submitted. You can find below the private link to your bug report. <b>Be careful before sharing it.</b> Bug report attachments (logs, screenshots, …) do not contain any private information. Still you're strongly advised to check them all before sharing this link. You can request deletion of your bug report by sending an email to <a href="mailto:jflesch@openpaper.work">jflesch@openpaper.work</a>. True center True True False True 0 True True center True <a href="https://openpaper.work/pouetpouet">https://openpaper.work/pouetpouet</a> True True char True False True 1 paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_method.glade000066400000000000000000000042241417573700700324270ustar00rootroot00000000000000 True False vertical True True False True True True False 15 10 10 label 0 False True 0 True False 30 10 10 label True 0 True True 1 paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/bug_report/bug_report_zip.glade000066400000000000000000000033461417573700700317550ustar00rootroot00000000000000 application/zip True False 0 none True False 12 True True save filefilterZip False False True False 10 Please select where to save the ZIP file paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/bug_report/http.py000066400000000000000000000214771417573700700272630ustar00rootroot00000000000000import logging try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_core.promise from .. import (_, deps) LOGGER = logging.getLogger(__name__) PATH_MAKE_BUG_REPORT = "/beacon/bug_report/create" PATH_ADD_ATTACHMENT = "/beacon/bug_report/add_attachment" class Sender(object): def __init__(self, core, http, progress, description, file_urls): self.core = core self.http = http self.progress = progress self.description = description self.file_urls = file_urls self.infos = None self.nb_steps = len(self.file_urls) + 2 self.current_step = 0 def _notify_progress(self, *args, **kwargs): assert(self.current_step <= self.nb_steps) if self.current_step == 0: txt = _("Creating bug report ...") else: txt = _("Sending bug report attachment ...") self.core.call_all( "on_progress", "bug_report_http", self.current_step / self.nb_steps, txt ) self.progress.set_fraction(self.current_step / self.nb_steps) self.progress.set_text(txt) self.current_step += 1 def _set_report_infos(self, infos): self.infos = infos def _str_to_http(self, file_name, string): data = { "file_name": file_name, "binary": string.encode("utf-8") } data.update(self.infos) return data def _file_to_http(self, file_url): with self.core.call_success("fs_open", file_url, "rb") as fd: data = { "file_name": self.core.call_success("fs_basename", file_url), "binary": fd.read() } data.update(self.infos) return data def get_promise(self): LOGGER.info("Will send %s", self.file_urls) promise = openpaperwork_core.promise.Promise( self.core, self._notify_progress ) promise = promise.then(lambda *args, **kwargs: { "app_name": self.core.call_success("app_get_name"), "app_version": self.core.call_success("app_get_version"), }) promise = promise.then(self.http.get_request_promise( PATH_MAKE_BUG_REPORT )) promise = promise.then(self._set_report_infos) promise = promise.then(self._notify_progress) promise = promise.then( self._str_to_http, "description.txt", self.description ) promise = promise.then(self.http.get_request_promise( PATH_ADD_ATTACHMENT )) promise = promise.then(self._notify_progress) for file_url in self.file_urls: promise = promise.then(self._file_to_http, file_url) promise = promise.then(self.http.get_request_promise( PATH_ADD_ATTACHMENT )) promise = promise.then(self._notify_progress) return promise def get_report_url(self): return self.infos['url'] class Plugin(openpaperwork_core.PluginBase): PRIORITY = 100 def __init__(self): super().__init__() self.widget_tree = None self.assistant = None self.file_urls = [] self.http = None self.progress = None self.prepare_handler_id = None self.description = None def get_interfaces(self): return [ 'bug_report_method', 'chkdeps', ] def get_deps(self): return [ { 'interface': 'app', 'defaults': [], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'http_json', 'defaults': ['openpaperwork_core.http'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_core.mainloop.asyncio'], }, ] def init(self, core): super().init(core) self.http = self.core.call_success( "http_json_get_client", "bug_report" ) def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(deps.GTK) def bug_report_complete(self, *args): self.core.call_all( "bug_report_add_method", _("Send automatically"), _("Send the bug report automatically to OpenPaper.work"), self._enable_http, self._disable_http, *args ) def _enable_http(self, bug_report_widget_tree): self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "openpaperwork_gtk.bug_report", "bug_report_http.glade" ) assistant = bug_report_widget_tree.get_object("bug_report_dialog") self.assistant = assistant page = self.widget_tree.get_object("bug_report_http_description_page") description_page = page assistant.append_page(page) assistant.set_page_complete(page, False) self.description = self.widget_tree.get_object( "bug_report_http_description_buffer" ) self.description.connect( "changed", lambda txt_buffer: assistant.set_page_complete( description_page, txt_buffer.get_char_count() > 0 ) ) page = self.widget_tree.get_object("bug_report_http_progress") self.progress = page assistant.append_page(page) assistant.set_page_complete(page, False) page = self.widget_tree.get_object("bug_report_http_result") self.page_idx = assistant.append_page(page) assistant.set_page_complete(page, True) assistant.set_page_type(page, Gtk.AssistantPageType.CONFIRM) self.prepare_handler_id = assistant.connect( "prepare", self._on_prepare ) def _disable_http(self, bug_report_widget_tree): if self.widget_tree is None: return assistant = bug_report_widget_tree.get_object("bug_report_dialog") assistant.remove(self.widget_tree.get_object( "bug_report_http_description_page" )) assistant.remove(self.widget_tree.get_object( "bug_report_http_progress" )) assistant.remove(self.widget_tree.get_object( "bug_report_http_result" )) assistant.disconnect(self.prepare_handler_id) self.widget_tree = None def bug_report_set_file_urls_to_send(self, file_urls): self.file_urls = file_urls def _on_prepare(self, assistant, page): if page != self.progress: return self._send() def _get_description(self): author = self.widget_tree.get_object("bug_report_http_author") author = author.get_text() start = self.description.get_iter_at_offset(0) end = self.description.get_iter_at_offset(-1) description = self.description.get_text(start, end, False) return "Author: {}\n\n{}".format(author, description) def _send(self): promise = openpaperwork_core.promise.Promise( self.core, self.core.call_all, args=("on_busy",) ) sender = Sender( self.core, self.http, self.progress, self._get_description(), self.file_urls ) promise = promise.then(sender.get_promise()) promise = promise.then(self._show_result, sender) promise = promise.catch(self._on_error) promise.schedule() def _show_result(self, sender): self.core.call_all("on_idle") if self.widget_tree is None: return LOGGER.info("Transfer successful") url = self.widget_tree.get_object("bug_report_http_url") url.set_markup('{url}'.format( url=sender.get_report_url(), )) self.assistant.set_page_complete(self.progress, True) self.progress.set_fraction(1.0) self.progress.set_text(_("Success")) self.assistant.set_current_page( # no point in staying on the progress bar page self.assistant.get_current_page() + 1 ) def _on_error(self, exc): self.core.call_all("on_idle") if self.widget_tree is None: return LOGGER.error("Transfer failed", exc_info=exc) exc_txt = str(exc)[:256] txt = self.widget_tree.get_object("bug_report_http_result_txt") txt.set_text(_("Transfer failed:\n\n%s") % exc_txt) url = self.widget_tree.get_object("bug_report_http_url") url.set_text("") self.assistant.set_page_complete(self.progress, True) self.progress.set_fraction(1.0) self.progress.set_text(_("FAILED")) self.core.call_all("on_progress", "bug_report_http", 1.0) paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/bug_report/zip.py000066400000000000000000000077221417573700700271030ustar00rootroot00000000000000import logging import re import zipfile try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core from .. import (_, deps) LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = -100 def __init__(self): super().__init__() self.widget_tree = None self.file_urls = [] self.apply_handler_id = None def get_interfaces(self): return [ 'bug_report_method', 'chkdeps', ] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, ] def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(deps.GTK) def bug_report_complete(self, *args): url = "https://gitlab.gnome.org/World/OpenPaperwork/paperwork/issues" self.core.call_all( "bug_report_add_method", _("ZIP file"), re.sub( '[ \t]+', ' ', ( _( "Build a ZIP file containing all the attachments.\n" " If you want, you can then submit a bug report" ' manually on Paperwork\'s bug' " tracker and attach this ZIP file to the ticket." ) % url ).strip() ), self._enable_zip, self._disable_zip, *args ) def _enable_zip(self, bug_report_widget_tree): self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "openpaperwork_gtk.bug_report", "bug_report_zip.glade" ) assistant = bug_report_widget_tree.get_object("bug_report_dialog") page = self.widget_tree.get_object("bug_report_zip_page") assistant.append_page(page) assistant.set_page_complete(page, False) assistant.set_page_type(page, Gtk.AssistantPageType.CONFIRM) self.apply_handler_id = assistant.connect("apply", self._make_zip) filechooser = self.widget_tree.get_object("bug_report_zip_filechooser") filechooser.connect( "selection-changed", lambda filechooser: assistant.set_page_complete( page, filechooser.get_uri() is not None ) ) def _disable_zip(self, bug_report_widget_tree): if self.widget_tree is None: return assistant = bug_report_widget_tree.get_object("bug_report_dialog") page = self.widget_tree.get_object("bug_report_zip_page") assistant.remove(page) assistant.disconnect(self.apply_handler_id) self.widget_tree = None def bug_report_set_file_urls_to_send(self, file_urls): self.file_urls = file_urls def _make_zip(self, assistant): in_file_urls = self.file_urls out_file_url = self.widget_tree.get_object( "bug_report_zip_filechooser" ).get_uri() out_file_url = out_file_url.strip() if not out_file_url.lower().endswith(".zip"): out_file_url += ".zip" LOGGER.info("Adding attachments to %s", out_file_url) out_file_path = self.core.call_success("fs_unsafe", out_file_url) with zipfile.ZipFile(out_file_path, 'w') as out_zip: for in_file_url in in_file_urls: file_name = self.core.call_success("fs_basename", in_file_url) with self.core.call_success( "fs_open", in_file_url, "rb") as in_fd: with out_zip.open(file_name, 'w') as out_fd: out_fd.write(in_fd.read()) LOGGER.info("%s written", out_file_url) paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/busy/000077500000000000000000000000001417573700700245315ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/busy/__init__.py000066400000000000000000000000001417573700700266300ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/busy/mouse.py000066400000000000000000000056151417573700700262420ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.deps GI_AVAILABLE = False GDK_AVAILABLE = False try: import gi GI_AVAILABLE = True except (ImportError, ValueError): pass if GI_AVAILABLE: try: gi.require_version('Gdk', '3.0') from gi.repository import Gdk GDK_AVAILABLE = True except (ImportError, ValueError): pass LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.refcount = 0 self.windows = [] self.realize_handler_id = None def get_interfaces(self): return [ 'busy', 'chkdeps', 'gtk_window_listener', ] def get_deps(self): return [ { 'interface': 'gtk_mainwindow', 'defaults': ['paperwork_gtk.mainwindow.window'], }, ] def chkdeps(self, out: dict): if not GDK_AVAILABLE: out['gdk'].update(openpaperwork_core.deps.GDK) def _set_mouse_cursor(self, offset): self.refcount += offset assert(self.refcount >= 0) if len(self.windows) <= 0: LOGGER.warning( "Cannot change mouse cursor: no main window defined" ) return if self.refcount > 0: LOGGER.info("Mouse cursor --> busy") try: display = self.windows[-1].get_display() cursor = Gdk.Cursor.new_for_display( display, Gdk.CursorType.WATCH ) except TypeError as exc: # may happen with Wayland LOGGER.error("Failed to switch mouse cursor", exc_info=exc) return else: LOGGER.info("Mouse cursor --> idle") cursor = None self.windows[-1].get_window().set_cursor(cursor) def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def on_busy(self): self._set_mouse_cursor(1) def on_idle(self): self._set_mouse_cursor(-1) def on_widget_busyness_changed(self, widget, busy): if busy: cursor = None else: display = widget.get_display() cursor = Gdk.Cursor.new_for_display(display, Gdk.CursorType.WATCH) window = widget.get_window() def _set_cursor(*args, **kwargs): window = widget.get_window() window.set_cursor(cursor) if self.realize_handler_id is not None: widget.disconnect(self.realize_handler_id) self.realize_handler_id = None if window is not None: _set_cursor() else: # assuming the widget will be realized soon widget.connect("realize", _set_cursor) paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/colors.py000066400000000000000000000024331417573700700254240ustar00rootroot00000000000000try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core from . import deps class Plugin(openpaperwork_core.PluginBase): def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(deps.GTK) def get_interfaces(self): return [ 'chkdeps', 'gtk_colors', ] def gtk_entry_set_colors(self, gtk_entry, fg="black", bg="#CC3030"): css = """ * { color: %s; background: %s; } * selection { color: white; background: #3b84e9; } """ % (fg, bg) css_provider = Gtk.CssProvider() css_provider.load_from_data(css.encode()) css_context = gtk_entry.get_style_context() css_context.add_provider( css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER ) def gtk_entry_reset_colors(self, gtk_entry): self.gtk_entry_set_colors( gtk_entry, fg="@theme_text_color", bg="@theme_bg_color" ) def gtk_theme_get_color(self, color_name): style = Gtk.StyleContext() return style.lookup_color("theme_bg_color")[1] paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/deps.py000066400000000000000000000016001417573700700250510ustar00rootroot00000000000000GDK_PIXBUF = { 'debian': 'gir1.2-gdkpixbuf-2.0', 'linuxmint': 'gir1.2-gdkpixbuf-2.0', 'raspbian': 'gir1.2-gdkpixbuf-2.0', 'ubuntu': 'gir1.2-gdkpixbuf-2.0', } GTK = { 'debian': 'gir1.2-gtk-3.0', 'fedora': 'gtk3', 'gentoo': 'x11-libs/gtk+', 'linuxmint': 'gir1.2-gtk-3.0', 'raspbian': 'gir1.2-gtk-3.0', 'suse': 'python-gtk', 'ubuntu': 'gir1.2-gtk-3.0', } HDY = { 'debian': 'gir1.2-handy-1', 'fedora': 'libhandy1', 'gentoo': 'gui-libs/libhandy', 'linuxmint': 'gir1.2-handy-1', 'raspbian': 'gir1.2-handy-1', 'suse': 'libhandy-1-0', 'ubuntu': 'gir1.2-handy-1', } NOTIFY = { 'debian': 'gir1.2-notify-0.7', 'fedora': 'libnotify', 'ubuntu': 'gir1.2-notify-0.7', } PANGO = { 'debian': 'gir1.2-pango-1.0', 'linuxmint': 'gir1.2-pango-1.0', 'raspbian': 'gir1.2-pango-1.0', 'ubuntu': 'gir1.2-pango-1.0', } paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/dialogs/000077500000000000000000000000001417573700700251715ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/dialogs/__init__.py000066400000000000000000000000001417573700700272700ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/dialogs/single_entry/000077500000000000000000000000001417573700700276735ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/dialogs/single_entry/__init__.py000066400000000000000000000045371417573700700320150ustar00rootroot00000000000000import logging try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_gtk.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.windows = [] def get_interfaces(self): return [ 'chkdeps', 'gtk_dialog_single_entry', 'gtk_window_listener', ] def get_deps(self): return [ { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, ] def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def gtk_show_dialog_single_entry( self, origin, title, original_value, *args, **kwargs): widget_tree = self.core.call_success( "gtk_load_widget_tree", "openpaperwork_gtk.dialogs.single_entry", "single_entry.glade" ) widget_tree.get_object("entry").set_text(original_value) dialog = widget_tree.get_object("dialog") dialog.set_title(title) dialog.set_transient_for(self.windows[-1]) dialog.set_modal(True) dialog.connect( "response", self._on_response, (widget_tree, origin, args, kwargs) ) dialog.show_all() return True def _on_response(self, dialog, response_id, args): (widget_tree, origin, args, kwargs) = args new_value = widget_tree.get_object("entry").get_text() dialog.destroy() if (response_id != 0 and response_id != Gtk.ResponseType.ACCEPT and response_id != Gtk.ResponseType.OK and response_id != Gtk.ResponseType.YES and response_id != Gtk.ResponseType.APPLY): LOGGER.info("User cancelled") r = False else: r = True self.core.call_all( "on_dialog_single_entry_reply", origin, r, new_value, *args, **kwargs ) paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/dialogs/single_entry/single_entry.glade000066400000000000000000000056561417573700700334070ustar00rootroot00000000000000 False 30 dialog False vertical 2 False end gtk-cancel True True True True True True 0 gtk-ok True True True True True True 1 False False 0 True True False True 1 button_cancel button_validate paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/dialogs/yes_no.py000066400000000000000000000040101417573700700270320ustar00rootroot00000000000000import logging try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_core.promise import openpaperwork_gtk.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): """ Provides a simple way to show a Yes/No question popup (usually something along the line of "are you really really really sure you want to do that ?"). """ def __init__(self): super().__init__() self.windows = [] def get_interfaces(self): return [ 'chkdeps', 'gtk_dialog_yes_no', 'gtk_window_listener', ] def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def gtk_show_dialog_yes_no(self, origin, msg, *args, **kwargs): confirm = Gtk.MessageDialog( parent=self.windows[-1], flags=( Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT ), message_type=Gtk.MessageType.WARNING, buttons=Gtk.ButtonsType.YES_NO, text=msg ) confirm.connect("response", self._on_response, origin, (args, kwargs)) confirm.show_all() return True def _on_response(self, dialog, response, origin, args): (args, kwargs) = args if response != Gtk.ResponseType.YES: LOGGER.info("User cancelled") dialog.destroy() self.core.call_all( "on_dialog_yes_no_reply", origin, False, *args, **kwargs ) return dialog.destroy() self.core.call_all( "on_dialog_yes_no_reply", origin, True, *args, **kwargs ) paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/drawer/000077500000000000000000000000001417573700700250335ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/drawer/__init__.py000066400000000000000000000000001417573700700271320ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/drawer/pillow.py000066400000000000000000000065731417573700700267260ustar00rootroot00000000000000""" Draw a Pillow image on top of GtkDrawingArea. """ import logging import openpaperwork_core LOGGER = logging.getLogger(__name__) class Drawer(object): BACKGROUND = (0.75, 0.75, 0.75) def __init__(self, core, drawing_area, pil_img): self.core = core self.drawing_area = drawing_area # expected: Gtk.DrawingArea self.img = core.call_success("pillow_to_surface", pil_img) self.draw_connect_id = drawing_area.connect("draw", self.on_draw) def stop(self): if self.draw_connect_id is not None: self.drawing_area.disconnect(self.draw_connect_id) self.draw_connect_id = None self.img = None # free the memory def on_draw(self, drawing_area, cairo_ctx): widget_height = self.drawing_area.get_allocated_height() widget_width = self.drawing_area.get_allocated_width() factor_w = self.img.surface.get_width() / widget_width factor_h = self.img.surface.get_height() / widget_height factor = max(factor_w, factor_h) # background cairo_ctx.save() try: cairo_ctx.set_source_rgb( self.BACKGROUND[0], self.BACKGROUND[1], self.BACKGROUND[2] ) cairo_ctx.rectangle(0, 0, widget_width, widget_height) cairo_ctx.clip() cairo_ctx.paint() finally: cairo_ctx.restore() # image cairo_ctx.save() try: cairo_ctx.scale(1.0 / factor, 1.0 / factor) cairo_ctx.set_source_surface(self.img.surface) cairo_ctx.rectangle( 0, 0, self.img.surface.get_width(), self.img.surface.get_height() ) cairo_ctx.clip() cairo_ctx.paint() finally: cairo_ctx.restore() class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() # drawing area --> Drawer self.active_drawers = {} def get_interfaces(self): return [ 'gtk_drawer_pillow', ] def get_deps(self): return [ { 'interface': 'pillow_to_surface', 'defaults': ['paperwork_backend.cairo.pillow'], }, ] def draw_pillow_start(self, drawing_area, pil_img): drawer = Drawer(self.core, drawing_area, pil_img) self.active_drawers[drawing_area] = drawer return drawer def draw_pillow_stop(self, drawing_area): drawer = self.active_drawers.pop(drawing_area) drawer.stop() return drawer if __name__ == "__main__": import sys import PIL import PIL.Image import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk img = PIL.Image.open(sys.argv[1]) core = openpaperwork_core.Core() core.load("openpaperwork_core.mainloop.asyncio") core.load("openpaperwork_gtk.fs.gio") core.load("openpaperwork_core.thread.simple") core.load("openpaperwork_core.work_queue.default") core.load("paperwork_backend.cairo.pillow") core.load("openpaperwork_core.pillow.img") core._load_module("test", sys.modules[__name__]) core.init() window = Gtk.Window() window.set_size_request(600, 600) drawing_area = Gtk.DrawingArea() window.add(drawing_area) core.call_success("draw_pillow_start", drawing_area, img) window.show_all() Gtk.main() paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/drawer/scan.py000066400000000000000000000242571417573700700263430ustar00rootroot00000000000000""" Plugin to draw scan on GTK widgets. This code should be on Paperwork-gtk, but Ironscanner needs it to. """ import logging import openpaperwork_core import openpaperwork_core.deps CAIRO_AVAILABLE = False try: import cairo CAIRO_AVAILABLE = True except (ImportError, ValueError): pass LOGGER = logging.getLogger(__name__) class Drawer(object): BACKGROUND = (0.75, 0.75, 0.75) def __init__(self, core, drawing_area=None): self.core = core self.drawing_areas = [] self.draw_connect_ids = {} self.scan_size = (0, 0) self.last_line = 0 self.show_scan_border = False self.image = None self.scan_ended = False if drawing_area is not None: self.add_drawing_area(drawing_area) def add_drawing_area(self, drawing_area): self.drawing_areas.append(drawing_area) self.draw_connect_ids[drawing_area] = drawing_area.connect( "draw", self.on_draw ) def remove_drawing_area(self, drawing_area): connect_id = self.draw_connect_ids.pop(drawing_area) drawing_area.disconnect(connect_id) self.drawing_areas.remove(drawing_area) if len(self.drawing_areas) <= 0: self.stop() def stop(self): for (drawing_area, connect_id) in self.draw_connect_ids.items(): drawing_area.disconnect(connect_id) self.draw_connect_ids = {} self.drawing_areas = [] self.image = None # release the memory def request_redraw(self): for d in self.drawing_areas: d.queue_draw() def on_scan_page_start(self, scan_params): self.scan_size = (scan_params.get_width(), scan_params.get_height()) self.last_line = 0 LOGGER.info( "Scan started: %s (expected: %dx%dpx)", self.scan_size, self.scan_size[0], self.scan_size[1] ) self.scan_size = ( # WORKAROUND(Jflesch): Some scanners (Fujistu mainly) return an # image far too big in height. Cairo only allows a maximum image # size of 32767 (see #define MAX_IMAGE_SIZE in # cairo-image-surface.c) min(self.scan_size[0], 32766), min(self.scan_size[1], 32766), ) self.image = cairo.ImageSurface( cairo.FORMAT_RGB24, self.scan_size[0], self.scan_size[1] ) cairo_ctx = cairo.Context(self.image) cairo_ctx.set_source_rgb( self.BACKGROUND[0], self.BACKGROUND[1], self.BACKGROUND[2] ) cairo_ctx.rectangle(0, 0, self.scan_size[0], self.scan_size[1]) cairo_ctx.fill() self.show_scan_border = True self.request_redraw() def on_scan_page_end(self): self.show_scan_border = False self.request_redraw() def on_scan_chunk(self, img_chunk): if self.image is None: return size = img_chunk.size LOGGER.debug("Scan chunk: %s", size) img_chunk = self.core.call_success( "pillow_to_surface", img_chunk ) cairo_ctx = cairo.Context(self.image) cairo_ctx.translate(0, self.last_line) cairo_ctx.set_source_surface(img_chunk.surface) cairo_ctx.rectangle(0, 0, size[0], size[1]) cairo_ctx.clip() cairo_ctx.paint() self.last_line += size[1] self.request_redraw() def on_draw(self, drawing_area, cairo_ctx): widget_height = drawing_area.get_allocated_height() widget_width = drawing_area.get_allocated_width() factor_w = self.scan_size[0] / widget_width factor_h = self.scan_size[1] / widget_height factor = max(factor_w, factor_h) # background cairo_ctx.save() try: cairo_ctx.set_source_rgb( self.BACKGROUND[0], self.BACKGROUND[1], self.BACKGROUND[2] ) cairo_ctx.rectangle(0, 0, widget_width, widget_height) cairo_ctx.clip() cairo_ctx.paint() finally: cairo_ctx.restore() # chunks if self.image is not None: cairo_ctx.save() try: cairo_ctx.scale(1.0 / factor, 1.0 / factor) cairo_ctx.set_source_surface(self.image) cairo_ctx.rectangle(0, 0, self.scan_size[0], self.scan_size[1]) cairo_ctx.clip() cairo_ctx.paint() finally: cairo_ctx.restore() # scan border if self.show_scan_border: cairo_ctx.save() try: position = int(self.last_line / factor) cairo_ctx.set_operator(cairo.OPERATOR_OVER) cairo_ctx.set_source_rgba( 1.0, 0.0, 0.0, 0.5 ) cairo_ctx.set_line_width(10.0) cairo_ctx.move_to(0, position) cairo_ctx.line_to(widget_width, position) cairo_ctx.stroke() finally: cairo_ctx.restore() class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() # scan id --> Drawer (scan id = None ==> any scan) self.active_drawers = {} def get_interfaces(self): return [ 'chkdeps', 'gtk_drawer_scan', ] def get_deps(self): return [ { 'interface': 'pillow_to_surface', 'defaults': ['paperwork_backend.cairo.pillow'], }, { 'interface': 'scan', 'defaults': ['paperwork_backend.docscan.libinsane'], } ] def chkdeps(self, out: dict): if not CAIRO_AVAILABLE: out['cairo'].update(openpaperwork_core.deps.CAIRO) def draw_scan_start(self, drawing_area, scan_id=None): if scan_id in self.active_drawers: drawer = self.active_drawers[scan_id] else: drawer = Drawer(self.core) self.active_drawers[scan_id] = drawer drawer.add_drawing_area(drawing_area) return drawer def draw_scan_stop(self, drawing_area): for (k, drawer) in self.active_drawers.items(): for d in drawer.drawing_areas: if d == drawing_area: break else: continue break else: return None drawer.remove_drawing_area(drawing_area) if drawer.scan_ended and len(drawer.drawing_areas) <= 0: self.active_drawers.pop(k) return drawer def draw_scan_get_max_size(self, scan_id): if scan_id not in self.active_drawers: return self.active_drawers[scan_id].scan_size def on_scan_feed_start(self, scan_id): self.core.call_all("on_busy") if scan_id not in self.active_drawers: return # we show the app as busy when the user clicks on 'scan' # and we stop the busy indicator when a page is actually scanning # instantiate a default drawer to keep track of the size and the chunks if scan_id not in self.active_drawers: self.active_drawers[scan_id] = Drawer(self.core) def on_scan_page_start(self, scan_id, page_nb, scan_params): self.core.call_all("on_idle") if None in self.active_drawers: self.active_drawers[None].on_scan_page_start(scan_params) if scan_id in self.active_drawers: self.active_drawers[scan_id].on_scan_page_start(scan_params) def on_scan_chunk(self, scan_id, scan_params, img_chunk): for k in (None, scan_id): if k not in self.active_drawers: continue self.active_drawers[k].on_scan_chunk(img_chunk) def on_scan_page_end(self, scan_id, page_nb, img): self.core.call_all("on_busy") for k in (None, scan_id): if k not in self.active_drawers: continue self.active_drawers[k].on_scan_page_end() self.active_drawers[k].scan_ended = True if len(self.active_drawers[k].drawing_areas) <= 0: self.active_drawers.pop(k) def on_scan_feed_end(self, scan_id): self.core.call_all("on_idle") for k in (None, scan_id): if k not in self.active_drawers: continue self.active_drawers[k].scan_ended = True if __name__ == "__main__": import sys import PIL import PIL.Image import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk from gi.repository import GLib img = PIL.Image.open(sys.argv[1]) chunks = [ img.crop((0, line, img.size[1], line + 100)) for line in range(0, img.size[1], 100) ] class FakeScanParams(object): def __init__(self, img): self.img = img def get_width(self): return self.img.size[0] def get_height(self): return self.img.size[1] class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['scan'] scan_params = FakeScanParams(img) core = openpaperwork_core.Core() core.load("openpaperwork_core.mainloop.asyncio") core.load("openpaperwork_core.thread.simple") core.load("openpaperwork_core.work_queue.default") core.load("openpaperwork_gtk.fs.gio") core.load("paperwork_backend.cairo.pillow") core.load("openpaperwork_core.pillow.img") core._load_module("scan", FakeModule) core._load_module("test", sys.modules[__name__]) core.init() window = Gtk.Window() window.set_size_request(600, 600) drawing_area = Gtk.DrawingArea() window.add(drawing_area) core.call_all("draw_scan_start", drawing_area, scan_id="pouet") calls = [ ("on_scan_page_start", "pouet", 0, scan_params), ] calls += [ ("on_scan_chunk", "pouet", scan_params, chunk) for chunk in chunks ] calls += [ ("on_scan_page_end", "pouet", 0, img) ] calls = 2 * calls def wrapper(func, *args): func(*args) return False for (t, call) in enumerate(calls): t = t * 500 + 500 GLib.timeout_add(t, wrapper, core.call_all, *call) window.show_all() Gtk.main() paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/external_apps/000077500000000000000000000000001417573700700264145ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/external_apps/__init__.py000066400000000000000000000000001417573700700305130ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/external_apps/gio.py000066400000000000000000000017051417573700700275470ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except ImportError: GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 75 def get_interfaces(self): return [ 'chkdeps', 'external_apps', ] def get_deps(self): return [] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'] = openpaperwork_core.deps.GLIB def external_app_open_file(self, file_url): LOGGER.info("Opening file '%s' using Gio", file_url) if not Gio.AppInfo.launch_default_for_uri(file_url): LOGGER.warning("Failed to opening file '%s' using Gio", file_url) return None return True def external_app_open_folder(self, folder_url): return self.external_app_open_file(folder_url) paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/fs/000077500000000000000000000000001417573700700241575ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/fs/__init__.py000066400000000000000000000000001417573700700262560ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/fs/gio.py000066400000000000000000000467061417573700700253240ustar00rootroot00000000000000import ctypes import io import logging import os import tempfile try: from gi.repository import Gio from gi.repository import GLib GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core.deps import openpaperwork_core.fs LOGGER = logging.getLogger(__name__) class _GioFileAdapter(io.RawIOBase): def __init__(self, gfile, mode='r'): super().__init__() self.gfile = gfile self.mode = mode if 'w' in mode and 'r' not in mode: self.size = 0 elif ('w' in mode or 'a' in mode) and not gfile.query_exists(): self.size = 0 else: try: fi = gfile.query_info( Gio.FILE_ATTRIBUTE_STANDARD_SIZE, Gio.FileQueryInfoFlags.NONE ) self.size = fi.get_attribute_uint64( Gio.FILE_ATTRIBUTE_STANDARD_SIZE ) except GLib.GError as exc: LOGGER.warning("Gio.Gerror", exc_info=exc) raise IOError(str(exc)) self.gfd = None self.gin = None self.gout = None if 'r' in mode and 'w' in mode: if gfile.query_exists(): self.gfd = gfile.open_readwrite() else: # create_readwrite() doesn't seem to always work on # Windows+MSYS2 self.gfd = gfile.create_readwrite(Gio.FileCreateFlags.PRIVATE) if 'w' in mode: self.gfd.seek(0, GLib.SeekType.SET) self.gfd.truncate(0) self.gin = self.gfd.get_input_stream() self.gout = self.gfd.get_output_stream() elif 'r' in mode: self.gfd = gfile.read() self.gin = self.gfd elif 'w' in mode or 'a' in mode: if 'w' in mode: self.gfd = gfile.replace( None, # etag False, # make_backup Gio.FileCreateFlags.PRIVATE ) elif 'a' in mode: self.gfd = gfile.append_to( Gio.FileCreateFlags.PRIVATE ) self.gout = self.gfd def readable(self): return True def writable(self): return 'w' in self.mode or 'a' in self.mode def read(self, size=-1): if not self.readable(): raise OSError("File is not readable") if size <= 0: size = self.size if size <= 0: return b"" assert(size > 0) return self.gin.read_bytes(size).get_data() def readall(self): return self.read(-1) def readinto(self, b): raise OSError("readinto() not supported on Gio.File objects") def readline(self, size=-1): raise OSError("readline() not supported on Gio.File objects") def readlines(self, hint=-1): LOGGER.warning("readlines() shouldn't be called on a binary file" " descriptor. This is not cross-platform") return [(x + b"\n") for x in self.readall().split(b"\n")] def seek(self, offset, whence=os.SEEK_SET): whence = { os.SEEK_CUR: GLib.SeekType.CUR, os.SEEK_END: GLib.SeekType.END, os.SEEK_SET: GLib.SeekType.SET, }[whence] self.gfd.seek(offset, whence) def seekable(self): return True def tell(self): return self.gin.tell() def flush(self): pass def truncate(self, size=None): if size is None: size = self.tell() self.gfd.truncate(size) def fileno(self): if self.gout is not None and hasattr(self.gout, 'get_fd'): return self.gout.get_fd() if self.gin is not None and hasattr(self.gin, 'get_fd'): return self.gin.get_fd() raise io.UnsupportedOperation("fileno() on Gio.File unsupported") def isatty(self): return False def write(self, b): res = self.gout.write_all(b) if not res[0]: raise OSError("write_all() failed on {}: {}".format( self.gfile.get_uri(), res) ) return res[1] def writelines(self, lines): self.write(b"".join(lines)) def close(self): self.flush() super().close() if self.gin is not None and self.gin is not self.gfd: self.gin.close() if self.gout is not None and self.gout is not self.gfd: self.gout.close() if self.gfd: self.gfd.close() def __enter__(self): return self def __exit__(self, type, value, traceback): self.close() class _GioUTF8FileAdapter(io.RawIOBase): def __init__(self, raw): super().__init__() self.raw = raw self.line_iterator = None def readable(self): return self.raw.readable() def writable(self): return self.raw.writable() def read(self, *args, **kwargs): r = self.raw.read(*args, **kwargs) return r.decode("utf-8") def readall(self, *args, **kwargs): r = self.raw.readall(*args, **kwargs) return r.decode("utf-8") def readinto(self, *args, **kwargs): r = self.raw.readinto(*args, **kwargs) return r.decode("utf-8") def readlines(self, hint=-1): all = self.readall() if os.linesep != "\n": all = all.replace(os.linesep, "\n") lines = [(x + "\n") for x in all.split("\n")] if lines[-1] == "\n": return lines[:-1] return lines def readline(self, hint=-1): if self.line_iterator is None: self.line_iterator = (line for line in self.readlines(hint)) try: return next(self.line_iterator) except StopIteration: return '' def seek(self, *args, **kwargs): return self.raw.seek(*args, **kwargs) def seekable(self, seekable): return self.raw.seekable() @property def closed(self): return self.raw.closed def tell(self): # XXX(Jflesch): wrong ... return self.raw.tell() def flush(self): return self.raw.flush() def truncate(self, *args, **kwargs): # XXX(Jflesch): wrong ... return self.raw.truncate(*args, **kwargs) def fileno(self): return self.raw.fileno() def isatty(self): return self.raw.isatty() def write(self, b): b = b.encode("utf-8") return self.raw.write(b) def writelines(self, lines): lines = [ (line + os.linesep).encode("utf-8") for line in lines ] return self.raw.writelines(lines) def close(self): self.raw.close() def __enter__(self): return self def __exit__(self, type, value, traceback): self.close() class Plugin(openpaperwork_core.fs.CommonFsPluginBase): PRIORITY = 50 def __init__(self): super().__init__() self.vfs = None if GLIB_AVAILABLE: self.vfs = Gio.Vfs.get_default() self.tmp_files = set() def get_interfaces(self): return super().get_interfaces() + ['chkdeps'] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def fs_open(self, uri, mode='r', needs_fileno=False, **kwargs): if needs_fileno: # On Windows, `Gio.[Unix]OutputStream.get_fd()` doesn't seem # to be available return f = self.vfs.get_file_for_uri(uri) if ('w' not in mode and 'a' not in mode): if self.fs_exists(uri) is None: return None try: raw = _GioFileAdapter(f, mode) if 'b' in mode: return raw return _GioUTF8FileAdapter(raw) except GLib.GError as exc: LOGGER.warning("Gio.Gerror", exc_info=exc) raise IOError("fs_open({}, mode={}): {}".format( uri, mode, str(exc) )) def fs_exists(self, url): if not GLIB_AVAILABLE: return None try: f = self.vfs.get_file_for_uri(url) if not f.query_exists(): # this file does not exist for us, but it does not mean # another implementation of the plugin interface 'fs' # cannot handle it return None return True except GLib.GError as exc: LOGGER.warning("Gio.Gerror", exc_info=exc) raise IOError(str(exc)) def fs_listdir(self, url): if not GLIB_AVAILABLE: return None try: f = self.vfs.get_file_for_uri(url) if not f.query_exists(): return None children = f.enumerate_children( Gio.FILE_ATTRIBUTE_STANDARD_NAME, Gio.FileQueryInfoFlags.NONE, None ) for child in children: child = f.get_child(child.get_name()) yield child.get_uri() except GLib.GError as exc: LOGGER.warning("Gio.Gerror", exc_info=exc) raise IOError(str(exc)) def fs_rename(self, old_url, new_url): try: old = self.vfs.get_file_for_uri(old_url) new = self.vfs.get_file_for_uri(new_url) assert(not old.equal(new)) if not old.query_exists(): return None old.move(new, Gio.FileCopyFlags.NONE) return True except GLib.GError as exc: LOGGER.warning("Gio.Gerror", exc_info=exc) raise IOError(str(exc)) def fs_unlink(self, url, trash=True, **kwargs): try: f = self.vfs.get_file_for_uri(url) if not f.query_exists(): return None LOGGER.info("Deleting %s (trash=%s) ...", url, trash) if not trash: deleted = f.delete() if not deleted: raise IOError("Failed to delete %s" % url) return None deleted = False try: deleted = f.trash() if not deleted: LOGGER.warning( "Failed to trash %s. Will try to delete it instead", url ) except Exception as exc: LOGGER.warning("Failed to trash %s. Will try to delete it" " instead", url, exc_info=exc) if deleted and self.fs_exists(url) is not None: deleted = False # WORKAROUND(Jflesch): It seems in Flatpak, f.trash() # returns True when it actually does nothing. LOGGER.warning( "trash(%s) returned True but file wasn't trashed." " Will try to deleting it instead", f.get_uri() ) if not deleted: try: deleted = f.delete() except Exception as exc: LOGGER.warning("Failed to deleted %s", url, exc_info=exc) if not deleted: raise IOError("Failed to delete %s" % url) return True except GLib.GError as exc: LOGGER.warning("Gio.Gerror", exc_info=exc) raise IOError(str(exc)) def fs_rm_rf(self, url, trash=True, **kwargs): if self.fs_exists(url) is None: return None try: LOGGER.info("Deleting %s ...", url) f = self.vfs.get_file_for_uri(url) deleted = False if trash: try: deleted = f.trash() if not deleted: LOGGER.warning( "Failed to trash %s." " Will try to delete it instead", url ) except Exception as exc: LOGGER.warning( "Failed to trash %s (trash()=%s)." " Will try to delete it instead", url, trash, exc_info=exc ) if deleted and self.fs_exists(url) is not None: deleted = False # WORKAROUND(Jflesch): It seems in Flatpak, f.trash() # returns True when it actually does nothing. LOGGER.warning( "trash(%s) returned True but file wasn't trashed." " Will try to deleting it instead", url ) if not deleted: self._rm_rf(f) LOGGER.info("%s deleted", url) return True except GLib.GError as exc: LOGGER.warning("Gio.Gerror", exc_info=exc) raise IOError(str(exc)) def _rm_rf(self, gfile): try: to_delete = [ f for f in self._recurse( gfile, dir_included=True, follow_symlinks=False ) ] # make sure to delete the parent directory last: to_delete.sort(reverse=True, key=lambda f: f.get_uri()) for f in to_delete: if not f.delete(): raise IOError("Failed to delete %s" % f.get_uri()) except GLib.GError as exc: LOGGER.warning("Gio.Gerror", exc_info=exc) raise IOError(str(exc)) def fs_get_mtime(self, url): try: f = self.vfs.get_file_for_uri(url) if not f.query_exists(): raise IOError("File {} does not exist".format(str(url))) fi = f.query_info( Gio.FILE_ATTRIBUTE_TIME_CHANGED, Gio.FileQueryInfoFlags.NONE ) r = fi.get_attribute_uint64(Gio.FILE_ATTRIBUTE_TIME_CHANGED) if int(r) != 0: return r # WORKAROUND(Jflesch): # On Windows+MSYS2, it seems Gio.File.query_info() # return always 0 for Gio.FILE_ATTRIBUTE_TIME_CHANGED. path = self.fs_unsafe(url) return os.stat(path).st_mtime except GLib.GError as exc: LOGGER.warning("Gio.Gerror", exc_info=exc) def fs_getsize(self, url): try: f = self.vfs.get_file_for_uri(url) fi = f.query_info( Gio.FILE_ATTRIBUTE_STANDARD_SIZE, Gio.FileQueryInfoFlags.NONE ) return fi.get_attribute_uint64(Gio.FILE_ATTRIBUTE_STANDARD_SIZE) except GLib.GError as exc: LOGGER.warning("Gio.Gerror", exc_info=exc) raise IOError(str(exc)) def fs_isdir(self, url): if not GLIB_AVAILABLE: return None try: f = self.vfs.get_file_for_uri(url) if not f.query_exists(): return None fi = f.query_info( Gio.FILE_ATTRIBUTE_STANDARD_TYPE, Gio.FileQueryInfoFlags.NONE ) if fi.get_file_type() == Gio.FileType.DIRECTORY: return True return None except GLib.GError as exc: LOGGER.warning("Gio.Gerror", exc_info=exc) raise IOError(str(exc)) def fs_copy(self, old_url, new_url): try: old = self.vfs.get_file_for_uri(old_url) new = self.vfs.get_file_for_uri(new_url) if new.query_exists(): new.delete() old.copy(new, Gio.FileCopyFlags.ALL_METADATA) return new_url except GLib.GError as exc: LOGGER.warning("Gio.Gerror", exc_info=exc) return None def fs_mkdir_p(self, url): if not GLIB_AVAILABLE: return None try: f = self.vfs.get_file_for_uri(url) if not f.query_exists(): if os.name == "nt": # WORKAROUND(Jflesch): On Windows+MSYS2, # Gio.File.make_directory_with_parents() raises # a Gio.GError "Unsupported operation" (bug ?) path = self.fs_unsafe(url) os.makedirs(path, mode=0o700, exist_ok=True) return True f.make_directory_with_parents() except GLib.GError as exc: LOGGER.warning("Gio.GError", exc_info=exc) raise IOError("fs_mkdir_p({}): {}".format(url, str(exc))) return True def _recurse(self, parent, dir_included=False, follow_symlinks=True): """ Yield all the children (depth first), but not the parent. """ try: children = parent.enumerate_children( Gio.FILE_ATTRIBUTE_STANDARD_NAME, Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, None ) except GLib.GError: # assumes it's a file and not a directory yield parent return for child in children: name = child.get_name() if child.get_is_symlink() and not follow_symlinks: yield parent.get_child(name) continue child = parent.get_child(name) try: for sub in self._recurse( child, dir_included=dir_included, follow_symlinks=follow_symlinks): yield sub except GLib.GError: yield child if dir_included: yield parent def fs_recurse(self, parent_uri, dir_included=False): parent = self.vfs.get_file_for_uri(parent_uri) for f in self._recurse(parent, dir_included): yield f.get_uri() def fs_hide(self, uri): if os.name != 'nt': LOGGER.warning("fs_hide('%s') can only works on Windows", uri) return None filepath = self.fs_unsafe(uri) LOGGER.info("Hiding file: {}".format(filepath)) ret = ctypes.windll.kernel32.SetFileAttributesW( filepath, 0x02 # hidden ) if not ret: raise ctypes.WinError() return True def fs_get_mime(self, uri): if os.name == 'nt': # WORKAROUND(Jflesch): # Gio.File.query_info().get_content_type() returns crap on Windows # (for instance '.pdf' instead of 'application/pdf'). return None gfile = self.vfs.get_file_for_uri(uri) info = gfile.query_info( "standard::content-type", Gio.FileQueryInfoFlags.NONE ) return info.get_content_type() def fs_mktemp(self, prefix=None, suffix=None, mode='w+b', **kwargs): if 'b' not in mode: tmp = tempfile.NamedTemporaryFile( prefix=prefix, suffix=suffix, delete=False, mode=mode, encoding='utf-8' ) else: tmp = tempfile.NamedTemporaryFile( prefix=prefix, suffix=suffix, delete=False, mode=mode ) self.tmp_files.add(tmp.name) return (self.fs_safe(tmp.name), tmp) def fs_iswritable(self, url): try: f = self.vfs.get_file_for_uri(url) fi = f.query_info( Gio.FILE_ATTRIBUTE_ACCESS_CAN_WRITE, Gio.FileQueryInfoFlags.NONE ) return fi.get_attribute_boolean( Gio.FILE_ATTRIBUTE_ACCESS_CAN_WRITE ) except GLib.GError as exc: LOGGER.warning("Gio.Gerror", exc_info=exc) raise IOError(str(exc)) def on_quit(self): for tmp_file in self.tmp_files: try: os.unlink(tmp_file) except FileNotFoundError: pass paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/gesture/000077500000000000000000000000001417573700700252255ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/gesture/__init__.py000066400000000000000000000000001417573700700273240ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/gesture/autoscrolling.py000066400000000000000000000101361417573700700304650ustar00rootroot00000000000000""" Scrolling with the middle mouse button. Useful for: - smoother scrolling - horizontal scrolling: the horizontal scrollbar is blocked by the half-transparent lower toolbar """ import logging import openpaperwork_core import openpaperwork_gtk.deps GI_AVAILABLE = False GTK_AVAILABLE = False try: import gi GI_AVAILABLE = True except (ImportError, ValueError): pass if GI_AVAILABLE: try: gi.require_version('Gdk', '3.0') from gi.repository import Gdk GTK_AVAILABLE = True except (ImportError, ValueError): pass LOGGER = logging.getLogger(__name__) class AutoScrollingHandler(object): def __init__(self, core, scrollview): self.core = core self.refcount = 0 self.mouse_start_position = (0, 0) # relative to the screen self.mouse_position = (0, 0) # relative to the screen display = scrollview.get_display() self.cursors = { 'inactive': None, 'active': Gdk.Cursor.new_for_display( display, Gdk.CursorType.TCROSS ), } self.scrollview = scrollview scrollview.add_events( Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK ) self.button_press_id = scrollview.connect( "button-press-event", self._on_button_press ) self.button_release_id = scrollview.connect( "button-release-event", self._on_button_release ) self.motion_notify_id = scrollview.connect( "motion-notify-event", self._on_mouse_motion ) def disable(self): self.scrollview.disconnect(self.button_press_id) self.scrollview.disconnect(self.button_release_id) self.scrollview.disconnect(self.motion_notify_id) def _on_tick(self): if self.mouse_start_position is None: self.refcount -= 1 return hdiff = (self.mouse_position[0] - self.mouse_start_position[0]) / 5 vdiff = (self.mouse_position[1] - self.mouse_start_position[1]) / 5 hadj = self.scrollview.get_hadjustment() vadj = self.scrollview.get_vadjustment() hval = hadj.get_value() + hdiff hval = max(hval, hadj.get_lower()) hval = min(hval, hadj.get_upper()) hadj.set_value(hval) vval = vadj.get_value() + vdiff vval = max(vval, vadj.get_lower()) vval = min(vval, vadj.get_upper()) vadj.set_value(vval) self.core.call_one("mainloop_schedule", self._on_tick, delay_s=0.1) def _on_button_press(self, scrollview, event): if event.button != 2: return LOGGER.info("Starting autoscrolling") self.mouse_start_position = (event.x_root, event.y_root) self.mouse_position = (event.x_root, event.y_root) if self.refcount <= 0: self.core.call_one("mainloop_schedule", self._on_tick, delay_s=0.1) self.refcount += 1 self.scrollview.get_window().set_cursor(self.cursors['active']) def _on_button_release(self, scrollview, event): if event.button != 2: return LOGGER.info("Ending autoscrolling") self.mouse_start_position = None self.scrollview.get_window().set_cursor(self.cursors['inactive']) def _on_mouse_motion(self, scrollview, event): if self.mouse_start_position is None: return self.mouse_position = (event.x_root, event.y_root) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'chkdeps', 'gtk_gesture_autoscrolling', ] def get_deps(self): return [] def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def gesture_enable_autoscrolling(self, scrollview): LOGGER.info("Enabling autoscrolling on %s", scrollview) try: return AutoScrollingHandler(self.core, scrollview) except TypeError as exc: # may happen with Wayland LOGGER.error("Failed to switch mouse cursor", exc_info=exc) return None paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/l10n/000077500000000000000000000000001417573700700243215ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/l10n/__init__.py000066400000000000000000000007351417573700700264370ustar00rootroot00000000000000import openpaperwork_core class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['l10n_init'] def get_deps(self): return [ { 'interface': 'l10n', 'defaults': ['openpaperwork_core.l10n.python'], }, ] def init(self, core): super().init(core) self.core.call_all( "l10n_load", "openpaperwork_gtk.l10n", "openpaperwork_gtk" ) paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/mainloop/000077500000000000000000000000001417573700700253655ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/mainloop/__init__.py000066400000000000000000000000001417573700700274640ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/mainloop/glib.py000066400000000000000000000147721417573700700266670ustar00rootroot00000000000000import collections import faulthandler import logging import threading try: from gi.repository import GLib GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 1000 """ A main loop based on GLib's mainloop. See `openpaperwork_core.mainloop.asyncio` for doc. """ def __init__(self): super().__init__() self.halt_on_uncaught_exception = True self.log_uncaught = True self.loop = None self.loop_ident = None self.halt_cause = None self.task_count = 0 self.lock = threading.RLock() self.active_tasks = collections.defaultdict(lambda: 0) def get_interfaces(self): return [ "chkdeps", "mainloop", ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def _check_mainloop_instantiated(self): if self.loop is None: self.loop = GLib.MainLoop.new(None, False) # !running def mainloop(self, halt_on_uncaught_exception=True, log_uncaught=True): if not GLIB_AVAILABLE: return None self._check_mainloop_instantiated() self.log_uncaught = log_uncaught self.halt_on_uncaught_exception = halt_on_uncaught_exception self.loop_ident = threading.current_thread().ident self.mainloop_schedule(self.core.call_all, "on_mainloop_start") try: self.loop.run() except Exception: faulthandler.dump_traceback() raise finally: self.loop_ident = None self.core.call_all("on_mainloop_quit") if self.halt_cause is not None: halt_cause = self.halt_cause self.halt_cause = None LOGGER.error("Main loop stopped because %s", str(halt_cause)) raise halt_cause self.loop = None return True def mainloop_get_thread_id(self): return self.loop_ident def mainloop_quit_graceful(self): self.mainloop_schedule(self._mainloop_quit_graceful) return True def _mainloop_quit_graceful(self): quit_now = True with self.lock: # keep in mind this function is in a task too if self.task_count > 1: quit_now = False LOGGER.info( "Quit graceful: Remaining tasks: %d", self.task_count - 1 ) for (k, v) in self.active_tasks.items(): LOGGER.info("Quit graceful: Remaining: %s = %d", k, v) if not quit_now: self.mainloop_schedule( self._mainloop_quit_graceful, delay_s=0.2 ) return LOGGER.info("Quit graceful: Quitting") self.mainloop_quit_now() with self.lock: self.task_count = 1 # we are actually the one task still running self.active_tasks = collections.defaultdict(lambda: 0) def mainloop_quit_now(self): if self.loop is None: return None with self.lock: self.loop.quit() self.loop = None self.task_count = 0 self.active_tasks = collections.defaultdict(lambda: 0) def mainloop_ref(self, obj): with self.lock: self.task_count += 1 self.active_tasks[str(obj)] += 1 def mainloop_unref(self, obj): with self.lock: self.task_count -= 1 assert(self.task_count >= 0) try: s = str(obj) self.active_tasks[s] -= 1 if self.active_tasks[s] <= 0: self.active_tasks.pop(s) except KeyError: pass def mainloop_schedule(self, func, *args, delay_s=0, **kwargs): if not GLIB_AVAILABLE: return None assert(hasattr(func, '__call__')) with self.lock: self._check_mainloop_instantiated() self.task_count += 1 self.active_tasks[str(func)] += 1 def decorator(func, args): (args, kwargs) = args try: func(*args, **kwargs) except Exception as exc: if self.halt_on_uncaught_exception: LOGGER.error( "Main loop: uncaught exception (%s) ! Quitting", func, exc_info=exc ) self.halt_cause = exc self.mainloop_quit_now() elif self.log_uncaught: LOGGER.error( "Main loop: uncaught exception (%s) !", func, exc_info=exc ) finally: with self.lock: self.task_count -= 1 try: s = str(func) self.active_tasks[s] -= 1 if self.active_tasks[s] <= 0: self.active_tasks.pop(s) except KeyError: pass return False args = (args, kwargs) if delay_s is None: GLib.idle_add(decorator, func, args, priority=GLib.PRIORITY_LOW) else: GLib.timeout_add(delay_s * 1000, decorator, func, args) return True def mainloop_execute(self, func, *args, **kwargs): current = threading.current_thread().ident # XXX(Jflesch): # if self.loop_ident is None, it means the mainloop hasn't been started # yet --> we cannot run the function on the mainloop anyway, so # we assume we are on the same thread that will later run the main # loop. if self.loop_ident is None or current == self.loop_ident: return func(*args, **kwargs) event = threading.Event() out = [None] exc = [None] def get_result(): try: out[0] = func(*args, **kwargs) except Exception as e: LOGGER.warning( "mainloop_execute exception (func=%s, args=%s, kwargs=%s)", func, args, kwargs, exc_info=e ) exc[0] = e event.set() self.mainloop_schedule(get_result) event.wait() if exc[0] is not None: raise exc[0] return out[0] paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/pixbuf/000077500000000000000000000000001417573700700250445ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/pixbuf/__init__.py000066400000000000000000000000001417573700700271430ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/pixbuf/pillow.py000066400000000000000000000042511417573700700267260ustar00rootroot00000000000000import io import logging import PIL import PIL.Image try: from gi.repository import GLib GLIB_AVAILABLE = True except (ValueError, ImportError): GLIB_AVAILABLE = False try: import gi gi.require_version('GdkPixbuf', '2.0') from gi.repository import GdkPixbuf GDK_PIXBUF_AVAILABLE = True except (ValueError, ImportError): GDK_PIXBUF_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from .. import deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'chkdeps', 'pixbuf_pillow', ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) if not GDK_PIXBUF_AVAILABLE: out['gdk_pixbuf'].update(deps.GDK_PIXBUF) @staticmethod def pixbuf_to_pillow(pixbuf): (width, height) = (pixbuf.get_width(), pixbuf.get_height()) pixels = pixbuf.get_pixels() colors = "RGB" if (width * height * 4 == len(pixels)): colors = "RGBA" return PIL.Image.frombytes( colors, (width, height), pixbuf.get_pixels() ) def pillow_to_pixbuf(self, img): """ Convert an image object to a GDK pixbuf """ if not GDK_PIXBUF_AVAILABLE: return None if img is None: return None img = img.convert("RGB") if hasattr(GdkPixbuf.Pixbuf, 'new_from_bytes'): data = GLib.Bytes.new(img.tobytes()) (width, height) = img.size return GdkPixbuf.Pixbuf.new_from_bytes( data, GdkPixbuf.Colorspace.RGB, False, 8, width, height, width * 3 ) file_desc = io.BytesIO() try: img.save(file_desc, "ppm") contents = file_desc.getvalue() finally: file_desc.close() loader = GdkPixbuf.PixbufLoader.new_with_type("pnm") try: loader.write(contents) pixbuf = loader.get_pixbuf() finally: loader.close() return pixbuf paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/resources.py000066400000000000000000000174601417573700700261430ustar00rootroot00000000000000import gettext import logging import os import re if os.name == "nt": import webbrowser import xml.etree.ElementTree GI_AVAILABLE = False GDK_PIXBUF_AVAILABLE = False GTK_AVAILABLE = False HDY_AVAILABLE = False try: import gi GI_AVAILABLE = True except (ValueError, ImportError): pass if GI_AVAILABLE: try: gi.require_version('GdkPixbuf', '2.0') from gi.repository import GdkPixbuf GDK_PIXBUF_AVAILABLE = True except (ValueError, ImportError): pass try: gi.require_version('Gdk', '3.0') gi.require_version('Gtk', '3.0') from gi.repository import Gdk from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): pass try: gi.require_version('Handy', '1') from gi.repository import Handy HDY_AVAILABLE = True except (ImportError, ValueError): pass import openpaperwork_core # noqa: E402 from . import deps # noqa: E402 LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.cache = {} def get_interfaces(self): return [ 'chkdeps', 'gtk_resources' ] def get_deps(self): return [ { 'interface': 'l10n_init', 'defaults': [ 'openpaperwork_core.l10n.python', 'openpaperwork_gtk.l10n', ], }, { 'interface': 'resources', 'defaults': ['openpaperwork_core.resources.setuptools'], }, ] def init(self, core): super().init(core) if not HDY_AVAILABLE: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to initalize Libhandy") return Handy.init() def chkdeps(self, out: dict): if not GDK_PIXBUF_AVAILABLE: out['gdk_pixbuf'].update(deps.GDK_PIXBUF) if not GTK_AVAILABLE: out['gtk'].update(deps.GTK) if not HDY_AVAILABLE: out['hdy'].update(deps.HDY) @staticmethod def _translate_xml(xml_str): root = xml.etree.ElementTree.fromstring(xml_str) translation_domain = "openpaperwork_gtk" if 'domain' in root.attrib: translation_domain = root.attrib['domain'] labels = root.findall('.//*[@translatable="yes"]') for label in labels: label.text = gettext.dgettext(translation_domain, label.text) out = xml.etree.ElementTree.tostring(root, encoding='UTF-8') return out.decode('utf-8') @staticmethod def _windows_fix_widgets(widget_tree): def open_uri(uri): # XXX(Jflesch): Seems we get some garbarge in the URI sometimes ?! uri = uri.replace("\u202f", "") LOGGER.info("Opening URI [%s]", uri) webbrowser.open(uri) for obj in widget_tree.get_objects(): if isinstance(obj, Gtk.LinkButton): obj.connect( "clicked", lambda button: open_uri(button.get_uri()) ) elif (isinstance(obj, Gtk.AboutDialog) or isinstance(obj, Gtk.Label)): obj.connect( "activate-link", lambda widget, uri: open_uri(uri) ) def gtk_load_widget_tree(self, pkg, filename): """ Load a .glade file Arguments: pkg -- Python package name filename -- css file name to load Returns: GTK Widget Tree """ if not GTK_AVAILABLE or not HDY_AVAILABLE: LOGGER.error( "gtk_load_widget_tree(): GTK|Libhandy is not available" ) return None LOGGER.debug("Loading GTK widgets from %s:%s", pkg, filename) screen = Gdk.Screen.get_default() if screen is None: LOGGER.warning( "Cannot load widget tree: Gdk.Screen.get_default()" " returned None" ) return None k = (pkg, filename) if k not in self.cache: try: filepath = self.core.call_success( "resources_get_file", pkg, filename ) with self.core.call_success("fs_open", filepath, 'r') as fd: xml = fd.read() if os.name == "nt": # WORKAROUND(Jflesch): # for some reason, # Gtk.Builder.new_from_file()/new_from_string doesn't # translate on Windows xml = self._translate_xml(xml) self.cache[k] = xml except Exception: LOGGER.error( "Failed to load widget tree from file %s:%s", pkg, filename ) raise try: content = self.cache[k] widget_tree = Gtk.Builder.new_from_string(content, -1) if os.name == "nt": self._windows_fix_widgets(widget_tree) return widget_tree except Exception: LOGGER.error("Failed to load widget tree %s:%s", pkg, filename) raise def gtk_load_css(self, pkg, filename): """ Load a .css file Arguments: pkg -- Python package name filename -- css file name to load. """ if not GTK_AVAILABLE or not HDY_AVAILABLE: LOGGER.error("gtk_load_css(): GTK|Libhandy is not available") return None LOGGER.debug("Loading CSS from %s:%s", pkg, filename) k = (pkg, filename) if k not in self.cache: try: filepath = self.core.call_success( "resources_get_file", pkg, filename ) with self.core.call_success("fs_open", filepath, 'rb') as fd: self.cache[k] = fd.read() except Exception: LOGGER.error("Failed to load CSS file %s:%s", pkg, filename) raise try: content = self.cache[k] css_provider = Gtk.CssProvider() css_provider.load_from_data(content) screen = Gdk.Screen.get_default() if screen is None: LOGGER.warning( "Cannot apply CSS: Gdk.Screen.get_default()" " returned None" ) return None Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) except Exception: LOGGER.error("Failed to load CSS %s:%s", pkg, filename) raise return True def gtk_load_pixbuf(self, pkg, filename): filepath = self.core.call_success("resources_get_file", pkg, filename) if filepath is None: return None filepath = self.core.call_success("fs_unsafe", filepath) return GdkPixbuf.Pixbuf.new_from_file(filepath) def gtk_fix_headerbar_buttons(self, headerbar): settings = Gtk.Settings.get_default() default_layout = settings.get_property("gtk-decoration-layout") default_layout = re.split("[:,]", default_layout) # disable the elements that are not enabled globally layout = headerbar.get_decoration_layout().split(":", 1) layout = [side.split(",") for side in layout] layout = ":".join([ ",".join([ element for element in side if element in default_layout ]) for side in layout ]) headerbar.set_decoration_layout(layout) paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/screenshots.py000066400000000000000000000222161417573700700264640ustar00rootroot00000000000000import logging import math try: import cairo CAIRO_AVAILABLE = True except (ImportError, ValueError): CAIRO_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from . import _ LOGGER = logging.getLogger(__name__) SCREENSHOT_DATE_FORMAT = "%Y%m%d_%H%M_%S" MAX_DAYS = 31 MAX_UNCAUGHT_EXCEPTION_SCREENSHOTS = 5 class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.windows = [] self.archiver = None self.nb_uncaught = 0 def get_interfaces(self): return [ 'bug_report_attachments', 'chkdeps', 'gtk_window_listener', 'uncaught_exception_listener', 'screenshot', ] def get_deps(self): return [ { 'interface': 'file_archives', 'defaults': ['openpaperwork_core.archives'], }, { 'interface': 'fs', 'defaults': [ 'openpaperwork_core.fs.memory', 'openpaperwork_core.fs.python', ], }, { 'interface': 'uncaught_exception', 'defaults': ['openpaperwork_core.uncaught_exception'], }, ] def init(self, core): super().init(core) self.archiver = self.core.call_success( "file_archive_get", storage_name="screenshots", file_extension="png" ) def chkdeps(self, out: dict): if not CAIRO_AVAILABLE: out['cairo'] = openpaperwork_core.deps.CAIRO def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def _snap_screenshot(self, fd): image_size = (0, 0) for window in self.windows: if not window.is_drawable(): LOGGER.warning("Window %s cannot be screenshoted", window) else: LOGGER.info("Screenshoting window %s", window) for window in self.windows: if not window.is_drawable(): continue alloc = window.get_allocation() image_size = ( max(image_size[0], alloc.width), image_size[1] + alloc.height ) surface = cairo.ImageSurface(cairo.FORMAT_RGB24, *image_size) cairo_ctx = cairo.Context(surface) for window in self.windows: if not window.is_drawable(): continue alloc = window.get_allocation() window.draw(cairo_ctx) cairo_ctx.translate(0, alloc.height) surface.write_to_png(fd) def screenshot_snap_all_promise(self, temporary=True, name="screenshots"): if temporary: (file_url, fd) = self.core.call_success( "fs_mktemp", prefix="{}_".format(name), suffix=".png", mode="wb", on_disk=True ) else: file_url = self.archiver.get_new(name=name) fd = self.core.call_success("fs_open", file_url, mode='wb') promise = openpaperwork_core.promise.Promise( self.core, self.core.call_all, args=("on_screenshot_before",) ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then( self.core.call_one, "mainloop_execute", self._snap_screenshot, fd ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.catch(lambda *args, **kwargs: fd.close()) promise = promise.then(fd.close) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(self.core.call_all, "on_screenshot_after") promise = promise.then(lambda *args, **kwargs: None) return (file_url, promise) def screenshot_snap_widget( self, gtk_widget, out_file_url, margins=(20, 20, 20, 20), highlight=(0.67, 0.80, 0.93) ): assert(out_file_url.endswith(".png")) if not gtk_widget.is_drawable() or not gtk_widget.get_visible(): LOGGER.warning( "%s is not visible. Cannot screenshot", gtk_widget ) return None # find the GTK window of the widget window = gtk_widget.get_toplevel() if not window.is_drawable() or not window.get_visible(): LOGGER.warning( "%s's window is not visible. Cannot screenshot", gtk_widget ) return None # take a screenshot of the whole window win_alloc = window.get_allocation() win_surface = cairo.ImageSurface( cairo.FORMAT_RGB24, win_alloc.width, win_alloc.height ) cairo_ctx = cairo.Context(win_surface) window.draw(cairo_ctx) widget_position = gtk_widget.translate_coordinates(window, 0, 0) widget_alloc = gtk_widget.get_allocation() # highlight if highlight is not None: cairo_ctx.save() try: cairo_ctx.new_path() border_width = 5 margin = 5 cairo_ctx.set_source_rgba(*highlight, 0.85) cairo_ctx.set_line_width(border_width) center = ( widget_position[0] + (widget_alloc.width / 2), widget_position[1] + (widget_alloc.height / 2), ) cairo_ctx.translate(*center) radius = ( (widget_alloc.width / 2) + (widget_alloc.height / 2) + margin ) if widget_alloc.width > widget_alloc.height: scale = (1.0, widget_alloc.height / widget_alloc.width) else: scale = (widget_alloc.width / widget_alloc.height, 1.0) cairo_ctx.scale(*scale) cairo_ctx.arc(0, 0, radius, 0, 2 * math.pi) cairo_ctx.scale(1.0 / scale[0], 1.0 / scale[1]) cairo_ctx.stroke() finally: cairo_ctx.restore() # check it's visible enough start = ( max(0, widget_position[0]), max(0, widget_position[1]), ) size = ( min(win_alloc.width - start[0], widget_alloc.width), min(win_alloc.height - start[1], widget_alloc.height), ) if size[0] <= 15 or size[1] <= 15: LOGGER.warning( "%s is folded/hidden. Cannot screenshot", gtk_widget ) return None # then cut it start = ( max(0, widget_position[0] - margins[0]), max(0, widget_position[1] - margins[1]) ) size = ( min( win_alloc.width - start[0], widget_alloc.width + margins[0] + margins[2] ), min( win_alloc.height - start[1], widget_alloc.height + margins[1] + margins[3], ) ) LOGGER.info( "Making screenshot of %s: %s (%s, %s)", gtk_widget, out_file_url, start, size ) surface = cairo.ImageSurface(cairo.FORMAT_RGB24, *size) cairo_ctx = cairo.Context(surface) cairo_ctx.translate(-start[0], -start[1]) cairo_ctx.set_source_surface(win_surface) cairo_ctx.paint() with self.core.call_success("fs_open", out_file_url, "wb") as fd: surface.write_to_png(fd) return True def on_uncaught_exception(self, exc_info): self.nb_uncaught += 1 if self.nb_uncaught > MAX_UNCAUGHT_EXCEPTION_SCREENSHOTS: # limit the number of screenshots in one session. return LOGGER.info("Uncaught exception. Taking screenshots") (_, promise) = self.screenshot_snap_all_promise( name="uncaught_exception_screenshots" ) promise.schedule() def bug_report_get_attachments(self, out: dict): for (date, file_url) in self.archiver.get_archived(): out[file_url] = { 'date': date, 'include_by_default': False, 'file_type': _("App. screenshots"), 'file_url': file_url, 'file_size': self.core.call_success("fs_getsize", file_url), } out['screenshot_now'] = { 'include_by_default': False, 'date': None, 'file_type': _("App. screenshots"), 'file_url': _("Select to generate"), 'file_size': 0, } def _update_attachment(self, file_url, *args): self.core.call_all( "bug_report_update_attachment", "screenshot_now", { "file_url": file_url, "file_size": self.core.call_success("fs_getsize", file_url), }, *args ) def on_bug_report_attachment_selected(self, attachment_id, *args): if attachment_id != 'screenshot_now': return (file_url, promise) = self.screenshot_snap_all_promise(temporary=True) promise = promise.then(self._update_attachment, file_url, *args) promise.schedule() paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/000077500000000000000000000000001417573700700274435ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/__init__.py000066400000000000000000000055761417573700700315710ustar00rootroot00000000000000import logging import traceback try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core from .. import deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.nb_dialogs = 3 self.windows = [] self.visible = False def get_interfaces(self): return [ 'chkdeps', 'gtk_uncaught_exception', 'gtk_window_listener', ] def get_deps(self): return [ { 'interface': 'gtk_bug_report_dialog', 'defaults': ['openpaperwork_gtk.bug_report'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'uncaught_exception', 'defaults': ['openpaperwork_core.uncaught_exception'], }, ] def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(deps.GTK) def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def on_uncaught_exception(self, exc_info): if exc_info[-1] is None: LOGGER.warning("No stacktrace. Won't display error dialog") return if self.visible: LOGGER.warning( "Error dialog currently displayed. Won't display another one" ) return if self.nb_dialogs <= 0: LOGGER.warning( "Too many error dialogs displayed. Won't display them anymore" ) return LOGGER.info("Uncaught exception. Showing error dialog") content = traceback.format_exception(*exc_info) content = ''.join(content) widget_tree = self.core.call_success( "gtk_load_widget_tree", "openpaperwork_gtk.uncaught_exception", "uncaught_exception.glade" ) if widget_tree is None: LOGGER.warning("Failed to load widget tree") return widget_tree.get_object("error").set_text(content, -1) dialog = widget_tree.get_object("dialog") if len(self.windows) > 0: dialog.set_transient_for(self.windows[-1]) dialog.connect("response", self._on_response) dialog.show_all() self.visible = True self.nb_dialogs -= 1 def _on_response(self, dialog, response_id): LOGGER.info("Response: %s", response_id) dialog.destroy() self.visible = False if response_id != Gtk.ResponseType.ACCEPT: return self.core.call_all("open_bug_report") paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/uncaught_exception/uncaught_exception.glade000066400000000000000000000162561417573700700343470ustar00rootroot00000000000000 False Unexpected Error 640 320 dialog False vertical 2 False end Report True True True True True 0 gtk-ok True True True True True True 1 False False 0 True False True False 20 20 20 20 gtk-dialog-error 6 False True 0 True False 20 20 20 20 vertical 20 True False An unexpected error occured. 0 False True 0 True True in True True False char 5 5 5 5 error True True 1 True True 1 True True 1 button1 button2 paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/widgets/000077500000000000000000000000001417573700700252155ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/widgets/__init__.py000066400000000000000000000000001417573700700273140ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/widgets/calendar/000077500000000000000000000000001417573700700267665ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/widgets/calendar/__init__.py000066400000000000000000000036641417573700700311100ustar00rootroot00000000000000import datetime import openpaperwork_core class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'gtk_calendar_popover', ] def get_deps(self): return [ { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, ] def gtk_calendar_add_popover(self, gtk_entry): widget_tree = self.core.call_success( "gtk_load_widget_tree", "openpaperwork_gtk.widgets.calendar", "calendar.glade" ) widget_tree.get_object("calendar_popover").set_relative_to( gtk_entry ) gtk_entry.connect( "icon-release", self._open_calendar, widget_tree ) widget_tree.get_object("calendar_calendar").connect( "day-selected-double-click", self._update_date, gtk_entry, widget_tree ) def _open_calendar( self, gtk_entry, icon_pos, event, widget_tree): date = self.core.call_success( "i18n_parse_date_short", gtk_entry.get_text() ) if date is None: return widget_tree.get_object("calendar_calendar").select_month( date.month - 1, date.year ) widget_tree.get_object("calendar_calendar").select_day( date.day ) widget_tree.get_object("calendar_popover").set_visible(True) def _update_date(self, gtk_calendar, gtk_entry, widget_tree): date = widget_tree.get_object("calendar_calendar").get_date() date = datetime.datetime(year=date[0], month=date[1] + 1, day=date[2]) date = self.core.call_success("i18n_date_short", date) gtk_entry.set_text(date) widget_tree.get_object("calendar_popover").set_visible(False) paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/widgets/calendar/calendar.glade000066400000000000000000000005301417573700700315330ustar00rootroot00000000000000 True paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/000077500000000000000000000000001417573700700265015ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/__init__.py000066400000000000000000000000001417573700700306000ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/widgets/charts/lines.py000066400000000000000000000632561417573700700302010ustar00rootroot00000000000000import collections import logging import math import random import statistics import openpaperwork_core import openpaperwork_core.deps import openpaperwork_gtk.deps from openpaperwork_gtk import _ GI_AVAILABLE = False GTK_AVAILABLE = False PANGO_AVAILABLE = False try: import gi from gi.repository import GObject GI_AVAILABLE = True except (ImportError, ValueError): pass if GI_AVAILABLE: try: gi.require_version('Gdk', '3.0') gi.require_version('Gtk', '3.0') from gi.repository import Gdk from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): pass try: gi.require_version('Pango', '1.0') gi.require_version('PangoCairo', '1.0') from gi.repository import Pango from gi.repository import PangoCairo PANGO_AVAILABLE = True except (ImportError, ValueError): pass LOGGER = logging.getLogger(__name__) SUM_COLOR = (0, 0, 0) class Schema(object): def __init__( self, column_value_id_idx, column_line_id_idx, column_axis_x_values_idx, column_axis_x_names_idx, column_axis_y_values_idx, column_axis_y_names_idx, highlight_x_range=(math.inf, -math.inf)): self.column_value_id_idx = column_value_id_idx self.column_line_id_idx = column_line_id_idx self.column_axis_x_values_idx = column_axis_x_values_idx self.column_axis_x_names_idx = column_axis_x_names_idx self.column_axis_y_values_idx = column_axis_y_values_idx self.column_axis_y_names_idx = column_axis_y_names_idx self.highlight_x_range = highlight_x_range class ColorGenerator(object): def __init__(self): self.colors = [] self.color_idx = -1 def reset(self): self.color_idx = -1 def __next__(self): self.color_idx += 1 if self.color_idx >= len(self.colors): self.colors.append(( random.randint(0, 0xFF) / 0xFF, random.randint(0, 0xFF) / 0xFF, random.randint(0, 0xFF) / 0xFF, )) return self.colors[self.color_idx] class DrawContext(object): """ Custom drawing context around cairo context, so we can change the scale without changing the line width. """ def __init__(self, ctx): self.ctx = ctx def get_cairo(self): if hasattr(self.ctx, 'get_cairo'): return self.ctx.get_cairo() return self.ctx def translate_pt(self, pt_x, pt_y): return (pt_x, pt_y) def untranslate_pt(self, pt_x, pt_y): return (pt_x, pt_y) def distance(self, pt_a_x, pt_a_y, pt_b_x, pt_b_y): d_x = abs(pt_a_x - pt_b_x) d_y = abs(pt_a_y - pt_b_y) return (d_x ** 2) + (d_y ** 2) def arc(self, x, y, radius, angle1, angle2): self.ctx.arc(x, y, radius, angle1, angle2) def save(self): self.ctx.save() def restore(self): self.ctx.restore() def set_source_rgb(self, r, g, b): self.ctx.set_source_rgb(r, g, b) def set_source_rgba(self, r, g, b, a): self.ctx.set_source_rgba(r, g, b, a) def set_line_width(self, line_width): self.ctx.set_line_width(line_width) def move_to(self, pt_x, pt_y): self.ctx.move_to(pt_x, pt_y) def line_to(self, pt_x, pt_y): self.ctx.line_to(pt_x, pt_y) def rectangle(self, x, y, w, h): self.ctx.rectangle(x, y, w, h) def stroke(self): self.ctx.stroke() def fill(self): self.ctx.fill() def translate(self, x, y): return DrawContextTranslated(self, x, y) def scale(self, x, y): return DrawContextScaled(self, x, y) class DrawContextTranslated(DrawContext): def __init__(self, ctx, translation_x, translation_y): super().__init__(ctx) self.x = translation_x self.y = translation_y def translate_pt(self, pt_x, pt_y): (pt_x, pt_y) = (pt_x + self.x, pt_y + self.y) return self.ctx.translate_pt(pt_x, pt_y) def untranslate_pt(self, pt_x, pt_y): (pt_x, pt_y) = self.ctx.untranslate_pt(pt_x, pt_y) return (pt_x - self.x, pt_y - self.y) def distance( self, pt_untranslated_x, pt_untranslated_y, pt_translated_x, pt_translated_y): pt_a_x = pt_untranslated_x + self.x pt_a_y = pt_untranslated_y + self.y return self.ctx.distance( pt_a_x, pt_a_y, pt_translated_x, pt_translated_y, ) def arc(self, x, y, radius, angle1, angle2): self.ctx.arc(x + self.x, y + self.y, radius, angle1, angle2) def move_to(self, pt_x, pt_y): self.ctx.move_to(pt_x + self.x, pt_y + self.y) def line_to(self, pt_x, pt_y): self.ctx.line_to(pt_x + self.x, pt_y + self.y) def rectangle(self, x, y, w, h): self.ctx.rectangle(x + self.x, y + self.y, w, h) class DrawContextScaled(DrawContext): def __init__(self, ctx, scale_x, scale_y): super().__init__(ctx) self.x = scale_x self.y = scale_y def translate_pt(self, pt_x, pt_y): (pt_x, pt_y) = (pt_x * self.x, pt_y * self.y) return self.ctx.translate_pt(pt_x, pt_y) def untranslate_pt(self, pt_x, pt_y): (pt_x, pt_y) = self.ctx.untranslate_pt(pt_x, pt_y) return (pt_x / self.x, pt_y / self.y) def distance( self, pt_unscaled_x, pt_unscaled_y, pt_scaled_x, pt_scaled_y): pt_a_x = pt_unscaled_x * self.x pt_a_y = pt_unscaled_y * self.y return self.ctx.distance( pt_a_x, pt_a_y, pt_scaled_x, pt_scaled_y, ) def arc(self, x, y, radius, angle1, angle2): self.ctx.arc(x * self.x, y * self.y, radius, angle1, angle2) def move_to(self, pt_x, pt_y): self.ctx.move_to(pt_x * self.x, pt_y * self.y) def line_to(self, pt_x, pt_y): self.ctx.line_to(pt_x * self.x, pt_y * self.y) def rectangle(self, x, y, w, h): self.ctx.rectangle(x * self.x, y * self.y, w * self.x, h * self.y) class Point(object): RADIUS = 2 LABEL_LINE_HEIGHT = 14 def __init__(self, liststore_idx, x, y, label_x, label_y, color): self.liststore_idx = liststore_idx self.x = x self.y = y self.label_x = label_x self.label_y = label_y self.color = color def copy(self): return Point( self.liststore_idx, self.x, self.y, self.label_x, self.label_y, self.color ) def __lt__(self, other): if self.x < other.x: return True if self.y < other.y: return True return False @staticmethod def merge(pts): merged_pt = pts[0].copy() merged_pt.y = statistics.mean([p.y for p in pts]) merged_pt.label_y = "\n".join([p.label_y for p in pts]) return merged_pt @staticmethod def merge_same_x(all_pts): pack = [] last_x = None pts = [] for pt in all_pts: if last_x is None: pack.append(pt) last_x = pt.x elif last_x == pt.x: pack.append(pt) else: pts.append(Point.merge(pack)) last_x = pt.x pack = [pt] if len(pack) > 0: pts.append(Point.merge(pack)) return pts @staticmethod def from_liststore_line(liststore_idx, line, schema, color): return Point( liststore_idx, line[schema.column_axis_x_values_idx], line[schema.column_axis_y_values_idx], line[schema.column_axis_x_names_idx], line[schema.column_axis_y_names_idx], color ) def draw_label_x(self, widget_size, draw_ctx): height = (self.label_x.count("\n") + 1) * self.LABEL_LINE_HEIGHT cairo_ctx = draw_ctx.get_cairo() x = draw_ctx.translate_pt(self.x, widget_size[1] - self.y)[0] widget_size = draw_ctx.translate_pt(widget_size[0], widget_size[1]) layout = PangoCairo.create_layout(cairo_ctx) layout.set_text(self.label_x, -1) txt_size = layout.get_size() if 0 in txt_size: return cairo_ctx.save() try: txt_scale = height / txt_size[1] txt_width = txt_size[0] * txt_scale if x <= widget_size[0] / 2: x += 5 else: x -= (txt_width + 10) cairo_ctx.set_source_rgb(0, 0, 0) cairo_ctx.translate(x, 0) cairo_ctx.scale(txt_scale * Pango.SCALE, txt_scale * Pango.SCALE) PangoCairo.update_layout(cairo_ctx, layout) PangoCairo.show_layout(cairo_ctx, layout) finally: cairo_ctx.restore() def draw_label_y(self, widget_size, draw_ctx): height = (self.label_y.count("\n") + 1) * self.LABEL_LINE_HEIGHT cairo_ctx = draw_ctx.get_cairo() y = draw_ctx.translate_pt(self.x, widget_size[1] - self.y)[1] widget_size = draw_ctx.translate_pt(widget_size[0], widget_size[1]) layout = PangoCairo.create_layout(cairo_ctx) layout.set_text(self.label_y, -1) txt_size = layout.get_size() if 0 in txt_size: return cairo_ctx.save() try: txt_scale = height / txt_size[1] if y <= widget_size[1] / 2: y += 2 else: y -= (height + 2) cairo_ctx.set_source_rgb(0, 0, 0) cairo_ctx.translate(0, y) cairo_ctx.scale(txt_scale * Pango.SCALE, txt_scale * Pango.SCALE) PangoCairo.update_layout(cairo_ctx, layout) PangoCairo.show_layout(cairo_ctx, layout) finally: cairo_ctx.restore() def draw_highlight(self, widget_size, draw_ctx): draw_ctx.set_source_rgb(0.57, 0.7, 0.837) draw_ctx.set_line_width(1) # TODO(Jflesch): we should use the real widget size, and not # the widget size relative to point (0, 0) draw_ctx.move_to(-widget_size[0] * 2, widget_size[1] - self.y) draw_ctx.line_to(widget_size[0] * 2, widget_size[1] - self.y) draw_ctx.stroke() # TODO(Jflesch): we should use the real widget size, and not # the widget size relative to point (0, 0) draw_ctx.move_to(self.x, -widget_size[1] * 2) draw_ctx.line_to(self.x, widget_size[1] * 2) draw_ctx.stroke() self.draw_label_x(widget_size, draw_ctx) self.draw_label_y(widget_size, draw_ctx) def distance(self, widget_size, draw_ctx, x, y): return draw_ctx.distance(self.x, widget_size[1] - self.y, x, y) class Line(object): LEGEND_CIRCLE_RADIUS = 8 LEGEND_LINE_HEIGHT = 16 LEGEND_SPACING = 8 def __init__(self, line_name, points, color, line_width=1.0): self.line_name = line_name self.points = points self.points.sort(key=lambda pt: pt.x) self.color = color self.minmax = ((math.inf, 0), (0, 0)) self.line_width = line_width for pt in points: self.minmax = ( ( min(self.minmax[0][0], pt.x), max(self.minmax[0][1], pt.x), ), ( min(self.minmax[1][0], pt.y), max(self.minmax[1][1], pt.y), ), ) @staticmethod def from_liststore_lines(line_name, liststore_lines, schema, color): pts = [ Point.from_liststore_line(liststore_idx, line, schema, color) for (liststore_idx, line) in liststore_lines ] pts = Point.merge_same_x(pts) return Line(line_name, pts, color) @staticmethod def make_line_sum(other_lines): pts = [] for line in other_lines: pts += [ (line.line_name, pt.copy()) for pt in line.points ] pts.sort(key=lambda pt: pt[1].x) total_txt = _("Total: {}") last_y = collections.defaultdict(lambda: 0) for (line_name, pt) in pts: last_y[line_name] = pt.y pt.y = sum(last_y.values()) pt.label_y = total_txt.format(pt.y) pts = [pt for (line_name, pt) in pts] pts = Point.merge_same_x(pts) return Line(_("Total"), pts, SUM_COLOR, 2.0) def draw_chart(self, widget_size, draw_ctx): draw_ctx.save() try: draw_ctx.set_line_width(self.line_width) draw_ctx.set_source_rgb(*self.color) for (idx, pt) in enumerate(self.points): if idx == 0: draw_ctx.move_to(pt.x, widget_size[1] - pt.y) else: draw_ctx.line_to(pt.x, widget_size[1] - pt.y) draw_ctx.stroke() finally: draw_ctx.restore() def draw_legend(self, widget_size, cairo_ctx, x, y): layout = PangoCairo.create_layout(cairo_ctx) layout.set_text(self.line_name, -1) txt_size = layout.get_size() if 0 in txt_size: return (x, y) cairo_ctx.save() try: txt_scale = self.LEGEND_LINE_HEIGHT / txt_size[1] txt_width = txt_size[0] * txt_scale width = txt_width width += 2 * self.LEGEND_CIRCLE_RADIUS width += 2 * self.LEGEND_SPACING if x + width >= widget_size[0]: y += self.LEGEND_LINE_HEIGHT + self.LEGEND_SPACING x = 0 cairo_ctx.set_source_rgb(*self.color) cairo_ctx.arc( x + self.LEGEND_CIRCLE_RADIUS, y + self.LEGEND_CIRCLE_RADIUS, self.LEGEND_CIRCLE_RADIUS, 0, math.pi * 2 ) cairo_ctx.fill() x += (2 * self.LEGEND_CIRCLE_RADIUS) + self.LEGEND_SPACING cairo_ctx.set_source_rgb(0.0, 0.0, 0.0) cairo_ctx.translate(x, y) cairo_ctx.scale(txt_scale * Pango.SCALE, txt_scale * Pango.SCALE) PangoCairo.update_layout(cairo_ctx, layout) PangoCairo.show_layout(cairo_ctx, layout) x += txt_width + self.LEGEND_SPACING finally: cairo_ctx.restore() return (x, y) def get_closest_point(self, widget_size, draw_ctx, x, y): return min(( (point.distance(widget_size, draw_ctx, x, y), point) for point in self.points ), default=None) class Lines(object): HIGHLIGHT_COLOR = (0.5, 0.5, 0.5, 0.5) def __init__(self, schema, liststore, color_generator): self.schema = schema self.liststore = liststore self.color_generator = color_generator self.active_point = None self.lines = [] self.minmax = ((math.inf, 0), (0, 0),) def reset(self): self.lines = [] def reload(self): self.minmax = ((math.inf, 0), (0, 0),) self.color_generator.reset() lines = collections.defaultdict(list) for (liststore_idx, line) in enumerate(self.liststore): line_name = line[self.schema.column_line_id_idx] lines[line_name].append((liststore_idx, line)) self.lines = [ Line.from_liststore_lines( k, v, self.schema, next(self.color_generator) ) for (k, v) in lines.items() ] if len(self.lines) > 1: self.lines = [Line.make_line_sum(self.lines)] + self.lines for line in self.lines: self.minmax = ( ( min(self.minmax[0][0], line.minmax[0][0]), max(self.minmax[0][1], line.minmax[0][1]), ), ( min(self.minmax[1][0], line.minmax[1][0]), max(self.minmax[1][1], line.minmax[1][1]), ), ) def draw_chart_highlight(self, widget_size, draw_ctx): x_range = self.schema.highlight_x_range if x_range[1] <= x_range[0]: return if x_range[0] is -math.inf: x_range = (self.minmax[0][0], self.x_range[1]) if x_range[1] is math.inf: x_range = (x_range[0], self.minmax[0][1]) x_range = ( ( x_range[0] - self.minmax[0][0] ) / ( self.minmax[0][1] - self.minmax[0][0] ), ( x_range[1] - self.minmax[0][0] ) / ( self.minmax[0][1] - self.minmax[0][0] ), ) x_range = (x_range[0] * widget_size[0], x_range[1] * widget_size[0]) draw_ctx.save() try: draw_ctx.set_source_rgba(*self.HIGHLIGHT_COLOR) draw_ctx.rectangle( int(x_range[0]) + 1, 0, x_range[1] - x_range[0], widget_size[1] ) draw_ctx.fill() finally: draw_ctx.restore() def draw_chart(self, widget_size, draw_ctx): w = self.minmax[0][1] - self.minmax[0][0] h = self.minmax[1][1] - self.minmax[1][0] if w == 0 or h == 0: return draw_ctx = draw_ctx.scale(widget_size[0] / w, widget_size[1] / h) widget_size = (w, h) self.draw_chart_highlight(widget_size, draw_ctx) draw_ctx = draw_ctx.translate( -self.minmax[0][0], min(0, self.minmax[1][0]) ) widget_size = (widget_size[0] + self.minmax[0][0], widget_size[1]) draw_ctx.save() try: for line in self.lines: line.draw_chart(widget_size, draw_ctx) finally: draw_ctx.restore() if self.active_point is not None: self.active_point.draw_highlight(widget_size, draw_ctx) def draw_legend(self, widget_size, cairo_ctx): x = 0 y = 0 for line in self.lines: (x, y) = line.draw_legend(widget_size, cairo_ctx, x, y) return (x, y) def get_closest_point(self, widget_size, draw_ctx, x, y): w = self.minmax[0][1] - self.minmax[0][0] h = self.minmax[1][1] - self.minmax[1][0] if w == 0 or h == 0: return None draw_ctx = draw_ctx.scale(widget_size[0] / w, widget_size[1] / h) widget_size = (w, h) draw_ctx = draw_ctx.translate( -self.minmax[0][0], min(0, self.minmax[1][0]) ) widget_size = (widget_size[0] - self.minmax[0][0], widget_size[1]) return min(( line.get_closest_point(widget_size, draw_ctx, x, y) for line in self.lines ), default=None) class ChartDrawer(object): MARGIN = 5 def __init__(self, core, schema, liststore, color_generator): self.core = core self.lines = Lines(schema, liststore, color_generator) self.chart_widget = Gtk.DrawingArea.new() self.chart_widget.set_size_request(-1, 200) self.chart_widget.set_visible(True) self.chart_widget.connect("draw", self.draw_chart) self.chart_widget.connect("realize", self._on_chart_realized) self.chart_widget.connect("motion-notify-event", self._on_mouse_motion) self.legend_widget = Gtk.DrawingArea.new() self.legend_widget.set_visible(True) self.legend_widget.connect("draw", self.draw_legend) liststore.connect("row-changed", self.reload) liststore.connect("row-deleted", self.reload) liststore.connect("row-inserted", self.reload) liststore.connect("rows-reordered", self.reload) self._on_chart_realized() self.has_changed = False self.reload_planned = False self._reload() def _on_chart_realized(self, *args, **kwargs): mask = Gdk.EventMask.POINTER_MOTION_MASK self.chart_widget.add_events(mask) if self.chart_widget.get_window() is not None: self.chart_widget.get_window().set_events( self.chart_widget.get_window().get_events() | mask ) def reload(self, *args, **kwargs): self.lines.reset() self.chart_widget.queue_draw() self.legend_widget.queue_draw() if self.reload_planned: self.has_changed = True else: self.has_changed = False self.reload_planned = True promise = openpaperwork_core.promise.DelayPromise( self.core, delay_s=0.25 ) promise.then(self._reload) promise.schedule() def _reload(self): self.reload_planned = False if self.has_changed: self.reload() return self.lines.reload() self.chart_widget.queue_draw() self.legend_widget.queue_draw() def _on_mouse_motion(self, widget, event): draw_ctx = DrawContext(None) draw_ctx = draw_ctx.translate(self.MARGIN, self.MARGIN) widget_size = ( widget.get_allocated_width() - (2 * self.MARGIN), widget.get_allocated_height() - (2 * self.MARGIN), ) r = self.lines.get_closest_point( widget_size, draw_ctx, event.x, event.y ) if r is None: self.lines.active_point = None return (dist, point) = r if self.lines.active_point is not point: self.lines.active_point = point self.chart_widget.queue_draw() def draw_chart(self, widget, cairo_ctx): widget_size = ( widget.get_allocated_width() - (2 * self.MARGIN), widget.get_allocated_height() - (2 * self.MARGIN), ) draw_ctx = DrawContext(cairo_ctx) draw_ctx = draw_ctx.translate(self.MARGIN, self.MARGIN) self.lines.draw_chart(widget_size, draw_ctx) def draw_legend(self, widget, cairo_ctx): widget_size = ( widget.get_allocated_width() - (2 * self.MARGIN), widget.get_allocated_height() - (2 * self.MARGIN), ) cairo_ctx.translate(self.MARGIN, self.MARGIN) (x, y) = self.lines.draw_legend(widget_size, cairo_ctx) if x > 0: # the y we got is relative to the top of the current line. # the height is starting at the bottom of the current line. y += Line.LEGEND_SPACING + Line.LEGEND_LINE_HEIGHT if y + self.MARGIN != widget_size[1]: widget.set_size_request(-1, y + self.MARGIN) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.color_generator = ColorGenerator() def get_interfaces(self): return [ 'chkdeps', 'gtk_charts_lines', ] def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) if not PANGO_AVAILABLE: out['pango'].update(openpaperwork_core.deps.PANGO) def gtk_charts_lines_get( self, liststore, *args, **kwargs): """ Return a Gtk.Widget showing the specified line chart. Can display multiple lines on single chart. In the model, each row is a value (x, y) for a given line. Arguments: - model: Gtk.ListStore to use as model - column_value_id_idx: column in the model that contains the value IDs. - column_line_id_idx: column that contains the name of the line to which this value belongs. - column_axis_x_values_idx: column that contains the values for the X axis. Must contain integers, floats or doubles. - column_axis_x_names_idx: column that contains the names corresponding to the values on the X axis. Must be strings. - column_axis_y_values_idx: column that contains the values for the Y axis. Must contain integers, floats or doubles. - column_axis_y_names_idx: column that contains the names corresponding toi the values on the Y axis. Must be strings """ schema = Schema(*args, **kwargs) return ChartDrawer(self.core, schema, liststore, self.color_generator) if __name__ == "__main__": model = Gtk.ListStore.new(( GObject.TYPE_STRING, # value id GObject.TYPE_STRING, # line id GObject.TYPE_INT64, # value X GObject.TYPE_INT64, # value Y GObject.TYPE_STRING, # name X GObject.TYPE_STRING, # name Y )) model_content = ( ("value A55", "line A", 5, 5, "A5", "A5"), ("value B55", "line B", 5, 5, "B5", "B5"), ("value A67", "line A", 6, 7, "A6", "A7"), ("value B66", "line B", 6, 6, "B6", "B6"), # graph must support having many values at the same position in X ("value A77", "line A", 7, 7, "A7", "A7"), ("value A73", "line A", 7, 3, "A7", "A3"), ("value A79", "line A", 7, 9, "A7", "A9"), ("value B79", "line B", 7, 9, "B7", "B9"), ("value A83", "line A", 8, 3, "A8", "A3"), ("value B88", "line B", 8, 8, "B8", "B8"), ("value A97", "line A", 9, 7, "A9", "A7"), ("value B95", "line B", 9, 5, "B9", "B5"), ("value A10-5", "line A", 10, -5, "A10", "A-5"), ("value B910", "line B", 9, 10, "B9", "B10"), ) model.clear() for line in model_content: model.append(line) core = openpaperwork_core.Core() core.load("openpaperwork_gtk.widgets.charts.lines") core.init() chart_lines = core.call_success( "gtk_charts_lines_get", model, column_value_id_idx=0, column_line_id_idx=1, column_axis_x_values_idx=2, column_axis_y_values_idx=3, column_axis_x_names_idx=4, column_axis_y_names_idx=5 ) window = Gtk.Window() window.set_default_size(600, 600) window.add(chart_lines.chart_widget) window.show_all() window_legend = Gtk.Window() window_legend.set_default_size(100, 100) window_legend.add(chart_lines.legend_widget) window_legend.show_all() Gtk.main() paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/widgets/progress/000077500000000000000000000000001417573700700270615ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/widgets/progress/__init__.py000066400000000000000000000214401417573700700311730ustar00rootroot00000000000000import logging import math import threading import time import openpaperwork_core import openpaperwork_core.deps try: import gi gi.require_version('Gdk', '3.0') from gi.repository import Gdk GDK_AVAILABLE = True except (ImportError, ValueError): GDK_AVAILABLE = False NEEDS_ATTENTION_TIMEOUT = 2.1 LOGGER = logging.getLogger(__name__) TIME_BETWEEN_UPDATES = 0.3 # Tasks are often chained one after the other. We don't want the button/popover # to disappear and reappear continually. So when a task ends, we # give them some extra time to live. # STAY_ALIVES is the number of updates we wait before hiding them. STAY_ALIVES = int(2.0 / TIME_BETWEEN_UPDATES) class ProgressWidget(object): def __init__(self, core): self.core = core self.widget = None # A thread updates the widgets every 300ms. We don't update them # each time on_progress() is called to not degrade performanes self.thread = None self.lock = threading.RLock() self.progress_widget_trees = {} # self.progresses is only used to transmist new progress updates # to the thread self.progresses = {} self.button_widget_tree = None self.details_widget_tree = None self.stay_alives = STAY_ALIVES self.button_widget_tree = self.core.call_success( "gtk_load_widget_tree", "openpaperwork_gtk.widgets.progress", "progress_button.glade" ) if self.button_widget_tree is None: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to load widget tree") return self.details_widget_tree = self.core.call_success( "gtk_load_widget_tree", "openpaperwork_gtk.widgets.progress", "progress_popover.glade" ) if self.details_widget_tree is None: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to load widget tree") return self.button_widget_tree.get_object("progress_button").set_popover( self.details_widget_tree.get_object( "progresses_popover" ) ) self.button_widget_tree.get_object("progress_icon").connect( "draw", self._on_icon_draw ) self.widget = self.button_widget_tree.get_object("progress_revealer") def needs_attention(self): button = self.button_widget_tree.get_object("progress_button") button.get_style_context().add_class("progress_button_needs_attention") self.core.call_success( "mainloop_schedule", button.get_style_context().remove_class, "progress_button_needs_attention", delay_s=NEEDS_ATTENTION_TIMEOUT ) def _thread(self): self.stay_alives = STAY_ALIVES while self.thread is not None: time.sleep(TIME_BETWEEN_UPDATES) self.core.call_one( "mainloop_execute", self._upd_progress_widgets ) def _upd_progress_widgets(self): with self.lock: for (upd_type, (progress, description)) in self.progresses.items(): self._upd_progress_widget( upd_type, progress, description ) self.progresses = {} if len(self.progress_widget_trees) > 0: self.stay_alives = STAY_ALIVES return if self.stay_alives > 0: self.stay_alives -= 1 return self.button_widget_tree.get_object( "progress_revealer" ).set_reveal_child(False) self.thread = None return def _upd_progress_widget(self, upd_type, progress, description): if progress >= 1.0: # deletion of progress if upd_type not in self.progress_widget_trees: LOGGER.warning( "Got 2 notifications of end of task for '%s'", upd_type ) return LOGGER.info("Task '%s' has ended", upd_type) widget_tree = self.progress_widget_trees.pop(upd_type) box = self.details_widget_tree.get_object( "progresses_box" ) details = widget_tree.get_object("progress_bar") box.remove(details) details.unparent() self.button_widget_tree.get_object("progress_button").queue_draw() self.needs_attention() LOGGER.info( "Task '%s' has ended (%d remaining)", upd_type, len(self.progress_widget_trees) ) return if upd_type not in self.progress_widget_trees: # creation of progress LOGGER.info( "Task '%s' has started (%d already active)", upd_type, len(self.progress_widget_trees) ) widget_tree = self.core.call_success( "gtk_load_widget_tree", "openpaperwork_gtk.widgets.progress", "progress_details.glade" ) box = self.details_widget_tree.get_object( "progresses_box" ) box.add(widget_tree.get_object("progress_bar")) self.progress_widget_trees[upd_type] = widget_tree self.needs_attention() else: widget_tree = self.progress_widget_trees[upd_type] # update of progress progress_bar = widget_tree.get_object("progress_bar") progress_bar.set_fraction(progress) progress_bar.set_text(description if description is not None else "") self.button_widget_tree.get_object("progress_button").queue_draw() self.button_widget_tree.get_object( "progress_revealer" ).set_reveal_child(True) def on_progress(self, upd_type, progress, description=None): with self.lock: if progress > 1.0: LOGGER.warning( "Invalid progression (%f) for [%s]", progress, upd_type ) progress = 1.0 self.progresses[upd_type] = (progress, description) if self.thread is None: self.thread = threading.Thread(target=self._thread) self.thread.daemon = True self.thread.start() def _on_icon_draw(self, drawing_area, cairo_ctx): if len(self.progress_widget_trees) <= 0: ratio = 1.0 else: ratio = sum([ widget_tree.get_object("progress_bar").get_fraction() for widget_tree in self.progress_widget_trees.values() ]) / len(self.progress_widget_trees) # Translated in Python from Nautilus source code # (2020/02/29: src/nautilus-toolbar.c:on_operations_icon_draw()) style_context = drawing_area.get_style_context() foreground = style_context.get_color(drawing_area.get_state_flags()) background = foreground background.alpha *= 0.3 w = drawing_area.get_allocated_width() h = drawing_area.get_allocated_height() Gdk.cairo_set_source_rgba(cairo_ctx, background) cairo_ctx.arc(w / 2.0, h / 2.0, min(w, h) / 2.0, 0, 2 * math.pi) cairo_ctx.fill() cairo_ctx.move_to(w / 2.0, h / 2.0) Gdk.cairo_set_source_rgba(cairo_ctx, foreground) cairo_ctx.arc( w / 2.0, h / 2.0, min(w, h) / 2.0, -math.pi / 2.0, (ratio * 2 * math.pi) - (math.pi / 2.0) ) cairo_ctx.fill() class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.widgets = [] def get_interfaces(self): return [ 'chkdeps', 'gtk_progress_widget', 'progress_listener', ] def get_deps(self): return [ { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, ] def init(self, core): super().init(core) self.core.call_success( "gtk_load_css", "openpaperwork_gtk.widgets.progress", "progress.css" ) def chkdeps(self, out: dict): if not GDK_AVAILABLE: out['gdk'].update(openpaperwork_core.deps.GDK) def gtk_progress_make_widget(self): widget = ProgressWidget(self.core) if widget.widget is not None: # gtk may not be available self.widgets.append(widget) return widget.widget def on_progress(self, upd_type, progress, description=None): for widget in self.widgets: widget.on_progress(upd_type, progress, description) paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/widgets/progress/progress.css000066400000000000000000000013651417573700700314440ustar00rootroot00000000000000/* copied from Nautilus and modified slightly */ @keyframes progress_needs_attention_keyframes { 0% { background-image: linear-gradient( to bottom, #fafafa, #ededed 40%, #e0e0e0 ); border-color: @borders; } 30% { background-image: linear-gradient( to bottom, @theme_base_color, @theme_base_color, @theme_base_color ); border-color: @theme_fg_color; } 90% { background-image: linear-gradient( to bottom, @theme_base_color, @theme_base_color, @theme_base_color ); border-color: @theme_fg_color; } 100% { background-image: linear-gradient( to bottom, #fafafa, #ededed 40%, #e0e0e0 ); border-color: @borders; } } .progress_button_needs_attention { animation: progress_needs_attention_keyframes 2s ease-in-out; } paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/widgets/progress/progress_button.glade000066400000000000000000000024721417573700700333230ustar00rootroot00000000000000 True False center center slide-right True True True False Show background tasks 6 16 16 True False center center paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/widgets/progress/progress_details.glade000066400000000000000000000012471417573700700334340ustar00rootroot00000000000000 True False center 2 4 True 0.05 True paperwork-2.1.1/openpaperwork-gtk/src/openpaperwork_gtk/widgets/progress/progress_popover.glade000066400000000000000000000011641417573700700334770ustar00rootroot00000000000000 False 500 True False vertical 18 paperwork-2.1.1/openpaperwork-gtk/tests/000077500000000000000000000000001417573700700203615ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/tests/__init__.py000066400000000000000000000000001417573700700224600ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/tests/fs/000077500000000000000000000000001417573700700207715ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/tests/fs/__init__.py000066400000000000000000000000001417573700700230700ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/tests/fs/tests_gio.py000066400000000000000000000034321417573700700233450ustar00rootroot00000000000000import openpaperwork_core.tests.local_file PLUGIN_NAME = "openpaperwork_gtk.fs.gio" class TestSafe(openpaperwork_core.tests.local_file.AbstractTestSafe): def get_plugin_name(self): return PLUGIN_NAME class TestUnsafe(openpaperwork_core.tests.local_file.AbstractTestUnsafe): def get_plugin_name(self): return PLUGIN_NAME class TestOpen(openpaperwork_core.tests.local_file.AbstractTestOpen): def get_plugin_name(self): return PLUGIN_NAME class TestExists(openpaperwork_core.tests.local_file.AbstractTestExists): def get_plugin_name(self): return PLUGIN_NAME class TestListDir(openpaperwork_core.tests.local_file.AbstractTestListDir): def get_plugin_name(self): return PLUGIN_NAME class TestRename(openpaperwork_core.tests.local_file.AbstractTestRename): def get_plugin_name(self): return PLUGIN_NAME class TestUnlink(openpaperwork_core.tests.local_file.AbstractTestUnlink): def get_plugin_name(self): return PLUGIN_NAME class TestGetMtime(openpaperwork_core.tests.local_file.AbstractTestGetMtime): def get_plugin_name(self): return PLUGIN_NAME class TestGetsize(openpaperwork_core.tests.local_file.AbstractTestGetsize): def get_plugin_name(self): return PLUGIN_NAME class TestIsdir(openpaperwork_core.tests.local_file.AbstractTestIsdir): def get_plugin_name(self): return PLUGIN_NAME class TestCopy(openpaperwork_core.tests.local_file.AbstractTestCopy): def get_plugin_name(self): return PLUGIN_NAME class TestMkdirP(openpaperwork_core.tests.local_file.AbstractTestMkdirP): def get_plugin_name(self): return PLUGIN_NAME class TestTemp(openpaperwork_core.tests.local_file.AbstractTestTemp): def get_plugin_name(self): return PLUGIN_NAME paperwork-2.1.1/openpaperwork-gtk/tests/mainloop/000077500000000000000000000000001417573700700221775ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/tests/mainloop/__init__.py000066400000000000000000000000001417573700700242760ustar00rootroot00000000000000paperwork-2.1.1/openpaperwork-gtk/tests/mainloop/tests_gtk.py000066400000000000000000000005431417573700700245620ustar00rootroot00000000000000import openpaperwork_core.mainloop.tests class TestCallback(openpaperwork_core.mainloop.tests.AbstractTestCallback): def get_plugin_name(self): return "openpaperwork_gtk.mainloop.glib" class TestPromise(openpaperwork_core.mainloop.tests.AbstractTestPromise): def get_plugin_name(self): return "openpaperwork_gtk.mainloop.glib" paperwork-2.1.1/paperwork-backend/000077500000000000000000000000001417573700700171375ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/ChangeLog000066400000000000000000000212471417573700700207170ustar00rootroot000000000000002022/01/31 - 2.1.1: - guesswork.label.sklearn: Fix: Handle gracefully documents without text - model.pdf: take into account some PDF may be really really damaged 2021/12/05 - 2.1.0: - Support for all document types supported by LibreOffice (requires LibreOffice to be installed) - Support for password-protected PDF files - Label guessing: replace simplebayes by sklearn (GaussianNB): require more resources but much more accurate - Version data files: If the version changes, rebuild them all - Fix page export: img: do not use page number in file names - Cropping by scanner calibration: Never crop pages that already have text (fix issue where cropping was applied when changing document date / ID) - API: rename transaction methods: add_obj() --> add_doc(), upd_obj() --> upd_doc(), del_obj() --> del_doc(), unchanged_obj() --> unchanged_doc() 2021/05/24 - 2.0.3: - hOCR: fix page_get_text_by_url(): Do not return the hOCR title in the text (it's always "OCR output"). - Image loading (used for file import): file extension check must be case-insensitive (".jpeg" and ".JPEG" must be both accepted) - OCR: Fix: By default, never run OCR on pages that already have text. - Take into account Cairo image size limitations (dimensions can't be higher than 32k). Crop images accordingly if required. - PDF: work around possible weird replies from LibPoppler regarding line/word boxes (avoid useless background exception) - Swedish translations added - Backend: openpaperwork_gtk.fs.gio has been removed from the minimum list of required plugins. fs.python is good enough to load the configuration. - Backend: model.labels: Call to callbacks "on_all_labels_loaded" has been removed. It was redundant with the call to callbacks "on_label_loading_end" 2021/01/01 - 2.0.2: - beacon.sysinfo: report some extra infos to openpaper.work: CPU max frequency, number of CPU cores, amount of memory, version of Python. - add dependency on psutil - PyOCR: Fix for people who seem to have no locale configured (?!) - PDF: Fix: Write page mapping in the order of original page indexes, as expected when we read it back later. (otherwise, we may get weird behaviours) - Labels: When removing labels, don't add extra empty lines 2020/11/15 - 2.0.1: - Model: Fix: When the user move a page, they may actually creating a new document. - Libinsane + bug report: the file to attach to the bug report should be called 'scanner_*.json', not 'statistics_*.json' - Import: Don't use the same name for recursive importer and single importer. - Import: If the single file importer has matched a file to import, make sure the recursive one doesn't match it too. - Windows: poppler.memory: work around suspected memory leak regarding Gio.MemoryInput.new_from_data() - When thumbnail are deleted by user action, never send them to trash, really delete them instead. - Include tests in Pypi package (thanks to Elliott Sales de Andrade) 2020/10/17 - 2.0: - Full rewrite - Use of plugin system of openpaperwork_core to split features - PDF can be edited - Pages can be reinitialized to their initial states (reset) - Multiple languages can be used for OCR - Automated tests have been added - Features that could be reused in other applications have been move to openpaperwork_core and openpaperwork_gtk - Thumbnails are slightly smaller (they will be resized automatically) 2019/12/20 - 1.3.1: - Backend: Check if thumbnail file is writable before updating it (thanks to Gregor Godbersen) - Backend: Make indexation more resilient to errors (corrupted PDFs, etc). - Backend: chkdeps: look for Libinsane (no known package yet) 2019/08/17 - 1.3.0: - PDF export: PDFs can now be regenerated when exporting. Regenerated versions will include words from the OCR, but some metadata may be lost. - Optimization: Speed up conversions from PIL image to GdkPixbuf (used for export previews and thumbnail display) - Disable the use of a dedicated process for index operations: it prevents debugging - New dependency: Do not use platform.dist() or platform.linux_distribution() anymore: It's deprecated and will be removed in Python 3.8. Use instead the module 'distro'. - paperwork-shell: Add name and label arguments to command 'import' (thanks to Stéphane Brunner) - Backend: Fix importing PNG files with transparency (thanks to Balló György) - Fix warnings related to regexes escaping + various other cleanups (thanks to Elliott Sales de Andrade) 2018/03/01 - 1.2.4: - Import: Remove ambiguity: Importers designed for import of directory will not try to import individual files. They will just let the importers designed for importing single file take care of it. - Label guessing: Fix the way bayesian filters are updated (will trigger an index rebuild). - paperwork-shell/labels/guessing: return scores as well as labels (useful for testing/debuging) - Optim: PDF: Keep in memory the page sizes. It's an information very often requested when rendering and it cannot change with PDFs 2018/02/01 - 1.2.3: - Windows: Fix labels handling: Fix CSV file reading - Fix global deletion of a label - Flatpak: Fix deletion of documents - PDF: Fix file descriptor leak - Flatpak: Fix support on English systems 2017/11/14 - 1.2.2: - PDF: Fix thumbnail sizes. Incorrect thumbnails will be automatically regenerated 2017/08/26 - 1.2.1: - paperwork-shell: improve help string of 'paperwork-shell chkdeps' - Fix label deletion / renaming - Windows: Fix FS.safe() when used for PDF import - Windows: Fix FS.unsafe() (used for PDF export) 2017/07/11 - 1.2.0: - API: remove methods doc.drop_cache() and page.drop_cache() - API: docsearch: add method close() - paperwork-shell: Use JSON format for the output (except for 'paperwork-shell dump') - Use GIO functions instead of Python functions (open(), read(), close(), etc) - Use URIs instead of Unix file paths (file:///...) - Index is now managed in a separate process (avoid Python GIL locking + UI freezes) - Import: Make it possible to import image folder - Importers: provide a list of supported file formats (mime types) - Import: To figure out a file type, look at the file extensions but also the mime type in case the extension is not set - Import: Make the importers able to handle multiple Files/URIs instead of just one - paperwork-shell import: Run OCR on imported pages that have no words - paperwork-shell: add command 'ocr' - Configuration: [Global]:workdirectory is now an URI encoded in base64 (base64 encoding was required due to limitations of Python's ConfigParser) - DocSearch: When unable to open the index, destroy it and rebuild it from scratch - Add a new document type: ExternalPdfDoc: Used to display PDF that are outside of the work directory (for instance application help manual) - Configuration setting [OCR]:lang is now managed by the backend instead of the frontend 2017/02/09 - 1.1.2: - PDF: When PdfDoc.drop_cache() is called, make sure *all* the references to the Poppler objects are dropped, including those to the pages of the document 2017/02/05 - 1.1.1: - No change. Version created only to match Paperwork-gui version. 2017/01/30 - 1.1.0: - Add methods doc.has_ocr() and page.has_ocr() indicating if OCR has already been run on a given doc/page or not yet. Used in GUI for the option "Redo OCR on all documents" as it must act only on documents where OCR has already been done in the past (ie not PDF with text included) - Optim: Provides a method page.get_image() returning an already resized Pillow image (PDF rendering optimisation) - Export: Report progression - Optim: PDF thumbnail rendering: Keep a cached version of the first page only. The other pages can be rendered on the fly - Fix: Label directory name use base64 encoding, and this encoding can result in strings containing '/'. Those characters must be replaced (by '_') - Fix: util/find_language(): If the system locale is not set properly, pycountry may raise UnicodeDecodeError. - paperwork-shell: Add commands 'search', 'dump', 'switch_workdir', 'rescan', 'show', 'import', 'delete_doc', 'guess_labels', 'add_label', 'remove_label', 'rename' - Import: When importing a single PDF, don't import it if it was already previously imported - Import: Provides detailed information and statistics regarding what has been imported (return value of Importer.import_doc() has changed) 1.0.6: - No change. Version created only to match Paperwork-gui version. 1.0.5: - Doc deletion: Drop cache and file descripts *before* deleting document (optional on GNU/Linux, but required on Windows) 1.0.4: - Windows: Fix image import 1.0.3: - Windows: Fix import/export 1.0.2: - No change. Version created only to match Paperwork-gui version. 1.0.1: - util/find_language(): fix pycountry db lookup - Windows: hide ~/.config instead of ~/.config/paperwork.conf paperwork-2.1.1/paperwork-backend/LICENSE000066400000000000000000001045051417573700700201510ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {one line to give the program's name and a brief idea of what it does.} Copyright (C) {year} {name of author} This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: {project} Copyright (C) {year} {fullname} This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . paperwork-2.1.1/paperwork-backend/MANIFEST.in000066400000000000000000000001441417573700700206740ustar00rootroot00000000000000recursive-include src *.py *.mo *.json recursive-include tests * include *.markdown include LICENSE paperwork-2.1.1/paperwork-backend/Makefile000066400000000000000000000046461417573700700206110ustar00rootroot00000000000000VERSION_FILE = src/paperwork_backend/_version.py PYTHON ?= python3 build: build_c build_py install: install_py install_c uninstall: uninstall_py build_py: ${VERSION_FILE} l10n_compile ${PYTHON} ./setup.py build build_c: version: ${VERSION_FILE} ${VERSION_FILE}: echo -n "version = \"" >| $@ echo -n $(shell git describe --always) >> $@ echo "\"" >> $@ doc: install_py $(MAKE) -C doc html doc/_build/html/index.html: doc upload_doc: doc/_build/html/index.html cd .. && ./ci/deliver_doc.sh ${CURDIR}/doc/_build/html paperwork_backend data: $(MAKE) -C $(CURDIR)/src/paperwork_backend/authors data check: flake8 src/paperwork_backend test: install python3 -m unittest discover --verbose -s tests linux_exe: windows_exe: ${PYTHON} /mingw64/bin/pip3-script.py install . # ugly, but "import pkg_resources" doesn't work in frozen environments # and I don't want to have to patch the build machine to fix it every # time. mkdir -p $(CURDIR)/../build/exe/data (cd $(CURDIR)/src && find . -name '*.mo' -exec cp --parents \{\} $(CURDIR)/../build/exe/data \; ) release: ifeq (${RELEASE}, ) @echo "You must specify a release version (make release RELEASE=1.2.3)" exit 1 else @echo "Will release: ${RELEASE}" @echo "Checking release is in ChangeLog ..." grep ${RELEASE} ChangeLog | grep -v "/xx" endif release_pypi: @echo "Releasing paperwork-backend ..." ${PYTHON} ./setup.py sdist twine upload dist/paperwork-backend-${RELEASE}.tar.gz @echo "All done" clean: rm -f ${VERSION_FILE} rm -rf build dist *.egg-info $(MAKE) -C $(CURDIR)/src/paperwork_backend/authors clean # PIP_ARGS is used by Flatpak build install_py: ${VERSION_FILE} l10n_compile ${PYTHON} ./setup.py install ${PIP_ARGS} install_c: uninstall_py: pip3 uninstall -y paperwork-backend uninstall_c: l10n_extract: $(CURDIR)/../tools/l10n_extract.sh "$(CURDIR)/src" "$(CURDIR)/l10n" l10n_compile: $(CURDIR)/../tools/l10n_compile.sh \ "$(CURDIR)/l10n" \ "$(CURDIR)/src/paperwork_backend/l10n" \ "paperwork_backend" help: @echo "make build || make build_py" @echo "make check" @echo "make help: display this message" @echo "make install || make install_py" @echo "make uninstall || make uninstall_py" @echo "make release" .PHONY: \ build \ build_c \ build_py \ check \ data \ doc \ exe \ help \ install \ install_c \ install_py \ l10n_compile \ l10n_extract \ release \ test \ upload_data \ upload_doc \ uninstall \ uninstall_c \ version paperwork-2.1.1/paperwork-backend/README.markdown000066400000000000000000000035011417573700700216370ustar00rootroot00000000000000## Description [Paperwork](https://gitlab.gnome.org/World/OpenPaperwork/paperwork#readme) is a GUI to make papers searchable. This is the backend part of Paperwork. It manages: - The work directory / Access to the documents - Indexing - Searching - Suggestions - Import - Export There is no GUI here. The GUI is located here: https://gitlab.gnome.org/World/OpenPaperwork/paperwork/tree/master/paperwork-gtk . Regarding the name "Paperwork", it can refer to both the GUI or the backend. If you want to be specific, you can call the gui "paperwork-gtk" instead of just Paperwork. ## Dependencies * [Pillow](https://pypi.python.org/pypi/Pillow/): Image manipulation (with JPEG support) * [Whoosh](https://pypi.python.org/pypi/Whoosh/): To index and search documents, and provide keyword suggestions * Libpoppler (PDF support) * Cairo * Gobject Introspection ## Usage You can find some examples in scripts/. You can also look at the code of [Paperwork](https://gitlab.gnome.org/World/OpenPaperwork/paperwork#readme) for reference. Here are some snippets: ```py # TODO(Jflesch): UPDATE ``` ## Contact/Help Developement is strongly related to Paperwork-gui. * [Mailing-list](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/wikis/Contact) * [Extra documentation / FAQ / Tips / Wiki](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/wikis/) * [Bug trackers](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/wikis/Contact) ## Contact * [Mailing-list](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/wikis/Contact) * [Bug tracker](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/wikis/Contact) ## Licence GPLv3 or later. See LICENSE. ## Development Developement is strongly related to Paperwork-gui. All the information can be found on [the wiki](https://gitlab.gnome.org/World/OpenPaperwork/paperwork/wikis). paperwork-2.1.1/paperwork-backend/doc/000077500000000000000000000000001417573700700177045ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/doc/Makefile000066400000000000000000000011041417573700700213400ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)paperwork-2.1.1/paperwork-backend/doc/conf.py000066400000000000000000000130271417573700700212060ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- project = 'Paperwork-Backend' copyright = '2019, Jerome Flesch' author = 'Jerome Flesch' # The short X.Y version version = '' # The full version, including alpha/beta/rc tags release = '' # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinxcontrib.plantuml', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = 'Paperwork-Backenddoc' # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'Paperwork-Backend.tex', 'Paperwork-Backend Documentation', 'Jerome Flesch', 'manual'), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'paperwork-backend', 'Paperwork-Backend Documentation', [author], 1) ] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'Paperwork-Backend', 'Paperwork-Backend Documentation', author, 'Paperwork-Backend', 'One line description of project.', 'Miscellaneous'), ] # -- Options for Epub output ------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # -- Extension configuration ------------------------------------------------- # -- Options for todo extension ---------------------------------------------- # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True autodoc_inherit_docstrings = False paperwork-2.1.1/paperwork-backend/doc/index.rst000066400000000000000000000003571417573700700215520ustar00rootroot00000000000000Welcome to Paperwork-Backend's documentation! ============================================= .. toctree:: :maxdepth: 2 :caption: Contents: Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` paperwork-2.1.1/paperwork-backend/l10n/000077500000000000000000000000001417573700700177115ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/l10n/de.po000066400000000000000000000464031417573700700206500ustar00rootroot00000000000000# German translations for PACKAGE package. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2021-10-24 07:40+0000\n" "Last-Translator: Andreas Forster \n" "Language-Team: German \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.4\n" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:38 msgid "centrally aligned" msgstr "zentriert" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:39 msgid "Feeder" msgstr "Einzug" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:40 msgid "Flatbed" msgstr "Flachbett" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:41 msgid "left aligned" msgstr "linksbündig" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:42 msgid "right aligned" msgstr "rechtsbündig" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:9 msgid "English" msgstr "Englisch" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:10 msgid "French" msgstr "Französisch" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:11 msgid "German" msgstr "Deutsch" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:80 msgid "Color equalization" msgstr "Farbausgleich" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:92 msgid "Cropping" msgstr "Schneiden" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:103 msgid "Clockwise Rotation" msgstr "Drehen im Uhrzeigersinn" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:112 msgid "Counterclockwise Rotation" msgstr "Drehen gegen Uhrzeigersinn" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:231 msgid "Label guesser: Garbage-collecting unused document features ..." msgstr "Label guesser: Garbage-collecting ungenutzer Dokumenten-Features ..." #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:254 msgid "Label guesser: Garbage-collecting unused words ..." msgstr "Label guesser: Garbage-collecting ungenutzter Wörter ..." #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:571 #, python-format msgid "Label guesser: added document %s" msgstr "Label guesser: Document %s hinzugefügt" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:581 #, python-format msgid "Label guesser: updated document %s" msgstr "Label guesser: Document %s aktualisiert" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:596 #, python-format msgid "Label guesser: deleted document %s" msgstr "Label guesser: Document %s gelöscht" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:681 msgid "Commiting changes for label guessing ..." msgstr "Speichere Änderungen für label guessing ..." #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:873 #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:929 msgid "Label guessing: Training ..." msgstr "Label guessing: Training ..." #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:43 #, python-brace-format msgid "" "Document {doc_id} p{page_idx} has already some text. Not guessing page " "orientation." msgstr "" "Dokument {doc_id} S{page_idx} hat bereits Text. Überspringe Ausrichtungs-" "Bestimmung." #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:53 #, python-brace-format msgid "Guessing orientation of document {doc_id} p{page_idx}" msgstr "Schätze Textrichtung von Dokument {doc_id} Seite {page_idx}" #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:77 msgid "Guessing page orientation" msgstr "Schätze Textrichtung" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:48 #, python-brace-format msgid "Document {doc_id} p{page_idx} already correctly colorized" msgstr "Dokument {doc_id} S{page_idx} wurde bereits farbkorrigiert" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:64 #, python-brace-format msgid "Adjusting colors of document {doc_id} p{page_idx}" msgstr "Passe die Farben auf Dokument {doc_id} Seite {page_idx} an" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:88 msgid "Adjusting colors of document" msgstr "Passe die Farben des Dokuments an" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:48 #, python-brace-format msgid "Document {doc_id} p{page_idx} has already some text. No OCR run" msgstr "" "Dokument {doc_id}p{page_idx} hat bereits erkannten Text. Überspringe OCR" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:57 #, python-brace-format msgid "Running OCR on document {doc_id} p{page_idx}" msgstr "Führe OCR auf Dokument {doc_id} Seite {page_idx} aus" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:83 msgid "Running OCR" msgstr "Führe OCR aus" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:45 #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:51 #, python-brace-format msgid "Document {doc_id} p{page_idx} already cropped" msgstr "Dokument {doc_id} S{page_idx} wurde bereits zugeschnitten" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:58 #, python-brace-format msgid "Document {doc_id} p{page_idx} has already some text. Not cropping." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:68 #, python-brace-format msgid "Using calibration to crop page borders of document {doc_id} p{page_idx}" msgstr "" "Nutze Kalibrierung um den Rand des Dokuments {doc_id} Seite {page_idx} zu " "schneiden" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:60 #, python-brace-format msgid "Guessing page borders of document {doc_id} p{page_idx}" msgstr "Schätze Seitenrand von Dokument {doc_id} Seite {page_idx}" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:85 msgid "Guessing page borders" msgstr "Schätze Seitenränder" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:233 msgid "Starting scan ..." msgstr "Starte Scan..." #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:241 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:293 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:304 #, python-format msgid "Scanning page %d ..." msgstr "Scanne Seite %d ..." #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:457 #, python-format msgid "Examining %s" msgstr "Prüfe %s" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:538 msgid "Getting scanner list ..." msgstr "Erhalte Scannerliste..." #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:752 msgid "Scanner info." msgstr "Scanner Info." #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:753 msgid "Select to generate" msgstr "Wähle zum Erzeugen" #: paperwork-backend/src/paperwork_backend/doctracker.py:68 #, python-format msgid "Document %s added" msgstr "Dokument %s hinzugefügt" #: paperwork-backend/src/paperwork_backend/doctracker.py:73 #, python-format msgid "Document %s updated" msgstr "Dokument %s aktualisiert" #: paperwork-backend/src/paperwork_backend/doctracker.py:89 #, python-format msgid "Document %s deleted" msgstr "Dokument %s gelöscht" #: paperwork-backend/src/paperwork_backend/doctracker.py:99 #: paperwork-backend/src/paperwork_backend/index/whoosh.py:140 #, python-format msgid "Examining document %s: unchanged" msgstr "Prüfe Dokument %s: unverändert" #: paperwork-backend/src/paperwork_backend/doctracker.py:104 msgid "Rolling back changes" msgstr "Mache Änderungen rückgängig" #: paperwork-backend/src/paperwork_backend/doctracker.py:110 msgid "Committing changes" msgstr "Speichere Änderungen" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:55 #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:44 #, python-format msgid "Checking doc %s" msgstr "Prüfe %s" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:87 #, python-format msgid "" "Document %s has label \"%s\" with color=%s while document %s has label \"%s" "\" with color=%s" msgstr "" "Dokument %s hat das Label \"%s\" (%s) während Dokument %s Label \"%s\" (%s) " "hat" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:106 #, python-format msgid "Set label color %s on label \"%s\" of document %s" msgstr "Setze Labelfarbe %s auf Label \"%s\" des Dokuments %s" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:130 #, python-format msgid "Fixing label on doc %s" msgstr "Korrigiere Label auf Dokument %s" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:59 #, python-format msgid "Document %s is empty" msgstr "Dokument %s ist leer" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:60 #, python-format msgid "Delete document %s" msgstr "Lösche Dokument %s" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:74 #, python-format msgid "Deleting empty doc %s" msgstr "Lösche das leere Dokument %s" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:113 #, python-format msgid "Indexing new document %s" msgstr "Indiziere das neue Dokument %s" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:122 #, python-format msgid "Removing document %s from index" msgstr "Entferne Dokument %s vom Index" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:132 #, python-format msgid "Indexing updated document %s" msgstr "Indiziere das aktualisierte Dokument %s" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:163 msgid "Committing changes in the index ..." msgstr "Speichere Änderungen im Index ..." #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:33 #: paperwork-backend/src/paperwork_backend/docimport/converted.py:33 msgid "Already imported" msgstr "Bereits importiert" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:44 msgid "PDF" msgstr "PDF" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:45 #: paperwork-backend/src/paperwork_backend/docimport/img.py:61 #: paperwork-backend/src/paperwork_backend/docimport/converted.py:41 msgid "Documents" msgstr "Dokumente" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:61 msgid "Import PDF" msgstr "Importiere PDF" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:65 msgid "Import PDFs recursively" msgstr "Importiere PDFs rekursiv" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:121 msgid "PDF folder" msgstr "PDF Ordner" #: paperwork-backend/src/paperwork_backend/docimport/img.py:58 msgid "Images" msgstr "Bilder" #: paperwork-backend/src/paperwork_backend/docimport/img.py:64 msgid "Pages" msgstr "Seiten" #: paperwork-backend/src/paperwork_backend/docimport/img.py:83 msgid "Append the image to the current document" msgstr "Hänge das Bild an das aktuelle Dokument an" #: paperwork-backend/src/paperwork_backend/docimport/img.py:88 msgid "Find the images recursively and import them to the current document" msgstr "Suche Bilder rekursiv und importiere sie in das aktuelle Dokument" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:67 msgid "Import office document" msgstr "Importiere Office-Datei" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:71 msgid "Import office documents recursively" msgstr "Importiere Office-Dateien rekursiv" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:139 msgid "Office document folder" msgstr "Office-Dateien Verzeichnis" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:60 msgid "Microsoft Word template (.dot)" msgstr "Microsoft Word Vorlage (.dot)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:68 msgid "Microsoft Excel template (.xlt)" msgstr "Microsoft Excel Vorlage (.xlt)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:76 msgid "Microsoft PowerPoint template (.ppt)" msgstr "Microsoft PowerPoint Vorlage (.ppt)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:80 msgid "OpenOffice/LibreOffice Chart (.odc)" msgstr "OpenOffice/LibreOffice Bild (.odc)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:84 msgid "OpenOffice/LibreOffice Database (.odb)" msgstr "OpenOffice/LibreOffice Datenbank (.odb)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:88 msgid "OpenOffice/LibreOffice Formula (.odf)" msgstr "OpenOffice/LibreOffice Formel (.odf)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:92 msgid "OpenOffice/LibreOffice Graphics (.odg)" msgstr "OpenOffice/LibreOffice Zeichnung (.odg)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:96 msgid "OpenOffice/LibreOffice Graphics template (.otg)" msgstr "OpenOffice/LibreOffice Zeichnung Vorlage (.otg)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:100 msgid "OpenOffice/LibreOffice Image template (.odi)" msgstr "OpenOffice/LibreOffice Bild Vorlage (.odi)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:104 msgid "OpenOffice/LibreOffice Presentation (.odp)" msgstr "OpenOffice/LibreOffice Präsentation (.odp)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:108 msgid "OpenOffice/LibreOffice Presentation template (.otp)" msgstr "OpenOffice/LibreOffice Präsentation Vorlage (.otp)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:112 msgid "OpenOffice/LibreOffice Spreadsheet (.ods)" msgstr "OpenOffice/LibreOffice Tabellendokument (.ods)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:116 msgid "OpenOffice/LibreOffice Spreadsheet template (.ots)" msgstr "OpenOffice/LibreOffice Tabellendokument Vorlage (.ots)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:120 msgid "OpenOffice/LibreOffice Text (.odt)" msgstr "OpenOffice/LibreOffice Text (.odt)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:124 msgid "OpenOffice/LibreOffice Text master (.odm)" msgstr "OpenOffice/LibreOffice Globaldokument (.odm)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:128 msgid "OpenOffice/LibreOffice Text template (.ott)" msgstr "OpenOffice/LibreOffice Text Vorlage (.ott)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:132 msgid "OpenOffice/LibreOffice Text web (.oth)" msgstr "OpenOffice/LibreOffice Text web (.oth)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:144 msgid "Microsoft PowerPoint slide (.sldx)" msgstr "Microsoft PowerPoint Folie (.sldx)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:150 msgid "Microsoft PowerPoint slideshow (.ppsx)" msgstr "Microsoft PowerPoint Präsentation (.ppsx)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:156 msgid "Microsoft PowerPoint presentation template (.potx)" msgstr "Microsoft PowerPoint Präsentation Vorlage (.potx)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:168 msgid "Microsoft Excel template (.xltx)" msgstr "Microsoft Excel Vorlage (.xltx)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:180 msgid "Microsoft Word template (.dotx)" msgstr "Microsoft Word Vorlage (.dotx)" #: paperwork-backend/src/paperwork_backend/authors/translators.py:23 msgid "[]" msgstr "[\"Andreas Forster\", \"Anna Lazic\", \"Tobias\"]" #: paperwork-backend/src/paperwork_backend/model/labels.py:72 msgid "Loading labels of document {}" msgstr "Lade Dokumentenlabels {}" #: paperwork-backend/src/paperwork_backend/model/converted.py:111 #, python-format msgid "Converting document %s to PDF ..." msgstr "Konvertiere Dokument %s nach PDF ..." #: paperwork-backend/src/paperwork_backend/model/converted.py:186 msgid "Checking converted documents" msgstr "Prüfe konvertierte Dokumente" #: paperwork-backend/src/paperwork_backend/docexport/pdf.py:100 msgid "Original PDF(s)" msgstr "Originale(s) PDF(s)" #: paperwork-backend/src/paperwork_backend/docexport/pdf.py:293 msgid "Generated PDF(s)" msgstr "Generierte PDF(s)" #: paperwork-backend/src/paperwork_backend/docexport/generic.py:98 msgid "Page by page processing" msgstr "Seitenweise Verarbeitung" #: paperwork-backend/src/paperwork_backend/docexport/img.py:41 #, python-brace-format msgid "Exporting {doc_id} p{page_idx} ..." msgstr "Exportiere {doc_id} p{page_idx} ..." #: paperwork-backend/src/paperwork_backend/docexport/img.py:86 msgid "Exporting ..." msgstr "Exportiere ..." #: paperwork-backend/src/paperwork_backend/docexport/img.py:121 msgid "Split page(s) into image(s) and text(s)" msgstr "Teile Seite(n) in Bild(er) und Text" #: paperwork-backend/src/paperwork_backend/docexport/img.py:171 msgid "Image file ({})" msgstr "Bilddatei ({})" #: paperwork-backend/src/paperwork_backend/docexport/img.py:184 msgid "Black & White" msgstr "Schwarz-Weiss" #: paperwork-backend/src/paperwork_backend/docexport/img.py:195 msgid "Grayscale" msgstr "Graustufen" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:31 msgid "Soft simplification" msgstr "Leichte Vereinfachung" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:50 msgid "Hard simplification" msgstr "Starke Vereinfachung" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:52 msgid "Extreme simplification" msgstr "Extreme Vereinfachung" #~ msgid "App. & system info." #~ msgstr "App. & System info." #~ msgid "Collecting statistics ..." #~ msgstr "Sammle Statistiken ..." #, python-brace-format #~ msgid "" #~ "Using calibration to crop page borders of document {doc_id} page " #~ "{page_idx}" #~ msgstr "" #~ "Nutze Kalibrierung um den Rand des Dokuments {doc_id} Seite {page_idx} zu " #~ "schneiden" #, python-brace-format #~ msgid "Guessing page borders of document {doc_id} page {page_idx}" #~ msgstr "Schätze Seitenrand von Dokument {doc_id} Seite {page_idx}" #, python-brace-format #~ msgid "Guessing orientation on document {doc_id} page {page_idx}" #~ msgstr "Schätze Textrichtung von Dokument {doc_id} Seite {page_idx}" #, python-brace-format #~ msgid "Running OCR on document {doc_id} page {page_idx}" #~ msgstr "Führe OCR auf Dokument {doc_id} Seite {page_idx} aus" #, python-format #~ msgid "Training label guesser with added document %s" #~ msgstr "Trainiere Label-Vorschlag mit hinzugefügtem Dokument %s" #, python-format #~ msgid "Untraining label guesser due to deleted document %s" #~ msgstr "Bereinige Label-Vorschlag wegen gelöschtem Dokument %s" #, python-format #~ msgid "Training label guesser with updated document %s" #~ msgstr "Trainiere Label-Vorschlag mit aktualisiertem Dokument %s" #~ msgid "Training label guesser for label '{}' with all known documents ..." #~ msgstr "" #~ "Trainiere Label-Vorschlag für das Label '{}' mit allen bekannten " #~ "Dokumenten ..." #~ msgid "Training label guessing ..." #~ msgstr "Trainiere Label-Vorschlag ..." #, python-brace-format #~ msgid "Adjusting colors of document {doc_id} page {page_idx}" #~ msgstr "Passe die Farben auf Dokument {doc_id} Seite {page_idx} an" paperwork-2.1.1/paperwork-backend/l10n/es.po000066400000000000000000000343441417573700700206700ustar00rootroot00000000000000# Spanish translations for PACKAGE package. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2020-05-03 15:37+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=ASCII\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:38 msgid "centrally aligned" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:39 msgid "Feeder" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:40 msgid "Flatbed" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:41 msgid "left aligned" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:42 msgid "right aligned" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:9 msgid "English" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:10 msgid "French" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:11 msgid "German" msgstr "" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:80 msgid "Color equalization" msgstr "" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:92 msgid "Cropping" msgstr "" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:103 msgid "Clockwise Rotation" msgstr "" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:112 msgid "Counterclockwise Rotation" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:231 msgid "Label guesser: Garbage-collecting unused document features ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:254 msgid "Label guesser: Garbage-collecting unused words ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:571 #, python-format msgid "Label guesser: added document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:581 #, python-format msgid "Label guesser: updated document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:596 #, python-format msgid "Label guesser: deleted document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:681 msgid "Commiting changes for label guessing ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:873 #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:929 msgid "Label guessing: Training ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:43 #, python-brace-format msgid "" "Document {doc_id} p{page_idx} has already some text. Not guessing page " "orientation." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:53 #, python-brace-format msgid "Guessing orientation of document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:77 msgid "Guessing page orientation" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:48 #, python-brace-format msgid "Document {doc_id} p{page_idx} already correctly colorized" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:64 #, python-brace-format msgid "Adjusting colors of document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:88 msgid "Adjusting colors of document" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:48 #, python-brace-format msgid "Document {doc_id} p{page_idx} has already some text. No OCR run" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:57 #, python-brace-format msgid "Running OCR on document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:83 msgid "Running OCR" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:45 #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:51 #, python-brace-format msgid "Document {doc_id} p{page_idx} already cropped" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:58 #, python-brace-format msgid "Document {doc_id} p{page_idx} has already some text. Not cropping." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:68 #, python-brace-format msgid "Using calibration to crop page borders of document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:60 #, python-brace-format msgid "Guessing page borders of document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:85 msgid "Guessing page borders" msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:233 msgid "Starting scan ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:241 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:293 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:304 #, python-format msgid "Scanning page %d ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:457 #, python-format msgid "Examining %s" msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:538 msgid "Getting scanner list ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:752 msgid "Scanner info." msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:753 msgid "Select to generate" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:68 #, python-format msgid "Document %s added" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:73 #, python-format msgid "Document %s updated" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:89 #, python-format msgid "Document %s deleted" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:99 #: paperwork-backend/src/paperwork_backend/index/whoosh.py:140 #, python-format msgid "Examining document %s: unchanged" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:104 msgid "Rolling back changes" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:110 msgid "Committing changes" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:55 #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:44 #, python-format msgid "Checking doc %s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:87 #, python-format msgid "" "Document %s has label \"%s\" with color=%s while document %s has label \"%s" "\" with color=%s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:106 #, python-format msgid "Set label color %s on label \"%s\" of document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:130 #, python-format msgid "Fixing label on doc %s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:59 #, python-format msgid "Document %s is empty" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:60 #, python-format msgid "Delete document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:74 #, python-format msgid "Deleting empty doc %s" msgstr "" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:113 #, python-format msgid "Indexing new document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:122 #, python-format msgid "Removing document %s from index" msgstr "" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:132 #, python-format msgid "Indexing updated document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:163 msgid "Committing changes in the index ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:33 #: paperwork-backend/src/paperwork_backend/docimport/converted.py:33 msgid "Already imported" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:44 msgid "PDF" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:45 #: paperwork-backend/src/paperwork_backend/docimport/img.py:61 #: paperwork-backend/src/paperwork_backend/docimport/converted.py:41 msgid "Documents" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:61 msgid "Import PDF" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:65 msgid "Import PDFs recursively" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:121 msgid "PDF folder" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/img.py:58 msgid "Images" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/img.py:64 msgid "Pages" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/img.py:83 msgid "Append the image to the current document" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/img.py:88 msgid "Find the images recursively and import them to the current document" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:67 msgid "Import office document" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:71 msgid "Import office documents recursively" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:139 msgid "Office document folder" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:60 msgid "Microsoft Word template (.dot)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:68 msgid "Microsoft Excel template (.xlt)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:76 msgid "Microsoft PowerPoint template (.ppt)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:80 msgid "OpenOffice/LibreOffice Chart (.odc)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:84 msgid "OpenOffice/LibreOffice Database (.odb)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:88 msgid "OpenOffice/LibreOffice Formula (.odf)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:92 msgid "OpenOffice/LibreOffice Graphics (.odg)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:96 msgid "OpenOffice/LibreOffice Graphics template (.otg)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:100 msgid "OpenOffice/LibreOffice Image template (.odi)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:104 msgid "OpenOffice/LibreOffice Presentation (.odp)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:108 msgid "OpenOffice/LibreOffice Presentation template (.otp)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:112 msgid "OpenOffice/LibreOffice Spreadsheet (.ods)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:116 msgid "OpenOffice/LibreOffice Spreadsheet template (.ots)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:120 msgid "OpenOffice/LibreOffice Text (.odt)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:124 msgid "OpenOffice/LibreOffice Text master (.odm)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:128 msgid "OpenOffice/LibreOffice Text template (.ott)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:132 msgid "OpenOffice/LibreOffice Text web (.oth)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:144 msgid "Microsoft PowerPoint slide (.sldx)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:150 msgid "Microsoft PowerPoint slideshow (.ppsx)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:156 msgid "Microsoft PowerPoint presentation template (.potx)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:168 msgid "Microsoft Excel template (.xltx)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:180 msgid "Microsoft Word template (.dotx)" msgstr "" #: paperwork-backend/src/paperwork_backend/authors/translators.py:23 msgid "[]" msgstr "" #: paperwork-backend/src/paperwork_backend/model/labels.py:72 msgid "Loading labels of document {}" msgstr "" #: paperwork-backend/src/paperwork_backend/model/converted.py:111 #, python-format msgid "Converting document %s to PDF ..." msgstr "" #: paperwork-backend/src/paperwork_backend/model/converted.py:186 msgid "Checking converted documents" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pdf.py:100 msgid "Original PDF(s)" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pdf.py:293 msgid "Generated PDF(s)" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/generic.py:98 msgid "Page by page processing" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:41 #, python-brace-format msgid "Exporting {doc_id} p{page_idx} ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:86 msgid "Exporting ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:121 msgid "Split page(s) into image(s) and text(s)" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:171 msgid "Image file ({})" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:184 msgid "Black & White" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:195 msgid "Grayscale" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:31 msgid "Soft simplification" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:50 msgid "Hard simplification" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:52 msgid "Extreme simplification" msgstr "" paperwork-2.1.1/paperwork-backend/l10n/fr.po000066400000000000000000000477441417573700700207000ustar00rootroot00000000000000# French translations for PACKAGE package # Traductions françaises du paquet PACKAGE. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2021-11-29 21:07+0000\n" "Last-Translator: Jerome Flesch \n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 4.9\n" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:38 msgid "centrally aligned" msgstr "centré" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:39 msgid "Feeder" msgstr "Bac d'alimentation" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:40 msgid "Flatbed" msgstr "Plateau" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:41 msgid "left aligned" msgstr "aligné à gauche" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:42 msgid "right aligned" msgstr "aligné à droite" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:9 msgid "English" msgstr "Anglais" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:10 msgid "French" msgstr "Français" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:11 msgid "German" msgstr "Allemand" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:80 msgid "Color equalization" msgstr "Égalisation des couleurs" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:92 msgid "Cropping" msgstr "Recadrage" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:103 msgid "Clockwise Rotation" msgstr "Rotation dans le sens horaire" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:112 msgid "Counterclockwise Rotation" msgstr "Rotation dans le sens anti-horaire" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:231 msgid "Label guesser: Garbage-collecting unused document features ..." msgstr "" "Devineur d'étiquettes : Suppression des caractéristiques de documents qui ne " "sont plus utilisées ..." #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:254 msgid "Label guesser: Garbage-collecting unused words ..." msgstr "Devineur d'étiquettes : Suppression des mots non-utilisés ..." #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:571 #, python-format msgid "Label guesser: added document %s" msgstr "Devineur d'étiquettes : document %s ajouté" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:581 #, python-format msgid "Label guesser: updated document %s" msgstr "Devineur d'étiquettes : document %s mis à jour" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:596 #, python-format msgid "Label guesser: deleted document %s" msgstr "Devineur d'étiquettes : document %s effacé" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:681 msgid "Commiting changes for label guessing ..." msgstr "Enregistrement des changements pour le devinage d'étiquettes ..." #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:873 #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:929 msgid "Label guessing: Training ..." msgstr "Devineur d'étiquettes : entraînement ..." #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:43 #, python-brace-format msgid "" "Document {doc_id} p{page_idx} has already some text. Not guessing page " "orientation." msgstr "" "La p{page_idx} du document {doc_id} a déjà du texte. Pas de tentative de " "deviner l'orientation de la page." #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:53 #, python-brace-format msgid "Guessing orientation of document {doc_id} p{page_idx}" msgstr "" "Entrain de deviner l'orientation de la p{page_idx} du document {doc_id}" #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:77 msgid "Guessing page orientation" msgstr "Détection de l'orientation de la page" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:48 #, python-brace-format msgid "Document {doc_id} p{page_idx} already correctly colorized" msgstr "La p{page_idx} du document {doc_id} est déjà correctement colorisée" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:64 #, python-brace-format msgid "Adjusting colors of document {doc_id} p{page_idx}" msgstr "Ajustement des couleurs de la p{page_idx} du document {doc_id}" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:88 msgid "Adjusting colors of document" msgstr "Ajustement des couleurs du document" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:48 #, python-brace-format msgid "Document {doc_id} p{page_idx} has already some text. No OCR run" msgstr "" "Le document {doc_id} p{page_idx} a déjà du texte associé. La ROC ne sera pas " "effectuée" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:57 #, python-brace-format msgid "Running OCR on document {doc_id} p{page_idx}" msgstr "Exécution de la ROC sur le document {doc_id} p{page_idx}" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:83 msgid "Running OCR" msgstr "Exécution de la ROC" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:45 #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:51 #, python-brace-format msgid "Document {doc_id} p{page_idx} already cropped" msgstr "Document {doc_id} p{page_idx} déjà découpée" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:58 #, python-brace-format msgid "Document {doc_id} p{page_idx} has already some text. Not cropping." msgstr "" "La page p{page_idx} du document {doc_id} a déjà du texte. Ne sera pas " "découpée." #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:68 #, python-brace-format msgid "Using calibration to crop page borders of document {doc_id} p{page_idx}" msgstr "" "Utilisation de la calibration pour découper les bords de la page {doc_id} " "p{page_idx}" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:60 #, python-brace-format msgid "Guessing page borders of document {doc_id} p{page_idx}" msgstr "Détection des bords de la page de {doc_id} p{page_idx}" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:85 msgid "Guessing page borders" msgstr "Détection des bords de la page" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:233 msgid "Starting scan ..." msgstr "Démarrage du scan…" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:241 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:293 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:304 #, python-format msgid "Scanning page %d ..." msgstr "Scan de la page %d…" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:457 #, python-format msgid "Examining %s" msgstr "Examen de %s" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:538 msgid "Getting scanner list ..." msgstr "Récupération de la liste des scanners…" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:752 msgid "Scanner info." msgstr "Info. du scanner" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:753 msgid "Select to generate" msgstr "Sélectionnez pour générer" #: paperwork-backend/src/paperwork_backend/doctracker.py:68 #, python-format msgid "Document %s added" msgstr "Document %s ajouté" #: paperwork-backend/src/paperwork_backend/doctracker.py:73 #, python-format msgid "Document %s updated" msgstr "Document %s mis à jour" #: paperwork-backend/src/paperwork_backend/doctracker.py:89 #, python-format msgid "Document %s deleted" msgstr "Document %s supprimé" #: paperwork-backend/src/paperwork_backend/doctracker.py:99 #: paperwork-backend/src/paperwork_backend/index/whoosh.py:140 #, python-format msgid "Examining document %s: unchanged" msgstr "Examen du document %s : inchangé" #: paperwork-backend/src/paperwork_backend/doctracker.py:104 msgid "Rolling back changes" msgstr "Annulation des modifications" #: paperwork-backend/src/paperwork_backend/doctracker.py:110 msgid "Committing changes" msgstr "Enregistrement des modifications" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:55 #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:44 #, python-format msgid "Checking doc %s" msgstr "Vérification du document %s" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:87 #, python-format msgid "" "Document %s has label \"%s\" with color=%s while document %s has label \"%s" "\" with color=%s" msgstr "" "Document %s a l'étiquette \"%s\" avec la couleur %s tandis que le document " "%s a l'étiquette \"%s\" avec la couleur %s" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:106 #, python-format msgid "Set label color %s on label \"%s\" of document %s" msgstr "Mise de la couleur %s sur l'étiquette \"%s\" du document %s" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:130 #, python-format msgid "Fixing label on doc %s" msgstr "Correction de l'étiquette sur le document %s" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:59 #, python-format msgid "Document %s is empty" msgstr "Le document %s est vide" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:60 #, python-format msgid "Delete document %s" msgstr "Effacer le document %s" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:74 #, python-format msgid "Deleting empty doc %s" msgstr "Effacement du document vide %s" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:113 #, python-format msgid "Indexing new document %s" msgstr "Indexation du nouveau document %s" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:122 #, python-format msgid "Removing document %s from index" msgstr "Suppression du document %s de l'index" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:132 #, python-format msgid "Indexing updated document %s" msgstr "Indexation de la mise à jour du document %s" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:163 msgid "Committing changes in the index ..." msgstr "Enregistrement des modifications dans l'index…" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:33 #: paperwork-backend/src/paperwork_backend/docimport/converted.py:33 msgid "Already imported" msgstr "Déjà importé" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:44 msgid "PDF" msgstr "PDF" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:45 #: paperwork-backend/src/paperwork_backend/docimport/img.py:61 #: paperwork-backend/src/paperwork_backend/docimport/converted.py:41 msgid "Documents" msgstr "Documents" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:61 msgid "Import PDF" msgstr "Importer un PDF" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:65 msgid "Import PDFs recursively" msgstr "Importer les PDFs récursivement" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:121 msgid "PDF folder" msgstr "Répertoire de PDFs" #: paperwork-backend/src/paperwork_backend/docimport/img.py:58 msgid "Images" msgstr "Images" #: paperwork-backend/src/paperwork_backend/docimport/img.py:64 msgid "Pages" msgstr "Pages" #: paperwork-backend/src/paperwork_backend/docimport/img.py:83 msgid "Append the image to the current document" msgstr "Ajouter l'image au document actuel" #: paperwork-backend/src/paperwork_backend/docimport/img.py:88 msgid "Find the images recursively and import them to the current document" msgstr "" "Trouver les images récursivement et les importer dans le document actif" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:67 msgid "Import office document" msgstr "Importer les documents Office/LibreOffice" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:71 msgid "Import office documents recursively" msgstr "Import les documents Office/Libreoffice récursivement" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:139 msgid "Office document folder" msgstr "Dossier de documents Office/LibreOffice" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:60 msgid "Microsoft Word template (.dot)" msgstr "Modèle de document Microsoft Word (.dot)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:68 msgid "Microsoft Excel template (.xlt)" msgstr "Modèle de document Microsoft Excel (.xlt)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:76 msgid "Microsoft PowerPoint template (.ppt)" msgstr "Modèle de document Microsoft PowerPoint (.ppt)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:80 msgid "OpenOffice/LibreOffice Chart (.odc)" msgstr "Graphique LibreOffice (.odc)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:84 msgid "OpenOffice/LibreOffice Database (.odb)" msgstr "Base de données LibreOffice (.odb)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:88 msgid "OpenOffice/LibreOffice Formula (.odf)" msgstr "Formule LibreOffice (.odf)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:92 msgid "OpenOffice/LibreOffice Graphics (.odg)" msgstr "Graphique LibreOffice (.odg)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:96 msgid "OpenOffice/LibreOffice Graphics template (.otg)" msgstr "Modèle de document graphique LibreOffice (.otg)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:100 msgid "OpenOffice/LibreOffice Image template (.odi)" msgstr "Modèle d'image LibreOffice (.odi)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:104 msgid "OpenOffice/LibreOffice Presentation (.odp)" msgstr "Présentation LibreOffice (.odp)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:108 msgid "OpenOffice/LibreOffice Presentation template (.otp)" msgstr "Modèle de présentation LibreOffice (.otp)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:112 msgid "OpenOffice/LibreOffice Spreadsheet (.ods)" msgstr "Tableau LibreOffice (.ods)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:116 msgid "OpenOffice/LibreOffice Spreadsheet template (.ots)" msgstr "Modèle de tableau LibreOffice (.ots)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:120 msgid "OpenOffice/LibreOffice Text (.odt)" msgstr "Texte LibreOffice (.odt)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:124 msgid "OpenOffice/LibreOffice Text master (.odm)" msgstr "Document texte maître LibreOffice (.odm)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:128 msgid "OpenOffice/LibreOffice Text template (.ott)" msgstr "Modèle de texte LibreOffice (.ott)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:132 msgid "OpenOffice/LibreOffice Text web (.oth)" msgstr "Texte web LibreOffice (.oth)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:144 msgid "Microsoft PowerPoint slide (.sldx)" msgstr "Diapositive Microsoft PowerPoint (.sldx)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:150 msgid "Microsoft PowerPoint slideshow (.ppsx)" msgstr "Présentation Microsoft PowerPoint (.ppsx)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:156 msgid "Microsoft PowerPoint presentation template (.potx)" msgstr "Modèle de présentation Microsoft PowerPoint (.potx)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:168 msgid "Microsoft Excel template (.xltx)" msgstr "Modèle de tableau Microsoft Excel (.xltx)" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:180 msgid "Microsoft Word template (.dotx)" msgstr "Modèle de document Microsoft Word (.dotx)" #: paperwork-backend/src/paperwork_backend/authors/translators.py:23 msgid "[]" msgstr "[\"Flesch Jérôme\"]" #: paperwork-backend/src/paperwork_backend/model/labels.py:72 msgid "Loading labels of document {}" msgstr "Chargement des étiquettes du document {}" #: paperwork-backend/src/paperwork_backend/model/converted.py:111 #, python-format msgid "Converting document %s to PDF ..." msgstr "Conversion du document %s en PDF …" #: paperwork-backend/src/paperwork_backend/model/converted.py:186 msgid "Checking converted documents" msgstr "Vérification des documents convertis" #: paperwork-backend/src/paperwork_backend/docexport/pdf.py:100 msgid "Original PDF(s)" msgstr "PDF(s) d'origine" #: paperwork-backend/src/paperwork_backend/docexport/pdf.py:293 msgid "Generated PDF(s)" msgstr "PDF(s) généré(s)" #: paperwork-backend/src/paperwork_backend/docexport/generic.py:98 msgid "Page by page processing" msgstr "Traitement page par page" #: paperwork-backend/src/paperwork_backend/docexport/img.py:41 #, python-brace-format msgid "Exporting {doc_id} p{page_idx} ..." msgstr "Export de {doc_id} p{page_idx}…" #: paperwork-backend/src/paperwork_backend/docexport/img.py:86 msgid "Exporting ..." msgstr "Export en cours…" #: paperwork-backend/src/paperwork_backend/docexport/img.py:121 msgid "Split page(s) into image(s) and text(s)" msgstr "Sépare le(s) page(s) en image(s) et texte(s)" #: paperwork-backend/src/paperwork_backend/docexport/img.py:171 msgid "Image file ({})" msgstr "Fichier image ({})" #: paperwork-backend/src/paperwork_backend/docexport/img.py:184 msgid "Black & White" msgstr "Noir et blanc" #: paperwork-backend/src/paperwork_backend/docexport/img.py:195 msgid "Grayscale" msgstr "Niveaux de gris" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:31 msgid "Soft simplification" msgstr "Simplification douce" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:50 msgid "Hard simplification" msgstr "Simplification forte" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:52 msgid "Extreme simplification" msgstr "Simplification extrême" #~ msgid "App. & system info." #~ msgstr "Info. app. & système" #~ msgid "Collecting statistics ..." #~ msgstr "Récupération des statistiques…" #, python-brace-format #~ msgid "" #~ "Using calibration to crop page borders of document {doc_id} page " #~ "{page_idx}" #~ msgstr "" #~ "Utilisation de la calibration pour recadrer la page {page_idx} du " #~ "document {doc_id}" #, python-brace-format #~ msgid "Guessing page borders of document {doc_id} page {page_idx}" #~ msgstr "Estimation des bords de la page {page_idx} du document {doc_id}" #, python-brace-format #~ msgid "Guessing orientation on document {doc_id} page {page_idx}" #~ msgstr "" #~ "Détection de l'orientation de la page {page_idx} du document {doc_id}" #, python-brace-format #~ msgid "Running OCR on document {doc_id} page {page_idx}" #~ msgstr "ROC en cours sur la page {page_idx} du document {doc_id}" #, python-format #~ msgid "Training label guesser with added document %s" #~ msgstr "" #~ "Entraînement de l'estimateur d'étiquettes avec le document %s ajouté" #, python-format #~ msgid "Untraining label guesser due to deleted document %s" #~ msgstr "" #~ "Dé-entraînement de l'estimateur d'étiquettes suite à la suppression du " #~ "document %s" #, python-format #~ msgid "Training label guesser with updated document %s" #~ msgstr "" #~ "Entraînement de l'estimateur d'étiquettes avec la mise à jour du document " #~ "%s" #~ msgid "Training label guesser for label '{}' with all known documents ..." #~ msgstr "" #~ "Entraînement de l'estimateur pour l'étiquette '{}' avec tous les " #~ "documents connus…" #~ msgid "Training label guessing ..." #~ msgstr "Entraînement de l'estimateur d'étiquettes…" #, python-brace-format #~ msgid "Adjusting colors of document {doc_id} page {page_idx}" #~ msgstr "Ajustement des couleurs de la page {page_idx} du document {doc_id}" paperwork-2.1.1/paperwork-backend/l10n/messages.pot000066400000000000000000000343031417573700700222470ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:38 msgid "centrally aligned" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:39 msgid "Feeder" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:40 msgid "Flatbed" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:41 msgid "left aligned" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:42 msgid "right aligned" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:9 msgid "English" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:10 msgid "French" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:11 msgid "German" msgstr "" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:80 msgid "Color equalization" msgstr "" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:92 msgid "Cropping" msgstr "" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:103 msgid "Clockwise Rotation" msgstr "" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:112 msgid "Counterclockwise Rotation" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:231 msgid "Label guesser: Garbage-collecting unused document features ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:254 msgid "Label guesser: Garbage-collecting unused words ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:571 #, python-format msgid "Label guesser: added document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:581 #, python-format msgid "Label guesser: updated document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:596 #, python-format msgid "Label guesser: deleted document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:681 msgid "Commiting changes for label guessing ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:873 #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:929 msgid "Label guessing: Training ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:43 #, python-brace-format msgid "" "Document {doc_id} p{page_idx} has already some text. Not guessing page " "orientation." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:53 #, python-brace-format msgid "Guessing orientation of document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:77 msgid "Guessing page orientation" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:48 #, python-brace-format msgid "Document {doc_id} p{page_idx} already correctly colorized" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:64 #, python-brace-format msgid "Adjusting colors of document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:88 msgid "Adjusting colors of document" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:48 #, python-brace-format msgid "Document {doc_id} p{page_idx} has already some text. No OCR run" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:57 #, python-brace-format msgid "Running OCR on document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:83 msgid "Running OCR" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:45 #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:51 #, python-brace-format msgid "Document {doc_id} p{page_idx} already cropped" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:58 #, python-brace-format msgid "Document {doc_id} p{page_idx} has already some text. Not cropping." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:68 #, python-brace-format msgid "Using calibration to crop page borders of document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:60 #, python-brace-format msgid "Guessing page borders of document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:85 msgid "Guessing page borders" msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:233 msgid "Starting scan ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:241 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:293 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:304 #, python-format msgid "Scanning page %d ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:457 #, python-format msgid "Examining %s" msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:538 msgid "Getting scanner list ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:752 msgid "Scanner info." msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:753 msgid "Select to generate" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:68 #, python-format msgid "Document %s added" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:73 #, python-format msgid "Document %s updated" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:89 #, python-format msgid "Document %s deleted" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:99 #: paperwork-backend/src/paperwork_backend/index/whoosh.py:140 #, python-format msgid "Examining document %s: unchanged" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:104 msgid "Rolling back changes" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:110 msgid "Committing changes" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:55 #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:44 #, python-format msgid "Checking doc %s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:87 #, python-format msgid "" "Document %s has label \"%s\" with color=%s while document %s has label \"%s" "\" with color=%s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:106 #, python-format msgid "Set label color %s on label \"%s\" of document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:130 #, python-format msgid "Fixing label on doc %s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:59 #, python-format msgid "Document %s is empty" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:60 #, python-format msgid "Delete document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:74 #, python-format msgid "Deleting empty doc %s" msgstr "" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:113 #, python-format msgid "Indexing new document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:122 #, python-format msgid "Removing document %s from index" msgstr "" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:132 #, python-format msgid "Indexing updated document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:163 msgid "Committing changes in the index ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:33 #: paperwork-backend/src/paperwork_backend/docimport/converted.py:33 msgid "Already imported" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:44 msgid "PDF" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:45 #: paperwork-backend/src/paperwork_backend/docimport/img.py:61 #: paperwork-backend/src/paperwork_backend/docimport/converted.py:41 msgid "Documents" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:61 msgid "Import PDF" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:65 msgid "Import PDFs recursively" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:121 msgid "PDF folder" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/img.py:58 msgid "Images" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/img.py:64 msgid "Pages" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/img.py:83 msgid "Append the image to the current document" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/img.py:88 msgid "Find the images recursively and import them to the current document" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:67 msgid "Import office document" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:71 msgid "Import office documents recursively" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:139 msgid "Office document folder" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:60 msgid "Microsoft Word template (.dot)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:68 msgid "Microsoft Excel template (.xlt)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:76 msgid "Microsoft PowerPoint template (.ppt)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:80 msgid "OpenOffice/LibreOffice Chart (.odc)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:84 msgid "OpenOffice/LibreOffice Database (.odb)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:88 msgid "OpenOffice/LibreOffice Formula (.odf)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:92 msgid "OpenOffice/LibreOffice Graphics (.odg)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:96 msgid "OpenOffice/LibreOffice Graphics template (.otg)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:100 msgid "OpenOffice/LibreOffice Image template (.odi)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:104 msgid "OpenOffice/LibreOffice Presentation (.odp)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:108 msgid "OpenOffice/LibreOffice Presentation template (.otp)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:112 msgid "OpenOffice/LibreOffice Spreadsheet (.ods)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:116 msgid "OpenOffice/LibreOffice Spreadsheet template (.ots)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:120 msgid "OpenOffice/LibreOffice Text (.odt)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:124 msgid "OpenOffice/LibreOffice Text master (.odm)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:128 msgid "OpenOffice/LibreOffice Text template (.ott)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:132 msgid "OpenOffice/LibreOffice Text web (.oth)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:144 msgid "Microsoft PowerPoint slide (.sldx)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:150 msgid "Microsoft PowerPoint slideshow (.ppsx)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:156 msgid "Microsoft PowerPoint presentation template (.potx)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:168 msgid "Microsoft Excel template (.xltx)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:180 msgid "Microsoft Word template (.dotx)" msgstr "" #: paperwork-backend/src/paperwork_backend/authors/translators.py:23 msgid "[]" msgstr "" #: paperwork-backend/src/paperwork_backend/model/labels.py:72 msgid "Loading labels of document {}" msgstr "" #: paperwork-backend/src/paperwork_backend/model/converted.py:111 #, python-format msgid "Converting document %s to PDF ..." msgstr "" #: paperwork-backend/src/paperwork_backend/model/converted.py:186 msgid "Checking converted documents" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pdf.py:100 msgid "Original PDF(s)" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pdf.py:293 msgid "Generated PDF(s)" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/generic.py:98 msgid "Page by page processing" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:41 #, python-brace-format msgid "Exporting {doc_id} p{page_idx} ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:86 msgid "Exporting ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:121 msgid "Split page(s) into image(s) and text(s)" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:171 msgid "Image file ({})" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:184 msgid "Black & White" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:195 msgid "Grayscale" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:31 msgid "Soft simplification" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:50 msgid "Hard simplification" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:52 msgid "Extreme simplification" msgstr "" paperwork-2.1.1/paperwork-backend/l10n/oc.po000066400000000000000000000425721417573700700206640ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2020-09-02 17:30+0000\n" "Last-Translator: Quentin PAGÈS \n" "Language-Team: Occitan \n" "Language: oc\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 4.1.1\n" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:38 msgid "centrally aligned" msgstr "centrat" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:39 msgid "Feeder" msgstr "Cargador de documents" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:40 msgid "Flatbed" msgstr "Platèl" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:41 msgid "left aligned" msgstr "alinhat a esquèrra" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:42 msgid "right aligned" msgstr "alinhat a drecha" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:9 msgid "English" msgstr "Anglés" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:10 msgid "French" msgstr "Francés" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:11 msgid "German" msgstr "Alemand" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:80 msgid "Color equalization" msgstr "Egalisacion de las colors" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:92 msgid "Cropping" msgstr "Retalhatge" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:103 msgid "Clockwise Rotation" msgstr "Rotacion orària" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:112 msgid "Counterclockwise Rotation" msgstr "Rotacion anti-orària" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:231 msgid "Label guesser: Garbage-collecting unused document features ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:254 msgid "Label guesser: Garbage-collecting unused words ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:571 #, python-format msgid "Label guesser: added document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:581 #, python-format msgid "Label guesser: updated document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:596 #, python-format msgid "Label guesser: deleted document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:681 msgid "Commiting changes for label guessing ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:873 #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:929 msgid "Label guessing: Training ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:43 #, python-brace-format msgid "" "Document {doc_id} p{page_idx} has already some text. Not guessing page " "orientation." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:53 #, python-brace-format msgid "Guessing orientation of document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:77 msgid "Guessing page orientation" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:48 #, python-brace-format msgid "Document {doc_id} p{page_idx} already correctly colorized" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:64 #, python-brace-format msgid "Adjusting colors of document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:88 msgid "Adjusting colors of document" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:48 #, python-brace-format msgid "Document {doc_id} p{page_idx} has already some text. No OCR run" msgstr "" "Lo document {doc_id} p{page_idx} a ja un tèxt associat. La ROC serà pas " "realizada" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:57 #, python-brace-format msgid "Running OCR on document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:83 msgid "Running OCR" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:45 #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:51 #, python-brace-format msgid "Document {doc_id} p{page_idx} already cropped" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:58 #, python-brace-format msgid "Document {doc_id} p{page_idx} has already some text. Not cropping." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:68 #, python-brace-format msgid "Using calibration to crop page borders of document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:60 #, python-brace-format msgid "Guessing page borders of document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:85 msgid "Guessing page borders" msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:233 msgid "Starting scan ..." msgstr "Aviada del numerizador…" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:241 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:293 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:304 #, python-format msgid "Scanning page %d ..." msgstr "Num. de la pagina %d…" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:457 #, python-format msgid "Examining %s" msgstr "Analisi de %s" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:538 msgid "Getting scanner list ..." msgstr "Recuperacion de la lista dels numerizadors…" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:752 msgid "Scanner info." msgstr "Informacion del numerizador" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:753 msgid "Select to generate" msgstr "Seleccionar per generar" #: paperwork-backend/src/paperwork_backend/doctracker.py:68 #, python-format msgid "Document %s added" msgstr "Document %s ajustat" #: paperwork-backend/src/paperwork_backend/doctracker.py:73 #, python-format msgid "Document %s updated" msgstr "Document %s mes a jorn" #: paperwork-backend/src/paperwork_backend/doctracker.py:89 #, python-format msgid "Document %s deleted" msgstr "Document %s suprimit" #: paperwork-backend/src/paperwork_backend/doctracker.py:99 #: paperwork-backend/src/paperwork_backend/index/whoosh.py:140 #, python-format msgid "Examining document %s: unchanged" msgstr "Analisi del document %s : pas cambiat" #: paperwork-backend/src/paperwork_backend/doctracker.py:104 msgid "Rolling back changes" msgstr "Anullacion de las modificacions" #: paperwork-backend/src/paperwork_backend/doctracker.py:110 msgid "Committing changes" msgstr "Enregistrament de las modificacions" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:55 #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:44 #, python-format msgid "Checking doc %s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:87 #, python-format msgid "" "Document %s has label \"%s\" with color=%s while document %s has label \"%s" "\" with color=%s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:106 #, python-format msgid "Set label color %s on label \"%s\" of document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:130 #, python-format msgid "Fixing label on doc %s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:59 #, python-format msgid "Document %s is empty" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:60 #, python-format msgid "Delete document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:74 #, python-format msgid "Deleting empty doc %s" msgstr "" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:113 #, python-format msgid "Indexing new document %s" msgstr "Indexacion del document novèl %s" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:122 #, python-format msgid "Removing document %s from index" msgstr "Supression del document %s de l’ensenhador" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:132 #, python-format msgid "Indexing updated document %s" msgstr "Indexacion de la mesa a jorn del document %s" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:163 msgid "Committing changes in the index ..." msgstr "Enregistrament de las modificacion dins l’ensenhador…" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:33 #: paperwork-backend/src/paperwork_backend/docimport/converted.py:33 msgid "Already imported" msgstr "Ja importat" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:44 msgid "PDF" msgstr "PDF" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:45 #: paperwork-backend/src/paperwork_backend/docimport/img.py:61 #: paperwork-backend/src/paperwork_backend/docimport/converted.py:41 msgid "Documents" msgstr "Documents" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:61 msgid "Import PDF" msgstr "Importar un PDF" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:65 msgid "Import PDFs recursively" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:121 msgid "PDF folder" msgstr "Repertòri dels PDF" #: paperwork-backend/src/paperwork_backend/docimport/img.py:58 msgid "Images" msgstr "Imatges" #: paperwork-backend/src/paperwork_backend/docimport/img.py:64 msgid "Pages" msgstr "Paginas" #: paperwork-backend/src/paperwork_backend/docimport/img.py:83 msgid "Append the image to the current document" msgstr "Ajustar l’imatge al document actual" #: paperwork-backend/src/paperwork_backend/docimport/img.py:88 msgid "Find the images recursively and import them to the current document" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:67 msgid "Import office document" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:71 msgid "Import office documents recursively" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:139 msgid "Office document folder" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:60 msgid "Microsoft Word template (.dot)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:68 msgid "Microsoft Excel template (.xlt)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:76 msgid "Microsoft PowerPoint template (.ppt)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:80 msgid "OpenOffice/LibreOffice Chart (.odc)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:84 msgid "OpenOffice/LibreOffice Database (.odb)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:88 msgid "OpenOffice/LibreOffice Formula (.odf)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:92 msgid "OpenOffice/LibreOffice Graphics (.odg)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:96 msgid "OpenOffice/LibreOffice Graphics template (.otg)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:100 msgid "OpenOffice/LibreOffice Image template (.odi)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:104 msgid "OpenOffice/LibreOffice Presentation (.odp)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:108 msgid "OpenOffice/LibreOffice Presentation template (.otp)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:112 msgid "OpenOffice/LibreOffice Spreadsheet (.ods)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:116 msgid "OpenOffice/LibreOffice Spreadsheet template (.ots)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:120 msgid "OpenOffice/LibreOffice Text (.odt)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:124 msgid "OpenOffice/LibreOffice Text master (.odm)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:128 msgid "OpenOffice/LibreOffice Text template (.ott)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:132 msgid "OpenOffice/LibreOffice Text web (.oth)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:144 msgid "Microsoft PowerPoint slide (.sldx)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:150 msgid "Microsoft PowerPoint slideshow (.ppsx)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:156 msgid "Microsoft PowerPoint presentation template (.potx)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:168 msgid "Microsoft Excel template (.xltx)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:180 msgid "Microsoft Word template (.dotx)" msgstr "" #: paperwork-backend/src/paperwork_backend/authors/translators.py:23 msgid "[]" msgstr "[\"Quentin PAGÈS\"]" #: paperwork-backend/src/paperwork_backend/model/labels.py:72 msgid "Loading labels of document {}" msgstr "Cargament de las etiquetas del document {}" #: paperwork-backend/src/paperwork_backend/model/converted.py:111 #, python-format msgid "Converting document %s to PDF ..." msgstr "" #: paperwork-backend/src/paperwork_backend/model/converted.py:186 msgid "Checking converted documents" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pdf.py:100 msgid "Original PDF(s)" msgstr "PDF original" #: paperwork-backend/src/paperwork_backend/docexport/pdf.py:293 msgid "Generated PDF(s)" msgstr "PDF generat(s)" #: paperwork-backend/src/paperwork_backend/docexport/generic.py:98 msgid "Page by page processing" msgstr "Tractament pagina per pagina" #: paperwork-backend/src/paperwork_backend/docexport/img.py:41 #, python-brace-format msgid "Exporting {doc_id} p{page_idx} ..." msgstr "Export. de {doc_id} p{page_idx}…" #: paperwork-backend/src/paperwork_backend/docexport/img.py:86 msgid "Exporting ..." msgstr "Export. en cors…" #: paperwork-backend/src/paperwork_backend/docexport/img.py:121 msgid "Split page(s) into image(s) and text(s)" msgstr "Separar la(s) pagina(s) en imatge(s) e tèxte(s)" #: paperwork-backend/src/paperwork_backend/docexport/img.py:171 msgid "Image file ({})" msgstr "Fichièr imatge ({})" #: paperwork-backend/src/paperwork_backend/docexport/img.py:184 msgid "Black & White" msgstr "Blanc e negre" #: paperwork-backend/src/paperwork_backend/docexport/img.py:195 msgid "Grayscale" msgstr "Nivèls de gris" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:31 msgid "Soft simplification" msgstr "Simplificacion doça" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:50 msgid "Hard simplification" msgstr "Simplificacion fòrta" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:52 msgid "Extreme simplification" msgstr "Simplificacion extrèma" #~ msgid "App. & system info." #~ msgstr "Info. ap. e sistèma" #~ msgid "Collecting statistics ..." #~ msgstr "Recuperacion de las estatisticas…" #, python-brace-format #~ msgid "" #~ "Using calibration to crop page borders of document {doc_id} page " #~ "{page_idx}" #~ msgstr "" #~ "Utilizacion del calibratge per retalhar la pagina {page_idx} del document " #~ "{doc_id}" #, python-brace-format #~ msgid "Guessing page borders of document {doc_id} page {page_idx}" #~ msgstr "" #~ "Localizacion dels bòrds de la pagina {page_idx} del document {doc_id}" #, python-brace-format #~ msgid "Guessing orientation on document {doc_id} page {page_idx}" #~ msgstr "" #~ "Deteccion de l’orientacion de la pagina {page_idx} del document {doc_id}" #, python-brace-format #~ msgid "Running OCR on document {doc_id} page {page_idx}" #~ msgstr "ROC en cors sus la pagina {page_idx} del document {doc_id}" #, python-format #~ msgid "Training label guesser with added document %s" #~ msgstr "Entrainament del devinaire d’etiquetas amb lo document %s ajustat" #, python-format #~ msgid "Untraining label guesser due to deleted document %s" #~ msgstr "" #~ "Mesa al deslembrièr de l’entrainament d’etiquetas en seguida de la " #~ "supression del document %s" #, python-format #~ msgid "Training label guesser with updated document %s" #~ msgstr "" #~ "Entrainament del devinaire d’etiquetas amb la mesa a jorn del document %s" #~ msgid "Training label guesser for label '{}' with all known documents ..." #~ msgstr "" #~ "Entrainament del devinaire d’etiquetas per l’etiqueta « {} » amb totes " #~ "los documents coneguts…" #~ msgid "Training label guessing ..." #~ msgstr "Entrainament del devinaire d’etiquetas…" #, python-brace-format #~ msgid "Adjusting colors of document {doc_id} page {page_idx}" #~ msgstr "" #~ "Ajustament de las color de la pagina {page_idx} del document {doc_id}" paperwork-2.1.1/paperwork-backend/l10n/sv.po000066400000000000000000000232671417573700700207130ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-09-01 22:01+0200\n" "PO-Revision-Date: 2021-01-04 15:31+0000\n" "Last-Translator: Åke Engelbrektson \n" "Language-Team: Swedish \n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.4\n" #: paperwork-backend/src/paperwork_backend/doctracker.py:68 #, python-format msgid "Document %s added" msgstr "Dokument %s tillagt" #: paperwork-backend/src/paperwork_backend/doctracker.py:73 #, python-format msgid "Document %s updated" msgstr "Dokument %s uppdaterat" #: paperwork-backend/src/paperwork_backend/doctracker.py:89 #, python-format msgid "Document %s deleted" msgstr "Dokument %s borttaget" #: paperwork-backend/src/paperwork_backend/doctracker.py:99 #: paperwork-backend/src/paperwork_backend/index/whoosh.py:140 #, python-format msgid "Examining document %s: unchanged" msgstr "Undersöker dokument %s: Oförändrat" #: paperwork-backend/src/paperwork_backend/doctracker.py:104 msgid "Rolling back changes" msgstr "Ångrar ändringar" #: paperwork-backend/src/paperwork_backend/doctracker.py:110 msgid "Committing changes" msgstr "Tillämpar ändringar" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:32 msgid "Already imported" msgstr "Redan importerat" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:40 msgid "PDF" msgstr "PDF" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:41 #: paperwork-backend/src/paperwork_backend/docimport/img.py:55 msgid "Documents" msgstr "Dokument" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:57 msgid "Import PDF" msgstr "Importera PDF" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:105 msgid "PDF folder" msgstr "PDF-mapp" #: paperwork-backend/src/paperwork_backend/docimport/img.py:52 msgid "Images" msgstr "Bilder" #: paperwork-backend/src/paperwork_backend/docimport/img.py:58 msgid "Pages" msgstr "Sidor" #: paperwork-backend/src/paperwork_backend/docimport/img.py:77 msgid "Append the image to the current document" msgstr "Bifoga bilden till det aktuella dokumentet" #: paperwork-backend/src/paperwork_backend/beacon/stats.py:126 msgid "App. & system info." msgstr "App. & systeminfo." #: paperwork-backend/src/paperwork_backend/beacon/stats.py:127 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:690 msgid "Select to generate" msgstr "Välj för att generera" #: paperwork-backend/src/paperwork_backend/beacon/stats.py:149 msgid "Collecting statistics ..." msgstr "Samlar in statistik..." #: paperwork-backend/src/paperwork_backend/model/labels.py:47 msgid "Loading labels of document {}" msgstr "Läser in etiketter för dokument {}" #: paperwork-backend/src/paperwork_backend/docexport/pdf.py:100 msgid "Original PDF(s)" msgstr "Original-PDF(:er)" #: paperwork-backend/src/paperwork_backend/docexport/pdf.py:293 msgid "Generated PDF(s)" msgstr "Genererade PDF(:er)" #: paperwork-backend/src/paperwork_backend/docexport/img.py:41 #, python-brace-format msgid "Exporting {doc_id} p{page_idx} ..." msgstr "Exporterar {doc_id} s.{page_idx}..." #: paperwork-backend/src/paperwork_backend/docexport/img.py:86 msgid "Exporting ..." msgstr "Exporterar..." #: paperwork-backend/src/paperwork_backend/docexport/img.py:121 msgid "Split page(s) into image(s) and text(s)" msgstr "Dela upp sida/sidor i bild(er) och text" #: paperwork-backend/src/paperwork_backend/docexport/img.py:171 msgid "Image file ({})" msgstr "Bildfil ({})" #: paperwork-backend/src/paperwork_backend/docexport/img.py:184 msgid "Black & White" msgstr "Svartvit" #: paperwork-backend/src/paperwork_backend/docexport/img.py:195 msgid "Grayscale" msgstr "Gråskala" #: paperwork-backend/src/paperwork_backend/docexport/generic.py:98 msgid "Page by page processing" msgstr "Bearbetning sida för sida" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:31 msgid "Soft simplification" msgstr "Mjuk förenkling" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:50 msgid "Hard simplification" msgstr "Hård förenkling" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:52 msgid "Extreme simplification" msgstr "Extrem förenkling" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:208 msgid "Starting scan ..." msgstr "Startar skanning..." #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:216 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:268 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:279 #, python-format msgid "Scanning page %d ..." msgstr "Skannar sida %d..." #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:423 #, python-format msgid "Examining %s" msgstr "Undersöker %s" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:494 msgid "Getting scanner list ..." msgstr "Hämtar skannerlista..." #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:689 msgid "Scanner info." msgstr "Skannerinfo." #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:9 msgid "English" msgstr "Engelska" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:10 msgid "French" msgstr "Franska" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:11 msgid "German" msgstr "Tyska" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:38 msgid "centrally aligned" msgstr "centrerat" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:39 msgid "Feeder" msgstr "Matare" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:40 msgid "Flatbed" msgstr "Flatbädd" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:41 msgid "left aligned" msgstr "vänsterjusterat" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:42 msgid "right aligned" msgstr "högerjusterat" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:80 msgid "Color equalization" msgstr "Färgutjämning" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:92 msgid "Cropping" msgstr "Beskär" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:103 msgid "Clockwise Rotation" msgstr "Medurs rotation" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:112 msgid "Counterclockwise Rotation" msgstr "Moturs rotation" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:53 #, python-brace-format msgid "" "Using calibration to crop page borders of document {doc_id} page {page_idx}" msgstr "" "Använder kalibrering för att beskära sidkantlinjer i dokument {doc_id} sida " "{page_idx}" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:57 #, python-brace-format msgid "Guessing page borders of document {doc_id} page {page_idx}" msgstr "Gissar sidkantlinjer för dokument {doc_id} sida {page_idx}" #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:39 #, python-brace-format msgid "Guessing orientation on document {doc_id} page {page_idx}" msgstr "Gissar orientering på dokument {doc_id} sida {page_idx}" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:50 #, python-brace-format msgid "Document {doc_id} p{page_idx} has already some text. No OCR run" msgstr "Dokument {doc_id} s.{page_idx} har redan text. Ingen OCR-körning" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:58 #, python-brace-format msgid "Running OCR on document {doc_id} page {page_idx}" msgstr "Kör OCR på dokument {doc_id} sida {page_idx}" #: paperwork-backend/src/paperwork_backend/guesswork/label/simplebayes.py:92 #, python-format msgid "Training label guesser with added document %s" msgstr "Tränar etikettgissaren med tillagt dokument %s" #: paperwork-backend/src/paperwork_backend/guesswork/label/simplebayes.py:100 #, python-format msgid "Untraining label guesser due to deleted document %s" msgstr "Rensar etikettgissaren med anledning av borttaget dokument %s" #: paperwork-backend/src/paperwork_backend/guesswork/label/simplebayes.py:107 #, python-format msgid "Training label guesser with updated document %s" msgstr "Tränar etikettgissaren med uppdaterat dokument %s" #: paperwork-backend/src/paperwork_backend/guesswork/label/simplebayes.py:240 msgid "Training label guesser for label '{}' with all known documents ..." msgstr "Tränar etikettgissaren för etikett \"{}\" med alla kända dokument..." #: paperwork-backend/src/paperwork_backend/guesswork/label/simplebayes.py:333 msgid "Training label guessing ..." msgstr "Tränar etikettgissaren..." #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:53 #, python-brace-format msgid "Adjusting colors of document {doc_id} page {page_idx}" msgstr "Justerar färger i dokument {doc_id} sida {page_idx}" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:113 #, python-format msgid "Indexing new document %s" msgstr "Indexerar nytt dokument %s" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:122 #, python-format msgid "Removing document %s from index" msgstr "Tar bort dokument %s från index" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:132 #, python-format msgid "Indexing updated document %s" msgstr "Indexerar uppdaterat dokument %s" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:163 msgid "Committing changes in the index ..." msgstr "Tillämpar ändringar i index..." #: paperwork-backend/src/paperwork_backend/authors/translators.py:23 msgid "[]" msgstr "[]" paperwork-2.1.1/paperwork-backend/l10n/uk.po000066400000000000000000000344631417573700700207020ustar00rootroot00000000000000# Ukrainian translations for PACKAGE package. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2020-05-03 15:37+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: uk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=ASCII\n" "Content-Transfer-Encoding: 8bit\n" "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);\n" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:38 msgid "centrally aligned" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:39 msgid "Feeder" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:40 msgid "Flatbed" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:41 msgid "left aligned" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:42 msgid "right aligned" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:9 msgid "English" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:10 msgid "French" msgstr "" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:11 msgid "German" msgstr "" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:80 msgid "Color equalization" msgstr "" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:92 msgid "Cropping" msgstr "" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:103 msgid "Clockwise Rotation" msgstr "" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:112 msgid "Counterclockwise Rotation" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:231 msgid "Label guesser: Garbage-collecting unused document features ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:254 msgid "Label guesser: Garbage-collecting unused words ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:571 #, python-format msgid "Label guesser: added document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:581 #, python-format msgid "Label guesser: updated document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:596 #, python-format msgid "Label guesser: deleted document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:681 msgid "Commiting changes for label guessing ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:873 #: paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py:929 msgid "Label guessing: Training ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:43 #, python-brace-format msgid "" "Document {doc_id} p{page_idx} has already some text. Not guessing page " "orientation." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:53 #, python-brace-format msgid "Guessing orientation of document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:77 msgid "Guessing page orientation" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:48 #, python-brace-format msgid "Document {doc_id} p{page_idx} already correctly colorized" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:64 #, python-brace-format msgid "Adjusting colors of document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:88 msgid "Adjusting colors of document" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:48 #, python-brace-format msgid "Document {doc_id} p{page_idx} has already some text. No OCR run" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:57 #, python-brace-format msgid "Running OCR on document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:83 msgid "Running OCR" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:45 #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:51 #, python-brace-format msgid "Document {doc_id} p{page_idx} already cropped" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:58 #, python-brace-format msgid "Document {doc_id} p{page_idx} has already some text. Not cropping." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:68 #, python-brace-format msgid "Using calibration to crop page borders of document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:60 #, python-brace-format msgid "Guessing page borders of document {doc_id} p{page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:85 msgid "Guessing page borders" msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:233 msgid "Starting scan ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:241 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:293 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:304 #, python-format msgid "Scanning page %d ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:457 #, python-format msgid "Examining %s" msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:538 msgid "Getting scanner list ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:752 msgid "Scanner info." msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:753 msgid "Select to generate" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:68 #, python-format msgid "Document %s added" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:73 #, python-format msgid "Document %s updated" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:89 #, python-format msgid "Document %s deleted" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:99 #: paperwork-backend/src/paperwork_backend/index/whoosh.py:140 #, python-format msgid "Examining document %s: unchanged" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:104 msgid "Rolling back changes" msgstr "" #: paperwork-backend/src/paperwork_backend/doctracker.py:110 msgid "Committing changes" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:55 #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:44 #, python-format msgid "Checking doc %s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:87 #, python-format msgid "" "Document %s has label \"%s\" with color=%s while document %s has label \"%s" "\" with color=%s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:106 #, python-format msgid "Set label color %s on label \"%s\" of document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py:130 #, python-format msgid "Fixing label on doc %s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:59 #, python-format msgid "Document %s is empty" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:60 #, python-format msgid "Delete document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py:74 #, python-format msgid "Deleting empty doc %s" msgstr "" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:113 #, python-format msgid "Indexing new document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:122 #, python-format msgid "Removing document %s from index" msgstr "" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:132 #, python-format msgid "Indexing updated document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:163 msgid "Committing changes in the index ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:33 #: paperwork-backend/src/paperwork_backend/docimport/converted.py:33 msgid "Already imported" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:44 msgid "PDF" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:45 #: paperwork-backend/src/paperwork_backend/docimport/img.py:61 #: paperwork-backend/src/paperwork_backend/docimport/converted.py:41 msgid "Documents" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:61 msgid "Import PDF" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:65 msgid "Import PDFs recursively" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:121 msgid "PDF folder" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/img.py:58 msgid "Images" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/img.py:64 msgid "Pages" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/img.py:83 msgid "Append the image to the current document" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/img.py:88 msgid "Find the images recursively and import them to the current document" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:67 msgid "Import office document" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:71 msgid "Import office documents recursively" msgstr "" #: paperwork-backend/src/paperwork_backend/docimport/converted.py:139 msgid "Office document folder" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:60 msgid "Microsoft Word template (.dot)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:68 msgid "Microsoft Excel template (.xlt)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:76 msgid "Microsoft PowerPoint template (.ppt)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:80 msgid "OpenOffice/LibreOffice Chart (.odc)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:84 msgid "OpenOffice/LibreOffice Database (.odb)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:88 msgid "OpenOffice/LibreOffice Formula (.odf)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:92 msgid "OpenOffice/LibreOffice Graphics (.odg)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:96 msgid "OpenOffice/LibreOffice Graphics template (.otg)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:100 msgid "OpenOffice/LibreOffice Image template (.odi)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:104 msgid "OpenOffice/LibreOffice Presentation (.odp)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:108 msgid "OpenOffice/LibreOffice Presentation template (.otp)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:112 msgid "OpenOffice/LibreOffice Spreadsheet (.ods)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:116 msgid "OpenOffice/LibreOffice Spreadsheet template (.ots)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:120 msgid "OpenOffice/LibreOffice Text (.odt)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:124 msgid "OpenOffice/LibreOffice Text master (.odm)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:128 msgid "OpenOffice/LibreOffice Text template (.ott)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:132 msgid "OpenOffice/LibreOffice Text web (.oth)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:144 msgid "Microsoft PowerPoint slide (.sldx)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:150 msgid "Microsoft PowerPoint slideshow (.ppsx)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:156 msgid "Microsoft PowerPoint presentation template (.potx)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:168 msgid "Microsoft Excel template (.xltx)" msgstr "" #: paperwork-backend/src/paperwork_backend/converter/libreoffice.py:180 msgid "Microsoft Word template (.dotx)" msgstr "" #: paperwork-backend/src/paperwork_backend/authors/translators.py:23 msgid "[]" msgstr "" #: paperwork-backend/src/paperwork_backend/model/labels.py:72 msgid "Loading labels of document {}" msgstr "" #: paperwork-backend/src/paperwork_backend/model/converted.py:111 #, python-format msgid "Converting document %s to PDF ..." msgstr "" #: paperwork-backend/src/paperwork_backend/model/converted.py:186 msgid "Checking converted documents" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pdf.py:100 msgid "Original PDF(s)" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pdf.py:293 msgid "Generated PDF(s)" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/generic.py:98 msgid "Page by page processing" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:41 #, python-brace-format msgid "Exporting {doc_id} p{page_idx} ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:86 msgid "Exporting ..." msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:121 msgid "Split page(s) into image(s) and text(s)" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:171 msgid "Image file ({})" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:184 msgid "Black & White" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/img.py:195 msgid "Grayscale" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:31 msgid "Soft simplification" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:50 msgid "Hard simplification" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:52 msgid "Extreme simplification" msgstr "" paperwork-2.1.1/paperwork-backend/l10n/zh_Hans.po000066400000000000000000000221321417573700700216430ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-09-01 22:01+0200\n" "PO-Revision-Date: 2021-02-06 09:20+0000\n" "Last-Translator: 玉堂白鹤 \n" "Language-Team: Chinese (Simplified) \n" "Language: zh_Hans\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Weblate 4.4\n" #: paperwork-backend/src/paperwork_backend/doctracker.py:68 #, python-format msgid "Document %s added" msgstr "文档 %s 已添加" #: paperwork-backend/src/paperwork_backend/doctracker.py:73 #, python-format msgid "Document %s updated" msgstr "文档 %s 已更新" #: paperwork-backend/src/paperwork_backend/doctracker.py:89 #, python-format msgid "Document %s deleted" msgstr "文档 %s 已删除" #: paperwork-backend/src/paperwork_backend/doctracker.py:99 #: paperwork-backend/src/paperwork_backend/index/whoosh.py:140 #, python-format msgid "Examining document %s: unchanged" msgstr "正在检查文档 %s:未更改" #: paperwork-backend/src/paperwork_backend/doctracker.py:104 msgid "Rolling back changes" msgstr "回滚更改" #: paperwork-backend/src/paperwork_backend/doctracker.py:110 msgid "Committing changes" msgstr "提交更改" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:32 msgid "Already imported" msgstr "已导入" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:40 msgid "PDF" msgstr "PDF" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:41 #: paperwork-backend/src/paperwork_backend/docimport/img.py:55 msgid "Documents" msgstr "文档" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:57 msgid "Import PDF" msgstr "导入 PDF" #: paperwork-backend/src/paperwork_backend/docimport/pdf.py:105 msgid "PDF folder" msgstr "PDF 文件夹" #: paperwork-backend/src/paperwork_backend/docimport/img.py:52 msgid "Images" msgstr "图像" #: paperwork-backend/src/paperwork_backend/docimport/img.py:58 msgid "Pages" msgstr "页面" #: paperwork-backend/src/paperwork_backend/docimport/img.py:77 msgid "Append the image to the current document" msgstr "将图像追加到当前文档" #: paperwork-backend/src/paperwork_backend/beacon/stats.py:126 msgid "App. & system info." msgstr "App. 和系统信息。" #: paperwork-backend/src/paperwork_backend/beacon/stats.py:127 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:690 msgid "Select to generate" msgstr "选择生成" #: paperwork-backend/src/paperwork_backend/beacon/stats.py:149 msgid "Collecting statistics ..." msgstr "收集统计数据 ..." #: paperwork-backend/src/paperwork_backend/model/labels.py:47 msgid "Loading labels of document {}" msgstr "加载文档标签 {}" #: paperwork-backend/src/paperwork_backend/docexport/pdf.py:100 msgid "Original PDF(s)" msgstr "原始 PDF" #: paperwork-backend/src/paperwork_backend/docexport/pdf.py:293 msgid "Generated PDF(s)" msgstr "生成的 PDF" #: paperwork-backend/src/paperwork_backend/docexport/img.py:41 #, python-brace-format msgid "Exporting {doc_id} p{page_idx} ..." msgstr "正在导出 {doc_id} 页{page_idx} ..." #: paperwork-backend/src/paperwork_backend/docexport/img.py:86 msgid "Exporting ..." msgstr "正在导出 ..." #: paperwork-backend/src/paperwork_backend/docexport/img.py:121 msgid "Split page(s) into image(s) and text(s)" msgstr "将页面拆分为图像和文本" #: paperwork-backend/src/paperwork_backend/docexport/img.py:171 msgid "Image file ({})" msgstr "图像文件 ({})" #: paperwork-backend/src/paperwork_backend/docexport/img.py:184 msgid "Black & White" msgstr "黑白" #: paperwork-backend/src/paperwork_backend/docexport/img.py:195 msgid "Grayscale" msgstr "灰度" #: paperwork-backend/src/paperwork_backend/docexport/generic.py:98 msgid "Page by page processing" msgstr "逐页处理" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:31 msgid "Soft simplification" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:50 msgid "Hard simplification" msgstr "" #: paperwork-backend/src/paperwork_backend/docexport/pillowfight.py:52 msgid "Extreme simplification" msgstr "" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:208 msgid "Starting scan ..." msgstr "开始扫描 ..." #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:216 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:268 #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:279 #, python-format msgid "Scanning page %d ..." msgstr "扫描页面 %d ..." #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:423 #, python-format msgid "Examining %s" msgstr "分析 %s" #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:494 msgid "Getting scanner list ..." msgstr "正在获取扫描仪列表 ..." #: paperwork-backend/src/paperwork_backend/docscan/libinsane.py:689 msgid "Scanner info." msgstr "扫描仪信息。" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:9 msgid "English" msgstr "英语" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:10 msgid "French" msgstr "法语" #: paperwork-backend/src/paperwork_backend/i18n/pycountry.py:11 msgid "German" msgstr "德语" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:38 msgid "centrally aligned" msgstr "中心对齐" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:39 msgid "Feeder" msgstr "馈纸式" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:40 msgid "Flatbed" msgstr "平台式" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:41 msgid "left aligned" msgstr "左对齐" #: paperwork-backend/src/paperwork_backend/i18n/scanner.py:42 msgid "right aligned" msgstr "右对齐" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:80 msgid "Color equalization" msgstr "色彩均衡" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:92 msgid "Cropping" msgstr "裁剪" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:103 msgid "Clockwise Rotation" msgstr "顺时针旋转" #: paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py:112 msgid "Counterclockwise Rotation" msgstr "逆时针旋转" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py:53 #, python-brace-format msgid "" "Using calibration to crop page borders of document {doc_id} page {page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py:57 #, python-brace-format msgid "Guessing page borders of document {doc_id} page {page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py:39 #, python-brace-format msgid "Guessing orientation on document {doc_id} page {page_idx}" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:50 #, python-brace-format msgid "Document {doc_id} p{page_idx} has already some text. No OCR run" msgstr "文档{doc_id}页面{page_idx}已经有一些文本。无 OCR 运行" #: paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py:58 #, python-brace-format msgid "Running OCR on document {doc_id} page {page_idx}" msgstr "正在文档 {doc_id} 页面 {page_idx} 上运行 OCR" #: paperwork-backend/src/paperwork_backend/guesswork/label/simplebayes.py:92 #, python-format msgid "Training label guesser with added document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/simplebayes.py:100 #, python-format msgid "Untraining label guesser due to deleted document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/simplebayes.py:107 #, python-format msgid "Training label guesser with updated document %s" msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/simplebayes.py:240 msgid "Training label guesser for label '{}' with all known documents ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/label/simplebayes.py:333 msgid "Training label guessing ..." msgstr "" #: paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py:53 #, python-brace-format msgid "Adjusting colors of document {doc_id} page {page_idx}" msgstr "调整文档 {doc_id} 页面 {page_idx} 的颜色" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:113 #, python-format msgid "Indexing new document %s" msgstr "索引新文档 %s" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:122 #, python-format msgid "Removing document %s from index" msgstr "从索引中移除文档 %s" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:132 #, python-format msgid "Indexing updated document %s" msgstr "文档 %s 索引更新中" #: paperwork-backend/src/paperwork_backend/index/whoosh.py:163 msgid "Committing changes in the index ..." msgstr "正在提交索引中的更改 ..." #: paperwork-backend/src/paperwork_backend/authors/translators.py:23 msgid "[]" msgstr "[\"玉堂白鹤\"]" paperwork-2.1.1/paperwork-backend/setup.py000077500000000000000000000061731417573700700206630ustar00rootroot00000000000000#!/usr/bin/env python3 import os import sys from setuptools import setup, find_packages if os.name == "nt": extra_deps = [] else: extra_deps = [ "python-Levenshtein", ] quiet = '--quiet' in sys.argv or '-q' in sys.argv try: with open("src/paperwork_backend/_version.py", "r") as file_descriptor: version = file_descriptor.read().strip() version = version.split(" ")[2][1:-1] if not quiet: print("Paperwork-backend version: {}".format(version)) if "-" in version: version = version.split("-")[0] except FileNotFoundError: print("ERROR: _version.py file is missing") print("ERROR: Please run 'make version' first") sys.exit(1) setup( name="paperwork-backend", version=version, description="Paperwork's backend", long_description="""Paperwork is a GUI to make papers searchable. This is the backend part of Paperwork. It manages: - The work directory / Access to the documents - Indexing - Searching - Suggestions - Import - Export There is no GUI here. The GUI is . """, keywords="documents", url=( "https://gitlab.gnome.org/World/OpenPaperwork/paperwork/tree/master/" "paperwork-backend" ), download_url=( "https://gitlab.gnome.org/World/OpenPaperwork/paperwork/-" "/archive/{}/paperwork-{}.tar.gz".format(version, version) ), classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: End Users/Desktop", ("License :: OSI Approved ::" " GNU General Public License v3 or later (GPLv3+)"), "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", "Topic :: Multimedia :: Graphics :: Capture :: Scanners", "Topic :: Multimedia :: Graphics :: Graphics Conversion", "Topic :: Scientific/Engineering :: Image Recognition", "Topic :: Text Processing :: Filters", "Topic :: Text Processing :: Indexing", ], license="GPLv3+", author="Jerome Flesch", author_email="jflesch@openpaper.work", packages=find_packages('src'), include_package_data=True, package_dir={'': 'src'}, zip_safe=(os.name != 'nt'), install_requires=[ "openpaperwork-core", "Pillow", "psutil", "pycountry", "pyocr", "pypillowfight>=0.3.0", "scikit-learn", "Whoosh", # paperwork-shell chkdeps take care of all the dependencies that can't # be handled here. Mainly, dependencies using gobject introspection # (libpoppler, etc) ] + extra_deps ) if quiet: sys.exit(0) print("============================================================") print("============================================================") print("|| IMPORTANT ||") print("|| Please run 'paperwork-cli chkdeps' ||") print("|| to find any missing dependency ||") print("============================================================") print("============================================================") paperwork-2.1.1/paperwork-backend/src/000077500000000000000000000000001417573700700177265ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/000077500000000000000000000000001417573700700234075ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/__init__.py000066400000000000000000000057151417573700700255300ustar00rootroot00000000000000import gettext import openpaperwork_core import openpaperwork_gtk def _(s): return gettext.dgettext('paperwork_backend', s) DEFAULT_CONFIG_PLUGINS = openpaperwork_core.MINIMUM_CONFIG_PLUGINS + [ 'paperwork_backend.app', ] DEFAULT_PLUGINS = ( openpaperwork_core.RECOMMENDED_PLUGINS + openpaperwork_gtk.CLI_PLUGINS + [ 'openpaperwork_core.beacon.stats', 'openpaperwork_core.beacon.sysinfo', 'openpaperwork_core.bug_report.censor', 'openpaperwork_core.censor', 'openpaperwork_core.external_apps.dbus', 'openpaperwork_core.external_apps.windows', 'openpaperwork_core.external_apps.xdg', 'openpaperwork_core.http', 'openpaperwork_core.perfcheck.log', 'openpaperwork_core.pillow.img', 'openpaperwork_core.pillow.util', 'paperwork_backend.authors', 'paperwork_backend.authors.translators', 'paperwork_backend.beacon.update', 'paperwork_backend.cairo.pillow', 'paperwork_backend.cairo.poppler', 'paperwork_backend.chkworkdir.empty_doc', 'paperwork_backend.chkworkdir.label_color', 'paperwork_backend.converter.libreoffice', 'paperwork_backend.datadirhandler', 'paperwork_backend.docexport.generic', 'paperwork_backend.docexport.img', 'paperwork_backend.docexport.pdf', 'paperwork_backend.docexport.pillowfight', 'paperwork_backend.docimport.converted', 'paperwork_backend.docimport.img', 'paperwork_backend.docimport.pdf', 'paperwork_backend.docscan.libinsane', 'paperwork_backend.docscan.scan2doc', 'paperwork_backend.doctracker', # ACE is disabled by default: it's slow, and actually makes some scans # harder to read. # 'paperwork_backend.guesswork.color.libpillowfight', 'paperwork_backend.guesswork.label.sklearn', 'paperwork_backend.guesswork.ocr.pyocr', 'paperwork_backend.guesswork.orientation.pyocr', 'paperwork_backend.i18n.pycountry', 'paperwork_backend.i18n.scanner', 'paperwork_backend.imgedit.color', 'paperwork_backend.imgedit.crop', 'paperwork_backend.imgedit.rotate', 'paperwork_backend.index.whoosh', 'paperwork_backend.l10n', 'paperwork_backend.model', 'paperwork_backend.model.converted', 'paperwork_backend.model.extra_text', 'paperwork_backend.model.hocr', 'paperwork_backend.model.img', 'paperwork_backend.model.img_overlay', 'paperwork_backend.model.labels', 'paperwork_backend.model.pdf', 'paperwork_backend.model.thumbnail', 'paperwork_backend.model.workdir', 'paperwork_backend.pageedit.pageeditor', 'paperwork_backend.pagetracker', 'paperwork_backend.pillow.pdf', 'paperwork_backend.poppler.file', 'paperwork_backend.poppler.memory', 'paperwork_backend.pyocr', 'paperwork_backend.sync', ] ) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/app.py000066400000000000000000000005121417573700700245370ustar00rootroot00000000000000import openpaperwork_core from . import _version class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['app'] def app_get_name(self): return "Paperwork" def app_get_fs_name(self): return "paperwork2" def app_get_version(self): return _version.version paperwork-2.1.1/paperwork-backend/src/paperwork_backend/authors/000077500000000000000000000000001417573700700250745ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/authors/Makefile000066400000000000000000000006261417573700700265400ustar00rootroot00000000000000AUTHORS.git.json: $(CURDIR)/../../../../tools/get_git_authors.py $(CURDIR)/../../../.. >| $(CURDIR)/AUTHORS.git.json AUTHORS.json: \ ../../../../AUTHORS.ui.json \ AUTHORS.git.json $(CURDIR)/../../../../tools/merge_authors_json.py $^ \ >| $(CURDIR)/AUTHORS.json data: $(MAKE) clean $(MAKE) AUTHORS.json clean: rm -f $(CURDIR)/AUTHORS.git.json rm -f $(CURDIR)/AUTHORS.json .PHONY: clean data paperwork-2.1.1/paperwork-backend/src/paperwork_backend/authors/__init__.py000066400000000000000000000024741417573700700272140ustar00rootroot00000000000000import json import logging import openpaperwork_core LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['authors'] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, { 'interface': 'resources', 'defaults': ['openpaperwork_core.resources.setuptools'], }, ] def authors_get(self, out: dict): file_path = self.core.call_success( "resources_get_file", "paperwork_backend.authors", "AUTHORS.json" ) if file_path is None: LOGGER.error("AUTHORS.json is missing !") return None try: with self.core.call_success("fs_open", file_path, 'r') as fd: content = fd.read() except FileNotFoundError as exc: LOGGER.error( "AUTHORS.json is missing ! (expected={})".format(file_path), exc_info=exc ) return None content = json.loads(content) for category in content: for (category_name, authors) in category.items(): out[category_name] = authors return True paperwork-2.1.1/paperwork-backend/src/paperwork_backend/authors/translators.py000066400000000000000000000014121417573700700300200ustar00rootroot00000000000000import json import logging import openpaperwork_core from .. import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['authors'] def authors_get(self, out: dict): try: translators = json.loads( # Translators: put your names here. See French translation # for reference. # Must valid JSON. Must be a list of strings (your names) _("[]") ) except json.JSONDecodeError as exc: LOGGER.error("Failed to load translator list", exc_info=exc) return translators = [('', translator, -1) for translator in translators] out['Translators'] = translators paperwork-2.1.1/paperwork-backend/src/paperwork_backend/beacon/000077500000000000000000000000001417573700700246365ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/beacon/__init__.py000066400000000000000000000000001417573700700267350ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/beacon/update.py000066400000000000000000000066521417573700700265030ustar00rootroot00000000000000import datetime import logging import os import openpaperwork_core import openpaperwork_core.beacon import openpaperwork_core.promise from .. import _version LOGGER = logging.getLogger(__name__) UPDATE_CHECK_INTERVAL = datetime.timedelta(days=7) UPDATE_PATH = "/beacon/latest" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.periodic = None self.http = None def get_interfaces(self): return ["update_detection"] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'http_json', 'defaults': ['openpaperwork_core.http'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'thread', 'defaults': ['openpaperwork_core.thread.simple'], }, ] def _register_config(self, core): setting = self.core.call_success( "config_build_simple", "update", "enabled", lambda: False ) self.core.call_all( "config_register", "check_for_update", setting ) setting = self.core.call_success( "config_build_simple", "update", "last_update_found", lambda: _version.version ) self.core.call_all( "config_register", "last_update_found", setting ) def _parse_version(self, version): version = version.split("-", 1) return tuple([int(x) for x in version[0].split(".")]) def update_check(self): LOGGER.info("Looking for updates...") def on_success(update_data, core): latest_version = update_data['paperwork'][os.name] LOGGER.info("Version advertised: %s", latest_version) self.core.call_all( "config_put", "last_update_found", latest_version ) self.core.call_all("config_save") self.update_compare() promise = openpaperwork_core.promise.Promise(self.core, lambda: "") promise = promise.then(self.http.get_request_promise(UPDATE_PATH)) promise = promise.then(on_success, self.core) promise.schedule() def update_compare(self): remote_version = self.core.call_success( "config_get", "last_update_found" ) remote_version = self._parse_version(remote_version) LOGGER.info("Remote version: %s", remote_version) local_version = self._parse_version(_version.version) LOGGER.info("Current version: %s", local_version) if remote_version > local_version: self.core.call_all( "on_update_detected", local_version, remote_version ) def init(self, core): super().init(core) self.periodic = openpaperwork_core.beacon.PeriodicTask( "update", UPDATE_CHECK_INTERVAL, self.update_check, self.update_compare ) self.http = self.core.call_success("http_json_get_client", "update") self._register_config(core) self.periodic.register_config(core) if self.core.call_success("config_get", "check_for_update"): self.core.call_all("mainloop_schedule", self.periodic.do, core) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/cairo/000077500000000000000000000000001417573700700245045ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/cairo/__init__.py000066400000000000000000000000001417573700700266030ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/cairo/pillow.py000066400000000000000000000313541417573700700263720ustar00rootroot00000000000000import io import logging import openpaperwork_core import openpaperwork_core.deps import openpaperwork_core.promise CAIRO_AVAILABLE = False GDK_AVAILABLE = False GI_AVAILABLE = False GLIB_AVAILABLE = False try: import gi GI_AVAILABLE = True except (ImportError, ValueError): pass try: import cairo CAIRO_AVAILABLE = True except (ImportError, ValueError): pass try: if GI_AVAILABLE: gi.require_version('Gdk', '3.0') gi.require_version('GdkPixbuf', '2.0') from gi.repository import Gdk from gi.repository import GdkPixbuf GDK_AVAILABLE = True except (ImportError, ValueError): pass try: from gi.repository import GLib from gi.repository import GObject GLIB_AVAILABLE = True except (ImportError, ValueError): # dummy so chkdeps can still be called class GObject(object): class SignalFlags(object): RUN_LAST = 0 class GObject(object): pass DELAY_SHORT = 0.01 DELAY_LONG = 0.3 LOGGER = logging.getLogger(__name__) BLUR_FACTOR = 8 MAX_IMG_DIMENSION = 16 * 1024 - 1 class ImgSurface(object): # wrapper so it can be weakref def __init__(self, surface): self.surface = surface def limit_img_size(size): (width, height) = size # Handle Cairo limitation: Dimensions of the image can't exceed 32k if width > MAX_IMG_DIMENSION: width = MAX_IMG_DIMENSION if height > MAX_IMG_DIMENSION: height = MAX_IMG_DIMENSION return (width, height) def pillow_to_surface(core, img, intermediate="pixbuf", quality=90): """ Convert a PIL image into a Cairo surface """ # TODO(Jflesch): Python 3 problem # cairo.ImageSurface.create_for_data() raises NotImplementedYet ... # img.putalpha(256) # (width, height) = img.size # imgd = img.tobytes('raw', 'BGRA') # imga = array.array('B', imgd) # stride = width * 4 # return cairo.ImageSurface.create_for_data( # imga, cairo.FORMAT_ARGB32, width, height, stride) # So we fall back to those methods: if intermediate == "pixbuf" and ( not hasattr(GdkPixbuf.Pixbuf, 'new_from_bytes') or img.getbands() != ('R', 'G', 'B') ): intermediate = "png" if intermediate == "pixbuf": data = GLib.Bytes.new(img.tobytes()) (width, height) = limit_img_size(img.size) if (width, height) != img.size: img = img.crop((0, 0, width, height)) pixbuf = GdkPixbuf.Pixbuf.new_from_bytes( data, GdkPixbuf.Colorspace.RGB, False, 8, width, height, width * 3 ) img_surface = ImgSurface(cairo.ImageSurface( cairo.FORMAT_RGB24, width, height )) ctx = cairo.Context(img_surface.surface) Gdk.cairo_set_source_pixbuf(ctx, pixbuf, 0.0, 0.0) ctx.rectangle(0, 0, width, height) ctx.fill() elif intermediate == "jpeg": if not hasattr(cairo.ImageSurface, 'set_mime_data'): LOGGER.warning( "Cairo %s does not support yet 'set_mime_data'." " Cannot include image as JPEG in the PDF." " Image will be included as PNG (much bigger)", cairo.version ) intermediate = 'png' else: # IMPORTANT: The actual surface will be empty. # but mime-data will have attached the correct data # to the surface that supports it img_surface = ImgSurface(cairo.ImageSurface( cairo.FORMAT_RGB24, img.size[0], img.size[1] )) img_io = io.BytesIO() img.save(img_io, format="JPEG", quality=quality) img_io.seek(0) data = img_io.read() img_surface.surface.set_mime_data(cairo.MIME_TYPE_JPEG, data) if intermediate == "png": img_io = io.BytesIO() img.save(img_io, format="PNG") img_io.seek(0) img_surface = ImgSurface(cairo.ImageSurface.create_from_png(img_io)) if img_surface is None: raise Exception( "pillow_to_surface(): unknown intermediate: {}".format( intermediate ) ) core.call_all("on_objref_track", img_surface) return img_surface class CairoRenderer(GObject.GObject): __gsignals__ = { 'getting_size': (GObject.SignalFlags.RUN_LAST, None, ()), 'size_obtained': (GObject.SignalFlags.RUN_LAST, None, ()), 'img_obtained': (GObject.SignalFlags.RUN_LAST, None, ()), } BACKGROUND = (0.5, 0.5, 0.5) OUTLINE = (0.5, 0.5, 0.5) def __init__(self, core, work_queue_name, file_url): super().__init__() self.core = core self.work_queue_name = work_queue_name self.file_url = file_url self.size = (0, 0) self.zoom = 1.0 self.blurry = False self.cairo_surface = None self.visible = False # very often, the image is much bigger than what we actually display # --> keep a copy of the reduced image in memory self.cache = (-1.0, None) promise = openpaperwork_core.promise.Promise( self.core, self.emit, args=("getting_size",) ) promise = promise.then( core.call_success("url_to_img_size_promise", file_url) ) promise = promise.then(self._set_img_size) # Gives back a bit of CPU time to GTK so the GUI remains # usable promise = promise.then(openpaperwork_core.promise.DelayPromise( core, DELAY_SHORT )) self.get_size_promise = promise self.getting_size = False # Gives back a bit of CPU time to GTK so the GUI remains # usable # Give also time so the loading can be cancelled promise = openpaperwork_core.promise.DelayPromise( core, DELAY_LONG ) promise = promise.then(core.call_success( "url_to_cairo_surface_promise", file_url )) promise = promise.then(self._set_cairo_surface) self.render_img_promise = promise self.render_job_in_queue = False def start(self): if self.getting_size: # seems render() may be called before start() in some cases # --> avoid calling twice work_queue_add_promise() to get the size return self.core.call_success( "work_queue_add_promise", self.work_queue_name, self.get_size_promise ) self.getting_size = True def render(self, force=False): if self.visible and not force: return self.visible = True if self.size == (0, 0): self.core.call_all( "work_queue_cancel", self.work_queue_name, self.get_size_promise ) # re add with a higher priority self.core.call_success( "work_queue_add_promise", self.work_queue_name, self.get_size_promise, priority=200 ) self.getting_size = True return if self.render_job_in_queue: return self.render_job_in_queue = True self.core.call_success( "work_queue_add_promise", self.work_queue_name, self.render_img_promise, priority=100 ) def hide(self): if not self.visible: return self.visible = False if self.size == (0, 0): self.core.call_all( "work_queue_cancel", self.work_queue_name, self.get_size_promise ) # re add with a lower priority self.core.call_success( "work_queue_add_promise", self.work_queue_name, self.get_size_promise ) self.getting_size = True if self.cairo_surface is not None: self.cairo_surface.surface.finish() self.cairo_surface = None self.cache = (-1.0, None) self.render_job_in_queue = False self.core.call_all( "work_queue_cancel", self.work_queue_name, self.render_img_promise ) def close(self): self.hide() self.core.call_all( "work_queue_cancel", self.work_queue_name, self.get_size_promise ) self.get_size_promise = None self.getting_size = False self.render_img_promise = None def _set_img_size(self, size): self.size = limit_img_size(size) self.getting_size = False if self.get_size_promise is None: # Document has been closed while we looked for its size return self.emit("size_obtained") if self.visible: self.render(force=True) def _set_cairo_surface(self, surface): self.render_job_in_queue = False if not self.visible: # visibility has changed surface.surface.finish() return self.cairo_surface = surface self.cache = (-1.0, None) self.emit("img_obtained") def _upd_cache(self): (cache_zoom, cache_img) = self.cache if cache_zoom == self.zoom: return img = ImgSurface(cairo.ImageSurface( cairo.FORMAT_RGB24, int(self.size[0] * self.zoom), int(self.size[1] * self.zoom), )) cairo_ctx = cairo.Context(img.surface) cairo_ctx.scale(self.zoom, self.zoom) cairo_ctx.set_source_surface(self.cairo_surface.surface) cairo_ctx.paint() self.cache = (self.zoom, img) def _draw(self, cairo_ctx): cairo_ctx.save() try: cairo_ctx.set_source_surface(self.cache[1].surface) cairo_ctx.paint() size = self.size cairo_ctx.set_source_rgb(*self.OUTLINE) cairo_ctx.set_line_width(1) cairo_ctx.rectangle( 0, 0, (size[0] * self.zoom) - 1, (size[1] * self.zoom) - 1 ) cairo_ctx.stroke() finally: cairo_ctx.restore() def draw(self, cairo_ctx): if self.cairo_surface is None: cairo_ctx.save() try: size = self.size cairo_ctx.set_source_rgb(*self.BACKGROUND) cairo_ctx.rectangle(0, 0, size[0], size[1]) cairo_ctx.clip() cairo_ctx.paint() finally: cairo_ctx.restore() elif self.blurry: zoom = self.zoom / BLUR_FACTOR reduced_surface = ImgSurface(cairo.ImageSurface( cairo.FORMAT_ARGB32, int(self.size[0] * zoom), int(self.size[1] * zoom) )) ctx = cairo.Context(reduced_surface.surface) ctx.scale(1 / BLUR_FACTOR, 1 / BLUR_FACTOR) self._draw(ctx) cairo_ctx.save() try: cairo_ctx.scale(BLUR_FACTOR, BLUR_FACTOR) cairo_ctx.set_source_surface(reduced_surface.surface) cairo_ctx.paint() finally: cairo_ctx.save() else: self._upd_cache() self._draw(cairo_ctx) def blur(self): self.blurry = True def unblur(self): self.blurry = False if GLIB_AVAILABLE: GObject.type_register(CairoRenderer) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'chkdeps', 'cairo_url', 'pillow_to_surface', ] def get_deps(self): return [ { 'interface': 'pillow', 'defaults': ['openpaperwork_core.pillow.img'], }, { 'interface': 'work_queue', 'defaults': ['openpaperwork_core.work_queue.default'], }, ] def chkdeps(self, out: dict): if not CAIRO_AVAILABLE: out['cairo'].update(openpaperwork_core.deps.CAIRO) if not GDK_AVAILABLE: out['gdk'].update(openpaperwork_core.deps.GDK) if not GI_AVAILABLE: out['gi'].update(openpaperwork_core.deps.GI) if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def pillow_to_surface(self, pillow, intermediate="pixbuf", quality=90): return pillow_to_surface(self.core, pillow, intermediate, quality) def url_to_cairo_surface_promise(self, file_url): promise = self.core.call_success("url_to_pillow_promise", file_url) promise = promise.then(self.pillow_to_surface) return promise def url_to_cairo_surface(self, file_url): pil_img = self.core.call_success("url_to_pillow", file_url) return self.pillow_to_surface(pil_img) def cairo_renderer_by_url(self, work_queue_name, file_url): return CairoRenderer(self.core, work_queue_name, file_url) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/cairo/poppler.py000066400000000000000000000265141417573700700265470ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.deps import openpaperwork_core.promise # TODO(Jflesch): bad import paperwork_backend.model.pdf CAIRO_AVAILABLE = False GLIB_AVAILABLE = False try: import cairo CAIRO_AVAILABLE = True except (ImportError, ValueError): pass try: from gi.repository import GObject GLIB_AVAILABLE = True except (ImportError, ValueError): # dummy so chkdeps can still be called class GObject(object): class SignalFlags(object): RUN_LAST = 0 class GObject(object): pass LOGGER = logging.getLogger(__name__) DELAY = 0.01 POPPLER_DOCS = {} BLUR_FACTOR = 8 class ImgSurface(object): # wrapper so it can be weakref def __init__(self, surface): self.surface = surface class CairoRenderer(GObject.GObject): __gsignals__ = { 'getting_size': (GObject.SignalFlags.RUN_LAST, None, ()), 'size_obtained': (GObject.SignalFlags.RUN_LAST, None, ()), 'img_obtained': (GObject.SignalFlags.RUN_LAST, None, ()), } OUTLINE = (0.5, 0.5, 0.5) def __init__( self, core, work_queue_name, file_url, page_idx, password=None): global POPPLER_DOCS super().__init__() self.core = core self.work_queue_name = work_queue_name self.file_url = file_url self.page_idx = page_idx self.password = password self.visible = False self.blurry = False self.size = (0, 0) self.zoom = 1.0 if file_url in POPPLER_DOCS: (doc, refcount) = POPPLER_DOCS[file_url] else: LOGGER.info("Opening PDF file {}".format(file_url)) doc = self.core.call_success( "poppler_open", file_url, password=password ) refcount = 0 POPPLER_DOCS[file_url] = (doc, refcount + 1) self.page = doc.get_page(page_idx) promise = openpaperwork_core.promise.Promise( self.core, self.emit, args=("getting_size",) ) promise = promise.then(openpaperwork_core.promise.Promise( self.core, self.page.get_size )) promise = promise.then(lambda size: ( size[0] * paperwork_backend.model.pdf.PDF_RENDER_FACTOR, size[1] * paperwork_backend.model.pdf.PDF_RENDER_FACTOR, )) promise.then(self._set_size) if page_idx % 25 == 0: # Gives back a bit of CPU time to GTK so the GUI remains # usable, but not too much so we don't recompute the layout too # often promise = promise.then(openpaperwork_core.promise.DelayPromise( core, DELAY )) self.get_size_promise = promise def __str__(self): return "CairoRenderer({} p{})".format(self.file_url, self.page_idx) def _set_size(self, size): if self.page is None: # Document has been closed while we looked for its size return self.size = size self.emit('size_obtained') if self.visible: self.render(force=True) def start(self): self.core.call_success( "work_queue_add_promise", self.work_queue_name, self.get_size_promise ) def render(self, force=False): if self.visible and not force: return self.visible = True if self.size == (0, 0): return self.emit('img_obtained') def hide(self): self.visible = False def close(self): global POPPLER_DOCS self.hide() self.page = None self.size = (0, 0) (doc, refcount) = POPPLER_DOCS.get(self.file_url, (None, None)) if refcount is None: LOGGER.warning("Double close for %s", self.file_url) return refcount -= 1 if refcount > 0: POPPLER_DOCS[self.file_url] = (doc, refcount) return LOGGER.info("Closing PDF file %s", self.file_url) POPPLER_DOCS.pop(self.file_url, None) def _draw(self, cairo_ctx, zoom): try: cairo_ctx.save() try: cairo_ctx.set_source_rgb(1.0, 1.0, 1.0) cairo_ctx.scale(zoom, zoom) cairo_ctx.rectangle(0, 0, self.size[0], self.size[1]) cairo_ctx.scale( paperwork_backend.model.pdf.PDF_RENDER_FACTOR, paperwork_backend.model.pdf.PDF_RENDER_FACTOR, ) cairo_ctx.clip() cairo_ctx.paint() self.page.render(cairo_ctx) cairo_ctx.scale( 1 / paperwork_backend.model.pdf.PDF_RENDER_FACTOR, 1 / paperwork_backend.model.pdf.PDF_RENDER_FACTOR, ) cairo_ctx.set_source_rgb(*self.OUTLINE) outline_width = 1 / zoom cairo_ctx.set_line_width(outline_width) cairo_ctx.rectangle( 0, 0, self.size[0] - outline_width, self.size[1] - outline_width ) cairo_ctx.stroke() finally: cairo_ctx.restore() except Exception as exc: LOGGER.error("CairoRenderer.draw() failed (PDF)", exc_info=exc) # WORKAROUND(Jflesch): with some malformed PDF file, we get an # exception on ctx.restore(), but the drawing was actually done. def draw(self, cairo_ctx): if not self.visible or self.page is None or self.size[0] == 0: return task = "pdf_to_cairo_draw({}, p{})".format( self.file_url, self.page_idx ) self.core.call_all("on_perfcheck_start", task) if not self.blurry: self._draw(cairo_ctx, self.zoom) else: zoom = self.zoom / BLUR_FACTOR reduced_surface = ImgSurface(cairo.ImageSurface( cairo.FORMAT_ARGB32, int(self.size[0] * zoom), int(self.size[1] * zoom) )) ctx = cairo.Context(reduced_surface.surface) self._draw(ctx, zoom) cairo_ctx.save() try: cairo_ctx.scale(BLUR_FACTOR, BLUR_FACTOR) cairo_ctx.set_source_surface(reduced_surface.surface) cairo_ctx.paint() finally: cairo_ctx.restore() self.core.call_all("on_perfcheck_stop", task, size=self.size) def blur(self): self.blurry = True def unblur(self): self.blurry = False if GLIB_AVAILABLE: GObject.type_register(CairoRenderer) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 1000 FILE_EXTENSION = ".pdf" def get_interfaces(self): return [ 'cairo_url', 'chkdeps', 'page_img_size', 'pdf_cairo_url', ] def get_deps(self): return [ { 'interface': 'poppler', 'defaults': ['paperwork_backend.poppler.memory'], }, { 'interface': 'urls', 'defaults': ['openpaperwork_core.urls'], }, ] def chkdeps(self, out: dict): if not CAIRO_AVAILABLE: out['cairo'].update(openpaperwork_core.deps.CAIRO) if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def _check_is_pdf(self, file_url): (url, args) = self.core.call_success("url_args_split", file_url) if not url.lower().endswith(self.FILE_EXTENSION): return (None, None, None) password = args.get('password', None) if password is not None: password = bytes.fromhex(password).decode("utf-8") page_idx = int(args.get('page', 1)) - 1 return (url, page_idx, password) def url_to_img_size(self, file_url): (file_url, page_idx, password) = self._check_is_pdf(file_url) if file_url is None: return None task = "url_to_img_size({})".format(file_url) self.core.call_all("on_perfcheck_start", task) doc = self.core.call_success( "poppler_open", file_url, password=password ) page = doc.get_page(page_idx) base_size = page.get_size() size = ( # scale up because default size if too small for reading int(base_size[0]) * paperwork_backend.model.pdf.PDF_RENDER_FACTOR, int(base_size[1]) * paperwork_backend.model.pdf.PDF_RENDER_FACTOR, ) self.core.call_all("on_perfcheck_stop", task, size=size) return size def url_to_img_size_promise(self, file_url): (page_url, page_idx, password) = self._check_is_pdf(file_url) if page_url is None: return None return openpaperwork_core.promise.Promise( self.core, self.url_to_img_size, args=(file_url,) ) def pdf_page_to_cairo_surface(self, file_url, page_idx, password=None): task = "pdf_page_to_cairo_surface({} p{})".format(file_url, page_idx) self.core.call_all("on_perfcheck_start", task) doc = self.core.call_success( "poppler_open", file_url, password=password ) page = doc.get_page(page_idx) base_size = page.get_size() size = ( # scale up because default size if too small for reading int(base_size[0]) * paperwork_backend.model.pdf.PDF_RENDER_FACTOR, int(base_size[1]) * paperwork_backend.model.pdf.PDF_RENDER_FACTOR, ) width = int(size[0]) height = int(size[1]) factor_w = width / base_size[0] factor_h = height / base_size[1] surface = ImgSurface(cairo.ImageSurface( cairo.FORMAT_ARGB32, width, height )) self.core.call_all("on_objref_track", surface) try: ctx = cairo.Context(surface.surface) ctx.save() try: ctx.set_source_rgb(1.0, 1.0, 1.0) ctx.rectangle(0, 0, width, height) ctx.clip() ctx.paint() ctx.scale(factor_w, factor_h) page.render(ctx) finally: ctx.restore() except Exception as exc: LOGGER.error("pdf_page_to_cairo_surface() failed", exc_info=exc) # WORKAROUND(Jflesch): with some malformed PDF file, we get an # exception on ctx.restore(), but the drawing was actually done. self.core.call_all("on_perfcheck_stop", task, size=(width, height)) return surface def url_to_cairo_surface(self, file_url): (file_url, page_idx, password) = self._check_is_pdf(file_url) if file_url is None: return None return self.pdf_page_to_cairo_surface(file_url, page_idx, password) def url_to_cairo_surface_promise(self, file_url): (file_url, page_idx, password) = self._check_is_pdf(file_url) if file_url is None: return None return openpaperwork_core.promise.Promise( self.core, self.pdf_page_to_cairo_surface, args=(file_url, page_idx, password) ) def cairo_renderer_by_url(self, work_queue_name, file_url): (file_url, page_idx, password) = self._check_is_pdf(file_url) if file_url is None: return None return CairoRenderer( self.core, work_queue_name, file_url, page_idx, password ) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/chkworkdir/000077500000000000000000000000001417573700700255565ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/chkworkdir/__init__.py000066400000000000000000000000001417573700700276550ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/chkworkdir/empty_doc.py000066400000000000000000000047561417573700700301270ustar00rootroot00000000000000import logging import openpaperwork_core from .. import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): """ Locates (and optionnally deletes) empty directories in the work directory. """ def get_interfaces(self): return ['chkworkdir'] def get_deps(self): return [ { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'nb_pages', 'defaults': [ 'paperwork_backend.model.img', 'paperwork_backend.model.pdf', ], }, ] def check_work_dir(self, out_problems: list): all_docs = [] self.core.call_all("storage_get_all_docs", all_docs, only_valid=False) all_docs.sort() total = len(all_docs) LOGGER.info("Checking work directory (%d documents)", total) for (idx, (doc_id, doc_url)) in enumerate(all_docs): self.core.call_all( "on_progress", "chkworkdir_empty_doc", idx / total, _("Checking doc %s") % (doc_id,) ) isdir = self.core.call_success("fs_isdir", doc_url) if not isdir: continue nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", doc_url ) if nb_pages is not None and nb_pages > 0: continue out_problems.append({ "problem": "empty_doc", "doc_id": doc_id, "doc_url": doc_url, "human_description": { "problem": _("Document %s is empty") % (doc_id,), "solution": _("Delete document %s") % (doc_id,), }, }) self.core.call_all("on_progress", "chkworkdir_empty_doc", 1.0) def fix_work_dir(self, problems): total = len(problems) for (idx, problem) in enumerate(problems): if problem['problem'] != 'empty_doc': continue LOGGER.info("Fixing document %s", problem['doc_url']) self.core.call_all( "on_progress", "fixworkdir_empty_doc", idx / total, _("Deleting empty doc %s") % (problem['doc_id'],) ) self.core.call_all("storage_delete_doc_id", problem['doc_id']) self.core.call_all("on_progress", "fixworkdir_empty_doc", 1.0) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/chkworkdir/label_color.py000066400000000000000000000122771417573700700304160ustar00rootroot00000000000000import logging import openpaperwork_core from .. import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): """ Label files in each document contains both the label names and their color. If the user changes a label color, label files are updated one-by-one. If this process is interrupted, we end up with 2 colors for the same label. """ def get_interfaces(self): return ['chkworkdir'] def get_deps(self): return [ { 'interface': 'doc_labels', 'defaults': ['paperwork_backend.model.labels'], }, { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, ] @staticmethod def _is_same_color(a, b): a = (int(a[0] * 0xFF), int(a[1] * 0xFF), int(a[2] * 0xFF)) b = (int(b[0] * 0xFF), int(b[1] * 0xFF), int(b[2] * 0xFF)) return a == b def check_work_dir(self, out_problems: list): all_docs = [] self.core.call_all("storage_get_all_docs", all_docs, only_valid=False) all_docs.sort() total = len(all_docs) LOGGER.info("Checking work directory (%d documents)", total) # label text --> ( # label color, first_seen_doc_id, first_seen_exact_color # ) first_seen_labels = {} for (idx, (doc_id, doc_url)) in enumerate(all_docs): self.core.call_all( "on_progress", "chkworkdir_label_color", idx / total, _("Checking doc %s") % (doc_id,) ) doc_labels = set() self.core.call_all("doc_get_labels_by_url", doc_labels, doc_url) for (doc_label_txt, doc_label_color_orig) in doc_labels: doc_label_color = self.core.call_success( "label_color_to_rgb", doc_label_color_orig ) first_color = first_seen_labels.get(doc_label_txt, None) if first_color is None: first_seen_labels[doc_label_txt] = ( doc_label_color, doc_id, doc_label_color_orig ) continue (first_color, first_docid, first_exact_color) = first_color if self._is_same_color(first_color, doc_label_color): continue out_problems.append({ "problem": "label_color", "doc_id": doc_id, "doc_url": doc_url, "problem_color": doc_label_color, "solution_color": first_color, "current_label": (doc_label_txt, doc_label_color), "fixed_label": (doc_label_txt, first_exact_color), "human_description": { "problem": ( _( "Document %s has label \"%s\" with color=%s" " while document %s has label" " \"%s\" with color=%s" ) % ( doc_id, doc_label_txt, self.core.call_success( "label_color_rgb_to_text", doc_label_color ), first_docid, doc_label_txt, self.core.call_success( "label_color_rgb_to_text", first_color ) ) ), "solution": ( _( "Set label color %s on label \"%s\"" " of document %s" ) % ( self.core.call_success( "label_color_rgb_to_text", first_color ), doc_label_txt, doc_id ) ) } }) self.core.call_all("on_progress", "chkworkdir_label_color", 1.0) def fix_work_dir(self, problems): total = len(problems) for (idx, problem) in enumerate(problems): if problem['problem'] != 'label_color': continue LOGGER.info("Fixing document %s", problem['doc_url']) self.core.call_all( "on_progress", "fixworkdir_label_color", idx / total, _("Fixing label on doc %s") % (problem['doc_id'],) ) self.core.call_all( "doc_remove_label_by_url", problem['doc_url'], problem['current_label'][0] ) self.core.call_all( "doc_add_label_by_url", problem['doc_url'], problem['fixed_label'][0], problem['fixed_label'][1] ) self.core.call_all("on_progress", "fixworkdir_label_color", 1.0) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/converter/000077500000000000000000000000001417573700700254165ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/converter/__init__.py000066400000000000000000000000001417573700700275150ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/converter/libreoffice.py000066400000000000000000000211571417573700700302470ustar00rootroot00000000000000import logging import os import subprocess import tempfile import openpaperwork_core import paperwork_backend.deps from .. import _ LOGGER = logging.getLogger(__name__) LIBREOFFICE = [ r'libreoffice', r'soffice.exe', r'C:\Program Files\LibreOffice\program\soffice.exe', r'/app/libreoffice/program/soffice.bin', # Flatpak ] LIBREOFFICE_ARGS = [ '--nocrashreport', '--nodefault', '--nofirststartwizard', '--nolockcheck', '--nologo', '--norestore', '--headless', '--convert-to', 'pdf', '--outdir', '{out_dir}', '{in_doc}', ] def is_exe(fpath): return os.path.isfile(fpath) and os.access(fpath, os.X_OK) def which(program): if os.path.sep in program: if is_exe(program): return program else: for path in os.environ["PATH"].split(os.pathsep): exe_file = os.path.join(path, program) if is_exe(exe_file): return exe_file return None class Plugin(openpaperwork_core.PluginBase): FILE_TYPES = { ( "application/msword", "doc", "Microsoft Word (.doc)", ), ( "application/msword", "dot", _("Microsoft Word template (.dot)"), ), ( "application/vnd.ms-excel", "xls", "Microsoft Excel (.xls)", ), ( "application/vnd.ms-excel", "xlt", _("Microsoft Excel template (.xlt)"), ), ( "application/vnd.ms-powerpoint", "pps", "Microsoft PowerPoint (.pps)", ), ( "application/vnd.ms-powerpoint", "ppt", _("Microsoft PowerPoint template (.ppt)"), ), ( "application/vnd.oasis.opendocument.chart", "odc", _("OpenOffice/LibreOffice Chart (.odc)"), ), ( "application/vnd.oasis.opendocument.database", "odb", _("OpenOffice/LibreOffice Database (.odb)"), ), ( "application/vnd.oasis.opendocument.formula", "odf", _("OpenOffice/LibreOffice Formula (.odf)"), ), ( "application/vnd.oasis.opendocument.graphics", "odg", _("OpenOffice/LibreOffice Graphics (.odg)"), ), ( "application/vnd.oasis.opendocument.graphics-template", "otg", _("OpenOffice/LibreOffice Graphics template (.otg)"), ), ( "application/vnd.oasis.opendocument.image", "odi", _("OpenOffice/LibreOffice Image template (.odi)"), ), ( "application/vnd.oasis.opendocument.presentation", "odp", _("OpenOffice/LibreOffice Presentation (.odp)"), ), ( "application/vnd.oasis.opendocument.presentation-template", "otp", _("OpenOffice/LibreOffice Presentation template (.otp)"), ), ( "application/vnd.oasis.opendocument.spreadsheet", "ods", _("OpenOffice/LibreOffice Spreadsheet (.ods)"), ), ( "application/vnd.oasis.opendocument.spreadsheet-template", "ots", _("OpenOffice/LibreOffice Spreadsheet template (.ots)"), ), ( "application/vnd.oasis.opendocument.text", "odt", _("OpenOffice/LibreOffice Text (.odt)"), ), ( "application/vnd.oasis.opendocument.text-master", "odm", _("OpenOffice/LibreOffice Text master (.odm)"), ), ( "application/vnd.oasis.opendocument.text-template", "ott", _("OpenOffice/LibreOffice Text template (.ott)"), ), ( "application/vnd.oasis.opendocument.text-web", "oth", _("OpenOffice/LibreOffice Text web (.oth)"), ), ( "application/vnd.openxmlformats-officedocument.presentationml" ".presentation", "pptx", "Microsoft PowerPoint presentation (.pptx)", ), ( "application/vnd.openxmlformats-officedocument.presentationml" ".slide", "sldx", _("Microsoft PowerPoint slide (.sldx)"), ), ( "application/vnd.openxmlformats-officedocument.presentationml" ".slideshow", "ppsx", _("Microsoft PowerPoint slideshow (.ppsx)"), ), ( "application/vnd.openxmlformats-officedocument.presentationml" ".template", "potx", _("Microsoft PowerPoint presentation template (.potx)"), ), ( "application/vnd.openxmlformats-officedocument.spreadsheetml" ".sheet", "xlsx", "Microsoft Excel (.xlsx)", ), ( "application/vnd.openxmlformats-officedocument.spreadsheetml" ".template", "xltx", _("Microsoft Excel template (.xltx)"), ), ( "application/vnd.openxmlformats-officedocument" ".wordprocessingml.document", "docx", "Microsoft Word (.docx)", ), ( "application/vnd.openxmlformats-officedocument" ".wordprocessingml.template", "dotx", _("Microsoft Word template (.dotx)"), ), } def __init__(self): self.libreoffice = None def get_interfaces(self): return [ "chkdeps", "doc_converter", ] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'] }, ] def init(self, core): super().init(core) for exe in LIBREOFFICE: self.libreoffice = which(exe) if self.libreoffice is not None: break LOGGER.info("Libreoffice: %s", self.libreoffice) def chkdeps(self, out: dict): if self.libreoffice is None: out['libreoffice'] = paperwork_backend.deps.LIBREOFFICE def converter_get_file_types(self, out: set): if self.libreoffice is None: return out.update(self.FILE_TYPES) def convert_file_to_pdf(self, doc_file_uri, mime_type, out_pdf_file_url): if self.libreoffice is None: return None file_types = {mime: ext for (mime, ext, desc) in self.FILE_TYPES} if mime_type not in file_types: return None LOGGER.info( "Converting %s (%s) to %s (PDF)", doc_file_uri, mime_type, out_pdf_file_url ) file_name = self.core.call_success("fs_basename", doc_file_uri) if "." not in file_name: LOGGER.error("No file extension ? %s", doc_file_uri) return None file_ext = file_name.rsplit(".", 1)[-1] cwd = os.getcwd() with tempfile.TemporaryDirectory() as tmp_dir: src_file = os.path.join(tmp_dir, "doc." + file_ext) dst_file = os.path.join(tmp_dir, "doc.pdf") # Assume Libreoffice only supports local files self.core.call_success( "fs_copy", doc_file_uri, self.core.call_success("fs_safe", src_file) ) if not self.core.call_success( "fs_exists", self.core.call_success("fs_safe", src_file)): LOGGER.error("Failed to copy file %s", doc_file_uri) return None os.chdir(tmp_dir) try: args = [ x.format(in_doc=src_file, out_dir=tmp_dir) for x in LIBREOFFICE_ARGS ] popen = subprocess.Popen( [self.libreoffice] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) (stdout, stderr) = popen.communicate() dst_file_url = self.core.call_success("fs_safe", dst_file) if not self.core.call_success("fs_exists", dst_file_url): LOGGER.error( "Failed to convert %s (%s) to %s|%s (PDF)", doc_file_uri, mime_type, dst_file_url, out_pdf_file_url ) LOGGER.error("Command was: %s", [self.libreoffice] + args) LOGGER.error("LibreOffice stdout: %s", stdout) LOGGER.error("LibreOffice stderr: %s", stderr) return False self.core.call_success( "fs_copy", dst_file_url, out_pdf_file_url ) finally: os.chdir(cwd) return True paperwork-2.1.1/paperwork-backend/src/paperwork_backend/datadirhandler.py000066400000000000000000000057141417573700700267360ustar00rootroot00000000000000""" This plugin handles different working directories by hashing their path and adding a part of this hash to the data directory. If a working directly was not loaded for at least one month, it is deleted, again. """ import datetime import logging import base64 import hashlib import os import openpaperwork_core LOGGER = logging.getLogger(__name__) WORK_DIR_NAME = "workdir_data" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() def get_interfaces(self): return ["data_dir_handler"] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, { 'interface': 'paths', 'defaults': ['openpaperwork_core.paths.xdg'], }, { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, ] def init(self, core): super().init(core) self._delete_old_directories() def on_storage_changed(self): LOGGER.info( "Work directory has changed --> data directory has to change too" ) self.core.call_all("on_data_dir_changed") @staticmethod def _hash_dir(url): dir_hash = hashlib.sha256(url.encode()).digest()[:6] encoded_hash = base64.urlsafe_b64encode(dir_hash).decode()[:8] return encoded_hash def _delete_old_directories(self, days_to_data_dir_deletion=31): data_dir = self.core.call_success("paths_get_data_dir") work_data_dir = self.core.call_success( "fs_join", data_dir, WORK_DIR_NAME) folder_content = self.core.call_success( "fs_listdir", work_data_dir) now = datetime.datetime.now() for file in folder_content: if self.core.call_success("fs_isdir", file): mtime = self.core.call_success("fs_get_mtime", file) modified = datetime.datetime.fromtimestamp(mtime) time_diff = now - modified if time_diff.days >= days_to_data_dir_deletion: LOGGER.info( "Removing directory %s as it is older than %i days." % (file, days_to_data_dir_deletion)) self.core.call_success("fs_rm_rf", file) def data_dir_handler_get_individual_data_dir(self): work_dir = self.core.call_success("storage_get_id") data_dir = self.core.call_success("paths_get_data_dir") encoded_hash = Plugin._hash_dir(work_dir) workdir_data_folder = self.core.call_success( "fs_join", data_dir, WORK_DIR_NAME) individual_data_dir = self.core.call_success( "fs_join", workdir_data_folder, "%s_%s" % (os.path.basename(work_dir), encoded_hash) ) self.core.call_success("fs_mkdir_p", individual_data_dir) return individual_data_dir paperwork-2.1.1/paperwork-backend/src/paperwork_backend/deps.py000066400000000000000000000002121417573700700247070ustar00rootroot00000000000000LIBREOFFICE = { 'debian': 'libreoffice', 'linuxmint': 'libreoffice', 'raspian': 'libreoffice', 'ubuntu': 'libreoffice', } paperwork-2.1.1/paperwork-backend/src/paperwork_backend/docexport/000077500000000000000000000000001417573700700254165ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/docexport/__init__.py000066400000000000000000000227461417573700700275420ustar00rootroot00000000000000""" Doc and page exporting are designed as pipelines. UI build the pipelines and run it. Data to export are represented as tree: Document set to export: data = set of (doc_id, doc_url)) |-- Document to export: data = (doc_id, doc_url) | |-- Page to export: data = (page_idx) | | |-- img_boxes: data = (Pillow image, boxes) | |-- Page to export | |-- Document_to_export | (...) When a pipes says it needs a given type in input, it means the content of the tree must be extended up to this data type. For example, if a pipe says it needs ExportDataType.PAGE as input, it must get an ExportData of type ExportDataType.DOCUMENT_SET as input, but expanded up to ExportDataType.PAGE: Document set to export |-- Document to export | |-- Page to export | |-- Page to export | |-- Document_to_export | (...) It must *not* be expanded any futher either (for instance, no img+boxes if the pipe says it expects a ExportDataType.PAGE as input). There are pipes dedicated to expanding unexpanded data (see docexport.generic). """ import enum import openpaperwork_core class ExportDataType(enum.Enum): DOCUMENT_SET = 0 DOCUMENT = 1 PAGE = 2 IMG_BOXES = 3 # the following ones are for final output only. # A list of paths (str) will be returned instead of an ExportData object. OUTPUT_URL_FILE = -1 class ExportData(object): def __init__(self, dtype, data): self.dtype = dtype self.data = data self._children = [] self.expanded = False def clone(self): out = ExportData(self.dtype, self.data) out._children = [c.clone() for c in self._children] out.expanded = self.expanded return out def iter(self, dtype): if self.dtype == dtype: yield self return for c in self.get_children(): for s in c.iter(dtype): yield (self, s) def get_children(self): assert(self.expanded) return self._children def set_children(self, children): self._children = children self.expanded = True @staticmethod def build_doc_set(docs): return ExportData(ExportDataType.DOCUMENT_SET, docs) @staticmethod def build_doc(doc_id, doc_url): root = ExportData.build_doc_set({(doc_id, doc_url)}) doc = ExportData(ExportDataType.DOCUMENT, (doc_id, doc_url)) root.set_children([doc]) return root @staticmethod def build_page(doc_id, doc_url, page_idx): return ExportData.build_pages(doc_id, doc_url, [page_idx]) @staticmethod def build_pages(doc_id, doc_url, page_indexes): root = ExportData.build_doc_set({(doc_id, doc_url)}) doc = ExportData(ExportDataType.DOCUMENT, (doc_id, doc_url)) pages = [ ExportData(ExportDataType.PAGE, page_idx) for page_idx in page_indexes ] doc.set_children(pages) root.set_children([doc]) return root class AbstractExportPipe(object): """ Pipes are used by the frontend/user to build an export pipeline (a list of pipes to apply in a specific order). There are input pipes, taking either DOCUMENT_SET, DOCUMENT, or PAGES as input. There are output pipes, providing a file URL (FILE_URL) as output or a directory URL. And there are processing pipes (usually taking Pillow images and text boxes as input (IMG_BOXES)). Once the pipeline is defined, the frontend code can obtain promises from the pipes (`export_get_pipe_*()`), chain them together, and schedule them (see get_promise()). """ def __init__(self, name, input_type, output_type): """ Arguments: name -- name of the export pipe input_type -- accepted ExportDataType output_type -- ExportDataType """ self.name = name self.input_type = input_type self.output_type = output_type self.can_change_quality = False self.can_change_page_format = False self.quality = 0.75 self.page_format = (595.2755905511812, 841.8897637795276) # A4 def can_export_doc(self, doc_url): return False def can_export_page(self, doc_url, page_idx): return False def get_promise(self, result='final', target_file_url=None): """ Returns a promise. Beware that the promise will modify the ExportData object given as input. If you want to preserve your copy, please use ExportData.clone(). Arguments: - result: either 'preview' (only one page) or 'final' (all pages) - target_file_url: Where the final pipe should write the file (None = temporary file) Returns: - A promise, that expect ExportData object as input and will return either an ExportData (if it must be chained to another pipe) or a list of str (list of paths) """ assert() # must be implemented by subclasses def get_estimated_size_factor(self, input_data): """ Return the factor to apply to the preview size to get an estimation of the final result size. Arguments: input_data -- ExportData. Won't be modified """ return 1 def set_quality(self, quality): """ Allow to define an output quality (between 0 and 100). Check `can_change_quality` before calling this method. """ assert(self.can_change_quality) self.quality = quality def set_page_format(self, page_format): """ Allow to define the expected output page format. Check `can_change_quality` before calling this method. Arguments: page_format: tuple (width, height), in points (1 point == 1/72.0 inch) """ assert(self.can_change_page_format) self.page_format = page_format def get_output_mime(self): """ If the pipe outputs a file, specifies its mime type and a list of possible file extensions. None if it doesn't outputs a file. """ return None def __str__(self): assert() # must be implemented by subclasses class ExportDataTransformedImgBoxes(ExportData): """ Page images takes a lot of memory --> we only generate them when actually requested. """ def __init__(self, pipe, original_page): super().__init__(ExportDataType.PAGE, original_page.data) self.expanded = True self.pipe = pipe self.original_page = original_page def get_children(self): children = self.original_page.get_children() for img_boxes in children: (img, boxes) = img_boxes.data img = self.pipe.transform(img) yield ExportData(ExportDataType.IMG_BOXES, (img, boxes)) class AbstractSimpleTransformExportPipe(AbstractExportPipe): """ Base template class for page image to page image transformations. """ def __init__(self, core, name): super().__init__( name=name, input_type=ExportDataType.IMG_BOXES, output_type=ExportDataType.IMG_BOXES ) self.core = core def transform(self, img): # sub-classes must implement it assert() def get_promise(self, result='final', target_file_url=None): def do(input_data): assert(input_data.dtype == ExportDataType.DOCUMENT_SET) docs = input_data.iter(ExportDataType.DOCUMENT) docs = list(docs) # replace the document page list by objects that will # generate their children (img+boxes) on-the-fly. for (doc_set, doc) in docs: assert(doc.expanded) doc.set_children([ ExportDataTransformedImgBoxes(self, page) for page in doc.get_children() ]) return input_data return openpaperwork_core.promise.ThreadedPromise(self.core, func=do) class AbstractExportPipePlugin(openpaperwork_core.PluginBase): def __init__(self): self.pipes = [] def get_interfaces(self): return ['export_pipes'] def get_deps(self): return [ { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'thread', 'defaults': ['openpaperwork_core.thread.simple'], }, ] def export_get_pipe_by_name(self, name): for pipe in self.pipes: if pipe.name == name: return pipe def export_get_pipes_by_input(self, out: list, input_type): for pipe in self.pipes: if input_type == pipe.input_type: out.append(pipe) def export_get_pipes_by_doc_urls(self, out: list, doc_urls): for pipe in self.pipes: if pipe.input_type != ExportDataType.DOCUMENT: continue for doc_url in doc_urls: if not pipe.can_export_doc(doc_url): break else: out.append(pipe) def export_get_pipes_by_doc_url(self, out: list, doc_url): return self.export_get_pipes_by_doc_urls(out, [doc_url]) def export_get_pipes_by_page(self, out: list, doc_url, page_nb): for pipe in self.pipes: if pipe.input_type != ExportDataType.PAGE: continue if not pipe.can_export_doc(doc_url, page_nb): continue out.append(pipe) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/docexport/generic.py000066400000000000000000000064751417573700700274200ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.promise from . import ( AbstractExportPipe, AbstractExportPipePlugin, ExportData, ExportDataType ) from .. import _ LOGGER = logging.getLogger(__name__) class DocSetToDoc(AbstractExportPipe): def __init__(self, core): super().__init__( name="doc_set_to_docs", input_type=ExportDataType.DOCUMENT_SET, output_type=ExportDataType.DOCUMENT ) self.core = core def get_promise(self, result='final', target_file_url=None): def do(input_data): assert(input_data.dtype == ExportDataType.DOCUMENT_SET) assert(not input_data.expanded) docs = input_data.data if result == 'preview': docs = docs[:1] children = [ ExportData(ExportDataType.DOCUMENT, doc) for doc in docs ] input_data.set_children(children) return input_data return openpaperwork_core.promise.Promise(self.core, do) def get_estimated_size_factor(self, input_data): assert(input_data.dtype == ExportDataType.DOCUMENT_SET) return len(input_data.data) def __str__(self): # this pipe shouldn't ever be visible to end-user return "Expand document set into documents (internal)" class DocToPage(AbstractExportPipe): def __init__(self, core): super().__init__( name="doc_to_pages", input_type=ExportDataType.DOCUMENT, output_type=ExportDataType.PAGE ) self.core = core def can_export_doc(self, doc_url): return True def get_promise(self, result='final', target_file_url=None): def do(input_data): assert(input_data.dtype == ExportDataType.DOCUMENT_SET) docs = input_data.iter(ExportDataType.DOCUMENT) for (doc_set, doc) in docs: assert(not doc.expanded) nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", doc.data[1] ) pages = [ ExportData(ExportDataType.PAGE, page_idx) for page_idx in range(0, nb_pages) ] doc.set_children(pages) return input_data return openpaperwork_core.promise.Promise(self.core, do) def get_estimated_size_factor(self, input_data): # average number of pages per document assert(input_data.dtype == ExportDataType.DOCUMENT_SET) nb_pages = 0 nb_docs = len(input_data.data) for (doc_id, doc_url) in input_data.data: nb_pages += self.core.call_success( "doc_get_nb_pages_by_url", doc_url ) return nb_pages / nb_docs def __str__(self): return _("Page by page processing") class Plugin(AbstractExportPipePlugin): def get_deps(self): return [ { 'interface': 'pages', 'defaults': [ 'paperwork_backend.model.img' 'paperwork_backend.model.pdf' ], }, ] def init(self, core): super().init(core) self.pipes = [ DocSetToDoc(self.core), DocToPage(self.core), ] paperwork-2.1.1/paperwork-backend/src/paperwork_backend/docexport/img.py000066400000000000000000000200201417573700700265360ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.promise from . import ( AbstractExportPipe, AbstractExportPipePlugin, AbstractSimpleTransformExportPipe, ExportData, ExportDataType ) from .. import _ LOGGER = logging.getLogger(__name__) class ExportDataPage(ExportData): """ Page images takes a lot of memory --> we only load them when actually requested. """ def __init__(self, core, doc_id, doc_url, page_idx, progress, final=False): super().__init__(ExportDataType.PAGE, page_idx) self.expanded = True self.core = core self.doc_id = doc_id self.doc_url = doc_url self.page_idx = page_idx self.progress = progress self.final = final def get_children(self): # ASSSUMPTION(Jflesch): pages are always requested sequentially self.core.call_success( "mainloop_schedule", self.core.call_all, "on_progress", "export", self.progress, _("Exporting {doc_id} p{page_idx} ...").format( doc_id=self.doc_id, page_idx=(self.page_idx + 1) ) ) page_url = (self.core.call_success( "page_get_img_url", self.doc_url, self.page_idx )) assert(page_url is not None) img = self.core.call_success("url_to_pillow", page_url) boxes = self.core.call_success( "page_get_boxes_by_url", self.doc_url, self.page_idx ) or [] yield ExportData(ExportDataType.IMG_BOXES, (img, boxes)) if self.final: self.core.call_success( "mainloop_schedule", self.core.call_all, "on_progress", "export", 1.0 ) class DocToPillowBoxesExportPipe(AbstractExportPipe): def __init__(self, core): super().__init__( name="img_boxes", input_type=ExportDataType.PAGE, output_type=ExportDataType.IMG_BOXES ) self.core = core def can_export_page(self, doc_url, page_nb): return True def get_promise(self, result='final', target_file_url=None): def to_img_and_boxes(input_data): assert(input_data.dtype == ExportDataType.DOCUMENT_SET) docs = input_data.iter(ExportDataType.DOCUMENT) docs = list(docs) self.core.call_success( "mainloop_schedule", self.core.call_all, "on_progress", "export", 0.0, _("Exporting ...") ) total_pages = 0 for (doc_set, doc) in docs: assert(doc.expanded) total_pages += len(doc.get_children()) nb_pages = 0 last_page = None for (doc_set, doc) in docs: assert(doc.expanded) # replace the document page list by objects that will # generate their children (img+boxes) on-the-fly. new_pages = [ ExportDataPage( self.core, doc.data[0], doc.data[1], page.data, (nb_pages + idx) / total_pages ) for (idx, page) in enumerate(doc.get_children()) ] doc.set_children(new_pages) nb_pages += len(new_pages) last_page = new_pages[-1] last_page.final = True return input_data promise = openpaperwork_core.promise.ThreadedPromise( self.core, to_img_and_boxes ) return promise def __str__(self): return _("Split page(s) into image(s) and text(s)") class PageToImageExportPipe(AbstractExportPipe): def __init__(self, core, name, format, file_extensions, mime, has_quality): super().__init__( name=name, input_type=ExportDataType.IMG_BOXES, output_type=ExportDataType.OUTPUT_URL_FILE, ) self.can_change_quality = has_quality self.format = format self.file_extensions = file_extensions self.mime = mime self.core = core def get_promise(self, result='final', target_file_url=None): def page_to_image(input_data, target_file_url): if target_file_url is None: (target_file_url, file_desc) = self.core.call_success( "fs_mktemp", prefix="paperwork-export-", suffix="." + self.file_extensions[0], mode="w" ) file_desc.close() list_img_boxes = input_data.iter(ExportDataType.IMG_BOXES) out_files = [] for (doc_set, (doc, (page, img_boxes))) in list_img_boxes: out = target_file_url if len(out_files) != 0: out = target_file_url.rsplit(".", 1) out = "{}_{}.{}".format(out[0], len(out_files), out[1]) self.core.call_success( "pillow_to_url", img_boxes.data[0], out, format=self.format, quality=self.quality ) out_files.append(out) return out_files return openpaperwork_core.promise.ThreadedPromise( self.core, page_to_image, kwargs={'target_file_url': target_file_url} ) def get_output_mime(self): return (self.mime, self.file_extensions) def __str__(self): return _("Image file ({})".format(self.format)) class BlackAndWhiteExportPipe(AbstractSimpleTransformExportPipe): def __init__(self, core): super().__init__(core, "bw") def transform(self, pil_img): pil_img = pil_img.convert("L") # to grayscale pil_img = pil_img.point(lambda x: 0 if x < 128 else 255, '1') return pil_img def __str__(self): return _("Black & White") class GrayscaleExportPipe(AbstractSimpleTransformExportPipe): def __init__(self, core): super().__init__(core, "grayscale") def transform(self, pil_img): return pil_img.convert("L") def __str__(self): return _("Grayscale") class Plugin(AbstractExportPipePlugin): def init(self, core): super().init(core) self.pipes = [ BlackAndWhiteExportPipe(core), DocToPillowBoxesExportPipe(core), GrayscaleExportPipe(core), PageToImageExportPipe( core, "bmp", "BMP", ("bmp",), "image/x-ms.bmp", False ), PageToImageExportPipe( core, "gif", "GIF", ("gif",), "image/gif", False ), PageToImageExportPipe( core, "jpeg", "JPEG", ("jpeg", "jpg"), "image/jpeg", True ), PageToImageExportPipe( core, "png", "PNG", ("png",), "image/png", False ), PageToImageExportPipe( core, "tiff", "TIFF", ("tiff",), "image/tiff", False ), ] def get_deps(self): return [ { 'interface': 'doc_text', 'defaults': [ # load also model.img because model.pdf will # satisfy the interface 'page_img' anyway 'paperwork_backend.model.img', 'paperwork_backend.model.hocr', 'paperwork_backend.model.pdf', ], }, { 'interface': 'page_img', 'defaults': [ # load also model.hocr because model.pdf will # satisfy the interface 'doc_text' anyway 'paperwork_backend.model.img', 'paperwork_backend.model.hocr', 'paperwork_backend.model.pdf', ], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'pillow', 'defaults': [ 'openpaperwork_core.pillow.img', 'paperwork_backend.pillow.pdf', ], }, ] paperwork-2.1.1/paperwork-backend/src/paperwork_backend/docexport/pdf.py000066400000000000000000000245151417573700700265500ustar00rootroot00000000000000import logging import time import PIL import PIL.Image import openpaperwork_core import openpaperwork_core.deps import openpaperwork_core.promise from . import ( AbstractExportPipe, AbstractExportPipePlugin, ExportDataType ) from .. import _ CAIRO_AVAILABLE = False GI_AVAILABLE = False GLIB_AVAILABLE = False PANGO_AVAILABLE = False try: import cairo CAIRO_AVAILABLE = True except (ImportError, ValueError): pass try: import gi GI_AVAILABLE = True except (ImportError, ValueError): pass if GI_AVAILABLE: try: gi.require_version('Pango', '1.0') gi.require_version('PangoCairo', '1.0') from gi.repository import Pango from gi.repository import PangoCairo PANGO_AVAILABLE = True except (ImportError, ValueError): pass LOGGER = logging.getLogger(__name__) class PdfDocUrlToPdfUrlExportPipe(AbstractExportPipe): """ Simply copy the PDF we have. """ def __init__(self, core): super().__init__( name="unmodified_pdf", input_type=ExportDataType.DOCUMENT, output_type=ExportDataType.OUTPUT_URL_FILE ) self.core = core def can_export_doc(self, doc_url): pdf_url = self.core.call_success("doc_get_pdf_url_by_url", doc_url) return pdf_url is not None def get_promise(self, result='final', target_file_url=None): def do(input_data, target_file_url): if target_file_url is None: (target_file_url, file_desc) = self.core.call_success( "fs_mktemp", prefix="paperwork-export-", suffix=".pdf", mode="wb", on_disk=True ) file_desc.close() list_docs = input_data.iter(ExportDataType.DOCUMENT) out_files = [] for (idx, (doc_set, doc)) in enumerate(list_docs): out = target_file_url if idx != 0: out = target_file_url.rsplit(".", 1) out = "{}_{}.{}".format(out[0], idx, out[1]) assert(not doc.expanded) pdf_url = self.core.call_success( "doc_get_pdf_url_by_url", doc.data[1] ) assert(pdf_url is not None) self.core.call_success("fs_copy", pdf_url, out) out_files.append(out) return out_files return openpaperwork_core.promise.Promise( self.core, do, kwargs={'target_file_url': target_file_url} ) def get_output_mime(self): return ("application/pdf", ("pdf",)) def __str__(self): return _("Original PDF(s)") class PdfCreator(object): def __init__(self, core, target_file_url, page_format, quality): self.core = core self.page_format = page_format self.quality = quality self.file_descriptor = core.call_success( "fs_open", target_file_url, 'wb' ) self.pdf_surface = cairo.PDFSurface( self.file_descriptor, self.page_format[0], self.page_format[1] ) self.pdf_context = cairo.Context(self.pdf_surface) def set_page_size(self, img_size): # portrait or landscape if (img_size[0] < img_size[1]): self.pdf_size = ( min(self.page_format[0], self.page_format[1]), max(self.page_format[0], self.page_format[1]) ) else: self.pdf_size = ( max(self.page_format[0], self.page_format[1]), min(self.page_format[0], self.page_format[1]) ) self.pdf_surface.set_size(self.pdf_size[0], self.pdf_size[1]) def paint_txt(self, boxes, img_size): scale_factor_x = self.pdf_size[0] / img_size[0] scale_factor_y = self.pdf_size[1] / img_size[1] scale_factor = min(scale_factor_x, scale_factor_y) for line in boxes: for word in line.word_boxes: if word.position[0][0] <= 0 and line.position[0][1] <= 0: continue box_size = ( (word.position[1][0] - word.position[0][0]) * scale_factor, (line.position[1][1] - line.position[0][1]) * scale_factor ) if 0 in box_size: continue layout = PangoCairo.create_layout(self.pdf_context) layout.set_text(word.content, -1) txt_size = layout.get_size() if 0 in txt_size: continue txt_factors = ( float(box_size[0]) * Pango.SCALE / txt_size[0], float(box_size[1]) * Pango.SCALE / txt_size[1], ) self.pdf_context.save() try: self.pdf_context.set_source_rgb(0, 0, 0) self.pdf_context.translate( word.position[0][0] * scale_factor, line.position[0][1] * scale_factor ) # make the text use the whole box space self.pdf_context.scale(txt_factors[0], txt_factors[1]) PangoCairo.update_layout(self.pdf_context, layout) PangoCairo.show_layout(self.pdf_context, layout) finally: self.pdf_context.restore() return self def paint_img(self, img): img_size = img.size scale_factor_x = self.pdf_size[0] / img_size[0] scale_factor_y = self.pdf_size[1] / img_size[1] scale_factor = min(scale_factor_x, scale_factor_y) img_surface = self.core.call_success( "pillow_to_surface", img, intermediate="jpeg", quality=int(self.quality * 100) ) self.pdf_context.save() try: self.pdf_context.identity_matrix() self.pdf_context.scale(scale_factor, scale_factor) self.pdf_context.set_source_surface(img_surface.surface) self.pdf_context.paint() finally: self.pdf_context.restore() def next_page(self): self.pdf_context.show_page() def finish(self): self.pdf_surface.flush() self.pdf_surface.finish() self.file_descriptor.close() class PagesToPdfUrlExportPipe(AbstractExportPipe): def __init__(self, core): super().__init__( name="generated_pdf", input_type=ExportDataType.IMG_BOXES, output_type=ExportDataType.OUTPUT_URL_FILE ) self.core = core self.can_change_quality = True self.can_change_page_format = True def set_page_format(self, page_format): self.page_format = page_format def get_promise(self, result='final', target_file_url=None): def do(input_data, target_file_url): if target_file_url is None: (target_file_url, file_desc) = self.core.call_success( "fs_mktemp", prefix="paperwork-export-", suffix=".pdf", mode="w", on_disk=True ) # we need the file name, not the file descriptor file_desc.close() last_doc = None doc_idx = 0 out = target_file_url list_img_boxes = input_data.iter(ExportDataType.IMG_BOXES) creator = self.core.call_one( "mainloop_execute", PdfCreator, self.core, target_file_url, self.page_format, self.quality ) out_files = [] for (doc_set, (doc, (page, img_boxes))) in list_img_boxes: if doc.data[1] != last_doc and last_doc is not None: # another doc, another output file, another PDFCreator self.core.call_one("mainloop_execute", creator.finish) out_files.append(out) doc_idx += 1 out = target_file_url.rsplit(".", 1) out = "{}_{}.{}".format(out[0], doc_idx, out[1]) creator = self.core.call_one( "mainloop_execute", PdfCreator, self.core, out, self.page_format, self.quality ) last_doc = doc.data[1] # PDFCreator is not thread-safe. We can not keep # the UI blocked for so long --> give some CPU time back to # the UI time.sleep(0.1) (img, boxes) = img_boxes.data self.core.call_one( "mainloop_execute", creator.set_page_size, img.size ) self.core.call_one( "mainloop_execute", creator.paint_txt, boxes, img.size ) img_size = img.size img_size = ( int(self.quality * img_size[0]), int(self.quality * img_size[1]) ) img = img.resize(img_size, PIL.Image.ANTIALIAS) self.core.call_one("mainloop_execute", creator.paint_img, img) self.core.call_one("mainloop_execute", creator.next_page) self.core.call_one("mainloop_execute", creator.finish) out_files.append(out) return out_files return openpaperwork_core.promise.ThreadedPromise( self.core, do, kwargs={'target_file_url': target_file_url} ) def get_output_mime(self): return ("application/pdf", ("pdf",)) def __str__(self): return _("Generated PDF(s)") class Plugin(AbstractExportPipePlugin): def init(self, core): super().init(core) self.pipes = [ PdfDocUrlToPdfUrlExportPipe(core), PagesToPdfUrlExportPipe(core), ] def get_interfaces(self): return super().get_interfaces() + ['chkdeps'] def get_deps(self): return super().get_deps() + [ { 'interface': 'pillow_to_surface', 'defaults': ['paperwork_backend.cairo.pillow'], }, ] def chkdeps(self, out: dict): if not CAIRO_AVAILABLE: out['cairo'].update(openpaperwork_core.deps.CAIRO) if not GI_AVAILABLE: out['gi'].update(openpaperwork_core.deps.GI) if not PANGO_AVAILABLE: out['pango'].update(openpaperwork_core.deps.PANGO) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/docexport/pillowfight.py000066400000000000000000000036171417573700700303270ustar00rootroot00000000000000import logging import pillowfight from . import ( AbstractExportPipePlugin, AbstractSimpleTransformExportPipe ) from .. import _ LOGGER = logging.getLogger(__name__) class UnpaperExportPipe(AbstractSimpleTransformExportPipe): def __init__(self, core): super().__init__(core, "unpaper") def transform(self, img): # Unpaper algorithms in Unpaper's order img = pillowfight.unpaper_blackfilter(img) img = pillowfight.unpaper_noisefilter(img) img = pillowfight.unpaper_blurfilter(img) img = pillowfight.unpaper_masks(img) img = pillowfight.unpaper_grayfilter(img) img = pillowfight.unpaper_border(img) return img def __str__(self): return _("Soft simplification") class SwtExportPipe(AbstractSimpleTransformExportPipe): def __init__(self, core, swt_output_type): super().__init__( core, "swt_soft" if swt_output_type == pillowfight.SWT_OUTPUT_ORIGINAL_BOXES else "swt_hard" ) self.swt_output_type = swt_output_type def transform(self, pil_img): pil_img = pil_img.convert("L") return pillowfight.swt(pil_img, output_type=self.swt_output_type) def __str__(self): if self.swt_output_type == pillowfight.SWT_OUTPUT_ORIGINAL_BOXES: return _("Hard simplification") else: return _("Extreme simplification") class Plugin(AbstractExportPipePlugin): def init(self, core): super().init(core) self.pipes = [ UnpaperExportPipe(core), SwtExportPipe(core, pillowfight.SWT_OUTPUT_ORIGINAL_BOXES), SwtExportPipe(core, pillowfight.SWT_OUTPUT_BW_TEXT), ] def get_deps(self): return [ { 'interface': 'mainloop', 'defaults': 'openpaperwork_gtk.mainloop.glib', }, ] paperwork-2.1.1/paperwork-backend/src/paperwork_backend/docimport/000077500000000000000000000000001417573700700254075ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/docimport/__init__.py000066400000000000000000000121151417573700700275200ustar00rootroot00000000000000import collections import openpaperwork_core.promise class FileImport(object): """ Used as both input and output for importer objects. Users may import many files at once and files may not be imported at all (for example if they were already imported) --> FileImport objects indicate what must still be done, what has been done, and what has not been done at all. """ def __init__(self, file_uris_to_import, active_doc_id=None, data=None): self.active_doc_id = active_doc_id # those attributes will be updated by importers self.ignored_files = list(file_uris_to_import) self.imported_files = set() self.new_doc_ids = set() self.upd_doc_ids = set() self.stats = collections.defaultdict(lambda: 0) class BaseFileImporter(object): def __init__(self, core, file_import, single_file_importer_factory): self.core = core self.file_import = file_import self.single_file_importer_factory = single_file_importer_factory self.to_import = list(self._get_importables()) def get_name(self): return self.single_file_importer_factory.get_name() def can_import(self): return len(self.to_import) > 0 def get_required_data(self): """ Possible requirements: - "password": Document is password-protected. We need the password to import it. Returns: {file_uri: {requirement_a, requirement_b, etc}} """ out = {} for (orig_url, file_url) in self.to_import: required = self.single_file_importer_factory.get_required_data( file_url ) if len(required) <= 0: continue out[file_url] = required return out @staticmethod def _upd_file_import(imported, file_import, orig_uri, file_uri): try: file_import.ignored_files.remove(orig_uri) except ValueError: pass if imported: file_import.imported_files.add(file_uri) else: file_import.ignored_files.append(file_uri) def _make_transactions(self, file_import): transactions = [] self.core.call_all( "doc_transaction_start", transactions, len(file_import.new_doc_ids) + len(file_import.upd_doc_ids) ) transactions.sort(key=lambda transaction: -transaction.priority) return transactions def _do_transactions(self, transactions, file_import): for doc_id in file_import.new_doc_ids: for transaction in transactions: transaction.add_doc(doc_id) for doc_id in file_import.upd_doc_ids: for transaction in transactions: transaction.upd_doc(doc_id) for transaction in transactions: transaction.commit() def get_import_promise(self, data=None): """ Return a promise with all the steps required to import files specified in `file_import` (see constructor), transactions included. Must be scheduled with 'transaction_manager.transaction_schedule()'. Arguments: data are extra data that may be required (see `get_required_data()`) """ if data is None: data = {} promise = openpaperwork_core.promise.Promise(self.core) for (orig_uri, file_uri) in self.to_import: promise = promise.then( self.single_file_importer_factory.make_importer( self.file_import, file_uri, data ).get_promise() ) promise = promise.then( self._upd_file_import, self.file_import, orig_uri, file_uri ) promise = promise.then(self._make_transactions, self.file_import) promise = promise.then( openpaperwork_core.promise.ThreadedPromise( self.core, self._do_transactions, args=(self.file_import,) ) ) return promise.then( self.core.call_all, "on_import_done", self.file_import ) class DirectFileImporter(BaseFileImporter): """ Designed to import only explicitly selected files """ def _get_importables(self): factory = self.single_file_importer_factory for file_uri in self.file_import.ignored_files: if factory.is_importable(self.core, file_uri): yield (file_uri, file_uri) class RecursiveFileImporter(BaseFileImporter): """ Assume files to import are actually directories. Look inside those directories to find files to import. """ def _get_importables(self): factory = self.single_file_importer_factory for dir_uri in self.file_import.ignored_files: if not self.core.call_success("fs_isdir", dir_uri): continue for file_uri in self.core.call_success("fs_recurse", dir_uri): if factory.is_importable(self.core, file_uri): yield (dir_uri, file_uri) def get_name(self): return self.single_file_importer_factory.get_recursive_name() paperwork-2.1.1/paperwork-backend/src/paperwork_backend/docimport/converted.py000066400000000000000000000112031417573700700277470ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.promise from . import ( DirectFileImporter, FileImport, RecursiveFileImporter ) from .. import _ LOGGER = logging.getLogger(__name__) class SingleDocImporter(object): def __init__(self, plugin, file_import, src_file_uri): self.plugin = plugin self.core = plugin.core self.file_import = file_import self.src_file_uri = src_file_uri self.doc_id = None self.doc_url = None def _basic_import(self, file_url): file_hash = self.core.call_success("fs_hash", file_url) other_doc_id = self.core.call_success( "index_get_doc_id_by_hash", file_hash ) if other_doc_id is not None: LOGGER.info("%s has already been imported", file_url) self.file_import.stats[_("Already imported")] += 1 return False LOGGER.info("Importing %s", file_url) (self.doc_id, self.doc_url) = self.core.call_success( "doc_convert_and_import", file_url ) self.file_import.new_doc_ids.add(self.doc_id) self.file_import.stats[_("Documents")] += 1 mime = self.core.call_success("fs_get_mime", file_url) if mime is not None: self.file_import.stats[self.plugin.file_types_by_mime[mime]] += 1 elif "." in file_url: file_ext = file_url.rsplit(".", 1)[-1].lower() self.file_import.stats[ self.plugin.file_types_by_ext[file_ext][1] ] += 1 return True def get_promise(self): return openpaperwork_core.promise.ThreadedPromise( self.core, self._basic_import, args=(self.src_file_uri,) ) class SingleDocImporterFactory(object): def __init__(self, plugin): self.plugin = plugin self.core = plugin.core @staticmethod def get_name(): return _("Import office document") @staticmethod def get_recursive_name(): return _("Import office documents recursively") def is_importable(self, core, file_url): mime = core.call_success("fs_get_mime", file_url) if mime is not None: return mime in self.plugin.file_types_by_mime if "." not in file_url: return False file_ext = file_url.rsplit(".", 1)[-1].lower() return file_ext in self.plugin.file_types_by_ext def get_required_data(self, file_uri): return set() def make_importer(self, file_import, file_uri, data): return SingleDocImporter(self.plugin, file_import, file_uri) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.file_types_by_ext = {} self.file_types_by_mime = {} def get_interfaces(self): return ["import"] def get_deps(self): return [ { "interface": "doc_convert_and_import", "defaults": ["paperwork_backend.model.converted"], }, { "interface": "doc_converter", "defaults": ["paperwork_backend.converter.libreoffice"], }, { "interface": "fs", "defaults": ["openpaperwork_gtk.fs.gio"], }, { "interface": "mainloop", "defaults": ["openpaperwork_gtk.mainloop.glib"], }, { "interface": "thread", "defaults": ["openpaperwork_core.thread.simple"], }, ] def init(self, core): super().init(core) file_types = set() self.core.call_all("converter_get_file_types", file_types) self.file_types_by_ext = { file_ext: (mime_type, human_name) for (mime_type, file_ext, human_name) in file_types } self.file_types_by_mime = { mime_type: human_name for (mime_type, file_ext, human_name) in file_types } def get_import_mime_types(self, out: list): for (mime, human_desc) in self.file_types_by_ext.values(): out.add((human_desc, mime)) if len(self.file_types_by_ext) > 0: out.add((_("Office document folder"), "inode/directory")) def get_importer(self, out: list, file_import: FileImport): importer = DirectFileImporter( self.core, file_import, SingleDocImporterFactory(self) ) if importer.can_import(): out.append(importer) importer = RecursiveFileImporter( self.core, file_import, SingleDocImporterFactory(self) ) if importer.can_import(): out.append(importer) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/docimport/img.py000066400000000000000000000115431417573700700265410ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.promise from . import ( DirectFileImporter, FileImport, RecursiveFileImporter ) from .. import _ LOGGER = logging.getLogger(__name__) class SingleImgImporter(object): def __init__(self, factory, file_import, src_file_uri): self.factory = factory self.core = factory.core self.file_import = file_import self.src_file_uri = src_file_uri self.doc_id = None self.doc_url = None def _append_file_to_doc(self, file_url, doc_id=None): if doc_id is None: # new document (doc_id, doc_url) = self.core.call_success("storage_get_new_doc") else: # update existing one doc_url = self.core.call_success("doc_id_to_url", doc_id) nb_pages = self.core.call_success("doc_get_nb_pages_by_url", doc_url) if nb_pages is None: nb_pages = 0 # Makes sure it's actually an image and convert it to the expected # format img = self.core.call_success("url_to_pillow", file_url) if img is None: LOGGER.error("Failed to load image %s", file_url) return (None, None) page_url = self.core.call_success( "page_get_img_url", doc_url, nb_pages, write=True ) self.core.call_success("pillow_to_url", img, page_url) return (doc_id, doc_url) def _basic_import(self, file_uri): (self.doc_id, self.doc_url) = self._append_file_to_doc( file_uri, self.file_import.active_doc_id ) if self.doc_id is None: return False self.file_import.stats[_("Images")] += 1 if self.file_import.active_doc_id is None: self.file_import.new_doc_ids.add(self.doc_id) self.file_import.stats[_("Documents")] += 1 else: self.file_import.upd_doc_ids.add(self.doc_id) self.file_import.stats[_("Pages")] += 1 self.file_import.active_doc_id = self.doc_id return True def get_promise(self): return openpaperwork_core.promise.ThreadedPromise( self.core, self._basic_import, args=(self.src_file_uri,) ) class SingleImgImporterFactory(object): def __init__(self, plugin): self.plugin = plugin self.core = plugin.core @staticmethod def get_name(): return _("Append the image to the current document") @staticmethod def get_recursive_name(): return _( "Find the images recursively and import them to the current" " document" ) def is_importable(self, core, file_uri): mime = core.call_success("fs_get_mime", file_uri) if mime is not None: mimes = [mime[1] for mime in Plugin.IMG_MIME_TYPES] if mime in mimes: return True return False file_ext = file_uri.split(".")[-1].lower() if file_ext in self.plugin.FILE_EXTENSIONS: return True def get_required_data(self, file_uri): return set() def make_importer(self, file_import, file_uri, data): return SingleImgImporter(self, file_import, file_uri) class Plugin(openpaperwork_core.PluginBase): IMG_MIME_TYPES = [ ("BMP", "image/x-ms-bmp"), ("GIF", "image/gif"), ("JPEG", "image/jpeg"), ("PNG", "image/png"), ("TIFF", "image/tiff"), ] FILE_EXTENSIONS = [ "bmp", "gif", "jpeg", "jpg", "png", "tiff", ] def __init__(self): super().__init__() def get_interfaces(self): return [ "import" ] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'page_img', 'defaults': ['paperwork_backend.model.img'], }, { 'interface': 'pillow', 'defaults': ['openpaperwork_core.pillow.img'], }, { 'interface': 'thread', 'defaults': ['openpaperwork_core.thread.simple'], }, ] def get_import_mime_types(self, out: set): out.update(self.IMG_MIME_TYPES) def get_importer(self, out: list, file_import: FileImport): importer = DirectFileImporter( self.core, file_import, SingleImgImporterFactory(self) ) if importer.can_import(): out.append(importer) importer = RecursiveFileImporter( self.core, file_import, SingleImgImporterFactory(self) ) if importer.can_import(): out.append(importer) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/docimport/pdf.py000066400000000000000000000077101417573700700265370ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.promise from . import ( DirectFileImporter, FileImport, RecursiveFileImporter ) from .. import _ LOGGER = logging.getLogger(__name__) class SinglePdfImporter(object): def __init__(self, core, file_import, src_file_uri, data): self.core = core self.file_import = file_import self.src_file_uri = src_file_uri self.data = data self.doc_id = None self.doc_url = None def _basic_import(self, file_uri): file_hash = self.core.call_success("fs_hash", file_uri) other_doc_id = self.core.call_success( "index_get_doc_id_by_hash", file_hash ) if other_doc_id is not None: LOGGER.info("%s has already been imported", file_uri) self.file_import.stats[_("Already imported")] += 1 return False LOGGER.info("Importing %s", file_uri) (self.doc_id, self.doc_url) = self.core.call_success( "doc_pdf_import", file_uri, password=self.data['password'] if 'password' in self.data else None ) if self.doc_id is None: return False self.file_import.new_doc_ids.add(self.doc_id) self.file_import.stats[_("PDF")] += 1 self.file_import.stats[_("Documents")] += 1 return True def get_promise(self): return openpaperwork_core.promise.ThreadedPromise( self.core, self._basic_import, args=(self.src_file_uri,) ) class SinglePdfImporterFactory(object): def __init__(self, plugin): self.plugin = plugin self.core = plugin.core @staticmethod def get_name(): return _("Import PDF") @staticmethod def get_recursive_name(): return _("Import PDFs recursively") def is_importable(self, core, file_uri): mime = core.call_success("fs_get_mime", file_uri) if mime is not None: if mime == Plugin.MIME_TYPE: return True return False if file_uri.lower().endswith(self.plugin.FILE_EXTENSION): return True def get_required_data(self, file_uri): try: self.core.call_success("poppler_open", file_uri, password=None) LOGGER.info("%s: not password protected", file_uri) return set() except Exception: # XXX(Jflesch): there is no specific exception type ... :/ LOGGER.info("%s: password protected", file_uri) return {"password"} def make_importer(self, file_import, file_uri, data): return SinglePdfImporter(self.core, file_import, file_uri, data) class Plugin(openpaperwork_core.PluginBase): FILE_EXTENSION = ".pdf" MIME_TYPE = "application/pdf" def get_interfaces(self): return [ "import" ] def get_deps(self): return [ { 'interface': 'doc_pdf_import', 'defaults': ['paperwork_backend.model.pdf'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'thread', 'defaults': ['openpaperwork_core.thread.simple'], }, ] def get_import_mime_types(self, out: set): out.add(("PDF", self.MIME_TYPE)) out.add((_("PDF folder"), "inode/directory")) def get_importer(self, out: list, file_import: FileImport): importer = DirectFileImporter( self.core, file_import, SinglePdfImporterFactory(self) ) if importer.can_import(): out.append(importer) importer = RecursiveFileImporter( self.core, file_import, SinglePdfImporterFactory(self) ) if importer.can_import(): out.append(importer) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/docscan/000077500000000000000000000000001417573700700250215ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/docscan/__init__.py000066400000000000000000000000001417573700700271200ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/docscan/autoselect_scanner.py000066400000000000000000000033571417573700700312640ustar00rootroot00000000000000""" Select a scanner if none has been selected yet. """ import logging import Levenshtein import openpaperwork_core LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'scan', 'defaults': ['paperwork_backend.docscan.libinsane'], }, ] def init(self, core): super().init(core) promise = self.core.call_success("scan_list_scanners_promise") promise = promise.then(self._check_dev_id) self.core.call_success("scan_schedule", promise) def _check_dev_id(self, devs): devs = [dev[0] for dev in devs] active = self.core.call_success("config_get", "scanner_dev_id") if active is not None and active in devs: LOGGER.info("Scanner '%s' found. Nothing to do", active) return LOGGER.info("Scanner '%s' not found", active) LOGGER.info("Available scanners: %s", devs) if len(devs) <= 0: active = None elif active is None: # pick a scanner at random. active = devs[0] else: LOGGER.info("Previously selected scanner: %s", active) # look for the closest scanner ID devs = [ (Levenshtein.distance(dev, active), dev) for dev in devs ] devs.sort() active = devs[0][1] LOGGER.info("Selected scanner: %s", active) self.core.call_success("config_put", "scanner_dev_id", active) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/docscan/fake.py000066400000000000000000000117521417573700700263070ustar00rootroot00000000000000import itertools import PIL.Image import openpaperwork_core import openpaperwork_core.promise SCAN_ID_GENERATOR = itertools.count() class Source(object): def __init__(self, core, scanner, source_id): self.core = core self.scanner = scanner self.source_id = source_id def __str__(self): return "{}:{}".format(str(self.scanner), self.source_id) def set_as_default(self): raise NotImplementedError() def get_resolutions_promise(self): def get_resolutions(): return [25, 100, 200, 300, 400, 500, 600] return openpaperwork_core.promise.Promise(self.core, get_resolutions) def set_default_resolution(self, resolution): raise NotImplementedError() def scan(self, scan_id=None, resolution=None, max_pages=9999): if scan_id is None: scan_id = next(SCAN_ID_GENERATOR) test_chunk = PIL.Image.new("RGB", (100, 200), (171, 205, 239)) test_page = PIL.Image.new("RGB", (200, 200), (171, 205, 239)) self.core.call_one( "mainloop_schedule", self.core.call_all, "on_scan_feed_start", scan_id ) self.core.call_one( "mainloop_schedule", self.core.call_all, "on_scan_page_start", scan_id, 0, None, # TODO(Jflesch): scan params ) self.core.call_one( "mainloop_schedule", self.core.call_all, "on_scan_chunk", scan_id, None, # TODO(Jflesch): scan_params test_chunk ) self.core.call_one( "mainloop_schedule", self.core.call_all, "on_scan_chunk", scan_id, None, # TODO(Jflesch): scan_params test_chunk ) self.core.call_one( "mainloop_schedule", self.core.call_all, "on_scan_chunk", scan_id, None, # TODO(Jflesch): scan_params test_chunk ) self.core.call_one( "mainloop_schedule", self.core.call_all, "on_scan_page_end", scan_id, 0, test_page ) self.core.call_one( "mainloop_schedule", self.core.call_all, "on_scan_feed_end", scan_id ) return (self, scan_id, [test_page]) def scan_promise(self, *args, scan_id=None, **kwargs): if scan_id is None: scan_id = next(SCAN_ID_GENERATOR) kwargs['scan_id'] = scan_id return (scan_id, openpaperwork_core.promise.ThreadedPromise( self.core, self.scan, args=args, kwargs=kwargs )) def close(self, *args, **kwargs): pass class Scanner(object): def __init__(self, core, scanner_id): self.core = core self.dev_id = scanner_id def __str__(self): return self.dev_id def close(self, *args, **kwargs): pass def get_sources(self): return { "fake_source0": Source(self.core, self, "fake_source0"), "fake_source1": Source(self.core, self, "fake_source1"), } def get_sources_promise(self): return openpaperwork_core.promise.Promise(self.core, self.get_sources) def get_source(self, source_id): return self.get_sources()[source_id] def set_as_default(self): raise NotImplementedError() class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ["scan"] def get_deps(self): return [ { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'thread', 'defaults': ['openpaperwork_core.thread.simple'], }, ] def scan_list_scanners_promise(self): def list_scanners(): return [ ("fake:scanner0", "Super scanner #0"), ("fake:scanner1", "Super scanner #1"), ] return openpaperwork_core.promise.Promise(self.core, list_scanners) def scan_get_scanner_promise(self, dev_id): def get_scanner(): if dev_id != "fake:scanner0" and dev_id != "fake:scanner1": return None return Scanner(self.core, dev_id) return openpaperwork_core.promise.Promise(self.core, get_scanner) def scan_promise(self, *args, **kwargs): scan_id = next(SCAN_ID_GENERATOR) promise = self.scan_get_scanner_promise("fake:scanner0") promise = promise.then( openpaperwork_core.promise.Promise( self.core, Scanner.get_source, args=("fake_source0",) ) ) promise = promise.then( openpaperwork_core.promise.ThreadedPromise( self.core, Source.scan, args=(scan_id,) ) ) def close(args): (source, scan_id, imgs) = args source.close() return (source, scan_id, imgs) promise = promise.then(close) return (scan_id, promise) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/docscan/libinsane.py000066400000000000000000000627701417573700700273530ustar00rootroot00000000000000import itertools import json import logging import threading try: import gi gi.require_version('Libinsane', '1.0') from gi.repository import GObject GI_AVAILABLE = True except (ImportError, ValueError): GI_AVAILABLE = False # dummy so chkdeps can still be called class GObject(object): class GObject(object): pass try: from gi.repository import Libinsane LIBINSANE_AVAILABLE = True except (ImportError, ValueError): LIBINSANE_AVAILABLE = False # dummy so chkdeps can still be called class Libinsane(object): class LogLevel(object): DEBUG = 0 INFO = 1 WARNING = 2 ERROR = 3 class Logger(object): pass import PIL import PIL.Image import openpaperwork_core import openpaperwork_core.deps import openpaperwork_core.promise from .. import _ LOGGER = logging.getLogger(__name__) SCAN_ID_GENERATOR = itertools.count() # Prevent closing or other operations on scanner and source instances when # they are being used (scanning) LOCK = threading.RLock() class LibinsaneLogger(GObject.GObject, Libinsane.Logger): LEVELS = { Libinsane.LogLevel.DEBUG: LOGGER.debug, Libinsane.LogLevel.INFO: LOGGER.info, Libinsane.LogLevel.WARNING: LOGGER.warning, Libinsane.LogLevel.ERROR: LOGGER.error, } min_level = Libinsane.LogLevel.DEBUG def do_log(self, lvl, msg): if lvl < self.min_level: return self.LEVELS[lvl]("[LibInsane] " + msg) def raw_to_img(params, img_bytes): fmt = params.get_format() assert(fmt == Libinsane.ImgFormat.RAW_RGB_24) (w, h) = ( params.get_width(), int(len(img_bytes) / 3 / params.get_width()) ) mode = "RGB" return PIL.Image.frombuffer(mode, (w, h), img_bytes, "raw", mode, 0, 1) class ImageAssembler(object): MIN_CHUNK_SIZE = 64 * 1024 def __init__(self, line_width): # 'Pieces' are pieces of the images that may or may not contain # full lines of pixels (or even partial pixel) # 'chunk': We want to provide the GUI with a preview of the scan. # To keep things simple, we provide chunk of the image and those # chunk must contain only entire lines of pixels. # and then we must also provide the whole image at the end. self.w = line_width # in bytes self.current_piece = [] self.all_chunks = [] def add_piece(self, piece): self.current_piece.append(piece) lcurrent_piece = sum((len(c) for c in self.current_piece)) if lcurrent_piece < self.w or lcurrent_piece < self.MIN_CHUNK_SIZE: return current_piece = b"".join(self.current_piece) chunkable = lcurrent_piece - (lcurrent_piece % self.w) new_chunk = current_piece[:chunkable] new_piece = current_piece[chunkable:] self.all_chunks.append(new_chunk) if len(new_piece) <= 0: self.current_piece = [] else: self.current_piece = [new_piece] def get_last_chunk(self): if len(self.all_chunks) <= 0: return None return self.all_chunks[-1] def get_image(self): return b"".join(self.all_chunks) + b"".join(self.current_piece) class Source(object): def __init__(self, core, scanner, source): self.core = core self.scanner = scanner self.source_id = source.get_name() self.source = source def __str__(self): return "{}:{}".format(str(self.scanner), self.source_id) def set_as_default(self): self.core.call_all( "config_put", "scanner_source_id", self.source_id ) def _get_option(self, name): LOGGER.info( "Looking for possible values for option '%s' on %s" " : %s ...", name, str(self.scanner), self.source_id ) options = self.source.get_options() options = {opt.get_name(): opt for opt in options} return options[name] def _get_opt_constraint(self, name): with LOCK: opt = self._get_option(name) constraint = opt.get_constraint() LOGGER.info( "%s : %s : %s : Possible values: %s", str(self.scanner), self.source_id, name, constraint ) return constraint def get_modes(self): return self._get_opt_constraint('mode') def get_resolutions(self): return self._get_opt_constraint('resolution') def get_resolutions_promise(self): return openpaperwork_core.promise.ThreadedPromise( self.core, self.get_resolutions ) def set_default_resolution(self, resolution): self.core.call_all( "config_put", "scanner_resolution", int(resolution) ) def scan( self, scan_id=None, resolution=None, max_pages=9999, close_on_end=False ): """ Returns the source, the scan ID and an image generator """ with LOCK: if scan_id is None: scan_id = next(SCAN_ID_GENERATOR) LOGGER.info("(id=%s) Setting scan options ...", scan_id) if resolution is None: resolution = self.core.call_success( "config_get", "scanner_resolution" ) mode = self.core.call_success("config_get", "scanner_mode") options = self.source.get_options() opts = {opt.get_name(): opt for opt in options} if 'resolution' in opts: opts['resolution'].set_value(resolution) if 'mode' in opts: try: opts['mode'].set_value(mode) except Exception as exc: LOGGER.warning( "Failed to set scan mode", exc_info=exc ) # will try to scan anyway imgs = self._scan(scan_id, resolution, max_pages, close_on_end) return (self, scan_id, imgs) def _scan(self, scan_id, resolution, max_pages, close_on_end=False): """ Returns an image generator """ # keep in mind that we are in a thread here, but listeners # must be called from the main loop LOGGER.info( "(id=%s) Scanning at resolution %d dpi ...", scan_id, resolution ) has_started = False session = None try: page_nb = 0 self.core.call_one( "mainloop_execute", self.core.call_all, "on_scan_feed_start", scan_id ) self.core.call_success( "mainloop_schedule", self.core.call_all, "on_progress", "scan", 0.0, _("Starting scan ...") ) session = self.source.scan_start() while not session.end_of_feed() and page_nb < max_pages: self.core.call_success( "mainloop_schedule", self.core.call_all, "on_progress", "scan", 0.0, _("Scanning page %d ...") % (page_nb + 1) ) scan_params = session.get_scan_parameters() LOGGER.info( "Expected scan parameters: %s ; %dx%d = %d bytes", scan_params.get_format(), scan_params.get_width(), scan_params.get_height(), scan_params.get_image_size() ) self.core.call_success( "mainloop_schedule", self.core.call_all, "on_scan_page_start", scan_id, page_nb, scan_params ) assert( scan_params.get_format() == Libinsane.ImgFormat.RAW_RGB_24 ) image = ImageAssembler(scan_params.get_width() * 3) last_chunk = None nb_lines = 0 total_lines = scan_params.get_height() buffer_size = (scan_params.get_width() * 3) + 1 LOGGER.info("Scanning page %d/%d ...", page_nb, max_pages) while not session.end_of_page(): new_piece = session.read_bytes(buffer_size).get_data() if not has_started: # Mark the application as busy until we get the first # read(). This is the only reliable time to be # sure scanning is actually started. self.core.call_success( "mainloop_schedule", self.core.call_all, "on_scan_started", scan_id ) has_started = True image.add_piece(new_piece) chunk = image.get_last_chunk() if chunk is not last_chunk: last_chunk = chunk pil = raw_to_img(scan_params, chunk) nb_lines += pil.size[1] progress = nb_lines / total_lines if progress >= 1.0: progress = 0.999 self.core.call_success( "mainloop_schedule", self.core.call_all, "on_progress", "scan", progress, _("Scanning page %d ...") % (page_nb + 1) ) self.core.call_success( "mainloop_schedule", self.core.call_all, "on_scan_chunk", scan_id, scan_params, pil ) LOGGER.info("Page %d/%d scanned", page_nb, max_pages) self.core.call_success( "mainloop_schedule", self.core.call_all, "on_progress", "scan", 0.999, _("Scanning page %d ...") % (page_nb + 1) ) img = raw_to_img(scan_params, image.get_image()) yield img self.core.call_success( "mainloop_schedule", self.core.call_all, "on_scan_page_end", scan_id, page_nb, img ) page_nb += 1 LOGGER.info("End of feed") self.core.call_success( "mainloop_schedule", self.core.call_all, "on_scan_feed_end", scan_id ) finally: self.core.call_success( "mainloop_schedule", self.core.call_all, "on_progress", "scan", 1.0 ) if session is not None: session.cancel() if close_on_end: self.close() def scan_promise(self, *args, scan_id=None, **kwargs): if scan_id is None: scan_id = next(SCAN_ID_GENERATOR) kwargs['scan_id'] = scan_id return ( scan_id, openpaperwork_core.promise.ThreadedPromise( self.core, self.scan, args=args, kwargs=kwargs ) ) def close(self, *args, **kwargs): with LOCK: self.scanner.close() # return the args for convience when used with promises if len(args) == 1 and len(kwargs) == 0: return args return (args, kwargs) class Scanner(object): def __init__(self, core, scanner): self.core = core self.dev_id = scanner.get_name() self.dev = scanner self.sources = None # WORKAROUND(Jflesch): just to keep ref on them def __str__(self): return self.dev_id def __del__(self): if self.dev is not None: # Shouldn't happen. LOGGER.warning( "Scanner(%s, %s) is being garbage-collected", self.dev_id, id(self) ) self.close() def close(self, *args, **kwargs): with LOCK: if self.dev is not None: LOGGER.info("Closing device %s (%s)", self.dev_id, id(self)) self.dev.close() self.dev = None # return the args for convenience when used with promises if len(args) == 1 and len(kwargs) == 0: return args return (args, kwargs) def get_sources(self): with LOCK: LOGGER.info("Looking for scan sources on %s ...", self.dev_id) sources = self.dev.get_children() sources = [ Source(self.core, self, source) for source in sources ] self.sources = { source.source_id: source for source in sources } LOGGER.info("%d sources found: %s", len(sources), sources) return self.sources def get_sources_promise(self): return openpaperwork_core.promise.ThreadedPromise( self.core, self.get_sources ) def get_source(self, source_id): sources = self.get_sources() src = sources[source_id] return src def set_as_default(self): self.core.call_all( "config_put", "scanner_dev_id", 'libinsane:' + self.dev_id ) class BugReportCollector(object): def __init__(self, plugin, update_args): self.core = plugin.core self.plugin = plugin self.update_args = update_args def _notify(self, msg): self.core.call_success( "mainloop_schedule", self.core.call_all, "bug_report_update_attachment", "scanner", {"file_url": msg}, *self.update_args ) @staticmethod def _get_error_proof(func): try: r = func() if type(r) is bool or type(r) is int or type(r) is float: return r return str(r) except Exception as exc: return str(exc) def _collect_opt_info(self, opt, out: dict): out['name'] = self._get_error_proof(opt.get_name) out['title'] = self._get_error_proof(opt.get_title) out['description'] = self._get_error_proof(opt.get_desc) out['capabilities'] = self._get_error_proof(opt.get_capabilities) out['unit'] = self._get_error_proof(opt.get_value_unit) out['constraint_type'] = self._get_error_proof(opt.get_constraint_type) out['constraint'] = self._get_error_proof(opt.get_constraint) out['is_readable'] = self._get_error_proof(opt.is_readable) out['is_writable'] = self._get_error_proof(opt.is_writable) out['value'] = self._get_error_proof(opt.get_value) def _collect_item_info(self, item, base_name, out: dict): if item is None or item is False: out['reachable'] = False return out['reachable'] = True try: name = item.get_name() if base_name is not None and base_name != "": name = base_name + "/" + name self._notify(_("Examining %s") % name) out['name'] = item.get_name() out['options'] = {} out['children'] = {} options = item.get_options() for opt in options: out['options'][opt.get_name()] = {} self._collect_opt_info(opt, out['options'][opt.get_name()]) children = item.get_children() for child in children: out['children'][child.get_name()] = {} self._collect_item_info( child, name, out['children'][child.get_name()] ) finally: item.close() def _write_scanners_info_to_tmp_file(self, infos): infos = json.dumps( infos, indent=4, separators=(",", ": "), sort_keys=True ) (file_url, fd) = self.core.call_success( "fs_mktemp", prefix="scanner_", suffix=".json", mode="w", on_disk=True ) with fd: fd.write(infos) return file_url @staticmethod def _collect_get_device(libinsane, dev_id): try: return libinsane.get_device(dev_id) except Exception as exc: LOGGER.error("Failed to get device [%s]", dev_id, exc_info=exc) return False # can't return None or it will be ignored def _collect_all_info(self, scanners): out = {} promise = openpaperwork_core.promise.Promise(self.core) for dev in scanners: dev_id = dev[0] dev_name = dev[1] out[dev_id] = { 'listing_name': dev_name } promise = promise.then( openpaperwork_core.promise.ThreadedPromise( self.core, self._collect_get_device, args=(self.plugin.libinsane, dev_id[len("libinsane:"):],) ) ) promise = promise.then(openpaperwork_core.promise.ThreadedPromise( self.core, self._collect_item_info, args=("", out[dev_id],) )) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then( openpaperwork_core.promise.ThreadedPromise( self.core, self._write_scanners_info_to_tmp_file, args=(out,) ) ) promise = promise.then( lambda file_url: self.core.call_all( "bug_report_update_attachment", "scanner", { 'file_url': file_url, 'file_size': self.core.call_success( 'fs_getsize', file_url ), }, *self.update_args ) ) self.core.call_success("scan_schedule", promise) def run(self): promise = openpaperwork_core.promise.Promise( self.core, self.core.call_all, args=( "bug_report_update_attachment", "scanner", {"file_url": _("Getting scanner list ...")}, *self.update_args ) ) promise = promise.then(self.plugin.scan_list_scanners_promise()) promise = promise.then(openpaperwork_core.promise.ThreadedPromise( self.core, self._collect_all_info )) self.core.call_success("scan_schedule", promise) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() # Looking for devices twice on Linux tends to crash ... self.devices_cache = [] LOGGER.info("Initializing Libinsane ...") self.libinsane_logger = LibinsaneLogger() self.libinsane = None if LIBINSANE_AVAILABLE: Libinsane.register_logger(self.libinsane_logger) self.libinsane = Libinsane.Api.new_safebet() LOGGER.info( "Libinsane %s initialized", self.libinsane.get_version() ) self._last_scanner = None def get_interfaces(self): return [ "bug_report_attachments", "chkdeps", "scan", "stats", ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'work_queue', 'defaults': ['openpaperwork_core.work_queue.default'], }, ] def init(self, core): super().init(core) self.core.call_all("work_queue_create", "scanner") settings = { 'scanner_dev_id': self.core.call_success( "config_build_simple", "scanner", "dev_id", lambda: None ), 'scanner_source_id': self.core.call_success( "config_build_simple", "scanner", "source", lambda: None ), 'scanner_resolution': self.core.call_success( "config_build_simple", "scanner", "resolution", lambda: 300 ), 'scanner_mode': self.core.call_success( "config_build_simple", "scanner", "mode", lambda: "Color" ), } for (k, setting) in settings.items(): self.core.call_all( "config_register", k, setting ) def chkdeps(self, out: dict): if not GI_AVAILABLE: out['gi'].update(openpaperwork_core.deps.GI) if not LIBINSANE_AVAILABLE: out['libinsane'] = { 'debian': 'gir1.2-libinsane-1.0', 'linuxmint': 'gir1.2-libinsane-1.0', 'raspbian': 'gir1.2-libinsane-1.0', 'ubuntu': 'gir1.2-libinsane-1.0', } def scan_schedule(self, promise): """ Any promise or chain of promises related to scanners must *always* be run sequentially to avoid crashes. Otherwise, 2 threaded promises could run in parrallel. So other plugins using those promises should use scan_schedule() instead of mainloop_schedule() or promise.schedule(). """ self.core.call_success("work_queue_add_promise", "scanner", promise) return True def scan_list_scanners_promise(self): """ Return a promise for listing scanners. Must be started with "scan_schedule()", not "promise.schedule()" ! """ def list_scanners(*args, **kwargs): with LOCK: if len(self.devices_cache) > 0: return self.devices_cache LOGGER.info("Looking for scan devices ...") devs = self.libinsane.list_devices( Libinsane.DeviceLocations.ANY ) devs = [ # (id, human readable name) # prefix the IDs with 'libinsane:' so we know it comes from # our plugin and not another scan plugin ( 'libinsane:' + dev.get_dev_id(), "{} {}".format( dev.get_dev_vendor(), dev.get_dev_model() ), dev.get_dev_vendor(), dev.get_dev_model(), ) for dev in devs ] devs.sort(key=lambda s: s[1]) LOGGER.info("%d devices found: %s", len(devs), devs) return devs def set_cache(devs): self.devices_cache = devs return devs promise = openpaperwork_core.promise.ThreadedPromise( self.core, list_scanners ) promise = promise.then(set_cache) return promise def scan_get_scanner_promise(self, scanner_dev_id=None): """ Return a promise getting a scanner instance. Must be started with "scan_schedule()", not "promise.schedule()" ! """ def get_scanner(scanner_dev_id): if self._last_scanner is not None: self._last_scanner.close() self._last_scanner = None if scanner_dev_id is None: scanner_dev_id = self.core.call_success( "config_get", "scanner_dev_id" ) if scanner_dev_id is None: return None if not scanner_dev_id.startswith("libinsane:"): return None scanner_dev_id = scanner_dev_id[len("libinsane:"):] with LOCK: LOGGER.info("Accessing scanner '%s' ...", scanner_dev_id) scanner = self.libinsane.get_device(scanner_dev_id) LOGGER.info("Scanner '%s' opened", scanner.get_name()) self._last_scanner = Scanner(self.core, scanner) return self._last_scanner return openpaperwork_core.promise.ThreadedPromise( self.core, get_scanner, args=(scanner_dev_id,) ) def scan_promise(self, *args, source_id=None, **kwargs): """ Return a promise that will run a scan. Must be started with "scan_schedule()", not "promise.schedule()" ! """ scanner_dev_id = self.core.call_success( "config_get", "scanner_dev_id" ) if source_id is None: source_id = self.core.call_success( "config_get", "scanner_source_id" ) if source_id is None: raise Exception("No source defined") scan_id = next(SCAN_ID_GENERATOR) promise = self.scan_get_scanner_promise(scanner_dev_id) promise = promise.then( openpaperwork_core.promise.ThreadedPromise( self.core, Scanner.get_source, args=(source_id,) ) ) promise = promise.then( openpaperwork_core.promise.ThreadedPromise( self.core, Source.scan, args=(scan_id,), kwargs={ 'close_on_end': True } ) ) return (scan_id, promise) def bug_report_get_attachments(self, out: dict): out['scanner'] = { 'include_by_default': False, 'date': None, 'file_type': _("Scanner info."), 'file_url': _("Select to generate"), 'file_size': 0, } def on_bug_report_attachment_selected(self, attachment_id, *args): if attachment_id != 'scanner': return collector = BugReportCollector(self, args) collector.run() def stats_get(self, out: dict): if not LIBINSANE_AVAILABLE: out['libinsane_version'] = "not installed" return out['libinsane_version'] = Libinsane.Api.get_version() paperwork-2.1.1/paperwork-backend/src/paperwork_backend/docscan/scan2doc.py000066400000000000000000000120101417573700700270610ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.promise LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.scan_id_to_doc_id = {} self.doc_id_to_scan_id = {} def get_interfaces(self): return ['scan2doc'] def get_deps(self): return [ { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_core.mainloop.asyncio'], }, { 'interface': 'page_img', 'defaults': ['paperwork_backend.model.img'], }, { 'interface': 'pillow', 'defaults': ['openpaperwork_core.pillow.img'], }, { 'interface': 'scan', 'defaults': ['paperwork_backend.docscan.libinsane'], }, { 'interface': 'thread', 'defaults': ['openpaperwork_core.thread.simple'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def scan2doc_scan_id_to_doc_id(self, scan_id): try: return self.scan_id_to_doc_id[scan_id] except KeyError: return None def scan2doc_doc_id_to_scan_id(self, doc_id): try: return self.doc_id_to_scan_id[doc_id] except KeyError: return None def scan2doc_promise(self, *args, doc_id=None, doc_url=None, **kwargs): """ The promise returned by this method should be scheduled with scan_schedule() to avoid any possible conflict with another scan (or scanner lookup). """ if doc_id is not None and doc_url is not None: nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", doc_url ) new = (nb_pages <= 0) if nb_pages is not None else True else: (doc_id, doc_url) = self.core.call_success("storage_get_new_doc") new = True (scan_id, p) = self.core.call_success("scan_promise", *args, **kwargs) promise = openpaperwork_core.promise.Promise( self.core, self.core.call_all, args=("on_scan2doc_start", scan_id, doc_id, doc_url) ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(p) self.scan_id_to_doc_id[scan_id] = doc_id self.doc_id_to_scan_id[doc_id] = scan_id def add_scans_to_doc(args): (source, scan_id, imgs) = args nb = 0 for img in imgs: nb += 1 nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", doc_url ) if nb_pages is None: nb_pages = 0 page_url = self.core.call_success( "page_get_img_url", doc_url, nb_pages, write=True ) self.core.call_success("pillow_to_url", img, page_url) self.core.call_all( "mainloop_schedule", self.core.call_all, "on_scan2doc_page_scanned", scan_id, doc_id, doc_url, nb_pages ) return nb def drop_scan_id(scan_id, doc_id): self.scan_id_to_doc_id.pop(scan_id, None) self.doc_id_to_scan_id.pop(doc_id, None) def notify_end(scan_id, doc_id): self.core.call_all("on_scan2doc_end", scan_id, doc_id, doc_url) return (doc_id, doc_url) def cancel(exc, scan_id, doc_id): drop_scan_id(scan_id, doc_id) if new: self.core.call_all("storage_delete_doc_id", doc_id) raise exc def run_transactions(nb_imgs, scan_id, doc_id): # start a second promise chain, but scheduled with # "transaction_schedule()" promise = openpaperwork_core.promise.Promise( self.core, drop_scan_id, args=(scan_id, doc_id) ) promise = promise.then(self.core.call_success( "transaction_simple_promise", (("add" if new else "upd", doc_id),) )) promise = promise.then(notify_end, scan_id, doc_id) promise = promise.catch(cancel, scan_id, doc_id) self.core.call_success("transaction_schedule", promise) return (doc_id, doc_url) promise = promise.then( openpaperwork_core.promise.ThreadedPromise( self.core, add_scans_to_doc ) ) promise = promise.then( openpaperwork_core.promise.ThreadedPromise( self.core, run_transactions, args=(scan_id, doc_id) ) ) promise = promise.catch(cancel, scan_id, doc_id) return promise paperwork-2.1.1/paperwork-backend/src/paperwork_backend/doctracker.py000066400000000000000000000211151417573700700261020ustar00rootroot00000000000000""" This plugin is an helper for other plugins. It provides an easy way to detect deleted, modified or added documents when synchronizing with the work directory. """ import datetime import sqlite3 import openpaperwork_core from . import (_, sync) # Beware that we use Sqlite, but sqlite python module is not thread-safe # --> all the calls to sqlite module functions must happen on the main loop, # even those in the transactions (which are run in a thread) CREATE_TABLES = [ ( "CREATE TABLE IF NOT EXISTS documents (" " doc_id TEXT PRIMARY KEY," " text TEXT NULL," " mtime INTEGER NOT NULL" ")" ), ] ID = "doctracker" class DocTrackerTransaction(sync.BaseTransaction): def __init__(self, plugin, sql, total_expected=-1): super().__init__(plugin.core, total_expected) self.priority = -10000 self.sql = self.core.call_one( "mainloop_execute", sql.cursor ) self.core.call_one( "mainloop_execute", self.sql.execute, "BEGIN TRANSACTION" ) def _get_actual_doc_data(self, doc_id, doc_url): if ( doc_url is not None and self.core.call_success("fs_exists", doc_url) is not None ): mtime = self.core.call_success("doc_get_mtime_by_url", doc_url) if mtime is None: mtime = 0 doc_text = [] self.core.call_all("doc_get_text_by_url", doc_text, doc_url) doc_text = "\n\n".join(doc_text) else: mtime = 0 doc_text = None return { 'mtime': mtime, 'text': doc_text, } def add_doc(self, doc_id): self.notify_progress(ID, _("Document %s added") % (doc_id)) self._upd_doc(doc_id) super().add_doc(doc_id) def upd_doc(self, doc_id): self.notify_progress(ID, _("Document %s updated") % (doc_id)) self._upd_doc(doc_id) super().upd_doc(doc_id) def _upd_doc(self, doc_id): doc_url = self.core.call_success("doc_id_to_url", doc_id) actual = self._get_actual_doc_data(doc_id, doc_url) self.core.call_one( "mainloop_execute", self.sql.execute, "INSERT OR REPLACE INTO documents (doc_id, text, mtime)" " VALUES (?, ?, ?)", (doc_id, actual['text'], actual['mtime']) ) def del_doc(self, doc_id): self.notify_progress(ID, _("Document %s deleted") % (doc_id)) self.core.call_one( "mainloop_execute", self.sql.execute, "DELETE FROM documents WHERE doc_id = ?", (doc_id,) ) super().del_doc(doc_id) def unchanged_doc(self, doc_id): self.notify_progress( ID, _("Examining document %s: unchanged") % (doc_id) ) super().unchanged_doc(doc_id) def cancel(self): self.notify_progress(ID, _("Rolling back changes")) self.core.call_one("mainloop_execute", self.sql.execute, "ROLLBACK") self.core.call_one("mainloop_execute", self.sql.close) self.notify_done(ID) def commit(self): self.notify_progress(ID, _("Committing changes")) self.core.call_one("mainloop_execute", self.sql.execute, "COMMIT") self.core.call_one("mainloop_execute", self.sql.close) self.notify_done(ID) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 10000 def __init__(self): self.sql = None self.transaction_factories = [] def get_interfaces(self): return [ 'doc_tracking', 'syncable', ] def get_deps(self): return [ { 'interface': 'data_versioning', 'defaults': ['openpaperwork_core.data_versioning'], }, { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'] }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'data_dir_handler', 'defaults': ['paperwork_backend.datadirhandler'], }, { 'interface': 'thread', 'defaults': ['openpaperwork_core.thread.simple'], }, ] def init(self, core): super().init(core) self._init() def _init(self): paperwork_dir = self.core.call_success( "data_dir_handler_get_individual_data_dir" ) sql_file = self.core.call_success( "fs_join", paperwork_dir, 'doc_tracking.db' ) sql_file = self.core.call_success("fs_unsafe", sql_file) self.sql = self.core.call_one( "mainloop_execute", sqlite3.connect, sql_file ) for query in CREATE_TABLES: self.core.call_one("mainloop_execute", self.sql.execute, query) def on_data_dir_changed(self): self.sql.close() self.sql = None self._init() def doc_tracker_register(self, name, transaction_factory): self.transaction_factories.append((name, transaction_factory)) def doc_tracker_get_all_doc_ids(self): doc_ids = self.core.call_one( "mainloop_execute", self.sql.execute, "SELECT doc_id FROM documents" ) doc_ids = self.core.call_one( "mainloop_execute", list, doc_ids ) return {doc_id[0] for doc_id in doc_ids} def doc_tracker_get_doc_text_by_id(self, doc_id): """ Return the text of a document as last-known. This method is useful when a document is deleted: when a plugin is notified that a document has been deleted, it may still need its text. Since the document won't be available anymore, we pull its text from the database (for instance, for label guesser untraining) """ text = self.core.call_one( "mainloop_execute", self.sql.execute, "SELECT text FROM documents WHERE doc_id = ? LIMIT 1", (doc_id,) ) text = self.core.call_one( "mainloop_execute", list, text ) if len(text) <= 0: return None return text[0][0] def doc_transaction_start(self, out: list, total_expected=-1): for (name, transaction_factory) in self.transaction_factories: out.append(transaction_factory( sync=False, total_expected=total_expected )) out.append(DocTrackerTransaction(self, self.sql, total_expected)) def sync(self, promises: list): storage_all_docs = [] names = [t[0] for t in self.transaction_factories] names.append('doc_tracker') promise = openpaperwork_core.promise.ThreadedPromise( self.core, self.core.call_all, args=("storage_get_all_docs", storage_all_docs,) ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(storage_all_docs.sort) class DbDoc(object): def __init__(self, result): self.key = result[0] self.extra = datetime.datetime.fromtimestamp(result[1]) promise = promise.then(self.sql.cursor) promise = promise.then(lambda cursor: ( [ sync.StorageDoc(self.core, doc[0], doc[1]) for doc in storage_all_docs ], [ DbDoc(r) for r in cursor.execute("SELECT doc_id, mtime FROM documents") ], )) promise = promise.then(lambda args: ( *args, sorted([ transaction_factory( sync=True, total_expected=max(len(storage_all_docs), len(args[1])) ) for (name, transaction_factory) in self.transaction_factories ] + [ DocTrackerTransaction( self, self.sql, total_expected=max(len(storage_all_docs), len(args[1])) ) ], key=lambda t: -1 * t.priority), )) promise = promise.then(lambda args: sync.Syncer( self.core, names, args[0], args[1], args[2] )) promise = promise.then(openpaperwork_core.promise.ThreadedPromise( self.core, lambda syncer: syncer.run() )) promises.append(promise) def tests_cleanup(self): self.sql.close() paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/000077500000000000000000000000001417573700700254405ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/__init__.py000066400000000000000000000000001417573700700275370ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/color/000077500000000000000000000000001417573700700265565ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/color/__init__.py000066400000000000000000000000001417573700700306550ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/color/libpillowfight.py000066400000000000000000000130061417573700700321470ustar00rootroot00000000000000""" Automatic page cropping using libpillowfight.find_scan_borders(). May or may not work. """ import logging import pillowfight import openpaperwork_core from ... import (_, sync) LOGGER = logging.getLogger(__name__) ID = "color" class PillowfightTransaction(sync.BaseTransaction): def __init__(self, plugin, sync, total_expected=-1): super().__init__(plugin.core, total_expected) self.priority = plugin.PRIORITY self.plugin = plugin self.sync = sync self.core = plugin.core # for each document, we need to track on which pages we have already # adjusted the color and on which page we didn't yet. self.page_tracker = self.core.call_success("page_tracker_get", ID) def __enter__(self): pass def __exit__(self, exc_type, exc_val, exc_tb): self.cancel() def _adjust_page_colors( self, doc_id, doc_url, page_idx, page_nb, total_pages): paper_size = self.core.call_success( "page_get_paper_size_by_url", doc_url, page_idx ) if paper_size is not None: # probably a PDF --> no need to adjust colors self.notify_progress( ID, _( "Document {doc_id} p{page_idx} already correctly" " colorized" ).format( doc_id=doc_id, page_idx=(page_idx + 1) ), page_nb=page_nb, total_pages=total_pages ) LOGGER.info( "Paper size for new page %d (document %s) is known." " --> Assuming we don't need to adjust colors", doc_id, page_idx ) return self.notify_progress( ID, _("Adjusting colors of document {doc_id} p{page_idx}").format( doc_id=doc_id, page_idx=(page_idx + 1) ), page_nb=page_nb, total_pages=total_pages ) self.plugin.adjust_page_colors_by_url(doc_url, page_idx) def _adjust_new_pages_colors(self, doc_id): doc_url = self.core.call_success("doc_id_to_url", doc_id) need_end_notification = False modified_pages = list(self.page_tracker.find_changes(doc_id, doc_url)) for (page_nb, (change, page_idx)) in enumerate(modified_pages): # Adjust page colors on new pages, but only if we are # not synchronizing with the work directory if not self.sync and change == 'new': self._adjust_page_colors( doc_id, doc_url, page_idx, page_nb, len(modified_pages) ) need_end_notification = True self.page_tracker.ack_page(doc_id, doc_url, page_idx) if need_end_notification: self.notify_progress( ID, _("Adjusting colors of document"), page_nb=len(modified_pages), total_pages=len(modified_pages) ) def add_doc(self, doc_id): self._adjust_new_pages_colors(doc_id) super().add_doc(doc_id) def upd_doc(self, doc_id): self._adjust_new_pages_colors(doc_id) super().upd_doc(doc_id) def del_doc(self, doc_id): self.page_tracker.delete_doc(doc_id) super().del_doc(doc_id) def cancel(self): self.page_tracker.cancel() self.notify_done(ID) def commit(self): self.page_tracker.commit() self.notify_done(ID) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 3000 def get_interfaces(self): return [ "color", "syncable", # actually satisfied by the plugin 'doctracker' ] def get_deps(self): return [ { 'interface': 'doc_tracking', 'defaults': ['paperwork_backend.doctracker'], }, { 'interface': 'page_tracking', 'defaults': ['paperwork_backend.pagetracker'], }, { 'interface': 'pillow', 'defaults': [ 'openpaperwork_core.pillow.img', 'paperwork_backend.pillow.pdf', ], }, ] def init(self, core): super().init(core) self.core.call_all( "doc_tracker_register", ID, lambda sync, total_expected=-1: PillowfightTransaction( self, sync, total_expected ) ) def adjust_page_colors_by_url(self, doc_url, page_idx): LOGGER.info("Adjusting colors of page %d of %s", page_idx, doc_url) doc_id = self.core.call_success("doc_url_to_id", doc_url) if doc_id is not None: self.core.call_one( "mainloop_schedule", self.core.call_all, "on_page_modification_start", doc_id, page_idx ) try: page_img_url = self.core.call_success( "page_get_img_url", doc_url, page_idx ) img = self.core.call_success("url_to_pillow", page_img_url) img = pillowfight.ace(img, samples=200) page_img_url = self.core.call_success( "page_get_img_url", doc_url, page_idx, write=True ) self.core.call_success("pillow_to_url", img, page_img_url) finally: if doc_id is not None: self.core.call_one( "mainloop_schedule", self.core.call_all, "on_page_modification_end", doc_id, page_idx ) return img paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/cropping/000077500000000000000000000000001417573700700272615ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/cropping/__init__.py000066400000000000000000000000201417573700700313620ustar00rootroot00000000000000ID = "cropping" paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/cropping/calibration.py000066400000000000000000000150151417573700700321240ustar00rootroot00000000000000""" Crop scanned images based on a predefined area. """ import logging import openpaperwork_core from ... import (_, sync) from . import ID LOGGER = logging.getLogger(__name__) class CalibrationTransaction(sync.BaseTransaction): def __init__(self, plugin, sync, total_expected=-1): super().__init__(plugin.core, total_expected) self.priority = plugin.PRIORITY self.plugin = plugin self.sync = sync # For each document, we need to track on which pages we have already # guessed the page borders and on which page we didn't yet. # We use the same ID for all the cropping plugins so we never crop # twice the same page. self.page_tracker = self.core.call_success("page_tracker_get", ID) def __enter__(self): pass def __exit__(self, exc_type, exc_val, exc_tb): self.cancel() def _crop_page(self, doc_id, doc_url, page_idx, page_nb, total_pages): paper_size = self.core.call_success( "page_get_paper_size_by_url", doc_url, page_idx ) if paper_size is not None: # We only want to crop scanned pages. self.notify_progress( ID, _("Document {doc_id} p{page_idx} already cropped").format( doc_id=doc_id, page_idx=(page_idx + 1) ), page_nb=page_nb, total_pages=total_pages ) return if self.core.call_success( "page_has_text_by_url", doc_url, page_idx ): self.notify_progress( ID, _( "Document {doc_id} p{page_idx} has already some text." " Not cropping." ).format(doc_id=doc_id, page_idx=(page_idx + 1)), page_nb=page_nb, total_pages=total_pages ) return self.notify_progress( ID, _( "Using calibration to crop page borders of" " document {doc_id} p{page_idx}" ).format(doc_id=doc_id, page_idx=(page_idx + 1)), page_nb=page_nb, total_pages=total_pages ) self.plugin.crop_page_borders_by_url(doc_url, page_idx) def _crop_new_pages(self, doc_id): doc_url = self.core.call_success("doc_id_to_url", doc_id) modified_pages = list(self.page_tracker.find_changes(doc_id, doc_url)) for (page_nb, (change, page_idx)) in enumerate(modified_pages): # Guess page borders on new pages, but only if we are # not currently synchronizing with the work directory # (when syncing we don't modify the documents, ever) if not self.sync and change == 'new': self._crop_page( doc_id, doc_url, page_idx, page_nb, len(modified_pages) ) self.page_tracker.ack_page(doc_id, doc_url, page_idx) def add_doc(self, doc_id): self._crop_new_pages(doc_id) super().add_doc(doc_id) def upd_doc(self, doc_id): self._crop_new_pages(doc_id) super().upd_doc(doc_id) def del_doc(self, doc_id): self.page_tracker.delete_doc(doc_id) super().del_doc(doc_id) def cancel(self): self.page_tracker.cancel() self.notify_done(ID) def commit(self): self.page_tracker.commit() self.notify_done(ID) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 4000 def get_interfaces(self): return [ "cropping", "scanner_calibration", "syncable", # actually satisfied by the plugin 'doctracker' ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'doc_tracking', 'defaults': ['paperwork_backend.doctracker'], }, { 'interface': 'page_tracking', 'defaults': ['paperwork_backend.pagetracker'], }, { 'interface': 'pillow', 'defaults': [ 'openpaperwork_core.pillow.img', 'paperwork_backend.pillow.pdf', ] } ] def init(self, core): super().init(core) self.core.call_all( "config_register", "scanner_calibration", self.core.call_success( "config_build_simple", "scanner", "calibration", lambda: None ) ) self.core.call_all( "doc_tracker_register", ID, lambda sync, total_expected=-1: CalibrationTransaction( self, sync, total_expected ) ) def crop_page_borders_by_url(self, doc_url, page_idx): frame = self.core.call_success( "config_get", "scanner_calibration" ) if frame is None: LOGGER.warning( "No calibration found. Cannot crop page %s p%d", doc_url, page_idx ) return None LOGGER.info( "Cropping page %d of %s (calibration=%s)", page_idx, doc_url, frame ) doc_id = self.core.call_success("doc_url_to_id", doc_url) if doc_id is not None: self.core.call_one( "mainloop_schedule", self.core.call_all, "on_page_modification_start", doc_id, page_idx ) try: page_img_url = self.core.call_success( "page_get_img_url", doc_url, page_idx ) img = self.core.call_success("url_to_pillow", page_img_url) LOGGER.info( "Cropping page %d of %s at %s", page_idx, doc_url, frame ) # make sure we don't extend the image frame = ( max(0, frame[0]), max(0, frame[1]), min(img.size[0], frame[2]), min(img.size[1], frame[3]), ) img = img.crop(frame) page_img_url = self.core.call_success( "page_get_img_url", doc_url, page_idx, write=True ) self.core.call_success("pillow_to_url", img, page_img_url) finally: if doc_id is not None: self.core.call_one( "mainloop_schedule", self.core.call_all, "on_page_modification_end", doc_id, page_idx ) return frame paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/cropping/libpillowfight.py000066400000000000000000000137511417573700700326610ustar00rootroot00000000000000""" Automatic page cropping using libpillowfight.find_scan_borders(). May or may not work. """ import logging import pillowfight import openpaperwork_core from . import ID from ... import (_, sync) LOGGER = logging.getLogger(__name__) class PillowfightTransaction(sync.BaseTransaction): def __init__(self, plugin, sync, total_expected=-1): super().__init__(plugin.core, total_expected) self.priority = plugin.PRIORITY self.plugin = plugin self.sync = sync # For each document, we need to track on which pages we have already # guessed the page borders and on which page we didn't yet. # We use the same ID for all the cropping plugins so we never crop # twice the same page. self.page_tracker = self.core.call_success("page_tracker_get", ID) def __enter__(self): pass def __exit__(self, exc_type, exc_val, exc_tb): self.cancel() def _guess_page_borders( self, doc_id, doc_url, page_idx, page_nb, total_pages): paper_size = self.core.call_success( "page_get_paper_size_by_url", doc_url, page_idx ) if paper_size is not None: # We don't want to guess page borders on PDF files since they # are usually already well-cropped. Also the page borders won't # appear in the document, so libpillowfight algorithm can only # fail. self.notify_progress( ID, _("Document {doc_id} p{page_idx} already cropped").format( doc_id=doc_id, page_idx=(page_idx + 1) ), page_nb=page_nb, total_pages=total_pages ) return self.notify_progress( ID, _( "Guessing page borders of" " document {doc_id} p{page_idx}" ).format(doc_id=doc_id, page_idx=(page_idx + 1)), page_nb=page_nb, total_pages=total_pages ) self.plugin.crop_page_borders_by_url(doc_url, page_idx) def _guess_new_pages_borders(self, doc_id): doc_url = self.core.call_success("doc_id_to_url", doc_id) need_end_notification = False modified_pages = list(self.page_tracker.find_changes(doc_id, doc_url)) for (page_nb, (change, page_idx)) in enumerate(modified_pages): # Guess page borders on new pages, but only if we are # not synchronizing with the work directory # (when syncing we don't modify the documents, ever) if not self.sync and change == 'new': self._guess_page_borders( doc_id, doc_url, page_idx, page_nb, len(modified_pages) ) need_end_notification = True self.page_tracker.ack_page(doc_id, doc_url, page_idx) if need_end_notification: self.notify_progress( ID, _("Guessing page borders"), page_nb=len(modified_pages), total_pages=len(modified_pages) ) def add_doc(self, doc_id): self._guess_new_pages_borders(doc_id) super().add_doc(doc_id) def upd_doc(self, doc_id): self._guess_new_pages_borders(doc_id) super().upd_doc(doc_id) def del_doc(self, doc_id): self.page_tracker.delete_doc(doc_id) super().del_doc(doc_id) def cancel(self): self.page_tracker.cancel() self.notify_done(ID) def commit(self): self.page_tracker.commit() self.notify_done(ID) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 4000 def get_interfaces(self): return [ "cropping", "syncable", # actually satisfied by the plugin 'doctracker' ] def get_deps(self): return [ { 'interface': 'doc_tracking', 'defaults': ['paperwork_backend.doctracker'], }, { 'interface': 'page_tracking', 'defaults': ['paperwork_backend.pagetracker'], }, { 'interface': 'pillow', 'defaults': [ 'openpaperwork_core.pillow.img', 'paperwork_backend.pillow.pdf', ] } ] def init(self, core): super().init(core) self.core.call_all( "doc_tracker_register", ID, lambda sync, total_expected=-1: PillowfightTransaction( self, sync, total_expected ) ) def crop_page_borders_by_url(self, doc_url, page_idx): LOGGER.info("Cropping page %d of %s", page_idx, doc_url) doc_id = self.core.call_success("doc_url_to_id", doc_url) if doc_id is not None: self.core.call_one( "mainloop_schedule", self.core.call_all, "on_page_modification_start", doc_id, page_idx ) try: page_img_url = self.core.call_success( "page_get_img_url", doc_url, page_idx ) img = self.core.call_success("url_to_pillow", page_img_url) frame = pillowfight.find_scan_borders(img) if frame[0] >= frame[2] or frame[1] >= frame[3]: LOGGER.warning( "Invalid frame found for page %d of %s: %s. Cannot" " crop automatically", page_idx, doc_url, frame ) return None LOGGER.info( "Cropping page %d of %s at %s", page_idx, doc_url, frame ) img = img.crop(frame) page_img_url = self.core.call_success( "page_get_img_url", doc_url, page_idx, write=True ) self.core.call_success("pillow_to_url", img, page_img_url) finally: if doc_id is not None: self.core.call_one( "mainloop_schedule", self.core.call_all, "on_page_modification_end", doc_id, page_idx ) return frame paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/label/000077500000000000000000000000001417573700700265175ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/label/__init__.py000066400000000000000000000000001417573700700306160ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/000077500000000000000000000000001417573700700301565ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/__init__.py000066400000000000000000001041411417573700700322700ustar00rootroot00000000000000""" Label guesser guesses label based on document text and bayes filters. It adds the guessed labels on new documents (in other words, freshly added documents). It stores data in 2 ways: - sklearn pickling of TfidVectorizer - sqlite database """ import collections import gc import logging import sqlite3 import threading import time import numpy import scipy.sparse import sklearn.feature_extraction.text import sklearn.naive_bayes import openpaperwork_core import openpaperwork_core.promise from .... import (_, sync) # Beware that we use Sqlite, but sqlite python module is not thread-safe # --> all the calls to sqlite module functions must happen on the main loop, # even those in the transactions (which are run in a thread) LOGGER = logging.getLogger(__name__) ID = "label_guesser" CREATE_TABLES = [ # we could use the labels file instead, but some people access their work # directory through very slow Internet connections, so we better # keep a copy in the DB. ( "CREATE TABLE IF NOT EXISTS labels (" " doc_id TEXT NOT NULL," " label TEXT NOT NULL," " PRIMARY KEY (doc_id, label)" ")" ), ( # see sklearn.feature_extraction.text.CountVectorizer.vocabulary "CREATE TABLE IF NOT EXISTS vocabulary (" " word TEXT NOT NULL," " feature INTEGER NOT NULL," " PRIMARY KEY (feature)" " UNIQUE (word)" ")" ), ( "CREATE TABLE IF NOT EXISTS features (" " doc_id TEXT NOT NULL," " vector NUMPY_ARRAY NOT NULL," " PRIMARY KEY (doc_id)" ")" ) ] class SqliteNumpyArrayHandler(object): @staticmethod def _to_sqlite(np_array): return np_array.tobytes() @staticmethod def _from_sqlite(raw): return numpy.frombuffer(raw) @classmethod def register(cls): sqlite3.register_adapter(numpy.array, cls._to_sqlite) sqlite3.register_converter("NUMPY_ARRAY", cls._from_sqlite) class PluginConfig(object): SETTINGS = { # settings --> default value 'batch_size': 200, # Limit the number of words used for performance reasons 'max_words': 15000, # Limit the number of documents used for performance reasons # backlog is the number of required documents with and without each # label 'max_doc_backlog': 100, 'max_time': 10, # seconds # if the document contains too few words, the classifiers tend to # put every possible labels on it. --> we ignore documents with too # few words / features. 'min_features': 10, } def __init__(self, core): self.core = core def register(self): class Setting(object): def __init__(s, setting, default_val): s.setting = setting s.default_val = default_val def register(s): setting = self.core.call_success( "config_build_simple", "label_guessing", s.setting, lambda: s.default_val ) self.core.call_all( "config_register", "label_guessing_" + s.setting, setting ) for (setting, default_val) in self.SETTINGS.items(): Setting(setting, default_val).register() def get(self, key): return self.core.call_success("config_get", "label_guessing_" + key) class UpdatableVectorizer(object): """ Vectorizer that we can update over time with new features (word identifiers). Store the word->features in the SQLite database. We only add words to the database, we never remove them, otherwise we wouldn't be able to read the feature vectors correctly anymore. """ def __init__(self, core, db_cursor): self.core = core self.db_cursor = db_cursor vocabulary = self.db_cursor.execute( "SELECT word, feature FROM vocabulary" ) vocabulary = {k.strip(): v for (k, v) in vocabulary} if "" in vocabulary: vocabulary.pop("") self.updatable_vocabulary = vocabulary self.last_feature_id = max(vocabulary.values(), default=-1) def partial_fit_transform(self, corpus): # A bit hackish: We just need the analyzer, so instantiating a full # TfidVectorizer() is probably overkill, but meh. tokenizer = sklearn.feature_extraction.text.TfidfVectorizer( ).build_analyzer() LOGGER.info( "Vocabulary contains %d words before fitting", self.last_feature_id ) for doc_txt in corpus: for word in tokenizer(doc_txt): if word in self.updatable_vocabulary: continue self.last_feature_id += 1 self.db_cursor.execute( "INSERT INTO vocabulary (word, feature)" " SELECT ?, ?" " WHERE NOT EXISTS(" " SELECT 1 FROM vocabulary WHERE word = ?" " )", (word.strip(), self.last_feature_id, word) ) self.updatable_vocabulary[word.strip()] = self.last_feature_id LOGGER.info( "Vocabulary contains %d words after fitting", self.last_feature_id ) return self.transform(corpus) def transform(self, corpus): # IMPORTANT: we must use use_idf=False here because we want the values # in each feature vector to be independant from other vectors. try: vectorizer = sklearn.feature_extraction.text.TfidfVectorizer( use_idf=False, vocabulary=self.updatable_vocabulary ) features = vectorizer.fit_transform(corpus) LOGGER.info("%s features extracted", features.shape) return features except ValueError as exc: LOGGER.warning("Failed to extract features", exc_info=exc) return scipy.sparse.csr_matrix((0, 0)) def _find_unused(self): doc_features = self.db_cursor.execute( "SELECT vector FROM features" ) sum_features = None for (doc_vector,) in doc_features: required_padding = ( self.last_feature_id + 1 - doc_vector.shape[0] ) if required_padding > 0: doc_vector = numpy.hstack([ doc_vector, numpy.zeros((required_padding,)) ]) if sum_features is None: sum_features = doc_vector else: sum_features = sum_features + doc_vector if sum_features is None: return ([], 0) return ( [f for (f, v) in enumerate(sum_features) if v == 0.0], len(sum_features) ) def _get_all_doc_ids(self): doc_ids = self.db_cursor.execute("SELECT doc_id FROM features") doc_ids = [doc_id[0] for doc_id in doc_ids] return doc_ids def gc(self): """ Drop features that are unused anymore. IMPORTANT: After this call, the vectorizer must no be used (this method doesn't update the internal state of the vectorizer) """ # we work in the main thread so we don't have to load all the feature # vectors all at once in memory (we just need their sum) LOGGER.info("Garbage collecting unused features ...") (to_drop, total) = self._find_unused() if len(to_drop) <= 0: LOGGER.info("No features to garbage collect (total=%d)", total) return LOGGER.info( "%d/%d features will be removed from the database", len(to_drop), total ) doc_ids = self._get_all_doc_ids() # first we reduce the document feature vectors msg = _( "Label guesser: Garbage-collecting unused document features ..." ) self.core.call_all("on_progress", "label_vector_gc", 0.0, msg) for (idx, doc_id) in enumerate(doc_ids): self.core.call_all( "on_progress", "label_vector_gc", idx / len(doc_ids), msg ) doc_vector = self.db_cursor.execute( "SELECT vector FROM features WHERE doc_id = ? LIMIT 1", (doc_id,) ) doc_vector = list(doc_vector) doc_vector = doc_vector[0][0] to_drop_for_this_doc = [f for f in to_drop if f < len(doc_vector)] doc_vector = numpy.delete(doc_vector, to_drop_for_this_doc) self.db_cursor.execute( "UPDATE features SET vector = ? WHERE doc_id = ?", (doc_vector, doc_id) ) self.core.call_all("on_progress", "label_vector_gc", 1.0) # then we reduce the vocabulary accordingly msg = _("Label guesser: Garbage-collecting unused words ...") self.core.call_all("on_progress", "label_vocabulary_gc", 0.0, msg) for (idx, f) in enumerate(to_drop): self.core.call_all( "on_progress", "label_vocabulary_gc", idx / len(to_drop), msg ) self.db_cursor.execute( "DELETE FROM vocabulary WHERE feature = ?", (f,) ) self.db_cursor.execute( "UPDATE vocabulary SET feature = feature - 1" " WHERE feature >= ?", (f,) ) self.core.call_all("on_progress", "label_vocabulary_gc", 1.0, ) def copy(self): r = UpdatableVectorizer(self.core, self.db_cursor) r.updatable_vocabulary = dict(self.updatable_vocabulary) r.last_feature_id = self.last_feature_id return r class BatchIterator(object): def __init__(self, config, features, targets): self.features = features self.targets = targets self.b = 0 self.batch_size = config.get("batch_size") def __iter__(self): self.b = 0 return self def __next__(self): batch_corpus = self.features[self.b:self.b + self.batch_size] if len(batch_corpus) <= 0: raise StopIteration() batch_corpus = scipy.sparse.vstack(batch_corpus).toarray() batch_targets = self.targets[self.b:self.b + self.batch_size] self.b += self.batch_size return (batch_corpus, batch_targets) class FeatureReductor(object): def __init__(self, to_drop): self.features_to_drop = to_drop def reduce_features(self, features): return numpy.delete(features, self.features_to_drop) class DummyFeatureReductor(object): def reduce_features(self, features): return features class Corpus(object): """ Handles doc_ids and their associate feature vectors. Make sure we train the document in the best order possible, so even if the training is interrupted (time limit), we still have some training for most labels. """ def __init__(self, config, doc_ids, features, targets): self.config = config self.doc_ids = doc_ids self.features = features self.targets = targets def standardize_feature_vectors(self, vectorizer): for idx in range(0, len(self.features)): doc_vector = self.features[idx] required_padding = ( vectorizer.last_feature_id + 1 - doc_vector.shape[0] ) assert(required_padding >= 0) if required_padding > 0: doc_vector = scipy.sparse.hstack([ scipy.sparse.csr_matrix(doc_vector), numpy.zeros((required_padding,)) ]) else: doc_vector = scipy.sparse.csr_matrix(doc_vector) self.features[idx] = doc_vector def reduce_corpus_words(self): """ We may end up with a lot of different words (about 76000 in my case). But most of them are actually too rare to be useful and they use a lot memory and CPU time. """ max_words = self.config.get("max_words") word_freq_sums = sum(self.features).toarray()[0] word_count = word_freq_sums.shape[0] LOGGER.info("Total word count before reduction: %d", word_count) if word_count <= max_words: LOGGER.info("No reduction to do") return DummyFeatureReductor() threshold = sorted(word_freq_sums, reverse=True)[max_words] LOGGER.info("Word frequency threshold: %f", threshold) features_to_drop = [] for (idx, freq) in enumerate(word_freq_sums): if freq > threshold: continue features_to_drop.append(idx) reductor = FeatureReductor(features_to_drop) for idx in range(0, len(self.features)): self.features[idx] = scipy.sparse.csr_matrix( reductor.reduce_features( self.features[idx].toarray()[0] ) ) LOGGER.info( "Total word count after reduction: %d", word_count - len(reductor.features_to_drop) ) return reductor def get_doc_count(self): return len(self.features) def get_labels(self): return self.targets.keys() def get_batches(self, label): return BatchIterator(self.config, self.features, self.targets[label]) @staticmethod def _add_doc_ids(max_doc_backlog, doc_weights, doc_ids): # Assumes doc_ids are in reverse order (most recent first) # Also assumes most recent documents are the most useful assert(len(doc_ids) <= max_doc_backlog) weigth = max_doc_backlog + 1 for doc_id in doc_ids: doc_weights[doc_id] += weigth weigth -= 1 @staticmethod def load(config, cursor): start = time.time() # doc_id --> weigth doc_weights = collections.defaultdict(lambda: 0) all_labels = cursor.execute("SELECT DISTINCT label FROM labels") all_labels = {label[0] for label in all_labels} all_docs = cursor.execute( "SELECT doc_id FROM features ORDER BY doc_id DESC" ) all_docs = [doc[0] for doc in all_docs] max_doc_backlog = config.get("max_doc_backlog") for label in all_labels: ds = cursor.execute( "SELECT doc_id FROM labels WHERE label = ?" " ORDER BY doc_id DESC LIMIT {}".format(max_doc_backlog), (label,) ) Corpus._add_doc_ids( max_doc_backlog, doc_weights, [d[0] for d in ds] ) # label --> number of doc without this label no_label_counts = {label: 0 for label in all_labels} no_label_docids = {label: [] for label in all_labels} for doc_id in all_docs: if len(no_label_counts) <= 0: break doc_labels = cursor.execute( "SELECT label FROM labels WHERE doc_id = ?", (doc_id,) ) for label in list(no_label_counts.keys()): if label in doc_labels: continue no_label_docids[label].append(doc_id) no_label_counts[label] += 1 if no_label_counts[label] >= max_doc_backlog: no_label_counts.pop(label) for doc_ids in no_label_docids.values(): doc_ids.sort(reverse=True) Corpus._add_doc_ids(max_doc_backlog, doc_weights, doc_ids) LOGGER.info( "Loading features of %d documents for %d labels", len(doc_weights), len(all_labels) ) all_features = {} for doc_id in doc_weights.keys(): vectors = cursor.execute( "SELECT vector FROM features WHERE doc_id = ? LIMIT 1", (doc_id,) ) for vector in vectors: vector = vector[0] if vector is None: continue all_features[doc_id] = vector all_features = [ [weight, doc_id, all_features[doc_id]] for (doc_id, weight) in doc_weights.items() if doc_id in all_features ] all_features.sort(reverse=True) doc_ids = [ doc_id for (weight, doc_id, features) in all_features ] features = [ features for (weight, doc_id, features) in all_features ] # Load labels targets = collections.defaultdict(list) for (idx, doc_id) in enumerate(doc_ids): doc_labels = cursor.execute( "SELECT label FROM labels WHERE doc_id = ?", (doc_id,) ) doc_labels = [label[0] for label in doc_labels] for label in all_labels: present = label in doc_labels targets[label].append(1 if present else 0) corpus = Corpus( config=config, doc_ids=doc_ids, features=features, targets=targets ) stop = time.time() LOGGER.info( "Took %dms to load features of %d documents", int((stop - start) * 1000), len(doc_ids) ) return corpus class LabelGuesserTransaction(sync.BaseTransaction): def __init__(self, plugin, guess_labels=False, total_expected=-1): super().__init__(plugin.core, total_expected) self.priority = plugin.PRIORITY self.plugin = plugin self.guess_labels = guess_labels LOGGER.info("Transaction start: Guessing labels: %s", guess_labels) self.cursor = None self.vectorizer = None self.lock_acquired = False self.nb_changes = 0 self.need_gc = False def __enter__(self): pass def __exit__(self, exc_type, exc_val, exc_tb): self.cancel() def _lazyinit_transaction(self): if self.cursor is not None: return # prevent reload of classifiers during the transaction assert(not self.lock_acquired) self.plugin.classifiers_cond.acquire() self.lock_acquired = True if not self.plugin.classifiers_loaded and self.guess_labels: # classifiers haven't been loaded at all yet. Now # looks like a good time for it (end of initial sync) self.plugin.reload_label_guessers() if self.plugin.classifiers is None and self.guess_labels: # Before starting the transaction, we wait for the classifiers # to be loaded, because we may need them # (see add_doc() -> _set_guessed_labels()) self.plugin.classifiers_cond.wait() self.cursor = sqlite3.connect( self.core.call_success("fs_unsafe", self.plugin.sql_file), detect_types=sqlite3.PARSE_DECLTYPES ) self.cursor.execute("BEGIN TRANSACTION") self.vectorizer = UpdatableVectorizer(self.core, self.cursor) def add_doc(self, doc_id): self._lazyinit_transaction() if self.guess_labels: # we have a higher priority than index plugins, so it is a good # time to update the document labels doc_url = self.core.call_success("doc_id_to_url", doc_id) has_labels = self.core.call_success( "doc_has_labels_by_url", doc_url ) if has_labels is not None: LOGGER.info( "Document %s already has labels. Won't guess labels", doc_url ) return self.plugin._set_guessed_labels(doc_url) self.notify_progress( ID, _("Label guesser: added document %s") % doc_id ) self._upd_doc(doc_id) self.nb_changes += 1 super().add_doc(doc_id) def upd_doc(self, doc_id): self._lazyinit_transaction() self.notify_progress( ID, _("Label guesser: updated document %s") % doc_id ) self._upd_doc(doc_id) self.nb_changes += 1 super().upd_doc(doc_id) def _del_doc(self, doc_id): self.cursor.execute("DELETE FROM labels WHERE doc_id = ?", (doc_id,)) self.cursor.execute("DELETE FROM features WHERE doc_id = ?", (doc_id,)) def del_doc(self, doc_id): self._lazyinit_transaction() self.notify_progress( ID, _("Label guesser: deleted document %s") % doc_id ) self.nb_changes += 1 self._del_doc(doc_id) LOGGER.info( "Document %s has been deleted." " Feature garbage-collecting will be run", doc_id ) self.need_gc = True super().del_doc(doc_id) def _upd_doc(self, doc_id): self._del_doc(doc_id) doc_url = self.core.call_success("doc_id_to_url", doc_id) doc_labels = set() if doc_url is not None: self.core.call_all("doc_get_labels_by_url", doc_labels, doc_url) doc_labels = {label[0] for label in doc_labels} for doc_label in doc_labels: self.cursor.execute( "INSERT INTO labels (doc_id, label)" " VALUES (?, ?)", (doc_id, doc_label) ) doc_txt = [] self.core.call_all("doc_get_text_by_url", doc_txt, doc_url) doc_txt = "\n\n".join(doc_txt).strip() vector = self.vectorizer.partial_fit_transform([doc_txt]) if vector.shape[0] <= 0 or vector.shape[1] <= 0: vector = numpy.array([]) else: vector = vector[0].toarray() self.cursor.execute( "INSERT INTO features (doc_id, vector) VALUES (?, ?)", (doc_id, vector) ) def cancel(self): try: self.core.call_success( "mainloop_schedule", self.core.call_all, "on_label_guesser_canceled" ) if self.cursor is not None: self.cursor.execute("ROLLBACK") self.cursor.close() self.cursor = None self.notify_done(ID) if not self.plugin.classifiers_loaded: # classifiers haven't been loaded at all yet. Now # looks like a good time for it (end of initial sync) self.plugin.reload_label_guessers() finally: if self.lock_acquired: self.plugin.classifiers_cond.release() self.lock_acquired = False def commit(self): try: LOGGER.info("Committing") self.core.call_success( "mainloop_schedule", self.core.call_all, "on_label_guesser_commit_start" ) if self.nb_changes <= 0: assert(self.cursor is None) self.notify_done(ID) self.core.call_success( "mainloop_schedule", self.core.call_all, 'on_label_guesser_commit_end' ) LOGGER.info("Nothing to do. Training left unchanged.") if self.plugin.classifiers is None: # classifiers haven't been loaded at all yet. Now # looks like a good time for it (end of initial sync) self.plugin.reload_label_guessers() return if self.need_gc: self.vectorizer.gc() self.vectorizer = None self.notify_progress( ID, _("Commiting changes for label guessing ...") ) self.cursor.execute("COMMIT") self.cursor.close() self.notify_done(ID) self.core.call_success( "mainloop_schedule", self.core.call_all, 'on_label_guesser_commit_end' ) LOGGER.info("Label guessing updated") self.plugin.reload_label_guessers() finally: if self.lock_acquired: self.plugin.classifiers_cond.release() self.lock_acquired = False class Plugin(openpaperwork_core.PluginBase): PRIORITY = 100 def __init__(self): self.bayes = None self.config = None self.sql = None # If we load the classifiers, we keep the vectorizer that goes with # them, because the vectorizer must return vectors that have the size # that the classifiers expect. If we would train the vectorizer with # new documents, the vector sizes could increase # Since loading the classifiers is a fairly long operations, we only # do it when we actually need them. self.word_reductor = None self.classifiers = None self.vectorizer = None # indicates whether the classifiers have been / are # been loaded self.classifiers_loaded = False # Do NOT use an RLock() here: The transaction locks this mutex # until commit() (or cancel()) is called. However, add_doc(), # del_doc(), upd_doc() and commit() may be called from different # threads. self.classifiers_cond = threading.Condition(threading.Lock()) self.bayes_dir = None self.sql_file = None SqliteNumpyArrayHandler.register() def get_interfaces(self): return [ 'sync', ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'data_versioning', 'defaults': ['openpaperwork_core.data_versioning'], }, { 'interface': 'doc_labels', 'defaults': ['paperwork_backend.model.labels'], }, { 'interface': 'doc_tracking', 'defaults': ['paperwork_backend.doctracker'], }, { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'data_dir_handler', 'defaults': ['paperwork_backend.datadirhandler'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, { 'interface': 'work_queue', 'defaults': ['openpaperwork_core.work_queue.default'], }, ] def init(self, core): super().init(core) self.config = PluginConfig(core) self.config.register() self.core.call_all( "work_queue_create", "label_sklearn", stop_on_quit=True ) self._init() def _init(self): data_dir = self.core.call_success( "data_dir_handler_get_individual_data_dir") if self.bayes_dir is None: # may be set by tests self.bayes_dir = self.core.call_success( "fs_join", data_dir, "bayes" ) self.core.call_success("fs_mkdir_p", self.bayes_dir) self.sql_file = self.core.call_success( "fs_join", self.bayes_dir, 'label_guesser.db' ) self.sql = sqlite3.connect( self.core.call_success("fs_unsafe", self.sql_file), detect_types=sqlite3.PARSE_DECLTYPES ) for query in CREATE_TABLES: self.sql.execute(query) self.core.call_all( "doc_tracker_register", "label_guesser", self._build_transaction ) def _build_transaction(self, sync, total_expected): return LabelGuesserTransaction( self, guess_labels=not sync, total_expected=total_expected ) def on_data_dir_changed(self): self.sql.close() self._init() def reload_label_guessers(self): self.classifiers = None self.classifiers_loaded = True promise = openpaperwork_core.promise.ThreadedPromise( self.core, self._reload_label_guessers ) self.core.call_all("work_queue_cancel_all", "label_sklearn") self.core.call_success( "work_queue_add_promise", "label_sklearn", promise ) def _reload_label_guessers(self): with self.classifiers_cond: try: cursor = sqlite3.connect( self.core.call_success("fs_unsafe", self.sql_file), detect_types=sqlite3.PARSE_DECLTYPES ) try: cursor.execute("BEGIN TRANSACTION") self.vectorizer = UpdatableVectorizer(self.core, cursor) ( self.word_reductor, self.classifiers ) = self._load_classifiers( cursor, self.vectorizer ) finally: cursor.execute("ROLLBACK") cursor.close() finally: self.classifiers_cond.notify_all() def _load_classifiers(self, cursor, vectorizer): # Jflesch> This is a very memory-intensive process. The Glib may try # to allocate memory before the GC runs and AFAIK the Glib # is not aware of Python GC, and so won't trigger it if required # (instead it will abort). # --> free as much memory as possible now # (Remember that there may be 32bits version of Paperwork out there) gc.collect() msg = _("Label guessing: Training ...") try: self.core.call_all("on_progress", "label_classifiers", 0.0, msg) corpus = Corpus.load(self.config, cursor) LOGGER.info("Training classifiers ...") start = time.time() if corpus.get_doc_count() <= 1: return (DummyFeatureReductor(), {}) corpus.standardize_feature_vectors(vectorizer) # no need to train on all the words. Only the most used words reductor = corpus.reduce_corpus_words() # Jflesch> This is a very memory-intensive process. The Glib may # try to allocate memory before the GC runs and AFAIK the Glib # is not aware of Python GC, and so won't trigger it if required # (instead it will abort). # --> free as much memory as possible now gc.collect() classifiers = collections.defaultdict( sklearn.naive_bayes.GaussianNB ) batch_iterators = [ (label, corpus.get_batches(label)) for label in corpus.get_labels() ] done = 0 total = len(batch_iterators) * corpus.get_doc_count() loop_nb = 0 timeout = False max_time = self.config.get("max_time") fit_start = time.time() try: while not timeout: for (label, batch_iterator) in batch_iterators: now = time.time() if loop_nb > 0 and now - fit_start > max_time: timeout = True LOGGER.warning( "Training is taking too long (%dms > %dms)." " Interrupting", (now - fit_start) * 1000, max_time * 1000 ) break (batch_corpus, batch_targets) = next(batch_iterator) self.core.call_all( "on_progress", "label_classifiers", done / total, _("Label guessing: Training ...") ) classifiers[label].partial_fit( batch_corpus, batch_targets, classes=[0, 1] ) done += len(batch_corpus) loop_nb += 1 except StopIteration: pass stop = time.time() LOGGER.info( "Training took %dms (Fitting: %dms) ;" " Training completed at %d%%", (stop - start) * 1000, (stop - fit_start) * 1000, done * 100 / total ) # Jflesch> This is a very memory-intensive process. The Glib may # try to allocate memory before the GC runs and AFAIK the Glib # is not aware of Python GC, and so won't trigger it if required # (instead it will abort). # --> free as much memory as possible now gc.collect() return (reductor, classifiers) finally: self.core.call_all("on_progress", "label_classifiers", 1.0, msg) def _guess(self, vectorizer, reductor, classifiers, doc_url): LOGGER.info("Guessing labels on %s", doc_url) doc_txt = [] self.core.call_all("doc_get_text_by_url", doc_txt, doc_url) doc_txt = "\n\n".join(doc_txt).strip() if doc_txt == u"": return vector = vectorizer.transform([doc_txt]) vector = vector.toarray()[0] vector = reductor.reduce_features(vector) min_features = self.config.get("min_features") nb_features = 0 for f in vector: if f > 0: nb_features += 1 if nb_features < min_features: LOGGER.warning( "Document doesn't contain enough different words" " (%d ; min required is %d). Labels won't be guessed", nb_features, min_features ) return LOGGER.info("Documents contains %d features", nb_features) for (label, classifier) in classifiers.items(): predicted = classifier.predict([vector])[0] if predicted: yield label def _set_guessed_labels(self, doc_url): # self.classifiers_cond must locked assert(self.classifiers is not None) labels = self._guess( self.vectorizer, self.word_reductor, self.classifiers, doc_url ) labels = list(labels) for label in labels: self.core.call_success("doc_add_label_by_url", doc_url, label) def tests_cleanup(self): self.sql.close() paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/compute_backlog.py000066400000000000000000000217201417573700700336700ustar00rootroot00000000000000""" To use it: ```sh paperwork-cli plugins add \ paperwork_backend.guesswork.label.sklearn.compute_backlog paperwork-cli compute_sklearn_label_guessing_backlog ``` """ import functools import itertools import time import sklearn.naive_bayes import sklearn.feature_extraction.text import openpaperwork_core def pairwise(iterable): (a, b) = itertools.tee(iterable) next(b, None) return zip(a, b) class Baye(object): def __init__(self): self.data = [] self.targets = [] self.sklearn_count_vectorizer = None self.sklearn_tfid_transformer = None self.sklearn_classifier = None self.reset() def reset(self): self.sklearn_count_vectorizer = \ sklearn.feature_extraction.text.CountVectorizer() self.sklearn_tfid_transformer = \ sklearn.feature_extraction.text.TfidfTransformer(use_idf=False) self.sklearn_classifier = sklearn.naive_bayes.GaussianNB() def fit(self, category, text): text = text.strip() if len(text) <= 0: return category = (1 if category == 'yes' else 0) self.data.append(text) self.targets.append(category) def seal(self): counts = self.sklearn_count_vectorizer.fit_transform(self.data) tfid = self.sklearn_tfid_transformer.fit_transform(counts) self.sklearn_classifier.fit(tfid.toarray(), self.targets) def score(self, text, label): counts = self.sklearn_count_vectorizer.transform([text]) tfid = self.sklearn_tfid_transformer.transform(counts) predicted = self.sklearn_classifier.predict_proba(tfid.toarray()) r = { "no": predicted[0][0], "yes": predicted[0][1], } return r class Bayes(object): def __init__(self, labels): self.bayes = { label: Baye() for label in labels } def reset(self): for v in self.bayes.values(): v.reset() def fit(self, label, present, text): self.bayes[label].fit("yes" if present else "no", text) def seal(self): for v in self.bayes.values(): v.seal() def score(self, label, text): score = self.bayes[label].score(text, label) return { "yes": score['yes'] if 'yes' in score else 0, "no": score['no'] if 'no' in score else 0, } @functools.total_ordering class Document(object): def __init__(self, core, doc_id, text, labels): self.core = core self.doc_id = doc_id self.text = text self.labels = labels self.scores = {} def __lt__(self, other): return self.doc_id < other.doc_id def __eq__(self, other): return self.doc_id == self.doc_id def __hash__(self): return hash(self.doc_id) def fit(self, bayes, all_labels): for label in all_labels: bayes.fit(label, label in self.labels, self.text) def compute_scores(self, bayes, all_labels): for label in all_labels: self.scores[label] = bayes.score(label, self.text) def compute_accuracy(self, all_labels): success = 0 for label in all_labels: score = self.scores[label] guessed_has_label = (score['yes'] > score['no']) if guessed_has_label == (label in self.labels): success += 1 return success / len(all_labels) class Corpus(object): """ All the texts of all the documents, and their labels. """ def __init__(self, core): self.core = core self.all_documents = [] self.documents = [] self.labels = set() self.bayes = None def reset(self): self.bayes.reset() def load_all(self): self.all_documents = [] docs = [] self.core.call_all("storage_get_all_docs", docs) total = len(docs) for (idx, (doc_id, doc_url)) in enumerate(docs): self.core.call_all( "on_progress", "load_docs", idx / total, "Loading document %s" % doc_id ) text = [] self.core.call_all("doc_get_text_by_url", text, doc_url) text = "\n\n".join(text) labels = set() self.core.call_all("doc_get_labels_by_url", labels, doc_url) labels = [label[0] for label in labels] labels.sort() for label in labels: self.labels.add(label) self.all_documents.append( Document(self.core, doc_id, text, labels) ) self.all_documents.sort() self.all_documents.reverse() self.core.call_all("on_progress", "load_docs", 1.0) self.bayes = Bayes(self.labels) def _get_doc_with_label(self, label, backlog): nb = 0 for doc in self.all_documents: if label in doc.labels: yield doc nb += 1 if nb >= backlog: break def _get_doc_without_label(self, label, backlog): nb = 0 for doc in self.all_documents: if label not in doc.labels: yield doc nb += 1 if nb >= backlog: break def fit(self, backlog): docs = set() for label in self.labels: docs.update(self._get_doc_with_label(label, backlog)) docs.update(self._get_doc_without_label(label, backlog)) self.documents = set(self.all_documents) for doc in docs: self.documents.remove(doc) self.documents = list(self.documents) self.documents.sort() self.documents.reverse() self.documents = self.documents[:200] if len(self.documents) < 200: print("Not enough documents left ({})".format( len(self.documents) )) return False for (idx, doc) in enumerate(docs): self.core.call_all( "on_progress", "training", idx / len(docs), "Extracting features from %d documents" % len(docs) ) doc.fit(self.bayes, self.labels) self.core.call_all("on_progress", "training", 1.0) print("Extracted features from %d documents" % len(docs)) return True def seal(self): print("Training...") self.bayes.seal() print("Training done") def compute_scores(self): total = len(self.documents) for (idx, doc) in enumerate(self.documents): self.core.call_all( "on_progress", "scoring", idx / total, "computing label scores on %s" % doc.doc_id ) doc.compute_scores(self.bayes, self.labels) self.core.call_all("on_progress", "scoring", 1.0) def compute_accuracy(self): accuracy_sum = 0 total = len(self.documents) for doc in self.documents: accuracy_sum += doc.compute_accuracy(self.labels) return accuracy_sum / total class Plugin(openpaperwork_core.PluginBase): """ Add a command to search for the best backlog for paperwork_backend.guesswork.label.sklearn heuristically and based on the user's documents. """ def __init__(self): super().__init__() self.interactive = False def get_interfaces(self): return [ 'shell', ] def get_deps(self): return [ { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'doc_labels', 'defaults': ['paperwork_backend.model.labels'], }, { 'interface': 'doc_text', 'defaults': [ 'paperwork_backend.model.hocr', 'paperwork_backend.model.pdf', ], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): parser.add_parser('compute_sklearn_label_guessing_backlog') def cmd_run(self, args): if args.command != 'compute_sklearn_label_guessing_backlog': return None corpus = Corpus(self.core) corpus.load_all() for backlog in (1, 10, 25, 50, 75, 100, 150, 200, 250, 300, 500, 2500): print() print("Backlog {}:".format(backlog)) corpus.reset() if not corpus.fit(backlog): return {} start = time.time() corpus.seal() stop = time.time() corpus.compute_scores() accuracy = corpus.compute_accuracy() print("Backlog: {} ; Accuracy: {} ; Training time: {}s".format( backlog, accuracy, int(stop - start) )) return {} paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/label/sklearn/compute_threshold.py000066400000000000000000000235221417573700700342640ustar00rootroot00000000000000""" To use it: ```sh paperwork-cli plugins add \ paperwork_backend.guesswork.label.sklearn.compute_threshold paperwork-cli compute_sklearn_label_guessing_threshold ``` """ import functools import itertools import time import sklearn.naive_bayes import sklearn.feature_extraction.text import openpaperwork_core NB_ITERATIONS = 2 # beware complexity will increase exponentionnally def pairwise(iterable): (a, b) = itertools.tee(iterable) next(b, None) return zip(a, b) class Baye(object): samples = 5 def __init__(self): self.data = [] self.targets = [] self.sklearn_count_vectorizer = \ sklearn.feature_extraction.text.CountVectorizer() self.sklearn_tfid_transformer = \ sklearn.feature_extraction.text.TfidfTransformer(use_idf=False) # For reference, always returning "no" gives an accuracy of ~0.9475 # For reference, always returning "yes" gives an accuracy of ~0.0524 # For reference, simplebayes: accuracy=~0.9912 with threshold ~0.162 # Best accuracy: ~0.9991 ; No threshold usable / to use self.sklearn_classifier = sklearn.naive_bayes.GaussianNB() # Best accuracy: ~0.9634 ; Best threshold: ~0.068 # self.sklearn_classifier = sklearn.naive_bayes.MultinomialNB() # Best accuracy: ~0.9642 ; Best threshold: ~0.078 # self.sklearn_classifier = sklearn.naive_bayes.ComplementNB() # Best accuracy: ~0.9687; Best threshold: ~0.9980 # self.sklearn_classifier = sklearn.naive_bayes.BernoulliNB() # Slow as hell. # self.sklearn_classifier = sklearn.naive_bayes.CategoricalNB() def fit(self, category, text): text = text.strip() if len(text) <= 0: return category = (1 if category == 'yes' else 0) self.data.append(text) self.targets.append(category) def seal(self): counts = self.sklearn_count_vectorizer.fit_transform(self.data) tfid = self.sklearn_tfid_transformer.fit_transform(counts) self.sklearn_classifier.fit(tfid.toarray(), self.targets) def score(self, text, label): counts = self.sklearn_count_vectorizer.transform([text]) tfid = self.sklearn_tfid_transformer.transform(counts) predicted = self.sklearn_classifier.predict_proba(tfid.toarray()) r = { "no": predicted[0][0], "yes": predicted[0][1], } if self.samples > 0: self.samples -= 1 print("Prediction sample: {}={}".format(label, r)) return r class Bayes(object): def __init__(self, labels): self.bayes = { label: Baye() for label in labels } def fit(self, label, present, text): self.bayes[label].fit("yes" if present else "no", text) def seal(self): for v in self.bayes.values(): v.seal() def score(self, label, text): score = self.bayes[label].score(text, label) return { "yes": score['yes'] if 'yes' in score else 0, "no": score['no'] if 'no' in score else 0, } @functools.total_ordering class Document(object): def __init__(self, core, doc_id, text, labels): self.core = core self.doc_id = doc_id self.text = text self.labels = labels self.scores = {} def __lt__(self, other): return self.doc_id < other.doc_id def __eq__(self, other): return self.doc_id == self.doc_id def __hash__(self): return hash(self.doc_id) def fit(self, bayes, all_labels): for label in all_labels: bayes.fit(label, label in self.labels, self.text) def compute_scores(self, bayes, all_labels): for label in all_labels: self.scores[label] = bayes.score(label, self.text) def compute_accuracy(self, threshold, all_labels): success = 0 for label in all_labels: score = self.scores[label] total = score['yes'] + score['no'] if total == 0: score = 0 else: score = score['yes'] / total guessed_has_label = score > threshold if guessed_has_label == (label in self.labels): success += 1 return success / len(all_labels) class Corpus(object): """ All the texts of all the documents, and their labels. """ def __init__(self, core): self.core = core self.documents = [] self.labels = set() self.bayes = None def load_all(self): docs = [] self.core.call_all("storage_get_all_docs", docs) total = len(docs) for (idx, (doc_id, doc_url)) in enumerate(docs): self.core.call_all( "on_progress", "load_docs", idx / total, "Loading document %s" % doc_id ) text = [] self.core.call_all("doc_get_text_by_url", text, doc_url) text = "\n\n".join(text) labels = set() self.core.call_all("doc_get_labels_by_url", labels, doc_url) labels = [label[0] for label in labels] labels.sort() for label in labels: self.labels.add(label) self.documents.append(Document(self.core, doc_id, text, labels)) self.documents.sort() self.core.call_all("on_progress", "load_docs", 1.0) self.bayes = Bayes(self.labels) def fit(self): for (idx, doc) in enumerate(self.documents): doc.fit(self.bayes, self.labels) def seal(self): self.bayes.seal() def compute_scores(self): total = len(self.documents) for (idx, doc) in enumerate(self.documents): self.core.call_all( "on_progress", "scoring", idx / total, "computing label scores on %s" % doc.doc_id ) doc.compute_scores(self.bayes, self.labels) self.core.call_all("on_progress", "scoring", 1.0) def compute_accuracy(self, threshold): accuracy_sum = 0 total = len(self.documents) for doc in self.documents: accuracy_sum += doc.compute_accuracy(threshold, self.labels) return accuracy_sum / total class Plugin(openpaperwork_core.PluginBase): """ Add a command to search for the best threshold for paperwork_backend.guesswork.label.simplebayes (see THRESHOLD_YES_NO_RATIO) heuristically and based on the user's documents. OBSOLETE: Now that we are using sklearn.GaussianNB, there is no need for a threshold anymore. """ def __init__(self): super().__init__() self.interactive = False def get_interfaces(self): return [ 'shell', ] def get_deps(self): return [ { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'doc_labels', 'defaults': ['paperwork_backend.model.labels'], }, { 'interface': 'doc_text', 'defaults': [ 'paperwork_backend.model.hocr', 'paperwork_backend.model.pdf', ], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): parser.add_parser('compute_sklearn_label_guessing_threshold') @staticmethod def _get_next_thresholds(thresholds): """ Heuristic figuring out the next thresholds to try to estimate """ middles = [] for (threshold, next_threshold) in pairwise(thresholds): middles.append( ( (threshold[0] + next_threshold[0]) / 2, (threshold[1] + next_threshold[1]) / 2, ) ) return [m[0] for m in middles] def cmd_run(self, args): if args.command != 'compute_sklearn_label_guessing_threshold': return None corpus = Corpus(self.core) corpus.load_all() corpus.fit() corpus.seal() corpus.compute_scores() iterations = NB_ITERATIONS thresholds = [ (threshold, corpus.compute_accuracy(threshold)) for threshold in (0.0, 1.0) ] for iteration in range(0, iterations): print("") best = max(thresholds, key=lambda x: x[1]) print("Iteration {}: best accuracy={}, best threshold={}".format( iteration, best[1], best[0] )) start = time.time() next_thresholds = self._get_next_thresholds(thresholds) total = len(next_thresholds) for (idx, next_threshold) in enumerate(next_thresholds): self.core.call_all( "on_progress", "iteration", idx / total, "Iteration %d" % iteration ) thresholds.append( (next_threshold, corpus.compute_accuracy(next_threshold)), ) thresholds.sort() self.core.call_all("on_progress", "iteration", 1.0) stop = time.time() s = int(stop - start) ms = int(((stop - start) * 1000) % 1000) print("Iteration took {}s {}ms".format(s, ms)) print("") for threshold in thresholds: print("Threshold: {}".format(threshold)) print("") best = max(thresholds, key=lambda x: x[1]) print("RESULT: best accuraccy={}, best threshold={}".format( best[1], best[0] )) return { "all": thresholds, "best": best, } gaussian_with_hashes.py000066400000000000000000000177451417573700700346670ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/label/sklearn""" To use it: ```sh paperwork-cli plugins add \ paperwork_backend.guesswork.label.sklearn.gaussian_with_hashes paperwork-cli test_sklearn_gaussian_with_hashes ``` """ import time import functools import sklearn.naive_bayes import openpaperwork_core samples = 5 class Baye(object): def __init__(self, core, label, n_features): self.core = core self.label = label self.data = [] self.counts = [] self.tfid = [] self.targets = [] self.sklearn_hash_vectorizer = \ sklearn.feature_extraction.text.HashingVectorizer( n_features=n_features ) self.sklearn_tfid_transformer = \ sklearn.feature_extraction.text.TfidfTransformer(use_idf=False) # Best accuracy: ~0.9991 self.sklearn_classifier = sklearn.naive_bayes.GaussianNB() def fit(self, category, text): text = text.strip() if len(text) <= 0: return category = (1 if category == 'yes' else 0) self.data.append(text) self.targets.append(category) def seal(self): r = 0 self.core.call_all( "on_progress", "fitting", 0.0, "Fitting training of label {} ...".format(self.label) ) for pos in range(0, len(self.data), 50): self.core.call_all( "on_progress", "fitting", pos / len(self.data), "Fitting training of label {} ...".format(self.label) ) counts = self.sklearn_hash_vectorizer.fit_transform( self.data[pos:pos + 50] ) tfid = self.sklearn_tfid_transformer.fit_transform( counts ).toarray() start = time.time() self.sklearn_classifier.partial_fit( tfid, self.targets[pos:pos + 50], classes=[0, 1] ) stop = time.time() r += stop - start self.core.call_all("on_progress", "fitting", 1.0) return r def score(self, text, label): global samples counts = self.sklearn_hash_vectorizer.transform([text]) tfid = self.sklearn_tfid_transformer.transform(counts) predicted = self.sklearn_classifier.predict_proba(tfid.toarray()) r = { "no": predicted[0][0], "yes": predicted[0][1], } if samples > 0: samples -= 1 print("Prediction sample: {}={}".format(label, r)) return r class Bayes(object): def __init__(self, core, labels, n_features): self.core = core self.bayes = { label: Baye(core, label, n_features) for label in labels } def fit(self, label, present, text): self.bayes[label].fit("yes" if present else "no", text) def seal(self): r = 0 for (idx, (k, v)) in enumerate(self.bayes.items()): print("Fitting training of label {} ({}/{}) ...".format( k, idx, len(self.bayes) )) r += v.seal() s = int(r) ms = int(((r) * 1000) % 1000) print("Fitting took {}s {}ms for {} labels".format( s, ms, len(self.bayes) )) def score(self, label, text): score = self.bayes[label].score(text, label) return { "yes": score['yes'] if 'yes' in score else 0, "no": score['no'] if 'no' in score else 0, } @functools.total_ordering class Document(object): def __init__(self, core, doc_id, text, labels): self.core = core self.doc_id = doc_id self.text = text self.labels = labels self.scores = {} def __lt__(self, other): return self.doc_id < other.doc_id def __eq__(self, other): return self.doc_id == self.doc_id def __hash__(self): return hash(self.doc_id) def fit(self, bayes, all_labels): for label in all_labels: bayes.fit(label, label in self.labels, self.text) def compute_scores(self, bayes, all_labels): for label in all_labels: self.scores[label] = bayes.score(label, self.text) def compute_accuracy(self, all_labels): success = 0 for label in all_labels: score = self.scores[label] guessed_has_label = score['yes'] > score['no'] if guessed_has_label == (label in self.labels): success += 1 return success / len(all_labels) class Corpus(object): """ All the texts of all the documents, and their labels. """ def __init__(self, core): self.core = core self.documents = [] self.labels = set() self.bayes = None def load_all(self): docs = [] self.core.call_all("storage_get_all_docs", docs) total = len(docs) for (idx, (doc_id, doc_url)) in enumerate(docs): self.core.call_all( "on_progress", "load_docs", idx / total, "Loading document %s" % doc_id ) text = [] self.core.call_all("doc_get_text_by_url", text, doc_url) text = "\n\n".join(text) labels = set() self.core.call_all("doc_get_labels_by_url", labels, doc_url) labels = [label[0] for label in labels] labels.sort() for label in labels: self.labels.add(label) self.documents.append(Document(self.core, doc_id, text, labels)) self.documents.sort() self.core.call_all("on_progress", "load_docs", 1.0) def mk_bayes(self, n_features): self.bayes = Bayes(self.core, self.labels, n_features) def fit(self): for (idx, doc) in enumerate(self.documents): doc.fit(self.bayes, self.labels) def seal(self): self.bayes.seal() def compute_scores(self): total = len(self.documents) for (idx, doc) in enumerate(self.documents): self.core.call_all( "on_progress", "scoring", idx / total, "computing label scores on %s" % doc.doc_id ) doc.compute_scores(self.bayes, self.labels) self.core.call_all("on_progress", "scoring", 1.0) def compute_accuracy(self): accuracy_sum = 0 total = len(self.documents) for doc in self.documents: accuracy_sum += doc.compute_accuracy(self.labels) return accuracy_sum / total class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.interactive = False def get_interfaces(self): return [ 'shell', ] def get_deps(self): return [ { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'doc_labels', 'defaults': ['paperwork_backend.model.labels'], }, { 'interface': 'doc_text', 'defaults': [ 'paperwork_backend.model.hocr', 'paperwork_backend.model.pdf', ], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): parser.add_parser('test_sklearn_gaussian_with_hashes') def cmd_run(self, args): if args.command != 'test_sklearn_gaussian_with_hashes': return None corpus = Corpus(self.core) corpus.load_all() for two_pow in (16, 18, 20,): n_features = 2 ** two_pow corpus.mk_bayes(n_features) corpus.fit() corpus.seal() corpus.compute_scores() accuracy = corpus.compute_accuracy() print("n_features=2**{} --> accuracy={}".format(two_pow, accuracy)) return True paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/ocr/000077500000000000000000000000001417573700700262235ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/ocr/__init__.py000066400000000000000000000000001417573700700303220ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/ocr/pyocr.py000066400000000000000000000146611417573700700277410ustar00rootroot00000000000000import logging import pyocr import pyocr.builders import openpaperwork_core from ... import (_, sync) LOGGER = logging.getLogger(__name__) ID = "ocr" class OcrTransaction(sync.BaseTransaction): def __init__(self, plugin, sync, total_expected=-1): super().__init__(plugin.core, total_expected) self.priority = plugin.PRIORITY self.plugin = plugin self.sync = sync # for each document, we need to track which pages have already been # OCR-ed, which have been modified (cropping, rotation, ...) # and must be re-OCRed, and which have not been changed. self.page_tracker = self.core.call_success("page_tracker_get", ID) def __enter__(self): pass def __exit__(self, exc_type, exc_val, exc_tb): self.cancel() def _run_ocr_on_page( self, doc_id, doc_url, page_idx, page_nb, total_pages, wordless_only=False): if wordless_only: has_text = self.core.call_success( "page_has_text_by_url", doc_url, page_idx ) if has_text: # there is already some text on this page self.notify_progress( ID, _( "Document {doc_id} p{page_idx} has already some text." " No OCR run" ).format(doc_id=doc_id, page_idx=(page_idx + 1)), page_nb=page_nb, total_pages=total_pages ) return self.notify_progress( ID, _("Running OCR on document {doc_id} p{page_idx}").format( doc_id=doc_id, page_idx=(page_idx + 1) ), page_nb=page_nb, total_pages=total_pages ) self.plugin.ocr_page_by_url(doc_url, page_idx) def _run_ocr_on_modified_pages(self, doc_id, wordless_only=False): doc_url = self.core.call_success("doc_id_to_url", doc_id) need_end_notification = False modified_pages = list(self.page_tracker.find_changes(doc_id, doc_url)) for (page_nb, (change, page_idx)) in enumerate(modified_pages): # Run OCR on modified pages, but only if we are not synchronizing # with the work directory (--> if the user just added or modified # a document) if not self.sync and (change == 'new' or change == 'upd'): self._run_ocr_on_page( doc_id, doc_url, page_idx, page_nb, len(modified_pages), wordless_only ) need_end_notification = True self.page_tracker.ack_page(doc_id, doc_url, page_idx) if need_end_notification: self.notify_progress( ID, _("Running OCR"), page_nb=len(modified_pages), total_pages=len(modified_pages) ) def add_doc(self, doc_id): self._run_ocr_on_modified_pages(doc_id, wordless_only=True) super().add_doc(doc_id) def upd_doc(self, doc_id): self._run_ocr_on_modified_pages(doc_id, wordless_only=True) super().upd_doc(doc_id) def del_doc(self, doc_id): self.page_tracker.delete_doc(doc_id) super().del_doc(doc_id) def cancel(self): self.page_tracker.cancel() self.notify_done(ID) def commit(self): self.page_tracker.commit() self.notify_done(ID) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 1000 def get_interfaces(self): return [ "ocr", "syncable", # actually satisfied by the plugin 'doctracker' ] def get_deps(self): return [ { 'interface': 'doc_tracking', 'defaults': ['paperwork_backend.doctracker'], }, { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'ocr_settings', 'defaults': ['paperwork_backend.pyocr'], }, { 'interface': 'page_boxes', 'defaults': [ 'paperwork_backend.model.hocr', 'paperwork_backend.model.pdf', ], }, { 'interface': 'page_tracking', 'defaults': ['paperwork_backend.pagetracker'], }, { 'interface': 'pillow', 'defaults': [ 'openpaperwork_core.pillow.img', 'paperwork_backend.pillow.pdf', ], }, ] def init(self, core): super().init(core) self.core.call_all( "doc_tracker_register", ID, lambda sync, total_expected=-1: OcrTransaction( self, sync, total_expected ) ) def ocr_page_by_url(self, doc_url, page_idx): if self.core.call_success("ocr_is_enabled") is None: LOGGER.info("OCR is disabled") return LOGGER.info("Running OCR on page %d of %s", page_idx, doc_url) doc_id = self.core.call_success("doc_url_to_id", doc_url) if doc_id is not None: self.core.call_one( "mainloop_schedule", self.core.call_all, "on_page_modification_start", doc_id, page_idx ) try: page_img_url = self.core.call_success( "page_get_img_url", doc_url, page_idx ) ocr_tool = pyocr.get_available_tools()[0] LOGGER.info( "Will use tool '%s' on %s p%d (%s)", ocr_tool.get_name(), doc_url, page_idx, page_img_url ) ocr_langs = self.core.call_success("ocr_get_active_langs") img = self.core.call_success("url_to_pillow", page_img_url) boxes = ocr_tool.image_to_string( img, lang="+".join(ocr_langs), builder=pyocr.builders.LineBoxBuilder() ) self.core.call_all( "page_set_boxes_by_url", doc_url, page_idx, boxes ) except Exception as exc: LOGGER.error("OCR FAILED", exc_info=exc) finally: if doc_id is not None: self.core.call_one( "mainloop_schedule", self.core.call_all, "on_page_modification_end", doc_id, page_idx ) return True paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/orientation/000077500000000000000000000000001417573700700277735ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/orientation/__init__.py000066400000000000000000000000001417573700700320720ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/guesswork/orientation/pyocr.py000066400000000000000000000154701417573700700315100ustar00rootroot00000000000000import logging import PIL import PIL.Image import pyocr import pyocr.builders import openpaperwork_core from ... import (_, sync) LOGGER = logging.getLogger(__name__) ID = "orientation_guesser" class OrientationTransaction(sync.BaseTransaction): def __init__(self, plugin, sync, total_expected=-1): super().__init__(plugin.core, total_expected) self.priority = plugin.PRIORITY self.plugin = plugin self.sync = sync # for each document, we need to track on which pages we have already # guessed the orientation and on which page we didn't yet. self.page_tracker = self.core.call_success("page_tracker_get", ID) def __enter__(self): pass def __exit__(self, exc_type, exc_val, exc_tb): self.cancel() def _guess_page_orientation( self, doc_id, doc_url, page_idx, page_nb, total_pages): if self.core.call_success( "page_has_text_by_url", doc_url, page_idx ): self.notify_progress( ID, _( "Document {doc_id} p{page_idx} has already some text." " Not guessing page orientation." ).format(doc_id=doc_id, page_idx=(page_idx + 1)), page_nb=page_nb, total_pages=total_pages ) return self.notify_progress( ID, _( "Guessing orientation of" " document {doc_id} p{page_idx}" ).format(doc_id=doc_id, page_idx=(page_idx + 1)), page_nb=page_nb, total_pages=total_pages ) self.plugin.guess_page_orientation_by_url(doc_url, page_idx) def _guess_new_page_orientations(self, doc_id): doc_url = self.core.call_success("doc_id_to_url", doc_id) need_end_notification = False modified_pages = list(self.page_tracker.find_changes(doc_id, doc_url)) for (page_nb, (change, page_idx)) in enumerate(modified_pages): # Guess page orientation on new pages, but only if we are # not synchronizing with the work directory if not self.sync and change == 'new': self._guess_page_orientation( doc_id, doc_url, page_idx, page_nb, len(modified_pages) ) need_end_notification = True self.page_tracker.ack_page(doc_id, doc_url, page_idx) if need_end_notification: self.notify_progress( ID, _("Guessing page orientation"), page_nb=len(modified_pages), total_pages=len(modified_pages) ) def add_doc(self, doc_id): self._guess_new_page_orientations(doc_id) super().add_doc(doc_id) def upd_doc(self, doc_id): self._guess_new_page_orientations(doc_id) super().upd_doc(doc_id) def del_doc(self, doc_id): self.page_tracker.delete_doc(doc_id) super().del_doc(doc_id) def cancel(self): self.page_tracker.cancel() self.notify_done(ID) def commit(self): self.page_tracker.commit() self.notify_done(ID) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 2000 def get_interfaces(self): return [ "orientation_guesser", "syncable", # actually satisfied by the plugin 'doctracker' ] def get_deps(self): return [ { 'interface': 'doc_tracking', 'defaults': ['paperwork_backend.doctracker'] }, { 'interface': 'ocr_settings', 'defaults': ['paperwork_backend.pyocr'], }, { 'interface': 'page_tracking', 'defaults': ['paperwork_backend.pagetracker'], }, { 'interface': 'pillow', 'defaults': [ 'openpaperwork_core.pillow.img', 'paperwork_backend.pillow.pdf', ], }, ] def init(self, core): super().init(core) self.core.call_all( "doc_tracker_register", ID, lambda sync, total_expected=-1: OrientationTransaction( self, sync, total_expected ) ) def guess_page_orientation_by_url(self, doc_url, page_idx): if self.core.call_success("ocr_is_enabled") is None: LOGGER.info("OCR is disabled") return LOGGER.info( "Using OCR to guess orientation of page %d of %s", page_idx, doc_url ) doc_id = self.core.call_success("doc_url_to_id", doc_url) if doc_id is not None: self.core.call_one( "mainloop_schedule", self.core.call_all, "on_page_modification_start", doc_id, page_idx ) try: page_img_url = self.core.call_success( "page_get_img_url", doc_url, page_idx ) for ocr_tool in pyocr.get_available_tools(): LOGGER.info( "Orientation guessing: Will use tool '%s'", ocr_tool.get_name() ) if ocr_tool.can_detect_orientation(): break LOGGER.warning( "Orientation guessing: Tool '%s' cannot detect" " orientation", ocr_tool.get_name() ) else: LOGGER.warning( "Orientation guessing: No tool found able to detect" " orientation" ) return None ocr_langs = self.core.call_success("ocr_get_active_langs") img = self.core.call_success("url_to_pillow", page_img_url) try: r = ocr_tool.detect_orientation(img, lang="+".join(ocr_langs)) except pyocr.PyocrException as exc: LOGGER.warning( "Orientation guessing: Failed to guess orientation", exc_info=exc ) return None angle = r['angle'] if angle == 0: return 0 t_angle = { 90: PIL.Image.ROTATE_90, 180: PIL.Image.ROTATE_180, 270: PIL.Image.ROTATE_270, }[angle] img = img.transpose(t_angle) page_img_url = self.core.call_success( "page_get_img_url", doc_url, page_idx, write=True ) self.core.call_success("pillow_to_url", img, page_img_url) finally: if doc_id is not None: self.core.call_one( "mainloop_schedule", self.core.call_all, "on_page_modification_end", doc_id, page_idx ) return angle paperwork-2.1.1/paperwork-backend/src/paperwork_backend/i18n/000077500000000000000000000000001417573700700241665ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/i18n/__init__.py000066400000000000000000000000001417573700700262650ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/i18n/pycountry.py000066400000000000000000000014601417573700700266150ustar00rootroot00000000000000import pycountry import openpaperwork_core from .. import _ LANGUAGES = { "English": _("English"), "French": _("French"), "German": _("German"), } class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['i18n_lang'] def i18n_lang_iso639_3_to_full(self, iso): attrs = [ 'iso639_3_code', 'terminology', 'alpha_3', ] for attr in attrs: try: r = pycountry.pycountry.languages.get(**{attr: iso}) if r is None: continue r = r.name if r in LANGUAGES: return LANGUAGES[r] return r except (KeyError, UnicodeDecodeError): pass return iso paperwork-2.1.1/paperwork-backend/src/paperwork_backend/i18n/scanner.py000066400000000000000000000027141417573700700261750ustar00rootroot00000000000000import logging import re import openpaperwork_core from .. import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): RE_SPLIT_SOURCE_NAME = [ re.compile(r'([()])'), re.compile(r'(\W)'), ] def __init__(self): super().__init__() # Need the l10n plugin to be loaded first before getting the # translations self.keywords = {} def get_interfaces(self): return ['i18n_scanner'] def get_deps(self): return [ { 'interface': 'l10n', 'defaults': ['openpaperwork_core.l10n.python'], }, ] def init(self, core): super().init(core) self.keywords = { "centrally aligned": _("centrally aligned"), "feeder": _("Feeder"), "flatbed": _("Flatbed"), "left aligned": _("left aligned"), "right aligned": _("right aligned"), } def i18n_scanner_source(self, source_name): original = source_name for regex in self.RE_SPLIT_SOURCE_NAME: source_name = regex.split(source_name) for i in range(0, len(source_name)): if source_name[i].lower() in self.keywords: source_name[i] = self.keywords[source_name[i].lower()] source_name = "".join(source_name) LOGGER.debug("I18n: %s --> %s", original, source_name) return source_name paperwork-2.1.1/paperwork-backend/src/paperwork_backend/imgedit/000077500000000000000000000000001417573700700250315ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/imgedit/__init__.py000066400000000000000000000013341417573700700271430ustar00rootroot00000000000000class AbstractImgEditor(object): def transform(self, img, preview=False): """ Apply the transformation operation. Arguments: img -- image to transform preview -- indicates if we intend to use the result as a preview or as final result """ assert() def transform_frame(self, img_size, frame): """ From the frame applied to the original image to the resulting image. """ return frame def untransform_frame(self, img_size, frame): """ From the frame on the resulting image to the original image. """ return frame def __eq__(self, o): return type(self) == type(o) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/imgedit/color.py000066400000000000000000000020571417573700700265250ustar00rootroot00000000000000import pillowfight import openpaperwork_core from . import AbstractImgEditor class ColorImgEditor(AbstractImgEditor): def transform(self, img, preview=False): return pillowfight.ace( img, samples=50 if preview else 200 ) class Plugin(openpaperwork_core.PluginBase): NAME = 'color_equalization' def get_interfaces(self): return ['img_editor'] def img_editor_get_names(self, out: list): out.append(self.NAME) def img_editor_get(self, name, *args, **kwargs): if name != self.NAME: return None return ColorImgEditor() def img_editor_set(self, inout: list, name, *args, **kwargs): if name != self.NAME: return None c = ColorImgEditor() if c in inout: # an ACE editor is already in the list return inout.append(c) def img_editor_unset(self, inout: list, name): if name != self.NAME: return None c = ColorImgEditor() while c in inout: inout.remove(c) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/imgedit/crop.py000066400000000000000000000045701417573700700263540ustar00rootroot00000000000000import logging import PIL import PIL.Image import pillowfight import openpaperwork_core from . import AbstractImgEditor LOGGER = logging.getLogger(__name__) class CropImgEditor(AbstractImgEditor): def __init__(self, frame=None, **kwargs): self.frame = frame def transform(self, img, preview=False): if self.frame is None: # optimize the default frame LOGGER.info("Guessing new default cropping frame ...") # It's actually faster to resize down the image than look # for the scan borders on the full-size image. smaller_img = img.resize( (int(img.size[0] / 2), int(img.size[1] / 2)), PIL.Image.ANTIALIAS ) frame = pillowfight.find_scan_borders(smaller_img) if frame[0] >= frame[2] or frame[1] >= frame[3]: LOGGER.info("Failed to guess a cropping frame") # defaulting to the whole image self.frame = (0, 0, img.size[0], img.sixe[1]) else: # if we have found a valid frame LOGGER.info("Guessed cropping frame: %s", frame) self.frame = ( frame[0] * 2, frame[1] * 2, frame[2] * 2, frame[3] * 2 ) if preview: # we do not crop the preview. The UI displays a frame on top # of it showing where the cropping will happen. return img img = img.crop(self.frame) return img class Plugin(openpaperwork_core.PluginBase): NAME = 'cropping' def get_interfaces(self): return ['img_editor'] def img_editor_get_names(self, out: list): out.append(self.NAME) def img_editor_get(self, name, *args, **kwargs): if name != self.NAME: return None return CropImgEditor(**kwargs) def img_editor_set(self, inout: list, name, *args, **kwargs): if name != self.NAME: return None c = CropImgEditor(**kwargs) try: # Check if we already have a CropImgEditor in the list. # If so, do nothing inout.index(c) except ValueError: inout.append(c) def img_editor_unset(self, inout: list, name): if name != self.NAME: return None c = CropImgEditor() while c in inout: inout.remove(c) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/imgedit/rotate.py000066400000000000000000000062271417573700700267100ustar00rootroot00000000000000import openpaperwork_core import PIL import PIL.Image from . import AbstractImgEditor class RotationImgEditor(AbstractImgEditor): def __init__(self, angle): self.angle = angle % 360 def transform(self, img, preview=False): # Pillow operates counter-clockwise, we operate clockwise. if self.angle == 0: return img angle = { 90: PIL.Image.ROTATE_90, 180: PIL.Image.ROTATE_180, 270: PIL.Image.ROTATE_270, }[self.angle] return img.transpose(angle) def _transform_frame(self, img_size, frame, transform_pt): frame = ( transform_pt(img_size, (frame[0], frame[1])), transform_pt(img_size, (frame[2], frame[3])), ) return ( min(frame[0][0], frame[1][0]), min(frame[0][1], frame[1][1]), max(frame[0][0], frame[1][0]), max(frame[0][1], frame[1][1]), ) def transform_frame(self, img_size, frame): return self._transform_frame(img_size, frame, self.transform_point) def untransform_frame(self, img_size, frame): return self._transform_frame(img_size, frame, self.untransform_point) def _transform_pt(self, img_size, pt, angle): r = { 0: pt, 90: (pt[1], img_size[0] - pt[0]), 270: (img_size[1] - pt[1], pt[0]), 180: (img_size[0] - pt[0], img_size[1] - pt[1]), }[angle] return r def transform_point(self, img_size, pt): return self._transform_pt(img_size, pt, self.angle) def untransform_point(self, img_size, pt): # we are given the image size before we apply our transformation # but here we want to go the other way around img_size = { 0: img_size, 90: (img_size[1], img_size[0]), 270: (img_size[1], img_size[0]), 180: img_size, }[self.angle] angle = ((-1 * self.angle) % 360) return self._transform_pt(img_size, pt, angle) class Plugin(openpaperwork_core.PluginBase): NAME = 'rotation' def get_interfaces(self): return ['img_editor'] def img_editor_get_names(self, out: list): out.append(self.NAME) def img_editor_get(self, name, *args, **kwargs): if name != self.NAME: return None angle = kwargs.pop('angle') return RotationImgEditor(angle) def img_editor_set(self, inout: list, name, *args, **kwargs): if name != self.NAME: return None angle = kwargs.pop('angle') c = RotationImgEditor(angle) # Check if we already have a RotationImgEditor in the list. # If so, update it instead of adding another one # Only if its the latest element in the list -> otherwise we may # mess up the cropping frame if len(inout) > 0 and inout[-1] == c: inout[-1].angle += angle inout[-1].angle %= 360 else: inout.append(c) def img_editor_unset(self, inout: list, name): if name != self.NAME: return None c = RotationImgEditor(0) while c in inout: inout.remove(c) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/index/000077500000000000000000000000001417573700700245165ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/index/__init__.py000066400000000000000000000000001417573700700266150ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/index/whoosh.py000066400000000000000000000350061417573700700264030ustar00rootroot00000000000000import datetime import logging import time import whoosh.fields import whoosh.index import whoosh.qparser import whoosh.query import whoosh.sorting import openpaperwork_core from .. import (_, sync) LOGGER = logging.getLogger(__name__) ID = "index" WHOOSH_SCHEMA = whoosh.fields.Schema( docid=whoosh.fields.ID(stored=True, unique=True, sortable=True), docfilehash=whoosh.fields.ID(), content=whoosh.fields.TEXT(spelling=True), label=whoosh.fields.KEYWORD(commas=True, scorable=True), date=whoosh.fields.DATETIME(sortable=True), last_read=whoosh.fields.DATETIME(stored=True), ) class CustomFuzzySearch(whoosh.qparser.query.FuzzyTerm): def __init__( self, fieldname, text, boost=1.0, maxdist=1, prefixlength=0, constantscore=True ): whoosh.qparser.query.FuzzyTerm.__init__( self, fieldname, text, boost, maxdist, prefixlength, constantscore=True ) class WhooshTransaction(sync.BaseTransaction): """ Transaction to apply on the index. Methods may be slow but they are thread-safe. """ def __init__(self, plugin, total_expected=-1): super().__init__(plugin.core, total_expected) self.priority = plugin.PRIORITY LOGGER.debug("Starting Whoosh index transaction") self.core = plugin.core self.writer = None self.modified = 0 self.writer = plugin.index.writer() def __enter__(self): pass def __exit__(self, exc_type, exc_val, exc_tb): self.cancel() def __del__(self): self.cancel() def _update_doc_in_index(self, doc_id): """ Collect infos on the document and add/update a document in the index """ doc_url = self.core.call_success("doc_id_to_url", doc_id) doc_mtime = self.core.call_success("doc_get_mtime_by_url", doc_url) if doc_mtime is None: doc_mtime = 0 doc_mtime = datetime.datetime.fromtimestamp(doc_mtime) doc_hash = self.core.call_success("doc_get_hash_by_url", doc_url) if doc_hash is None: # we get a hash only for PDF documents, not image documents. doc_hash = "undefined" else: doc_hash = ("%X" % doc_hash) doc_text = [] self.core.call_all("doc_get_text_by_url", doc_text, doc_url) doc_text = "\n\n".join(doc_text) doc_text = self.core.call_success("i18n_strip_accents", doc_text) doc_labels = set() self.core.call_all("doc_get_labels_by_url", doc_labels, doc_url) doc_labels = ",".join([label[0] for label in doc_labels]) doc_labels = self.core.call_success("i18n_strip_accents", doc_labels) doc_date = self.core.call_success("doc_get_date_by_id", doc_id) if doc_date is None: doc_date = datetime.datetime(year=1970, month=1, day=1) query = whoosh.query.Term("docid", doc_id) self.writer.delete_by_query(query) self.writer.update_document( docid=doc_id, docfilehash=doc_hash, content=doc_text, label=doc_labels, date=doc_date, last_read=doc_mtime ) def add_doc(self, doc_id): LOGGER.info("Adding document '%s' to index", doc_id) self.notify_progress( ID, _("Indexing new document %s") % doc_id ) self._update_doc_in_index(doc_id) self.modified += 1 super().add_doc(doc_id) def del_doc(self, doc_id): LOGGER.info("Removing document '%s' from index", doc_id) self.notify_progress( ID, _("Removing document %s from index") % doc_id ) query = whoosh.query.Term("docid", doc_id) self.writer.delete_by_query(query) self.modified += 1 super().del_doc(doc_id) def upd_doc(self, doc_id): LOGGER.info("Updating document '%s' in index", doc_id) self.notify_progress( ID, _("Indexing updated document %s") % doc_id ) self._update_doc_in_index(doc_id) self.modified += 1 super().upd_doc(doc_id) def unchanged_doc(self, doc_id): self.notify_progress( ID, _("Examining document %s: unchanged") % (doc_id) ) super().unchanged_doc(doc_id) def cancel(self): if self.writer is None: return self.core.call_one( "mainloop_schedule", self.core.call_all, 'on_index_cancel' ) LOGGER.info("Canceling transaction") self.writer.cancel() self.writer = None self.core.call_one( "mainloop_schedule", self.core.call_all, "on_index_updated" ) self.notify_done(ID) def commit(self): self.notify_progress( ID, _("Committing changes in the index ...") ) self.core.call_one( "mainloop_schedule", self.core.call_all, 'on_index_commit_start' ) if self.modified <= 0: LOGGER.info( "commit() called but nothing to commit." " Cancelling transaction" ) self.writer.cancel() self.writer = None else: LOGGER.info( "Committing %d changes to Whoosh index", self.modified ) self.writer.commit() self.writer = None self.notify_done(ID) self.core.call_success( "mainloop_schedule", self.core.call_all, 'on_index_commit_end' ) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.obs_callbacks = [] self.query_parsers = { 'strict': [], 'fuzzy': [], } self.index = None self.local_dir = None self.index_dir = None def get_interfaces(self): return [ "index", "suggestions", "syncable", ] def get_deps(self): return [ { 'interface': 'data_versioning', 'defaults': ['openpaperwork_core.data_versioning'], }, { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'data_dir_handler', 'defaults': ['paperwork_backend.datadirhandler'], }, { 'interface': 'thread', 'defaults': ['openpaperwork_core.thread.simple'], }, # Optional dependencies: # { # 'interface': 'page_boxes', # 'defaults': [ # 'paperwork_backend.model.hocr', # 'paperwork_backend.model.pdf', # ], # }, # { # 'interface': 'doc_hash', # 'defaults': ['paperwork_backend.model.pdf'], # }, # { # 'interface': 'doc_labels', # 'defaults': ['paperwork_backend.model.labels'], # }, ] def init(self, core): super().init(core) self._init() def _init(self): data_dir = self.core.call_success( "data_dir_handler_get_individual_data_dir" ) self.index_dir = self.core.call_success( "fs_join", data_dir, "index" ) need_index_rewrite = True while need_index_rewrite: try: LOGGER.info( "Opening Whoosh index '%s' ...", self.index_dir ) self.index = whoosh.index.open_dir( self.core.call_success("fs_unsafe", self.index_dir) ) # check that the schema is up-to-date # We use the string representation of the schemas, because # previous versions of whoosh don't always implement __eq__ if str(self.index.schema) != str(WHOOSH_SCHEMA): raise Exception("Index version mismatch") need_index_rewrite = False except Exception as exc: LOGGER.warning( "Failed to open index '%s': %s." " Will rebuild index from scratch", self.index_dir, exc ) if need_index_rewrite: self._destroy() LOGGER.info("Creating a new index") self.core.call_success("fs_mkdir_p", self.index_dir) new_index = whoosh.index.create_in( self.core.call_success("fs_unsafe", self.index_dir), WHOOSH_SCHEMA ) new_index.close() LOGGER.info("Index '%s' created", self.index_dir) self.query_parsers = { 'fuzzy': [ whoosh.qparser.MultifieldParser( ["label", "content"], schema=self.index.schema, termclass=CustomFuzzySearch ), whoosh.qparser.MultifieldParser( ["label", "content"], schema=self.index.schema, termclass=whoosh.qparser.query.Prefix ), ], 'strict': [ whoosh.qparser.MultifieldParser( ["label", "content"], schema=self.index.schema, termclass=whoosh.query.Term ), ], } def on_data_dir_changed(self): self._close() self._init() def _close(self): LOGGER.info("Closing Whoosh index") if self.index is not None: self.index.close() self.index = None def _destroy(self): self._close() if self.core.call_success("fs_exists", self.index_dir) is not None: LOGGER.warning("Destroying the index ...") self.core.call_success("fs_rm_rf", self.index_dir) LOGGER.warning("Index destroyed") def doc_transaction_start(self, out: list, total_expected=-1): out.append(WhooshTransaction(self, total_expected)) def index_search(self, out: list, query, limit=None, search_type='fuzzy'): start = time.time() out_set = set() query = query.strip() query = self.core.call_success("i18n_strip_accents", query) if query == "": queries = [whoosh.query.Every()] else: queries = [] for parser in self.query_parsers[search_type]: queries.append(parser.parse(query)) with self.index.searcher() as searcher: for q in queries: facet = whoosh.sorting.FieldFacet("docid", reverse=True) results = searcher.search(q, limit=limit, sortedby=facet) has_results = False for result in results: has_results = True doc_id = result['docid'] doc_url = self.core.call_success("doc_id_to_url", doc_id) if doc_url is None: continue out_set.add((doc_id, doc_url)) if limit is not None and len(out_set) >= limit: break if has_results: break out += out_set stop = time.time() LOGGER.info( "Search [%s] took %dms (limit=%s, type=%s)", query, (stop - start) * 1000, limit, search_type ) def index_get_doc_id_by_hash(self, doc_hash): doc_hash = "%X" % doc_hash with self.index.searcher() as searcher: results = searcher.search( whoosh.query.Term('docfilehash', doc_hash) ) if len(results) <= 0: return None return results[0] def suggestion_get(self, out: set, sentence): query_parser = self.query_parsers['strict'][0] query = query_parser.parse(sentence) with self.index.searcher() as searcher: corrected = searcher.correct_query( query, sentence, correctors={ 'content': searcher.corrector("content"), 'label': searcher.corrector("label"), } ) if corrected.query != query: out.add(corrected.string) def sync(self, promises: list): """ Requests the document list from the document storage plugin and updates the index accordingly. This call is asynchronous and use the main loop to do its job. """ storage_all_docs = [] promise = openpaperwork_core.promise.ThreadedPromise( self.core, self.core.call_all, args=("storage_get_all_docs", storage_all_docs,) ) promise = promise.then(lambda *args, **kwargs: None) class IndexDoc(object): def __init__(s, index_result): (s.key, s.extra) = index_result def get_index_docs(): with self.index.searcher() as searcher: index_all_docs = searcher.search( whoosh.query.Every(), limit=None ) index_all_docs = [ (result['docid'], result['last_read']) for result in index_all_docs ] index_all_docs = [IndexDoc(r) for r in index_all_docs] return index_all_docs promise = promise.then( lambda: ( [ sync.StorageDoc(self.core, doc[0], doc[1]) for doc in storage_all_docs ], get_index_docs() ) ) promise = promise.then( lambda args: ( args[0], args[1], [WhooshTransaction( self, max(len(storage_all_docs), len(args[1])) )] ) ) promise = promise.then(lambda args: sync.Syncer( self.core, ["whoosh"], args[0], args[1], args[2] )) promise = promise.then(openpaperwork_core.promise.ThreadedPromise( self.core, lambda syncer: syncer.run() )) promises.append(promise) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/l10n/000077500000000000000000000000001417573700700241615ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/l10n/__init__.py000066400000000000000000000007351417573700700262770ustar00rootroot00000000000000import openpaperwork_core class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['l10n_init'] def get_deps(self): return [ { 'interface': 'l10n', 'defaults': ['openpaperwork_core.l10n.python'], }, ] def init(self, core): super().init(core) self.core.call_all( "l10n_load", "paperwork_backend.l10n", "paperwork_backend" ) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/model/000077500000000000000000000000001417573700700245075ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/model/__init__.py000066400000000000000000000017631417573700700266270ustar00rootroot00000000000000import openpaperwork_core class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['nb_pages'] def doc_get_nb_pages_by_url(self, doc_url): out = [] self.core.call_all("doc_internal_get_nb_pages_by_url", out, doc_url) r = max(out, default=0) if r == 0: return None return r def page_get_hash_by_url(self, doc_url, page_idx): out = [] self.core.call_all( "page_internal_get_hash_by_url", out, doc_url, page_idx ) r = 0 for h in out: r ^= h return r def doc_get_mtime_by_url(self, doc_url): out = [] self.core.call_all("doc_internal_get_mtime_by_url", out, doc_url) return max(out, default=None) def page_get_mtime_by_url(self, doc_url, page_idx): out = [] self.core.call_all( "page_internal_get_mtime_by_url", out, doc_url, page_idx ) return max(out, default=None) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/model/converted.py000066400000000000000000000177021417573700700270610ustar00rootroot00000000000000""" Takes of various document format that are actually converted to PDF files so Paperwork can read them quickly """ import logging import openpaperwork_core import openpaperwork_core.promise from .. import _ LOGGER = logging.Logger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 200 def __init__(self): super().__init__() self.pending_doc_urls = set() self.file_types_by_ext = {} self.file_types_by_mime = {} self.cache_hash = {} def get_interfaces(self): return [ "doc_convert_and_import", "doc_hash", "sync", ] def get_deps(self): return [ { 'interface': 'doc_converter', 'defaults': ['paperwork_backend.converter.libreoffice'], }, { 'interface': 'doc_pdf_import', 'defaults': ['paperwork_backend.model.pdf'], }, { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def init(self, core): super().init(core) file_types = set() self.core.call_all("converter_get_file_types", file_types) self.file_types_by_ext = { ext: mime for (mime, ext, human_name) in file_types } self.file_types_by_mime = { mime: ext for (mime, ext, human_name) in file_types } def _is_converted(self, doc_url): if not self.core.call_success("fs_isdir", doc_url): return (None, None) for doc_file_url in self.core.call_success("fs_listdir", doc_url): doc_filename = self.core.call_success("fs_basename", doc_file_url) if "." not in doc_filename: continue (doc_filename, doc_ext) = doc_filename.rsplit(".", 1) doc_filename = doc_filename.lower() doc_ext = doc_ext.lower() if doc_filename == "doc" and doc_ext in self.file_types_by_ext: return (doc_file_url, doc_ext) return (None, None) else: # not a converted document return (None, None) def _update_pdf(self, doc_id, doc_url, doc_file_url, doc_ext): LOGGER.debug( "Document %s: %s (%s)", doc_id, doc_file_url, self.file_types_by_ext[doc_ext] ) doc_mtime = self.core.call_success("fs_get_mtime", doc_file_url) pdf = self.core.call_success( "doc_get_pdf_url_by_url", doc_url, write=True ) if not self.core.call_success("fs_exists", pdf): pdf_mtime = -1 else: pdf_mtime = self.core.call_success("fs_get_mtime", pdf) if pdf_mtime >= doc_mtime: LOGGER.debug("Document %s: PDF is up-to-date") return False LOGGER.info( "Document %s: Updating PDF: %s --> %s", doc_id, doc_file_url, pdf ) self.core.call_all( "on_progress", "converting", 0.0, _("Converting document %s to PDF ...") % self.core.call_success( "fs_basename", doc_file_url ) ) self.core.call_success( "convert_file_to_pdf", doc_file_url, self.file_types_by_ext[doc_ext], pdf ) self.core.call_all("flush_doc_cache", doc_url) LOGGER.info("PDF updated") self.core.call_all("on_progress", "converting", 1.0) return True def page_get_img_url(self, doc_url, page_idx, write=False): if page_idx != 0: return # prevent double conversion if doc_url in self.pending_doc_urls: LOGGER.debug("Doc %s will already be checked", doc_url) return self.pending_doc_urls.add(doc_url) (doc_file_url, doc_ext) = self._is_converted(doc_url) if doc_file_url is None: self.pending_doc_urls.remove(doc_url) return doc_id = self.core.call_success("doc_url_to_id", doc_url) LOGGER.info("Checking conversion of document %s is up-to-date", doc_id) promise = openpaperwork_core.promise.ThreadedPromise( self.core, self._update_pdf, args=( doc_id, doc_url, doc_file_url, doc_ext ) ) def do_transaction(has_changed): if not has_changed: return transactions = [] self.core.call_all("doc_transaction_start", transactions, 1) transactions.sort(key=lambda t: -t.priority) try: for transaction in transactions: transaction.del_doc(doc_id) for transaction in transactions: transaction.commit() except Exception: for transaction in transactions: transaction.cancel() promise = promise.then(openpaperwork_core.promise.ThreadedPromise( self.core, do_transaction )) promise = promise.then(self.pending_doc_urls.remove, doc_url) self.core.call_success("transaction_schedule", promise) return None def _check_all_docs(self): if len(self.file_types_by_ext) <= 0: LOGGER.info("No file converter available") return LOGGER.info("Checking all converted documents ...") all_docs = [] self.core.call_all("storage_get_all_docs", all_docs, only_valid=False) self.pending_doc_urls.update((x[1] for x in all_docs)) msg = _("Checking converted documents") self.core.call_all("on_progress", "converted_check", 0.0, msg) total = len(all_docs) for (idx, (doc_id, doc_url)) in enumerate(all_docs): (doc_file_url, doc_ext) = self._is_converted(doc_url) if doc_file_url is None: continue self._update_pdf(doc_id, doc_url, doc_file_url, doc_ext) self.pending_doc_urls.remove(doc_url) if idx % 100 == 0: self.core.call_all( "on_progress", "converted_check", idx / total, msg ) self.core.call_all("on_progress", "converted_check", 1.0) LOGGER.info("All converted documents have been checked") def sync(self, promises: list): promise = openpaperwork_core.promise.ThreadedPromise( self.core, self._check_all_docs ) promises.insert(0, promise) def doc_convert_and_import(self, file_url): mime = self.core.call_success("fs_get_mime", file_url) if mime is not None: file_ext = self.file_types_by_mime[mime] elif "." in file_url: file_ext = file_url.rsplit(".", 1)[-1].lower() else: LOGGER.error("Failed to figure out file type of '%s'", file_url) return (None, None) file_name = "doc." + file_ext (doc_id, doc_url) = self.core.call_success("storage_get_new_doc") dst_file_url = self.core.call_success("fs_join", doc_url, file_name) self.core.call_success("fs_mkdir_p", doc_url) try: self.core.call_success("fs_copy", file_url, dst_file_url) self._update_pdf(doc_id, doc_url, dst_file_url, file_ext) return (doc_id, doc_url) except Exception: self.core.call_success("fs_rm_rf", doc_url, trash=False) raise def doc_get_hash_by_url(self, doc_url): (doc_file_url, doc_ext) = self._is_converted(doc_url) if doc_file_url is None: return None if doc_url not in self.cache_hash: h = self.core.call_success("fs_hash", doc_file_url) self.cache_hash[doc_url] = h return self.cache_hash[doc_url] paperwork-2.1.1/paperwork-backend/src/paperwork_backend/model/extra_text.py000066400000000000000000000033761417573700700272610ustar00rootroot00000000000000#!/usr/bin/python3 import logging import openpaperwork_core EXTRA_TEXT_FILENAME = 'extra.txt' LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ "doc_text", "extra_text", ] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, ] def doc_internal_get_mtime_by_url(self, out: list, doc_url): extra_url = self.core.call_success( "fs_join", doc_url, EXTRA_TEXT_FILENAME ) if self.core.call_success("fs_exists", extra_url) is None: return out.append(self.core.call_success("fs_get_mtime", extra_url)) def doc_get_text_by_url(self, out: list, doc_url): self.doc_get_extra_text_by_url(out, doc_url) def doc_get_extra_text_by_url(self, out: list, doc_url): extra_url = self.core.call_success( "fs_join", doc_url, EXTRA_TEXT_FILENAME ) if self.core.call_success("fs_exists", extra_url) is None: return with self.core.call_success("fs_open", extra_url, 'r') as fd: out.append(fd.read()) def doc_set_extra_text_by_url(self, doc_url, text): if not self.core.call_success("fs_isdir", doc_url): # Can happen on integrated documentation PDF files LOGGER.warning( "%s is not a directory. Cannot set extra text", doc_url ) return extra_url = self.core.call_success( "fs_join", doc_url, EXTRA_TEXT_FILENAME ) with self.core.call_success("fs_open", extra_url, 'w') as fd: fd.write(text) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/model/fake.py000066400000000000000000000205641417573700700257760ustar00rootroot00000000000000import datetime import time import openpaperwork_core from . import workdir class Plugin(openpaperwork_core.PluginBase): PRIORITY = 10000 def __init__(self): super().__init__() # expected in self.docs: # [ # { # 'id': 'some_id', # 'url': 'file:///some_work_dir/some_id', # 'mtime': 12345, # unix timestamp # 'labels': [(label_name, label_color), ...] # 'hash': 12345, # optional # 'text': "pouet", # optional # 'page_boxes: [ # optional # [LineBox, LineBox, ...], # page 0 # [LineBox, LineBox, ...], # page 1 # (...) # ], # 'page_imgs': [ # optional # (img_url, PIL.Image), # page 0 # (img_url, PIL.Image), # page 1 # ], # 'page_mtimes': [ # optional # (img_url, mtime), # page 0 # (img_url, mtime), # page 1 # ], # 'page_hashes': [ # optional # (img_url, hash), # page 0 # (img_url, hash), # page 1 # ], # 'page_paper_sizes': [ # optional # (img_url, hash), # page 0 # (img_url, hash), # page 1 # ], # }, # (...) # ] self.docs = [] self.new_doc_idx = 0 def get_interfaces(self): return [ "doc_hash", "doc_labels", "doc_text", "doc_type", "document_storage", "page_boxes", "page_paper", "pillow", ] def get_deps(self): return [] def storage_get_id(self): return "file:///home/jflesch/papers" def storage_get_all_docs(self, out: list, only_valid=True): out += [ (doc['id'], doc['url']) for doc in self.docs ] def doc_id_to_url(self, doc_id, existing=True): for doc in self.docs: if doc['id'] == doc_id: return doc['url'] return None def doc_url_to_id(self, doc_url): for doc in self.docs: if doc['url'] == doc_url: return doc['id'] return None def is_doc(self, doc_url): return True def doc_get_hash_by_url(self, doc_url): for doc in self.docs: if doc['url'] == doc_url: if 'hash' in doc: return doc['hash'] def doc_get_mtime_by_url(self, doc_url): for doc in self.docs: if doc['url'] == doc_url: return doc['mtime'] def doc_get_nb_pages_by_url(self, doc_url): for doc in self.docs: if doc['url'] == doc_url: l_boxes = len(doc['page_boxes']) if 'page_boxes' in doc else 0 l_imgs = len(doc['page_imgs']) if 'page_imgs' in doc else 0 l_mtimes = ( len(doc['page_mtimes']) if 'page_mtimes' in doc else 0 ) l_hashes = ( len(doc['page_hashes']) if 'page_hashes' in doc else 0 ) l_paper_sizes = ( len(doc['page_paper_sizes']) if 'page_paper_sizes' in doc else 0 ) return max(l_boxes, l_imgs, l_mtimes, l_hashes, l_paper_sizes) return None def doc_get_text_by_url(self, out: list, doc_url): for doc in self.docs: if doc['url'] == doc_url: out.append(doc['text']) def doc_get_labels_by_url(self, out: set, doc_url): for doc in self.docs: if doc['url'] == doc_url: out.update(doc['labels']) def doc_add_label_by_url(self, doc_url, label, color=None): if color is None: all_labels = set() self.labels_get_all(all_labels) for (label_name, c) in all_labels: if label_name == label: color = c break else: raise Exception( "label {} provided without color," " but label is unknown".format(label) ) for doc in self.docs: if doc['url'] == doc_url: doc['labels'].add((label, color)) return True def page_has_text_by_url(self, doc_url, page_idx): for doc in self.docs: if doc['url'] == doc_url: if page_idx >= len(doc['page_boxes']): return None return True return None def page_get_boxes_by_url(self, doc_url, page_idx): for doc in self.docs: if doc['url'] == doc_url: if page_idx >= len(doc['page_boxes']): return None return doc['page_boxes'][page_idx] return None def page_set_boxes_by_url(self, doc_url, page_idx, boxes): for doc in self.docs: if doc['url'] == doc_url: if page_idx >= len(doc['page_boxes']): missing = page_idx + 1 - len(doc['page_boxes']) assert(missing >= 1) doc['page_boxes'] += ([None] * missing) doc['page_boxes'][page_idx] = boxes text = "" for page_boxes in doc['page_boxes']: if text != "": text += "\n\n" if page_boxes is None: continue for line_boxes in page_boxes: text += line_boxes.content + "\n" doc['text'] = text return None def page_get_img_url(self, doc_url, page_idx, write=False): for doc in self.docs: if doc['url'] == doc_url: for k in [ 'page_imgs', 'page_mtimes', 'page_hashes', 'page_sizes' ]: if k in doc: if page_idx >= len(doc[k]): return None return doc[k][page_idx][0] if write: return "file:///some_doc/new_page.jpeg" else: return None return None def url_to_pillow(self, img_url): for doc in self.docs: if 'page_imgs' not in doc: continue for (page_img_url, img) in doc['page_imgs']: if page_img_url == img_url: return img return None def labels_get_all(self, out: set): for doc in self.docs: out.update(doc['labels']) def storage_get_new_doc(self): self.new_doc_idx += 1 doc = { 'url': 'file:///some_work_dir/{}'.format(self.new_doc_idx), 'id': str(self.new_doc_idx), 'mtime': time.time(), 'labels': [], } self.docs.append(doc) return (doc['id'], doc['url']) def doc_get_date_by_id(self, doc_id): # Doc id is expected to have this format: # YYYYMMDD_hhmm_ss_NN_something_else doc_id = doc_id.split("_", 3) doc_id = "_".join(doc_id[:3]) try: return datetime.datetime.strptime(doc_id, workdir.DOCNAME_FORMAT) except ValueError: return None def storage_delete_doc_id(self, doc_id): for (idx, doc) in enumerate(self.docs[:]): if doc['id'] == doc_id: self.docs.pop(idx) return True def page_delete(self, doc_url, page_idx): raise NotImplementedError() def page_get_mtime_by_url(self, doc_url, page_idx): for doc in self.docs: if doc['url'] != doc_url: continue if 'page_mtimes' not in doc: continue return doc['page_mtimes'][page_idx][1] def page_get_hash_by_url(self, doc_url, page_idx): for doc in self.docs: if doc['url'] != doc_url: continue if 'page_hashes' not in doc: continue return doc['page_hashes'][page_idx][1] def page_get_paper_size_by_url(self, doc_url, page_idx): for doc in self.docs: if doc['url'] != doc_url: continue if 'page_paper_sizes' not in doc: continue return doc['page_paper_sizes'][page_idx][1] paperwork-2.1.1/paperwork-backend/src/paperwork_backend/model/hocr.py000066400000000000000000000146511417573700700260230ustar00rootroot00000000000000import logging import re import xml.etree.cElementTree as etree import pyocr import pyocr.builders import openpaperwork_core from . import util LOGGER = logging.getLogger(__name__) PAGE_FILENAME_FMT = "paper.{}.words" PAGE_FILENAME_REGEX = re.compile(r"paper\.(\d+)\.words") class Plugin(openpaperwork_core.PluginBase): PRIORITY = 100 def get_interfaces(self): return [ "doc_text", "page_boxes", 'pages', ] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, ] def doc_internal_get_mtime_by_url(self, out: list, doc_url): mtime = util.get_doc_mtime(self.core, doc_url, PAGE_FILENAME_REGEX) if mtime is None: return out.append(mtime) def page_internal_get_mtime_by_url(self, out: list, doc_url, page_idx): mtime = util.get_page_mtime( self.core, doc_url, page_idx, PAGE_FILENAME_FMT ) if mtime is None: return out.append(mtime) def page_internal_get_hash_by_url(self, out: list, doc_url, page_idx): h = util.get_page_hash(self.core, doc_url, page_idx, PAGE_FILENAME_FMT) if h is None: return out.append(h) def doc_get_text_by_url(self, out: list, doc_url): doc_nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", doc_url ) if doc_nb_pages is None: return for page_idx in range(0, doc_nb_pages): text = self.page_get_text_by_url(doc_url, page_idx) if text is None: continue out.append(text) def page_reset_by_url(self, doc_url, page_idx): page_url = self.core.call_success( "fs_join", doc_url, PAGE_FILENAME_FMT.format(page_idx + 1) ) if self.core.call_success("fs_exists", page_url) is None: return None self.core.call_all("fs_unlink", page_url, trash=False) def page_has_text_by_url(self, doc_url, page_idx): page_url = self.core.call_success( "fs_join", doc_url, PAGE_FILENAME_FMT.format(page_idx + 1) ) if self.core.call_success("fs_exists", page_url) is None: return None text = self.page_get_text_by_url(doc_url, page_idx) if text is None: return None text = text.strip() # If the file exists and is valid, it takes precedence over text from, # for instance, a PDF file --> we return False instead of None return len(text) > 0 def page_get_text_by_url(self, doc_url, page_idx): task = "hocr_load_page_text({} p{})".format(doc_url, page_idx) self.core.call_all("on_perfcheck_start", task) page_url = self.core.call_success( "fs_join", doc_url, PAGE_FILENAME_FMT.format(page_idx + 1) ) file_desc = self.core.call_success("fs_open", page_url) if file_desc is None: self.core.call_all("on_perfcheck_stop", task) return None with file_desc: txt = file_desc.read().strip() try: tree = etree.XML(txt) for tag in tree.iter(): tag_name = tag.tag.rsplit("}", 1)[-1] # ignore namespace if tag_name != 'body': continue txt = etree.tostring(tag, encoding='utf-8', method='text') break else: # No body ?! LOGGER.warning("No tag 'body' found in %s", page_url) txt = etree.tostring(tree, encoding='utf-8', method='text') if isinstance(txt, bytes): txt = txt.decode('utf-8') return txt except etree.ParseError as exc: LOGGER.warning( "%s contains invalid XML (%s). Will try with HTML parser", page_url, exc ) def line_txt_generator(line): return " ".join( (word_box.content for word_box in line.word_boxes) ) line_boxes = self.page_get_boxes_by_url(doc_url, page_idx) if line_boxes is None: return None return "\n".join( (line_txt_generator(line_box) for line_box in line_boxes) ) def page_get_boxes_by_url(self, doc_url, page_idx): task = "hocr_load_page_boxes({} p{})".format(doc_url, page_idx) self.core.call_all("on_perfcheck_start", task) page_url = self.core.call_success( "fs_join", doc_url, PAGE_FILENAME_FMT.format(page_idx + 1) ) file_desc = self.core.call_success("fs_open", page_url) if file_desc is None: self.core.call_all("on_perfcheck_stop", task) return None with file_desc: box_builder = pyocr.builders.LineBoxBuilder() boxes = box_builder.read_file(file_desc) if len(boxes) > 0: self.core.call_all("on_perfcheck_stop", task) return boxes with self.core.call_success("fs_open", page_url) as file_desc: # fallback: old format: word boxes # shouldn't be used anymore ... box_builder = pyocr.builders.WordBoxBuilder() boxes = box_builder.read_file(file_desc) if len(boxes) > 0: LOGGER.warning( "Doc %s (page %d) uses old box format", doc_url, page_idx ) self.core.call_all("on_perfcheck_stop", task) return boxes self.core.call_all("on_perfcheck_stop", task) def page_set_boxes_by_url(self, doc_url, page_idx, boxes): page_url = self.core.call_success( "fs_join", doc_url, PAGE_FILENAME_FMT.format(page_idx + 1) ) with self.core.call_success("fs_open", page_url, 'w') as file_desc: pyocr.builders.LineBoxBuilder().write_file(file_desc, boxes) def page_delete_by_url(self, doc_url, page_idx): return util.delete_page_file( self.core, PAGE_FILENAME_FMT, doc_url, page_idx ) def page_move_by_url( self, source_doc_url, source_page_idx, dest_doc_url, dest_page_idx ): return util.move_page_file( self.core, PAGE_FILENAME_FMT, source_doc_url, source_page_idx, dest_doc_url, dest_page_idx ) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/model/img.py000066400000000000000000000054361417573700700256450ustar00rootroot00000000000000import logging import re import openpaperwork_core from . import util LOGGER = logging.getLogger(__name__) PAGE_FILENAME_FMT = "paper.{}.jpg" PAGE_FILENAME_REGEX = re.compile(r"paper\.(\d+)\.jpg") PAGE_FILE_FORMAT = 'JPEG' PAGE_QUALITY = 90 class Plugin(openpaperwork_core.PluginBase): PRIORITY = 100 def get_interfaces(self): return [ "doc_type", "page_img", 'pages', ] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, { # to provide doc_get_nb_pages_by_url() 'interface': 'nb_pages', 'defaults': ['paperwork_backend.model'], }, ] def is_doc(self, doc_url): page_url = self.core.call_success( "fs_join", doc_url, PAGE_FILENAME_FMT.format(1) ) if self.core.call_success("fs_exists", page_url) is None: return None return True def doc_internal_get_mtime_by_url(self, out: list, doc_url): mtime = util.get_doc_mtime(self.core, doc_url, PAGE_FILENAME_REGEX) if mtime is None: return out.append(mtime) def page_internal_get_mtime_by_url(self, out: list, doc_url, page_idx): mtime = util.get_page_mtime( self.core, doc_url, page_idx, PAGE_FILENAME_FMT ) if mtime is None: return out.append(mtime) def page_internal_get_hash_by_url(self, out: list, doc_url, page_idx): h = util.get_page_hash(self.core, doc_url, page_idx, PAGE_FILENAME_FMT) if h is None: return out.append(h) def doc_internal_get_nb_pages_by_url(self, out: list, doc_url): nb_pages = util.get_nb_pages(self.core, doc_url, PAGE_FILENAME_REGEX) if nb_pages is None: return out.append(nb_pages) def page_get_img_url(self, doc_url, page_idx, write=False): if write: self.core.call_success("fs_mkdir_p", doc_url) page_url = self.core.call_success( "fs_join", doc_url, PAGE_FILENAME_FMT.format(page_idx + 1) ) if not write and self.core.call_success("fs_exists", page_url) is None: return None return page_url def page_delete_by_url(self, doc_url, page_idx): return util.delete_page_file( self.core, PAGE_FILENAME_FMT, doc_url, page_idx ) def page_move_by_url( self, source_doc_url, source_page_idx, dest_doc_url, dest_page_idx ): return util.move_page_file( self.core, PAGE_FILENAME_FMT, source_doc_url, source_page_idx, dest_doc_url, dest_page_idx ) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/model/img_overlay.py000066400000000000000000000065621417573700700274070ustar00rootroot00000000000000""" Let other plugins replace a page image without actually smashing the original image. Also provide a method to drop the modified version of a page and revert to the original only. """ import logging import re import openpaperwork_core from . import util LOGGER = logging.getLogger(__name__) PAGE_FILENAME_FMT = "paper.{}.edited.jpg" PAGE_FILENAME_REGEX = re.compile(r"paper\.(\d+)\.edited.jpg") PAGE_FILE_FORMAT = 'JPEG' PAGE_QUALITY = 90 class Plugin(openpaperwork_core.PluginBase): PRIORITY = 1000 # must have a higher priority than model.img / model.pdf def get_interfaces(self): return [ "page_img", "page_reset", 'pages', ] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, ] def doc_internal_get_mtime_by_url(self, out: list, doc_url): mtime = util.get_doc_mtime(self.core, doc_url, PAGE_FILENAME_REGEX) if mtime is None: return out.append(mtime) def page_internal_get_mtime_by_url(self, out: list, doc_url, page_idx): mtime = util.get_page_mtime( self.core, doc_url, page_idx, PAGE_FILENAME_FMT ) if mtime is None: return out.append(mtime) def page_internal_get_hash_by_url(self, out: list, doc_url, page_idx): h = util.get_page_hash(self.core, doc_url, page_idx, PAGE_FILENAME_FMT) if h is None: return out.append(h) def page_get_img_url(self, doc_url, page_idx, write=False): page_url = self.core.call_success( "fs_join", doc_url, PAGE_FILENAME_FMT.format(page_idx + 1) ) if self.core.call_success("fs_exists", page_url) is not None: return page_url if not write: return None # caller wants to modify or create a page # check if we already have an original image. If so, we return our # URL. Otherwise, we let the caller write the original image first. if self.core.call_success( "page_get_img_url", doc_url, page_idx, write=False ) is not None: # has already an original page (or even an edited one) # --> return our URL return page_url # let the caller write an original page (see model.img) return None def page_delete_by_url(self, doc_url, page_idx): return util.delete_page_file( self.core, PAGE_FILENAME_FMT, doc_url, page_idx ) def page_move_by_url( self, source_doc_url, source_page_idx, dest_doc_url, dest_page_idx ): return util.move_page_file( self.core, PAGE_FILENAME_FMT, source_doc_url, source_page_idx, dest_doc_url, dest_page_idx ) def page_reset_by_url(self, doc_url, page_idx): """ Reset a page image to its original content. In other word, we simply delete the edited image so the original one takes over again (see model.img). """ page_url = self.core.call_success( "fs_join", doc_url, PAGE_FILENAME_FMT.format(page_idx + 1) ) if self.core.call_success("fs_exists", page_url): self.core.call_success("fs_unlink", page_url, trash=False) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/model/labels.py000066400000000000000000000233121417573700700263240ustar00rootroot00000000000000import logging import random import openpaperwork_core import openpaperwork_core.promise from .. import _ LOGGER = logging.getLogger(__name__) LABELS_FILENAME = "labels" COLOR_NAMES = [ ('aqua', (0, 1, 1)), ('black', (0, 0, 0)), ('blue', (0, 0, 1)), ('fuchsia', (1, 0, 1)), ('gray', (0x80 / 0xFF, 0x80 / 0xFF, 0x80 / 0xFF)), ('green', (0, 0x80 / 0xFF, 0)), ('lime', (0, 1, 0)), ('maroon', (0x80 / 0xFF, 0, 0)), ('navy', (0, 0, 0x80 / 0xFF)), ('olive', (0x80 / 0xFF, 0x80 / 0xFF, 0)), ('purple', (0x80 / 0xFF, 0, 0x80 / 0xFF)), ('red', (1, 0, 0)), ('silver', (0xC0 / 0xFF, 0xC0 / 0xFF, 0xC0 / 0xFF)), ('teal', (0, 0x80 / 0xFF, 0x80 / 0xFF)), ('white', (1, 1, 1)), ('yellow', (1, 1, 0)), ] class LabelLoader(object): """ Go through all the documents to figure out what labels exist. """ def __init__(self, plugin): self.plugin = plugin self.core = plugin.core self.all_docs = [] def get_promise(self): promise = openpaperwork_core.promise.ThreadedPromise( self.core, self.core.call_all, args=("storage_get_all_docs", self.all_docs) ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then( self.core.call_all, "on_label_loading_start", ) promise = promise.then( openpaperwork_core.promise.ThreadedPromise( self.core, self.load_labels ) ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then( self.core.call_all, "on_label_loading_end" ) return promise def load_labels(self, *args, **kwargs): nb_docs = len(self.all_docs) LOGGER.info("Loading labels from %d documents", nb_docs) for (doc_idx, (doc_id, doc_url)) in enumerate(self.all_docs): self.core.call_all( "on_progress", "label_loading", doc_idx / nb_docs, _("Loading labels of document {}").format(doc_id) ) labels = set() self.plugin.doc_get_labels_by_url(labels, doc_url) for label in labels: self.plugin.all_labels[label[0]] = label[1] self.core.call_all("on_progress", "label_loading", 1.0) LOGGER.info( "%d labels loaded from %d documents", len(self.plugin.all_labels), nb_docs ) class Plugin(openpaperwork_core.PluginBase): def __init__(self): # {label_name: color, label_name: color, ...} self.all_labels = {} def get_interfaces(self): return [ "doc_labels", "syncable", ] def get_deps(self): return [ { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'thread', 'defaults': ['openpaperwork_core.thread.simple'], }, ] def doc_internal_get_mtime_by_url(self, out: list, doc_url): labels_url = self.core.call_success( "fs_join", doc_url, LABELS_FILENAME ) if self.core.call_success("fs_exists", labels_url) is None: return out.append(self.core.call_success("fs_get_mtime", labels_url)) def doc_has_labels_by_url(self, doc_url): labels_url = self.core.call_success( "fs_join", doc_url, LABELS_FILENAME ) return self.core.call_success("fs_exists", labels_url) def doc_get_labels_by_url(self, out: set, doc_url): labels_url = self.core.call_success( "fs_join", doc_url, LABELS_FILENAME ) if self.core.call_success("fs_exists", labels_url) is None: return with self.core.call_success("fs_open", labels_url, 'r') as file_desc: for line in file_desc.readlines(): line = line.strip() if line == "": continue # Expected: ('label', '#rrrrggggbbbb') out.add(tuple(x.strip() for x in line.split(",", 1))) def doc_get_labels_by_url_promise(self, out: list, doc_url): def get_labels(labels=None): if labels is None: labels = set() self.doc_get_labels_by_url(labels, doc_url) return labels promise = openpaperwork_core.promise.ThreadedPromise( self.core, get_labels ) out.append(promise) def label_generate_color(self): color = ( random.randint(0, 255) / 255, random.randint(0, 255) / 255, random.randint(0, 255) / 255, ) return self.label_color_from_rgb(color) def label_get_foreground_color(self, bg_color): brightness = ( (bg_color[0] * 0.299) + (bg_color[1] * 0.587) + (bg_color[2] * 0.114) ) if brightness > (69 / 255): return (0, 0, 0) # black else: return (1, 1, 1) # white def doc_add_label_by_url(self, doc_url, label, color=None): assert("," not in label) current = set() self.doc_get_labels_by_url(current, doc_url) current = {k: v for (k, v) in current} if label in current: LOGGER.warning( "Label '%s' already on document '%s'", label, doc_url ) return if color is not None: self.all_labels[label] = color if label in self.all_labels: color = self.all_labels[label] else: color = self.label_generate_color() LOGGER.info("Adding label '%s' on document '%s'", label, doc_url) labels_url = self.core.call_success( "fs_join", doc_url, LABELS_FILENAME ) with self.core.call_success("fs_open", labels_url, 'a') as file_desc: file_desc.write("{},{}\n".format(label, color)) if label not in self.all_labels: self.all_labels[label] = color return True def doc_remove_label_by_url(self, doc_url, label): LOGGER.info("Removing label '%s' from document '%s'", label, doc_url) labels_url = self.core.call_success( "fs_join", doc_url, LABELS_FILENAME ) if self.core.call_success("fs_exists", labels_url) is None: return with self.core.call_success("fs_open", labels_url, 'r') as file_desc: labels = file_desc.readlines() labels = [lab.strip() for lab in labels] labels = [lab.split(",", 1) for lab in labels if len(lab) > 0] labels = {l: c for (l, c) in labels} try: labels.pop(label) except KeyError: LOGGER.warning( "Tried to remove label '%s' from document '%s', but label" " was not found on the document", label, doc_url ) labels = [(label, color) for (label, color) in labels.items()] labels.sort() with self.core.call_success("fs_open", labels_url, "w") as file_desc: for (label, color) in labels: file_desc.write("{},{}\n".format(label, color)) return True def labels_get_all(self, out: set): for (label, color) in self.all_labels.items(): out.add((label, color)) def label_color_to_rgb(self, color): if color[0] == '#': if len(color) == 13: return ( int(color[1:5], 16) / 0xFFFF, int(color[5:9], 16) / 0xFFFF, int(color[9:13], 16) / 0xFFFF, ) else: return ( int(color[1:3], 16) / 0xFF, int(color[3:5], 16) / 0xFF, int(color[5:7], 16) / 0xFF, ) else: if color.startswith("rgb("): color = color[len("rgb("):-1] elif color.startswith("("): color = color[len("("):-1] color = color.split(",") color = tuple([int(x.strip()) for x in color]) color = (color[0] / 0xFF, color[1] / 0xFF, color[2] / 0xFF) return color def label_color_from_rgb(self, color): return ( "#" + format(int(color[0] * 0xFF), '02x') + "00" + format(int(color[1] * 0xFF), '02x') + "00" + format(int(color[2] * 0xFF), '02x') + "00" ) def label_color_rgb_to_text(self, color): for (name, rgb) in COLOR_NAMES: if color == rgb: break else: def color_distance(a, b): return ( (abs(a[0] - b[0]) ** 2) + (abs(a[1] - b[1]) ** 2) + (abs(a[2] - b[2]) ** 2) ) closest_color = min(( (color_distance(rgb, color), name) for (name, rgb) in COLOR_NAMES )) name = "~" + closest_color[1] return "#{:02X}{:02X}{:02X} ({})".format( int(color[0] * 0xFF), int(color[1] * 0xFF), int(color[2] * 0xFF), name ) def label_load_all(self, promises: list): self.all_labels = {} promise = LabelLoader(self).get_promise() # drop the return value of 'call_all' promise = promise.then(lambda *args, **kwargs: None) promises.append(promise) def sync(self, promises: list): self.label_load_all(promises) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/model/pdf.py000066400000000000000000000746311417573700700256450ustar00rootroot00000000000000import datetime import itertools import logging import math import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) PDF_FILENAME = 'doc.pdf' PASSWD_FILENAME = 'passwd.txt' PDF_RENDER_FACTOR = 4 def minmax_rects(rects): (mx1, my1, mx2, my2) = (math.inf, math.inf, 0, 0) for rectangle in rects: ((x1, y1), (x2, y2)) = ( (int(rectangle.x1 * PDF_RENDER_FACTOR), int(rectangle.y2 * PDF_RENDER_FACTOR)), (int(rectangle.x2 * PDF_RENDER_FACTOR), int(rectangle.y1 * PDF_RENDER_FACTOR)) ) (x1, x2) = (min(x1, x2), max(x1, x2)) (y1, y2) = (min(y1, y2), max(y1, y2)) mx1 = min(mx1, x1) my1 = min(my1, y1) mx2 = max(mx2, x2) my2 = max(my2, y2) rect = ((mx1, my1), (mx2, my2)) return rect class PdfWordBox(object): def __init__(self, content, position): self.content = content self.position = minmax_rects(position) def __str__(self): return "{{ .position={}, .content={} }}".format( self.position, str(self.content) ) def __lt__(self, o): return self.position < o.position def __repr__(self): return str(self) class PdfLineBox(object): def __init__(self, word_boxes, position): self.word_boxes = word_boxes self.position = minmax_rects(position) def __str__(self): return "{{ .position={}, .word_boxes={} }}".format( self.position, str(self.word_boxes) ) def __repr__(self): return str(self) def _get_content(self): return " ".join([w.content for w in self.word_boxes]) content = property(_get_content) class PdfPageMapping(object): MAPPING_FILE = "page_map.csv" SEPARATOR = "," def __init__(self, plugin, doc_url): self.plugin = plugin self.core = plugin.core self.doc_url = doc_url self.map_url = self.core.call_success( "fs_join", self.doc_url, self.MAPPING_FILE ) self.mapping = None self.reverse_mapping = None self.page_mtimes = None self.nb_pages = -1 self.real_nb_pages = -1 def set_mapping(self, original_page_idx, target_page_idx): """ Indicates that the page 'original_page_idx' in the actual PDF file is displayed as being the page 'target_page_idx'. If 'target_page_idx' < 0, it means the page is not displayed anymore (--> act if it is deleted) """ if original_page_idx >= self.real_nb_pages: # comes from another set of plugins, no point in tracking it return now = datetime.datetime.now().timestamp() if target_page_idx is not None: old_original = self.reverse_mapping.get(target_page_idx, None) if old_original is not None: self.mapping.pop(old_original) old_target = self.mapping.get(original_page_idx, None) if old_target is not None: self.reverse_mapping.pop(old_target) self.page_mtimes[old_target] = now if target_page_idx is None: self.mapping.pop(original_page_idx) else: self.mapping[original_page_idx] = target_page_idx if target_page_idx is not None: self.reverse_mapping[target_page_idx] = original_page_idx self.page_mtimes[target_page_idx] = now def update_nb_pages(self): self.nb_pages = 0 if len(self.mapping) <= 0: return for v in self.mapping.values(): if v >= self.nb_pages: self.nb_pages = v + 1 def load(self): self.real_nb_pages = self.plugin._doc_internal_get_nb_pages_by_url( self.doc_url, mapping=False ) self.mapping = {p: p for p in range(0, self.real_nb_pages)} self.reverse_mapping = {p: p for p in range(0, self.real_nb_pages)} now = datetime.datetime.now().timestamp() self.page_mtimes = {p: now for p in range(0, self.real_nb_pages)} if self.core.call_success("fs_exists", self.map_url) is None: self.update_nb_pages() return with self.core.call_success("fs_open", self.map_url, "r") as fd: # drop the first line lines = fd.readlines()[1:] lines = [line.split(",", 1) for line in lines] lines = [ (int(orig_page_idx), int(target_page_idx)) for (orig_page_idx, target_page_idx) in lines ] lines.sort() for (orig_page_idx, target_page_idx) in lines: if target_page_idx < 0: target_page_idx = None self.set_mapping(orig_page_idx, target_page_idx) self.update_nb_pages() def load_reverse_only(self): """ Load the mapping, but doesn't look at the total number of pages in the document. Avoid opening the PDF file. """ self.mapping = None self.reverse_mapping = {} if self.core.call_success("fs_exists", self.map_url) is None: return with self.core.call_success("fs_open", self.map_url, "r") as fd: # drop the first line lines = fd.readlines()[1:] for line in lines: (orig_page_idx, target_page_idx) = line.split(",", 1) orig_page_idx = int(orig_page_idx) target_page_idx = int(target_page_idx) if target_page_idx < 0: continue self.reverse_mapping[target_page_idx] = orig_page_idx def save(self): if self.core.call_success("fs_isdir", self.doc_url) is None: return nb_maps = 0 for (orig_page_idx, target_page_idx) in self.mapping.items(): if orig_page_idx == target_page_idx: continue nb_maps += 1 if nb_maps <= 0: if self.core.call_success("fs_exists", self.map_url) is not None: self.core.call_success("fs_unlink", self.map_url) return with self.core.call_success("fs_open", self.map_url, "w") as fd: fd.write("original_page_index,target_page_index\n") mapping = list(self.mapping.items()) mapping.sort() for (orig_page_idx, target_page_idx) in mapping: if orig_page_idx == target_page_idx: continue fd.write("{},{}\n".format(orig_page_idx, target_page_idx)) for page_idx in range(0, self.real_nb_pages): if page_idx not in self.mapping: fd.write("{},-1\n".format(page_idx)) def has_original_page_idx(self, original_page_idx): if self.mapping is None: self.load() return original_page_idx in self.mapping def get_original_page_idx(self, target_page_idx, reverse_only=False): if reverse_only: if self.reverse_mapping is None: self.load_reverse_only() else: if self.mapping is None: self.load() # Keep in mind we may have loaded only the mapping --> we don't know # how many pages there are in the PDF, so the mapping may be incomplete original_page_idx = self.reverse_mapping.get( target_page_idx, None ) return original_page_idx def get_target_page_mtime(self, target_page_idx): if self.mapping is None: self.load() return self.page_mtimes.get(target_page_idx, None) def get_target_page_hash(self, target_page_idx): if self.mapping is None: self.load() original_page_idx = self.reverse_mapping.get( target_page_idx, None ) if original_page_idx is None: return None return hash(original_page_idx) def _move_pages(self, original_page_idx, target_page_idx, offset): mapping = list(self.mapping.items()) mapping.sort(key=lambda x: x[1], reverse=offset > 0) for (original, target) in mapping: if target >= target_page_idx: LOGGER.info( "Page %d (original) ; target: %d --> %d", original, target, target + offset ) self.set_mapping(original, target + offset) self.update_nb_pages() def delete_target_page(self, target_page_idx): if self.mapping is None: self.load() LOGGER.info( "Deleting page %d from PDF %s", target_page_idx, self.doc_url ) original_page_idx = self.reverse_mapping.pop( target_page_idx, None ) if original_page_idx is not None: self.mapping.pop(original_page_idx, None) else: original_page_idx = target_page_idx self._move_pages(original_page_idx, target_page_idx, offset=-1) def make_room_for_target_page(self, target_page_idx): if self.mapping is None: self.load() original_page_idx = self.reverse_mapping.get( target_page_idx, target_page_idx ) self._move_pages(original_page_idx, target_page_idx, offset=1) def print_mapping(self): if self.mapping is None: self.load() nb_pages = self.plugin._doc_internal_get_nb_pages_by_url( self.doc_url, mapping=False ) print("==== MAPPING OF {} (nb_pages={}|{}) ====".format( self.doc_url, nb_pages, self.nb_pages )) for original_page_idx in range(0, nb_pages): target_page_idx = self.mapping.get(original_page_idx, None) if target_page_idx is None: print("{} --> {}".format(original_page_idx, original_page_idx)) continue print("{} --> {}".format(original_page_idx, target_page_idx)) if target_page_idx < 0: continue if target_page_idx not in self.reverse_mapping: print("WARNING: NO REVERSE") else: if original_page_idx != self.reverse_mapping[target_page_idx]: print("WARNING: REVERSE DOESN'T MATCH: {} <-- {}".format( target_page_idx, original_page_idx )) print("=======================") def get_map_hash(self): if self.core.call_success("fs_exists", self.map_url) is None: return None return self.core.call_success("fs_hash", self.map_url) def get_map_mtime(self): if self.core.call_success("fs_exists", self.map_url) is None: return None return self.core.call_success("fs_get_mtime", self.map_url) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 20 def __init__(self): super().__init__() # we cache the hash of PDF files since they never change self.cache_hash = {} # we cache the number of pages in PDF files since they never change # (real number before mapping) self.cache_nb_pages = {} self.cache_mappings = {} self.cache_passwords = {} def get_interfaces(self): return [ "doc_hash", "doc_pdf_import", "doc_pdf_url", "doc_text", "doc_type", "page_boxes", "page_img", "page_paper", 'pages', ] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { # to provide doc_get_nb_pages_by_url() 'interface': 'nb_pages', 'defaults': ['paperwork_backend.model'], }, # for page_move_by_url(): { 'interface': 'pillow', 'defaults': [ 'openpaperwork_core.pillow.img', 'paperwork_backend.pillow.pdf', ], }, { 'interface': 'poppler', 'defaults': ['paperwork_backend.poppler.memory'], }, { 'interface': 'urls', 'defaults': ['openpaperwork_core.urls'], }, ] def _get_pdf_url(self, doc_url): if doc_url.endswith(".pdf"): return doc_url pdf_url = self.core.call_success("fs_join", doc_url, PDF_FILENAME) if self.core.call_success("fs_exists", pdf_url) is None: return None return pdf_url def _get_pdf_password(self, doc_url): password = self.cache_passwords.get(doc_url, None) if password is not None: return password passwd_url = self.core.call_success( "fs_join", doc_url, PASSWD_FILENAME ) if self.core.call_success("fs_exists", passwd_url): with self.core.call_success("fs_open", passwd_url, "r") as fd: password = fd.read().strip() self.cache_passwords[doc_url] = password return password def _get_page_mapping(self, doc_url): if doc_url in self.cache_mappings: return self.cache_mappings[doc_url] mapping = PdfPageMapping(self, doc_url) self.cache_mappings[doc_url] = mapping return mapping def _open_pdf(self, doc_url): pdf_url = self._get_pdf_url(doc_url) if pdf_url is None: return (None, None) password = self._get_pdf_password(doc_url) LOGGER.info("Opening %s (password=%s)", pdf_url, bool(password)) doc = self.core.call_success( "poppler_open", pdf_url, password=password ) return (pdf_url, doc) def is_doc(self, doc_url): pdf_url = self._get_pdf_url(doc_url) if pdf_url is None: return None return True def doc_get_pdf_url_by_url(self, doc_url, write=False): if write: return self.core.call_success("fs_join", doc_url, PDF_FILENAME) return self._get_pdf_url(doc_url) def flush_doc_cache(self, doc_url): self.cache_hash.pop(doc_url, None) self.cache_passwords.pop(doc_url, None) self.cache_mappings.pop(doc_url, None) self.cache_nb_pages.pop(doc_url, None) def doc_get_hash_by_url(self, doc_url): pdf_url = self._get_pdf_url(doc_url) if pdf_url is None: return # cache the hash of doc.pdf to speed up imports if doc_url not in self.cache_hash: h = self.core.call_success("fs_hash", pdf_url) self.cache_hash[doc_url] = h return self.cache_hash[doc_url] def page_internal_get_hash_by_url(self, out: list, doc_url, page_idx): pdf_url = self._get_pdf_url(doc_url) if pdf_url is None: return mapping = self._get_page_mapping(doc_url) page_hash = mapping.get_target_page_hash(page_idx) if page_hash is None: # deleted page or handled by another plugin return out.append(page_hash) # cache the hash of doc.pdf to speed up imports if doc_url not in self.cache_hash: h = self.core.call_success("fs_hash", pdf_url) self.cache_hash[doc_url] = h out.append(self.cache_hash[doc_url]) def doc_internal_get_mtime_by_url(self, out: list, doc_url): pdf_url = self._get_pdf_url(doc_url) if pdf_url is None: return None mapping = self._get_page_mapping(doc_url) mtime = mapping.get_map_mtime() if mtime is not None: out.append(mtime) mtime = self.core.call_success("fs_get_mtime", pdf_url) if mtime is None: return None out.append(mtime) def page_internal_get_mtime_by_url(self, out: list, doc_url, page_idx): pdf_url = self._get_pdf_url(doc_url) if pdf_url is None: return mapping = self._get_page_mapping(doc_url) mtime = mapping.get_target_page_mtime(page_idx) if mtime is not None: out.append(mtime) mtime = self.core.call_success("fs_get_mtime", pdf_url) if mtime is not None: out.append(mtime) return def _doc_get_real_nb_pages_by_url(self, doc_url): (pdf_url, pdf) = self._open_pdf(doc_url) if pdf is None: return None nb_pages = pdf.get_n_pages() return nb_pages def _doc_internal_get_nb_pages_by_url( self, doc_url, mapping=True): if mapping: pdf_url = self._get_pdf_url(doc_url) if pdf_url is None: return 0 mapping = self._get_page_mapping(doc_url) if mapping.nb_pages < 0: mapping.load() return mapping.nb_pages if doc_url in self.cache_nb_pages: r = self.cache_nb_pages[doc_url] else: # Poppler is not thread-safe r = self.core.call_one( "mainloop_execute", self._doc_get_real_nb_pages_by_url, doc_url ) if r is not None: self.cache_nb_pages[doc_url] = r return r if r is not None else 0 def doc_internal_get_nb_pages_by_url(self, out: list, doc_url): r = self._doc_internal_get_nb_pages_by_url(doc_url) if r == 0: return out.append(r) def page_get_img_url(self, doc_url, page_idx, write=False): if write: return None pdf_url = self._get_pdf_url(doc_url) if pdf_url is None: return None # The first page is requested much more often than the others, due # to thumbnailing. # ASSUMPTION(Jflesch): If there is a doc.pdf, there is a first page # in it. # We don't want to open the PDF file for each thumbnail (first page) # but at the same time, we still need to check the mapping file. mapping = self._get_page_mapping(doc_url) original_page_idx = mapping.get_original_page_idx( page_idx, reverse_only=(page_idx == 0) ) if original_page_idx is None: if page_idx != 0: return None else: original_page_idx = page_idx password = self._get_pdf_password(doc_url) if password is not None: password = password.encode("utf-8").hex() # same URL used in browsers url = self.core.call_success( "url_args_join", pdf_url, page=str(original_page_idx + 1), password=password ) return url @staticmethod def _custom_split(input_str, input_rects, splitter, log_txt): # turn text and layout from Poppler into boxes # XXX(Jflesch): following assert fails sometimes ? oO # assert(len(input_str) == len(input_rects)) if len(input_str) != len(input_rects): LOGGER.warning( "%s: Input strings: %d ; Input rects: %d", log_txt, len(input_str), len(input_rects) ) m = min(len(input_str), len(input_rects)) input_str = input_str[:m] input_rects = input_rects[:m] input_el = zip(input_str, input_rects) for (is_split, group) in itertools.groupby( input_el, lambda x: splitter(x[0]) ): if is_split: continue letters = "" rects = [] for (letter, rect) in group: letters += letter rects.append(rect) yield(letters, rects) def _doc_get_text_by_url(self, out: list, doc_url): task = "pdf_get_text_by_url({})".format(doc_url) self.core.call_all("on_perfcheck_start", task) (pdf_url, pdf) = self._open_pdf(doc_url) if pdf is None: self.core.call_all("on_perfcheck_stop", task) return mapping = self._get_page_mapping(doc_url) for page_idx in range(0, pdf.get_n_pages()): if not mapping.has_original_page_idx(page_idx): continue page = pdf.get_page(page_idx) # some PDF are really badly damaged if page is None: continue txt = page.get_text() txt = txt.strip() if txt == "": continue out.append(txt) self.core.call_all( "on_perfcheck_stop", task, nb_pages=pdf.get_n_pages() ) return True def doc_get_text_by_url(self, out: list, doc_url): # Poppler is not thread-safe return self.core.call_one( "mainloop_execute", self._doc_get_text_by_url, out, doc_url ) def _page_has_text_by_url(self, doc_url, page_idx): if doc_url in self.cache_nb_pages: if page_idx >= self.cache_nb_pages[doc_url]: return None (pdf_url, pdf) = self._open_pdf(doc_url) if pdf is None: return None page = pdf.get_page(page_idx) if page is None: return None return len(page.get_text().strip()) > 0 def page_has_text_by_url(self, doc_url, page_idx): pdf_url = self._get_pdf_url(doc_url) if pdf_url is None: return None mapping = self._get_page_mapping(doc_url) page_idx = mapping.get_original_page_idx(page_idx) if page_idx is None: return None # Poppler is not thread-safe return self.core.call_one( "mainloop_execute", self._page_has_text_by_url, doc_url, page_idx ) def _page_get_text_by_url(self, doc_url, page_idx): if doc_url in self.cache_nb_pages: if page_idx >= self.cache_nb_pages[doc_url]: return None (pdf_url, pdf) = self._open_pdf(doc_url) if pdf is None: return None page = pdf.get_page(page_idx) if page is None: return None return page.get_text().strip() def page_get_text_by_url(self, doc_url, page_idx): pdf_url = self._get_pdf_url(doc_url) if pdf_url is None: return None mapping = self._get_page_mapping(doc_url) page_idx = mapping.get_original_page_idx(page_idx) if page_idx is None: return None # Poppler is not thread-safe return self.core.call_one( "mainloop_execute", self._page_get_text_by_url, doc_url, page_idx ) def _page_get_boxes_by_url(self, doc_url, page_idx): if doc_url in self.cache_nb_pages: if page_idx >= self.cache_nb_pages[doc_url]: return None (pdf_url, pdf) = self._open_pdf(doc_url) if pdf is None: return pdf_page = pdf.get_page(page_idx) if pdf_page is None: return txt = pdf_page.get_text() if txt.strip() == "": return None layout = pdf_page.get_text_layout() if not layout[0]: return None layout = layout[1] line_boxes = [] for (line, line_rects) in self._custom_split( txt, layout, lambda x: x == "\n", "lines", ): words = [] for (word, word_rects) in self._custom_split( line, line_rects, lambda x: x.isspace(), "words" ): word_box = PdfWordBox(word, word_rects) words.append(word_box) line_boxes.append(PdfLineBox(words, line_rects)) return line_boxes def page_get_boxes_by_url(self, doc_url, page_idx): pdf_url = self._get_pdf_url(doc_url) if pdf_url is None: return None mapping = self._get_page_mapping(doc_url) page_idx = mapping.get_original_page_idx(page_idx) if page_idx is None: return None # Poppler is not thread-safe return self.core.call_one( "mainloop_execute", self._page_get_boxes_by_url, doc_url, page_idx ) def doc_pdf_import(self, src_file_uri, password=None): (doc_id, doc_url) = self.core.call_success("storage_get_new_doc") # just to be safe self.cache_mappings.pop(doc_url, None) pdf_url = self.core.call_success("fs_join", doc_url, PDF_FILENAME) self.core.call_success("fs_mkdir_p", doc_url) self.core.call_success("fs_copy", src_file_uri, pdf_url) if password is not None: passwd_url = self.core.call_success( "fs_join", doc_url, PASSWD_FILENAME ) with self.core.call_success("fs_open", passwd_url, "w") as fd: fd.write(password) try: # check the PDF is readable doc = self.core.call_success( "poppler_open", pdf_url, password=password ) exc_info = None except Exception as exc: doc = None exc_info = exc if doc is None: if exc_info is None: LOGGER.error("Failed to read %s", pdf_url) else: LOGGER.error("Failed to read %s", pdf_url, exc_info=exc_info) self.core.call_success("fs_rm_rf", doc_url, trash=False) return (None, None) return (doc_id, doc_url) def page_delete_by_url(self, doc_url, page_idx): pdf_url = self._get_pdf_url(doc_url) if pdf_url is None: return mapping = self._get_page_mapping(doc_url) mapping.delete_target_page(page_idx) mapping.save() def _page_get_paper_size_by_url(self, doc_url, page_idx): if doc_url in self.cache_nb_pages: if page_idx >= self.cache_nb_pages[doc_url]: return None (pdf_url, pdf) = self._open_pdf(doc_url) if pdf is None: return None page = pdf.get_page(page_idx) if page is None: return size = page.get_size() # points --> inches: / 72 # inches --> millimeters (i18n unit): * 25.4 return ( size[0] / 72.0 * 25.4, size[0] / 72.0 * 25.4, ) def page_get_paper_size_by_url(self, doc_url, page_idx): pdf_url = self._get_pdf_url(doc_url) if pdf_url is None: return None mapping = self._get_page_mapping(doc_url) page_idx = mapping.get_original_page_idx(page_idx) if page_idx is None: return None # Poppler is not thread-safe return self.core.call_one( "mainloop_execute", self._page_get_paper_size_by_url, doc_url, page_idx ) def page_move_by_url( self, source_doc_url, target_source_page_idx, dest_doc_url, target_dest_page_idx ): # 'source' is about the source document. # 'dest' is about the destination document. # 'original' is about the original position of a page in a PDF file. # 'target' is about the position of a page as shown to the end-user. source_pdf_url = self._get_pdf_url(source_doc_url) source_is_pdf = source_pdf_url is not None dest_is_pdf = self._get_pdf_url(dest_doc_url) is not None if not source_is_pdf and not dest_is_pdf: return if source_is_pdf: source_mapping = self._get_page_mapping(source_doc_url) if dest_is_pdf: dest_mapping = self._get_page_mapping(dest_doc_url) LOGGER.info( "%s (%s) p%d --> %s (%s) p%d", source_doc_url, "PDF" if source_is_pdf else "non-PDF", target_source_page_idx, dest_doc_url, "PDF" if dest_is_pdf else "non-PDF", target_dest_page_idx, ) if source_is_pdf: original_source_page_idx = source_mapping.get_original_page_idx( target_source_page_idx ) # if original_source_page_idx is None means # this page is not handled by us ; still, we must shift # all our pages LOGGER.info( "- Removing page (original=%s, target=%d) from %s", original_source_page_idx, target_source_page_idx, source_doc_url ) source_mapping.delete_target_page(target_source_page_idx) if dest_is_pdf: LOGGER.info( "- Making room for a new page (target=%d) in %s", target_dest_page_idx, dest_doc_url ) dest_mapping.make_room_for_target_page(target_dest_page_idx) if source_doc_url == dest_doc_url: assert(source_mapping is dest_mapping) if original_source_page_idx is not None: LOGGER.info( "New mapping: %s: original=p%d --> target=p%d", source_doc_url, original_source_page_idx, target_dest_page_idx ) source_mapping.set_mapping( original_source_page_idx, target_dest_page_idx ) source_mapping.update_nb_pages() elif source_is_pdf: if original_source_page_idx is not None: # export the PDF page as an image file # it relies on other model plugins (interface 'page_img'), but # they can't be declared as dependencies, as we do provide # 'page_img' too. It would make a dependency loop. # we are a low priority plugin: other plugins should # already have made room for our page if required source_img = "{}#page={}".format( source_pdf_url, str(original_source_page_idx + 1) ) LOGGER.info("Generating image from %s", source_img) source_img = self.core.call_success( "url_to_pillow", source_img ) # since PDF are not writable by themselves, we can call # page_get_img_url(write=True). We are sure that our # implementation of this method won't reply dest_img = self.core.call_success( "page_get_img_url", dest_doc_url, target_dest_page_idx, write=True ) LOGGER.info("Writting page image back as %s", dest_img) self.core.call_success("pillow_to_url", source_img, dest_img) if source_is_pdf: source_mapping.save() if dest_is_pdf: dest_mapping.save() paperwork-2.1.1/paperwork-backend/src/paperwork_backend/model/thumbnail.py000066400000000000000000000120001417573700700270350ustar00rootroot00000000000000#!/usr/bin/python3 import logging import time import PIL.Image import openpaperwork_core import openpaperwork_core.promise from . import util THUMBNAIL_WIDTH = 64 THUMBNAIL_HEIGHT = 80 PAGE_THUMBNAIL_FILENAME = 'paper.{}.thumb.jpg' LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 5000 # see page_get_img_url() def get_interfaces(self): return [ 'pages', 'thumbnail', # thumbnail_get_path() 'thumbnailer', # thumbnail_from_img() ] def get_deps(self): return [ { 'interface': 'page_img', 'defaults': [ 'paperwork_backend.model.img', 'paperwork_backend.model.pdf', ], }, { 'interface': 'pillow', 'defaults': [ 'openpaperwork_core.pillow.img', 'paperwork_backend.pillow.pdf', ], }, { 'interface': 'thread', 'defaults': ['openpaperwork_core.thread.simple'], }, ] def thumbnail_get_doc(self, doc_url): return self.thumbnail_get_page(doc_url, page_idx=0) def thumbnail_get_doc_promise(self, doc_url): return openpaperwork_core.promise.ThreadedPromise( self.core, self.thumbnail_get_doc, args=(doc_url,) ) def thumbnail_from_img(self, img): (width, height) = img.size scale = max( float(width) / THUMBNAIL_WIDTH, float(height) / THUMBNAIL_HEIGHT ) width /= scale height /= scale return img.resize((int(width), int(height)), PIL.Image.ANTIALIAS) def thumbnail_get_page(self, doc_url, page_idx): thumbnail_url = self.core.call_success( "fs_join", doc_url, PAGE_THUMBNAIL_FILENAME.format(page_idx + 1) ) page_url = self.core.call_success( "page_get_img_url", doc_url, page_idx ) if page_url is None: LOGGER.warning( "Failed to get thumbnail for %s p%d. No page URL", doc_url, page_idx ) return None if self.core.call_success("fs_exists", thumbnail_url) is not None: thumbnail_mtime = self.core.call_success( "fs_get_mtime", thumbnail_url ) if not self.core.call_success("fs_iswritable", thumbnail_url): page_mtime = self.core.call_success("fs_get_mtime", page_url) if thumbnail_mtime < page_mtime: self.core.call_success("fs_unlink", thumbnail_url) if self.core.call_success("fs_exists", thumbnail_url) is not None: LOGGER.debug("Loading thumbnail for %s page %d", doc_url, page_idx) thumbnail = self.core.call_success("url_to_pillow", thumbnail_url) size = thumbnail.size if size[0] == THUMBNAIL_WIDTH or size[1] == THUMBNAIL_HEIGHT: return thumbnail LOGGER.info( "Thumbnail for %s page %d doesn't have the expected size" " (%s instead of %s). Regenerating.", doc_url, page_idx, size, (THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT) ) LOGGER.info("Generating thumbnail for %s page %d", doc_url, page_idx) start = time.time() page = self.core.call_success("url_to_pillow", page_url) thumbnail = self.thumbnail_from_img(page) self.core.call_success( "pillow_to_url", thumbnail, thumbnail_url, format='JPEG', quality=0.85 ) stop = time.time() LOGGER.info( "Thumbnail for %s page %d generated in %f seconds", doc_url, page_idx, stop - start ) return thumbnail def thumbnail_get_page_promise(self, doc_url, page_idx): return openpaperwork_core.promise.ThreadedPromise( self.core, self.thumbnail_get_page, args=(doc_url, page_idx) ) def page_delete_by_url(self, doc_url, page_idx): return util.delete_page_file( self.core, PAGE_THUMBNAIL_FILENAME, doc_url, page_idx, trash=False ) def page_move_by_url( self, source_doc_url, source_page_idx, dest_doc_url, dest_page_idx ): return util.move_page_file( self.core, PAGE_THUMBNAIL_FILENAME, source_doc_url, source_page_idx, dest_doc_url, dest_page_idx ) def page_reset_by_url(self, doc_url, page_idx): # see model.img_overlay.page_reset_by_url() # We must force a rebuild of the thumbnail self.page_delete_by_url(doc_url, page_idx) def page_get_img_url(self, doc_url, page_idx, write=False): if not write: return None # Image is going to be updated --> We must force a rebuild of the # thumbnail self.page_delete_by_url(doc_url, page_idx) return None paperwork-2.1.1/paperwork-backend/src/paperwork_backend/model/util.py000066400000000000000000000115651417573700700260460ustar00rootroot00000000000000import logging LOGGER = logging.getLogger(__name__) def _shift_pages(core, page_filename_fmt, doc_url, start_page_idx, offset): assert(offset != 0) total_pages = core.call_success("doc_get_nb_pages_by_url", doc_url) if total_pages is None: total_pages = 0 if offset < 0: rng = range(start_page_idx + 1, total_pages + 1) elif offset > 0: rng = range(total_pages - 1, start_page_idx - 1, -1) for page_idx in rng: old_url = core.call_success( "fs_join", doc_url, page_filename_fmt.format(page_idx + 1) ) if core.call_success("fs_exists", old_url) is None: continue new_url = core.call_success( "fs_join", doc_url, page_filename_fmt.format( page_idx + 1 + offset ) ) LOGGER.info(" - %s --> %s", old_url, new_url) core.call_success("fs_rename", old_url, new_url) def delete_page_file(core, page_filename_fmt, doc_url, page_idx, trash=True): file_url = core.call_success( "fs_join", doc_url, page_filename_fmt.format(page_idx + 1) ) if core.call_success("fs_exists", file_url) is not None: LOGGER.info( "(%s) Deleting %s p%d:", page_filename_fmt, doc_url, page_idx ) core.call_success("fs_unlink", file_url, trash=trash) # move all the other pages 1 level down _shift_pages(core, page_filename_fmt, doc_url, page_idx, -1) return True def move_page_file( core, page_filename_fmt, source_doc_url, source_page_idx, dest_doc_url, dest_page_idx ): if core.call_success("fs_exists", dest_doc_url) is None: assert(dest_page_idx == 0) core.call_success("fs_mkdir_p", dest_doc_url) LOGGER.info( "(%s) Move %s p%d --> %s p%d:", page_filename_fmt, source_doc_url, source_page_idx, dest_doc_url, dest_page_idx ) # source_doc_url and dest_doc_url can be the same document, making # this change a little bit tricky. The simplest way to handle all the cases # is to: # --> move the page out of the source document (we rename it temporarily # page_filename_fmt.format(0)) # --> then to insert it in the destination document # Move the page out of the source document src = core.call_success( "fs_join", source_doc_url, page_filename_fmt.format(source_page_idx + 1) ) dst = core.call_success( "fs_join", dest_doc_url, page_filename_fmt.format(0) ) if core.call_success("fs_exists", src) is not None: LOGGER.info(" - %s --> %s", src, dst) core.call_success("fs_rename", src, dst) _shift_pages(core, page_filename_fmt, source_doc_url, source_page_idx, -1) # Move the page in the destination document src = dst dst = core.call_success( "fs_join", dest_doc_url, page_filename_fmt.format(dest_page_idx + 1) ) _shift_pages(core, page_filename_fmt, dest_doc_url, dest_page_idx, 1) if core.call_success("fs_exists", src) is not None: LOGGER.info(" - %s --> %s", src, dst) core.call_success("fs_rename", src, dst) return True def get_nb_pages(core, doc_url, filename_regex): if core.call_success("fs_exists", doc_url) is None: return None if core.call_success("fs_isdir", doc_url) is None: return None files = core.call_success("fs_listdir", doc_url) if files is None: return None nb_pages = -1 for f in files: f = core.call_success("fs_basename", f) match = filename_regex.match(f) if match is None: continue nb_pages = max(nb_pages, int(match.group(1))) if nb_pages <= 0: return None return nb_pages def get_page_hash(core, doc_url, page_idx, filename_fmt): page_url = core.call_success( "fs_join", doc_url, filename_fmt.format(page_idx + 1) ) if core.call_success("fs_exists", page_url) is None: return None return core.call_success("fs_hash", page_url) def get_page_mtime(core, doc_url, page_idx, filename_fmt): page_url = core.call_success( "fs_join", doc_url, filename_fmt.format(page_idx + 1) ) if core.call_success("fs_exists", page_url) is None: return return core.call_success("fs_get_mtime", page_url) def get_doc_mtime(core, doc_url, filename_regex): r = -1 if core.call_success("fs_exists", doc_url) is None: return if core.call_success("fs_isdir", doc_url) is None: return files = core.call_success("fs_listdir", doc_url) if files is None: return None for f in files: f = core.call_success("fs_basename", f) match = filename_regex.match(f) if match is None: continue page_url = core.call_success("fs_join", doc_url, f) r = max(r, core.call_success("fs_get_mtime", page_url)) if r < 0: return None return r paperwork-2.1.1/paperwork-backend/src/paperwork_backend/model/workdir.py000066400000000000000000000150771417573700700265540ustar00rootroot00000000000000import datetime import logging import os import openpaperwork_core LOGGER = logging.getLogger(__name__) DOCNAME_FORMAT = "%Y%m%d_%H%M_%S" class Plugin(openpaperwork_core.PluginBase): PRIORITY = -1000 # see page_delete_by_url() / page_move_by_url() def get_interfaces(self): return [ "document_storage", "stats" ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, ] def init(self, core): super().init(core) setting = self.core.call_success( "config_build_simple", "Global", "WorkDirectory", lambda: self.core.call_success( "fs_safe", os.path.expanduser("~/papers") ) ) self.core.call_all("config_register", "workdir", setting) self.core.call_all( "config_add_observer", "workdir", self._on_work_dir_changed ) def _on_work_dir_changed(self): LOGGER.info("Work directory has changed") self.core.call_all("on_storage_changed") def storage_get_id(self): """ Returns a string identifying the storage (work directory) currently used. Do not assume it's a valid directory path. For instance, this plugin could be replaced by a MariaDB database someday, and so the string would identify the database instead of a directory. """ return self.core.call_success('config_get', 'workdir') def storage_get_all_docs(self, out: list, only_valid=True): """ Returns all document IDs and URLs in the work directory """ workdir = self.core.call_success('config_get', 'workdir') if self.core.call_success('fs_exists', workdir) is None: # we are not the plugin handling this storage (?) return LOGGER.info("Loading document list from %s", workdir) nb = 0 for doc_url in self.core.call_success('fs_listdir', workdir): if not only_valid and not self.core.call_success( "fs_isdir", doc_url): continue if ( only_valid and self.core.call_success("is_doc", doc_url) is None): continue out.append(( self.core.call_success("fs_basename", doc_url), doc_url )) nb += 1 LOGGER.info("%d documents found in %s", nb, workdir) def doc_id_to_url(self, doc_id, existing=True): assert(doc_id is not None) workdir = self.core.call_success('config_get', 'workdir') url = self.core.call_success("fs_join", workdir, doc_id) if existing and self.core.call_success("fs_isdir", url) is None: return None return url def doc_url_to_id(self, doc_url): workdir = self.core.call_success('config_get', 'workdir') if not doc_url.startswith(workdir): return None return self.core.call_success("fs_basename", doc_url) def doc_get_date_by_id(self, doc_id): # Doc id is expected to have this format: # YYYYMMDD_hhmm_ss_NN_something_else doc_id = doc_id.split("_", 3) doc_id = "_".join(doc_id[:3]) try: return datetime.datetime.strptime(doc_id, DOCNAME_FORMAT) except ValueError: return None def doc_get_id_by_date(self, date): return date.strftime(DOCNAME_FORMAT) # datetime.datetime.now cannot be mocked with unittest.mock.patch # (datetime is built-in) --> allow dependency injection here def storage_get_new_doc(self, now_func=datetime.datetime.now): workdir = self.core.call_success('config_get', 'workdir') base_doc_id = now_func().strftime(DOCNAME_FORMAT) base_doc_url = self.core.call_success("fs_join", workdir, base_doc_id) doc_id = base_doc_id doc_url = base_doc_url doc_idx = 0 while self.core.call_success("fs_exists", doc_url) is not None: doc_idx += 1 doc_id = "{}_{}".format(base_doc_id, doc_idx) doc_url = "{}_{}".format(base_doc_url, doc_idx) return (doc_id, doc_url) def storage_delete_doc_id(self, doc_id, trash=True): doc_url = self.doc_id_to_url(doc_id) if doc_url is None: return self.core.call_success("fs_rm_rf", doc_url, trash=trash) def stats_get(self, stats): LOGGER.info("Counting documents for statistics...") all_docs = [] self.storage_get_all_docs(all_docs) stats['nb_documents'] += len(all_docs) def page_delete_by_url(self, doc_url, page_idx): workdir = self.core.call_success('config_get', 'workdir') if not doc_url.startswith(workdir): return None nb_pages = self.core.call_success("doc_get_nb_pages_by_url", doc_url) if nb_pages is not None and nb_pages > 0: return LOGGER.warning( "All pages of document %s have been removed. Removing document", doc_url ) self.core.call_success("fs_rm_rf", doc_url) def page_move_by_url( self, source_doc_url, source_page_idx, dest_doc_url, dest_page_idx ): workdir = self.core.call_success('config_get', 'workdir') if not source_doc_url.startswith(workdir): return None workdir = self.core.call_success('config_get', 'workdir') if not dest_doc_url.startswith(workdir): return None nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", source_doc_url ) if nb_pages is not None and nb_pages > 0: return LOGGER.warning( "All pages of document %s have been removed. Removing document", source_doc_url ) self.core.call_success("fs_rm_rf", source_doc_url) def doc_rename_by_url(self, src_doc_url, dst_doc_url): if not self.core.call_success("fs_isdir", src_doc_url): # May happen on integrated documentation PDF files LOGGER.warning("Cannot rename non-directory documents") return idx = 0 dst_url = dst_doc_url while self.core.call_success("fs_exists", dst_url) is not None: idx += 1 dst_url = "{}_{}".format(dst_doc_url, idx) self.core.call_success("fs_rename", src_doc_url, dst_url) return dst_url paperwork-2.1.1/paperwork-backend/src/paperwork_backend/pageedit/000077500000000000000000000000001417573700700251715ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/pageedit/__init__.py000066400000000000000000000024711417573700700273060ustar00rootroot00000000000000class AbstractPageEditorUI(object): CAPABILITY_SHOW_FRAME = (1 << 0) CAPABILITIES = 0 def can(self, capability): return bool(self.CAPABILITIES & capability) def set_modifier_state(self, modifier_id, enabled): """ Indicates if an modifier must be shown as enabled or disabled. """ pass def show_preview(self, img): """ Indicates the image to display, with all the changes applied from the modifiers already applied on it. Only called when the image changed. """ return def show_frame_selector(self): """ Tells the UI that it must let the user select a frame on the image (image provided by `show_image`). It also specify the current frame to display to the user. Called every time the image changes if we want the frame to be shown. Frame can be obtained by calling PageEditor.frame.get() and can be updated with PageEditor.frame.set(). """ return def hide_frame_selector(self): """ Tells the UI to not let the user select a frame anymore. """ return def on_edit_end(self, doc_url, page_idx): """ Called when the user modifications have been applied or cancelled. """ pass paperwork-2.1.1/paperwork-backend/src/paperwork_backend/pageedit/pageeditor.py000066400000000000000000000204141417573700700276670ustar00rootroot00000000000000""" Plugin providing a controller object for page editing. This controller object uses a paperwork_backend.imgedit.AbstractPageEditorUI object to tells the UI what to do. The UI must reciprocate by transmitting some events to the controller object. """ import logging import openpaperwork_core import openpaperwork_core.promise from .. import _ LOGGER = logging.getLogger(__name__) class Frame(object): def __init__(self, editor, original): # convert the frame coordinates into a more convenient format self.editor = editor self.original = original def get(self): frame = None for (e, img_size) in zip( self.editor.active_modifiers, self.editor.img_sizes ): if hasattr(e, 'frame'): frame = e.frame elif frame is not None: frame = e.transform_frame(img_size, frame) return frame def set(self, frame): for (e, img_size) in zip( reversed(self.editor.active_modifiers), reversed(self.editor.img_sizes) ): if hasattr(e, 'frame'): e.frame = frame else: frame = e.untransform_frame(img_size, frame) class PageEditor(object): def __init__(self, core, doc_url, page_idx, page_editor_ui): self.core = core self.ui = page_editor_ui self.doc_url = doc_url self.page_idx = page_idx if doc_url is not None: page_url = self.core.call_success( "page_get_img_url", doc_url, page_idx, write=False ) self.original_img = self.core.call_success( "url_to_pillow", page_url ) # The frame coordinates we keep here are relative to the original # image (not the resulting image, that can, for instance, be # rotated) # TODO(Jflesch): We should use libpillowfight.scan_border() # to predefined an useful frame. original_frame = ((0, 0), self.original_img.size) else: self.original_img = None original_frame = ((0, 0), (0, 0)) self.frame = Frame(self, original_frame) modifiers = [] self.core.call_all("img_editor_get_names", modifiers) self.modifier_descriptors = {} if 'color_equalization' in modifiers: self.modifier_descriptors['color_equalization'] = { "id": "color_equalization", "name": _("Color equalization"), "modifier": 'color_equalization', "default_kwargs": {}, "need_frame": False, "togglable": True, "enabled": False, "priority": -999, } if ('cropping' in modifiers and self.ui.can(self.ui.CAPABILITY_SHOW_FRAME)): self.modifier_descriptors['crop'] = { "id": "crop", "name": _("Cropping"), "modifier": 'cropping', "default_kwargs": {}, "need_frame": True, "togglable": True, "enabled": False, "priority": 100, } if 'rotation' in modifiers: self.modifier_descriptors['rotate_clockwise'] = { "id": "rotate_clockwise", "name": _("Clockwise Rotation"), "modifier": 'rotation', "default_kwargs": {'angle': 90}, "need_frame": False, "togglable": False, "priority": 50, } self.modifier_descriptors['rotate_counterclockwise'] = { "id": "rotate_counterclockwise", "name": _("Counterclockwise Rotation"), "modifier": 'rotation', "default_kwargs": {'angle': -90}, "need_frame": False, "togglable": False, "priority": 49, } self.active_modifiers = [] # image sizes before transformation of each modifier self.img_sizes = [] if self.ui is not None: self._refresh_preview() self._refresh_frame() def get_modifiers(self): r = list(self.modifier_descriptors.values()) r.sort(key=lambda m: -m['priority']) return r def _needs_frame(self): if not self.ui.can(self.ui.CAPABILITY_SHOW_FRAME): return for e in self.active_modifiers: if hasattr(e, 'frame'): return True return False def _refresh_modifiers(self): for modifier in self.modifier_descriptors.values(): enabled = False if modifier['togglable'] and modifier['enabled']: enabled = True self.ui.set_modifier_state(modifier['id'], enabled) def _refresh_preview(self): img = self.original_img self.img_sizes = [] for e in self.active_modifiers: self.img_sizes.append(img.size) img = e.transform(img, preview=True) self.core.call_one("mainloop_execute", self.ui.show_preview, img) def _refresh_frame(self): if not self._needs_frame(): self.core.call_one("mainloop_execute", self.ui.hide_frame_selector) return self.core.call_one( "mainloop_execute", self.ui.show_frame_selector ) def _on_modifier_selected(self, modifier_id): modifier_descriptor = self.modifier_descriptors[modifier_id] if modifier_descriptor['togglable'] and modifier_descriptor['enabled']: self.core.call_all( "img_editor_unset", self.active_modifiers, modifier_descriptor['modifier'] ) modifier_descriptor['enabled'] = False else: self.core.call_all( "img_editor_set", self.active_modifiers, modifier_descriptor['modifier'], **modifier_descriptor['default_kwargs'] ) modifier_descriptor['enabled'] = True self._refresh_modifiers() self._refresh_preview() self._refresh_frame() def on_modifier_selected(self, modifier_id): return openpaperwork_core.promise.ThreadedPromise( self.core, self._on_modifier_selected, args=(modifier_id,) ) def _on_save(self): img = self.original_img for e in self.active_modifiers: img = e.transform(img, preview=False) page_url = self.core.call_success( "page_get_img_url", self.doc_url, self.page_idx, write=True ) self.core.call_success("pillow_to_url", img, page_url) # Drop the text so the OCR will be run again self.core.call_all( "page_set_boxes_by_url", self.doc_url, self.page_idx, [] ) self.core.call_success( "mainloop_schedule", self.ui.on_edit_end, self.doc_url, self.page_idx ) def on_save(self): return openpaperwork_core.promise.ThreadedPromise( self.core, self._on_save ) def on_cancel(self): self.active_modifiers = [] self.ui.on_edit_end(self.doc_url, self.page_idx) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['page_editor'] def get_deps(self): return [ { "interface": "img_editor", 'defaults': [ 'paperwork_backend.imgedit.color', 'paperwork_backend.imgedit.crop', 'paperwork_backend.imgedit.rotate', ], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'pillow', 'defaults': [ 'openpaperwork_core.pillow.img', 'paperwork_backend.pillow.pdf', ] }, { 'interface': 'thread', 'defaults': ['openpaperwork_core.thread.simple'], }, ] def page_editor_get(self, doc_url, page_idx, page_editor_ui): return PageEditor(self.core, doc_url, page_idx, page_editor_ui) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/pagetracker.py000066400000000000000000000127211417573700700262540ustar00rootroot00000000000000""" This plugin is an helper for other plugins. It provides an easy way to track pages that have not yet being treated in each document. For instance, when a document is notified as updated (see transactions), the OCR plugin needs to know which pages of this document have already been OCR-ed and which haven't. """ import logging import sqlite3 import openpaperwork_core # Beware that we use Sqlite, but sqlite python module is not thread-safe # --> all the calls to sqlite module functions must happen on the main loop, # even those in the transactions (which are run in a thread) LOGGER = logging.getLogger(__name__) CREATE_TABLES = [ ( "CREATE TABLE IF NOT EXISTS pages (" " doc_id TEXT NOT NULL," " page INTEGER NOT NULL," " hash TEXT NOT NULL," " PRIMARY KEY (doc_id, page)" ")" ), ] class PageTracker(object): def __init__(self, core, sql_file): self.core = core sql_file = self.core.call_success("fs_unsafe", sql_file) self.sql = self.core.call_one( "mainloop_execute", sqlite3.connect, sql_file ) for query in CREATE_TABLES: self.core.call_one("mainloop_execute", self.sql.execute, query) self.core.call_one( "mainloop_execute", self.sql.execute, "BEGIN TRANSACTION" ) def _close(self): self.core.call_one("mainloop_execute", self.sql.close) def cancel(self): self.core.call_one( "mainloop_execute", self.sql.execute, "ROLLBACK" ) self._close() def commit(self): self.core.call_one("mainloop_execute", self.sql.execute, "COMMIT") self._close() def find_changes(self, doc_id, doc_url): """ Examine a document. Return page that haven't been handled yet or that have been modified since. Don't forget to call ack_page() once you've handled each page. """ out = [] db_pages = self.core.call_one( "mainloop_execute", self.sql.execute, "SELECT page, hash FROM pages" " WHERE doc_id = ?", (doc_id,) ) db_pages = self.core.call_one( "mainloop_execute", lambda pages: {r[0]: int(r[1], 16) for r in pages}, db_pages ) db_hashes = set(db_pages.values()) fs_nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", doc_url ) fs_pages = {} for page_idx in range(0, fs_nb_pages): fs_pages[page_idx] = self.core.call_success( "page_get_hash_by_url", doc_url, page_idx ) for (page_idx, fs_page_hash) in fs_pages.items(): if page_idx not in db_pages: out.append(('new', page_idx)) else: db_page_hash = db_pages.pop(page_idx) if db_page_hash != fs_page_hash: if fs_page_hash in db_hashes: # this page existed before out.append(('moved', page_idx)) else: out.append(('upd', page_idx)) for (db_page_idx, h) in db_pages.items(): self.core.call_one( "mainloop_execute", self.sql.execute, "DELETE FROM pages" " WHERE doc_id = ? AND page = ?", (doc_id, db_page_idx) ) return out def ack_page(self, doc_id, doc_url, page_idx): """ Mark the page update has handled. """ page_hash = self.core.call_success( "page_get_hash_by_url", doc_url, page_idx ) self.core.call_one( "mainloop_execute", self.sql.execute, "INSERT OR REPLACE" " INTO pages (doc_id, page, hash)" " VALUES (?, ?, ?)", (doc_id, page_idx, format(page_hash, 'x')) ) def delete_doc(self, doc_id): self.core.call_one( "mainloop_execute", self.sql.execute, "DELETE FROM pages WHERE doc_id = ?", (doc_id,) ) class Plugin(openpaperwork_core.PluginBase): def __init__(self): pass def get_interfaces(self): return ['page_tracking'] def get_deps(self): return [ { 'interface': 'data_versioning', 'defaults': ['openpaperwork_core.data_versioning'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'] }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'page_boxes', 'defaults': ['paperwork_backend.model.hocr'], }, { 'interface': 'page_img', 'defaults': [ 'paperwork_backend.model.img', 'paperwork_backend.model.pdf', ], }, { 'interface': 'data_dir_handler', 'defaults': ['paperwork_backend.datadirhandler'], }, ] def page_tracker_get(self, tracking_id): paperwork_dir = self.core.call_success( "data_dir_handler_get_individual_data_dir" ) sql_file = self.core.call_success( "fs_join", paperwork_dir, 'page_tracking_{}.db'.format(tracking_id) ) return PageTracker(self.core, sql_file) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/pillow/000077500000000000000000000000001417573700700247155ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/pillow/__init__.py000066400000000000000000000000001417573700700270140ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/pillow/pdf.py000066400000000000000000000072351417573700700260470ustar00rootroot00000000000000import io import logging import PIL import PIL.Image import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) def surface2image(core, surface): """ Convert a cairo surface into a PIL image """ # XXX(Jflesch): Python 3 problem # cairo.ImageSurface.get_data() raises NotImplementedYet ... # import PIL.ImageDraw # # if surface is None: # return None # dimension = (surface.get_width(), surface.get_height()) # img = PIL.Image.frombuffer("RGBA", dimension, # surface.get_data(), "raw", "BGRA", 0, 1) # # background = PIL.Image.new("RGB", img.size, (255, 255, 255)) # background.paste(img, mask=img.split()[3]) # 3 is the alpha channel # return background core.call_all("on_perfcheck_start", "surface2image") img_io = io.BytesIO() surface.surface.write_to_png(img_io) img_io.seek(0) img = PIL.Image.open(img_io) core.call_all("on_objref_track", img) img.load() if "A" not in img.getbands(): core.call_all("on_perfcheck_stop", "surface2image", size=img.size) return img img_no_alpha = PIL.Image.new("RGB", img.size, (255, 255, 255)) core.call_all("on_objref_track", img_no_alpha) img_no_alpha.paste(img, mask=img.split()[3]) # 3 is the alpha channel core.call_all( "on_perfcheck_stop", "surface2image", size=img_no_alpha.size ) return img_no_alpha class Plugin(openpaperwork_core.PluginBase): FILE_EXTENSION = ".pdf" def get_interfaces(self): return [ 'page_img_size', 'pillow', ] def get_deps(self): return [ { 'interface': 'pdf_cairo_url', 'defaults': ['paperwork_backend.cairo.poppler'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'urls', 'defaults': ['openpaperwork_core.urls'], }, ] def _check_is_pdf(self, file_url): (url, args) = self.core.call_success("url_args_split", file_url) if not url.lower().endswith(self.FILE_EXTENSION): return (None, None, None) page_idx = int(args.get("page", 1)) - 1 password = args.get('password', None) if password is not None: password = bytes.fromhex(password).decode("utf-8") return (file_url, page_idx, password) def url_to_pillow(self, file_url): (file_url, page_idx, password) = self._check_is_pdf(file_url) if file_url is None: return None pillow = self.core.call_one( # Poppler is not really thread safe "mainloop_execute", self._url_to_pillow, file_url, page_idx, password ) return pillow def cairo_surface_to_pillow(self, surface): return surface2image(self.core, surface) def _url_to_pillow(self, file_url, page_idx, password): surface = self.core.call_success( "pdf_page_to_cairo_surface", file_url, page_idx, password ) img = surface2image(self.core, surface) surface.surface.finish() img.load() return img def url_to_pillow_promise(self, file_url): (doc_url, page_idx, password) = self._check_is_pdf(file_url) if doc_url is None: return None return openpaperwork_core.promise.Promise( self.core, self.url_to_pillow, args=(file_url,) ) def pillow_to_url(self, *args, **kwargs): # It could be implemented, but there are no known use-cases. return None paperwork-2.1.1/paperwork-backend/src/paperwork_backend/poppler/000077500000000000000000000000001417573700700250705ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/poppler/__init__.py000066400000000000000000000000001417573700700271670ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/src/paperwork_backend/poppler/file.py000066400000000000000000000034251417573700700263650ustar00rootroot00000000000000import os import openpaperwork_core import openpaperwork_core.deps GI_AVAILABLE = False GLIB_AVAILABLE = False POPPLER_AVAILABLE = False try: import gi GI_AVAILABLE = True except (ImportError, ValueError): pass if GI_AVAILABLE: try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): pass try: gi.require_version('Poppler', '0.18') from gi.repository import Poppler POPPLER_AVAILABLE = True except (ImportError, ValueError): pass class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'chkdeps', 'poppler', ] def get_deps(self): return [ { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, ] def chkdeps(self, out: dict): if not GI_AVAILABLE: out['gi'] = openpaperwork_core.deps.GI if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) if not POPPLER_AVAILABLE: out['poppler'] = openpaperwork_core.deps.POPPLER def poppler_open(self, url, password=None): if os.name == "nt": # WORKAROUND(Jflesch): # Disabled for now on Windows: There is a file descriptor leak # somewhere. # While it causes little problems on GNU/Linux, on Windows it # prevents deleting documents. return None gio_file = Gio.File.new_for_uri(url) doc = self.core.call_one( "mainloop_execute", Poppler.Document.new_from_gfile, gio_file, password=password ) self.core.call_all("on_objref_track", doc) return doc paperwork-2.1.1/paperwork-backend/src/paperwork_backend/poppler/memory.py000066400000000000000000000042031417573700700267510ustar00rootroot00000000000000import openpaperwork_core import openpaperwork_core.deps GI_AVAILABLE = False GLIB_AVAILABLE = False POPPLER_AVAILABLE = False try: import gi from gi.repository import GLib GI_AVAILABLE = True except (ImportError, ValueError): pass if GI_AVAILABLE: try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): pass try: gi.require_version('Poppler', '0.18') from gi.repository import Poppler POPPLER_AVAILABLE = True except (ImportError, ValueError): pass class Plugin(openpaperwork_core.PluginBase): PRIORITY = -100 def get_interfaces(self): return [ 'chkdeps', 'poppler', ] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, ] def chkdeps(self, out: dict): if not GI_AVAILABLE: out['gi'] = openpaperwork_core.deps.GI if not GLIB_AVAILABLE: out['glib'] = openpaperwork_core.deps.GLIB if not POPPLER_AVAILABLE: out['poppler'] = openpaperwork_core.deps.POPPLER def poppler_open(self, url, password=None): # Poppler.Document.new_from_data() expects .. a string # Poppler.Document.new_from_bytes() only exist starting with 0.82 with self.core.call_success("fs_open", url, "rb") as fd: data = fd.read() ldata = len(data) data = GLib.Bytes.new(data) # Gio.MemoryInputStream.new_from_data() may leak # https://stackoverflow.com/questions/45838863/gio-memoryinputstream # --> use Gio.MemoryInputStream.new_from_bytes() instead data = Gio.MemoryInputStream.new_from_bytes(data) self.core.call_all("on_objref_track", data) doc = self.core.call_one( "mainloop_execute", Poppler.Document.new_from_stream, data, ldata, password=password ) return doc paperwork-2.1.1/paperwork-backend/src/paperwork_backend/pyocr.py000066400000000000000000000157101417573700700251210ustar00rootroot00000000000000import glob import locale import logging import os import pycountry import pyocr import pyocr.builders import openpaperwork_core from . import util LOGGER = logging.getLogger(__name__) DEFAULT_OCR_LANG = "eng" # if really we can't guess anything def init_flatpak(core): """ If we are in Flatpak, we must build a tessdata/ directory using the .traineddata files from each locale directory """ tessdata_files = glob.glob("/app/share/locale/*/*.traineddata") if len(tessdata_files) <= 0: return paperwork_dir = core.call_success("paths_get_data_dir") tessdatadir = core.call_success("fs_join", paperwork_dir, "tessdata") tessdatadir = core.call_success("fs_unsafe", tessdatadir) LOGGER.info("Assuming we are running in Flatpak." " Building tessdata directory %s ...", tessdatadir) util.rm_rf(tessdatadir) os.makedirs(tessdatadir, exist_ok=True) os.symlink("/app/share/tessdata/eng.traineddata", os.path.join(tessdatadir, "eng.traineddata")) os.symlink("/app/share/tessdata/osd.traineddata", os.path.join(tessdatadir, "osd.traineddata")) os.symlink("/app/share/tessdata/configs", os.path.join(tessdatadir, "configs")) os.symlink("/app/share/tessdata/tessconfigs", os.path.join(tessdatadir, "tessconfigs")) for tessdata in tessdata_files: LOGGER.info("%s found", tessdata) os.symlink( tessdata, os.path.join(tessdatadir, os.path.basename(tessdata)) ) os.environ['TESSDATA_PREFIX'] = tessdatadir LOGGER.info("Tessdata directory ready") def find_language(lang_str=None, allow_none=False): if lang_str is None: lang_str = locale.getdefaultlocale()[0] if lang_str is None and not allow_none: LOGGER.warning("Unable to figure out locale. Assuming english !") return find_language(DEFAULT_OCR_LANG) if lang_str is None: LOGGER.warning("Unable to figure out locale !") return None lang_str = lang_str.lower() if "_" in lang_str: lang_str = lang_str.split("_")[0] LOGGER.info("System language: {}".format(lang_str)) attrs = ( 'iso_639_3_code', 'iso639_3_code', 'iso639_2T_code', 'iso639_1_code', 'terminology', 'bibliographic', 'alpha_3', 'alpha_2', 'alpha2', 'name', ) for attr in attrs: try: r = pycountry.pycountry.languages.get(**{attr: lang_str}) if r is not None: LOGGER.info("OCR language: {}".format(r)) return r except (KeyError, UnicodeDecodeError): pass if allow_none: LOGGER.warning("Unknown language [{}]".format(lang_str)) return None if lang_str is not None and lang_str == DEFAULT_OCR_LANG: raise Exception("Unable to find language !") LOGGER.warning("Unknown language [{}]. Switching back to english".format( lang_str )) return find_language(DEFAULT_OCR_LANG) def pycountry_to_tesseract(ocr_langs, possibles=None): attrs = [ 'iso639_3_code', 'terminology', 'alpha_3', ] for attr in attrs: if not hasattr(ocr_langs, attr): continue if possibles is None or getattr(ocr_langs, attr) in possibles: r = getattr(ocr_langs, attr) if r is not None: return r return None def get_default_ocr_langs(allow_none=False): # Try to guess based on the system locale what would be # the best OCR language ocr_tools = pyocr.get_available_tools() if len(ocr_tools) == 0: return None if allow_none else [DEFAULT_OCR_LANG] ocr_langs = ocr_tools[0].get_available_languages() lang = find_language(allow_none=True) if lang is None: return None if allow_none else [DEFAULT_OCR_LANG] lang = pycountry_to_tesseract(lang, ocr_langs) if lang is not None: return [lang] return None if allow_none else [DEFAULT_OCR_LANG] class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() def get_interfaces(self): return [ "chkdeps", "ocr_settings", ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'paths', 'defaults': ['openpaperwork_core.paths.xdg'], }, { 'interface': 'data_versioning', 'defaults': ['openpaperwork_core.data_versioning'], }, ] def init(self, core): super().init(core) init_flatpak(self.core) ocr_langs = self.core.call_success( "config_build_simple", "OCR", "Lang", get_default_ocr_langs ) self.core.call_all("config_register", "ocr_langs", ocr_langs) def chkdeps(self, out: dict): ocr_tools = pyocr.get_available_tools() if len(ocr_tools) <= 0: out['tesseract']['debian'] = 'tesseract-ocr' out['tesseract']['fedora'] = 'tesseract' out['tesseract']['gentoo'] = 'app-text/tesseract' out['tesseract']['linuxmint'] = 'tesseract-ocr' out['tesseract']['raspbian'] = 'tesseract-ocr' out['tesseract']['ubuntu'] = 'tesseract-ocr' ocr_lang = get_default_ocr_langs(allow_none=True) if ocr_lang is None: ocr_lang = find_language(allow_none=True) if ocr_lang is None: ocr_lang = "UNKNOWN" else: ocr_lang = pycountry_to_tesseract(ocr_lang) if ocr_lang is None: ocr_lang = "" name = 'tesseract-data-{}'.format(ocr_lang) out[name]['debian'] = 'tesseract-ocr-{}'.format(ocr_lang) out[name]['fedora'] = 'tesseract-langpack-{}'.format(ocr_lang) out[name]['linuxmint'] = 'tesseract-ocr-{}'.format(ocr_lang) out[name]['raspbian'] = 'tesseract-ocr-{}'.format(ocr_lang) out[name]['ubuntu'] = 'tesseract-ocr-{}'.format(ocr_lang) def ocr_get_active_langs(self): return self.core.call_success("config_get", "ocr_langs") def ocr_set_active_langs(self, langs): return self.core.call_success("config_put", "ocr_langs", langs) def ocr_is_enabled(self): if len(self.ocr_get_active_langs()) > 0: return True return None def ocr_add_observer_on_enabled(self, callback): self.core.call_all("config_add_observer", "ocr_langs", callback) def ocr_get_available_langs(self): ocr_tools = pyocr.get_available_tools() if len(ocr_tools) <= 0: return [] return ocr_tools[0].get_available_languages() def ocr_set_lang(self, lang): return self.core.call_success( "config_set", "ocr_langs", lang ) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/sync.py000066400000000000000000000231441417573700700247410ustar00rootroot00000000000000import datetime import logging import time import openpaperwork_core import openpaperwork_core.promise LOGGER = logging.getLogger(__name__) class BaseTransaction(object): def __init__(self, core, total_expected): self.core = core self.processed = 0 self.total = total_expected self._current_doc = None self._current_doc_pages = -1 def notify_progress( self, upd_type, description, page_nb=-1, total_pages=-1): if self.total <= self.processed: self.total = self.processed + 1 if self.total <= 0: progression = 0 else: progression = self.processed / self.total if page_nb >= 0 and total_pages > 0: progression += (page_nb / total_pages / self.total) self.core.call_one( "mainloop_schedule", self.core.call_all, "on_progress", upd_type, progression, description ) def notify_done(self, upd_type): self.core.call_one( "mainloop_schedule", self.core.call_all, "on_progress", upd_type, 1.0 ) def add_doc(self, doc_id): self._current_doc = doc_id self._current_doc_pages = -1 self.processed += 1 def upd_doc(self, doc_id): self._current_doc = doc_id self._current_doc_pages = -1 self.processed += 1 def del_doc(self, doc_id): self._current_doc = doc_id self._current_doc_pages = -1 self.processed += 1 def unchanged_doc(self, doc_id): self.processed += 1 def cancel(self): self._current_doc = None self._current_doc_pages = -1 def commit(self): self._current_doc = None def diff_lists(list_old, list_new): """ Returns a dictionary giving the differences between both lists. Objects in the list must have 2 attributes: - `key` - `extra` `key` values identify each object in both lists. Comparisons are done on `key` + `extra`. `list_old` will be unrolled immediately. Arguments: `list_old` -- [a, b, c, ...] `list_new` -- [a, c_modified, d, ...] Returns: [ ('same', a.key), ('del', b.key), ('upd', c.key), ('add', d.key), ] list_old + diff => list_new """ list_old = {obj.key: obj for obj in list_old} examined_new = set() for obj_new in list_new: examined_new.add(obj_new.key) if obj_new.key not in list_old: yield ('added', obj_new.key) continue obj_old = list_old[obj_new.key] if obj_new.extra != obj_old.extra: yield ('updated', obj_new.key) else: yield ('unchanged', obj_new.key) for obj_old in list_old.values(): if obj_old.key not in examined_new: yield ('deleted', obj_old.key) class StorageDoc(object): def __init__(self, core, doc_id, doc_url): self.core = core self.key = doc_id self.doc_url = doc_url def get_mtime(self): mtime = self.core.call_success("doc_get_mtime_by_url", self.doc_url) if mtime is None: mtime = 0 return datetime.datetime.fromtimestamp(mtime) extra = property(get_mtime) class Syncer(object): """ This object allows to compare mtimes progressively. It then calls the methods `add_doc`, `del_doc`, `upd_doc` and `commit` on the given transaction object. Useful to handle calls to 'sync' (see interface 'syncable'). """ def __init__(self, core, names, new_all, old_all, transactions): self.core = core self.names = names self.new_all = new_all self.old_all = old_all self.transactions = transactions self.diff_generator = None self.start = None self.nb_compared = 0 def get_promise(self): return openpaperwork_core.promise.ThreadedPromise( self.core, self.run ) def run(self): self.start = time.time() self.diff_generator = diff_lists(self.old_all, self.new_all) try: for (action, key) in self.diff_generator: self.nb_compared += 1 if action != 'unchanged': LOGGER.info("Sync: %s, %s", action, key) for name in self.names: self.core.call_one( "mainloop_schedule", self.core.call_all, "on_sync", name, action, key ) if action == "added": for transaction in self.transactions: LOGGER.debug( "transaction.add_doc<%s>(%s)", transaction.add_doc, key ) transaction.add_doc(key) elif action == "updated": for transaction in self.transactions: LOGGER.debug( "transaction.upd_doc<%s>(%s)", transaction.upd_doc, key ) transaction.upd_doc(key) elif action == "deleted": for transaction in self.transactions: LOGGER.debug( "transaction.del_doc<%s>(%s)", transaction.del_doc, key ) transaction.del_doc(key) else: for transaction in self.transactions: LOGGER.debug( "transaction.unchanged_doc<%s>(%s)", transaction.unchanged_doc, key ) transaction.unchanged_doc(key) LOGGER.info("Sync: Committing ...") for transaction in self.transactions: transaction.commit() LOGGER.info("Sync: Committed") stop = time.time() LOGGER.info( "%s: Has compared %d objects in %.3fs", self.names, self.nb_compared, stop - self.start ) except Exception as exc: LOGGER.error( "%s: Fail to sync. Cancelling transactions", self.names, exc_info=exc ) for transaction in self.transactions: transaction.cancel() class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['transaction_manager'] def get_deps(self): return [ { 'interface': 'work_queue', 'defaults': ['openpaperwork_core.work_queue.default'], }, ] def init(self, core): super().init(core) self.core.call_all("work_queue_create", "transactions") def transaction_schedule(self, promise): """ Transactions should never be run in parrallel (even if on the same thread). Some databases (Sqlite3) don't support that. --> we use a work queue to ensure they are run one after the other. """ return self.core.call_success( "work_queue_add_promise", "transactions", promise ) def _transaction_simple(self, changes): if len(changes) <= 0: LOGGER.info("No change. Nothing to do") return transactions = [] self.core.call_all("doc_transaction_start", transactions, len(changes)) transactions.sort(key=lambda transaction: -transaction.priority) try: for (change, doc_id) in changes: doc_url = self.core.call_success("doc_id_to_url", doc_id) if doc_url is None: change = 'del' elif self.core.call_success("is_doc", doc_url) is None: change = 'del' for transaction in transactions: if change == 'add': transaction.add_doc(doc_id) elif change == 'upd': transaction.upd_doc(doc_id) elif change == 'del': transaction.del_doc(doc_id) else: raise Exception("Unknown change type: %s" % change) for transaction in transactions: transaction.commit() except Exception as exc: LOGGER.error("Transactions have failed", exc_info=exc) for transaction in transactions: transaction.cancel() raise def transaction_simple_promise(self, changes): """ See transaction_simple(). Must be scheduled with 'transaction_schedule()'. """ return openpaperwork_core.promise.ThreadedPromise( self.core, self._transaction_simple, args=(changes,) ) def transaction_simple(self, changes: list): """ Helper method. Schedules a transaction for a bunch of document ids. Changes must be a list: [ ('add', 'some_doc_id'), ('upd', 'some_doc_id_2'), ('upd', 'some_doc_id_3'), ('del', 'some_doc_id_4'), ] """ return self.transaction_schedule( self.transaction_simple_promise(changes) ) def transaction_sync_all(self): """ Make sure all the plugins synchronize their databases with the work directory. """ promises = [] self.core.call_all("sync", promises) promise = promises[0] for p in promises[1:]: promise = promise.then(p) self.transaction_schedule(promise) paperwork-2.1.1/paperwork-backend/src/paperwork_backend/util.py000066400000000000000000000031551417573700700247420ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2014 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see import logging import os LOGGER = logging.getLogger(__name__) def rm_rf(path): """ Act as 'rm -rf' in the shell """ if os.path.isfile(path): os.unlink(path) elif os.path.isdir(path): for root, dirs, files in os.walk(path, topdown=False): for filename in files: filepath = os.path.join(root, filename) LOGGER.info("Deleting file %s" % filepath) os.unlink(filepath) for dirname in dirs: dirpath = os.path.join(root, dirname) if os.path.islink(dirpath): LOGGER.info("Deleting link %s" % dirpath) os.unlink(dirpath) else: LOGGER.info("Deleting dir %s" % dirpath) os.rmdir(dirpath) LOGGER.info("Deleting dir %s", path) os.rmdir(path) paperwork-2.1.1/paperwork-backend/tests/000077500000000000000000000000001417573700700203015ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/__init__.py000066400000000000000000000000001417573700700224000ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/beacon/000077500000000000000000000000001417573700700215305ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/beacon/__init__.py000066400000000000000000000000001417573700700236270ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/beacon/tests_update.py000066400000000000000000000037741417573700700246210ustar00rootroot00000000000000import datetime import http import http.server import json import unittest import threading import time import openpaperwork_core class TestUpdate(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.config.fake") self.core.load("paperwork_backend.app") self.core.load("paperwork_backend.beacon.update") self.config = self.core.get_by_name("openpaperwork_core.config.fake") self.received = [] def test_check_update(self): self.received = [] class TestRequestHandler(http.server.BaseHTTPRequestHandler): def do_GET(s): self.assertEqual(s.path, "/beacon/latest") s.send_response(200) s.send_header('Content-type', 'application/json') s.end_headers() s.wfile.write(json.dumps({ "paperwork": { "posix": "999.1.2", "nt": "999.1.2", }, }).encode("utf-8")) with http.server.HTTPServer(('', 0), TestRequestHandler) as h: self.config.settings = { "check_for_update": True, "last_update_found": "0.1.3", "update_last_run": datetime.date(1995, 1, 1), "update_protocol": "http", "update_server": "127.0.0.1:{}".format(h.server_port), } threading.Thread(target=h.handle_request).start() time.sleep(0.1) class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): def on_update_detected(s, current, new): self.assertEqual(new, (999, 1, 2)) self.core.call_all("mainloop_quit_graceful") self.core._load_module( "fake_module", FakeModule() ) self.core.init() self.core.call_one('mainloop') paperwork-2.1.1/paperwork-backend/tests/chkworkdir/000077500000000000000000000000001417573700700224505ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/chkworkdir/__init__.py000066400000000000000000000000001417573700700245470ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/chkworkdir/tests_empty_doc.py000066400000000000000000000044441417573700700262350ustar00rootroot00000000000000import unittest import openpaperwork_core class TestChkWorkDirEmptyDirectory(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.config.fake") self.core.load("openpaperwork_core.fs.fake") self.core.load("paperwork_backend.chkworkdir.empty_doc") self.core.init() self.config = self.core.get_by_name("openpaperwork_core.config.fake") self.config.settings = { "workdir": "file:///some_work_dir" } self.fs = self.core.get_by_name("openpaperwork_core.fs.fake") def test_no_problem(self): self.fs.fs = { "some_work_dir": { "some_doc_a": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", }, "some_doc_b": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", }, }, } problems = [] self.core.call_all("check_work_dir", problems) self.assertEqual(len(problems), 0) def test_check_fix(self): self.fs.fs = { "some_work_dir": { "some_doc_a": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", }, "some_doc_b": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", }, "some_doc_empty": {}, }, } problems = [] self.core.call_all("check_work_dir", problems) self.assertEqual(len(problems), 1) self.core.call_all("fix_work_dir", problems) self.assertEqual( self.fs.fs, { "some_work_dir": { "some_doc_a": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", }, "some_doc_b": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", }, }, } ) paperwork-2.1.1/paperwork-backend/tests/chkworkdir/tests_label_color.py000066400000000000000000000162741417573700700265330ustar00rootroot00000000000000import unittest import openpaperwork_core class TestChkWorkDirEmptyDirectory(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.config.fake") self.core.load("openpaperwork_core.fs.fake") self.core.load("paperwork_backend.chkworkdir.label_color") self.core.init() self.config = self.core.get_by_name("openpaperwork_core.config.fake") self.config.settings = { "workdir": "file:///some_work_dir" } self.fs = self.core.get_by_name("openpaperwork_core.fs.fake") def test_no_problem(self): self.fs.fs = { "some_work_dir": { "some_doc_a": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", "labels": ( "coloc,rgb(182,133,45)\n" "facture,rgb(0,177,140)\n" "logement,rgb(246,255,0)\n" ) }, "some_doc_b": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", "labels": ( "banque,#702000006e5f\n" "fiche de paie,rgb(0,155,0)\n" ) }, }, } problems = [] self.core.call_all("check_work_dir", problems) self.assertEqual(len(problems), 0) def test_check_fix(self): self.fs.fs = { "some_work_dir": { "some_doc_a": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", "labels": ( "coloc,rgb(182,133,45)\n" "facture,#aabbccddeeff\n" "logement,rgb(246,255,0)\n" ) }, "some_doc_b": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", "labels": ( "banque,#702000006e5f\n" "fiche de paie,rgb(0,155,0)\n" ) }, "some_doc_zzz_bad_label_color": { "labels": ( "coloc,rgb(182,133,45)\n" "facture,rgb(0,177,140)\n" "logement,rgb(246,255,0)\n" ) }, }, } problems = [] self.core.call_all("check_work_dir", problems) self.assertEqual(len(problems), 1) self.core.call_all("fix_work_dir", problems) fs = self.fs.fs self.assertEqual( fs['some_work_dir']['some_doc_zzz_bad_label_color']['labels'], ( "coloc,rgb(182,133,45)\n" "logement,rgb(246,255,0)\n" "facture,#aabbccddeeff\n" # fixed ) ) self.maxDiff = None self.assertEqual( self.fs.fs, { "some_work_dir": { "some_doc_a": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", "labels": ( "coloc,rgb(182,133,45)\n" "facture,#aabbccddeeff\n" "logement,rgb(246,255,0)\n" ) }, "some_doc_b": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", "labels": ( "banque,#702000006e5f\n" "fiche de paie,rgb(0,155,0)\n" ) }, "some_doc_zzz_bad_label_color": { "labels": ( "coloc,rgb(182,133,45)\n" "logement,rgb(246,255,0)\n" "facture,#aabbccddeeff\n" # fixed ) }, }, } ) problems = [] self.core.call_all("check_work_dir", problems) self.assertEqual(len(problems), 0) def test_check_rgb(self): self.fs.fs = { "some_work_dir": { "some_doc_a": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", "labels": ( "coloc,rgb(182,133,45)\n" "facture,rgb(1,10,20)\n" "logement,rgb(246,255,0)\n" ) }, "some_doc_b": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", "labels": ( "banque,#702000006e5f\n" "fiche de paie,rgb(0,155,0)\n" ) }, "some_doc_zzz_bad_label_color": { "labels": ( "coloc,rgb(182,133,45)\n" "facture,rgb(0,177,140)\n" "logement,rgb(246,255,0)\n" ) }, }, } problems = [] self.core.call_all("check_work_dir", problems) self.assertEqual(len(problems), 1) self.core.call_all("fix_work_dir", problems) fs = self.fs.fs self.assertEqual( fs['some_work_dir']['some_doc_zzz_bad_label_color']['labels'], ( "coloc,rgb(182,133,45)\n" "logement,rgb(246,255,0)\n" "facture,rgb(1,10,20)\n" # fixed ) ) self.maxDiff = None self.assertEqual( self.fs.fs, { "some_work_dir": { "some_doc_a": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", "labels": ( "coloc,rgb(182,133,45)\n" "facture,rgb(1,10,20)\n" "logement,rgb(246,255,0)\n" ) }, "some_doc_b": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", "labels": ( "banque,#702000006e5f\n" "fiche de paie,rgb(0,155,0)\n" ) }, "some_doc_zzz_bad_label_color": { "labels": ( "coloc,rgb(182,133,45)\n" "logement,rgb(246,255,0)\n" "facture,rgb(1,10,20)\n" # fixed ) }, }, } ) problems = [] self.core.call_all("check_work_dir", problems) self.assertEqual(len(problems), 0) paperwork-2.1.1/paperwork-backend/tests/converter/000077500000000000000000000000001417573700700223105ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/converter/__init__.py000066400000000000000000000000001417573700700244070ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/converter/test.docx000066400000000000000000000100731417573700700241470ustar00rootroot00000000000000PKf0R _rels/.relsMKA Cl+"Bo"3iA PǼymNAêiAq0Ѻ0jx=/`/W>J\*ބaIL41q!fORWeS.IC iW)D ^dVNx!1_Ļ97'd,Ӧhp]5nTQ?PK]#PKf0RdocProps/core.xmlRN0T*`%'*!(q365Ďe-{I mggRud DEy# W(riFCЬȹ`KpQ0ҎrSbsIP@V0*YN Rll;LZV:D8+Q(vnIC.QcUq@e~hr ̃ n9Gef$NILKrM)[o eA8n=#pt /W6?v1՞f/W>x C#]R29i0*[+:¶kyFb/} }zPK aPKf0Rword/_rels/document.xml.relsM 0"ަUnDp+16 (z{Z(}1/__]m,IQҦp(%INR\ vDnyP-2$֡^R,}ÝT' O&Uʀ7m]k=nHA>.?|m ?@IwPK/0PKf0Rword/settings.xmlEK0 D"SBkRbG +73z+E"#f 0l7%>jn)Ȃ3ReW.)hf'.C.ܣHhέl #AW/?Lm#iiQOrTε m]/PKe"PKf0Rword/fontTable.xmlPAN0 wz[gXב$N+!ȡٙY?=6*9R i_:T}r(G'YWwˡl]jQʁ!y 1pHq h! 59JBCěw@['NE!Ug2 D^={'HfLGvkqkD<OWǐ[ ɮ`&}d[=O{>Ο:?xPKUPKf0Rword/document.xmlTێ0}W 'j%Q*R0ƀDzMXHUU`9g۷wR$'f,Ul) WM~6(P@n+d%A [2IJrjBVdu) S9c֠X F痦c.jiz yNl?韤KT{06@~Rp5d B_\?|nd7 }E9X{qzgϗ/-lak@'6I/V01hwùTv6W/gw|aHkR t- xm!YT_HiX8 R)9 t.Q꾓j-=}(6ހ [qڄ s,AiPKI zPKf0Rword/styles.xmlUmO0_{IAjJP7Z8s()}i|~:(Utcqx0dRcv28y(q,VHçJ!Eivǔ.:05rJtE4.ΤHK 'q RVRΐAjL.T]2} ]e\ςs_dDTin8^3^bk~L-G@cq%踼 : -.4! c134x|$!RM%ރq:͛AQoCr7\?$eƼr0ĸ:nc)3LXgTlʲ"C,h¶$ %vpIm}ejq]Py-~!;Ak>s ̾>4>OgwxhgkIUfʺ9Ǡ0ܣh(^<6{=ƢRۿfiM7-lUtˡ6XIU0ymfD9D>ؕl]H}eo\ζI'`L54Pe?+V<,Yx~beЋ/J6ekw뽦{_PK-e PKf0R[Content_Types].xmlT9O0+"(qa@%1B0#c$BUh ﴜ|Tm9$͍.Sy^d+ >] ҄`)|f,hT)2j%F! L[ؼ ׺'z/JYJi^.snev|#?ZzO@,#^-Cbn'$3S@c8O0|ýșcA-@) is۬}qbS4Gѝ)_702l-T(ZyQ\i=&I=|a_Fǻ_N>PKBS^PKf0R#= _rels/.relsPKf0R]#docProps/app.xmlPKf0R asdocProps/core.xmlPKf0R/0word/_rels/document.xml.relsPKf0Re""word/settings.xmlPKf0RUword/fontTable.xmlPKf0RI zcword/document.xmlPKf0R-e  word/styles.xmlPKf0RBS^U [Content_Types].xmlPK < paperwork-2.1.1/paperwork-backend/tests/converter/tests_libreoffice.py000066400000000000000000000021231417573700700263530ustar00rootroot00000000000000import os import unittest import openpaperwork_core class TestLibreOfficePdf(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("paperwork_backend.converter.libreoffice") self.core.init() self.test_doc = os.path.join( os.path.dirname(os.path.abspath(__file__)), "test.docx" ) def test_convert_pdf(self): (tmp_url, tmp_fd) = self.core.call_success( "fs_mktemp", suffix=".pdf" ) tmp_fd.close() self.core.call_success("fs_unlink", tmp_url, trash=False) self.assertIsNone(self.core.call_success("fs_exists", tmp_url)) self.core.call_success( "convert_file_to_pdf", self.core.call_success("fs_safe", self.test_doc), "application/vnd.openxmlformats-officedocument" ".wordprocessingml.document", tmp_url ) self.assertTrue(self.core.call_success("fs_exists", tmp_url)) self.core.call_success("fs_unlink", tmp_url, trash=False) paperwork-2.1.1/paperwork-backend/tests/docexport/000077500000000000000000000000001417573700700223105ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/docexport/__init__.py000066400000000000000000000000001417573700700244070ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/docexport/test_img_doc/000077500000000000000000000000001417573700700247505ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/docexport/test_img_doc/paper.1.jpg000066400000000000000000000142231417573700700267220ustar00rootroot00000000000000JFIFvvCreated with GIMPC        d > !"1A2 #Q8qu$6B%9Rart?JR)JR)JR)QLq_%?ϗ#_._ )3O>CBJ+m w}%}~F=@M>0 ]ET_|W~I -GQ=3Ȫ|71وnMl Mn [ eK#`[XJVZ㍅t݈b5ƉG.֕ 7|ڶ-4(x񦡷!j A5Nc $DC]*ș˾"BI/A뿏}5ؚ:YaK :z~}?e:v]oC8 ~;''6A1>?_{KoA})JR\H{iTb-[c`k>_7io.WYgM T@A/ Ow$*Ó c8 wuu9#ț?%\DaIfL% twN{ L@vx 7]~^r3N&Iӹ#\it]0} LsF'2Cu坪ܘqqbH t3pKSzq:;ͮ8tN(/L-1& )휉v8JN Л~ =FS0) 0U+ɚü%%YX|ټڱt+&_9v!AL_u_Jq59YHߎ/l֧3c+-ٿ=^`~"Tɤk-Rc aP7V XnTx3ހSYӴeE@YS~!A.TMBXsWgx˻:Ju+NIbeGi?*Μ9H)AƯ~Oّ2%r$ kA)JR췃n72C# d]/}9Jm&rh5YVk:q֓֒$5ZEElCtPɐQd9xŶ-nKuCEnEDHS(b} #g2;"cqhE кtIFˑEʐ3Q%NT\*`|%d)bW;sh#LS}.=akz] 쏞2 s*E38 A4c&9&0ƽܢŗmmW%HfibCmC&CEGRv?Lwuúp(E,}"Shw¹:W&pׅ+y\q HlE 9YA:! U~`ŷV.|&LK* J(D@t?}jȘ·vd!\8h M$ݨΓd~:a *W{θhkB f+, =玥I|y#E[,ʡ@J"~ -كq,sm^ؙ z#G>D*nXHES855nsyͯ,Z7rۑ<0 g>NbwY`U׮0JɤRЯLvG ( 'o_l~\e ϖ,L̓pjT$ssLaq/Ɩ q1Oٿk<&2wR)JR)JR)JR)JO-jO#IL.θIMM Rc<"%."a8l\66y E3^2!E¥M'Jy=JmǹMSD6@gى#~qKT w̋AeJo; St==WTwuͅ$TE;h4HYw*(5Db~Gq6Zs{'HjE銉`) őyC9qYR[wqYkX*ɀr.aaR6R_ĕY)UPxʁ2S)LY5NE D5\]svŕ>W}ٹF b<.V,+Bm™5@?i/nJ@ˤTF= ™1CB03y?&Yv™YX0MC;)P1c({~n"HplDfMEt0x-H&0h}{K q"UC~^ Wy7nO8B}זĢG*.e{6ҧQj'̑fN:o.8cxJtJ eDPMuqmݮ\^2-͍s#3HUbGD D!|IH=۪g=l)>j5EQ\<]7}ht;oG&r 2BYS> !H~C.:Vf'0@NQ(@ bba?[ZR_H闭Zi z`B-=K`ѓj9eKL(bR4j3NƳ9.xgΥ‚ ,P1+2M{(&ټ`!T]=c9sW(IIW1g#ڟFPpeHꐆ0z}nW`>Hg[?5]q6bd+Ny,JQ( YSDȨb`zG5eds լSBT;3U.ݓ7t"wwdK 9s;0+:~X ~&E'YeU1"Mn6>ն( M@]j+Cq )o\#ضXHz:2 "hUznxH`~^,cUiUv-ĂJaps(&SJ>C`V0ȜWmwz-eRA"* o)Cj&y[ޖ"vw# ZU= s%8{umX³LO^w&[wԓ2ɼh"XLĢ>cǰf9.>^Faz\HWxL%1.NhC@([g0᭿-f)JTlq`ۆݙC8QQUA;~3 80NvܷGV//H:G)3o@\p>R"{]hS:G)D5]3֍uC s3&(~' 49DʼM:fIn pRR, pl`.eˮ%Z78be}4RY"L1tsvMeRA3p6Gq.F2f$X]Wd aYKQ!<*`(_`n{$l+$ HȻ*f ABҲ1P0L =_T ecd\mn4\vH&)@k"kb=Ɲ{z024jr1xF8(dmw)&l+^V&z%JxH C+lXMZYt Qq &P X^B-糭Cn"QA '/>eiZHlĂ?dX]y\ȱٍ؎qԽ FAy |F˔""mWmǍa[nۯ%C(QҳY3bԌ4EtЈ/FI1Jm٧2pdVIt?>jR(DۤDHB{~+)Jx_qDvOs^?C=WOHv 6U)T%yaV4= ,SϘneeS9?i a/#^lIMx^?OmQTNKxT)DHOLm`: jcsEw ]-_'vyо=6muaނǥ)U"˿e|;_QR'?~~ګ |&]yvR)JR<1f;\~ks[p?:X6-'aRNQUS)2UEE_BX8bԝr̜^]/bGVݸA{6!U-߲ "";N:|ltLdA0Dnf._Vf"=h߸Vc com1R  &kg9QRxE8H|/O?| [7NۯugaoD%[z"};ywM{n[W$Jb ǩJ[ETcq(~@6ƳV^qd h.(Q\tRC} >S+A*|psG(Y JvRiJL}I.%–1̜'کvD[VIѬ!XG;tj}əCaj<Ұ]]s敒Y?A]sC a@"8lc rk] <,AcZH% t̶d(GPrpï)JR)Ze?1V.I%*E+5 Nb&DHR1U1W q)Wpkl֟R៙wpܓ" J:?w2{@)}R6?z׻ E58wvM r,(hh Qut5QfLCrjglF/ mD::gYdDb&Ra Ҕ)JREi3d]}-|Kb jbSe0 !a&*Ø&{.:ڈvRTP:RcGEހ*eJR)JR)JR)JR)JR)JRpaperwork-2.1.1/paperwork-backend/tests/docexport/test_pdf_doc/000077500000000000000000000000001417573700700247455ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/docexport/test_pdf_doc/doc.pdf000066400000000000000000000230121417573700700262030ustar00rootroot00000000000000%PDF-1.5 %äüöß 2 0 obj <> stream xm 0EY 3I2`kpWw>vbQ3%K<%*&ߒ`Ցr}Ji|xD@ !Pέ&UX\|d=pWb}q/#AQ,J%a]V>DhHN )nH&;z7ND endstream endobj 3 0 obj 181 endobj 5 0 obj <> stream xzkxU=UzXl=,$+JQCI+~۴B,ْ`[BciZW] @~=DN3(y\$Db_/7+]!|S,eWɗ^!=r|I/!2AnA[%z L _Q'T1"Qay9</(܆2J L/7-+6r&AI<^ z 3?6KF9 __l)r辌cSv9B¿j@BMjodcگ}ΫvvZ[wM۷mݲq kUk׬.[_:fH_+j*Rɋi2юJ1ZUFD>ʋRwtP?*^\"tDsx s yN0[V ?/~üxWSX6JoFg"h# [-ªJ2[GP֟ہڶͳ )(őEcb*wVJ"-TiT%?&Ns35H!ElegضDsXoo#6Bڹ'O.AT ᣥєH$PdZD.W}=3L7gf dQمq{¢)2 Ѓ{:ŒkE,ȏFM~&Ϝ }[9aOrÑĆ8/y2zD$\b(9J^d+DԎ )$`~tPK|?GyQKcC8\U"gɹL1XZ\͎e92?S왑-))Mf] ǵ7ᔦzfV,];ezZZ)QPe7PXr " u:X] g/# Bisu fvjJ=,w%7p8F kHu]M~(j=ys:^PkkcX}puee*`pX,W;lf kf L 9 h#PaA:Kc@v>i9)@2J%`ۡ Wyٰ`no<ݩl$5':~Ugǁ|΁{[j7pVOc-;X}WYJR:ŘUp]XSX\-aYCeBaVo9m1hr@!)K:FKcmCjL lu6يqh+x=ۭ5o;xGiK}B1ld9*uK lF$h׉v:   [Luwy@ x |?W;G!+>;mv5nJS|[).!"ki HΚa66Y'N8NH:4W,DL7c2Z'fl'odMO^pO_e]y))pn)ggbJwRV;$,@YL"sŤNm6kt:FV;hTr O8'L;!ㄘTN^':u'䄼9'j3TC%B 7,%Y.4N|/)k)uSϿ)_m^f|ϽS<ƉlqaN'. X^Rr\NW(k5fKPXTnݐqC nnx /H\$#X)u5{ 7[UVQλu7pôn6MoIQ6ZV|Il0_ɁQ(4^AZvih~OV4+Yϗz-/|r k7y\H: Y5:^=<p2zazP1(1=+z!Aszp6Mz zHȷmReI^YqB)R$z ] C-h4"Vl iRj԰ЁCmmA,ZmzxPmUs61BV=6kbŸgJb\ǁ9xqp9 8XM.sOs|Gp&?Qp 69XŁ2⋵3Fa`q([T l<`/jiuP!GS y띱*5oS^1uQiCC]gg%XهVnʬՂ`4f,c5BaQk(r6@^q'M:\̹2FkVjUv<0UlvEխ1L\ ,km췑`:n <0`sM͂iO5=<Pyx じz=z򀋒a;X|)/Du76R٘? (!zN`_gpIqZhnbw=R](\ip'aɈQ31zL<;ɧA(8y3Wv+B)V(جt`wnɔN:{o]nx0o7Q7G[wfSdFX0JXHeѻ%fC1Vl$0KV`=p 4x?=mrAZ!^xx>Ni)JXh\?Q~̰38Oy`{r \?oǙlL<.|c==RmyAXe(#u>ݻ<`+ViZ8-bX,e^5WG6ӓɕ5dr"%]wc}΍G"pa`;"lJľClk>܏o>y*y'>DD&AlHX-f 5Ψ0c快Zӹ!stHЯMR+}Ŵ\˟přE=`J%=9ŨW#]͞{TBWŔ{Oq5b7[j=c(l(0qV֊#.8Lp@p,=o ʹX陕ݽ߹ƊL}㍫f?=Ȏ 5̍ƭd/ژ b58XMHlb̒4 7LEi$2GIddf~H_DDPjld4ï*kkxoIT43.lYVAL%srz`\{⩱=hjGz(>*~95TZjVT7\.gKQ> |bx!|*>2S{P4ߛSP<"s"3^?KƆ,rGO&~ _dds4}ect%ptlh?Mxzld2- Y3ь4x&56?1H Xf;Dtղ)at*?6L%PCx|;Ƣcc6MEc趱4:OF'ڦRd-F+htb,qO1G4@|Di<ÉˌV-|81ApԄ'ts&g\t(@Zrxflq>V$+ Z";J(gJKY6Hb_FL>(VJ23M>xj}OQu9HP!ď2&+r_L:H2 J&)+4n4W"Ͱ4Ϛ!eT+6r]I#emܥZ4s5h]/J4Hf[lr6a;CIS_V1 {Ehz-Ц/fu'0BJ"d7 >Aoinmd!~;"~ .^|6݅x9j#Үv%JOmB nWAķMi6IҏyTi8???g0ɱO?\,>}EくC`p₦o{#{{W,5s5sɹ]+鱟2y{l)0>} =y9vǽG>~5^|90w"sy]dpiv\2Ӌw.xߋ7{݋wv ؁}*#wNyNvcw0O8wIʽ d:ѧc4 .,[ dvowo{ҧz&MXmۍ#tb.>vU؝roG&h5 B<_pEg31#}ゑ14f5c=g {:łе"-HO{[$}{NGIS#pC@iLY4L)| DOX4qJS!6B!Bޟ&C"VBtZQGg endstream endobj 6 0 obj 7404 endobj 7 0 obj <> endobj 8 0 obj <> stream x]Mn0t DBH)I$Qi"cV陰ΕՍYtn d,t浢U B[/ Ce1σ͟M[ipGY[YDAQ fl}˟} b%[Q6 \cnQTz-0YlJ/}Rxg;=q!kh΁GOqBGd=D]+<ˈ}2OwΚƄPw|ؔ9ho; endstream endobj 9 0 obj <> endobj 10 0 obj <> endobj 11 0 obj <> endobj 1 0 obj <>/Contents 2 0 R>> endobj 4 0 obj <> endobj 12 0 obj <> endobj 13 0 obj < /Producer /CreationDate(D:20190902205629+02'00')>> endobj xref 0 14 0000000000 65535 f 0000008715 00000 n 0000000019 00000 n 0000000271 00000 n 0000008884 00000 n 0000000291 00000 n 0000007780 00000 n 0000007801 00000 n 0000007996 00000 n 0000008384 00000 n 0000008628 00000 n 0000008660 00000 n 0000008983 00000 n 0000009080 00000 n trailer < ] /DocChecksum /72B44A81E5EEF3D98178B73CE31BA672 >> startxref 9255 %%EOF paperwork-2.1.1/paperwork-backend/tests/docexport/tests_export.py000066400000000000000000000114421417573700700254270ustar00rootroot00000000000000import os import unittest import openpaperwork_core import openpaperwork_core.promise import paperwork_backend.docexport class TestExport(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_gtk.fs.gio") self.core.load("openpaperwork_core.fs.memory") self.core.load("paperwork_backend.docexport.img") self.core.load("paperwork_backend.docexport.pdf") self.core.load("paperwork_backend.docexport.pillowfight") self.core.init() self.test_doc_pdf_url = self.core.call_success( "fs_safe", "{}/test_pdf_doc".format( os.path.dirname(os.path.abspath(__file__)) ) ) self.test_doc_img_url = self.core.call_success( "fs_safe", "{}/test_img_doc".format( os.path.dirname(os.path.abspath(__file__)) ) ) self.result = None def set_result(self, result): self.result = list(result) def test_pdf_to_img(self): pipeline = [ self.core.call_success("export_get_pipe_by_name", "img_boxes"), self.core.call_success("export_get_pipe_by_name", "unpaper"), self.core.call_success("export_get_pipe_by_name", "swt_soft"), self.core.call_success("export_get_pipe_by_name", "png"), ] def origin(): return paperwork_backend.docexport.ExportData.build_page( # 1st page of the PDF "some_doc_id", self.test_doc_pdf_url, 0 ) promise = openpaperwork_core.promise.Promise(self.core, origin) for pipe in pipeline: promise = promise.then(pipe.get_promise(result='preview')) promise = promise.then(self.set_result) promise = promise.then(self.core.call_all, "mainloop_quit_graceful") promise = promise.schedule() self.core.call_one("mainloop") self.assertEqual(len(self.result), 1) self.assertTrue(self.result[0].startswith("memory://")) self.core.call_success("fs_unlink", self.result[0], trash=False) (tmp_file, fd) = self.core.call_success( "fs_mktemp", prefix="paperwork-test-", suffix=".png" ) fd.close() promise = openpaperwork_core.promise.Promise(self.core, origin) for pipe in pipeline: promise = promise.then( pipe.get_promise(result='final', target_file_url=tmp_file) ) promise = promise.then(self.set_result) promise = promise.then(self.core.call_all, "mainloop_quit_graceful") promise.schedule() self.core.call_one("mainloop") self.assertEqual(len(self.result), 1) self.assertTrue( self.core.call_success("fs_getsize", self.result[0]) > 0 ) self.core.call_success("fs_unlink", self.result[0], trash=False) def test_img_to_pdf(self): pipeline = [ self.core.call_success("export_get_pipe_by_name", "img_boxes"), self.core.call_success("export_get_pipe_by_name", "unpaper"), self.core.call_success("export_get_pipe_by_name", "swt_soft"), self.core.call_success("export_get_pipe_by_name", "generated_pdf"), ] def origin(): return paperwork_backend.docexport.ExportData.build_page( # 1st page of the image doc "some_doc_id", self.test_doc_img_url, 0 ) promise = openpaperwork_core.promise.Promise(self.core, origin) for pipe in pipeline: promise = promise.then(pipe.get_promise(result='preview')) promise = promise.then(self.set_result) promise = promise.then(self.core.call_all, "mainloop_quit_graceful") promise = promise.schedule() self.core.call_one("mainloop") # required by Poppler self.assertEqual(len(self.result), 1) self.assertTrue(self.result[0].startswith("file://")) self.core.call_success("fs_unlink", self.result[0], trash=False) (tmp_file, fd) = self.core.call_success( "fs_mktemp", prefix="paperwork-test-", suffix=".png" ) fd.close() promise = openpaperwork_core.promise.Promise(self.core, origin) for pipe in pipeline: promise = promise.then( pipe.get_promise(result='final', target_file_url=tmp_file) ) promise = promise.then(self.set_result) promise = promise.then(self.core.call_all, "mainloop_quit_graceful") promise.schedule() self.core.call_one("mainloop") self.assertEqual(len(self.result), 1) self.assertTrue(self.core.call_success( "fs_getsize", self.result[0] ) > 0) self.core.call_success("fs_unlink", self.result[0], trash=False) paperwork-2.1.1/paperwork-backend/tests/docimport/000077500000000000000000000000001417573700700223015ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/docimport/__init__.py000066400000000000000000000000001417573700700244000ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/docimport/pdfs/000077500000000000000000000000001417573700700232355ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/docimport/pdfs/test_doc.pdf000066400000000000000000000230121417573700700255320ustar00rootroot00000000000000%PDF-1.5 %äüöß 2 0 obj <> stream xm 0EY 3I2`kpWw>vbQ3%K<%*&ߒ`Ցr}Ji|xD@ !Pέ&UX\|d=pWb}q/#AQ,J%a]V>DhHN )nH&;z7ND endstream endobj 3 0 obj 181 endobj 5 0 obj <> stream xzkxU=UzXl=,$+JQCI+~۴B,ْ`[BciZW] @~=DN3(y\$Db_/7+]!|S,eWɗ^!=r|I/!2AnA[%z L _Q'T1"Qay9</(܆2J L/7-+6r&AI<^ z 3?6KF9 __l)r辌cSv9B¿j@BMjodcگ}ΫvvZ[wM۷mݲq kUk׬.[_:fH_+j*Rɋi2юJ1ZUFD>ʋRwtP?*^\"tDsx s yN0[V ?/~üxWSX6JoFg"h# [-ªJ2[GP֟ہڶͳ )(őEcb*wVJ"-TiT%?&Ns35H!ElegضDsXoo#6Bڹ'O.AT ᣥєH$PdZD.W}=3L7gf dQمq{¢)2 Ѓ{:ŒkE,ȏFM~&Ϝ }[9aOrÑĆ8/y2zD$\b(9J^d+DԎ )$`~tPK|?GyQKcC8\U"gɹL1XZ\͎e92?S왑-))Mf] ǵ7ᔦzfV,];ezZZ)QPe7PXr " u:X] g/# Bisu fvjJ=,w%7p8F kHu]M~(j=ys:^PkkcX}puee*`pX,W;lf kf L 9 h#PaA:Kc@v>i9)@2J%`ۡ Wyٰ`no<ݩl$5':~Ugǁ|΁{[j7pVOc-;X}WYJR:ŘUp]XSX\-aYCeBaVo9m1hr@!)K:FKcmCjL lu6يqh+x=ۭ5o;xGiK}B1ld9*uK lF$h׉v:   [Luwy@ x |?W;G!+>;mv5nJS|[).!"ki HΚa66Y'N8NH:4W,DL7c2Z'fl'odMO^pO_e]y))pn)ggbJwRV;$,@YL"sŤNm6kt:FV;hTr O8'L;!ㄘTN^':u'䄼9'j3TC%B 7,%Y.4N|/)k)uSϿ)_m^f|ϽS<ƉlqaN'. X^Rr\NW(k5fKPXTnݐqC nnx /H\$#X)u5{ 7[UVQλu7pôn6MoIQ6ZV|Il0_ɁQ(4^AZvih~OV4+Yϗz-/|r k7y\H: Y5:^=<p2zazP1(1=+z!Aszp6Mz zHȷmReI^YqB)R$z ] C-h4"Vl iRj԰ЁCmmA,ZmzxPmUs61BV=6kbŸgJb\ǁ9xqp9 8XM.sOs|Gp&?Qp 69XŁ2⋵3Fa`q([T l<`/jiuP!GS y띱*5oS^1uQiCC]gg%XهVnʬՂ`4f,c5BaQk(r6@^q'M:\̹2FkVjUv<0UlvEխ1L\ ,km췑`:n <0`sM͂iO5=<Pyx じz=z򀋒a;X|)/Du76R٘? (!zN`_gpIqZhnbw=R](\ip'aɈQ31zL<;ɧA(8y3Wv+B)V(جt`wnɔN:{o]nx0o7Q7G[wfSdFX0JXHeѻ%fC1Vl$0KV`=p 4x?=mrAZ!^xx>Ni)JXh\?Q~̰38Oy`{r \?oǙlL<.|c==RmyAXe(#u>ݻ<`+ViZ8-bX,e^5WG6ӓɕ5dr"%]wc}΍G"pa`;"lJľClk>܏o>y*y'>DD&AlHX-f 5Ψ0c快Zӹ!stHЯMR+}Ŵ\˟přE=`J%=9ŨW#]͞{TBWŔ{Oq5b7[j=c(l(0qV֊#.8Lp@p,=o ʹX陕ݽ߹ƊL}㍫f?=Ȏ 5̍ƭd/ژ b58XMHlb̒4 7LEi$2GIddf~H_DDPjld4ï*kkxoIT43.lYVAL%srz`\{⩱=hjGz(>*~95TZjVT7\.gKQ> |bx!|*>2S{P4ߛSP<"s"3^?KƆ,rGO&~ _dds4}ect%ptlh?Mxzld2- Y3ь4x&56?1H Xf;Dtղ)at*?6L%PCx|;Ƣcc6MEc趱4:OF'ڦRd-F+htb,qO1G4@|Di<ÉˌV-|81ApԄ'ts&g\t(@Zrxflq>V$+ Z";J(gJKY6Hb_FL>(VJ23M>xj}OQu9HP!ď2&+r_L:H2 J&)+4n4W"Ͱ4Ϛ!eT+6r]I#emܥZ4s5h]/J4Hf[lr6a;CIS_V1 {Ehz-Ц/fu'0BJ"d7 >Aoinmd!~;"~ .^|6݅x9j#Үv%JOmB nWAķMi6IҏyTi8???g0ɱO?\,>}EくC`p₦o{#{{W,5s5sɹ]+鱟2y{l)0>} =y9vǽG>~5^|90w"sy]dpiv\2Ӌw.xߋ7{݋wv ؁}*#wNyNvcw0O8wIʽ d:ѧc4 .,[ dvowo{ҧz&MXmۍ#tb.>vU؝roG&h5 B<_pEg31#}ゑ14f5c=g {:łе"-HO{[$}{NGIS#pC@iLY4L)| DOX4qJS!6B!Bޟ&C"VBtZQGg endstream endobj 6 0 obj 7404 endobj 7 0 obj <> endobj 8 0 obj <> stream x]Mn0t DBH)I$Qi"cV陰ΕՍYtn d,t浢U B[/ Ce1σ͟M[ipGY[YDAQ fl}˟} b%[Q6 \cnQTz-0YlJ/}Rxg;=q!kh΁GOqBGd=D]+<ˈ}2OwΚƄPw|ؔ9ho; endstream endobj 9 0 obj <> endobj 10 0 obj <> endobj 11 0 obj <> endobj 1 0 obj <>/Contents 2 0 R>> endobj 4 0 obj <> endobj 12 0 obj <> endobj 13 0 obj < /Producer /CreationDate(D:20190902205629+02'00')>> endobj xref 0 14 0000000000 65535 f 0000008715 00000 n 0000000019 00000 n 0000000271 00000 n 0000008884 00000 n 0000000291 00000 n 0000007780 00000 n 0000007801 00000 n 0000007996 00000 n 0000008384 00000 n 0000008628 00000 n 0000008660 00000 n 0000008983 00000 n 0000009080 00000 n trailer < ] /DocChecksum /72B44A81E5EEF3D98178B73CE31BA672 >> startxref 9255 %%EOF paperwork-2.1.1/paperwork-backend/tests/docimport/test.docx000066400000000000000000000100731417573700700241400ustar00rootroot00000000000000PKf0R _rels/.relsMKA Cl+"Bo"3iA PǼymNAêiAq0Ѻ0jx=/`/W>J\*ބaIL41q!fORWeS.IC iW)D ^dVNx!1_Ļ97'd,Ӧhp]5nTQ?PK]#PKf0RdocProps/core.xmlRN0T*`%'*!(q365Ďe-{I mggRud DEy# W(riFCЬȹ`KpQ0ҎrSbsIP@V0*YN Rll;LZV:D8+Q(vnIC.QcUq@e~hr ̃ n9Gef$NILKrM)[o eA8n=#pt /W6?v1՞f/W>x C#]R29i0*[+:¶kyFb/} }zPK aPKf0Rword/_rels/document.xml.relsM 0"ަUnDp+16 (z{Z(}1/__]m,IQҦp(%INR\ vDnyP-2$֡^R,}ÝT' O&Uʀ7m]k=nHA>.?|m ?@IwPK/0PKf0Rword/settings.xmlEK0 D"SBkRbG +73z+E"#f 0l7%>jn)Ȃ3ReW.)hf'.C.ܣHhέl #AW/?Lm#iiQOrTε m]/PKe"PKf0Rword/fontTable.xmlPAN0 wz[gXב$N+!ȡٙY?=6*9R i_:T}r(G'YWwˡl]jQʁ!y 1pHq h! 59JBCěw@['NE!Ug2 D^={'HfLGvkqkD<OWǐ[ ɮ`&}d[=O{>Ο:?xPKUPKf0Rword/document.xmlTێ0}W 'j%Q*R0ƀDzMXHUU`9g۷wR$'f,Ul) WM~6(P@n+d%A [2IJrjBVdu) S9c֠X F痦c.jiz yNl?韤KT{06@~Rp5d B_\?|nd7 }E9X{qzgϗ/-lak@'6I/V01hwùTv6W/gw|aHkR t- xm!YT_HiX8 R)9 t.Q꾓j-=}(6ހ [qڄ s,AiPKI zPKf0Rword/styles.xmlUmO0_{IAjJP7Z8s()}i|~:(Utcqx0dRcv28y(q,VHçJ!Eivǔ.:05rJtE4.ΤHK 'q RVRΐAjL.T]2} ]e\ςs_dDTin8^3^bk~L-G@cq%踼 : -.4! c134x|$!RM%ރq:͛AQoCr7\?$eƼr0ĸ:nc)3LXgTlʲ"C,h¶$ %vpIm}ejq]Py-~!;Ak>s ̾>4>OgwxhgkIUfʺ9Ǡ0ܣh(^<6{=ƢRۿfiM7-lUtˡ6XIU0ymfD9D>ؕl]H}eo\ζI'`L54Pe?+V<,Yx~beЋ/J6ekw뽦{_PK-e PKf0R[Content_Types].xmlT9O0+"(qa@%1B0#c$BUh ﴜ|Tm9$͍.Sy^d+ >] ҄`)|f,hT)2j%F! L[ؼ ׺'z/JYJi^.snev|#?ZzO@,#^-Cbn'$3S@c8O0|ýșcA-@) is۬}qbS4Gѝ)_702l-T(ZyQ\i=&I=|a_Fǻ_N>PKBS^PKf0R#= _rels/.relsPKf0R]#docProps/app.xmlPKf0R asdocProps/core.xmlPKf0R/0word/_rels/document.xml.relsPKf0Re""word/settings.xmlPKf0RUword/fontTable.xmlPKf0RI zcword/document.xmlPKf0R-e  word/styles.xmlPKf0RBS^U [Content_Types].xmlPK < paperwork-2.1.1/paperwork-backend/tests/docimport/test_img.png000066400000000000000000000107061417573700700246260ustar00rootroot00000000000000PNG  IHDRdÆ bKGD pHYs.#.#x?vtIME vtEXtCommentCreated with GIMPW.IDATx{LSgǿU,E`Vo+dTQScj:ܘHy&Bp@ P"rSN[V~۾@O=PI9ym H0 0,a0 aX að@0 a^D"t0H$TWWDF㴊wE`ʕH$Xa\P Y|TUU ^ƭ/3ӳG|tE! PTy D"8>غu+juXɓ@^^@&aܸq8GOFqq1+`ڴixwo؜WAAƏo/'0uVNJT*RL0AAA?/8bb鱐@UUU^J(%%<<#Vz=EDDc6ץ+9/׉' ڵkkNq@PLL jT[[K |Ҧ7''… h4Vھ}`^8x F!NGZ-eeeT*%tUn؊+ݾ}52ܹsT[[K? $JI:Tw[bat (jnn8M(''Ǧ7!!ٳg;]QF{>}О={:U Z<<<ϯPݟ8}nZiL&3φ!C7otZ]Q ///0;BTVVbɒ%4h5>>>3gtKyC|hq?3`{^ja޽{G [liwh"uhޙe Zvռ=*j^VVh}u8HsSj*#??EEEχLiӦO?˔ѧO|w4i1cr<*SPP#G ;;˖-ԩS'O… 6l J!k9Tw[bHؼaA:ð@0,a0 aX a0,:T[q{WP@" ##AX 8 =l'X ÇQUUNddffӧOǂ 0zhJII"a…x7-sÇHKKCrr2 \`(J̙31c`z{lm<}||0qDbĈul 55YYYCRR~mQqۦ] 8~8qx{{Cpp(D=i`RR]hRH& ?uTjZ?`޽{ ֖ե]N{mz)qۦ&"ŋ_.e=ڑ@P\\ݹsbcc ͝;믿7o^ڿ?ɓ'^?[%Kʕ+d0.\@X\mGHܽ{Wo>H[پKNúuP^^^K.O?m+˶ݻw>deet(//ݻq&CG|===i&ڵkq:<>y(]潻i&@eee.vV5@`e~"T*-Ҋe%=..NJ6#m,`"… ]~%ݥy4h~ )) d9s&@[W-uoT*&O# @p =H:4w^̙3s2 3g΄\.Rtg/r1 ׮]ȑ#!JqVqA#ܽ{PTtӧtR,T;<],"BII N8B៽0;Hg'z 0,a0 aX $,, e6Xi,^U/jaƂrZ| 0 JBRDuu5baSWW#Gbxys[rssm۶+qIdddd2ƍ???^FBtT*`޼yp8 %&&Z-vAt۷o1ؖիW>e@iii6+R^vwϵuwtħ7%%<<<8pnm۷h4R]]ϵ}JKK ^Z0_ܹs[v@ӔQ}}=FjTRRB۷oN鍉!ZMzjkkIPϵ-&̝;hCxSSyFHw@ǎ+`@-$ٳ]EVͺsrr)voވxPPtիyF۽azEW#WvȐ!7o\#>d29ba`֬Y,&M777ڵ/_Fnn.6olԖi^k.~VT*Ehh(֭[R7n_K:tqͪ5*b䟤-nbtt4rssqeT\.ﶊvaGѣG#$$6l{_/_ޭ[`;ÓٳDh4VrO/i;0az{dd$ `Y@^旡b)StkŝkzE3|zHbb"lق 455A qa_^[c; 8r_455* ;wVouO/ӅtJo|?`5g/hfѢET[[t޼<@ X ޼(//GRR233q=!$$'N03vZ@*ė_UV(**B~~>0eL6 e.Č3?[y `4Sr3=ϳ644رc8tKbUVVZT]vͮwl0c5j"##qeÿ ÿ a0,a0 aX ah dARIENDB`paperwork-2.1.1/paperwork-backend/tests/docimport/test_password.pdf000066400000000000000000000214351417573700700257020ustar00rootroot00000000000000%PDF-1.5 %äüöß 2 0 obj <> stream {tJCP$ N$eFY/JC$_4Jw.Ƀ%F4s$YpqYYnlj8ȦڥiGAYhs|_<BWߑݏѷ endstream endobj 3 0 obj 146 endobj 5 0 obj <> stream `fy INWNvON5eᡪ\& a$?r>dbRL!l0ccO!v7@P4;~}e"/s>TK\3 ރ,kW0G?m5\?帱fL5=!{UÎ^#b%{/r\zc5[/KuYW!8tcMb[dq[-K%aJL MS3Լa<%LmAt5 (qy|%.lo Xw*a!IO,]}I@ l#<"ķcf,',vSP>jjݒjcᘇkpv4f]o`C>_Ή6 ރB7 "ςّdtgStz&[;qre\\3̿b O! Z6 9@z9GtÀCLF&C<, .mz4`3Dz(+M,ߓG/}VU# ?oXXB4cqDqKCoSY8ݤZo'ƥZP!MJ,FI+%u1e!d߶{x.B7 e.KCA BD9K"9e1e0`H;kZOEn;J[)DVń* A.w$LfƮ<RYX5myW$,^/lo6^k91d!g1x@Zd6Y V&WEQ ' Hyeb7~=,Ryq]K1te*2_zQBtBWM"++螰#5`5h #[|ffӻ31/u+ĹL qO=+@$]UHp x;D)U?)wDX] l`IDW?T^{.vFGO>|S`>ot284y6,7N)埿x4?0u#)r[*vE=MixhL7ܕ&w6jZuBt/7!xXݓN@YRAӹo屮$Gm\B9Od0K6vAH_n$޼MulM<#~PAL Zbǚ2yyH{Dߵ{kuMۏ0y' }I8O~cY NX@Ct.Ռ=g ! LKJW5G/&s y3ߥGE !M00]UE0͸6=bԕc+P*B+@c>pG(bgM$AtR<.k?w]X}ȋuIeISm80x`\k8rKTih4S}B9Ӽ~so4P'i ?t`R-A 5͐\ hwf&Tݼjw=pp9X/rD*\yx&tٞ}rvmc3x ˳3cд[4q贖Eyf=DjK:yiO9>?eʯse+[n@I'llǓuǹ3LK=mh-4>'#붽b${uқ{Sz4UdT-YIՇE}M;m}`IxsSf7E޲ `]>(2[WGǗmdz½w_hI+QuGDRjBg,Ę ݀% ~w|5D8Q&"M9F eRU_8,^N*p=f6U.ǰ 5x9x2uR]7@#g { kUF v`ڬDQY|1<esب+866ݻPSWt߷wOJOE{^uijP[Fg/2goLF a`eQr^.k2݌tK:S^̼l.8{|y6P嵬b%}y#xO%O xZ 0}g 0Cok;-C]*oX 8eRWW>I v׼uO/o='-N '9 {.l?UVl6HW#&AS xW.l^rwؤhm!I 7ؗ8e*"zùEIjhjo LدL2H aW:dAɢ!gݏ~m[7y 10M]C/0-μ, Tm2 yq[s3u4~^=&.ĺm_.HP1ak2x7a7ӥuo瀰xfs{GhII,DJhxZ֕ͧj|;6;&RL>KMC?ALK'0CTՋo}tcҫ|5>ƭ-Qww缇(nҐ:Ԗ Sn.Zֆ߁y} մSʅ' 7:;h\4lLa>LH dJѰڝ0ށiE|]6b֠p N%[`h?~voN`Q"݊w&IcKaJ)e ۷nsY%0\/`rG,sDW8}=灠f@Z+G٢x66 G݌ <k\Su!PO9` J8lSgźũR[WAG>pτjxѸx N3){4dqFD{+'(OެmkM.(UX8ޚ]jPJHmDѣS]SB=ݳ΃8.'y_p9=xG1Pm;;EXwfp5MC&"k p~n)=Q^| EJD'Qv1 Qh'Z+ʢ+ yt OxqZRtݒکQU, 4KC(k|~bx{jC[^wPp2??yA>%D3o){3Z}J2VB*lqsˢg6d,ޟBn(O kT6{Lu0jaBQQKْÌV9 x!Dˆ*V&(gOԶ舮ά*"޹!'w`>.E7قEU1%3>d _Y~&+h sᱭ|KZiCU>1ޤӚĮPLҺ]B̌KH6b۔WI:{ga} ~m3R5.Ă7,M|9%4oO0:AB:˶U?Rd=}PW d7癭}N{~}Tۈj%ƙǭY}U_ΝD"~J4RjJιo8q%`,ڐ(2D{k0 fy۷xZ8Ev!ֵL B8>n/~\O4_ydD[pXM}ug+\;Wr:rZ??%{D֩X!/OHv0g?EHU]_Kv1BhN_7Y!?? g[GԚl=nfHÍҀ# D!Dž!xZf8o5QLE!BY|E+k-#GueQ,O^GjZ-2+1>IH=Y03؁" 1?Alb=8%e\Zz}a0w$ c46Q)NOp5dzRgr]e<5U'sA|=ØOA$5&N$ܮXwϤY|ţSX ͳoyw&VjXm,8i|-Q4S 9ua8(5K Zˋ_4jbF8Il9z^6BX;9B@-p(i@#P*R@Jy?7HN,,e쳭o=/ U"C]FM݁q(ߵƒ@L nBkAyrdj9< ׏ˑ\N{++\g ӀeXZx'n4_$2&`"1V9)}qWQ8Ɛ^xy Uמ$@T5y)6"wPGkƻݦ>KP8Q&j&7ÌWzw`l5ء)C'%ZԽRp|x1-aFfN⍚PAF<kv.c"Nϭ7MnVXN S0IVZ `o*xRLzL$s-\UpG:V?;77&X7PGxK`Wdw{XY?@W?~__h72Ѥ\?oIPdwrl0TgҐ IT́A1/ l!)46 Bq4rw2 ߏm)6{-VOpu$ghaMJJ zi*'6 .Y9Stit>«m%x"<? endstream endobj 6 0 obj 6557 endobj 7 0 obj <> endobj 8 0 obj <> stream ]­UZ OiѼ6z O]"+E)h RUQ&QJcG$E6yr/(Ja2=SÒ[-X(d,k"̇/ejpB!y9}kHCd:eN 96۪@5|o.E 1%" -p^C5#c> endobj 10 0 obj <> endobj 11 0 obj <> endobj 1 0 obj <>/Contents 2 0 R>> endobj 4 0 obj <> endobj 12 0 obj <> endobj 13 0 obj < /Producer<0F4AD7BD3225496D4174E8D76D12897CC877CEED24DC9DB568A5637A671CE5E2> /CreationDate(}y:s5ڀ\\d"!)>> endobj 14 0 obj <> endobj xref 0 15 0000000000 65535 f 0000007789 00000 n 0000000019 00000 n 0000000236 00000 n 0000007958 00000 n 0000000256 00000 n 0000006898 00000 n 0000006919 00000 n 0000007114 00000 n 0000007478 00000 n 0000007702 00000 n 0000007734 00000 n 0000008057 00000 n 0000008154 00000 n 0000008330 00000 n trailer < <4CC2DA594C5DCB199E0D108A41E1AA2B> ] /DocChecksum /4234D31F6DCF9FDC719B74544277637C >> startxref 8470 %%EOF paperwork-2.1.1/paperwork-backend/tests/docimport/tests_converted.py000066400000000000000000000140531417573700700260710ustar00rootroot00000000000000import os import tempfile import unittest import paperwork_backend.docimport import paperwork_backend.sync import openpaperwork_core class TestConvertedImport(unittest.TestCase): def setUp(self): self.test_doc = os.path.join( os.path.dirname(os.path.abspath(__file__)), "test.docx" ) self.add_docs = set() self.upd_docs = set() self.nb_commits = 0 class TestTransaction(paperwork_backend.sync.BaseTransaction): priority = 0 def add_doc(s, doc_id): super().add_doc(doc_id) self.add_docs.add(doc_id) def upd_doc(s, doc_id): super().upd_doc(doc_id) self.upd_docs.add(doc_id) def commit(s): super().commit() self.nb_commits += 1 class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 999999999999999999 def doc_transaction_start(s, transactions, expected=-1): transactions.append(TestTransaction( self.core, expected )) def index_get_doc_id_by_hash(s, h): all_docs = [] self.core.call_success("storage_get_all_docs", all_docs) for (doc_id, doc_url) in all_docs: doc_h = self.core.call_success( "doc_get_hash_by_url", doc_url ) if doc_h == h: return doc_id return None self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core._load_module("fake_module", FakeModule()) self.core.load("openpaperwork_core.config.fake") self.core.load("paperwork_backend.docimport.converted") self.core.init() self.config = self.core.get_by_name("openpaperwork_core.config.fake") def test_import_docx(self): with tempfile.TemporaryDirectory() as tmp_dir: self.config.settings = { "workdir": self.core.call_success("fs_safe", tmp_dir), } file_to_import = self.core.call_success("fs_safe", self.test_doc) file_import = paperwork_backend.docimport.FileImport( file_uris_to_import=[file_to_import] ) importers = [] self.core.call_all("get_importer", importers, file_import) self.assertEqual(len(importers), 1) promise = importers[0].get_import_promise() promise.schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertEqual(len(self.add_docs), 1) self.assertEqual(self.nb_commits, 1) self.assertEqual(file_import.ignored_files, []) self.assertEqual(file_import.imported_files, {file_to_import}) self.assertEqual(len(file_import.new_doc_ids), 1) self.assertEqual(file_import.upd_doc_ids, set()) self.assertEqual(file_import.stats['Microsoft Word (.docx)'], 1) self.assertEqual(file_import.stats['Documents'], 1) self.assertIsNotNone( self.core.call_success( "fs_join", self.core.call_success( "doc_id_to_url", list(file_import.new_doc_ids)[0] ), "doc.pdf" ) ) def test_import_duplicated_docx(self): with tempfile.TemporaryDirectory() as tmp_dir: self.config.settings = { "workdir": self.core.call_success("fs_safe", tmp_dir), } file_to_import = self.core.call_success("fs_safe", self.test_doc) # 1st import file_import = paperwork_backend.docimport.FileImport( file_uris_to_import=[file_to_import] ) importers = [] self.core.call_all("get_importer", importers, file_import) self.assertEqual(len(importers), 1) promise = importers[0].get_import_promise() promise.schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertEqual(len(self.add_docs), 1) self.assertEqual(self.nb_commits, 1) self.assertEqual(file_import.ignored_files, []) self.assertEqual(file_import.imported_files, {file_to_import}) self.assertEqual(len(file_import.new_doc_ids), 1) self.assertEqual(file_import.upd_doc_ids, set()) self.assertEqual(file_import.stats['Microsoft Word (.docx)'], 1) self.assertEqual(file_import.stats['Documents'], 1) self.assertIsNotNone( self.core.call_success( "fs_join", self.core.call_success( "doc_id_to_url", list(file_import.new_doc_ids)[0] ), "doc.pdf" ) ) # 2nd import file_import = paperwork_backend.docimport.FileImport( file_uris_to_import=[file_to_import] ) importers = [] self.core.call_all("get_importer", importers, file_import) self.assertEqual(len(importers), 1) promise = importers[0].get_import_promise() promise.schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertEqual(len(self.add_docs), 1) self.assertEqual(self.nb_commits, 2) self.assertEqual(file_import.ignored_files, [file_to_import]) self.assertEqual(file_import.imported_files, set()) self.assertEqual(file_import.new_doc_ids, set()) self.assertEqual(file_import.upd_doc_ids, set()) self.assertNotIn('Microsoft Word (.docx)', file_import.stats) self.assertEqual(file_import.stats['Already imported'], 1) paperwork-2.1.1/paperwork-backend/tests/docimport/tests_img.py000066400000000000000000000133431417573700700246550ustar00rootroot00000000000000import os import unittest import openpaperwork_core import openpaperwork_core.fs import paperwork_backend.docimport class TestImgImport(unittest.TestCase): def setUp(self): self.test_img_url = openpaperwork_core.fs.CommonFsPluginBase.fs_safe( os.path.join( os.path.dirname(os.path.abspath(__file__)), "test_img.png" ) ) self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.pillowed = [] self.add_docs = [] self.upd_docs = [] self.nb_commits = 0 class FakeTransaction(object): priority = 0 def add_doc(s, doc_id): self.add_docs.append(doc_id) def del_doc(s, doc_id): pass def upd_doc(s, doc_id): self.upd_docs.append(doc_id) def unchanged_doc(s, doc_id): pass def commit(s): self.nb_commits += 1 class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 999999999999999999 def fs_isdir(s, dir_uri): return not dir_uri.lower().endswith(".png") def fs_get_mime(s, file_uri): if s.fs_isdir(file_uri): return "inode/directory" return "image/png" def fs_mkdir_p(s, dir_uri): return True def url_to_pillow(s, file_uri): return "non-null value" def pillow_to_url(s, img, dst_uri): self.pillowed.append(dst_uri) return dst_uri def on_import_done(s, file_import): self.core.call_all("mainloop_quit_graceful") def doc_transaction_start(s, transactions, expected=-1): transactions.append(FakeTransaction()) self.core._load_module("fake_module", FakeModule()) self.core.load("paperwork_backend.model.fake") self.core.load("paperwork_backend.docimport.img") self.fake_storage = self.core.get_by_name( "paperwork_backend.model.fake" ) self.core.init() def test_import_new_doc(self): self.fake_storage.docs = [ { 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': 'Simplebayes and Flesch are\nthe best', 'labels': {("some_label", "#123412341234")}, }, { 'id': 'test_doc_2', 'url': 'file:///somewhere/test_doc_2', 'mtime': 123, 'text': 'Flesch Simplebayes\nbest', 'labels': {("some_label", "#123412341234")}, }, ] file_import = paperwork_backend.docimport.FileImport( file_uris_to_import=[self.test_img_url] ) importers = [] self.core.call_all("get_importer", importers, file_import) self.assertEqual(len(importers), 1) promise = importers[0].get_import_promise() promise.schedule() self.core.call_all("mainloop") # see fake storage behaviour self.assertEqual(self.pillowed, [ 'file:///some_doc/new_page.jpeg' ]) self.assertEqual(self.add_docs, ['1']) self.assertEqual(self.upd_docs, []) self.assertEqual(self.nb_commits, 1) self.assertEqual(file_import.ignored_files, []) self.assertEqual(file_import.imported_files, {self.test_img_url}) self.assertEqual(file_import.new_doc_ids, {'1'}) self.assertEqual(file_import.upd_doc_ids, set()) self.assertEqual(file_import.stats['Images'], 1) self.assertEqual(file_import.stats['Documents'], 1) self.assertEqual(file_import.stats['Pages'], 0) def test_import_upd_doc(self): self.fake_storage.docs = [ { 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': 'Simplebayes and Flesch are\nthe best', 'labels': {("some_label", "#123412341234")}, }, { 'id': 'test_doc_2', 'url': 'file:///somewhere/test_doc_2', 'mtime': 123, 'text': 'Flesch Simplebayes\nbest', 'labels': {("some_label", "#123412341234")}, 'page_boxes': [ 'some_content_for_page_1', 'some_content_for_page_2', ] }, ] file_import = paperwork_backend.docimport.FileImport( file_uris_to_import=[self.test_img_url], active_doc_id='test_doc_2' ) importers = [] self.core.call_all("get_importer", importers, file_import) self.assertEqual(len(importers), 1) promise = importers[0].get_import_promise() promise.schedule() self.core.call_all("mainloop") # see fake storage behaviour self.assertEqual(self.pillowed, [ 'file:///some_doc/new_page.jpeg' ]) self.assertEqual(self.add_docs, []) self.assertEqual(self.upd_docs, ['test_doc_2']) self.assertEqual(self.nb_commits, 1) self.assertEqual(file_import.ignored_files, []) self.assertEqual(file_import.imported_files, {self.test_img_url}) self.assertEqual(file_import.new_doc_ids, set()) self.assertEqual(file_import.upd_doc_ids, {'test_doc_2'}) self.assertEqual(file_import.stats['Images'], 1) self.assertEqual(file_import.stats['Documents'], 0) self.assertEqual(file_import.stats['Pages'], 1) paperwork-2.1.1/paperwork-backend/tests/docimport/tests_pdf.py000066400000000000000000000400041417573700700246440ustar00rootroot00000000000000import os import unittest import unittest.mock import openpaperwork_core import openpaperwork_core.fs import paperwork_backend.docimport class TestPdfImport(unittest.TestCase): def setUp(self): self.test_doc_url = openpaperwork_core.fs.CommonFsPluginBase.fs_safe( os.path.join( os.path.dirname(os.path.abspath(__file__)), "pdfs", "test_doc.pdf" ) ) self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.copies = [] self.add_docs = [] self.nb_commits = 0 self.hash_to_docid = {} class FakeTransaction(object): priority = 0 def add_doc(s, doc_id): self.add_docs.append(doc_id) def unchanged_doc(s, doc_id): pass def commit(s): self.nb_commits += 1 class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 999999999999999999 def fs_isdir(s, dir_uri): return not dir_uri.lower().endswith(".pdf") def fs_get_mime(s, file_uri): if s.fs_isdir(file_uri): return "inode/directory" return "application/pdf" def fs_mkdir_p(s, dir_uri): return True def fs_copy(s, src_uri, dst_uri): self.copies.append((src_uri, dst_uri)) return dst_uri def on_import_done(s, file_import): self.core.call_all("mainloop_quit_graceful") def doc_transaction_start(s, transactions, expected=-1): transactions.append(FakeTransaction()) def fs_hash(s, file_url): if file_url == self.test_doc_url: return "DEADBEEF" return "ABCDEF" def index_get_doc_id_by_hash(s, h): if h in self.hash_to_docid: return self.hash_to_docid[h] return None def poppler_open(s, file_url, password=None): self.assertIsNone(password) return "something" self.core._load_module("fake_module", FakeModule()) self.core.load("paperwork_backend.model.fake") self.core.load("paperwork_backend.docimport.pdf") self.fake_storage = self.core.get_by_name( "paperwork_backend.model.fake" ) self.core.init() def test_import_pdf(self): self.hash_to_docid = {} self.fake_storage.docs = [ { 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': 'Simplebayes and Flesch are\nthe best', 'labels': {("some_label", "#123412341234")}, }, { 'id': 'test_doc_2', 'url': 'file:///somewhere/test_doc_2', 'mtime': 123, 'text': 'Flesch Simplebayes\nbest', 'labels': {("some_label", "#123412341234")}, }, ] file_import = paperwork_backend.docimport.FileImport( file_uris_to_import=[self.test_doc_url] ) importers = [] self.core.call_all("get_importer", importers, file_import) self.assertEqual(len(importers), 1) self.assertEqual(len(importers[0].get_required_data()), 0) promise = importers[0].get_import_promise() promise.schedule() self.core.call_all("mainloop") # see fake storage behaviour self.assertEqual(self.copies, [ (self.test_doc_url, 'file:///some_work_dir/1/doc.pdf') ]) self.assertEqual(self.add_docs, ['1']) self.assertEqual(self.nb_commits, 1) self.assertEqual(file_import.ignored_files, []) self.assertEqual(file_import.imported_files, {self.test_doc_url}) self.assertEqual(file_import.new_doc_ids, {'1'}) self.assertEqual(file_import.upd_doc_ids, set()) self.assertEqual(file_import.stats['PDF'], 1) self.assertEqual(file_import.stats['Documents'], 1) def test_duplicated_pdf(self): self.hash_to_docid = {"DEADBEEF": "some_doc_id"} self.fake_storage.docs = [ { 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': 'Simplebayes and Flesch are\nthe best', 'labels': {("some_label", "#123412341234")}, }, { 'id': 'test_doc_2', 'url': 'file:///somewhere/test_doc_2', 'mtime': 123, 'text': 'Flesch Simplebayes\nbest', 'labels': {("some_label", "#123412341234")}, }, ] file_import = paperwork_backend.docimport.FileImport( file_uris_to_import=[self.test_doc_url] ) importers = [] self.core.call_all("get_importer", importers, file_import) self.assertEqual(len(importers), 1) promise = importers[0].get_import_promise() promise.schedule() self.core.call_all("mainloop") self.assertEqual(self.copies, []) self.assertEqual(self.add_docs, []) self.assertEqual(self.nb_commits, 1) self.assertEqual(file_import.ignored_files, [self.test_doc_url]) self.assertEqual(file_import.imported_files, set()) self.assertEqual(file_import.new_doc_ids, set()) self.assertEqual(file_import.upd_doc_ids, set()) self.assertNotIn('PDF', file_import.stats) self.assertEqual(file_import.stats['Already imported'], 1) class TestRecursivePdfImport(unittest.TestCase): def setUp(self): self.test_doc_url = openpaperwork_core.fs.CommonFsPluginBase.fs_safe( os.path.join( os.path.dirname(os.path.abspath(__file__)), "pdfs", "test_doc.pdf" ) ) self.test_dir_url = openpaperwork_core.fs.CommonFsPluginBase.fs_safe( os.path.join( os.path.dirname(os.path.abspath(__file__)), "pdfs", ) ) self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.copies = [] self.add_docs = [] self.nb_commits = 0 self.hash_to_docid = {} class FakeTransaction(object): priority = 0 def add_doc(s, doc_id): self.add_docs.append(doc_id) def unchanged_doc(s, doc_id): pass def commit(s): self.nb_commits += 1 class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 999999999999999999 def fs_isdir(s, dir_uri): return not dir_uri.lower().endswith(".pdf") def fs_get_mime(s, file_uri): if s.fs_isdir(file_uri): return "inode/directory" return "application/pdf" def fs_mkdir_p(self, dir_uri): return True def fs_copy(s, src_uri, dst_uri): self.copies.append((src_uri, dst_uri)) return dst_uri def on_import_done(s, file_import): self.core.call_all("mainloop_quit_graceful") def doc_transaction_start(s, transactions, expected=-1): transactions.append(FakeTransaction()) def fs_hash(s, file_url): if file_url == self.test_doc_url: return "DEADBEEF" return "ABCDEF" def index_get_doc_id_by_hash(s, h): if h in self.hash_to_docid: return self.hash_to_docid[h] return None def poppler_open(s, file_url, password=None): self.assertIsNone(password) return "something" self.core._load_module("fake_module", FakeModule()) self.core.load("paperwork_backend.model.fake") self.core.load("paperwork_backend.docimport.pdf") self.fake_storage = self.core.get_by_name( "paperwork_backend.model.fake" ) self.core.init() def test_import_pdf(self): self.hash_to_docid = {} self.fake_storage.docs = [ { 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': 'Simplebayes and Flesch are\nthe best', 'labels': {("some_label", "#123412341234")}, }, { 'id': 'test_doc_2', 'url': 'file:///somewhere/test_doc_2', 'mtime': 123, 'text': 'Flesch Simplebayes\nbest', 'labels': {("some_label", "#123412341234")}, }, ] file_import = paperwork_backend.docimport.FileImport( file_uris_to_import=[self.test_dir_url] ) importers = [] self.core.call_all("get_importer", importers, file_import) self.assertEqual(len(importers), 1) promise = importers[0].get_import_promise() promise.schedule() self.core.call_all("mainloop") # see fake storage behaviour self.assertEqual(self.copies, [ (self.test_doc_url, 'file:///some_work_dir/1/doc.pdf') ]) self.assertEqual(self.add_docs, ['1']) self.assertEqual(self.nb_commits, 1) self.assertEqual(file_import.ignored_files, []) self.assertEqual(file_import.imported_files, {self.test_doc_url}) self.assertEqual(file_import.new_doc_ids, {'1'}) self.assertEqual(file_import.upd_doc_ids, set()) self.assertEqual(file_import.stats['PDF'], 1) self.assertEqual(file_import.stats['Documents'], 1) def test_duplicated_pdf(self): self.hash_to_docid = {"DEADBEEF": "some_doc_id"} self.fake_storage.docs = [ { 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': 'Simplebayes and Flesch are\nthe best', 'labels': {("some_label", "#123412341234")}, }, { 'id': 'test_doc_2', 'url': 'file:///somewhere/test_doc_2', 'mtime': 123, 'text': 'Flesch Simplebayes\nbest', 'labels': {("some_label", "#123412341234")}, }, ] file_import = paperwork_backend.docimport.FileImport( file_uris_to_import=[self.test_dir_url] ) importers = [] self.core.call_all("get_importer", importers, file_import) self.assertEqual(len(importers), 1) promise = importers[0].get_import_promise() promise.schedule() self.core.call_all("mainloop") self.assertEqual(self.copies, []) self.assertEqual(self.add_docs, []) self.assertEqual(self.nb_commits, 1) self.assertEqual(file_import.ignored_files, [self.test_doc_url]) self.assertEqual(file_import.imported_files, set()) self.assertEqual(file_import.new_doc_ids, set()) self.assertEqual(file_import.upd_doc_ids, set()) self.assertNotIn('PDF', file_import.stats) class TestPasswordPdfImport(unittest.TestCase): def setUp(self): self.test_doc_url = openpaperwork_core.fs.CommonFsPluginBase.fs_safe( os.path.join( os.path.dirname(os.path.abspath(__file__)), "pdfs", "test_password.pdf" ) ) self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.copies = [] self.add_docs = [] self.nb_commits = 0 self.hash_to_docid = {} class FakeTransaction(object): priority = 0 def add_doc(s, doc_id): self.add_docs.append(doc_id) def unchanged_doc(s, doc_id): pass def commit(s): self.nb_commits += 1 class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 999999999999999999 def fs_isdir(s, dir_uri): return not dir_uri.lower().endswith(".pdf") def fs_get_mime(s, file_uri): if s.fs_isdir(file_uri): return "inode/directory" return "application/pdf" def fs_mkdir_p(s, dir_uri): return True def fs_copy(s, src_uri, dst_uri): self.copies.append((src_uri, dst_uri)) return dst_uri def on_import_done(s, file_import): self.core.call_all("mainloop_quit_graceful") def doc_transaction_start(s, transactions, expected=-1): transactions.append(FakeTransaction()) def fs_hash(s, file_url): if file_url == self.test_doc_url: return "DEADBEEF" return "ABCDEF" def index_get_doc_id_by_hash(s, h): if h in self.hash_to_docid: return self.hash_to_docid[h] return None def poppler_open(s, file_url, password=None): self.assertIsNotNone(password) return "something" def fs_open(s, file_url, mode): class FsMock(object): def __enter__(se): return se def __exit__(se, *args, **kwargs): return se def write(se, b): pass return FsMock() self.core._load_module("fake_module", FakeModule()) self.core.load("paperwork_backend.model.fake") self.core.load("paperwork_backend.docimport.pdf") self.fake_storage = self.core.get_by_name( "paperwork_backend.model.fake" ) self.core.init() def test_import_password_pdf(self): self.hash_to_docid = {} self.fake_storage.docs = [ { 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': 'Simplebayes and Flesch are\nthe best', 'labels': {("some_label", "#123412341234")}, }, { 'id': 'test_doc_2', 'url': 'file:///somewhere/test_doc_2', 'mtime': 123, 'text': 'Flesch Simplebayes\nbest', 'labels': {("some_label", "#123412341234")}, }, ] file_import = paperwork_backend.docimport.FileImport( file_uris_to_import=[self.test_doc_url] ) importers = [] self.core.call_all("get_importer", importers, file_import) self.assertEqual(len(importers), 1) self.assertEqual(importers[0].get_required_data(), { self.test_doc_url: {"password"} }) promise = importers[0].get_import_promise({ "password": "test1234", }) promise.schedule() self.core.call_all("mainloop") # see fake storage behaviour self.assertEqual(self.copies, [ (self.test_doc_url, 'file:///some_work_dir/1/doc.pdf') ]) self.assertEqual(self.add_docs, ['1']) self.assertEqual(self.nb_commits, 1) self.assertEqual(file_import.ignored_files, []) self.assertEqual(file_import.imported_files, {self.test_doc_url}) self.assertEqual(file_import.new_doc_ids, {'1'}) self.assertEqual(file_import.upd_doc_ids, set()) self.assertEqual(file_import.stats['PDF'], 1) self.assertEqual(file_import.stats['Documents'], 1) paperwork-2.1.1/paperwork-backend/tests/docscan/000077500000000000000000000000001417573700700217135ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/docscan/__init__.py000066400000000000000000000000001417573700700240120ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/docscan/tests_libinsane.py000066400000000000000000000152061417573700700254570ustar00rootroot00000000000000import os import platform import unittest import gi gi.require_version('Libinsane', '1.0') from gi.repository import Libinsane # noqa: E402 import openpaperwork_core # noqa: E402 import paperwork_backend.docscan.libinsane # noqa: E402 class TestImageAssembler(unittest.TestCase): def test_assembler(self): assembler = paperwork_backend.docscan.libinsane.ImageAssembler( line_width=5 ) assembler.MIN_CHUNK_SIZE = 12 self.assertIsNone(assembler.get_last_chunk()) assembler.add_piece(b"abc") self.assertIsNone(assembler.get_last_chunk()) assembler.add_piece(b"def") self.assertIsNone(assembler.get_last_chunk()) assembler.add_piece(b"hij") self.assertIsNone(assembler.get_last_chunk()) assembler.add_piece(b"klmn") self.assertEqual(assembler.get_last_chunk(), b"abcdefhijk") assembler.add_piece(b"abc") self.assertEqual(assembler.get_last_chunk(), b"abcdefhijk") assembler.add_piece(b"def") self.assertEqual(assembler.get_last_chunk(), b"abcdefhijk") assembler.add_piece(b"hij") self.assertEqual(assembler.get_last_chunk(), b"lmnabcdefh") self.assertEqual( assembler.get_image(), b"abcdefhijklmnabcdefhij" ) class TestLibinsane(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.config.fake") self.core.load("openpaperwork_core.thread.simple") self.core.load("openpaperwork_core.work_queue.default") self.core.load("paperwork_backend.docscan.libinsane") self.called = False self.results = [] # drop warnings logs from Libinsane because they pollute tests output plugin = self.core.get_by_name("paperwork_backend.docscan.libinsane") plugin.libinsane_logger.min_level = Libinsane.LogLevel.ERROR self.config = self.core.get_by_name("openpaperwork_core.config.fake") def test_list_devs(self): self.core.init() def get_devs(devs): # not much else we can test: depends on the system on which we are self.called = True promise = self.core.call_success("scan_list_scanners_promise") promise = promise.then(get_devs) promise.schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_all("mainloop") self.assertTrue(self.called) # We need Sane test backend for this test @unittest.skipUnless(os.name == 'posix', reason="Linux only") # Test is broken on ARM 32bits for some reason @unittest.skip(platform.machine() == "aarch64" and platform.architecture()[0] == "32bit") @unittest.skip(platform.machine() == "armhf") def test_scan(self): TEST_DEV_ID = "libinsane:sane:test:0" class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): def on_scan_feed_start(s, scan_id): self.results.append("on_scan_feed_start") def on_scan_page_start(s, *args, **kwargs): self.results.append("on_scan_page_start") def on_scan_chunk(s, *args, **kwargs): self.results.append("on_scan_chunk") def on_scan_page_end(s, *args, **kwargs): self.results.append("on_scan_page_end") def on_scan_feed_end(s, scan_id): self.results.append("on_scan_feed_end") self.core._load_module("fake_module", FakeModule()) self.core.init() def scan(sources): source = sources['flatbed'] (scan_id, promise) = source.scan_promise(resolution=150) promise = promise.then( # roll out the image generator lambda args: list(args[2]) ) promise = promise.then(source.close) self.core.call_success("scan_schedule", promise) self.called = True def get_sources_and_scan(scanner): promise = scanner.get_sources_promise() promise = promise.then(scan) self.core.call_success("scan_schedule", promise) promise = self.core.call_success( "scan_get_scanner_promise", TEST_DEV_ID ) promise = promise.then(get_sources_and_scan) promise.schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_all("mainloop") self.assertTrue(self.called) self.assertIn("on_scan_feed_start", self.results) self.assertIn("on_scan_page_start", self.results) self.assertIn("on_scan_chunk", self.results) self.assertIn("on_scan_page_end", self.results) self.assertIn("on_scan_feed_end", self.results) # We need Sane test backend for this test @unittest.skipUnless(os.name == 'posix', reason="Linux only") # Test is broken on ARM 32bits for some reason @unittest.skip(platform.machine() == "aarch64" and platform.architecture()[0] == "32bit") @unittest.skip(platform.machine() == "armhf") def test_scan_default(self): TEST_DEV_ID = "libinsane:sane:test:0" self.config.settings = { "scanner_dev_id": TEST_DEV_ID, "scanner_resolution": 150, } class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): def on_scan_feed_start(s, scan_id): self.results.append("on_scan_feed_start") def on_scan_page_start(s, *args, **kwargs): self.results.append("on_scan_page_start") def on_scan_chunk(s, *args, **kwargs): self.results.append("on_scan_chunk") def on_scan_page_end(s, *args, **kwargs): self.results.append("on_scan_page_end") def on_scan_feed_end(s, scan_id): self.results.append("on_scan_feed_end") self.core._load_module("fake_module", FakeModule()) self.core.init() (scan_id, promise) = self.core.call_success( "scan_promise", source_id="flatbed" ) promise = promise.then( # roll out the image generator lambda args: list(args[2]) ) self.core.call_success("scan_schedule", promise) self.core.call_all("mainloop_quit_graceful") self.core.call_all("mainloop") self.assertIn("on_scan_feed_start", self.results) self.assertIn("on_scan_page_start", self.results) self.assertIn("on_scan_chunk", self.results) self.assertIn("on_scan_page_end", self.results) self.assertIn("on_scan_feed_end", self.results) paperwork-2.1.1/paperwork-backend/tests/docscan/tests_scan2doc.py000066400000000000000000000075561417573700700252200ustar00rootroot00000000000000import unittest import openpaperwork_core class TestScan2Doc(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.fs.fake") self.core.load("paperwork_backend.docscan.fake") self.core.load("paperwork_backend.docscan.scan2doc") self.fs = self.core.get_by_name("openpaperwork_core.fs.fake") self.results = [] self.pillowed = [] self.transaction_type = None self.nb_commits = 0 class FakeTransaction(object): priority = 0 def add_doc(s, doc_id): self.assertIsNone(self.transaction_type) self.transaction_type = "add" def upd_doc(s, doc_id): self.assertIsNone(self.transaction_type) self.transaction_type = "upd" def del_doc(s, doc_id): pass def unchanged_doc(s, doc_id): pass def commit(s): self.nb_commits += 1 class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 10000000000 def on_scan_feed_start(s, scan_id): doc_id = self.core.call_success( "scan2doc_scan_id_to_doc_id", scan_id ) self.assertIsNotNone(doc_id) def doc_transaction_start(self, transactions, nb_expected=-1): transactions.append(FakeTransaction()) def fs_exists(self, file_url): if "paper.10" in file_url: return None if "existing" in file_url: return True return None def fs_isdir(self, file_url): if "." in file_url: return None return True def fs_listdir(self, file_url): if "exist" not in file_url: return None r = [ (file_url + "/paper.{}.jpg".format(x)) for x in range(1, 10) ] return r def doc_id_to_url(s, doc_id, existing=True): return 'file:///some_existing_doc' def storage_get_new_doc(s, *args, **kwargs): return ('new_doc_id', 'file:///new_doc') def pillow_to_url(s, img, url): self.pillowed.append(url) return url self.core._load_module("fake_module", FakeModule()) self.core.init() def test_scan2doc_new(self): def at_the_end(args): (doc_id, doc_url) = args self.results.append(doc_id) promise = self.core.call_success("scan2doc_promise") promise.then(at_the_end) promise.schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertTrue(len(self.results) > 0) self.assertEqual(self.transaction_type, "add") self.assertEqual(self.pillowed, ['file:///new_doc/paper.1.jpg']) def test_scan2doc_upd(self): def at_the_end(args): (doc_id, doc_url) = args self.results.append(doc_id) promise = self.core.call_success( "scan2doc_promise", doc_id="existing", doc_url="file:///some_existing_doc" ) promise.then(at_the_end) promise.schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertTrue(len(self.results) > 0) self.assertEqual(self.transaction_type, "upd") self.assertEqual(self.pillowed, [ 'file:///some_existing_doc/paper.10.jpg' ]) paperwork-2.1.1/paperwork-backend/tests/guesswork/000077500000000000000000000000001417573700700223325ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/guesswork/__init__.py000066400000000000000000000000001417573700700244310ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/guesswork/color/000077500000000000000000000000001417573700700234505ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/guesswork/color/__init__.py000066400000000000000000000000001417573700700255470ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/guesswork/color/test_img.jpeg000066400000000000000000015364311417573700700261470ustar00rootroot00000000000000JFIFC    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222 " }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?5S9F=J^>Lך8sKdzpqC~AG9G[J0z@ G` 8;hhXzKr=7o8>—>Ϳ1?iy@ǹmKG4qGi6=><(GO=9mڗ+uh9OΎa/^299(4(:F 2}384`)g? 0:@pz\P()dRҀtQQQ'J\zQ-K&4@ c/?Z0}qEhϱ3@ A( !#!<@?F1'ց׽(ў8*sG?ZL;4d('huG?֌(8)bwcvGf OF8sFOoҀ1GA.؟82{sHHhQ=*Nz~}(s=&Op){Ⓩa@)G!>Oϵ/,0GF= PvΎJ_ƂOH3֗usNt/N4>Ws#HyLRӟS(#< Mґ3@S}8uǭTsJ ؚLjL3QB)qt翽&@994wҟR?Zn;џSu7jd?/^?4 8<l҃@ KRRp{  F}3R\8Q@ <~ǭ(> A(sA4v v99Fi֌ uۊOP8㟥w&9R>P2~F7P ipOHFOZ\F=u₾dZ3 )z}(3x#C:K@==*LsS`hlGAtF=@ ノ}~{yݞ7 {њNOqFKǭt21z\ҏ΀Դ~tӱZ(0( R;hF}(uZPKJgzRi@ Ri2=3~OF :ڔ~4gҀ g֌zfƗjL֔h>€рs1^{S9?8sIzRv@KIz2?t\g2h֓IFOp(i3MQK&GJ\J:sCJ9_&:Rx$ =ZLFZ=G>֣oʀ|QNjNsF-ߝGA4֗ڎqϥⓏZ\4^J ">cJ]Q(ڀ`OŠ1P (s@8ёFzrhǽE/8Lg4sRғ ԏƓiҌh&3ӟdvvOKN;~4wROS^wҎ})F=рx4@ւ9Լz}h=J2i ӟ&1F=1G=OA&:;N&ќ=)~+?\(Ahإށϵ' {яƁ=ڗzLUG^F#ځfi{zQHҎ}8&.x㧵Gr8x>qC PxOKϾhPϷKQђ8" cIǸ&4~"}Z>~4sߚCA4q܊\})JOʗ=M!ש:\v<@I^0;:C ?/^`>(i9A*NzQi01hތL( PpGCftրڗ>\RRsQF3hzFsQ(2֗w($ ? 4d ^{RKzQ@ <^=i3q֠ɣ~4qހ Z;񠃎ƀAMqFʎ3րҗP9@#9G uրq$QF^)3Jw>Ɠvv=>lюiǧ>4M8FN944dd:I!u4L/8SsfZJ1s@ F椥BIw”6~ ^()(3E/H:zb(֌昄QKzё@9ɣ9Ͻ&HKZ>g`hb?:zCϽ'q)6s}{(ǿH1KڀOsAvzC 揩{8aF.8j3F⌃_~tRRzQ2;P3S@i}'(;s攚8@ ?ZBqԊHGd~TM#Rd&yQL q)b@/zFލ3@F̭E!Jlnqߊu?1;=>ր G>n?Z_(ǧ&G}q&}џzLր0ߵ.O84t ?A@##4=dw#d;sK&y{>e:o}3G4OCJ v=(}L`pqI\hc ;"cFK 7QKG/: 1E0 Qzinyv&}?JnrqQ׿h֓9=Hi{rx G(=?Z03@ïSǥg@ 9m֏G44_Œj9(hL}0tr83 _F ǭ1.#uqh@KIv&}p;b ǽ8sFSKہ@y>g@<4 ^sۏqg=3JsGn(lёp:@)NhŒG v.;i3K_ҁip~Nh8>t'9~tq4gށ xcJZC4IRʀƗƀ?z)zQ'E-&}>;L@ QGG4p}ix99#Fx'4sJ34}9ϵ. L֠{Ȣh֌cQGJ1_Όj\ړ'=p?ɣҎ}4֏= 9`v/M>Q 縣?#?AI ^h&=?21h֗Ach ??3Qނi>}8tրF(R(9fLqu.IOʀi:P~t RgڀZ:Z3QQ.xqF}SP(=(E8hGncހ R'?Z(Qb(ƒd@=hǶ(z)m=4/CS:1=ҎAz}(u4=SGgu?}pi?_ƀ c`9>J9p?Z7i2zAϭ)'M? ցJn\ԿZL~;q8hր?*2= /?&?]GnsoR/曞q"@>r~'>G@ 2#=)d ;WAORaLqc{RdSH[AHɣ\R~&-،}(_i32EHIZOBR`iB=3Fiph >## uܣ\|+; n ,PCpi E{"QZvs}hLF: f:\ƀc9ew; Sq@.a|rʎUu Z@+w֏EDxے9-\q!u`Sm@b f,g`zRZ=3ʎp; (?J2:RmKF1MO΀ }?*\>Pq@*3UCPHӉ2b9Ih$g,1sd vЫ /At`wcA&wԿY\p\jՅnR"M/3l+qք eΓpZ" N1O,~[+uQhZNE®ݷ9MbؑbRyn?:=Sɹf`FA{tQT|j3ne<$ò4Fy5@ѐx;)Ո&g2+.'9қ4%Hp $'iIY^ΩuFژM)),\Ӛ7`?Z NKi#) y28,bIHiÌH@N>E݌ک]\#!9m:u{u;w<2&qU#dᕄQ "Yb(F8Q˸X~ q9S4(E'P!zߕ(=1(ǹ=$L`HXQyR.W'#4Ҹ#GVr4hp)t砈rOEb4g=P:2 Rg&.}!8Y.|˘>6|SG=j(WVi%*zP]<<,xC#;!q6ri-IF})qigީf})@]2ס`d~~k*= 2v^G'bOz@ۀ z @;RgO$7oG֤?kaطQRVRKFi77iXV3H7L($o (#v2Oc'<>C_>Pli/R06qX,[{ޡS2mR*\ڀߥrN*ҷ7L<4Q|(zv ⒪)$JWG掾߅~&)s)擌4~~T>gG֌O8ƌ/ޔ t4g= ѶtQZo{ю4`р94ΓE'yuP F~#sFN0:џ@ ʎ}hҀCIhx4`!} 8zORM=h>`(((Qz1Ƞތ4p8 {P>&}Fi~?J:PRcҀ1ҀƎJ(sў(_G֎bGjL{zF9Gӯ9z9q3ڌ?1Z~t}(ؠhأc1K%/րҊ0;@Z\Rs@=GҌ((q@ /)?Q~Td4R֓/ZNGGn 3@ ?:Lv @CFK@ģAK֏h"GOF8^{QGGQ4vF=( ֌}~sF1idpsF>Rӭ&qSۓIsLARz\:u=?Z^>ݚ6 v}&@ʎ)E)8)zzP>4Kzn{G#kQFs#QHbq'R5Hp;t; (Gl8<v{?*BqA>^}jE^2>*$vъL`яAI)Lu<҃vmj0}#=~Eߟ/QK0G֚, :txeLcG})iZGcMY#B'p99L8co=?:B gKpE<*1ǵ0*;'scoF}3QFבJ!aE%ThdI֚8P?K;fю9${P yᚊ1&vɩq(2:@~T V1 FsU#Y [;~q–(GHrwvv6[t]?7j}i1E.K8#B 9jP}0~`>1KgҀ mZB"D;6\nnr?:/7,F+/3\leU+X̑'GZ3̊}({W P06+B(@szjۏBr"'ڧǵR61DsWr) \?tP6gkyyݫHXV\J&n$"֜'? Q~$f\Mcv܌C)$W3!S-S-`5] LH֗QzڤҪDde@!m(RD@AudK Q)8Jz4;HAre<܃jThGoȦI>cĎ0-sU\HL15d!C#=UJ)Z q xm8nvІRs"rw8'BRq=C|̜E[lXb7KcjݬV񙣷dԩqsbyarvI4C/`tBɱJ5^] [ c2\y~K(۝qBMjc@۷Mi*?Rǵ'?G R֓m.= ?J?PAjeF|eתHiMrq-24x1NQ 6MY&n y. HjoaVTgˇ%[*9?XQF5HK6Yl^T69I1V,JnR4nNa~0.]XqR^2xm=0E2Pd`rC[0jך^5[erĎ*.*.OBf9JU#fb@I%#˙I=~:ߥg%ž,CA`m/TjJA(j< uq3tʹcDA=LQێtHN*@?JOSU݀E cS$t} :{/N?Z_ Z8d~t(4~Gڐ>cz60htGJ@A8ritH=#@׵&)sғϦ)Qz)ASǥ#4ހg(F fEPۨƓqG_jLAG@ 4GR~`w8@ KIu4P1Iғ#ڐm/c8)-/N'OtPױ(xJ0{t@4{b~4 / _lSvL@ (0(p}13@^>4}Ji=(v?4g׊>t (((<iN;یo:?.=P𢂾ŒP룧~qb1ӏJRg.(G`qM8aG>ZNj>}((~\{R~FJ?J`/_Zo@y!Fi>z0ϵvޏ΁8Ϡhց1RRsIPI8qI@Z1Pq@cQ@XR~q)3—$tR=2}qK֘6pVFp2pjJ{'<'E}*1<{A瞜RSY9JNSUs) ~A#=M G=[qdc,`Uc*ڥIcsy(N14q9?.=)N*89f?)*u`JpOJhe:b.͍M8J,"N}'PYUU6 M)X`85o@# d;ہnE! CRp=9({0;QZ> O֗8i9<֎wŽ㎔gڪ\n8dTāt4䖜apHfxI]2n6A+?*^āS@k\dZ2w/ljFNzz {ڳ\0rG?ҔDP)}(JP8޼|4=ܒt5h<7*8-68[}*9IyPsޥX F_䊔$6/3!UU-O'* Z =l^=7/<5gDc9愮$s?F'B1 dLt,3FĢBP{vT$u5wLl |ҫƈzVHǵAqiwcRdV! &zƗ;Rd".w4Vv.TqT<#Pٵ͙tUGП(qu3אj.9D(R|t¨F`YтOj䷕@oW ާ#ުFN.w.ÒzS#@t:G5"*HYPs-X&0JTb3ˉ 3ҭv`z}Z)HAI۞hу#(֏|QPp:fSސh&vʐ;{:QG=J0)xhq@ G?Rc@4wz8E^)~Pr2{~tG?.}ϭ&=hϦM.~J988gߊ^:P:8?ZO/9y\Kh<;Nhz> >'G~)r}:690O^M.} '8恎KϠ1F(4~tbc!籣 qFq@=h>wҀ4s(hhJ3G(4gӥ ?R);@ A&`zџNh@/44f ƌhϮ( P8>=Rn⎴uя֏ҏ0 zbQϠ(>4sIF}hǯGu挏OʀѨ:FHh>oGIzRw s)r=q@0''.GJ1@4(z@6jSh4QKP>}MzƗ}( 4zQׯҀڎ})/8J31IKR-zi >;G4Q(9)9;w4Lؼ F1>;P6)>5qL1j`FoJxP3}qcS iiw?.zS1>U%\H3HG'=xKr8ut/hYVqϵ0ćcMVH*017A@ HBS8p -*{cL؊ϵ.[Q9NQz~TsIF>hǢ(;cځI&7 $taHUq/^xMnOq sځ}6l9柳S)p}ȌxL{~t Hiq@ <ʍǞR`٠C7d*!gW+Vx"Ҍ" h?ƌzQt +0rTʞ֪@=v;͔e`4h &ǹ;拰=:\CKw):zڐLX63 D,!V?9 LwK@@ eܤ׸f"$[IdiTk3<۝9@aq Nq{KǦ~u12XʃIҀ+sw,G([P Y?JOӻ @ 1 `TI#?z-/fffo0}$#c4N.VcO%Jj\j0{ʕw/OLPKm#fGҏd`b1&޻1t9=YCKϱ{PJPBtZ)9BvBV<#!F>՞^iڋQ2@xzO‹dtE$ASAϸ8c/&&ǵ'Eha$`sv#4t=M-PJ:Q֐ҌQԟ(F3K1P`J9{Qa@ @'0>I@ah擷=(i30:)y𥢓€4Qǿ44bPc{qGG8m)0OoҀ<IҏIFNyQK((@(Ϡh2}*PhϷE{dt&Ku4dɥ8Pxj3€(=Fi})ߍh'qi@ǥy㱢I/>~t`9sF(9@>`= Oi7QArx֐zQzq)v\I@FƘIsE\} =x`4c=>nH` FM&?QҀ>ʌzQG wG#)A>f ڐIzcSz~QsF}h"Ԅq;9֔Q@ KG4 qFOI4@QǭCF\ 3\Ϧ "?:{^?*:G^pZ\P lQۯIۭ;f=q@4f(s۟Ǝ{'QJ8?cC~g.OG41\}(֐g<bϭ'=G^s֓r:PI@FƖc;@A=@ǡ#Ɨ4Ґ&cG=ɤFZ5 9P= ixNhdQ?O΀ΘǠi4qޗ( 8{^֞Byl' M΀@lS$pڪWiҺ=i&: &_09<3\v4ؚN??@~ϡ'JwE #T&Dg RC+#|)`tCF7f3g#dJ@'>(G4¬\18Zw饕TO >Bz c3.rjH8,(X4”ߚN=hsOqR`_! @7cw7. ̞ITy&:gQ:Hpp=i,\FܠH wF#ҤԊ@Uɕ@gq$`U\z} U qc*<qޕ{/p1VaVH³n#))=HlclLJ=5(9P})Ip[t&7B8aXOG+~xvZemě' ^blsǮ)4va!#QF &iU?t}72Qd{@rg*[mN"#I 2AN\,?*2s>cJ&p=hdRusE-'=֐ 3?тzLdc?z`-IH$vU4~dzJ\wϿGG@~LzP֌dwJ0O|Qz2{ Lgm'&;\^(g9Q4PϩZ0/֐x8sIǨ&';N{c?J9΀Q#jnnp<(yݑF3TqۥZt?*7 g'#<q98<R@ƀ`hi)?ZSҀ9P??hy~p M((ϹbZ?3I_A<=3@ hғ$hݎԻ^q'N~`z@Q9Q3I@ :p}p(h(Ͻ-: b"= /i?: @1E?j3M(l8"u@?QJ:{Rs4zQqPi2=E~c&?P Ҏ=( O^}(ϽsFq;P&GRx:Q@ G^{ 94v;vÏz_ 3Sg\4(Fj;u4q@q?FsaU(sG4p3Z.(<:d~4naQ:f ^&N:~sQj?@Rdwir; 1GNsg9>CH~.;R(ϡ({PQ/A϶)xRg})yAA3H(ipGz?;QG>cfC r OÊLthn?:QڀP>Phڐ3(2]i0ΗԙրSdixy<'8'5یcihLfRpdzRzQ zj`}ZsO” w?ҐF[!{u$i3qHIZ7e+xTHHО0ϯƘBKyL8*e2%o AzTWK1¤VFL|r:u>^Ӷ" n;qRvC*;h #fӎ99 7nVOH6 66Q!5+3QEr*6wH&{T#^kwS͏%5st3\MPq1-m S{Y8֢ĭ4jƬ*&VSЊllVS֎F}"3JaO?/:(zҤ*jF0 #*<@0A@֕X>2se>qST dw9I$BO P*&s?E=Sp~j(}G4Dծ Ċ6B731UML塺  Gj1$t2$`ͼAu[*ώq桄Tc!"+T3# 3O#IH.^嚥m+]PHS9榈m}imN=T`: җG4f R~44 )zt\Ҏ;04"QQ:яʁ=4p)hRgތv#@(בF@ç 9Ҏ!1G^9Pzސ{Rc> R{g2R(:?'3폩 FG~iIMZ9=?>Ԙj\擁mP&&3F@sEI}@# Qߍz z\T 0xE&.}8џSI֐ ?0=E`igގhF=G@ L=y?*0@Gg=8igGFqޗGSi:P8)AzLڎ;s@< \t㩥<(ǹғ|zӁIs =8*Lz\ :zRt?\fQ=9#sQ@}h~s(h9Fɤix ʀwFOE'>ߍgޔE GC֏m>Z I^:0M&z?ƗhQ(g8i1 挏o΀?ȣh9= /I8ǭ/OLRgh㡠+GGl`{1@GJ9O@4(Ҍ3Iz:\_Ҁ INyIN)q$GG_zP($v4,?.N?Ԝvt{ך^p~^tRʓ<=z0tc=KI?}h{bƌt}Iix 4cڀ'֎!F=4Z?T=9isEh3IӁ@ CGIFր4czAǭ/nQ;P~#b3җjO=hZSz@'9zQF=O@thRFÿAH@=FGzb:`z#dKJ32!HP1Ȫ"Do)UQ}w),6r:R\Ee0Sjmn#Vt9d87QČ56@Kr2] *>fl`qS`Ua"a F!pēSҏvhG.G4Ȥs(}(ɣ=(ʉ]Cd.q~jƩ!p0ǩ~8p䐊90\z&fBc` ˟[(ph8X}JY#nu?9h`QzcކPPPzJzu3|rc `˅ qۑ;gOeu,xQTsoWn݁xUȩuLg߽=yz@@1Mt|ROҀl0>vYbSI$24&y&3U\#n;YweG9ʜЁ`uQI)XG\#B=ҐßRE Glgx4YI* E)Iw ^8 pĶ99⎀M`z)G"eB9'$i\D G/7]t4"mAwP!1tpqI˷R%ӖB*Ī@T7[shݻR4W&1eWZ|q4k Fy4Kc#hf+ߚ" + vH #?(x :sUd8? j xdY9%eR?ՀT +؜p>j:C V8SҦ)pyΒ2~njD(Ӿ.1Ҍgwǵ ~3s4^(GhzڀԜz2sך:4g}ix/>u>1`9{@֌zR|.3G R`h@Κ=ڗ $zdёLu&d{F~~4g'P3׏ʌF9qGs@O?@ƌL(siN~c@2GvG4JG {r3@x?9Ǡu=@G)ZN= '~4c>Ը'֓q0>z:@ 7K 3Z@%.qڎ8dߘ (ȥ4?:C')=4sKxKޓqqL>'G*?w\HO4gу?Z1'>QQө4q&?G/J8=A 8Ӯ1GRs@3@ (?*9R=p}J1gcҎ}QF(#~:`H w(#A_z^=(9ހ~A<Hvրϥ.)9hԟ;Psړ41~Tqץ/“}gg4}y'?yʗlǶh(4s(iԙaG>zߧ@~I 1IzS2J2{KG@;N}.(ǭ sN* 0$tQ= P}(rLҎ'>tp>O—>Ps8Ҍc)8 AQӧ}J9J^{z1@=~uր4zgL܏Ɯށ0=7GP!2Ou'ڃht~4tGA/ OŒcK1@{QөؠbgK@ϵ'@s~Th&ґ@x(4@y_ҏ>@xKGAGӊ`}h$ccgK@ sF7px4OGbi2OSKޔ{qHZ\Q(((#Oʀ#E$j) o("gFqb9~TSd(8- 84r;~c@GJ2h)/zN9Rj^;sޖ`E\g PI)1y$yRc@8<{R#<QSݤ1@OG4sJ LF1<H߭.x@(F8> gϵ;Qϵ'^ QaG8z>IP!x?jNҏ) L=2iqK9т=(#y*L~4x$Q#=,tQGQ@Z>{i984 :F=hw=?E zqM/>ؠ#/9AP@3;4sb^EZ)1L}(J)ӹ?ۡց}98G`=^>t?&OnhqK@֎==?Q@ ϵ.8G|PRޓZN=hr=*ZNh=izr^:1@ G>RzOҎ}/F1(4c֌{@'qGǷJ\PbqGF=R tHKF)܁4@}2)qGqtϽ/KIsF=p(BqwQKҌv12;`hQg@ bh4dv=hG=@'Sӭ(`ʏlRKӽ&}#FhϨ8cQdہGAQ4Q0)qG7bԊR~AF=(>oLcKz(yQFhs?8&0(NhQGǥ- cKҟQPcҊ?/{gzgcQIR~)Fԃ)w1b$~4:>93KIPji p(>сʐzKÜ@;f> A֐ ^}Lch}E{Ɓ^c0;/>'^h9 #Ҁ /nTu?:6=G 0c)N1~4>`ҎQRsڗ4ʀϭ4Ҟ=hsA'4vTs}? hoΎ A32A8?J7qށ~sڌ/?J%qҗ<{Rd~4(ҁʏҏ4zKPnz4qҀ(J_G4r=GIQPJғZ1ր^uG4 >QҌ(i?F1E'hNhs)2G@ g'ZP(}(h8h(g3I\J:uPs@3/IގF=)0y掽hhG{ŒA?@Œ~sG@?z>shց~QE j(Af𢁉: \ALŒڎhQE {( QGzz@.Ic-^Sk}*Yln$cWn7h #?X p~Im3K 8?^Z;3A'&Ť'zZNOF>KItM{QڌOʎi^;S'N*1qSjfpޤC;zN\{R:3FvϽ(=z:T2O0}y(ǵ)1@ QGIӵ QKIo֌~uMcԜKNhhi0i ?>֓Nh7QϽ'>g@~g=GJ3zSIQ9 3Q\'^s8ъ8lQϦhqG>Ԙ\b/gڀ֗94*B;9&=(Ҁ Η>1@})i0}GIǿE/Phҁw#擟\J_/I֎0sގ Z@qG(ޘK'nq)h(s)( IKI1OŽ~4~4`z 0=(3GG.( =01CӦOցRtおhQA z~tdL\}M~tI{RIPџQGJ>bPs@SG82}1GjM=P~z掴w7J9K֏րڗPywZO€{Rs&bΒjJ)1R%.1gE-&=s@ Mb\4{qҎ~t=IҀ=/>)8Ⓛ֗Bi:g󣞽/#);Z\gҎGqGQ J=Ҁ=~@ɥh|> =E֍L1(sLQxsR(08?&=8?\})0P֖zPO{'AGhsIӠ(&(8ќs I p=M㌚MQ?.G&Fz? :wh= R*8Ǡ4='◭t}hǨ{IR =RqFsҌci oۚ^3c' Q41@Ͻ.hܹyڀ  ޏƀv@4OŽ ^sȣ@RsKӭI>jC;uJ9 9њOƌ~gb>Ɨ:J_ƏƁ_j?yaqGJLQ`13>@NDTsN: $[vY ^S_vߔݽ)f*FJJ#u5DI9@4(Vo`rUtZ LCl`ߧV)'W`Yq$ԠB@P{Տ^* wK$eKJ\S\,F#;nPۯF$MDc]wS VUT~\O%0IדPCL=? @YP.z`4K&61< sQ(Hy 矛DλŻ#9*Yc>[wiDK3"xU٩ذR@dpLc Yd9D$!HE֤X:Xݿ$ae2{;~4&}h4i>9((Ҙx4dv#G>=1IA \zC:bAZ9}(qGo-PL֗>s;^(4sFGPh@g})y-'cEfER(ϵ-OҀZ11/'#~?%&q)IZ?:LF@FN8.ub`/ QnKڀғ?{ގ3H1~GI8j`ϱޖ_=)hE'@ ߁Fi1KuhڎڏQ3@ý-&hϵv QQGjORM/ Q()9ph; ^Q!s@ G4Q8ʓ@})9—(ۏ'җQғQKۥZ\(y4~t9:1/z)9Q4_?`.hQHF{~bh=s?'4gހ~4Ͻ%/QёhGѓ@'>nn([Eғ$@84,Rtp=?(u?4rqsA_N(hOr=}JOh0UqPF{ :PJ:O/n^{b>v39GҎ}9`(?*;h~4Ϯ=ڀ?{c CJҘ ǠFЌSj^@ ;RiI1}0|RsQӵS@3F =14(=($Qmg< BqԀ}hݓ㎦u'> җRw h٠OCKo#ڗcI֎?})psZO4thHXM!"1ӟ.OlRbqi1S@F8xyp{^`g!bh4~(1Eh(P(qJ?8~>{@uF44 vӏI@Ͻ!gڃ@G4p:fy)i?SQQ{ /&G/OOCIaFI{Q\QQPzRuJ>Ϧ(F}(&G4(zLŒ@'M(r}s/nG@ IJZ:1Isa׿c/G'QK4P0)0AсQߜP}v#J\gގh?/ʎ= 6ѷ$OBE66dh`W-Г(CMZhPádA6Ԍ-⥉7ne Tc 1+(FWGS6Lj&'*3$'bz 1"3) Vc@auVE lE1ʕjQ`(?!\ B[$/.#܋RAR `⎀:89?tU֫s Ё;e/Mb8Swj e1GABdhCۥט,WnsS7Bw q2ff@; cc?L3+P~a0*v8r0 $"QuX<~\ӭ[G>ZT" r@񦖣,In=yHNh<ZN~@ ގ(?st'>cҗy(#@=yNRQgRdwqKۜ%.(3G_J_ʌL>Kϭ : 'Kv4tIڏ€ FEp(Rgڔ~4PQGZ h=J:'N/}}01s@ J8flh:@>ڎGV)49dv?\ 7ɣ>}Z2}_~g Co֗@=(i?)z2}(ϱj(wZ8~~T~JJ8?Z>z^(ϵ&O~(Ϩ9>4{IK)~g~t)~4dc?ցGF'#֏Ҏ1G{b@(9(hRMQ. %P~g>gIO4⌜uF 1KP ޓ'b ^})1Lړ9xRp?ZGRs^;d}(QIqKǩq)hC:Q4Qu?*8^hhd9#JO=xzRO4dzcҗy R~4cހ'~Q;Nя|xH@9Ip=>㏭h))@s6=hZ`c8s xTqI Q=1@ Z0(E&.284`t?/n(|FGc@&GLҜ<94 QCGޠ/9~fci.y=(@=w=(9=*^G^zQ ʂN€?:zZ?hM&R{/iy=(Jnz=(9#?/?RO@ @#IӧJ=M(~ϵ.t}I>SM/4}iK'4ގGpM/:LQG> sF?*9Ԥ9>c٠gցhnQ')Hncя(Ҁz1j^84z3P:>r{?z9ZL\Rqҏo@_֓Z2õ=9?/xi2{ю8/=>8NqFq !#~=OҎ-&qڎz_ƚ}y֓4ғ>Qހ })8|vO''!;PQ?SL*? C@}h t>(@t1|PHQVP:}$qӥIOZUg2Lt9$+7-n\Zr@4eqCQ[\ܶGK rHZњO.Mw8(NO&{SB*"4K`ŔuS.W*8': $:Uު#]φ?z`ڠUKmY? $*ȐsDh`*<FcjyI"&w[vBrQO`ŀS֥?Q@=τw:ԑ4F5矘cZ+3c݆*lv=}S`8^ҀcRRHsGNBQ}qFEZ@&h;~QS3^h8$TqϮ)ʓ9c#׊GO]h: J)1>`zQ4`cK?րu—𣟭)h(?*\{AP΁A#ғ?ZwR`aё`6_Ɠ#րR8&{b }(ǩғ@R=h(Ϩ4syL =sN`;Q:Qӵ'@G>٥ZN޴:\qփ@ ތJ2hZL 8"0>;44p}ixO֗J>@ :t8ARgM(hAj?@GF}(ԟΓ4S@GNsFq@ N:?J@oQxLl;룑"P4җ N2{L?zRdOŒztQGZ\{(9h{49ti3A4;Ό@ʎ=hϭ@dc\0~h8JH>1{R@ ܌ѕ#)}\oRj90}iy4dwȤ#4f@Z>ozZOnEǥ/ cE(RrG8jN$j\c7"=h9cpçOz;rF)H"z/E4`c1o\~ʌ}M/>)3'E"\\n֎}J ќ9d~P:J\dQ1CHF;jOn1@_J6㰥{;~4J\RHsGMy~ -Aϧ@G=R8P`Kԙ>j'G>;H}h]g= 8/NzIzPʌG9~{Gjv=11GN4(?CF3TLPFGj )F}*2sȠ Rdc<◟lRmxtRt'ր ~4}0(N}@P gҀ :cތ8M'Ԋ^Nh'4~4OQE/=t~4v9 ;33@R4q'>(u1\c54yعTC#'=Gj`qG@;$ɍ ?FRC!c8rNsp",8M"hprr}8HޙSQE !37f@Uq TD8=*NpQQ\X2c}ޅ+ݤwRCA22.$ d'Ē, ua6i8UKp#B'! ПM#~}*ksv!!R8SfDWqx#lž͊i|tGbN֛NԈ8EP~>) 9SѱG4e(G_Ҁri::zьc8uPA`;PO҃)r}9Q4Rޓ?sYhQ3N{zQQi8>9ONFhyutҏƌq_~i3ё4t J;(Pǭv2=EN3Gu94(R@8/r)= fiz:\? Rqo'~ ?1z^)3?@Ҏ{2s(d9>܂(4 җ>9bsQ'"J;P{~urj\@ j2;Ҁ1>sFAQ@{yy@}_u&֣'BtG>/pGZAh/揥'>ѐ)~g⁆( !4z/*2dE\;{E7FlSJYAh~TA:q87zP03G=E'y)Tq>(\4azP4{sM$) [3֜$ؠ'H}HR9r1HFFK:cց _Œʃ׃s}( RQBGz@`ގhM&}zPp:sZSG~t ~oƗ;P?ȥ’qځ jPhBq)3ϵTLRڂ}hHs)gv ;t&h̸C4(^O;&xaG>sG?T-S< pϭ'..BO53LB9 `}('vgҬ (#GcTd1uMH}3@ @v&DbLO># c:D+>*\Žq?*toL` ^})?I~X9Ks w`c mj9Q}h\N(KjZ@ Z0x/JLzLRdJwh1Z?J;0֎s:RsҐ^PqMLP`QN>@Ϩ>nP9Iz?/^` ^M/4cZ}(.(=4(#? ^Iti ^.9hcۊ14q?g@_'=h1@ ~h;fz:gWK^gQ" GQgڀzьIQϰSI{@ ǩS:Qy!&)Hj9?QIRZoZ_Z;PQ_ QڗJ3Kӭh/9 'LGj^7AS Q&3ސ:+ӟSFPd{~} HsKj_ƀ׸QfȣF\Qޏ΀8h0r9;ɠ:Q@֎;sF=h )2ZBxA“ pys&(QGn(3@ÓE&}:۠AhIϾi? Q):vbϥ&}F{PE LإsI &=hPҗGZ@:tTj(ҊA֏q2NwsILǨR6y/w'ӚSrHn‰%sOj`QrLS -=4p\d d*{zIgc/M6B8-G蹬GQ >[gzv(n$i/vpjC,ck㑞%;Cr2?9r tj~Uy0(Ty>HlrS-F*PM,H '`@'2cM4 !ʬaTRCpPlܤ qaF(<>xʰ?oCwim3"('6OPhȋ6ҤݎmYsSg Lf`t @ Y~` %=qOJ?'?1( xO'P?9? &>ԄJWM-V%K==)g Le N8Rj(1օvé:tTHtrzqKx ~Z`iv[1Spfg M-d4m?'N*_Ґoo8jd)-ȸ?OtbȐsSe( #sM=TNU`ܸ'81ZRI/֤ޠQv+O&}M viL4㸠G٣o`cR&'?Z_ʎ?Z'Z?:N3?J@( ivI(+w9R{ Pr:>4zRc`Oƀ r0 ^ZLc𣯥c;qAh9{sF}G?JNTFጃGJ=h<'/@=(xQ)?Z3ǭ.Olj4=zR2hރғ RP4c$E/4xzfnN0\Rts@ ۽h#2(ϩs(zb~`10OzԊ\gFsBcь :G@ŢS@Ꮽ{ŠJZ84zs@ 9sAК1qF)1Kj:h &8H4 JSA@ ֏/Pg֔I3~sQ?@yQʌh}持9z8>zRM.s'??)hggڊ8=hz\Z7gN●z@>i7RihF~dR3qǹi>Ͻ/^3s)tױ>zgg_L3J2(9H3` *2|MCl̋Gq;`xXm+s:Tpo݀8'P ;b0z FNqƘ! <}(֎wh@N;RzsE%A9QԹ'B; ÌzԮiیhI9t 4URIvNpF tE@UJP;LdB͌dywJNM4"ƹfSJubFhty׭7p~yzxdqm(dʢ<2sQmDN-S=j@,V0H5`:Pt @$ifg)GFK&B2zsNADžRH*H(7 Jxhъ(,KNiAd="#IOjvzP=GJ?*((ȣhގ=h 2xⓏ#O.} pMW[mKx gm9|rp)مyIlL? $#G'1wIlӣdiBs) L( \ciip'DF@^uOZ0 $q4.{H8GZ*e)'n*9-ugӰXG`9r;ԀMŒ^薍WÑ@2=3:~Hvj)s X>vTʹj" AeDY<;{S`n2g 1=RrnN RH 5J%o7srZ-d'Ij9\-P1ڝő֎g6G, UDs"[=:[%&ubXӰ[b? B RpxEfQz\ NGzo<ќ(wEs1I{@ ǭ'҃HG?_'F3ҏƗ|/hQ-C׶G'FJ2ցG>7'K}P8si `ӺQN~zRR`c&qԌ#e\|RzP\u uɂ_jv3$,`Tڡa xj 26H#<`tq5~3T Pq?!>8Vh($t`c;SnwZzp=Gs8ߚ3 $꼛Ԁ" >.]r#&A7nOjqH~Q6„";iH<ALh>nHVJ9`VpIhO3f3K{'6D'p>H4eI)?ZA#ԑןZkaDMKvΓn(Qq N:is =i8# vQz1O4pzc@ F}Q3ځh!ڀ><~}qG>Pj^L8F?ƀ 1т:`QrF}'PGߜR`w(8zsJ@=B{ P1qF1ۊ:sLQv;qM/=.3I^ BPmpb94 tt~~t}9Ǡ.yɣƂ?:\uF1@ {QONi@qf<)؏Ɩs8G#ڌ~sGFaGh~9)JpSIsѐN:?'I~GtǾ(r()x>@)i0{ Pd(KR`Z.hb \q4h0r9Ib {P4`v?LJw~@ FߧKO^€=?*:\QOŒQF)ݾRLҌhǵ`PR@&@QcGGGo0 s)?iq@?1N;KQnK( QN(?-4=9vg 1tAfY (UVb9NCYLXy{qZ$>[{ V4XLH6lO6oV2:vĆ]bNʭPݶ*XJ drҭt[n.pG Vy&zf旂y=?Z@-&F=p^h :p=FA); 8 Z(srHhH8?N*a#ҩ[ТLlCb+4cք8r=RȪȄ5D zxʳ:ųN)Z3sc?{5 F:FG$XNOqHC?6E[9>*ˌxs1mWceqjlo΄ 0})rI>1;,I>gt吂,N*ourEF d=jq~ rII;8Ma13!ImxhPlQʊC([*( c4lp‡qn: Q: 9KbgQQр=("(ҏ &sG}s@=pMA֎}?:;sK׵R8R(ڏS1ޘ#G3ߊ t&)@D-!S,M.Q}vOňSGDbK0 ibQ}ҍsb2ӊx;PRy8@.,2T<@(Q6Fe`S٨ @ ROjTܹ*}})jT=xXRTw ?$ ~;xcmWS03F69(S frjga6;5!P9@Н$Ir9X Gj|pQUsQ(L-V -4c$`gv֚6{O#AQm!$a4<³ W "D%7gxX4$2ƿyJYCQP8Y%XAlu5V%F3ךZMh=bz~֎ >Q׮(u=(ȠޗZ9&}J:gޗiz(ǹtG@(K⎔ߥ&GjwqրQI8KiԸ#F})1@ (Z\c@ _RvŒH@@Kt11F;4`JZ>сGGAGN|Q()~3PQf\Q֓=(qJZ?J1tB &q'G>cҌ?:0}M/NfhQ4~8aGR):g@ ޗ?Q;u9oƗޓ0}OAx QLJ~1hG^bt6w6f}1zT}(я!T ;0rHJb"`vhﻟ|J 37vv$AVq42*>1|};? %e4wGm u$8!@ cS]@F FGsPL: ,0m^ԽQ)XcJjΙ89 Rx91 8vQRNOQH :Sd FrҚ(!'ޚeI"a-@X.6 ,fAu2K1ʩO 2I>ӎ)>8 HHqP78Tu )$ޘ@4~hgfKHqު80 ;R!168E"$cj9$ĨSK܊M]0.G@ ڌqIG@ 9M- cbnORуh1cڔ~4ch\\cփ@@I88?J~ 'N O΍ތ@ ޔ?ƏʏҀ 6tRF8xB{ &xM/s@ Ni8Ҁ9?Îiq<Ɛ9?H>@cڀ?ƃZ:d~m捠v{(f'ӊNztAh#g:9&F34qҊ;Ӹz~J9#>^??3}h&ȣ4b9Z:!qIO<4>bOVF\#Z'4vJ ''Rgxbcڎ=?_ sFH1L擜(G4֗֌q)gؚ2s)?\>`sH=hR 1 שأ4118i6=E^^hh`bzJ3sHz@=)Z0c|gHNrր$ӹ#qA w@ bd=4T9SOpGL=Ҕ8Q6.zQO=1Oh0҈y8OF9 rF0xnhاtv3pҋ)h@=F{g'b#/4Ǧ(7EAAY$ƹ'$Կ~T qҚ2Ϩ1FFsN@OU 0Fs܏€$ ! ZXNvzI-U`2 M@\[N!qNOO6ɜh2 R`gi݅F=)#3&rNjO)~99~b) 0R,u4n>Cl H8vB%`g@I8qY7n$c=wc>U[|9f͑Kssڤh:3Eq\ԿS@4`g)~<`Œ@ӊ^(qOå_ʓoPx@K֓(ޓ';ؤ4Q=(h'ގ(9CA>Z\:sސ/ )k}9R`} / ~\@G@J0{v=H!2yz9'QƀQIz?@ ߚ3u%(<Ɠ#1Kߡ>1H~/E~$юxȣ0ݏ֓t8i3@LcK)HxŒ~Ԁ~?S^}0 ssց>}\(y v4~4z㨣>-j&} 4}?*L}>}^_ҁ cߟ,AG8QEG~tzʍbb)hJќw\}(OQG}}(׊(~u:2}1FqC?ϱ}@ 8Q3E@ש'4c?h֗Qǵg֗PG0>Ծ 4dv84~8p"8 3LR(q@ ֊N:bn)i3G=hϡހvJ\R{htTi{PT=h3Fsףg(0qFOLP~Ԛ:яPџzLPhs9'P=Z9`~qGz3sأNJ>tP~4ii:v  o֝IEw(:b`{R@&~ A_ΓU:?0Ҋ) Oΐ)*8Ҍ4d{t"(ϥ8鏭x?J2}hϭƗgQ֎*;wHhgځ81IױA2=1K'N4`Ԇ}8.'0 N=ycޓ"4@?\<=Ɠhdw@}yc@ _F2sI^;qHa cFsץLBn*^8!0?ɠsޗ҂@ }ij-KǠ'> *.D;c@I: )`xתZ0~}Q=OKPLR`փ~t(9c>? L>u#֏Γ=(4})/>@qR4΀VZs4sր ^}hLgsK:cF>R)=E/>s֔})9֐$`hgap(VwHqJp擟AsK|PG䎌M:=9ރ(q@Gr)2{I{~w@IB.yFZ:w@ y4#/uKIs84~'zƏh*=4qGڎ)>`Aƌ~r8&>Q;PޏIh)r;sI K@:n{RFO: tR@p_hǠ.(@`r=ғQ߽p֗ZMKx9)84G^0}qrsG=(Ӛ?1G=R}3F(ϱ=#Qߡ?Qh9QK:L@=*CKn($@SGG@n?R.H&Gh(Ao֗4s@ïz3CGҎ}h~CS/ҌRKpџcPFG>&Rf+R:q@ Œ&@QA^{ր @KPSGj?*:PPHzGZ:P}h"}i8~R4KPt=sIsۥ*N=*\Ԅ`s>3G>qFqڀ<'@'ހO|Q)x΁w/_U'~p@3@Fq? w#9 Q@`zG~#8K 4;7J\z`Z_4`K@ 1G~"c=M/NcҀ>>~T!=GZ\꠭08iqF}= -\gF{4=h9@wfPFF(8ڗhzʐ;&Q3Iϭ}p(h8@(?:tGnz}h0iONh@⁇n(җL 3 F>q@ƁC(ph(@?~tb (F_“- J1Fh=ڏ|LcG :&s@'hdPҎ=/_Z^Ԝ1R(yIZ0=(Gi7hsF}hgޗ>“֎dz>IKJJ9^>}8$J`ZAEk}掽I{T{ƀswB#$q :cM: ^:t&@J\7zҀ=>1Q{c(J_!x&)*:tKj1(0;>C(~E&6cڏpTӚ8ˊN3@FqQ&HZ^E&O֏Ҙ Kǭ'c+&q)r=G42:>QB)0;Hj^~4c;~s@t`tt~ ?sR~8(A$sF(@W4cߊ? Q@Rs@Iz14cՍ PqGC(ǧ>Z_IJ8usҎG@=0lQz|@~=(GZlfs)0=sz?ZS=8ؠ9L hP(Q1ӓIקJ3Qяz?J10{(I@ j0> p:3?*2;P &sg>–恆=~3IE)O֓@;Qӿ@ ~i31FqAq9CGځh`t)j3=AP3F=q֌}i;g_c}h⁇9Z\b(Rdq}:93KրqH? 4)8`q(փ@#!1Rf=IإҀ }i~ph4Q) g_z:ihJ('oj`)~=`g4?^(wQKʏ@ /E \Ɠ΀aQh 3tj@s:cFE'Rb 3L>8#'F(ր4GZ8ui9'^C1"u9^##?:Qj;qL~= ^iy(QQ׮(:@qцE2z?(sIGd_“1\N}?Z9R8bc) ;F14vځ}H9P0Q;Z;sIBgEq@b_ 8=PIBњNNhʐތ/PBҐNb(~}hhc@ ۭ&/Qz83 ?1cE//E'K4~@CG=(~fƊ?OSJN;i8O>/p;Zh6Ɓ*cb "L$b[dpszli(ؾP)3h}==i3Iϭ.M? LZ^H8 Ͽ@IPsnJ\jnḼ#Xg@3Q:r3gQۮ")t A~B} zTp=jO5q HX)ぐj)r2G T 0?~t\39~TҸsy&4(0IM=*0 oNw=([,u ^qRsZRNsH -/Lc rFm r2>SQFd|=‚BDa~fCUaظq' (dsR!̳W8T"dHxާ0dR\G*1;)?ʥێje۟#XE8$OD3$GVPy䁃ӮmeX8B2@&Xd֤A0ܘ#R|fwSYv12ӒHm&?I#{rEt!w8 ,zI6) cqlɳt7#{ COסj]]FS@?jrO5( :zX;x㧰! ?44 ImeXq*UmA2gf*78*HN^-cSo"D ;ӆ*|'1 Ԓ mrw$Y7T6 qjŪ:GTұM N7zT.jD? PX{{b;uCY$ I< lp<$6qT"x`@N$`}*#ZG0 BxRHell)O  J 9i8Ͻ.O4)*&7Zw#Tu yM2wapsS[W#ձNWWPq/hb9hEHՐ]Tng4v8*-'vHꧽ@HZI\4@*iB`*rCeKwwq m]_T(i!4M&vz2Yj6`'$UmX zhLN90H@ eXCTnHO愮$ߍ/$vk6TcHN) ?0zҪ]k*K,Ϊй&+nLH'-~ YR3Z^#DyJ|5'nԀ_ʓ4w:sL~tp:"Q$Ϛİ)q_!j.NXTӅh1q=hjՉANAu+rBʀ0O_ƒ||&"1 Ǜ.=Fj94X'oib 9${G:Z>΀;QZRj8r>hR)vۺcEl;˃1@ёG.Ր  uңGӭIׁG,uaطӯ44Zc!bCjNF1E-'^3Iߕ/J:~'񣯭Gz@20Q A>:=T~Tszяj(h1~O1GNdя΀ ;R`dRP֊((֓?J;J 4PP)h1IBFi~hȣG?zNhPt@>G@8\QG=4gIq֌{Qڌ{G8u(ގ(u)xԔr: |P dz@Œ{RsKCqE~44 cJ0}hRgqF3IA$uǥJ3@NsGI@ Aiz ?*(zPQ(`QьQ@I` _c0(z \C4~tsh:^iϯZ;(Ͻ':1Ə CӠE \AE=qA~}(s{ՃL?bOք$-9ELBQzUr0ܜU{mwݬ!˴ O5j8YX$*q;c hSTs2rOj[}ٹ<$gd/,z:Rq*8P ]7o ([,~z|/6-#C{^VbH82ٓڒH[v]tA *8 ro9siCP.$I }Z[UUUBYGvE,yMCBەc8*0sEC,[c4u_Z]CiX =?Ʈ.cYbټ=ҤUwIʴ|)R\*<,&~T6uApcb:m?>P"NNsR(Nʲ:-`6Gʬ9늁>b9?J}&H?Z8(RJB\=iMSF}Rۈ]YA$*sW_}HX ijjwX9Zn$`U@ݒ*P  E<[Ƒْ duH 2~4[d.ry4GƷN[{ sx*K ?>i1l1QxN1sUh)+{xo OW]k;(@B>œ!)a`ABaPF:bAhհJP c s[T=qǥAoΔvBv(>^)9i 2P S- *9ܩvn $mJU0 c掃 ;4\nu4_ j&ݏ>vp*YcYahlH2 M$teba$#;~~ޝ'e6({R`EC&Iu8<*l_;$B&6ϐ XT7`1qȫQⓎzj: WPq]X\gd1ЁXeۻGlRLr rE"F>(2n5rr9%D dK2aGN!"t$-ҒQ2h0Շ_1 r3C@ C s 6`.Cw82كW$sQRc ,P >[#ఈ@^R$FT~B&1*gSUD1j9Yv18o)Ǹj+]rr`T6 !Re#ҖRV2QAa) H⣹b@VppgG' y9X;sU7PrVPGCMѓΌchԈ1P\䢁|==~>^!,ޘ  Ŝ+)B# >Ԝzأn%)". vY Xw޲nN!! $7@nVҴ"sSܠC"F*@.b,n~@,MҎ}})Jb;uW $hHj?Kϥ'n @9>8Nq)QHq)9(Z@FMǽ0ӊN`(qG)1KPRZ\v<J8)~u=1I*@/nhhj^E7(N:9Q? _4}iy)?@u3@~tQIyqFp)1@ 'GJ948rzq@u%'^RQFh*;pJ(#;QF3ۚ(/@4s &1J:qҌ)h4>4 (n':9}hҌzbK@?ZR=9bRqގJ9>⌟N)?h,QQw? ;ьvFqK/IQg(w3L((?stOʀӦ=)=@_^(4=F)2O>uf!0$i7:xJdm.QTc@v1M?P\pc<NxcAĬ}@:TO=Q;};b0]6~4 \Jdvӥ (ѻ U9掃{C%6pZty(I'i 7V$N*˶N:Ogګ8=:[HFIr([$*R\ T)%^:vߐ`m$'|Jv}B- )z0f(QnG݃S'c `eUe%MȖB)JՉj0r3ӊmj B;UT 4ʑ|'&dªEE|ňRBDAt =QoCl姰܄Cj'([ F . Te+̔X`2A>s$G}qh @c&OJ3*DV|SAp>8U}0e'g>=xI*›lߴǸ)'qԬS]*:nt3^s@[gvtszQ^ZFNwGMq!@~eG*)OA 0j¦IdJƧ`H UTI:b@0(٨Fî[3Vs늊(@~NFGJ҇ZAe+b[TB̹Ձj:A8=bmѫ+ǵ$ɐ9M:'H*zG@?&K?B *!$:FWV\297vhdSȍA rE>/d r 6늊$̼ Aq]q#UJ\F=E$=W~Γa02GzS*.S&PўO=]_jle M@c~N3,YN{JjnSCM>\9튕srO;trcPBQ+֥#".!&6 $X;P̫ˎ{)*]\46yRx'Y/<XޫF\ѓ$$M!Q ҂I㩤fӶib ~4!TiTQy 7 <-PHr;fgZ7;f0v\ԫriB ӕy4.0VϷHC)?UɒV)px*`OR9'cviU bFdD ˻!"gS 7\OYV |m,1vy,JCcJ;挃E~@* !wϭX֠#i#=EjSNSp) jz.d yB[:?04ʆ2WnJ}rH*feQ`Qrf0 :_g>i0"I>@4O Hc4g>nUb>[/1dm\qfvdJJ]IU;#9I )U`{6jD]8( aVlq%r#b:㊆ՃuPa9U^PQwBΛ*E`H*k9L*]m9ջvޙ Tz56+}1Er9rOS-ВnVZtHUcfc2@\ dF.OzBGQ_Ij ݓޟ?IM4?*8Q=@qE&@=@i?3Fh)Fx u4pzQڌ(:N=ϧO.AE'Oz8@ ӥ{~t}iq@(4 QJ:LJ3٥qFshfN}is@xz9KIⓎh$ w u/Iڀ\KzO?u?Z?:OҖGZ({G^>&}h(gAKI@?Z3KQғu(qҀ=~P='^襤a}iq@Iҟ_I@MF=Qbz34^ 9֌Bq؏ΗZ? =:f#=v8җ'P0Ɨ#ړΗ@~c1I-OQG?AE=Gxh4sМQFh>J2;j^y58mf4? uxVHpF:c#ҐpHc&K0)J 4*Y2 j4UʶI{fD vˁL[.2zS9#㸨J\v(arcT \n+ɹL;ȫ@H JZusɩ$kn oꣶjD&1~VL ! IjSHYݱV9ϽUte,7l?Zl@ tfVFkX@Mh9lLIMϭ,I ! fn!EE^9l$Ny5Z!m~^`vV"TM % By\" 6B4B6UyQV9\yw}1Xg8"`އcY|-6#Q*t\\111pE&&lQ$btϽ2\MUp]v&ɸ=jL9!޴f ㎙Ր*],m'*ҝ{c.=zԶ0:XPAbBNzsEI%xdigm8$V ]Fq׵Y66U{[a4d #KJv=qKBv(Is}qؼi enڬJ]c '5ZŹː3Œ,##'y XTi4ILQnpŁ9'UU) tU!<靸nquߘAuBbGx11ȧ$ iq3Mqn␊adR 0A[Q7d߽#5=&3h᪘ɀhgMI!1pf`\D7; UVJIP}%qԾT|XbQ>Q̈דml V945a3=o#3@q9kve1I5s4r A8`H-ԩ );qH=:}*pWw ݋,I!c];T6򬒳yLט-}d@Rx;к|̎7G*9Ap]G8~\)Ҝ݀&`>k4l0 u0h݀%EOUܒfBⅨZKuG.v8uaF*8n `Hw-Kb5GFvYQq ֡#HSޒ"$ѩ $xcڪN\j9\c>dwveE,x棄X yl4wc%Vm &{U -+2NuMdWRSք/>Hda 'S[oܨ0ҦhYt(P~;*85T[ys:[@i0{H1cuR[ z 'U%*F=E6 rq`Ugz9⣐~B*la,$.3Q淳fm*>C1T3g1!<*!"|:Tˈ:32GYlgڇ2[œL<"Q| rF84\.:+ Xؖ"*ƁQeB2R AjլګDlĀ6j}JlZRF3 V@51l`wU\ :C@4[AvH0ܪҢ.K6sD<`mG$?”7=Hnؽ3DC8تހő[GnG2 d`qj)%X$@i'lUVPf4H縫rѰ‚|&`CE,dqH- œ=(T亶Fg'%dTJ1ERg>JBG2yrԼ^@9=h r{RҡW]Wwڤ0hv!zB9Cr<6p:tt$ ޔ݇$t0>IY3TǍw1bIMG*+TMv^:Rh`*,h*1S8ٰ4ll$ڡDA88'w}ZQӃH99jN`4iqh1Q.=i0{@}zs?:9a돥S~&Oj:9J`2{F['#=)r@aKҌf It({P֗I4 2;rhFMus3ڗ֗=0@Z>oQ@ÁG4#GKq@# Q׵֌QgM'thϹ>"t@ ך\֏恆N:Rmڗ4NGq(z}%n{ (?OQ@qɠ:cҐ.=E! QL7RCnNEG&SsU{zMϽA)Uucbp2(bN)<)ȎKaҀPLy ✄Њl{T*W#|S[aw{ `vߌN}E$ De9Y|LY,fIbOOsc'QZn{#@msTRzu('}MzUPU/] 9WdRimYC ǐÚQ+sGA9@0C ㊱ơ>3mNAMH;#T⠍qӊ񡉙n<  jT08Q\M29t?xmٞ=ύ} K#mBwvmmo.E`IS1O'#֢HVNitX6G}q,ꠠC?zӾ ^ ݶ`9K'~V>QRJ1TSC2ۺC3Ўkvc20'&Kp)*ܒq;j>^V avٙnzҪq1!8֮A#\> ` &UlhmA4~9jFbKc֑*{"(hz`RJŤOQڒVJsLo2;Q`T(9G{LTgjI!܁7u^$cerOsSBCjL®s*3}4c=:R^f;VUk:;աҟA\3FgwRzɸgIj=FPs*q׌bTpS@_ƐzΗHAӁ֡# pCRתM28QerPvULwd?,YXc80vG'ޟq(wUe q8?J\ja"V3?rK_1L_JΫy$~c jҢCѻ'*oUb?֐*2'x-X/`x439P)2*ѶTG2x 58PLf V1]>{i9>ьsf"w?ƎG|{du-DEԞ{kqe2p95cֆS+̙8QX2p=i%\DDN@VӻAr `$Cz≚5='MپM Ng40qF \P}N8$z}iMc@T~;R(ǩQ@ /&8J= G^:RcuK1@:z})4}h@sҀ4gZ\-'ucQLP7L=H܌t T!4ytp…bG|Ӗ$W.߭;  S}*mW Yc$(f(ʒ{TUGPbTԸ8#,nKsM9z$ubO)VQ#9SN?"%d:#08:.$eK6qJv\ZGA#|#*Kƨ9ldܩJB҉#jr'*n `v4LaG$li tDc'N1Å<?҃leq;R˜0t2R1Ud2${=Jǚ[q#$u#$y 7^"`v*lT㟛csMP9$7~lI$Rig1)Lay2)6/BTYFf aYqQ H~Y]sQ_Q4۰榊9?Ȩ7YBSqҦ Y8`#jXX 7z(j.1NyPZ;'=.c>E-ᦞ6f\Ҥ1\nvHS,k$_@Ao Τ#fzbr&Yw *b>^8(`p'栖bIe;Y4yMJ>{Q}G}J[[c n*Ub@^Ni$c 9'7+ spxlU~uDZtp zEMn2\5:xurzXtcJT8.uiOl치LLsI]$stBE8!q)Ae'P sS>mlq6! yA\;PlԊÀ8,F8'*t6o W#֖xљ$va4IřpP$n t$:Fy< [S +G#:R8H9@~ܹo)@<ՐN;l}GJiBg [ӊ-Wnn(6{R8qui:CfRJI@A+  ʀ0;R8= ;;;9,2粂M* L_-T "RԻy7O$jC(#)7JI ( @ p3(?(Nd"7q {2i'Ya'9Bc ˏ|u`-%R:tbޠ6FܡA=>sX qHI%tQt`[jRt\㊪`(R̓p=j[8q dޛ>F'+dހyaOJte/>n].QBٓZ G0I4? "U(`ٜ63d,hXYZnK.6g#Oҙ1/TcR=12YI+lcjl5'(p W V#taj@䊊T۱eaSEbQӎB9wc 7?G&<ʒ{j_J=/I׶OցO֓$w ШA߭2YgR"%4nSzYK`zqXP& 9=蔀=I.ta@!#ncPOz=#tdS!B=ޙ F}=qIp19sS&qR3*F)$6Aլ d2ϸ*RF(F/RG 7Y6 E+Ð:tWѺҩHHȳa"[ISw#בR);FH/88 L1YCq*<$qI ȓld|䌯5K{y^M26y3JL#a.~PR$KpB۹<`u,SF:4UhmZ={P? ˚3Ϸ֓\٦ w47,{c`jb> 44JGLQ=h=i >܁QLF}ё?9{P:p(s؊0{8Ql0(ϥ_KI&=bEoր H3Ѹw IN|_C'KϱA׿IKQ::Lg>c`ց~zO|R⎜b4{~Ttw/P}ir}1yPRvR}*OQסҗ ^hJ\f fA揨PqjB=wJn(N?'@ ϭ.=@}1@{:Ҏsڀd]/րњ0{FE SG8 :"sphz4}h>4sIӷ@܏ʏ:gƗg4d>CGGZLd8I)h;&(dt␑Ғ:K֌tȠH (?OQ@$1K>>Mڊ=8_~—>߭FBrcҐ~_˜0qHG)Z t=h2sz@{Fb1Qˎ*N8Lr)r:!& 1PGJcU%Hϸw5( H=G>8cځޗd"8ZPT1RLwG#}h'@z`#4R9J8ʀ u#'9;sMdc_š0:gn?@34s2hhdҎ}s7"P ӥ,h«Ere1 چSTRb؃ղ8d|sޔIo>|P!2}K9=}=@*-QYW ЕZ3q RBA jn} Xlё:ё.Oc&IЃA$wiGC v[(=y"`I=9ɥB'vn=POUf\ïŒ㓏ME̻Fyj(fYx)&U x4Ybm֎1\vb;Rjtt@hpj9gH#! 3PŨ[LI ӳ]zqGnzUIofr;Rϭ5Ry Hh/ b V?,1O#F8GԊp}?~dKz\~T~?/@9.=Z 7@ӾgE& ci>bsK9FNx>K9F=ր)0G֝s1^ғ)㨣zfyJI.s֌Pr:qFOK99; Sy>18E.>&2O$zKRz€ϧFўR&P~u1ހ $:\P1?E9:Q@ <O&Gh(JN=iy旟JLQ2#^?874_Pm@'9BR*3P=84ߞx\fJ9K=(hŽ03K{tzjv?J\q@ {QGzwqE7R_zLzPa9>A8AEP@) SS8| }i}iךv3FBd zOTTUy❊0=hLHyA>(h0?ȠGSN@D'$(<4?*ю:0ҫdӱ4viC2?*=0}zяj`IKӷFMOғw͌j^RJ@7iq:}(;O\R#9 ׽/IL4p)qƓ4{P~=@'tp~R~LhzdRQj='#/N(/4ps@u(=ih4{Qj\s@Iy$z`sA>z^i3GOG ތ-tg ^;M<}}IqJh3aG@cE֣ϵP`QA'4 8?OƔ~t b  BO(@Z/ғ^4/I,He(v`;s3@ptSyWPrnsVI֖ ZRHsGG4sF(Ƿ4:QCKHi~4(;Q@ i@4gF(u"ʁӽ(}?KR}3Lc $ !y֥`# JnyxNYc=$z܃*3К" юyEE18 9끚T0k{joR`&iHI2?JEzvG, rՁ2*DT.~Gj/"hј:Tr>b1P Tv$)hIݕ3348\?*cnjϨ4&Am oKvH_T͹[l}jr2~COh=LRcsQ ȘFD>i$sJw#J,Jt%v v,fPn0?Q &犂ٳ$2'9& 7<鴓"0'P"NjOf4DƬyML TI)J@"'MO"YF%!N[X4 5VMݒ(:⢷gFco9RK)qi-xIl$C+Hh[e ` WZ2NG> qt$9F{`Tr8QҐJDpr8e/&`NG[R*4d2f( |zz$vib\A$ʚʉj#r0jx?&`noй~lR]41o# Ԓb2fd o PKub?"G5f"WFGBiX(CIt9R)n@ؤ8< DV0ymdpqULCh9ݻIuF9?:SdĞr*Kt;y'<,,LRO Tz4 7/9,9'.qVd7V'3*Xm#eYViHq▓R}x?!>biQ֐LAz:1?~tOJ1{Q8qG@j1Gցם1hcғ ''@$S(j1Թ>uhJ_sAP{@ J1.?ɥǵ7./Agbc3HqIz=;~4g=(~T`RG~(>qK}3F(3P 8E&tg(PrG^ZPGqzRc>Bz`;sIP3>}hh{P9QׯZFaHF: C cҁǥi8)p=(q@ סǽ◊?!@#z@3GQ4qт(Ҁ=i}@Ґ=x8&Ҍ#J:sFFsߚ1i``?3R}wc2B>QAǥ0b8ր}IǧFҎ1BpqKǥ/|c7/4S)7sFaKI^J:sHGzSI)Ґ$/ъ9`{Qߚ1Ihя|q@ j?ɤ{Trz(ړ#֖s@㞴rAGPПʏGGj\zIGq#֓>sEh(ތBg֔Rs4`c;'(Rp(QGA}(hO'ը:(נ@h$c(&W&9ҏ֓?ޥ=8a1׊Q` L搞:噇| ((qGLu#ڗF(w27TRqv4b?)~tƍqu$`g!Hr? DbTt8{!{g@̥Xq@`zfYFMJ,Azi+m @!ttx- TI@!jC^ 8$rǜ}(pͼDc\ct@f5}QϱP#)*o>IPP$:TUk *Kq 6pw10 lhɱb(3ՍX-ц?ZhhXBBnZ@[銊?6SUoϽ >΅#?OϡCjK۫ez1Oq=&T. !n13V8"\2E*0niyK8QXl:0K9#%S*fe0ҧf3Mn@> nu=*6ƶ~ը"^69$0 9əBaqMt#CU6SbFEҤ䎕,WfmO9T$?tCWeF~0sTSA(F<0=}!p(Wx3I)RqMrTWG f+=qFJX!y5mT8O[2˷Zn{Jc9KYCjC$CL?IN}1B*_ r;Վfr@"A 3SG7ʐđrAwH99c=-X1q1{-KG]XSJ#H1J4Ը擵!-ٌ#7 UK01paC@8yd6 1;T2G0 PGP0('#2vM[xaiNw q֠rGB܃ҬDD )tAzTTUmoS̛e2TJevyeEF0sK(fIWi9Hȅmu뚁#lV$I@8fȺ!vԌ%h]M̢*qi+lڛn u=8?֜ar9W `ցj?^=N=it ^DQ| 7'QQq/j(~ti}?Z(փisIzq@Ϡh8}yցz:1z14c:~T`cʌ>ch`0?JO@RdюyT~TggA4cP^>яsz\j1Iz0ž1@q@ȣ}(g~Qt 2{ JZ A@uc:F;сzP0sGJ_wу@=O4ր(1z擎;P18( ڝHj1G~?_@٣Oʖhut48 _Fsu@ NsQ@ǯ4I@I;}){ё QGҀ qbpisE whL~`uK:iy>oj;fӡ4Pcъ1`-NsҐ>&v)1@G#ڂOJNfO={Qv/=Pc`P18?#AZ= LTm\@hcQG4 穠8(=M/>sJF}{~ 8'$w?!)yQϥ!RG=c@ W4cTcޖ?:?Z^h9TL㍆>#4fw1`ўghL_Ҁh( 9@ Rt9hGQȣ' Gj_֏s@ P=yɠ(`zR◥%:旃z\{Q@&={w4mQFOOhOB (F(9Qj\Ǿhȣz`P#ޝϭ>&G|Q0hP~ ?GH ғOlAE/z)s=(F('@'=)&qӚLdOPdVlmlM Ԝ犓x—‸]bԼj+mϵ&$p~\ԃJ2sJvߎsIcRt x+J.z4ZsU`qNq,HY#cqSOZ@DA梖Qn|@OG^}(ȲF?180b6:լ)ǘe$'n x~Tc\.'#G$uqIJB#1(fpo\VG_SSjwIR)1H@.20Zz\wrf@*3jР0=?i9lB{OZwN HE)[J&Dvfl^pFA&1#,;Ěw!@#SfNZ-XA[Oj2}\.TG,&7RVG_SM`֌8"+bR +wŖ卑W7qNLnϗnx銨Pٓwi1W Uy4sgrҝqn;ȤM;&;c(Yyr(1,J\{j1^9tڋb{cQ}isrd$os'VޛP1ӎ;w%[GD*̌ێy*FA#' $Gp@ n ާ @\p1LQp^hILUN:qBª(K2A'aۻ'oV{PUHq)u-bU`OWI)w<Z:0ƌڤ担Β (SzU]p84C PG|Sc/*vaсHCw~4p}ip=K fӜ8=N}ǭ.NqZpǥ07z2}EIRgi'#G@'Ҕg4gRs@ғxv9&HR:uBp1@ ǭ&h^ N:q(J\zQ擃^ Z)9΁(:1MN}?Z>?p(gu _ƊL(}-'j~ih@GNs(&.GJ='Rǽ;@ j^uj@~LLcKE`QtR}(}3Q?&L\}:N83F=(sILc4bQHsH㸥 ^} 6z}(2/n"'1441@4})h(3Ӛ9'G-9?;>Pc#QEƌR4bhڊnAKG}=4cށ4'iyր 9: 0GҌ}qH>/hp{Pi $~T 0g& Ic>ԘA9P>}4ёG_l4`{fo(Qҗ)=:?!F{d}(9=Fu?ZN޴ Ro—=>ʌ@9&> Bc0֌c.xƁ ~ԃړ83 {Q& 2x֓֏hrhJ0=h~&QhϽFiq QJ??>ƗIފ2hyK%#Rg:PKL(ހQ3tQizfҎjbL¥PYϨ֖4;v)t(Z2I}h>n8?ZNsEf'@QӠ\Rg L#4oƁ((8&X/oJ1P֌v#'/bq&P &?T'?K)?*31x(ϭ'>ɣP=(fG?(g<P>J1KoΓoʀ'^}0}M{=(ǽJ\֓_ʗI=)}>{'Ng( ֗?JOfv@ Ϯ)p}hh?-1i?Tc(ǧ'֎h=(֌}i{Qۧh2\Q)A Nh8'QϩJ8`{~ QKϽ'#/~4'9уKH x֗N`{\ҁ4tv4 &Kq@?L9$ hSz)Np &2&Ar)w<џa-'nG^ԣ)(9I?^hi38s@7nBQ@ךRQZ( QΌ~TA@ڗc=hcҌ4:Q>Iڎ=hG4>g93Rp8hM/(I4~dΓ4{~z~4OSQ}h~&Ǝ?Z1@=旯jC~t{Q8_(=)*'aڏcFqKh>t(hhi8>9ғ?8gғOΓӑG^C;4=i?(2>P~"z:`}(>ǚ=yA#ր~8'g=-)s1sF}(Ǿ? 2sP{sS}:8vi3$Tg4qҏh(;Pis'4)Ew~wG4v@ phIӮM.} Ҍv= :֗(籤zgQH~qphZ4f߅.1ץQN(ǡZ9Q8=iy'K?3F@(=h~4`P x4\ ^;bq@ KA֎}:Ⓝ@ GF>`~I?ԸFO}(On)hwaAN}ǽM^is@ LN=sFGBE&GAO^`zƓj1cހ p)2s)hdQcG(*/}Ma8(ޏ( ~z׌@ÊCr;phgޢ f/OJ;(Alpz ~VU"8)N'أUDyHOd4j\$$ay6ݳrŲ?JRBnAHgu>^sqNvwa :֟C+M8{϶E!sUAyPH2L di,pvCc^q):;d júٙ@xրF%)85R?wFp4@'@(~4d3ː)k6N7y!1#0+݇q .΀j_1zClp@!ap =q@ ^0 7NŸt`+O@` }N:.%>^q7 R.ǭKێ? @6I}Fi:RRp2A47,8\@F)9 ӑ>Rs@JlUlc;I4сڗv(I#ғۊ9GhG4p}~Œў{ &޹9x{1@>KǮ~`P!0(;4(QE}?{Qh(hp}9 Aӟց>sڏʏƀ wM&=h?/u:h~ttRcgO8/֓4Z_aIQTs@无J1@ב@cޗ=(AҌg-.N9gqHix@ qKBFq\c'nh9cK(@ 2Əƀ'Nd~4;NФKm|t?Tc9-!}(PI&ޓv:u@>4!Rmz.'4h:d>g):LџL~t?*>QZ8@Ú2hG{}E ۊ: 1~ǡ4`BGphM0w8M@;<:c4 &ybE7M#P1lcM {sRyFc)\ߕ.f8$uaa@ϧZ\tғ99A>/AHGz_?J?('hG)3/;uJ1s_ʓqF@rh$zQӽ':9\sѻQRg#FO_Nn3s@ {RRE9@8֗?J1LR~4` 9ȣ@)\)vyZLsɥ4(c4g=JOրϵGǵ #w=hΒ3QӰ2HqP{с@(E.=(wSց?*3ןҌ~Rԙm@?ƀZ?)8J(2}8sE&=ހ~'}(p='dwj\(} Z0a@9 g4n`n>''>GI4/4)r\PF~vRh@8p)q :Q>GIvPr;tё@ ׾i;>Fd 03P0v?.@֐`R2(Nh'z9.sE iqIP }u#4}3F=E—jLqEG~Oր㹣z8bL\GҌԧ4P0(i842M&E/"qSK@ŠCG>ؠAaF[lђ;R{J21@ O?}h#֓lQn8EW&V9$*>NA#*0ƢSp榨&BCncW`3S8* yA:##"@+3QV 8 A,!vX$=eGQKJ)RR@Ehv9#qYz`h`.9 52zT>[.sL s֎$LB?xWOgfȫ1\b$ o r9 Q?$VJxaO}1_.)y▏S{Qi=9JQרn8Ύ{9'җ⌊_94N?(8Gў(4}ϧ?/Q@qK'J8E.~ҏu4sG=h QQ4~"җnh9/oj9@^~4}Ҏ}ZLSFE-'|(x1IϨ斀9~s稣>u&G9 Qz3Q^(hQ)QL{9q?挎g#`R=GI|P48}h3G${!J=h9 9(ɤ;bړw(E@ϭz@JNZ9z\v}(P0L((qG=&=ȥ@v (tQH}@i@a:}q@p: 3'^h{t'Fo֗S?_j^);qGP ǡfΌ!Ͻ.}<`QߦhgG~=(`H#h##g4Rs(( JsP(:(GJ'=Fh=ph>} F=@v{F~`j9-~4sʎgҀBh=:f8)G<'Թi9??Kj;PLn"=z\{`ތ;4: )9=3<}h_Z&{bؠ#ޓ#ߥphJ2{r?-:b{Qбތx/#G~i9} .1ӧ`?JN{gӡF?@sZ8I1\dA\QccI~nN)1\((gF=I4cdQۥc=zg~tdFGb)s#Kz@ /L{~4`8p=!f@1zgQ@={ ֓z0h0){qA@ R;\;3I:RisMөJ1h=Lg~J?J:Ǿ)q((t֏~t OĊ(?wz>Y8=FqRc?J9@2 ^O.5*,M6D.@"ǥUX8ރN8"=h H3,\`*QZ>4tJxP ;ғEB1o]9 (9m[mStM'w RM+*|nJ\4q W8VҞCC^Rxq#@:l hJK`ɀ:6z .%9>9feq\PNr9cTsMdWg#L{VThRdsޝAV_0۴0i9oƗ@SKIQ@8'"P3G?bQEg (xz0=hOjLK}PxF}hP^h?:3J0:=nh{=}hހ P ^3@4;?cG>= ((>R}(=?\P>ь@ uRQM?TӟAR}(/&~s(r}(c;I>iybqM! 1{KI: _@ޗQӚ?Eށ ( 㹣z~ts@ ǽG׃@Դ}EGZ3)2;~Q3G(i9PF:w4 vPwudPҎ=sG֊c(o͞R?JȣM?1p3s^F8"ғh_—R}Ez~TG)NpM\Qr:яQ92sG4rzr(?paKy;s@=E=<_ހ 3Aph>cL4~fʓc0}1K֛;/N~sڀ J3-)8z2>g@֎ P^(9hϨ8G4g4s@ t{Ύ}@ G$|\wyyS@y:=4qAǠ4? >o€:vT}GhҀNNxF3Ҁ7K:OE {~9}MPϵw4; t>z~T~5 R#gڥϥSԤ;`Pۆ7P%kB@@hYFr{t#$#:ȭ$@q*AHj9[/x6ؓ#URg'ץ{PfTkE.3{ԟhQ_ L yd=2i%D)~"esRH jA֣FڡHI Dױ)aǡ԰ʳ$SLKlD\KWYUaI,q g# 0j@H{v (qp+AC3@n 3E;Bw`pdhO=)Nv;#Ÿjᒛ *_!{wqG/N€ph“j2qƀ\4u9g4LcJ\}hPx??*\ޗn} 8=hwP~TuJ^ 4Q>dTwQ@_J8Z?Z?*2(gZ\fnh9c:?I旌bϽ&=JQO€){џO2}A>o4Ȥ=qF=3ю88' z084~Td81IZ3Mюhh9Qh' .(),&4sIxJHr(&n{ (lyRO|QZL`*w>~?du9)zv?z^=i9qғ8Q_K7#4cz:?^zPuF;ё܁@ c䏥&u&f(`ZZ?ABtN{ 1FsI@ h4Gցaz'K֓_ʀr]ޟ/j03ҁ {T)x8(:QhԘ# G4K֌g4o֏Z8hx~4Sqh}IpGKzg9);dJv=zN=`ҜccbΌq=? hGӊ^=3@ aF3?1K@ 4qKzNuhcޏQ_'=>t)ȣ"" 94O@3֌g6F80G#4g=q@ (78?FF{8sK3G1@2ӊA4c@ =)61(1iqZ2q@JPGjL 0;f3ߟj_l~4q@ &~ >٣ u#Rg?1r*:ށR9>h'?92>Q9\(xџcFGh= RNh8RqR_ʔcdP1h w4(\یh4q:PϵF~*NHzџZ>_z8(zRvў8Psj)o<!݉F;57;1֧C!<`ASnZ2ͷvABS15Z>a1cb66qPZlhbR5=V:V!GF.֭cMB sDR#9[>silD Ras0}:`OS .oZXFz qKBgҏΌ@'5hBcbGc"c𪓼iu2njEѤkw T Ğj{@>:# KOI=x7lOo:' C1Q)`F8$S>s3y6=EI')5 ǖ]C9$Uy;A4<0OmՎF##).6P#*jq)xJOzh*-2`t]6"N~s]{Ovž (RU A9MG5'9G L^~wHihO'8RڀaK۠PE'~sPJ?ZBGc(8F)ssKϥ&}P9&G>яj:v}(Q;"֏&}x4d1^{vɠs֥ihx(IۚL~f8)> L//֓0(Bi0>*;8Fs  _ƀQ@?9j9`/4)G#րh֊(h2z\P Ӟ)njJ1K@>sFqE 7cƏp(sHM->sRxbdw~4ui8K)8Ҁ{b:uh?Lg>q @LQϥ-WtSpKg;Gz)?~AExzH4B 9=?Z0{LB.=i4Nqߥ;j1[z|/j,;{{E@C0c +N0;P~b0֢dWQ?) O`hǠrpA5o h@Mv!#G[5'N  1G#?*NE.= t9s@F({ MҲa#+N;żsGN51dRO,TNqQ`99TY.A}:R5*TJ v@8 $3~tqqbu(TMA%*r.?Z\{~UV9 Y" Azu=)5`Ajڮ6yau1Ȁ3v 5{ӿ*@&\dJB;hq)}9YyUVE rǿ)qؼGzL֨W$ub dt*1,&KP\La2bX @N:uԋsJq9^OhqF(ϯZ$UMѐy44?AG=4~BZNJ=h=qz6P+}h9@{QKIfʍ jZ2qQ8?JNsZZ(h9֗J)9>߅ -%-& c𣞄PG'ր {~T4q31@ ~ABђ8=?AKZB==(SGCJ?ȣ g;yHIRO>h(ǥc:\~$hHriy=1`@ dG^Oc@ zQdྼ1无柁H8}hh?B=> O9?(%zҜQրҎ&>}Z)p}E@ւTw⌯p(#ix2 ֐88w}4zwU1eٖ'?X;7 0#"l_Zv W `gHYrF=aec`+cMH`O,4.C.9_Zm"6 @F@Ҟ9b( R(zGTҠǒrNY~` :SJD'J2;¢`<(&?ɦPA9;F2i3>F.i7Q r՞q C*40.`և #=Cr2Cc?JOj aIu"^j'L+;mB0IUGOxcGI8QߨLw ߒ9UnW!cB?T\Uq#uwڄ}Q~xb9Br)bޕa } HR06,#>LL0,ꮙbpxBHA\@*c3`1ͻ3ҟQ"hO`Gծ6)?7N8ӮG)r*?'*Ӿe9R<-&v3ggs횭LѤYNYeʞzwďvߜc*LXKxyn ~ty8!~Z1G|PdR(ց\m4nsRހ}dzё@q&z)x<s~t('~)sR{\ތz:v&=hݞ1ϭ'^4dǵ=)ZL~tZ9ږ'g<~8>qFVjLi9җ4QS@aʗ 'ђ;@ F? C^>g4P?0}h=M(֎Ga'~ Q”j9DZ4ǭ!_΀wG/֎h&\AEos@z61K@9(?@#Zg[`8OJsh ,} ǰ ?\H[{nEj'ЃQD{wDJc* {Ul@r1?22!`pO9>s`cu&%ߏzNAn[n\pƞH#9&#pۏ 纑S-1?7 1F}sF9Fv;aI jv{Q̻a@46R29n3* x"T2G j.s C2i< rq)VfLX9f%EI]2N a4F=K"Rc#Ӹ= @W-h:Kp)3^ 3) }baؑ!h :c qXqК*U/ $2ΙczT o9Ӹ4Gi3ZT0H!F*jOq0hDZr?Ԅ!}) iݺq#@ !)="2p }*xb[>QV:,qVу{⩌l҈"i=:-DB˸99LiUa19'LAKg}iiEf ?HsOƌQqf gJ,nULp^QI^ ˇ$*pP8 pG`~unTΌZl+.2>:xiP4sצGQT}(3-KGCGҀFLRG#.A@qKGғ(SIŒh K'$J2 QLLqΗhys:Rbgh4QӾhpGZ2j? 23@AJ(Bfz0{(#=i)qI@4>R`u?Z8oZ8CGSӥ(>ђ8~T=>oQ@iqHz (;=O/=Abz\g'4=pJO{R~4 LzuRDXn2vrg_„,P(iOqʀ=i={Ӌw6]09ǵ,GrM]`#''~\oVai \c)o:Y0{fʓz1EI30OM6q5^#=Z>בQvHEXۖÜ9(G 2}GTx;1:1G>/"8RI}c#A8olyMĀypseF"lj I]fU0S2㜨 [țK9  sZ^RpRb 8BISHDU[ IQ%n05Iؤ#ګ܌C8gUC:q=InJܔg . =ɩcUIo(F m፼`ޝ:34l$euJY Qx4K9\C;O -UyQ ' .sC^%vS\5*yvQR=^C`Η>?w\I8gJ)zbtKzq:P{c~4{R)yC}j_8qGOa.=q@HOoFv8@ 4OF3(ϥ/^4/Ǧ1Kꣿ#"?794~4r;td98ujzs@$?(F3ތ&N:P֎=i:hIӦE/G-!4sP(4cdGj2}E>gKj1(=qFO/Ol~Thqs@}Qϭ/A8 3Kۊ&}=*3K@UhQ)?f}OGx2z>ΖQH}fb}(ʞ@uP1x@>2)h4sI41Fq$S#رc@=?G, g,ܟԐSa,N)B'4Ȉ j]݀5^?9ߕՌPzǵǯF)9S%aP̭q82@ÏAna#N@#ޫہ\c4468%c=}j(xhж =*X›o0bu:wh LPKρլ^!!ݟNC{t:^}?*N=3HBg*&C qSg(hZaCc9hpC#zf ,=?ҖG }j&=ߗ~=sV*,#*Υ${|ch esw)2G`ʀ)'K ap;AL=NUyat*})ݾB MYG>i0daq~U]U_vV?J`(Ri?Œ/N(rhH}џJ^xxLҌ;9=@^>&O>99-&OZG~vc3Iϯ>&IF~2{qGj\Qۥ'nM/@ QcAz(ގh=Q{Rc~4zRcP׵擭/"=ȣր'֓hz_? Lfԇ& K~4~4}(פ (QP 8RQϵqG>{P!qE#0揩4`zgҀ wqQZ^ 2=@z @)0:R?J_Ɠ?P2zHϷAq) `g< 0?F9?\PO@nFyV$K - 㰧GAp $lp,`ӎG:@oG4CaNh#h=)ߥ!w Õ c_ҡeٵXOR) 89?6q+.Y$!T $zcW8E@aެFqUt˕8 RцEu!YUkX!AQ?:4tU6c)=?*|˜G n3OF P[8`_pv }j$,._2p0QF:@9?.QVCFAA8o4lLw&6lpqO= @#'=E;ަzzOpa4G9xoրH4RshLcF>`OΗJ==Ҏi0RGIzG|qKHp{T)1gG=s08.3sGL\ 19(qJ9Ҏc'<ƀ`QבP p/4v QϭcފB2yуG Qj8Rr(s~4>#4qҀJZAQK3G@Œq{R@ tRE AaKϵ~?@IRzQ@ wtƏ@?ZLwNhcғy=K'H?J?_ŒgFɠ>Q-OQ@??Ҍ{cG=:g0x$+(=0sO`A/}(}96W$95)jO2@pqOÚi(WʹO$(<O㚰sR֢q0{ 9c''R8?F4\G:T& $w49cךuEӜQ8 2{K &3KEC2JSܞWG՜)wlTHS."y.T߉pVHg`9"ves֫vךw ^8(=ORRbzc~^tMDc;]oPxZpHR4;\dewg4sF~{~}I'~}h:1vcq@1F(Q 3Z:N(/tNO|}((r}?CGN}*L}3G4~4dӿ:C ~@-'~ 8Fy4sJ~pⓧz6b1G>R4z9ǭ'Zh~x1@ Ύ{Ҏ1hCG7O“13'g>4 ?i{vI~T} g2 Sh ߕ&1ҏd@OO&/>g 3 ݛz۹⦤tWa@QhTTJ\P21\gDpw7zvL!7Bn7$@1%sRkh (l ynR7qS˯o OzY 0$穧yKnqS |9 r;tp(TI4(RݷU`oJ" }9n RgSBBDXN稢MH]M=c09u >(̬Lj=S|1lh(=)y@&Ol~T0@~tIoLSFd#/֜eLÜqJ`IeN)R1vy$}@ʑxEXϡv!g˒2J#1ܡFxD899jNOJm!FrJ犛=≠=ij@Acc>d.G3F1)e]q@Z{V {ۗU< _38l)7NxW ͳ 9J}Jdw 9TAx l;LI1$yir9O4U]*V9~!B&9Rc/?O‚ *⌟O֗?Rq?}hiz4_E4sKHzr*:u4(;_ƓҀ ZN~>dԙ8֊1jZ?Ch-@t\^J\уI׽z/>=hr:tgKJ19~Rq}(q룯oƌlP0֌{R8'>~q0HxQ`71Qə(}(Ǡڗɥ>)xր gQ<[iSN*LSՄ]}Hl S3<\4 e-81I5PI?t g}@GI N;F`i$rr{s@&ryHΊɧc>( /4壨j)`It.em:5V] Ü'G`>^SЭ ;֠r)$ʖNaFBQ؍^Y$i#!Q1FGz{G Ĥ i:9fT\az*zB~t'Qipn꿝=⎕Rdې8o !NLA?0G eK2._1MNZ6ieBn`Ov*)yb# ǘ4g ǷzxtGTw QU0'ڞg+F5X2>❂*D;䟥/4E}w=1M6reP i8j_BsFr:wMOpip(x;I$ mȑR{T)coH }SV4Y1`$SdK*F隮,8|ÜfLgbF ލB}Liӫ2Uxcy-?ugcpOsNb|cGz3dlRhq ڌftTSdB8 B c5?^XQI֢/"wm`c5.@zQcޢ2$Ì~1ww(R9.tSp^y)7{@20Nb{TXJqH.6cڋ6})]2Rϥ &ǯJO¡20ߐ>'h'z,;)!ob=AKϜTX `*(N>$ TdZv :K*QUJڎzѐzךN98p=ܽsFYF@@?Q.%VKw āT#JRH°\N,OӎiE2FeL InCI,Ԭ:rxv\P;ҥwx:m$e4 Bª= 70\RCtcd+XŽ}9$(U-*_0kwP?0E̷sU܄B3ǭX#H4d*2JSu95?=FO@l7p*a3:KT3adX; 4X,IҎs gf&w 2h^Γ(B~Qt@ ϥZC@>cʓ''hqGc3؊2sKJIQ@ <GⓌr(P@7 4s8tL●Z2=()2>czqF'\qѓϸf"\>d 2{ 3@N{ 2OMd{RP?:1ƌ?;~Hiq&?8恈Ws'Ru(?AE 3ߝFd*)>&@Iux:m>}UqɩzA\,C0M%  ֡kTuUg֝t3C?s&?2HAO:rPHT$ <+1ɢ5!X <>kdw%m3y֘# =꽹NǚۥSydjj!c}Ӏȫ=;Tf2;;c۵ cڌRdZ\{5^]Yw:,$p5>=B uh?t{RiOP>gu }Ԡ}hǥtu깣—'c@ N`Kci3zQz:zc1@ /'1@J? 94uJNF3F8(z)G=)?4s@ϵ/րƏ֌ƍg#?_ƀ NG{z9<4GPH>Z0(~ӻ>>}_b JOzu!@?ϵ}s8'lP=E&j8hs6?b('-7P} )8ǭ◯ZN{4Ҍ {QQz^ZOsQ:Q=:ciN(3iy4w>@~4w.A3Il8.;70؜~.q+20td #QC46 5Qmcsݸ >be$)= 9џCQŢRF9'B#IMCgX!MPrbs3Od""rJY(c>xo1cjpDh3MOZ#yo+vlIRt $p(DVQJbr;լ:(2YL$TqC` Z=5UdtteҧG6ܞ@hRUqDe68«!gRIdS/ZU`"1>jV_f7柷1J.e!CqB[ufv#nKɣZ$(aO9=֪q‰ >Ry98qZ};b%65o G.8BH&2mǭ!phay!\;RK$1м\qқK` Oa}\{TdV8\m!K$d)!ɐ`cO֧ǩ1P>4*AtcJ-vQMIv zNO| \z:#@ VU!2\FMZQNH J@:TEs'!3GP9A2c5 ||Óh4爂j_ژHRQGq;zT2}(Sd#o8TG"g9qU ,M#0,P;u61bdܪNT@yղ\:RTX)@^AB:PܹU_Xޫ3*Pw$$:V , 7fm8%C#ڟABmXrdLX(n{qM?(:1(s!Ud:yfܘM8IgaD1hM($T<(jo5Ć< `Oz]|PjS朷L~Ҧ2hd4P3烏Ν@UdY4L=ml}1 iVH-Ё[d26[ : lBV7flZ:w L{PsQ=(ސȨ$m39by8*9cYbh@av x#A2YR(UxnbG{eZ2i<h09#4&I 9R"WF2h*3i9WnsѸn\ҫݴg*w|S?bފv#8L `Q:S8fb8GԚnqҖ\gH}(Z@?1TR͜Acl*G$O0@EQUbLR P4!X|?1󞇏J$no@ɧ#tvM’|Q?0א&3MEu3"XkARA [DZ>UqQG3giaj<ɧaUIP}*W99#9SA4j )3IP.@='׊;8Q<.O4sG ƌsN:@}hϽJ</AIGFQ׾(M(|⎽( 0;ΌP3Hc@ њCS0;@ 8*L{_~) 4`{('ǹ߭c IvH1@ ׵'L.}h? ޓiAzRs@CG|Q'9i ((Ϡ.h= .{4n'?LP133((isF}h(#=htbc(#p(/dR#+cҟ;d)p֢WB4q/SdVpHZdl,q3X7lGn78-r9\t11$qOu$l>`*03Kϩe{17_1blpFx@K${&pMM 5Uc؃g*Q1BvSl8෦j(#XAgb䑸(,!A:SbTnJd,%"chiv}*K* Tw79=:@P @9$NXb`p8Gc;zU:* c'ǥ/_U`OlRm\PTW '#ҡ C5DD#5*SS(Q7IHrYjT@9?Z4B ~E{(B)7}3K@'Oڣ Ty$;h=:6:2WQ.$b%9'2 7f\$͞([{Zr/QҠU`WzBBSb-`1ÜqOWqǹǟ-rIdM cIVx zԊ˸QCa />,P9Ȣ/eHy `=m{I3ܠieJa)R:Ǹ0*=M‘рc`2KZq 2:(m *2 qK0H zSԱ䍽?J0}N)r{iN3JLJ3 `xtc=4s(P11qIϯu@HHHրJ;Rn'GJ3@⓷j_ÊZNOj>QjOގ=MO4vAϵ'Oj149GG'ixbQ~t8(Z\|?J^qIu?ZN֗J3ژyES >B~{d1@1dwŽ/nFxǦ:B{R؟Ɠ!m/'~&'}):4}i:`C,cڏCG_Q@B7 @R{0Miy9K2UW'3@ ڗQgǚL:'0OZ$R(gH[ڀ{ϊ\iBhyhޓQF}Iǰr)9qG|b -qtяnh;ZNhG@(ڊ_ҀP98@AP`zTtǵ 9=?\zsΔ`g$g }h )@q@Ѹ #gҝϭF֎B@/ѓz\ӥ''G@Ú9*L@К:AF=sӭvRn8q#GgF ($GJ?^uG &;z>tu .}F(@唤̃9 s\J@Ksh\L{™e K9Td~Ry/@яΌѐ:P{_OZsdSdcv{ 2"^=~ucOƀ#7Bw6GR}1K@>0%xlHT-S⎜1@ eq*L~TdPE"c+S}(UB 2qth.TOup}xQ(d ]q~5T=T9Te%c~&$Ќ= 1$~`_w"O Rd34g(L~ <ghFF8~֏*1I*N~ltGҏʀ+`S}G ?.* o XtJJl)y2HEч 6,Lv/nN)Cu""REK}qPR/JYq%KڤϹ;@ ˎ>8FsRsq rociH!'NGr4B 14gq OcMϥps847.@@A\q"~{q)}'3ϥ'~bQsт:PIshcw:ƌ*&(E? :r4N3@ F(h >>߭&}9ҁ I 'z3@?:2>qdw ~tSp:M H3 tRf#?OG?#P*0&2N2G99[(T 01Q0 Z/IG&}hpyw8"IQ@ ׿4ɤϭ)2 ?Z_`hP14q(h@== s@}>~d= N8()~cuA: NOA@Fy4t1iq (;?*NqҎJ8$(s8?Z\GҐzܑKSyqI~i:&O.ӏQ@>N{ьcPG~);8/4RtJ1FGϵ c{ӽϭZ2GBOҀO/Ҏh Qג?Zq68#F1dSqPh8h?zғP HhԚ8K1HG9-F (h#Z1ǭ&29oʗ@iz֗_{qN (~>{=x4po€9?Gn/df 4PA>ړs93G>\Pm$wqKPh( 3Fp1AZ0)2:`4?G_Ҋ1F9ϥ/qG=~_J^G?ʓn~88J>/Ip(88CKs(Ac=hןj^?'c1@ģ>}ցh##-&;cZ8.=0)8j;t=ށϨ?=Kq@4~gdZ/I鎔g@z_΁ Qۥdy?J!J94gZ?FqFNzQf1oZ^}(;8&!8݊\hq#=hZ3i7 ㏭)80>”LQ</8HsnWN(3(XAyt(GҤI6;UQʋy H=jw1$ڶ#XFKcH=h_I@Q߁WP|r})np9f>dzџ@i:RbezT t$e4-CqR_J,# !$gNX`ǭC+C30$sB7mH'FFGZ\DFFj[FZ.fʉib; ҟ+`L'P @/}vVC c@),ܞZZLQJ@){S]hY EC9<`\PcQz|ž/rGLc@'0;/ x坧 B [JrXԈܝpUSvpyx!bX}qJQIf&fHeA *X.G3q[1KGpC:Fy4j6)T i[;H(\1@#җLKN)3?*^4:@J\sHaϭ&I=r3LgE#p~F3:AKzړ֎>)F:T`t(ҁ :f@3ќz~TcF=y$@ 4#4ctIϧGNьP0:dE.3ތ}?:2~.N9Tw82r@):?Ls(8ߥ)0O ivLz{RᏮ)*A~`q?>t>ƊL@ uHqp 2:@ `?4J^OJ\tIϭLz4>zȠ!$v/^(Ϡ OnNG?΃'(P=h\T/?'F=9 ; ހNh>Γ?:^8P4&8Px}*];j19I{ v(P0{~4cRc(8)qGn(ws(!~c=}?*_(ғFsր֓\P~s4z=Z0ǵ/4d}h=;P RGbȚ_ʚqG p8 ?#G4q֗ 擧8Pxj?2;*^(#xq'=R(8)F)8Kz\sMhϸ .hc4gڀ 1@=2=q.'sԸ>Œ1G^P/CGz0AG=PϵQ€A=*32n_šŒ1!GNtQci2ޔv &Ӟdw ?CџjR}h;qIjQz2=h4`c ~΂>~M̦y# 5kߨ s%0 %>=278Ȩ F3lbP6=4(m'H" ݌vp2A@m!iLFIJ9WV未#PVX`$`/RVLc=GdyғH9^=sT H-ާlUkdEh<8@8TrGʀ{ 3@zNsUa?wFJhx# `dNA#ڠ6KeӢ`c\=h6C.ifK.^h@T-s49.rzUpsКD"c(㎕$rqǧ/>ri>ʎM q {L@? qi'Jc4珥;>E9<Ӹ=qHGTF}m$Um+ɲV|ԓ40b/[6RE]W y2’MYIZp X$,Iڣ(!'0cR~X,,yZn'gS:eXsOȨ%1l Kq"uT*:TRY ndֻsL!UJQO]XYaۥ$U)ZKa-c=@ޡdr \i!"c$;UE+i ].Nrxղ8WL6UF\Q׵LRshE.}&~qF3ހ FOz3ڗ&(;QϮiyJ?րhhǭ8<K:Qzњ3@=gPc—>Q@^=;#(8i()yJ;wCP0rz zsP A?2(8(-NKsG1@AQCG~)ϷG@ϭ&}J@DL8EOǥG|\š .G/@sJ~''( :Qϥ(ҁ lQM&ATҁPcLdPi=Ssc؟zBFI)A{^B('M]S@2Oޥ4sQ8abNJ\{(1G9~dz"h֓ؠ=:^"4?J;{8>џ\b}i3h{yf\:PJL4`~Lџj\z(}Pq\(1{N&;#=GJ?t34z 1Fj1(G@Nud1(P>`P~@:}=rh\9~4gAhtqF)q'>);~AFxG=x>t8>@&=x>ʓj\hǽ'Kh4(ƓF1s@ \P8O(Ͻ4Kϵduџaѷ<N(8H;@ ZN#\q{~G;:QC98~(@sFh 7g<>`z })p?t'.h-# 3A׊jLנC=)ʃjn2?W@'83/~9M!}M.8(6?{[I5lz΢I T;NQ\ eFrp*eBP} +"0uw)}A-˟9!K2#5UQTh ƫLx>ӂ8US,!n x|3֥Mk:'ޡ'U?R3=i6a R[tR0MB!7)략b4 Ut4licBZrM ckե6vV%Hh:i:g6GA\#lt'lm98-mW,:tO,sF/A0,_uA1SlY'l( "( 9ԞM6UVے:T{JZH#?Jl!x/QyK݀M+ @qL ?яzMF=qHA)vr8d0JX\>ϻC~Z2Km1`zT߅G/ʄg)+,B1v;*y5PUVb!`ۏ9jJY`GCdJ(OJX%7aZHDHh)қ,k,l@ vb)zUt*D=i%U]wDdٌ6;%F#>nDs׎E p'idgIRHȤ#?qSm8sx)7ÕRc-3)4n 'rRKR(Z?(˓$r0}gCFQ9&U1UxXǽVO|V@l#ך@= q돭iPx?Z1F(y(ϠhZ\i?O.}(RQG>'?֗q~4g4t4}i2֎j^EJ8?;>擷L4G>CF=/i1KPc(㰣J?Gq\PҌ?F\P3F(ȣ.*1I>i}Kǽ#=yRc1Z/Fs2IstiqQӵ& }8jO+z^O&=ȣ (=(/OL)N:s@^Žs`㠣zSgQ&LQJ^Gn('o$3pl-&Ce`O"Ŝg A4ǥ>QؽsR~r)b'ށ .34nG}((zSYv P2j0&0?:i\v5 IO# fi+QmBűKfN᳕G5nVml;'*7,-Wo:jx]3a$P, 9*Rbj1s~T~z^ScOJ;ک'>rHͻ*rO>Ԭ+2=($fy}Ѓ23V!ghHX?QӞi!sQǥPy/c31@ 8 W|w#8M$Y=g^NtavIsM;qFx*4m \ t󨢔I*Q@=f2 Oғ<MR!R=MlQҐڋ69^ikUh.e cՀ^Glџ'/hg=3z):{p(y'>c=f =㱠9qG=i9^sJ8{ւM(0##րPE&1G1@KU'Nx4ؤ8^( /RdJZ3@qGGў>9#֍E1{'?J3Pg/&ʓ|(9c&AQרڎs|QӃ)NqҚvPZOёǽ(P1)q1RLZA)2 1i\ю;ԏƂF(0O&)G)1PsAc4gޗhq_”(G:(4^uϩ1F 89F>(yoIzQM{1h8{тF3i 0&w@=GҗL"g tir'isK{`9}c>rJM~df0 ri`gʌP ؚ:JV(9ҀZ]}p;ft;.=@>~whہy' :3O@ ~4g>J8"o9AIB/C ?8;K?J>FQF8808hǽts#uh.8(hǹ4~1G~ugցJ=уwm 9ڀ (ސQF&){PE)2O\:: ~^}(4`iz LqGnbIAߥ(4\Qƌf:hHF=y4` qސ^#&GA@1G֎1Qz94c4dQG^Ԙp8iqh?JPuڎ}KE'2zQI.|ސ7Ͻ;Ӂ@J8t=qFF}\wΛ֝۷Htg@*zedrfI"ܪ\m4 YCWw 4HҮ E[<ǵB#hc z)dK\.4=ۮdVx Gt2͎3HR$$fgq␴hLnb!9jq$(>NGV=jN;L bağK+ٷyL8CN7*E6S&9AR"+@<¸$wlcj5h}pbt})rsJB)BG?J|^)bN3SΒ.0G##@M#0 ֩XO!ccۊm^Zc,}j6ˬW~֦m9ѪAЄAu7:z1~O|]m2DFF' dR4-ECgpp=OX [%8BY=$J2{Q׭!j%p^I7*^M* t8{HaAȫ wjSp(_P=jddpyU$H^&Y4nB~\zP6P#Rs$?or1PHE$-F7$NNI*80qڟsC)'5Vc35kx?j@Eq`۰zUFli@PqOs2*XRC"ʶe'jj2 jAlc;xU[if V9 /5"\ @?-ZIsϯJYv@ ~5g~UTI Nj)HS,7p nKO_ΐ!`B;LSlfd TѶ~̪?COf/.·$eG똌mu1ZEey?(= jͮ*QKn !3tY.Nps)e`l2o#.Hg-VAi=*:u" a%I2ANG4}!y\c93*8˱rH=6Apc>egKGRyS|]T Z֊LҌP'=)}(03?QyMQJ1G=Q1q@p:;t4QQF)3(yR~&=I4cMf$vG?:A`wtz?F 1hcF=h:ў8@=zNG|AGP1:юhy>R`]џʁd4>ߕ~֏ğ _ʓ͏Zw׏'_Z&isށ /Q(ǭ;GBSdE/>ө>OҌ{Ҝ{f? CG\dRg#1J3\fRz;FSqߵx&OzGZ@&y.zG#P&l~t{3LBCǨRh2};whPF=@ .sK;=)rFzo0 m!^q/APڎ(o|e} Z?:4ؚ: ϸchgO :G#@A9)s~4"QJNG9<sIQBhRh@"ƌRG'>Ghc0{Q۱{J^zcQj>vKҊ1GI?JҎΓ8ѓߥ_I^P~tv֌€OΗ@Ӛ9#h/GG#J0hM/i??N{@?\dP|◎ԘnP"K?cށqؚMTc@~Tv;S{wCJ@NZ_sj)4CINq@=y'4TL7<8>@9m7sN w''(AץJG&&P8U)$dAEmr2IȠ8E &)PG~MずL4>-Y=v*fT'uLXyvUJd'ޤ > #㩧f1*;s!Wd(ݖS]X*0a ʖF;Q-22(bBP$.68xH  )`(\ *XQe #K<0ޥas֫$k5ɕFxN)p:`RcZaK?{z`b#񪐱sȧ mbd("E#f6\0ݎ:qқv# jqM1\4i|T_9ߟқrEZڬ%JX߭Fn;n[OZO`8br)™&)9!RMPZjl}iц99 ~ZkP})G$rNDŎdI'j~u2) ? Rfwj[ib1o5Lll-˟Zjb+DrxV})tSP-̩'q< ֋hHwH\HB?4`gB^j2d ZECtʱbˎsB%8Ͻ$*Ēe ;jM43G$g5aUT`p"pqJ*+l=j=r *(OJ98>{sKQ@GZ=GJ -&Mҗ'IB:qt'is?J^}<4Z?Q#NրъLџ((zg:( 4uёZ=8R'3@ AKtw@NԴΓRڊZNLӎ)9 2~4bC@ F(&;h@ &9!ip~4SN“OCR44w?.=>gcŒzir=(s#Q@擯RGKc@ AQ1E/4@ _Η)z2 7_Εx*SGhtI߂iF;Q@Hc z29Z3|h1$z!=P0t ?Q@ KRry@n?Z\h/=Q?')s ?_Ž .r3@ Ͻ OI:@N3H3bө?\z{fq֎}r~}49q4bK?J_Ɠz @ wKZnGLR恋ө? Ϧ(Ͽ@ EÚ(ϵ ьQF}h(Ϩw4\(&s \Rpyl )=Q?(OJ1~? ^}GG42sG9⁋ӽ'F@hҏ{Q< qH'ւ}N(0GԀҖdE}ސgR hsG4~?=)xzQӭ2( rWOϯ>+G={oNs`֎@1U>qa6G*ph/&#jucW >Sʋ>'FFM $’zfVաQǨhLw UgG|uƠRBN*l_Γ΋㈤*aIq ZlQԆr9܂> grF@w |fӻ aNi sKitOց?SF} ڌ{Q)3Z^ԟ\ɤ84,@ 9N~Ziw"3;%$)!q;s_Ҹ"4g#eeO'4p2h4۸65ǥ##iF{R Q2)ul*@J).1@FSG. #]c =90O\Ž=y=(D$W=H2\20V8GN0^*,rP"4"6PKzNqA^rhǥE崊:&x{pq3G#@cEƀ#~ñ{T.ɹo Y}4;rB6vI 3=N:լ' )+NeIQW3S4\."G>ԧy *]9ެtGzBEn댜Ҫ3BY|bΝc$m{,:R\3_| 3Gi(t ]F2jw8ǵT$ A' {$aNh:NyHP_0af]@.j4Qpm\`/PmhAT86]^n[OjLq3)zQ掜"3 UztU(3Sz 8ȝ!<Q+o?]t5k4r{Ӹ\r"tcqobGoG'!AуJx㠤i~s#GP:rsF@}xǵs@E-'S1ԙRv@ ֗SJwGcK@ J1ړ(H€Iџր}M-~Rh:^RdzяqG;zҖtI=J\N}hw\QoRuRcs@/֊0(G'GJ:S~(bz_€ LZ?!@=:ZOq@4wK۵'-'h3F;ҁF= LA#.=3F}(PN})q x#H=8)2;4sA LwK*p=?PEz1'?Z4ҏƊLZ8IjNi*2=ϧ_J7zO{P1shϩڏ{ѐ:\Z?ʓ#4{gd{4\?Iz:wBH'4 L$~m01)i Gӊ>_Z&;rh99:\q_Ċ\s@=&RzH6u4&Oҗ"Kw=@ IbxJzf<4pz@ F?8#8ZN/} (NJLЃp:҃QI=)s?Lz8C@ ?*8ǯGw?j3FG4~tdzʌRdRցzџcA(p{QGj>3ځ׽ Ǯih" 1ϱz\`u"Կ^i2=hh϶(3I=w/hZ?)34 _c)??hܮy< v=zLHa9^c8j`C'ץIҊ@;AKt$H)q@3F} P1LB5 I 5 y4jrho`B30nV:""zqҡGp!\0q@5qƑCuy }ra#c БO1G4$ /OEb0jN A9yaq8?/_PRwG4>T= 4sb.P2G5"^?JLgҌ4SRr})/΁(8 B=@4'?2zRހx(qҏƀ F ;g4c>cըc4J? ^1@(<{P;΃ԝO/&׊^*1֓(K})mPGIlQۮ~{J\4dҀSAu Rg \th9A:y#yzlR`4z9F1@:_Ό^)? {ts€٣'tё@}(:3&O@K4'PzsFGz(#N3ץa@G9@':9RpOLۓ\)Ҁ ~YRqG hzH}ip=3'G)G{3G^ǽI}sFOGnMy'}h>ބ~td wJ2MҀ ;4tq?’FxZ3(ڀ4Qu#Ktў(րv"bx擌-}J8|Q?$ړG@Ig4Qy/IhրQҎ3@Ìh:8@ΗzC3ە co0e-;0;O)G3GsI!q{T Æ3;6㯭:ewWHEŪ*)dUL۸I A,{֏֪CHIN3VJx斫iY)GqSq"mwQQ`-sLG"bSV*A@㞔s SHn,( ۻ X}cNb():r4sHA۠'ҌhP0&GDp]=jjLgZ} Nz2yǷ'J#( SXG=ȕHv5Y_4cيV4lqc=Ei;(U5:h/󣞸wT=M(jc(?ҷm=(Xxӯ֨1mi7<`@I׽&#4#֏֓>S`q:KɠzdQΌdti}hݽ{P1F=p(vpF1ǥ4\Q֓z_I=}pi03@NQ84?Oc?hߚ1@ q89iGǵ79R џ_ΌAZCzZ87=s/^cގ1QwL4gP;&8m!sڀ#FsLamɠyhIcڗA@ R?^@ϯwPQ9>uP}Z3ܑ@ LQsրF=G?Z('Rq.N:&=@}h9&GFOؠQH=)sΓp@ G)yq!2j&#\pG6}FO֫M\ynGhhN;YL8^yFTirA'kUkuQ,aumܖ~d8Ui$ĥ7Aڬ(`ȢaRcF=1FGfCcO5 F1Ё{PxJO zƀ#s1GzlH I9ʬc!+ds=>Ў}4DCU w6 cYJUdX9eb85ab䜁櫤g훌'J"\ R1L+ԄEh`x֬}F(f9-IRCeh sGR tSx3@qE(J8?΁ ǩ:KϮ(~qG8G('\}&GPi~RahF=7b)ydF}qߧ4uRd0dQ zNF)HPqEϵ&'^J9GF})9>gڗ\td&qc◞?1FO/=)8=x"=M'P&dtȠJ^HGE@ =P~}i9R=ڀI*^4Pn?j\QPϮ}J3ϵ9QcmQ@OGns@uG(>gAG#&q8J:d9 /Is@ Iw4d€ c/?Zn}zM/4vΎ((h(ր‚OLΎG^ʀ F?(=h)Tq:thtgߏƀ A>~@Ri3~3g=G~cը掝Mϭ'4Z9F=qK|Rc c?Z^Ih1(ؠQG zQրO1FxhcތZ\R}Z9((旟j(Rⓚ4bM'NA^hOQL`A䊓n `q )u&MC{h-$ktIneqSTqrwݰTp.?uFz)bVt#p֪6+f|% ޜ-˷ɂsLG۴j˓/E Y W1Pߴh{xm`Ae=fZq.94JW1x+a+:$Ql XM8}id0V+F`WS*_*wQ~p00KdRFT8H9A78c;SAtXFH\q$$Lᵽ?>-sB)8?Z;qK;RvD,V ;O۽VjQ#)g^>!4B89WB ܚsj2o߅Ǧ(@U1z`Z5Wo[ ZSGO!\0I8J2Td~) =52Tuށ=8Jqrjϔ0=jlzf4Lʮpin!Jȹ8^OҝX&[`3(ޓ?P֌{~rFzP:P4cRG@4K֖ =dǽt(IKڎ(9@ F/E&\T֔;RPsڗI{(F}@ Ki;Pרi:ڀHAQ49`qI3ʔ?KG> zcisӭ&?hvɠ^i?P(4}1@N'8ǽG@OGӏ“ҏ@E׌hE>ǠϥO'NfZU [`IbƩj~@ڹ/$H$ 7𲌬t2; sSx=;x1y`NA'5/) ϽWqH5`T:X"p|f95R3!Y:$Wi4R0$8P0kdn2=;{`Fbޟzi; 'ƪcq#8#? v`MFHCc8b/2spN8v:{q˃sQ[Ȫ{csNHdo0({ji-g=Ts>qяѺ)ibP/ *\V&y 4lPяo֔@(?J1GnRcGM.=@?tq@Ni?ǵ&r=0)7q>NюzsFrhߥ!8@ @ Ҋh搨b .F8@\~FGO44d} i3ߧG?-(ۡ{<1K8ȣ(Ͻ>&2yϷ4 u&}1s@4A4ڌIJQKtܚ_{I恆dJ( n6@֦8_hY"&>8{fΓp۞ަP!r{~f'=~P1s3F8KPڌZ\ R~PG#E'?'J_d9B_;ZQqAϽQ? Q@ fh$F:bϨ4t=(G֗S??z?*> ~4cJM:bA#%}hn'Qzz((2;g48T}i ~~}3H h8&?ȣ?Q߭/}(Mw,~/zQGGhsGփHրނ=4dP8ތހh#j2{`sQF>@($'^hځ{u֓:Pt~ъBaz>4G& QӦ3G^ƌA@NQ6| 3iFI:GiG@x F8 ΀ ֘bBNUs)Þ"S@KEM/C\cDyLEIjN[B"$AJʊ$u5'IpF(Td\aH­D`^(63M*ӎ}cӚO-Cؠ k[' ?Ǿh*O(&6M* (<`t4A-+CJaSPs؊~`~TP88uz\=(/֎(F1۵٠@A=:_R|Zv(߻򢗌֎ &=z1Z&=qGvRbvG4G>Ns(?җJ%>Ɨ9c=q_z?;@GzN{s u&n bzǮhǵ/NԜ}(y{R`wc~4C.=3@ =iM!is@ ~&#ځP=tsڗj@1J?!'0@ȣ<;{~4uKzQh1E( &(IϿK(~4tNOZh@ZSK2=4{*{~tP T(h ccF}( P`KFx q8PGr)qހ#=\/^h4st:LzQzڏ€;QPGhGJ83Gғ{^q$dd3)qJN{IzRuҀzQϰ>QFZ3O΀'d4*3)={Kq@49QRuK^hc~qFGCIg(ӓFq Ҍ~4{czLw&QQT NGj2}:NL<ș7Ap(wx G tɡ-.NsV)3ҋ`ʚYrGP)أPQPI'֝JLp1@11l T)1@EIlKA#'d^ʒpuߕ 8i1R`;@\|q:( eRgEcI4?::BsM8ȣ7Gi1ҏ-ԹewWq+Ҧ_@HNEJ\>08Bۉ'OdҋF6=)1o8!|ڌ} ΍sJ!pc/͌ M''4c( */)SSR{PF,: e3v#ȼzc>~ڦ~EHb*]CzsS@Q;ӕЊ>4\hړ҃۽ =#րӌQ4{Խ4fQK@40(KIGj_ΊN:4g\dz? \>~-w4p;~_Əր GFӚ1׃2}?Z3&㍦(ڏƏƎ}h(q@3֎NhEȤ' SIӱ~1QcJzt7#Kױ(鞿J2Oo΀z:ztZRZL{Ptc (z4{@~u3j:QG>⌌jP?9^(sLgGݽh>˚^3K=v aaI9?;s@ {J&000(-RI.\@M𢉱~RElz48`}({?:bG]HFE8~n4LFbA3U܀zJS6><%wU݊T4vR;p0ߍY;UIF6 OVFI(#S>㸌`*KQ m5ib\0:%y$R9c*̸EKGK9SqҮvUY @{r $z0:r?!$(9I2ӕksi?8Hc5d A=9&Ǡ֪"UZ2K(,F7ҠK9 Pb[`!Hf˹eg7U1…&"@L\’=z<3|x9Υ]A'u?*k6X(Ӻ21 3)d+=Y 㚞 ZsIz5l[Asݤqv jfhRV6^29D)R"ènJ>E8R`؅eKu4yTƩj 0'8zTbUR ܣ` 2C¬pc_F}Z e881U%EFEќI+!%Q_<y*k~`FEX5鎕I Ίb͹]rJ^ʹÁD8 ښevPĤJI㊳w &v5gӧ#g$t0Q v̩ rxos\ V-v24^[c]KV->Q`$F|ة _|*(IـG`HA4aҏ֓^~&"^z);tghqASAϭZ^ʓ>}h?JSt-'9@ @)y;?٠ q((@ 2hx#Z\u9ѓ٢=(sG>fPyIzGIiIzQӧ4sK9{P~8ph<҃Iߎh{{ޗڀqAsI(8KG#i9>@:rh# {:`O|Q׭'~>sԓ@K :v'j?_G4~iOh})~GБ@ ~t@=?Z3?Z:֏ʃמi2AP!ڛȥhvŒSER81~b{4Aw} t43(9?AK9@'׊3Ґ\?#I() '4MKsLǽ 1xo֐G~G`.}htRNi (KLSv/H/@(~t~ :8CRG\j:'?*^{P8)Ҁ 1E8Gh;h1IZSt&֗4P`—4h1_E&y@>{PzZ?Gz>P =:?΁Qތ RsQZ8~4:I?΀?2}E{1xJ))h`~4)i3@GAH=~}h8&\gGPKI4dzR}?:1^)=R֐Pd\RU'P0?=sڊzfzѓJQP &KuoGnMp;Q)3K@сF=O@ _Η|ё߭ {f 4PFO@L`R4O@ \}?Z_ƀ `QD )gi~?Z;wJr?(Ȧ ?Z9`uh9A=i!% Wo?K1 cZHe2׃Oϴ]F78Rw c>b2ͼhMJ+E 3sG@@tc;, @a(rӣfw.Jc0sP,64Zl!cӡg^G03Urp╴\qI[4FyBk <"޼jە='OfpUU}Ȯ[dI Sxv|U~N#lvP dRe€cC VdhT ՜c)vcJ{/~T'Rp*g\4q0:`3NEn :chXj#vb&![ڃל|~rRE`&\L3kMAKL w?gjDW)Y[ #X;qҮ`^D@H#;2N 4 JxWs*q 欁CRdĒ:m?6i%{jx&e2nIҧ c w_Dd%WܫZ`IϨgT]˸@dG@&Ԉ #{UHFo!IZǵCH"M8#gGl"Hzg@E;:w)u6 R۰:%RMJ$(En3J$fsJH敎Usa(9C}1Hy>i 5VY֘,0E4tF0i̞ N?:QINw*)m8` ċ yqOߚ$"}'cc L:T`u4Ѩg$zPrElץ&Ga@_4~'^:Ҏs8'>/@ Ƞc#ޗ(1@t#pM9ysp{fc@ҌFh7}HR4g"dc}(ցqԊ\&GcK@&{Jp{~c1`F=I&}i>mF?ʗ8s}{ 4gxHai:'ڔcQ)1h`{?(>FqO.=tQQj:v PFGAGJ1@;4b i3;jB“\~4.3h;:NGZQh4cތP:Ccڌ AFhp zO?_ʌ}ҀQIz>_QIG4~v%~ghAoΏ=Q֏΁%/J y(Z({@.q((җsIN}(@^'4P0hGE (Gր)9Ƃ}.} "ߥ=@Ҁ?QӵhʪYԚ^e&T\RH0'{Resgڊ;P~`gތџƀ =i z\:qKSyZ_/J3)84cڎ=ӊ^;h;q}i8)y>~/>T=hʎ{Q9 !?'|MM}hɤ0(P1G&Fsߚ\{s@n)dE sF@M}(`/ǽ 5 P!}iiqɥ@MUvϥ>0.;`ಕ0iı0;ft)VJܘʈB>O;sWg&o5F6L"€*(lg5cLT)qL JWgȈUơTB2R3IloI'wGHddր*|rwzU2ev:`Jv/Q6鎵+dd'b ft! HF*PJC#?Jbi;,lPp1N4A<!T0O'ަʀOS6rL|yao$`rF2OZ>h[vp >D \)gϛmC-?=5cҁQJG֢*L=oPQd˦zQ\"Wv)OGyN9:3 v@;ie -*FN@Uf2wsJB khc=|2@ր6:$ >VR?Z8T cR@<)3ߥ;֛ӟҀ U!12ld,cFK7 tbDV*wA8qQ[G2YxU9 K@hϾi2{Qϵ$q4&=1уPbaG>h? >&ri@QќQy8'h#Kϵ7ө?j1GQG~3@>r})i>QRv\3G49EjlQ@EJL&JL4d4r{P~"j?ғE~c'z1M('4_ʓQz Oʀ'=hsG4cazPFߥ/h?Bhf;◓A(р{0j}iJO@3_Ҁ(t񣏧G@"zP9h Qԟ/I:ޗ@֗$FFxy@(@=I@cHy׾i;?*N)A|QhIs@΀xIsG>oEbzRgA yi~@N(h&{P\i0:?,0݁Kzu@4?_jNR(ձ@M/jN.hҎg4pGZ9֌=F}@ Ǧ(4cҎ;@ 'K׭ZN3@`_w$t? #~ty(|Q֎hq9?'R \j0 F~Qy`Qӏt揩փhϵ'GN3@ hIthڔIG}(zd(z1AKqF@@ځ)yҍFH= ~Ts@ 4v?5-'' x< wL~LFAF}i9?C/s{QHd($jN\R=( #h=)Q~tqc~tNUvi *QP,q& whс '#H7>IJ(Q 6~oP(HeJ1TgRTubH"l4&(5M.VQlCQV 8Ú,):nL_КjɕCD1愂Ŏsڀ3JL Iǯ viqI9 qcԊ8Am;O@F3>=@HRu40Ol 0sꄠ,} Ҍ M#>Rp 4I.J0Un;@OΝŬJ=s g <8C~q BW sc⓯T[9TduX8j@x,3¬c'{Հ>T-3Bx57,0hvfh!ǜF{#?f!`li?TXwR;H.X=i4j(ZGOަޡ՝bHO}N ~PO֪$ԔsPG n>KUފ=v44;ҎJ9B?B@>€^gHx@ȣ"8 }9uCA#XP 4qG JNxq:0?֐;Ηp=Oz8#P ?*z3Ҁ?T{qրG_Z3d?:8?j94~4dt'>٣߭z iѓP!xhhϽ $9h7-ހ@ފ? 9= 4o=<:^@; R`g֗@aG=CG f?Zw4}MLJ@4p:RsRc4zΌC`E/j;i~gH8g*ZAΗsFhG &(=:@4uӊ`/j9PP(=4~:''Ԋ 9({RdhȦ8IT(A>ԟGz?i:phQʀ R瞼(H# !A84A砣8+IKϥBp}@ )A'4qޘlڏ({~Tg4gMޣCҀ7v8dc4~E&}(Ϫ=?:L4dw8PJ?M L=(R| oM8P=y#?*2/?*?ZN{Ҍ~A4}i31њ:RIJ3B  wDZ8fh##JIp1@d4}M'~P{QۀhpތQ?j?"h^})0GLPŽ{Ώ юP3'Q|GlTc> ~>Mڌj0=Ghuz9?Z^qޛ㹤=sG4O>Q'J(=qN9?>pz=!=?U]^5%<~Ugq=:p$V@B3kqʱuUxіC m}U /7,9 vcdH7wgw:yD8r>)֌}s*>bxhtĬT?mV}Ў?JI&{G"+v݀GaVOZ!q8) pD?j#lOS!"qKp!Ǎw>SNbCK3A9Ǩ4y3NiAn%hgTGaP aݫ8Q 4-[=JZM |_^#{02КwјNN~\U܊h (n*xUsXN*‚rp8Mn r^=*| %2I]8AНRh +F"n|5&XHᵰMCh_Λ|"8 GR9HفsCCõI#4Z1*5 =6㻃VϕOPsT%CpHslҬI8Q2d^֥'5K/W1ǵW@@j=:ʰ5fޣMX|ymKLDX3KqڕםԳ0v={RB^(٦e ɢLJd jo{41Ʃ;5i >馢0O9[Ic  ]=HBw6Zu"O5+?F:lV">b~sVcM,gF+?=x /~@Y=iFOcA&ޙcRHw8}) hs8=I}iqM4ptz (u鏭~t`sA'\gQ@ GnU!.~'ӚLb“z9hs(=)y4vtw ь`t{g4cK4~4@ ۥxt(9!Ss@ ڔz9{P>nQӵ'ן€Qgz2=h3ۊ_G4}h{G(Pǵcڎ}G@|}h{Rs@FFM)ތ{N>4cu֌cKj?!җ@ װ8"1@ {\z6 hRNʁggA?*9OFv\Fw=zRg'tu3"QzP0ޓ4Z&~sҗ4c!}KILP RjCGȣ#!ZZ3ZL#Qn)43,FszLр: S0^}2{P@n_>SvRp4'Ԇ/^߭ >RR9{s8/9┊o=)h~ssI<1I=1='<{PO&PMsԟ 4sG#ߥ?LB(vސ1xy\qFܒyǷ@AG>&)֗ѓO Gq@ $QNP1;cN4ցy#Kw&G>N\R4{Qۧ4T N€\׮(zP0ގ=:P w? ;/nX{QӠQxF 8d8Ϡڀ M94=0(J09_Z/s@ (힔{P:Fs4''Z>\4p?OKcc2q@Lx(!€3RzqNaG◯ҁǵ(qhBqK99:q@'?:_v@O`&~ 9!րv{Q{Rh b{џ)p1Aǽ'^(Լv'(>QDx}(ߊM҃qKPr8Q┒8ϥ0'_ƠoFTG*}:@L%F*NaKU , d;_PqM!ʹu=/jsjۂ*-n3Dw,LUGsUjx,QܲpphiF9`HR/i|?I#5pI=*Đk&dأn)<S.21y7MT wo|vP_6)ZRvv_jaH)EH mGp.#֛F8"G?vd%qzkFX#9Ysʌ">IL?m39 ulXM(0@۟(0u 0r`wfrJfUw hhd6>ƾTHȩ`C-B,22Cd*Pp6́Մ`Jrcmt$.1O`WEf _Z6wY#8}qI3yΗђQE,dk/\ۯFVr2SĬ# b:^K7rd*!h@I̟#)$r{Ճg _4ß-qҴx(hZvU8Ej41#0 u/5Glz7: <:TI#mbKYE„~NixҫN40}h"pz62)ⓟ(<ft~cܚ:}iyRs~cw^uhrZ2=4s@GQғ?.sI8◧oƀF=4Q@z>\Q1@?AGO\@ Ҏi}isQ@ Kwq@Kғ=Ž=2iqIގ dQҎ84fdr~U.)3hz9K1@  qcpI!4c4>ʃZ_j1g:ƌsޗ'ƀ ~4F)swF9G@}3CG>OҌ@ڏ@ Is~'SȠbi=Rm)My@ 44{cҀoʌc/F=&Ҕhi9KGӹ0ΌG" lҐz>&ڐ4q-'M.GJL~{Qi{~}(88&ht@ ^ '='>(<4cڐhZ`/i? })p:`{RހIG@t@4`Rњ^=ZoN3d@☃P@J1^@ c jC?MKzZM4$s(bc~=>bƗ@?8M.shϦ0z(ܹ((0{QKG^h>Rb`wF&cJ1G11Gv@ H?3Ni3@N󣧵4֌iN{Fx4zZ^3G?㎟&=qI')Qۭޗ9*@Kq@xGҀ )}('`@.?8#P1~sH ֗<c|;xa_(֎s@Z;w u4~Z/^)1KQ ~t`Ru>)hM!" c9 hJJe?082?;' NF9t'O\Pfx;A/W>cE;Ƭ#P,$S~'9P\ )`y'qH@=hC*Ļo sI W@΢峇 oZ@NrGhpG~Ԧ;r" ]*D~s1ܯ4S0]֣TwMW@&+ `M%4 =F>F[̱rᕼpsd_ʀ;T Ը=K\0I v?H}P(<Jmqj|g0΀ }:\~?&>Rh`v3&}H3ZSIzrhv)>Q@ N1HqQҐ~4(|gҀGc/?S 1vhzz 3J1(z/җ9E'A\hNG~=(8 @Q}GZ8uGNڗJ2z⁜r8?'?1P>tсI֊9=14ќ>Ž=:3)rZ?9a@-&i2(hIϮ(=?PIw89#⒓ cލޘ&CAs›/ Pz~T`zg=*\If}:0}ht>`uA>(G( ih8#򥢁n2);b{R@}):s \fCړ /Zʎ}J 4`u4sqIVwcޓqK׎)8(s}?tz0v4Rg<9cҀҐpz~"8"}IڀKs.xKCO^ lcPys9_΁)qA@=h#F1bs@sґۓIKsF}HGҁсIGJ\zсI(@ih QFh@AAQGJJZ1Is@J?JZL; ȣvɥ1@c;G(@ }(T&;fR-F hy?&>(<~tcڌړ^~4Qץ@GJ? LZZ\Qޚqg?J;׊8I&A֠(Ǧ(q@{R~`b'H bih֌R~4phFG^dSI=4?P<ќ֎M})8GZ9}( j=@}xi8=iz4}ցdK@84uGr?3(Mi?CH}Ǧ)h4Q/?Zb8D3rj~GJ6e/QTks@;6d^@=J낪b `ʫAjF8 qfI$.z<^9"*cRJH ːqR3>SPʂGZ]F ʁVixJ\UV"Ie *wm~p5"Lr{`PBES5'ГTyb{H5gzی~@6[[w "95ZI#,~M0v1ߏAԸZD27zu'RAȩ.ˤJWirqRG"gj25k@F1!NÊSt:8$uATH72 cЮҞapw='ڦ1ٷsR)sPtzR{gGhϦ=D!uD.zI,P)$N8mhm^w uvG:qON$ l!f P@ͼ:gQj,1aؚ9D8PZˏj*bdz=3E/=H Al%|7&͑PF*I]v5Fr PTuVo`H2u"d0FȀ&Q4! ϭ'k +[_*E(3 [An`sSȤ; >!dmf g9Jԗ_Q֧1k*zU[#hc@CTYZm#%CIRPU;I"q@*TlApd'~h! QL]q(^HgޓI*^2Γ$uJ>s=(s(㱠wʃ4@R=.p;Tn8#џ΀?\=Rg=q @ {~4i3TɧH#!8ϥB̜;m߭K>jc#~@hzҫE;=Lb&zq$@zSQ2JBtg bJ4dQzQ(Ҏ{Mǵ/=z>=PXѴ}(Ƕ)y\zL}(Pz>4du'=.>FE'GJ8??\gҁԄ}(I4>vc?J^*LGz1J\~tf: O΁OΓIdqϷFxأIŒzџU.)0(8yȠސg)pi1@Z94n)9 RQހ>dZ^Ƞa94P (h wqP0'^qϵ}E$d R9@qԌ)0sҗHb`QSIҘ? sG>~Tsf~vGPqH4\gvmDO#gsڔ/P:v:v֘⏧' ғ0ΗgZ0~CE;?`'4} (@ Ǡ>O֎)Nq֎} A) 88568-r)-1qʄs !c"{Ѽe{_c)c8A O@БTd85TқHؖ`ێ3O R*w vt}gMރe5čl7=F`4" G8(cA`HZb6? P挾;PBzSRB˝}Fp9,<1Kךڤh`Z?OKhsFhiy=hϡ)phҀ$уK JNhҗ#ڏҀLzS)2{ я&y3G~p?=PC'RGh?=3G4ۧu1|~4{Q{ƓRF3?Z:?@ ƌ^:34挊QE&@#TqEir4OisQG@ Fx4Z: OAg#ހ U!yh1@ xisQfqGC@ sLc)qL_>R~q84s@~\R=n}F>|qjw?NG@J\G^?9?:(]]Hr89V@BcNT02=iCqPNCvȎX&r3R}<!Qvmh1[V;d WFH\4vA隵8#*G9c֧T$;g$fdt‡Io֔ԇ=!\/1N|=qL [b{SD}+x%H3Fq.-yQdP- ; XUU?S۸D}6hr)3ړb($d 0<…(,#aU bC+2GH4 l`p۰_֙"<o&{t1HJrġ[߁MUYoE+BRYޮ+ 2dF$PMܰFqF( {zJ:j ,[?> =(8e#wQګ<+,R7}i1ђұhΡWfdv \sdI"tf c62|6?nNR>=({q\4zsH+4mɒ2BO!܊θlr:⠝Hm8P\Hf,ǐOJ[M)rn)tb"}xemxbGn8:׊1;ҎR$ao;N?=DVeic9Pb`LiaAtftEvʈ4Ԍ ا@6cY|3NixՇqӀǵ&&;8Ȥ<~F'?w1IK8}K>hGCf cP~gQu8^1i!~@ Ǡ>lwI4 +VajE8@@=+i"$qP=7v@2MnlgnzV9⢴U/x"1'ޚb"\zSّ0ܞ[o!0,iHu+e#i.%(b|1,N[veLjXJ|E^N]ʆT{V#98]G6*ƤGԙq{UGD1A{Q :RU$CƒE(㌁@ќ Q @sہKIh(Rn֎z_b~?}! ޗQqJ@GNK.~4~TcQAQ@{`G@8Ҏ yI΃@~Lʓ?:?({R~4dtiݩ'ҁRߍL{:zP t}HsGJ:Ҏd)2@ ҌGC^))>ցǵj>QGԊ33@Qdv8Kg>z}i:qǯ/@?(Ϯ~_Ɠ7!Hq֎$vs@GEhpi9^4}3Iȣ#4֗j(ϵ탊@/^ԟS (q@zqKے(>>h`AI?ZQqҐژAjS@׊QEAJQQ?JNz_@0=)sڎ:3E!8? \{U{m7I?OΪ qz^*ITgL e Az`yϥ:Fr ܁C`t 2{hls6斀&#ڀJ(hRE'?A@Su '8NKJ zu=(t)p=4u=ZoJ)fE >''/0 ?*11]GNߕVsV1U^Mtm}TU~Sw]dY($2:TW1yAfd$vs=ЊqU#,S%ñV.%1F !~zc4#>Иd+G?L=k^]W2@X.w9⛋[h\~5$ܙ|v0ȩ"/v9JT2OҨf(ǓJheg_⡺XFi O:j\rpK w E:8a=A9qWTat'\{^ˎBp֖(r7'2qrj$B2H'AfmN$`7n< I Ɓjc&F!Z:f]mYlHX mC,!UpJs{Ҍ8yBbB(v`8mC*'!C+smV-Džf!~nW4lz8)Dp؀╴ 8Mp3biOP2}D!lԌNӴvTVu~=ǹbNxRU/5mEWwlZ dh۸4{CЎ:J#=qTn( mJGXdhh|Vm -{Qw?R˸BI8R# Sq1 A'#0 ,A.'n}7HV Ta|ŋQ"1ItM\Zϒ>E4<D])éysds)>Un%T[3 N8<RՄ4}IOzB㩣>> l#ڀ&9o{1PҎԽF|ƀ:9 /8ƀt;W HG }>cc@onWf(@:T7aL t22>\)sށ-N3@ M/~i3?8H3K{PPZ\^i9(>4~"Gҏ€ G(-~4LяƀFOc/G&8ƌ}G>Ǝst1Iϵ.:~f}*8OΓa/$ ^i2zQ3@ s;Lcђc4 :Nqj? Ob?*NE;z(ӃI~ ր❏j9oZ]wA(ҏZ=@ǭ\=:QzAR €z>r?:9tҔ@hG>&{:`}h('')Rw8>}iyKgIߧ4 ^1Q{уP!1@w9K|S{zQ?:\QA31*O L0q.(ҐgRzRy?C@4 v9`_—csKǦiN(GG>t3sKyM.OzL@!)V~j|*(rw4M;#.K1i28X!XX=iP ĎpO 8ɌvqS䴎YۂHxӥH?;UbvQ֖ ȩXM0K&x!.1nB@5QZ/;N3*97JI-R?q֥'{t{Q!Fq@NzR%b(ǡ Hp{~|z9(=s0s&|~Pcޏ)qI3cޗ(3F3ڂ=(ǵ&GNs@ /N~?/_Z(EwzRt=h AG>@G>3iyPڎ=Z?4u^Rd;Rsi Lz1b?=hڀ QGCގ3b|G8ۿh Ό=E)J~q8o>RL'ڌJLv~}(`^Լ N12:~18:4=hh4F(ǷqQ׶(0:R㞔o43@9^=h8?Ǧ=0=(O>u~bP10G֎@ ?F>ǿ4`g#bgR)s؏ցW.z4R~(qKztIG>S@_9ZiJ\q)O"'ӊ\fJ:>€i sNAG{c@ @ dv1@ uϮ)ݿQ@Td{vs}{tNEzhu n3KqRP>o~Lwڝϥ i^zgI{}1Oq)0}(1j1=F)QoJ'ǥ.rzcP=1G8.=i8~$ 3b:9@L3oΌ@R}֌)N)q0}zӾ(EZ3A:BhQ?tu4{Z>GYE;4tA9@pz擥/}7(NqFy@ )G 7 fix^^}h\iJ1Fih? 0z'^2hgN~'=R@? ^{Q@xcӿ`zPLip s@I村M&h8A:GހF9iy=Z7{RC\hI SRs@=|RKʀIǽ:#Ҏj;t `c'4{ROJBҗCqiAӷ9(ϧ4~ELSs)ybIKO@ Iџj Z?*8}:Rg1GCҎc@Oi8P=)=P!3:4~$k,e|9@Px` Z1 : 5cHؠ_Ɨ>ՍPu9+RA3C'FN1K$I* O&W4 0:₠O Q8&')xLi9(OցRuGKqGH?—vTd_ʏP;ё)}4cߚ>;~(@GEǭQ@ ֗'ޏxR;QڀӃE4}hǽ?JL  њ1ISu%'OZ)h'^~&(8\њ3ѐ{QI{)Ng֎*Bqh~dfj1`g8h9;P=I⛓@(ϡ?Z@4?)J(;}(1.r(ǵ(AB999!sJ?(ǥs֓€(s_å'd3;h:)GҔh@>~}M.8PnG#QNIގhϧ?Z0}Hr(#ŽM;ӱ894z籣cG9!ix~.=4րףQ@}F}M/"gޓۨjOZ2Iށ3I◟hRt@GJF{b(& ﲎh=(bʎ'Q|R~ghqҖ#Z2\@)y)24{q@ =hGHON .Gj “a@xG KL~TtE^{.h&>4 h#4tΎy(tѻ=֗曟ou>ҀKM(hǵ'ہ@?'?\J2xړ׏z٠s4IQ'4{~u(җj;Rb|QSGЊn}(TtdP@4)(zwQۥq@?lǽ&%A>Ҁ1:3_“dQɣj:wF~O&J:vc31IϷ@ h9ϵ@>\RI=QIhsҎzLh/ICϧqJ=4Qӵʀ zt:F9@{ϭ&H-~TQ8bL{Ps4JN~TsA:GfyR(E)na`z^qAq@ {AZ9=sP1qI8Rs@QFOr?*9aJ֎?tfLQGR}8_ƀ zG)wJ1EG'?JN\u挟N)0Gz.}G4s0G~)9֐ 32}GG8SP8G/Nzь{~4 ?Z3I}r{ќ擁@ g9/L~4u=#F ^=(ږOJ:?F3hތQQӵ I߭K'nڀӥ'FFy#?/Q?Qۥ/Kjh3?(>)zsI4q^sIv?Z? ?Q4t4c@(hP:}ix 'nގ>b/N oLxN=~cLI@ G4bbLqҎ1ȧcސךNsKu* \0*ov6shN;dФ*S|6z҄;CYH=N>\.TeR sp)^)amF?[aܪ舲G|3_ncH$gyR.U/|T݅GNsʍ7ʤoF{Ry]~ Tݪ$S^2G0r =iW[F] n0PVq!Ӧ'V(KW(Ͱ/ŏ'ߊzXnlJ)r=qp Bff=(+~emϙBdԂ͈^y"x4cڟ7\fv$<qף.SY/H[Ҝy Tl?sVqc=1C\yd{pnܗ-!Kyw(<ږ!)[<\]6aSW0=1G.Vi\L `r*=E.o!\gi>ϴ(1 MgԼ{ Z#l(#;>6suQL7SZd}sAw(5 .! ߚc<ձۜRc4_W(,dҀT=_Œg9޲2ppG>&4@Xoh94syEhXđ֛_m}`񷸫}(ǽBE7!LGJj\mB^8 \?8 j0o6FVGNrz8'm37 V*G|w ۓɌ}A-ۄ"=ry\m'96DrC=i}m^A,95gp]g>Ih6=)\l&|`/QV֗r\G)-5n/LT-{RvEe+A$7k++'YN)}h\pr$+9j8'ьAr\NcB\P8[y2}* u?p1~+j?:FX|Yq(\^,n85ݎOaև4 h\.Xmp1Ԝ^v" /8ִ9RZ'րӚ.)wR np} NǃqڠȌOV;1i9XFi "`?Z>ryJR2be?0eҜf@?t]zm_.S{ݎGps, U$pMZF. 3yGҔyE`s>V=IĒa|MXISE%"Jd&լzH0Iu_.Wn ꛅ&(}c.+ f괫u+>"uYF{Nʟio6EDuPjw`JU{hg#5C>cs7L7,ڶ:sڭ;qK=ueQF6@ d7:kdbBЪ۔ }1L>w7N* Go֋ú)hH;s}gJne6yUc`EB)2=d )}&IWpq֌qj.׎Q;ǭ"_V&@mY׎)v q)%B@؏LR[d$`zczo gZ_9zTh!7#taFj52b|U;TjKYagiEFDKU9lQu4![*-3 e?.X9y@Ы$Jw6+ObݱA#PzL7觘یV0>WAYu(ai8mJi,GrO-e_<SFSMB7TÒ# _ d[.q8H(GAh'c I~϶J@qKu)㠥⻐@ޘ/8*NpApO@+#8B7)PWiv 5Y A .[ 3qNkؑIOMz7b`)AtEsN*Sy#'jBQ;ҍ^)Ł#sJnsRm_AHs篽)~!Q^(W(ASQ9|QtKwM1*?ǵc9NG<z4 Pb +#B1VS_҅@ .dx8~N :jbu* .*=x4!8N8Ho0A§!x8ڄh gx xsgsSa{vv1sF msMzoݜr:ՅE_>gzEhC©Üp:R.AJ4 PO)QӊP6@U/Jcڗh''A_B.2ܽiHBRE{.0?*<> ӻ=Q71Zdl]v@9=H(kBtB1l/bHOGA@סLYO/֞.'I N) qFk\,p7kƛ\p*^3.94"CtGP9=<+0cLW$ Z6aBhV>ƌM ~t{HBQQMLq4Rc?(:aUGҎ&}3K=Ҏʗy(>Pip;(uhb$zSc'Au(=4weǽ/SQߍ#sHqAm ~1߭'֍Z] >@1S?JCP=8?JP='Ҁ(H(yڌޏQ>({QRi wP: C1cKuI(IKҌg&3KAր“hnp v)s@?€(֌@QQŒcZR1=ϮGғ@xoq(ǠP/z@ zQ&N?G9/=ZGuRƀ 4qfyQ'|g@9#)v}>;Ҏ`{◃CG?&I=Ru4c@M.c&z^CgQzPIhڎ=3@ _A={PcKӠipiN1Fx׊L):q(i"F()2s;bb=?Z1@'OJP3F(A0)=:\AhPF}:1s?KO9Eo,9jnjEEas1xRzs@9:A@z(&s@'CKG4r{Mӱ@ JiPNyyKϨF;PXW;A^@Is@ קghϯ4Ҁh(wgF~dt9i9$Rގ@9PqڌQ@'_cqHqӓӳ'jS{Ruh\s?:Mp>za}hA"NGz{1g;ƒdϊ4wG֎;M8I 9Q۽/^ -W {bd㌩b\5(cdu9=q&PszQџj3:P:h4gFI@P(zҎ:` >i ?Zvi ~t 0eNj~G öOƆN:*ȧt&=yP2FZ8z =)? 4C@o΀ 掜sG't/4R;t=88N(9:LczГE#sԟ~t{FsҎ}(ۚ?*?ZN@ ϭ'GKP;|أ#cs@ џLs4&%T3@aA4h@ Kz( P/ġ'SS6(8cI) 4sF~q@,B%}SGZd28P2P81(KғJ3J\fPE=3ь`_ΏP1N1֓>ب:b 9sG()yj)%?J){/"u&zQxbNs4R{P}qNF8 jpeP\ab7zzRp::;Ǵ vc5|gqMIsMe79!9c8RQOoҬPÚLsJ=E hяʁ}1ǭ:^E6;Pc A0:P (Z3"&oP(7qsF}:Bs4 1K߭4o֔q?БRg~t@@-3K4|ǡ@ ')G^F.}/zo”g"PPXy'ړ&)mߎ7(dtm^_[t"$yBw9|ʱ~{fQ,aHi??Hi9Y!Ғ0VJv )9֎GjB'ϿGn~gaQ\r1ڣO hҚCkǟqGIۧK֐Z94c1:PO?*03 zp:_9$J:wQϩ$u4#שtgu8=NJ?4& (E'~2~sF{R(=i:ǵA4*s'i\?=֖dBs@h5@x[16ԁLO^-R~iSztLzqF׊сfHOsKj'(;N):sB~M d{qh~&{dA9'S~s@C=i,у?l :w!}i b(4 ~="G9:Iրz1F.;c48Gq(ў>~<ќ4~=GIϠǨJwxci?t= I4QMRXs~p'?J0;=Ui,tߴOi,[ҏVKIsՎi4џoʛz9;0x4l( s3BT R}syA1P}An8LEEDWi9sJ"s֐s4`\ޏZ X\<@G=i2GLcFp;!y>cRdF}E(<4U%hഊ` aػ>v5q|.z椷i~põ = |~ 2HP09Muy4PPIGuqF bm=hUOmr~Զ ah6{SXsI=ǵ/~QΡYsTSP1r(w8za0A9n/J212{'jh(@:\i)GBסg)i;/ր'^E/__ʏĚ9'#c1F=8iN}( QI?J)Qޔg=(`E1=>l}ϸ(ɠG=6hFO|~tH}~ZFA֗:H 2@ \ ԠzUpwݐprMY֛lA0L挟!N:~@(p(P*M4q+Α9e'ҩhryB̻01%ϴ%A/Rv_ef@u e1EXyOG&n$ ((ǘl800桜o #r0sS'V[h"c, J%X(F= vjWdA\u!Xwbwy6,#.G j}[:6&FĄdd1B\6)^TyQۀ#LvK@9PAŒhy͵7qK,13@7DǝH 1֞6"v޳Wi´I<BsoJz n$;X'NǒDց%ڋ1an+5p;U;H–gi݁c6mHbIVӊA'hOeC&G P/(c< RKr)V?Z/dD3Ϸ?qkP69H̑>Չ d )\]_$^>3wŬl'pvdIgNjs+ۯM1N9Tm$KlpWWM/oR ; 1[i G֐BI Hiٱ؟ڀOtHpԱ<abn{ RcUBĊoq'Zn2(_yVR0*!^ [)qGQMfF 6K4$ѬQVI*(5cqn?DnwAS297EK(]9X[V5SUn~g#^7-9la50W!?://4mOp X(BtPoD$`?:@ݎ@v8giX]Qt! da5<+6UZlą4 EdRhO֠ EՔ`Z@1fr7[=ۚ D[qp aSAHUAȦB &p5M2U,P_L7o1#ު19c~uKr6GSD.zthǽTCs*}G,ŀc3(}1\=AU U5f}wcI75}ܝqWqOjoaݘQ.jհq4q$qV$7C %~7W\ Ja z<ж'eI"ڋX՜uFRRx2{մ} &L~0#8 I>IR*a)Uz"?} c$jKxmgS7L‘Ǵ`MYUnv ^jqYcoAc*9B$Pғ6␇df=ށu~4 E.4h'O5d%ǖ$;jNdsdBWq=& {Sg?ώ:RK\u$"ܤpJȖ8[#3V1\5$nB1⣸1`ӿJ:gQ[vl8i1 .rzR`R GS^@4d}h;ӾlucI z9:g4J\O΀HGNhgylϽ T> т89):P@U8\Ng8Ul7zX|G$t gEsŕmrM!qDe- :4q<8B0_[,.4HHFp 06Eq:¨&xT33*X/#9818T8IT9`M *x#=(Ub︓*S懸1{fzN}>^j2?b@F;>;Qϭ'ϩ#9E&y1Gz`=5*5''4T$"=B[O{Tp,1'vq0ǽUBsIX;~9Htn4(c,8F;0֐ Kp2~n.+~shx( zS4G)ګS-['4lGK1`늚 6pi9ܥIQ68=OF8)1`R$^#!Zd˙S1R= E j{Tu/8tgnkLU}23ڂv9'ŬI2C 7`T2H!֜ɑҟ"r O€8Ut;sI}ZE$wIz\{Iz@v2Xu52 ⣸ʏ~SNG-HAqKی&09JG" .wsj1P ֦*EtqTH( # U vI19]ɠejq6dӒh$#0NX4K~0jNq? d]G#9??& 'O3`v-^c C[SG6 k`.{U?,lNT!WBbʨN CZ$@l8?Yʑ?3bU {SG@)gߊ3/\U*4y~@*?0@jBz1)ݨ=&=)8RGI3@J>h<d{1sGzP@s~qC7J)eJ)J\0E(ڌALAҌ{\ t@81GSKހa=8 dGV/"ǩ-[w )! *!m`1Uf&O35r8jXd2F?h.Dx 8 U[dB3YB &&'ӟǚjBP܇9W:gUf,E1B;;~ FaPx%~w8QK+7dwv)l@, JmͫeV*M:C*= f@ R*v}gV#sR?=N5 0Ls `Aw,a;x5R5ެѱ>aVp}(n QMp<9c=|Ş22_]+MOrFZhmAVSٰՂW~^*eku.Ӫch?[$3PC7+Oz1Swh3dV4r?b$NG)ʡ@POSJ0$v#Rgn_ J1(#(R1VA vǭUu֧J lE=p3$"2}c`⢉c%<9ɧ)Ԃ1([ _GԆoVNA=đI%ö'rG!OnJVR䢫qGV.%0V|JpRZwYv8x?N3W->̈́,0* cJ$wB=@3yByMw?Uvہai-ċs|7mÇ?iU=@oLˡH]C 8SMB -rO?Ju[c,]8*rjc!2 73OJbUasl$Yt=2qސ$F1;9›r` :ԉcO E)h8zպ`NBZUkFc9V}]yPO#M,ecۻ)шC0@!\:g8oh" {T+C*HV0= t{ RHBc٥81Iǧ4w€d!җJ?*>S@Ru?:_QRdQ89}hǷG @PH@r?*wIe%}8&0Bi'#n >K1> q GN{Q~G==1K 4 /vhȫ`IPpNjRx80dr3<|Sj 698Pq6-RWCP픩MMj>D##"C ZYKqQ#\;+048=8zRgڐ~@4A֧9Sv$"n}h4tHyZ~ye<ӡ\)A̞XA͎*Hy$SPpH0IǭO9#kKKې3I<2i`FIzTWq*’zzзJ 8=( `Wx֔9>Ԁi=xYǹ֬;!VyM U/wj=WU!͓gX0aN=9J0qR~TcҜrGjkOzFF3P[gS ߵNGڛlEެ1{7=CU5 8x jNJhnY돭`'&q4)BQތL}( 2\)BTG*OJu2q5* }ЈZ['Lyb"Jbo\Ռ;UhPw?.~cM"[& ~8J1QLvGTS֝ r7lG;G_‹;hb,q4rAg9WS RM+vA"{ ż2X@8 ll\g?:*66Jvx @B8\ZH6֎G!ԟƫq}) F2j~zw2uQzPJڐmK1ތdpxwp{8HyQ<.XOWC }u#Iրt+v"1zT;v&;ԌP&/&G}zi9=HpNz}w!8`FRs@Va*\ ?ZrH2L_\Oҫjs2XH}}LgKN ofum81O+Hӳ>XS ;IUgv2`Z@=OG?QJTݛRj5\{w f(qmo),E=<sAnh\ \'ڛU!xjOSт:P8cH [#v#wIgg?C7ZwapLHʾF1x33Rsz@/N *YK3ҥ q#J8=qUͬRn 'V)q .V2prHq֟o 0egz{Q߭;zXgܓ\`>?ݤ!FpqZO—);t8hҀv8*t'=U?<i2q"y9F,vU*F2O?X'G4qq3;E;Rhz&˜ A¡H>a#՝AM`v( >P2ǧ4c_Ž=(c4qsKǧ4fҗ?!h֌c>Qz/J&>mK{~TAm_H?tƐ1){8} ϿK֠"r E"H+}SRgAr= SGiݽiR ;1j`.3Lxذ(اC@GrH'rsRp}qKyրsK:Gbi`f4;Ss }jBb*9|M=P24Yc*L} cKhd`reKp@V>7Thpy׶(?Vu+#~8In:U 1.L ce!XTQv4 N{ S֌&~t{cZ1P!:~4bsR~cG#R}E/'z}(4j1'?PCdRS ֗\{P#F OݧcҀ1R(cP/1/>(4g8 :g@b]@2:֒U nGlU jwFIɧTgR Y|ô)R}i`dԟ;h^!;Rvr>n€+&URZ.~?Jcץ?adr!ʪ_! T.=sRx:PFIϱҐP!}wأ'"z#7^$657^Ի}M]eB\4e\;>;^{Ν3,HsL+?dgchr5V2x}D9#wԀ_Ul=KC"!9s֥ǥWTnQ䚱sQ=)r#5.;b#vb=?/l:5 # ɩуƀ9hzFx3iN(chRhӊB=(K41F1*0}h;tc3I8GOjA7J)fRj^ʀs@cL@EJ1h :vQ94sg}1GP3?z\Ji%H<͞2q桔7/0_QO ӽ!$w?.{g|)M4py3/p94c4IހFzcyP~ǽ;zPzS9=)qӵh(?:\wOz~TAh>Ž#( 翵&G=?}AFFr1h>S₤1#fhR] Իxr)x=ր}/ƌqFGlTz)G440$4A9Z=M G8*L=?J8;P1CQh4pJLz8=r~#4dwIt<z 9<@9Zy#Gz0=I)8#|A>x:0>Ú\s;@ ?*8=u:Rc{Glj0}hr'u4Kڀ>`j0:`1Ch94m}(۟S@8 LA9 &jv@=Ϡϵ&^i9FOJN{R(1֌٥yך^Ԙ)qۭ~T٣Ah=G_aFbږ(֌Es(u4~TQsIPz^OjLPh(zQ1@PZ=h4gz(#\F d)v<ӤlFd5,'$}>c t(! ֛Kרv(8QΗ#BdGճ4>P10@}p1~=3h恍S$s$ҤǵG1eQg"0Ќ{|{ƀ3Z^N=P(Bphv)zQ{ N1FiF(<?z94d?ӱbKoΏ𥣊Aj? 23Rڀ2I4F{Pސ㸧S[hȦICyd1RqU`Uz qI\RڗڌP=h{QF=:OQy=(44&L/Nj3?#vYWp^뚂Ї#?ZIJ\qE-G@ (8G^ƀ ?G>4AKAK^JNw4=(}4bcJ?q@:u'~s@sg\OҐ~Ld}h\Q=qRR@ hK΀o 1sK~c`uKJ^hjP8ϰǷG=?ZN{Ҁ#I+Ƣ7YdcЊ?Z(SKc1IfJk ڟ׵A??.0r8!{cNR`zP =r1A@NyhRRdP1Lg/Z1 ߥ~QҎ{1z>}(3Kyu㱢H G2=nT8O4OҎG9A)x֗O֍P 6_ }7( {vm&K:y([I@egQm=O8@.O+=4"k (m {g5"pA@Am{Xf22)a'Bl 1{zMuh<㞿Z=М\Bs G5* a9'8gt=М΁y3OfEK3H#x 8#Vy=G@|ȹ^[g]d.y4ϱܫ'\NkX7(B},n2 {5!}3Qg[ǗǦiV h<^CD@=nbTȪG<6c64} c `QB_>yX 0Q:6j![n=I=ia}ÑhL2"894466pPE;J4 -ջ(q:0'V2ךlT)Glrri0‘ G4ݗwgzX[\oŒi @Z!isGB̄Cҟ&2FjR敬2FWi-Bs$xD9 H^g{ [GYʖ<ҵG~N)1FL0qS j/E =ߖ Cih0N<$ȃl_/z(:39=Sv 2@={lN£6v$pzR8Y^AhLg>11}rqM F"A} 4 Fx$ȟ/^h;pzo%y0ĺexz& \)|Ⱥ}U&!1 4尅d2)nhCBKjTe#A<_(-T" Ib`4XZ!\CZq d|ÓCB̀BP&۸:|,a~B,_d JHgj!c(Idw٣*@-jc>XI4-oL]yLkYd i%?c+ducGanߘ~4h<Õ2qR\ȿ7L7Zhgiw6]j%ޞh}"N pL$g=:,bJy2g&d 63N2ˌ5[0O:`1t/N{cϋ & ޤ=*i% ^na q斁kΌyN3Ԟ|{CypjYDF &c&N9 ~hNnb885qPa>c99nJSdwn)h~ DCZ<j,`_N +##!lBߝb wjP錇=jW.ҟHzZ Bf02X<Fm-Z@N@#>BQt܌Ce:4 qLx֠: gFVF02C҉SsdCަ-K1" J4 Fy^)DxUcFjU@gNF8)g.0QhKY\ *S -֪*$c֟(B(`~^h= "T'>̣ULjF\3Ke3v!hXgx#Zp0,!Uh>ThF@HYƲ0%֕sd2?٣lv m$R '&yifuv*$DdfSv]B9FVdQ f8@qFlޓpC0=QJ6jg#:qJ4=USi,0gs@c>aP:ѠhLY ^=)xUbiLyY!\p3,ȵHpA"[eYwۊZi#R4k$"=Fڱw#5PZ1Rvcq"وM&[[8.Ґ8՚(ѥ?(y d- r?3"6 Oѝ,cPMRK6E)&.;F,K[ ]B##>c>IiYd MNg,}t>Yw v u#mR[I` WOӇXi>&h?{8 (E܁qJ Qk72:uTv4BGxj[J0|qEqg,3 ӲE}jCp M4[M prziYw r1Ijb MG ܍❗pмX >}o4;HjC\4Yw ">ҨY?) iۦ$Ir +.dh gތԊ<Ƹ%8\S[hwS]ȹzN:#Wɐ0ğ'9R)g,/JZsFjE1uˌ?(2d/LYw "P\m/c sJ e2y1Қ+IPiAOz:H?! >}i8?:7~T s֥#O`c=h3(ϿKI@ǭ^94~{ wD}~I޸4R1;'8h8LAc<>K;83ڟjvxI?t 23hqϵ=i34gZ'3F9:sIzӳҁ(/K|~4`h<N =@ԘhS@x> vJN}? 0I{M:P!GݥTӂy恋>ԇBΗ8)O> |gF}Inip;ў:^=OL})`~G N$\sEbhtҏ~ҌP0=?w3BxKQ(t:Qa@PHPϨisJCsF?:_Ҏs@ h;=(Pw`qNO4r=0QP3;zQw#sHA8杂;A(Ǝ1_44Lc>p@Qg;I٣LuFir@i~n~9gi:-/ NqIuϥ'FyhϯJC>u`zԇ>`PhϾi(րsR1OhҌ{Q~9?J^#ښ3؊\P `Sv4c@sRǵʗ?#t~T3IyJIQ׾)O.@IJQH8Z\hs3Oʐi1zw''7(=)RqףŽsΓƀjE.88>Q@>mP0ȥ:u(cQpLz f'Ri29x8Oր%84} 29RD 94~bLA=8=)NTn%@Ig4LP>HhF~u73`})d*[x+@'=1L})x.=yQ @ögQӿUVMepMZ4`=$c'z#+=sH8?֗ZkP62qҚ.*`ӳ ӭ7̱f3Ԩ"F=iXߍhRe^Gb(j50{DrB'Qf; =KcLD7Hޘ.oGEGN4{сSI9eGQfGq,=QI{oFTrsM& xFz~tQΌҌZO RtghcI8iîWoJz8.~ZL;QʢDl3j?LQaFGzk:)zdI$V=p4ƌisAP9s j̅W8{ Q#f |' ;b]K4"z2Ȥ3ϭbԊpqK\f 0}P i9M(tȣxPj0zRLh9wҀ tK=(ǥcE#q{Rh'@`OOӎ(;u◁MWVS4tc?!*X3#cF8zQC@ irq?v> ~dh>5)3~tǵ(9K)}E&@nK;P=(I@ h@x>cAAϽ'lM/J;q@ j^i;:Z2:RgR`~=E(1?Œ} _;ΔhFG0+}t4QqhzzSAzs֐sf E.=6ؠ8"SZO@ڌPdϥZܳ6O?/ wLHB*ndX9\HDU8'b9®Z{=ǷhesNx! !ۭ8Bc&X1R1[+#ɑBFPSB,*WT pT~vtq(ںӃhr3WvӴ|LHz6qJl!xAd%X6jm9g^^`V9-" CQj#{3֕,qrTvro[q3Y&k0_z}+C1Uwq%$V^B >2yTZR 9,٣v,(~a*kbBR+QZ*yx1HQ-ae S!^15َTܛr=)YBsQ ._=W)6@ʑnh$FWozR[ I J ;TVȨWvM^sw"ܑy8Ҥ=: >=ʜR"(рrNrE 1 kjU$SQ\Rـaj;3K4[px)(3֙5>.@Y87ʶds"HT FW4Fr6#ۤU[1Eю z܆eKfyaI2D|US Awtw9RG¤4xL[=)^hng)췈NVsMQF՜ pN%o P8><4m9桶P#Rc^sROғ?"Eړp1M.):p?Jkow#ԟTm[< g(I kB Z=:: N;/2?:L{FF?Ԟ# 3 *KuĬP‘Vs|桷Ab}ˎ1ҝՇДTd/"QLH^9zUgB|xZkpCv"ډ9!~ 0mq_W C?N˂\]ry?.حP$$:ԡFjY4YF:{rOn(G5q֤v99 /I`~j$G8PsH@*\QR&Hl#2iq !hY6 ֫]6=T+yB#Yd`C)m9!qu[$tU 9#q|B#rõ6ݐ­R8',Ih^EcsZROzR6vږ,cހ%gGҎ} @Uuޑ29)$;yR\S%t_hC^1a6TYy/J ]qQ1[G,e GZA2)21San^%*]!i^8QD*nFG,; 8szv}icP4$29f>i,G)=ۥ#'njAP"P|qhznKɠnEO|]!N)?)xQڌz}iHu4PCAϭPmih繣Ui8iy?4dp9fadR8ъoϙmcU@Y'6ZJllO1 ?)86ĆR[حO(&27[s 9tMjmj1Fiqh)]c{0l2Vg%\:U0=?34``/N3Iv! \FR }h@F9h2PG`uIqR@ŒF{Q@<4C?_ vF:QzLyLB探1֎lzGjS2O.A OK폭):~TO#ޠ? @5du5ZȬ[sMn4?mf&{T2K4H7c5(#c ծ5b⫼("Pu .^P `LHy !#3 u!]@)҂bliUQ(1K±jy ʃGArc'𣠺8+"w[NӎE>6T@ScPElĦuc?ON?:c;E[wKq1ON^_5TTR2}gm[OZo 3BQл,"P;(<ͨRDp*qڠRQvsu p$@V5t@'Hb:UD3\Ut N84rH1+Êd.H ?Jz[r!e?*dolO4ծ\7MևvZ96H4X1U:J(DvR[]qN)"G()wmK@bTOc;Ri1Ԓ#ƻrj>XDI##ӥf29iP*8[*)eDWnw>gdOw?e3LEHJz$ 1ܜJK( 9Q56O~*I. o-xgWsI:D|͸=NzRfa=9>\#K9'ҫwq,JZE$(:v3P!i4~fOށLqʗ@J'4{~uh=EhzQG@4~SUx]7/q@,RI1iS|<=*+V 2 |3ƒM+,3{KsPDcN{3̒Fg%(z{\\e% 819lY?B0>m'1N[(ܜ=-% '?)R3zFYݢ60sO7I%+*u4WxUnh@APl*iYeY$ *iA16 qО V XZ|(2J ;$ҠRl\$ )XvhKP ІDnv㚰S>™ eO==g֣P#lBsK3 Ѳ)dTу GtX[TG>x#$"0qtRR!s,1Ўifɚ2"VOJFgHa^c?jLߞԸ$O֐SFZ*r JHji)&\ܣ$ao8 -$U˦p3s42VF;tmJ *G lzba=(hHB"޼?*6JLQGKDZ=(f쏧ISF)r}('֔ڎ@Î^ŒoƁ zQjN}hE=y?J1F@{EwA3ڏ&3@F)2Fk(pqǥE 1L(A Tv,i}:zM˸❴Ui*ހD 2*2錚ݳ df73SKQ&;Q@̍鎵1# UXA[L)n9>݃ >`|SPن_7(=nƥ=𺍼pdbGJwQAR8 cΗ>w恌{G>/&b?_aqQߨG㏥ HG?z0^cր4 4QhbCU>cZrcvQH &qEЯ)}VnXӼ2%8#dlP0VIG0c6 F3Ѫnq֍@s#np*XdavMGXg3 硪r,ѐNoQn18NDdcR9 ޠwd$Nrj@DIb^*U~$u?5Y@ 㡡iX񌃟{󪏳AAÃo6 />`7zQaGF@) y8>d e(A#4Xa崲"F*u$6pMW#̸)PN8Lpd{RBWv@3L.0[W*.~b24sΪLvэOni@0&le}}AׁϽ@J R>fX?xHU\;cӡW J/Sqӭ1pY :$)p}~s4٤!c>(f F=R} ~uҀRIހ FISq~4gh>QRG&;RqCF)ԙ#y<c;*lqS? 3 -FzSF[d ~i\{Tmmb};QlR#!gg$1*͎4;!=3SUG ĩ 0-}܃19:c44"M8l@l~)+2C4#FORRN( He%G4mے{EY #ڜpHv`W5)& J_ŽGj9)u=&qQGVmn95?U@a;h(s=E@ }h/P`(v-?? w)y@ qRwqa)*;p!@ ~sI9 K4RrcE ڗ֗9<){bގ1=hOOPb1gK^zRc'LюzNqO?Z^1!t{`7s׊ʬFTsCg& G&?!_Z"ءyBy4F$3 O'$9;fLN9UWlwrUEaNN)J+JAҚi1_R)]Y@bv󞟕>[c.sSAR\ˎ Q*XN=*w0=ycOJPq_ʚEchXM_A!MoaC`g?m +҃ {'ZAtWX̡'oDd;EB)X4أqGM>QϽ5Hˑ}?8ԁրޠmIruSyqfѿG4 xSނ R*0{P"-#),a@L:c l|1ԁV$ڹB{pѲH[{ی~51 nHJu Min\pq$CmrGJǏ)E8֋fQď)4G#ޭ2I!@\] X4XScH4`#WJ)]=)6.=)w+} g2.Aޗ Z OF[i#pAdI²w]#;k>b*~A|bG);T:sbrOcu㯥YǦE!U8ʮ{fcp*ZqE'RϮ)yJmPAZSa(7犜5QT`p>G4fHsG4:Ƙ$03O0uURc4c^?OGSCF09&:sc@ ғh=?z9:4րFGhŽs@)})i?@|'Kϥ/zqz1FGF)VU`zHo1$T(9uˌ6v80_z4Ĩ1zS…})@H${@2(#&,fE[.=* (!g^WQvFrp:Ǐ5If! CI sV5|nP94yhvݏ^E)G掽)0}GzN?ZQZn}ڏʂhoh-G#}hAקKp(g=Q5ڝt5# 2?:Vgt c j$^}< HP )D 89t@I<`ON84g#219y)jO1I)Ee?(@Z? Bo>0?694T9pc4zP,Ւ@# K#ɷcJaiJFezri1Ii.A*d}E"FKh`4m?>Bc={QO@O #N4ÀsҚ`F9cAIb:@NN})!Lv"@+HVt (èmA҅$08ia# +.GNip2$A'Sd~QMXrMpDd@`IDLǎ OMt0=1@.CF})u*S0 #BΌP=sF3f3@th'Ҍf94tK@ ߓ@@җ Lgޗ}"#4֐Ļ1б"*9>I z∘,d<4 wc&[ơ@3zHI &#iTX3 zШˌt#ٳh1n8p(XœґH_.I7nP 4(לPS;}?ԅsҔ P:vd>;8y`=(#ϖ2۸Np:RQFhtUXzJJH4"90#ڗ1Qcv8`d@v9Iz ? 6J3?#w4u4}h4tn\\ q@MhoCE $ԟ—P)1URʀsGE/(Ӛ8HM/J>Eh"RhNi;R;'1l Swj[wFP۶NĹ_J\TSK 94Tg\B@otQLby|f8I E&24rޣsT'H3 =*e0ru# Î1>Ir3. jp%KcbNq(ҳ.h^F^w&p?¦);ȏ_ >]/qػ?T7X B"K m4N9MgQ;z֤,0Vu: -Ҍ{˕$XP弁\qXу2 IY'~PpHWȸB`EbqI?J%b!p}*|ycv4:`MW21m2he?^XV.zP~ZGeE ߍMo{{ Sjñk.z4<,){Q äǗ9E&b>tfdg<;aHs2҄bN'xu]}$OH`z,-GN*9X65:[ۘleaX1ڎʙ3|ޠS؟΀?U֓$zfL.;QJL($g.}IP 'G^?Z=)h.}Z3|J3n{R>J1QjLڊQK4uD.䚂0#+hG,ѐji3pq?"3&$qnZT+"cӰXǥҨWab vSg"+*z,.KQnu %GSb1˂r;~X,YҌVy=*(5,+1H' abN(w1uH$=" C hXy>i v*|PՁ9KuTs LdB@~HD?Ui%ID F1J,Ɍiv-~@"g,FxnZ1pnQBW/'ӭAmIH#ޢcxaIb4;UE3 lj7uijBH?8'VUèeE q)d=*r~_OsH/U!Gtڰ Ͻ^;TWKp2qHD U%D2gsRB:wCc'U,eFO\)6ܤḥhtvUgdF&b[9i2})_Z? ~>izw' s@ŤO@(P!qIߎJ:R*L{z;eOR>qi8Kd}(= &^{ђ;fr(ǥ>czL=)O=8p+ yhϹ<9(&*\sT\E2`r*hVw4X,MǭE#R8V=3Q0B h#r)xi9(Ǩё( FGyjLƗgڀ(zcM}Ö@:uUQY 'nqǵ;QUYe$Z߭ n@ȢbPzg^"w,Nv"ap3Jb\z= Bm玦I*s(XvAdj2}h}xh>R.9V$(Hv.tL~J+nDFB@Z{Ukw;d=hj斓>l;t#c8/lΐH튭]K`2'~Q4I.~R J P1i^ U9".r=`׷8f~tcSHAџJ0>Wu۳m Q@\QTq34`DBTe3@blUdQ"Kq欃69ZZB;IG8Ri( q@:袎? L◯4PqEb{sޣ]#q hGAޤG1yoC;Ԅɪ-Bt78}im't`2ʮH)BK{ \0K>H p1i4gҫ4c:Ռ`At<~eH7~\8;QDW9iB'T4$⏯Gc@&AF}LQǧA8Ρ2IGB0:P@cڎ.>O@0;~`K@њ1K?f^=@N}(ǩ4㌊L94ZN4gFx知.(h O>Y~H #>d1Q6oi0s irhҀۺRړp鑟.H3FOL^)2qFxv2檏;4 ѨŹn|QN%b[8ZՁhZn&'(jvUr~[xUFT*𪷮nX !慸-Yǽ!9t6}ȥ@U>q='ҧrDlS 8Vk CVFnpZ26*ED.Jm,oVrIrv۷=L銲tdsLGv$ԜJO2 ~)݃Ix:(EH]py\Y1,epi` b = UHU=bv2H|OPRp=RaƓLv}i8 R`!=?Qގ@?(ϯ@ 84旜Rs܌3费=(Rq8Z21@ 2;}M;&pA;hȎ%bcSHrN:U8C2zE:+wmV-0c{*k{:3e q fBvҐ1'Vzn^f Ŵ#E򳐠n#54 q m̱z 2ҡ(m0s}EKJzɥ] &x'gTnKiI.m$➚J$cN͡عz:BKׁI!8OFhF<&HSU@nXKL(~=AWlD|7Un0b([,M&6:Sl=@1 #,  d uP.O[:bHۄw˜VxOɁ¬N 0޴1E =sVNJc>,"~?I @ 'hy>x=6OU,@)͕#?ҭ攷15Z7GdUW? X(@OMIcA3Musf)@stZT?1 V!r60d;&hp_7Fb\sSQE+rSGƢLm\dsplWDAěqdsN İEWK[N06ҋ{€=Q fPes֤,OM('= VkDhw+6Ǹ!T'5 Ԍm ֦WUinԳzh[ܷIjRgf\ L@FG84RҐ&6})2OZ9'aRp;K3zR=#@$FI(M/>4g>#@EFhGNsKvbG^ihch#Q1ĒNEEoIJ,(9QFIf; Mp}ztSs@4!a@Q*eO9Tj ˨ETwry4s2:MvOZ{>9?J}F90V!O M>HHʰ E_;prN>*6}fWmAcÐ*}(`(Q3)#i3ǧ@xgM'K@9?GG(9>Q 3&3v')3G)dMȻwơˆV<ڝ bA TwC? c#eoǔ+աKלT2qb>n KK|OS'<: 1)[oB t2v8SQ)ڥy<OD[`cgM%aXڋhQzⓧz:"J4#g$JѫG/aQ7M͌Tcǭ>BDF+ p3In1)jm$g&Ͼ(dmf> C.e"Og 8wHBI}Bw4} !@ʹ:XK`85( ҆c֖}Nm~2JJlub:~ms8^=j^~;ۛw>"Qj\R~4G8-]AK"$zr bЯ"z~6n60~P:6n1:S"94H*2N7a0 K}sHBr>sǭQѓF3ޏJLwGhq:8I_Z\ɠ>'~{*L1oQSA#'@(i',:⡉eQsMmH#z < !- ;Ի2 ȡ;C,Trʰı҂ 0Fp3֤!-lԿAzsS ?BR;"LxV?“񁺃ϭQEC7>%cފpM!'E ]0cwK qBzƁ Qp)zzу@ qG 'KG~HR1İM<|S$!+dp3@@WH.|5֦=,/ˠՅ_qUB rQ7-M "AieшW#RGqF~m qjcE;T{~Iۧ9u#yX8ǥMRGQjQ&*@<A bChVDNy*Bz}TT :^j ! Cx(Ԯ(=h"%s"2ImOPn&N{~URtA=3BZ &3D{jik||279Ǹ`Z,6:v8ݷ](3)x@4W"ہ4tE ݐp5ZA%v$MCg]qQB\Ĵ{j|;z3C0Ǩ9&l "M:vx$bB{*DcPWic:B)̃qqzddJ-֍;qVQv:N:ў9!A.G3ץ/ql|O?O㞵^7 PB}>/NƠ̂ 98T[- mnynCL"'3i p~_ZBlYо qMiB$lsTǪu 7m%n>N;c  UwBgR`M1|e+|9~гa<KaDH 1'g@d yy.{)ȩcm1`Z~= w$pwcƸd9GJH6=x*A TH߿`#w='n)Ҷ؜ۀy#4pNdsRHsX+P _8;&7Ch$y `j2:RyjuN$ #֫\ɸcKm>ȿlaސ=(##dPڢ,Q :zR}}Ϧ>gހ`p?A+.C(R2r*UgL:Mn4KGJC(GҁIcΎyړP ;OΗ8~P 3FO  38(S}Ib+8sȠ MCq'Υǹ9Gքb'(I{Q8#8NwgM(rdz'M7M7Ǩl*l.]:l2;Qm@Tgpv5)h\e pJOCd+HDvE1S֬ v64 y9ȡgt@.@'֊LZ^*NM&q۟Z2@?>s@}zу1Hp9'҃Ґw@梐# XdY0˞)fG*sE\w.fN9* s>M p%@vJpSfɤ$ɤX[VB <}hI$R[m+z4AebǒOz~ JbOJY&2:=G@C>^Զ "R3~cm N2M)1@=Oz}*a$Q ;(@P[bQK )iz1knHTe)!! 7?1恎@BP<*'Bpj~qת6W'4-L0I毒$ Fpx"I!&X6GNws'#194M ̠ut85 $ n|WjPyjG8E*ĄzӡIG2shgN̓n39w/:owI恓>BqM|}D{qD I߸7#HD)G#AsGGNfF~sE@1F(ϡ Nwz8??*LPJ_ŽsHO&G L6 ٹ R FR*E]8.@5AC'Tw&qK+팒=c? $m4*!HuT ҄'?Uy6 } @|ۀTÕ_ Bx$ڬvQT/'}uFEݕtv\Zl,'D3e(.0tt"Q, 3IHAϨK? @hlџşx~\}JۭV.mQS[,q \R>/4^IϨAFBAGngMzё4sތAIM'y?~4 19`HJ8#(@͍%?8[K+q&l/$t'ZJ@H.WStg \c#tN{Ϛ`.ƺ)*1CI;meas>A?Xxy<)jPyK\gӻ 9 OA~cۏZ@PCٹҥjJi=h 7T=3@`$b5>9l` hIJrF59~'"3H}p+$i~֜!roocn:TzR]P, lj|҃.T(ĉ`nZUV eYxiy>f`G>ZLI(9"? B}1J Rcڏƀ{)8r(qǽ/$tJQ)'Qq8 s@ IǩҍFq@$t'i,p:Ti}3@ >{y?€ M1 25/sA#P74BzPW \9! y|}iqOSNùN5~;SS#bg846\V U]jH#x͸j~;tE\34L|RyaP(lӊAIirqҁHA#(}) _5ZzFg};;D92h8JqG mPI; :gn#*N2(Ƕ~ q zsʬ3i 9q0)qv mcI4P֓#g{QE=h;)r1:L3fO&c^p(wc9Eր1)CL@>}G@ M'~4r8d88#RL?Jv}EϨ#JA#^~i7 Ab~sQqI1M8$ƀu8)q?4gу@h4E/9@;nÎzSOQ1fi**~Hmp|;S}q9Iӿ4@<&RR <Ï?pz>\.1d  Q)HPWU GKG? ?J@/(ғ#=ޗ$zb J bx}E)'2G\ j2 7@4Piz)qJ= 9#){ΗpE.H)co G 6o-2yl nCGƋ}OҘU̜ǭI;F;րʙ*B>:m~aY82)9G4VppTh<уq֣0bAjx?JA棒7f1ԙAy99뚛Ɨsh_J\QҎM&.?ɠ!7)G\TOD~l=*Ғؒw`Hs?Jv{\+rgzSz^:ўz Զ6žh r{\AA$1G.~rtM=(P a@Nn8'(D$|5>O@)^”}hhB瞴 ip{;Pӥ/߃I֌TdsctR vpq@ Ҍ(; ?*3 f}1J;⡑db:g6=0h2hB0"*I`P}=sEr Leir˰x>3!Mƀ‘Q0j0~`zP7 l{`=?*OǼ3sKRQz 2A!縤+)^ nTE/W;PdWw`dtOҚv '7̄gOS{>Oj@8cP_у8nSgiw@GIOo€&}y&@HQ4nH`@N(p(QFG`(΢!27m ׎-c y4(lrY8]ʰJ24 ~|qC!q)$ɌYnXՋa>y IanzF i&ԝANH.n'm9,LexS.ģ<VRpqϧZky%fx$`w89Ai]vgpA' xӁך->n9:reWNH1dA(H''?LjSq"I=}#hH PܼѨ1 P*;Ĩ\(\֛(ۨ3v;}ik`f[G%06I5r# jmg){h,G?݁R{3\,oo"+:,ǭ6좨wpOD`spq76WKX TPUO/ 9Ry#)lyn-Ic1sU徙%!meuX`e!l͕`cK`d6$vf4Xr]"3jg'U5N7ً냜`d""d+"rpsEnr(,b@Ҭ[Nۤ\W*;HL1&yo'#aks湑(Qڍ64(&U.5dtK?0ܿ6?> >Y^Yp8|zh<.ј ֦qں-Ԩ%1U>}*9-3c*L>V])UwB-Hv~2OHҌ `3@?Z9ϭϽO4q(;ڀf=A< _##@Ҝt:u$hsFp:N~hE/QҐQ4dP371+'mP22.{E'p M-.!VLW!:TײMq$MИFi,tM9~o֝<&CI=}^-gA򼎯U oQLCn%qP-loP6:P_%9aݼm*Q qȤjr#D˱-nbIJ2FJcQӤ xP)nHga3dZ:3\jdԶB L~).\$l-*rF΢!c8<ңu!aA7ﴸWʓq|3I&$L{5"rc p3Hle XP7' 3yzGs@ H3J֡3Cڦ40s\R`!~8?_lъGF}E'^߭(jN猊^}G(1@?F}h>Z9QӞ(:fch$?{}) ?(=gdpGN)x>z`q}1KI@ Wq8M$2MOZx;Lm`R),M;h>z~Tc?}n)@ܑڌsVT39ϘD3{VFD'͏iϒ|V#Ld3!a֑B c95e:1^3ĞhC6(s'4_MFޡ}izHBw7?;B?J^@!Kq?΀G&G(?O— Ҍ~4@h_Q9"SOZ!9v돨:Ou6w%Th3ȫDb 8آXRi$z-4mi9lrh} qҋ\<*p gj;rt*jxb@\uR!gHqLsHR ́(Eox'!v:)iEn<6VEϩA-O ahByhpn OJǍۏ+.p֝TY$f Fѷ[ w>=8}O@QP#$W'x (h@uKq3uB܎dTc;qD)Hba匊zuj–T"}['%LK;۩i?Z?@2YF 3Q C2u2̈#V tQ ʢo(QO2 1g66yn>XeF!L_ ja2h>SzX:hhCⓞ&OϷ@ R9J2):L硣,3Z\R}hϥ(ڀf9ϮsE~cڎ1?Z0O8}gi OH@Mtw Uq3Ғ!qƝ!hTPgwfF ldyp U[V.PaQ}F:sQ3Wz.z[nJ_«db8"VN9?=O@#6ɣ{J>'E4rUk`z$6mI˶s@ zQӑ>f~? /#ڐu8=i3(#THG@ޛ"9<5/qA4cޓ돭.2::qiqу(#PH<h>Qujw> z\YrA84Pq\.UBmww&Gh3?6Nj%S-FqC[TQܪ_k\;=pqՕIbMXBy#+y2Xx4:@?8 )<yȀz}aFrMHA"3O2p.w9kbGUڍZ>.%sj2h']6)ZMHa.wc{jb݊QY0bRRm$DRySSOΥĊ;Cg1g.3QKȲPpr*lcmeEX,Spp~\1V95E-{waAR=j<b?([0CʁީYѸ@[^K1o6 ` d. `E_1!ݸɨnp r3RDKĬA\Gtf0~lbKr)$aeڍ1U}bݠ~cI\A7nVN}85;/96S{Q|d,3V2}?*>a pDTd ALB6 aJw^GQ֎}x@}> :wZ1q@ HO~.=h>NZ~tqQoʀ/8(>AKsAc@ӽtsi03֠AvN#>ҐƲOR%ʳ( 9늻!41UoGE TO 0)c)0 wxÕ$;u2l?bУ8䎃w n,[{j$*rx掃?'ҌvŠl z6 ˰ǚVaÃ|JM>PD/WGQBQqRdjHS~X'Z?h# Nut665V UzdUcT`ӄAsZ zQp1IM)Lz \YU֗=(SLW8⁈]l`ǎ[p5Y|&:蠎.v= FN982n8}EEk,DK?t 0$٨D[Dl?ƅF>t%c$s 2eC\V `[};>j ^(9mǏ¬߭7G|u$zue&L\͜[r%Wh$FIh(G`)&*{[:4 僴qɣ~zq:P{R8;:sPRnH4n@ KMQhϾ(:3ȣCG@OuGOU'9)sz?)? ^OIh/cQ\T r8cNFG 9ɨ)0P? 8Qn֣ n݉jAk8V4QFj$p9$T dXq\ 5^DTx)2,]N 4=1TS+D[KHRPܪy3(XGQ̂68%>@T36Gp*:rmOR"a*yI##zў94{G '?Z6Pq4$a('5&cPF;B'Ӹ(:9{{P'FOG8hg48OiBP1)@?z~ph8ݧϵ(v~4)H'ڎ;dp)2:wR?9ǵ ߟ.z9?Z^}?*p~e)n 0}){SI?QlQ|@aE& 39 G_Lr)9c#>ぁA"iq@Td1@9 D!ULvf|u@UP`APGL֛z_(K('!/ <?#ΜĮ,@`*UiMȈ9UuF/>xsVßFхL*nG|Խ4B+A RڀL>v&u)ǎ6%Hqؚc"MdId7YTpq\Ocқ:! +[桝…Tl0$?yQ: l|)HփB~Te'eH?^1,gDw,|">^,K0>=ii0׵A3D-#t9Z=qP c? XBE*CO*{P?׻EV $ á+u#P*q88ei@|6`Gz=q;~U+nQV83Q¯]PzcI DTz~co 8#'Y<9*2F9PP. aMH >hxQn\6tS"nF.Դ TiYgv!qHAdTRc&%89q o"+̟<A(0!Bq@@rTo8,q 52HO099"9,Pxqe#*6$``dCP:hdd+URQ:OčdU*Wj U 7c ,Î2W @1Ыׁwy$?)*nOE$gr~bGQN%N O1357=Aòr90=8gQ4t擾  cFGFThɠ:R"'>lREAQ3_җri:a~z( r;Rt 4uizw4 nA}ӱIR(_.OhҊ(M@ ~cj8:i2<{(PQQơ"KH ԈdܤsMqlOQ@28|97, Uv GEq)nd3#aT "1[*ÎsLe"9q2FUBJ`C,/4#inHdI pqԒ!?:" 3%^2DVUv==)qHB ڌz^G~(ڐ ʔ` f .&GN4cҌl)N SHrq”) NN~t}iE"ܬA`:D́HI"'DwRozHcwҸp˰.]ݨHf_o6du]8 ɻ@;_0+nI&b<`TĤ0lš0Y#Rr2;Ԙ Lf@A+p$ [4M 8 g&!%#; c% x5$l3=Wj1P0xAX8 (&rʠO`F)\Jn8OC(lz})r;*MĞT1T;R~ .wO63>*q҉YPR@93tJ۟ʫ$V28$׾yJ켌&6<@xy7\TSDgXHQp$0h*ʉS<ɛe;,˚ɣ@$QҌB sQҗNg׭֔{*91PTnBi2} @Oj^}dcրh}(93?:1F@fxP2Pϭ!>F#@ԡv Ft˜@*Gv*@l9K߁Sϸ65 @)PRO(`@%B;4& $w D&+!G TN=ll,8J<ˀH9Q"c? J/I +FcPc bvbdё LR \hƎ8='z䓟ZLRriASӚ;QtO>GqG9A'}i'7A֏Zh}({3&Ѵ{R? 1@ iG=P(h (08h= /G@ =q/0HK(ϩsOΗPdo.=GA_J0;֗؟ 'Kcގ{\с@ ~Z1O48fzր ?\PǮ(QP#4#QbԘI:ΌzZ?:8rhޗhߊ hԏƌZ\sF)? F~~t恋׮(RRn٥֌€ v`/'֌& 84maP4zAG^9sGҀQ QלiqG"q؟(9BOnN@?<z(㹥ǥQx?1P4}F=AZ0?})Pq 94`Z^}Hw 6hPvxhSIǮh: `8tv(cG'€ b \Qciq g>? Z;0zx`ɧcZo֗cKQA@ לsGNԘShO RJ7zfz?r?. 4~`P4=)=Eʔ>v=AqKϠ&A= '׏Q~Ҍz R=ր <@㹥9cڎ֎hPG3GN 239ϥ/i2M=.!Q\dq@т=(O@ QZ^GnP1Iwi4|&3"hK@ipG'~E'ҔL}(P`J=9ޓZѓE.qc4 4(|;uKqɣPNE7JQP1{1)=ڗ IQA Lq֔:њN@Z8(3֗џ@q9y9= LdP2:Ґ /Nߥ.ȤdSd~dR`րFpphp}K~Gz.OZLd4woI֔8@ǾL8iAc~Gu ?F wnwsFNz@֏OFGrz\gph^OzqL(FGAKu(<{ ֓9ʀxϨxp:2}?J8>||R(&=zҐ84I(9dqG.j:4us@1OAǽ{3<{R:`ҌzQ/(8@GҀ:wǾhǽ)i9?Z1GHhRqN4~qR(K\cN=${h 8P~ƌL}(#8>}84`@#(֌gOҌ{~})Ҁ {E/N{֎{AFA99$4у؊? N=(Q?>1gތCQ9SϧG>4}h9{L cրր ?Z_lRc=((z>olR@ Q2=9KץZL7~h9899" ~?>tqII4fGQր3؟JM9QQlR~tOJ9좃Hq(J0Ob(q)0>izj?ZL4 ^(BR/qG4|ޔ%''֗J:v)i2sҀ!/—>bFh=?ґG@ .F3(=(ϥ'iM/րF(R(.(7^hJI9I֝lP ҐtzO@Ê2=iqRcM—pNhgptɥ :ȥ:I.}8s@GjZ3@ fO>g~uԿHFI1Fsր=hq !qJ:3814٣ (9ڐuipG'(sIJ\ڎhޓ"(;.x'ڎh1KѓF}:Lҝ'>?;pqJyF:zMQ?:^Hޞdsn)9#')Q ORSFҗɠFAhhۚ61GNIo#zLZ9@ zJNܜR3@ h9yRcI9hΗs9 {L})p=hs?BO&=Z& )q@ z8"Ϯ@({ @:R 'IOR)4t(϶MK94g(ߥ4`>3hdюy'tqLQj3\Lʓ^;LlRu= ;c@9tE?}2Np>sAH:PL)= n)$1I0}0(s?:0GOF21Lր\~Tq@#ڎz!>ŒQ@=')sI@ >(=0~GRs)hy&<9qqրӡvt@ 6s?OzM“tszNI⏟zR@ 玟(ϭ/Q)2;sޗ#;fZ3E. 1\EQPcQ٢Ǹzq@ϭ!2>4?*B9aI@i >Q 9-@9ԄP \b N{0hꣀ9vsƀ=?ls4@ {cG҃r(4 QGsM#~Tc1@ێsZNƗ>—'>gޏ@ PO>8&dZ8OG8֎}x4~? zс4i?Z8FIߥ\QzAqzdKgM)ғhנ1(;P}j\s4#q s@ ӯc>mt''җwǵ#K=4{3~tdz9>z^c@9/?4wIsIRczR@㢯J3O(Ǧs@ 3GFONgbހOp?*4~ۧhCځ {~tc?\P03HG|`>Z\ʀN=is#ހ`P=qZ1Sяjh`E!x'ZAqi :QzMyhG(ϱ(23EzPz~K9/>Q ( tǵ0 ʗ֤})1IӀh~tR=1AF?Ɛ@쿕.xg)&1ԟ;zfsځty`b >Q(AI|}h#h)h9 {Rc}})qL{ 2yK8 )JL:=E'~gh{/@ OΌRюhǽ.j1?J>4cFsڀxT~s:܎ʝ^ Onz0 `y—c9f L3أ(= /=Hh;Rc=J3K>{JxISJ>ѓQAjqhzh==E{QӓH1^33@ϵi1K)?*0)q/~R 9A@ڀۊ1sFI@8RbPcQHO sѓ@穠ccGlROʀ2{ BiqLҎ \O@.Gzq>QEqisZL|v?ӳM'i rs3LCGNy^ JLRvFH ^Nq8Ҁ^Rcހ Ucz9sKgc:ip/@ 9J\sKj8h?Z8p(P1{pu 3PrzbvF=Ibz|n8=>:33׵ w)9(1R}E&=Gh<w AzFhb;RIݠo@ h{h Ҁ +>oʀh}0:@ )q{RgIJ_lbcIJ:z(tAKNJN}ϵѐG^ Qn?3G=q@Ѵg }OO|Sy@ /OчЊw^qIsATc:P:4}.sڀcҎ1Mjv{R~u?tP3{R sI>ߝ(y g4s9=&:”ۑFF:ʎ}z 2; N^h?Z4J1Z64}9v=((qFy~ZC7@ dсi0z`b}8Esړ>sGNn JhZ0}s9Hr(?348ƁO.}I(fZ9sPhF(1JIHN: =R'ю籥8ƌz3} 0sI&H?{4oZ@/L)h1IHϡ.HE'>gR:N{>J`%(?>G'К\ƀ(㱣9@}=hTP ?Z3JblGSG>ԀN~'?^}i1i`Qq}(41g-i@>)'ځϧqҖӑ@}:G4NQϭ\f;3ؚr(c֊(J3ZNp~J7z339Z3FN?Ž?٣ =J@Խz(34v/I@ @֗Rǭ\q֎}i9Z:\Qϭ'~~t~`F4QnSG(F(Ƿ4b R=)~~&߅.?.>m'^1>HP:RŒ}x? 9ǯ֎=i:t8n)piR~яn(lӿ֓o7`~(/=0(3GN@=h(> = =;pqFGւq@ u&A4)rz7֖@(ϵ'1I1\掼sI Qր{bFyg!>ל4|PGԿ}={ڎOҗQwQ@ {PO}1F;9QҌzPP(>=^qoh8sG_sGҎ~Ͻ&v0O@ߕڏL@P1G(~4=z䏕I4?ish{SI?(Ƞ4zP#~)擌t'KIN(;(9~@ v?zN< g ~J3~t3ќv7GKؚ3x;_dcҐw43m3GP izv?wdŒJ9F?ɠh@:恇QG8h/} Qn(/Z#<' qG>٤IBNM'I8`=u^Ԝ(>>sԽ9BJZ0O\QsF(ހ('ڌ`S@(&IPw":_(=sGF=y#>ځ@Ҍ$rh(q@F}Mj^2P}u!_ΓuɠB:` rqHOc?jOcKIyQԽ((9P@K'' ix}縠s(F;RtR;ӹ_Ύh'qJN}3cZ^{ 9֎dQO~ts\Z?Ni1p=h0(R~{RhϭPKSwz({~&'Ni? C@ 4t((1?\Qs)Q@?&p u( ^G(cڎOp(sq@9hgZ38QO֔{LOт{ QC3F}<('ғN}qG>}?ZL`R )r|(;QN=)s@9HsoΓddwTGhL~t89h΀ŒџCsIhq'KGs֎:`sK@ ތF}@ Znh=g41sO:b F=ZN( Pќu>~ޞ1{Rs@'H0{#4tӊ? ZN}(Ҕdv@9ۥ(?#&{Rペ?@qף?AIZO|0p{Γ$QKJ:bRgz~SAΌ{Qq@1:{P!hڎ}b'ڌ'៭8sGQ8H1RAҖQI? PsF?2=?JBq@\|ξI7zQ@ܠsLӺsLw#@ސ&diy?:bG>2Oғ(8$ \xP֚T LNh NJ_a؁F~~_g!sBJQڌ{ iyiq1P`.ɣs@CI<Rsގ;hyZ>zsGn!/>ߝ(&tGZ(9Z\RqLz^fs? >RފNdQ[_ΌC@(1FAh4}(89ǵ/4}s@P9>" ?1FH(GiNu(Kc}sEʏʀ{SqM;J2'Aъ^)3:G`1R gg/Aސgޗ(ϭ=0}h8g4PwIϮhϭ/Gph'J?3F3ǭ{}E'F}h.E4>q@K٠QNG^hҀ F}IP>Rz ր Qw"NJ= (EƗ#&q֔QQPQv@=AF1֌Q;@~4}?Lg .(dwmc;R:LAG>9=hhǵ%$1xh?{J10ڎzp}FGn(qt٤9>4܎u :9'SwcQ&= 8K7߭&1&Qրu"(R$wh\~4 ~8G4=6Riytt`v=(ڏ˜ uEb)8G/֌}h@E:4&=h.}84sE >Edc}z_(Ɠ1ր 1G@ F~hhF3Q8ҎJCR(=Ev&iOdzE/GNMJ^i֎3>b϶(:;@ cq@N֗I94`hQњ\~TQ3@ G=:f~ ϧKځ QqK:9bv8=~Bd)s?@/(OZ4m1ΔzQќ}(Qz5!1?KŠO &q)Z?@{1KzIZ2{Hb>g>Z:яƏƌJLR(bcKF=@4cڏҎ>ތs@ P(ϸq(ggԟ7lqJs !8>oQ=(z~tќI&B2@ ;y's40Qh*: _jNjZ9ix4d㞴u(簣Z? ((ϽE/GJ{Ž=?}hϵ/Zm;gޝMҀ>qߚ3@ ~:col\dc (g'Ci~9Z>Fqތ&=s,(,qI׌phz €Lڗ"AڀxҎGђ:1~Ͽ~~0h֎2;P0G4s@v:{9J)8Ԁi}I9#̰'?M'<~G\FsLBc<=ߓFFzsI"){ΐK3In?!8*`;PH؃QLw19擜sj:(4G=1M(ǭ ;ZL4pyp>N?™:ҁ_րaI:H֎)xA4\H}8 PtgښXRo(znMIQLѸ9ƙM~h3w\uRvG0(ʓ4g_€=?Jh#/phϵ&}9"AGP!i8#֗?R>f Rt/GjnZ8g\\LJZN}Z8i*(cڀ)({ъO—`(ǰԹPc֗R Qi({ ҟlQڀƐ)x@Qh~c 4M2/ @oҏ_z^{R (QphK!ϥ7v=h=Ni~أZNOLpzӾ@z(vh4`(ܾߝR3Z`UoE>f@3ڊ@H t5'>zP 3~4sюFi\J7b'#.I֍ۚJ\ 7ŒhH=sP֌Ͻ.Or?*LҁXh:sJ>G(r{3\P'g4uF}&{}hGJ?:L3@sKRs/#aњ?1GOҁFɤ=A> 8@}$?/OjByGӭ uK:F@3FWր9OCZ8sIATt@ bQIё@ r1R Ҍ籠P3hi8g>c&F= z~T?Z8ҌKf֗@Mhn(sQi7g(~ө4h@>Пʐ\&;J2)qM, '^P(9dPсܚ;r~4c8#֏{ΌzhJOZ7}?:SI>~?ni9K)G@ E84u4gހ9)x=2OaI4/Ԛh`y҂}@ EFzq3)>9QzHsG@ b}N(>ߝG'Iϯh(=)>a>GIqF}O@KI϶(@At4gҌc@')>f$v{7Z2})3T|}3FONі'{s@sؚ=A<lcQ\4gߊ0?T.}8Pݸiw}hQFxM ?ȥHOQ}qPJ\n}90#Sϭ  \cIJ3xLH@.}GA9ԝ=)3Is2)=&9@ Rl)P?*l9^8}h֔'=iqǧ.NB;KHLcA9}sg4o9PI Kzp?\~TdU0>ڀycwz;b z=ʏ˜=)rHiOF{QGQGN')irGqF '4E'^Aޓh)dg={qA>ߥʀ{;u!~O.OsL?Z>'i֛Ԡ4ǧJ9ޤ~t_P#i{ RRE4K{ܟJ\/Fx8f>( \f v_^}hKo"PKjh$џ::4ϧGҁFGϭv>n?zQF~/jN=(M.hǡ;Fyhϱ(yIJ0((cړ/>s@z^.qۊL{ 9c}JLQG/Z3QE)Zn=gG^ʁ :b'> 3NH(Bp;L(_P!F=Z"@rHhR|Χ88&8.(1߅OcNi8#N(h8Oj7(~_5'NQ0:cҗ4gRcPG@}3$z)==>}iOojNs@32(ϵ/SA83h`})7zJ4:Oڍ .Oi(ڀIs@R“'ZL~{FM& 4r{“p=wܜnP:}i'~(bQ8PϯF{38֠9ۓ0}iqKzLҞ(ǭ҃hy#4d" GJ 4?@$g?_šG1ǥ;2p7c)B1 ]3=)8jv(\)sϭ4/?@ =(ϸIs@(OM; 3pG?v8y)dhǩ:jn{.RO>du?0?2Ϡ;pѻ4|qP?ƎG| ztw.G|nq=A~AϽ&iryJv}4f4g>&Aќ?S@ ?8~8Z^ SKi2}J>٠d rHs('=14Rv((8J@>IzPǭ/Qڎ L:vz~(.}M.}:fGb}KpG@Ȥ9 9Rc> =Ej8L<“w:P϶h}>R@ SRcqRpx;s4cj>pGRdvg1@ ǹ֛dtPb)z\(' 1ӵ@ Kڛis/Z?*L'?aHbI1 Q`1MJP1oRm>d11R sߵ(ǽȣ8#4v\ZBXG}Z}(=qnF>)>Z9=OҎG^P>I{4\tl= Rqn^{4~dA' 2?T]W4ԛzNE@Ϩu~>ނ:RE(sI)3cvAKfh?&nO ݺ3}i=>(w1E~) 'g4J>4Jp$zn֐PE&֚0?siI@ }iyHsQրgIqG G=;~g ݉'p4=@E.37i03Ҙ=? M7hɥP:v4A3JIHJ0{ 0|ƌ{ @|߅/j;4OhsKE7◟oΗ1Mh9A1S@-H(٥րRA9}zj^qsh$qG׊iB~s{ʛoƓʛh?;=ܿZ`Ǯ}JvxJ3tMϮ1}i=fg3RҀzf{HyҀ>w&w.9LGRtџi3.OaƓzBOjOu93GC4GCF[?;?㩦}:\Z>Bw4@zR} 7@PchX3PqR`?ZM#ҎI@G#" v"8'Mq4Kf NNsIӱ*\@أ$'~җL8>uH>ԘlPOO_;޾~8 1x&8\: P==sPrMV@#ړSsG4=F) =n}Pq֌?\ѻNhOp0i74GR~>4{@'<ŒfX4oa4'=dOsL.{hO=j-<{攟JM˜ϿҌ9?+Iy}is PGl &~4ƙj3s@ =1Ky5cG'z~#2ǡtXjN(  0?x \c$==hʃ>q:dt)sM8}h#~~n/J;NyJ2O#?d3\Ɨ:v=@4n.lP<џ@)S$R?9a#xh~-ۏ|rz?H7{|d<}MQ@s)0EϷZvONGғ$z@ KCSH3ǽyzQO?LRu3Kƀ֐{~b”3h~nAMFOoրqt7|} #9ۊ\{I~\RPq&ΛPpi1(q(r&s8ϥ.N:ӯOQIynQio42NG?Z^{84d`ϊ7{hFx`c׊\t@>t\Rpz ǭ`I{A.}i2hGM4vTg>498dqitt zMO>qN> \; >dg@}s.Q֓p!`}*`?sIo4'{P[րғ\w#4@ؠ~\v ?!QGy(#LAt$;uڽqҁp>s@,>j\d.)1_֌€ƌ٤ǧ_j\`րsI3NۚL{N:_3=ȣzP2{`'Gtu@=.^a@ L ?NzsҌgi=@;ёӥ3(;b߿/hއ(,TdiH#Ԉ23= !SFNu94@ip 1 &1)OJ)2izi2ANI >^'FKv}:2;c4< ~4'@ $sҎQy>dt#Ip:8&~n4gݽWs6.%RhyH&@ȇ!)3H0"VEPBAqGHB: xf'8֌֌cO΀ _җ&/(9"zQz~4ʓq#Fi;4d眚 >`{џn(GN8=hN)2sLǹ{zU7(FjoJ7zZ9r3IL4Z7cI 8h@oR?:f{J; 9p)1^,3Q*aR|4gɌt(Nژ <0h^QprD`09#ny=jDRG ǧJE=qJ=>.y烃i9?hۜRl8zQP"$ Pu{p*Eyӌ~4?N"cJ=E3?J1M?ԛ3EEN~T5#^qyHF1b?L@~w'h=i}iN}H4:TZb]"*qȣkש2ܑ_ր#R#C~)G @|irß}qM<`hϩ'{gq@<Ȥ$b` ":R`rG7r7zT` Q?2i _~}6zV H#1b~ ׫:Qwy&S59{ y{}N:S` nlsHcc0)3ПOA^|JO]_1(F0N? <Ҙ l ך#QsL {O.܎YؽZ ~Xtǧ>P}<ц?O <ʀ c#9 sލ91 q~u>ēI寧#q\wRlS~t~$ާyc_FǵOJI>ޕW c?(]w~K=sP y9M[ҟ@@ h01=1SEt5&lс 4며s?* 4)wgʤ*@ ~TK#)Y3|.ie-W ]ml;hZ>n~n44Ԁn8F8:PK~4ͧKTO`&G8сЌ1sۚ~<}Nz0Cvў4MJ?OsFs р:*]K^ (ӿŸ9?@Qۿ P#ߵ&۟zц{1jM9@Mj 88OA @2:i)v1Oc:@AeRgxS@+뺛TP)vցތcjL8gj\8" d[Orzf@=wG_4ЧQ҃њ_΀sғIcKh1—4gڌ@B)0/4P!1Œ}ihԹ>=Rg4~@w g֗&/bz ܓN('QcG^GZ1׷'}(ǭ)zR@(zcޏhtʐ {P0ړ&PF? N(c(`c1GҖ 8>'ҌJ.1KIۥ'=}hMc@~R })s@?3@b4 \RcڏŽ}1@)qI\Q'G?4җњ\QA\P0ǧ&~@G=1KE% `Qџj31E'Ϯii1PH^=)1 ׊L /Z7bqIAGKsh4 NSI?p4~tzqI^}(sG(QFhf@ 0)1K>c4vp)1F("czQ@){sE(&ހR1F?hJ>$Pc0($ќ&{R*Z`G{&G&=6&f=LR:v⁴tsLb=-zs@ 患Rfmih1E&Өwb{ v1G}h۟ZZ?`&6uLQRL(b1@F=-ucڌ{RE0ix% ;җFhh1GNԛNI)rz:QIGLqIIQs@ jL ?F}J3(⒎`QGF=%/Ez2qI2}4~uқӶisJ;RQ^=(c-&41@ 4/:v QъLPgN~3Gj@(zgs0Zsi{gf~q(f9fuC lwbLPG4)FhIҌ1s!h \gѳF0i^HFeG;Rq(;QE )h ڔo#{?1 "qF=Z.} 7APΌ\g" KTg߅&yoҀRu>c :dqڌ9֌^iG)s旵 z֍ƌ3@ =JNhu(>sQ€p;ѻ=ZC֏Γ>uJwpi23H:KS:(f#hnMނ~tI=A#ךv7{S~4~tџ7'hǽ4`@(hJLր=4dn(P9np:ZOH=Hh4Rn4gu4>@{Rd)ir?^j?JOzz~ bޓJv3o/@ 37#=yR4g4ghrh擟jL23ڛRҀ>&}h~4q\O L&\h4oE'4SQQ~4fƌNhhh&h-/I)0iyFi9@)i }}wgh?ʌ~~YBgRz@Þ4zp:џznAdzьq? P7pϠBތZL2:ޒ(-7'/JZ?H9aOƎ=hP ?/IoΖ >Q4֌1fLR} &htց&ƊnqKPsIZ(qߊNh(((Q@ 'z}h<0:߭-!iq@ .}zG?'RސQQǵ.xG#Ⓨ qOF{BFOH1{џz\J?2=2=h# dR2(ҁIרPk4RE ;4/ƀ>ԃ4 0Ҍ^ؠA:Ύ;сZN *?J@${S?֌B?9s@9ECA\h~Q ߧ'?Ru{`&0is۽=?J^}): Rs ;#)GC@ ~#Ɨ7ҀM'=O|@ 4tŽN(=);Q:<~?:Q_Ό\{ \) 1۠@4~/I&=OKj2:2;J\{9}?*^G& R?:_ t9# P1`I=(є^=&AwN)>>@8Mzwր qК21? \zQȤϥ/^)84PG<`тO^>M94Qzp=1F0(r(%/'-&~n}(?/P94v擎v"lq9gތPӚ3@ ⌁@ IzF=< yh}@flz1ƀ џz9GGnQױ? \SzRcftȠFG4q3@ 3M7w_j\4g}o=x=#FG&Gs@)23RcqIZvI?nqџjvN(CIi3s@ϡ>'g҂Qҙ׭/=@iq&}(:GSMrhZ9(}hAhpKϭ4O49}(LP&4({px?Oցzus@'ҁҀs9@B掣I\o&>.@яc>}(}F0?~g^{Rs?4ug֓' ^4)3o΁tJniG@qh{>ԙ'/Fs94@ E=Hi3^h?P=9hFɤɣj\>4~1IjQIhir=E~PG 硢aџA\QRtMchOO^{PwhҎzQ׽O׊Mѷ.hҀ sd #JNvrGoƀƌz1&\Qy8A1cG/N{E%O`wޟʚӿ*?JQI֌{!{Rs=dJ\ނp9SHO@إ# Rڀ }KFh3أ#Ըh@t94R~'" {Rc)g'@4{iNi2G|H[4tҌ{PtG.׮MRq8h9#b"A@ zq@41)9x9F2~(Q&z_ÚfGL4/>җ0:ڀؤ8џz1ԹJ8yuF}@ }(>֌Pq@ j03 wOnsGG~TnoҖxP=@AƌqEJ9=ZNH$z MdP >nќh]qO=E'~)O_aG9=?*>KL w|zzFR&3ܚΗƎP(*:ivbw?r= LtQtۚ]ҌRr{cڗ4\TzQx>qG~^;qHq(=N(wzR~=}(#NJE'T ҀZ2<ʏz~ 03Q?!Kr(Ǯ?*?J'K=>}hϥ'Iւ8M:ҎzN3@ i2IjLsdJLG=F9џʓ s?:\֗&'JO\^?g4cלQ(Ԛ\?ZMwz hzB3/?*9#)>=~=?Z\M?(hT~_/I@=}(Q >~4?Z( r9qI1KCPӸ?AF?:\zPNi2{Rh!9>t@ G?揭_~!۟Ɠ1 {fZ.{;vAgߥ'٨@ގi;R) 3K= ): b}L}qAg4HOI]hlTRڐ~o=K:fC@ ^*3Zv)vZh9zNzh=M0s33}Ks֐ r>4SLLtgy)@~t cKd#1EB(hwz:N8&r=ѐ}i2 co֗4` @4pGAF}|`ьt~4F}@ 2}8pGsG?\_ʎnHIғp}.N98= ;4k&Y[-ǡN(ǯ4,=A \jOhڌ84=7gbG!fsR3(4O@47J8z^(#/&:㓊9@ ʓ#֏ތj>ҁz? R"zёQ3Ž{ch=hhhSs/8(ZN=E A()zRp;=hH)#3Mﶗ=x`EƗQ(C@ ǥҎhZNir=yP@!4t9LA=~4tOҤL\ x{Y9?z?Rz`QŒ=)e֗i3h'@.=4PuG>ԸGӚ@sƗ#8'4OaF9 Pj&?ъ^p9shtqRggAhH@^~`{~TqۏhhE-&\ 0短.GcRI 2} g“$hߠNG9zR4m稣#83}(>ʃG^r/>~M'^~g('=sA}}7p@9{?J1~({4FAퟭ.(2O` Pir(K9(LQS@~8?Kz)xsϊv;Z3B)*n?*1;4dzH}OKMRm_ƗOƎ 4Ai@9G@ v3NⓃF\}h~q~oQK~/:Bt~4gQ9J;z 1Pc'=`@R>Ըc=tzsړo4c}րc qK;ƀ{3G'h~& ;u hA֍ԘrOSQt14p:`QzRN(?ьs(hGi21P|gҎI?7 wig \c&Όx iր=q@ Gnh?N{IېFx SAғhpȣ?ZNg(F:OlRuK~tC/IAR.~m'=ڹƀ &9QM o^ހW:=E Rcu֝t9KlQ(8 lTpL8{Rzc<т)9@(h=1iHh Q4?@JLAGϵ/8B~Tޗa1`hHN;Ҁ{P!g'ۚBy(pxbMҀ BrÑ@a(zr)2֗ӏ! ^=G=J2=:1fFzL~#1K=hoEGh\j0Q LƏ(~1bsF|bǚLR?~})y4J8#LҔ`iq؜Rn$St?z2=yP><19f >=hqA sKxAq_ \} MKA&(91G^ ֎)zw4@9v~ uzdK|~1L{R~9'^p2G^掂Ƞ}=FH?@ @(ϵ!40`'(nN{( 2ԃE 1 I۵1 :Ѹ)ir=}HGj>:v.r3q@z\q~"1F1~e}?JN:g4QB=?U?z9d=<@$wgP;΀ /~8>?j2}qN4!;cG?#Kϵ~" ?LRZ6pGbO֏}(Qz9):vPI'( />}(4uG>-ZOQPLJOZ? ~4힞>=(>ZPFxG'4PIO@ .8jOȎԸ=ڐ4 Q/Q}q@ Q:R擿\PoʗgLR@ ǯ"ޗ>u)2NQ=wр}NF\΀Ӱizϵ&3ԓhz v=("q28 N B ;p1F'K?ir4ڗZ@7zF|~dc9i29I= z^}K7ь}h|hN\QPGJ_}1G#iy>CA'@ @>x`<{Q:PI ϰ`ϡsǵpN(߈ 񑟭&ǥLz ] lc .3tG^3y'=E'AFAgd_Q@ Z\dqKHvTN=:L*p :QPu`zf~tggvF3ߚ_4NAJvb (3\jҀ4{QQvIAPFMƀ:z~=4}H4sGQCF1E$~}2(Ps87z w/'ށQEG~h⎣(.G@Ə}(Ͻ.)3ڀ ~T~d~T4=G(}h;PqqK&ܚov.? 13Kj_)@> AN)QҎ=Fhn(#s0hPΎ“4Rx \ԙ>h~sK}z3AA@4FOёޏFqPlQ 4gc> cҏϿ)1G퓚9=L~tGJnFpW8~\wGp(t :2AI֌=0~vQѻQxi{}ӊC`qO LPc dw\8 oZ3ڐ48^(ڍ}(R1~t?Z?*4i8=94IN}Q/wԟ֌@htz=Կ^hʀǦ 7~Z1@ Gi҃gj(Ϲ" G1FM.G4gӊ@4;=qGNPH\L}G@8@ӀhҀ;s29FxⓟJ\ L9ixh>lqM/d}ixKNh>n4`灏G@ wQFXvڀZ^h<b8Ғ@ 0Ih`Q4{ (EezQ@ qAam#GaJPIF`)~o^Sc9ޗ99hE_4d =34cKϽ b\4n4n(ۅAI^zIu8Rs~T$t)9?M G'䑞 8M;SL΀xG'M !'Z7q~ HixZFG|Qؚ@=րAs<`Nɥ1&}@K'Z2{2=((q}hqM R9)G~>'##=y)s4dњw7^(s4qz4tK{N4c܏ƗPlO?Z^hҌ f4M/:GLu4c{9=n98sɣFs)m8#LRs׌Q?”AI@ lRG94s#GQހ 8o4sF=(2ǡ1FOhǩ?ր ѓ(?QF>>lg߭!Q.1@ LR`~4i:K@;nsKIz9CGQj2~QG0{Hx@niyIqG(7(Ki=y?J^qր g)Ԡ(84ݧ9#h&91v#>`—q@~uG9QAsގ~4wIxFsHH4xQ@^أhdph= 6`(.=G1I@qM.t>1q@>u 3l0^A?G>9I&@h9Kj1h{qJԛE^N 3ך2{~Tq#IϦ(?(8m/>8Ioƀf3ڂ8{_i>ߕ.=sHAǽy+E=MzgԜ('= 87FI2 CK44Qh QHd'sRq4ޘ3МRQ{P Gnq8Pg8#<Qȥ=ryQ@F2qK4&.zPzΎO|)? ^}(RL~{3hƌ:z_s(<z:zaGl}iA{p}(n>sҏΎ(PQJ#r֓sR@}hKt?@^G_zN8c zcJZN{ў= v=1I{{:+bTvRFN:џR(<@+QC@TG Z>n€Z2PIz= gޓ4gߚ8SLwOo֌Q@ txȣƀ &Iϩ#KX~4d N)?FFsў4gץ!n2>u8K014 ~s4'Ҁ qFzLr@_Δ&(b=qKj>(9clh93Q^c\R3QtqF}9ړퟭ.A{VhE'SҀPhcΓ&w'4ޓ}1FGMqG4qڗ|v?ZL.GsJ=3@ 9ڗdq=ssIqҌc ''KPq3b?:ҌCI4cG4@F`)sHA=W8th촠tzϵ/F}FOb(1?:^AƀƒB=/“ u)z> 8I4(49—c&1Fց0ichɣ `R~4Rua~4gތ88ր Ґ04cڗǷ@RrzNhϨLB~9 9z}SFrhG@o&=sKt~TqԊLM.1}iz A<8FM#&)*?T.@pZ &٥4gs2}A?*oR,=(6?*ۚ7O֎h}(“$z#sbŽ;@ cIzF{f8PH3@"{}r{#1K:g@4sOR(sɣ?LPI&jvh)9GSӚ\ҎsԜcOfsQ4f~b@ qF3HKPҗsK{ƌ>gg3cjLӊ^Iۭsϵ.@>/}J9CKFqg}zQ)G=@ RgK׭G@ .=sIzP?J>KP29RE$8 )#Ѩ ϧ41lFys(9ל}(z Qx\N:h8'/Hq3v1{ڗA@ сRu#@ ?{qƔ)O?LzR}H1z~ OpGKNr~`sӏN 'Ny1IJ4j^xu#4chQsc?J9EPG?P}zRg4Cӯ&1ԜӉhJnyqڔ OipJoRu~4ch(Pzv76@ ^M.}'|38zbz1iphǾ 7җl@ LPJ?/>}I9 +#O˚_PsI鿉Z^3ך^=(GAzSϷ'~tp日&sT; zPiG3h8:L(*B}A@ q4{M/aLQϠ<'֓#<旂Ɠ#P`f?01(##GN 'J3A "|ނ~&14uI=Jڌ砣i^.GRhz4dzƚvM(r1t$~4PQR P)9}~d'NϽ({SA@!#8= .}1@IuEJׁJ0zb_֝=&Gz\NbjZJNsgRI?€>ʀ#?Z3ߥ.) O/? > N{:b \}h2=Ap} .OPi(RZ h=h94gJ< N~Z9GJ@=G dfP1=ix!'Z\GNAcp3K#48֗ocFpxq@g4&1Ҕc sGN٥#旌psI|8OK;0t<я84cP!ާ4}oz2}2} p.=3K߮(aȤhޓ=1J3M>P.p:Rv8@ }3Z8oz\0㠠Z>s֣cKjA;P(Nh@jQM;n:Rd)3j_.1G&G-'|8@ shHH84ZQM#4F1 ϭޔs3`@GzPAn^s/@ A):q:p=(>olRQ>(I֗jC4{ⓧAQ2IǸϵRG9'h0}I ҟrp)>{{Rz?J'8J=Fisp'$v}(w2+4Q1%OƐ{g#h>gQ8?R2=(} aGFJ\@ җJNA4dG(9P9L48?1. 4c=4h;PgQԟ8ǯKz8(?Qюx)Z:@x{P4TccP)0}J&^:L)N=@&})}Tgh"@c3(҃dv>=xPp}*9g(7?=Ƞ;KI@ z җR? :2=J2= 8q{A49 ^{3sGl~4nȤ4d1pp(j2QJŽqȣH(?@x\gshzZ93>I?B\ւ@ }0:4}i=Z?@ ֓'~4l(tr1=Mԟ/\g9ϵ6@zLvA'@ ӷF@i.ʗ9)3Fx€B)rinE-e*\ёП@rhQlQ{:'o:^1OLR(qKCގp=4c=:@sIv):9Nh?:C:ƀvKEO֌ hN}szL'sъ8(#Ҁ cJ1؏ΌL}hjO.~42}HNzQzsG8j1M.=F)n0E٠sII)O4у 2;>tqI:P (=ixQzsG)q@4`4~` ZOʎ(^N)?Z1G@Rq持IьsF}('TwԊL`vsl~4~IG@GOKdR ? {~?M(4^;sHN9 M.2{Өސ&xLBџ]ԜQ!1RzGK;J(s8}h0;N0掞}@נhⓧz\Z({?zP~;Qߖ'ړt@ QNփƀ SJxHFjN11y׸4sKhc1F=GZя.8#JLcduTp{Q@硣Q(h4mIPǽcz?ϵ9xRg}:{њOz~Tzq@iye}Z^| 1?:8{94q?2z2;sGi~PI=h rFhsҗw\ѸJ.Hԝ^:a~T\dђhn*1zP~`٣ bqяJ\c)2=hϿG>Կ&O@=Iv#g:cڀށrj3O 3P3Aw$c)3#`8ے}(RcP߿F}iHހ'Gc4=(g>th8sGvj9=F .;g'yR7@Nh?#L<R?/S#ڀN2pO;QlR`df'?:; 3n&OsIf ;3 4:v}I)?4(z`Rd"ڀ`}q@I{_ҌOҀ{FGҌh<(㱠8z1F=pzS1QzPg~uF(xE/?zo^򣎙.9J:ǾhG?l4~T/֒΀~sGz8<zOƗ&}>s?K׸=FA:M9GчF3ғGz1&:;c?J^*OяZ\J9GҌq4b' sI>g@=9&q()8/^j'__ΗR3&}J_()qEǭ&K8'=4^4pz ZRc@#i0ퟭ=?(4)r}hs1:?:9=bqԏʌT|~JP j3)sڐgQJbc>~#B==r}E#-71iz4g?*A^m4@GK:b>ʗ?Q@Q4z1Hh!Q~T"ghq@R3~hgQϵ(Ss/=M/=QG@ht}qK?*1E&@B}qGFtJ;gё~Ti:3Ew?/@ϥ/Mx#N.1AF=)y4|K)0?@z(𣞙hg(=8SGI^=p(9ҎG|R "_Z3Lcy N:hs֎{LcԠ~J2}is@ק- 8:P?>ԙ Z?Z:rx" 硣?QϭK}('.{R}EqGNPϦE֓җ#( &>^)~(z:Rq^P0t~4 z^jCE2v4RK>ёN'\Rg"cLAϡь&٥>>K^3ր Ȥ;Rr8F~cӏ€<җzs@B2pzRz=h0;֗ߩǡvʁ}Qz '?*3 9gf {RiԃKcRg=ڌ/֗>N{ QKQFOҏր {QRi}h1ϵ(/'b::?:89ɠzL⌏sKAPuGz\KPQ瞴w@)\KގG@O@ R:~TȠ`(z> 2}1Aӊ_Ґ48uiĎihZ?N9/Fryǽj:(ϰsdz1Re? 2=(8(?~)~3sAh4 ^vȤϸ@qڐ?:?2~r:hiGtF Ip?Nq{g~isc=?>_APqڗ&:&'ЊNրRd~'K =xǯ4~T`u֔{?Z9 &OҗN(b4~4?4giZNy΀ zQ_Ҏ(?Z@=z9Kfڀ {fwaуNIL~ Ns~`)pqgJ7$~t'1KR}?Z1ތqsG=Џ/>Ɠ=Mwb恃''.(hQɠFOhg&G9IjQG^߭?N(ϵ(9Q@Qq.p:9>s /Rs֣J@)(81 Lis8Q׹0⎼(☃8A x4g\(M=R@ )y44u?Q@>g2M}i \Q&(@ ҏ@M= }(ZQaG'qހK'K@sI|zf` '4|ޣ(@~gZ9@Tn9'K@ d穥ɤ'}0}i3Z9sK@ӟjKϥ'?A/9惟LP!3CKN:d~t \t{sF(s@8I^}(Pߵ}? O PڗzLNǵ'^P(99F)0~d'J0)? sFP3sҀ< \\RrG\98)3~@(tϩuF>0?NOw(#=_G@GPM/9MF;Rc.\\KN{A w?0=@ 8FO>had?'~(ϭFxG:./0iߍ'xh>_Z^Ȥǧ֎ɠqF}*NqC!CE$Õ(@HK ?:Z`#0{Rד@9("s~uP~> P(tsץ}(`(2OdO:?_Ɠ>P瞿yhPџoƃz1Ec(恉gFv8w@<`Rd4Ҏ chZ;p&8c=A>FsҀԹGҎz(Qo^(Fx3F>Ni9旷ZN{yRإ3@}J@ 4f#=I_@y4qҗt@ cϭ'=Ύ}u8(ɠ~9s&yiyc@=qSI^T}GI\g4p(1"3Gr?!ʀGʽhǫR=qP& Pphz`J3@#}~tm>'"Ps89}q=)O $hgڐP?:_ɣր?JL44c>HZ_—Q=nH<֝}FxۿrhI篵iqۊ>/Nإ$~4 0>Ɠ.>u\џ|LqM#Ahq~`P)8')3s@=OgKڀ}h4}/8zP4`ctdF~s_.=Z:QzqgϽzPp:_jNGE?/G>N}Nip}(gQяJ\㰤Ҁb(~uzPIKs>4bƒ@ڎzfspGt)1Afq@L\yڀ 9=1րv=:QP׷'NPIp(~> _t`4{ۂ(hh#=GP1y&'Q@'@j\{Ӓ &xF})28?-4(PG&y q<4@@GQRGҌRhǰ40@}3In};qqFHϽ.)1b~4g4` 1@ ǥ(֐ؤRZ\Q@$~~?/( b `z`9Ǡ?/J?:0~qLgך84csF=@ <@=@(㸠)8ƒdz{9_(RAl#!?(@ 9>Լ3?Z3I/I&_փzC7U߽;қuǵ簤GFO4 _b '~?'4Z32{sIp~=zQ@9hNi:hQۥg'ފ^?Z94GOq.(3Ghю>/?ZOcQJ1F&€sqI~iy(@0{Rs>&PyP=? v$_=GJ:Rg=s8掼&zv=΀A\?G>PM}KC@ `hyƗQ@)L縠ZN}s@"(#zqЊP89hAcQ\QQ@S4M8юPc?ÁGןҗ$f&#֔.(?9t4`vzsGO}hw{PF2:q?*\٤  _ҀC/@ }i: cЊ8'K}i1F~!{s@Gdяj3րG#@ ӥq돡R!hR&^3}h{Gˌ`@ IױhКSӚ9thxG>/QsQ@‚=h?4` =MSA@44擟zSy~&O֌zq@&=?4`]/N;(@ Z1@P1}(-'@*OK) 8hތQ!i84–"f =h4v@ yL▎R3Gip}h1LϽ'^8Kz9(uN3&zʀOQKI^i{A=(3CH}0icGupQR='^ҀރQoJ0N;bp;Qҁ =hm(s@()i> ɥ&('hǽS@Kz2}OI@ ۦhb G9@#g J?3Is h4rhGsI?p0sG> tgiy4~J2Oq юQ.h68◎=֌c4~grGS@>ԙ{>ui{`)qMicځJ^&Aq(MK '8鏥/8"z`48dzmqҀJ9ԛ}GJ:)pQ@RsFNx?/ni18A>z`zbhPCRJS9'P18◟A 8!; p) );R4i}Rf_ցs}h֗F1@Eڎ(֓Ҁ=(q@>Z_Rg4LΓ4ⓧz^H#11'G&g=\NJ{`ьJ8<3GOjL1@ GZN;@.3֢'i{t"N(Ҏ~qZ&O#?&} ?: g"@ P1֗PtsߥؤK*80(?Z? іG4sK9^h8T&s4sN=R?1I ^i2{v>p8w◚שǽ/G4hROʎq@ y4 8Ҕ āXd"F= #0s@~K׶*%w!GL*Lo΀ 㯽7+K: ^M'Z\}3K@ 1F1sN֏74`vȥ# zQI?-c )Q@ '.=) Rg`3р=Z_ʚG֗Q@ q.)hQQ'җQ JNqE(R ^>sތъ0zъ18֗}h@;QF=4Q}ϊ\Rd~T`_Ό}OրZ2=@o(4@=4i;4dAAQ9EKwv.~F'@<9=8hisZ31)1FrqҎҌMz`}hdRS:;M7:Kf(r3?^(<Z\{ {ёGGg4sяz9hJ(#֐c~=Ni9.= ir=i91I`)Ohϵu''.=)? .}i?@ҌJB'ҍޠZ(?(cIRSqEc2(sG4u?:7zߠ>jL٠Nh_ʁ ۭ烏yz֏ғѓ@9? 34pxcHAϩI/z`%.hڃU'(M/#M!_@ H"Rl◟J>9#@'ғZ\z_ ֗rh9Z1i)? :vM7hi@րʌΎ(wo΀z(⃆Tt>qG\s@ Ҏ4{wƀLӽs} hMqPwhZLOғv{Ɨ:_֌ 握vT)PgQz>4eG"Z\wɣϱ4GA8HO/Q@ >uf8"ڀ  (9>4J3/_] >ќyuP}{Pױ 'Z3C{\sGIu'@'֓8 ob~tgԹpNӌuE,?Z) h=ޜތ{{w9(T&WFAsK4OƗG@ K4'~44`U(Ҏ6z~4*3ڗ€J^c?*94O4t384sPjOn\~tq(Ґq@ (?'/8?Z\I恋נ4g?ZLȣ8Bғ>Ͻ.M.sޓ>>dPFE&Oih'( 8ŽGj: 9M{(i2301 &}H!>~4уA13@Ng~44sA>٠}Et4'RgJ>4ds@@=9qF}1?>xF=isۊ``'~T4#'>~4PsGKzQր Fz&}.}'P1y(M.F;c握@ hރ&M'@yK?1FE/Ό@s֣=hx{ӧ@ F=(ҁ@▒9֓4RRthRc'@ '^RPgqҌG84*NEc=E.:Rr{zdӾ(ӥ&qގHP w=C&x&NKތ€֌i}QS@֓z\Fz&?ɥFtcߊ^{ N;QӽqRNsց qF}JB#hQ)84Ըcqޗ^(F{@ #<4:AsAZ0c\R`F9J^ F3Hpt-&i2AL(qG?Z\GG'sbKsH9G4`v4>d{ѐ:?Q@4#^NAzP1~tƁ҂Z(qc'})8<h =(֓>J\)2~b:1sIӿ@n=@/=IcFxbIގiqLF)hh9 sKF)7AF=M/@=4O4 恎& OQKBm4bqG>gҀ :N ?J\{2=!hڀ#GLOtZ^}( sL#F 2=yR;q2:? cb=A?@9G?0h9ϷG@ ŠAJ9 Z3bf89r? wN)?:^hf{Pc٣'>GZ83F=sK@ F֝@h=F8h/@ Oʏc@^њ\gځ ?㠣`Pp{~tbcR)3΀ gFq׏Ɓ1FyGNI h)i׷ITr94ʌ&08qҊN}E(FރMG#ҏ{}S@'֗=;~4g&F1~?/PzyUd(&F9.p)1€ b>Ǡ(qHpz1ipG&s3tsG=G>cGI/J9I;PLRg?j3Ns@!т{L :ɠ'=J32s1@ ~= (?J^@ :׌~}M/cǽ'NP⏧~F(4sGG=G◧Z?:>P9'9{c҃ =R.H=$u'4`K~=? sGZNq(.q'ڌ:~4u({Qq֣擧oƗ'ҀG@}(E}i0{c򣧠#ց ?*P9bׂi8\}h9"a1I"9@<@KRg4@KG㏭\1;MƓ> }րsM{Қ{у뢁""dRP!RE;i Ҍ(8g u'?t}sFO9La _"G>.}'H EcSFNqҚ8>}(sE&OciyBsR2:g(hSG> F\0@çR(Z9PiAB֏@?S@(*;QKӭ sGhLaϨ(P!{w4PG^v׸zсIJ^ht ? 3G#P F8";2;=;}~1Q؁@'4`w=@;1֖n>m. '<((r(JNM.3 H:gKCu 1Iך^cF{G>ƀ M.}Hzui=h:(Ep(QvsJ~s\sTsQOPތG&hր q҂S=p(҂(8KHx@Qړ?QF2:8~4gc=h~/)?J?*=^8ҌqZ^Kzn;~ȡ4p=QF@8ɠ@hw 88SQOΔM&}q8 ~vQ?RSJ=BPr:qG^R~TQΗ؁ )JP1Si#?ހ'(19h4sG)zѓQ@ 9 =?::Ǧ? ?F=.y@Og(Ͻ78$$;t'G( >~tgqMϭ&{mh{(/vZL7:7Rsڌ@i9ȣdz4IwE?ރ9ܓ311Fh@ A)0{?*8qޏz^{9$R`{P=@?&=1K?уhhP>⃏_y4w@𥤥=;v}4њ3=}h(=08((01RGZ=ҊLPޏ֗?h8hҏ4KEIZ^{QB(9mǥ'#4mO?Ҏ1Jg#>^Gz>(=@ ъN}(84E.}9##Jў=SL旚LQsK) 9@v&}(֓t@G'GoJC3@ =M=fPӵFE&Gր :΁ҔǮMQzRq@ 1Gғ87qր(~d{8@`:֎)O*Lg4:p(u; Rރ4LP12qFqF?ғߵhj3@hI8? zQS@Ï&Ocz {QILAb'NQ!q9(qHb (~&=ϵ=AMw"xqF=rhq }E!~q@tёuN bɥנ"۟z=Z\bgHz1 #N\CƀJ:(GNph^Խ{AP?*01@(ǽ)Eb>~|Ґ \>:fwl zRtP}(9@Fzd8=1@`vQI})r{3^Gz(>R>yQ2?Ɲ1@tƛ=h& @?q؊9?h֌ K@h>򢎝8h9G@~=)p=0i3PI?*=qG=sϠ '>{Q-E1Ƞdt}'~Nh\bq֓?dѓq@Ð80G(? wޓJ? /4gi<@b :~g(j}>c4`Qǥ(abw@~iIz:v>&( 0 WhbGy89@YVut4=I{JOAj=Pnƀޗ'ӽ?JQ\i1GNޓcҀƓ{PzъNGG4`8=~bi ^g >"bJN1 ^=1;ccc&)?:\fj\'=J83Fx_“>\'J:@ )G@Ì?h O֎}sA8Z9#!3Rp:hh})LG\cG')9<҃R}H ;ѓG_ƀR=hQQF9ϭz4GbG:Rⓞc3qF1ǩ;f9h9إǥ4c@ Z=oKJғ>}h_z39?Izѷހёs 4cIl4QP0: R@ ר$vڌPwҗ▓A\_4dE.='hy֗Q`g׏'Z6㡠8 c:PshzQ׶hj=1iGOZCTT>ߕRnqG~={GKק4zmQ QCb(JLzΏ| G^hǧ烁/֊:^;JlQ@dQߧKPuGI=Q4 2}3R*4nN9=w:֓aׯ!ێxA<%z Kۊ);ߐ(zP`qK1 s~J0(7 sGM:\RG?ր<.;:w8@րq(ǿ@ LҗcڃQ!yz2;(uqFN)7hz~99R{⌜h*4q zi@?AIہу^}34G`j8=@h>@^?ɣxzƂ}APxRђGQQϮ P(ڌLP0O4(ց>RRQ;Pϵ'~z=8q8ގH&=r=i>`c&=hϩ4`ciqI1LQsJ=~ÚPG= ((QzLR;PIϽ.Hrz~4KE7hNiqsQJ8G@ E'N@OKFhƐƊ^;)zv(GE'4? (юzfuchF8`zQ=(?ʓ>vZA(Ҍ ((~IN( (ހ{~'ǵ%-'Kގb?ZZN;bGj>cQӵ.qIs\ʐހc4v=)? 3({? 1G(ii;})3h@Q;u Md;џE)zvғJ\{Kfn)'?QJ8旧ր?J7g.1(3Όc0Ҍ:b}iq9?)164 Q?,Q?:1G^11j0}яn>qM ףL֎OZ ?J0j{۠Bc?j>QږA@ (Hv֗= !O";`d=i0GQGQҀ1@ KI4q@PGz)3PE&3R((Z=Ǡh:bP8Kq@ \J8" ӷ($ 1@1@ vh*1@P ގtJ0(^(yw@ N.J=&3qNȥB  :֗w9 ^{?\J`›vzw4p{n0:~>(أIӰzZAڀ)z>8w\{`vwRaO@ ~Rz3ړ?)AP'K9(=q{Q])s'qGPأ4 PǡqKǠ:u>٥zdPgۏ.?j>t$==?r(s Z1q@Rtb= >M.(dw4NhIߩ>R}h JL?j8#OҀ QfhcCӦ=z~tqq@ⓎGJ ?2lZ) 88JIޗ#֘agJ>(P ҏN =iO?hǨ`gsAA֌}h4u8.Px>}1I(1Aw#5(1@2֗'/^~R㞟4 @2zSywK:~T 0cF3ڍttRgqQZ.}i?~qGlPc> ǥ=i~bcӊ2}(@j1Q(@GsF?;Qgs(ǰBE@(-@h'iq>٣(@HKRRd߭f!N}i1GN߭?2}xR('GC@ zRt~jK9KLtN() ?Z(( €QJC'ӹJ0=)x)(Qc? )y9h(ڎ}:?:(?Z=Pp.pqEʀ AGfjJZOҌhϹ4qF:PI"@=|Rh?) u4?J8Z^((h3@EP9'?jZ>N=KRP =z~4~s@_OƗR@jLuǥ~9ϭ&P0>_ΗJ;{zۨ~Tp֗ :ьi3' 3Ah:phz 3j8ϽsLQ@<{{R@{rMv98\ԙ#ހ`tI#\繠Bd߅/f鏭RBu 8"hy')9KϽQ2{ѓ؁@F)>ߕ3@&iEޓrh4gQс@FGP( 9Q(@i33 b)9zP!4~j3($sڐ=Ҁ?ZC&1R}(qIt4ҁ &aLjGp(4(?gڀ;>ќvIש1^'z3?^})4g}(}s@SG~0(Oݣ=J^3 NqA?ҊLRsFGd :vǷ4F=>=~4p:uրGnZ\z; #RҀ'48(FA?_Ώ~z)sIϯG@q? ^|Qh\v*LO4h8ё@3GGG~dzq؜'G9c4F O@n q/QF}I{#րIƗ'4az?ZN{q@̟Z3@z1Z2(AӽJO(:2{Ghяh8ڎG=yɠaߧJHƓ8ց 0qi:LcڌgKFɥT~c!fhP׵&}()zRsP֗?7G=1E&q֌uё4@R3֎ϥsA4b)`iy.(<`;ts}E<@ Ǧh֌Q|׭zQ:Qק@1ڊ:zњҌџqI@ zʗp@ Ɨhq8J;Rg4QQ~Qϥ&OqJ:P:2}(yI{Q߮(hǿԽ?:1Atp;Q03_A8?0E8?:Ltg@w?J9Q}(#z/G'#RqQ@ jJ\3Q&})qҀҎԃ8ZF`XAPGB2(~h稠4 B}J^"Z3PޗפQ q ?ҁG@ƃJ>/>%'秥/ @44)shs@ zdj>9@ qIQwnF4E $RdȣF #;N`JG֘ P>.G=OI8wJ8&{G9C^?@qsK׽0ƒZJ1P84`}i0:Ɛ9j9FOli}.sz1j9=q(Rgf@ qGOj3KIۥ\P擐zd(3ړ Z/JC.(8ށ⏩QA8-'G=@zQz)9@^}H֊&ӚpFz@ x:gQ@l_L@=瞴Kcbu4`Rc\zNsڀ[89'iy=!z\{ L旑Oh~1K9&{P{ƀюLѓ449?JA;ړ2hK~8~֗$8~:fzp1Ө>`ьg.OzCG4(?,Rn4s%~t=hϱϭ.(&==~AϽ&Z4PZ@֗&|(K@.ݢ/'P>(ǽu 1LGn\Z>&])M >s ?, \Qz'ӊ1i?(4zFxqFE&GGP0=i:ZL KQzL 3FG$-'>4r}Ǹ9/1Ij\bb3Ҏ9{ӽ֎dpGAKG>@^?CG_J1ZCӕʀG| ;bΊ0>p:}h1Fq4|ߍZ8? \RmŒb&xi:(:bڀjLv(ǡ!*1~4wR@ЊNsKڏƀ&ih'RQP>0;@ q4@Fz :4w●HhҀ>>~4.(ϠhHhhl:sh>ԽG4{QIG>t?/I@1G4s@>HΪ>f}(cG Ԋ^}hhRƎz3?9r(rhl~Qƀq}~uOŒP~(@8럭&1ޗQEgc"3~RG G|zсIz\('R@ җҗj?4 N}hih7c_ q\(P0F}(cҀqJ0;E'Qt4 ~4s8(ɠAތ֌O@ϷJ2N0(Sџ|t~IhEi@}sIKJqPZ8&PhO)(8U!`Ϧh{L@AIQ08Qڌڗ#J/hQ~)r=O֗Ƞ/1~4RO'tq@K9.~4`x:輜Iњ}G4d1GIQ:Q@q@ A<џZ84}x=xh);qNϧJhv:;~Δ_Ҁ?"ΎqE~t~{j23ڗ@hڀ&ӽ'ZN{RPs@8)9I$tKJNނ4QzQ ֓Sh@}EP P(P旭>繠ϩsȥ9GIϵ@@#c֊29AAzP{QRƀ;ʏ–۟ʀŽPsGhHsϵ.AzPގ}('?D9Rގ !cϵ{P#E,S_E!HH2< 2;tFz~tǽ.E1 ӯICQJ^ތi:vfjLR=H^sGQI|{h(ր .):׾>cҗIKր:OR:~ԟd('FsގzNh@(=1ޓ;8яlѐ|Pzb{bŽzc}(~z⁎()?ާ~_!8K :Rr;!zwҎC@4`vz-L:~twIө4>Њ\LR}xfʏ(@ @'^_€#Q3O:H}4Ϩ">TzF}?z\RRA@(zfʓp*Q(ڏ'Q@ ϥ?Œz@4~_)2;(EJ2=<:sEhQnh9:h?Κ RP/J;t G\G#>?In(F~h;tc=hǷ@ RhҀLs/>ߝ'F=;P2}E&[=?J^=)yԽi1G>Ǧ(3F}hrzOҀQoΌ'_QKCq\M'ޗ`ƍ#F-!ǵ?&y?/3yR 1K@ ϵ_΍=q@ގ=)@c#'(i.OsM4}F(P19={{ _z:L wyǭ(=9<{уGJ__ʃ}(44 {Qz4c(uZOQ>F)j9֓uRG>Q:Η9ǯ~?J_M3KA?Z;r1@ï` !.)0@ƀ{Iڌs@qHN=1Fq@據9?S֓>4s}&z^=hތG>:HlKAǭ.yFr:fLќuFOrP)'4.8{S{dw(Fi9JG>KR?:Lb?ƀ Ax3pzh(1Kh?^}P֓҂PZ^>Թ8y4dN:G&3p .9 PLz1jGz8~Tchqhǡ(H.: ҁ1֏t׵{Q(C@ZO qh4b3@>sFG@!g`zR(;pqj^i2q3GnxQ TtɣJLۊ01)9=h(o΀Qf(ǭG\RuG~T ?#K~F/ArxNQEފ@J3/= vsF^R}? _jJ8>Jb ;cNa =Ns4=?Z2@RhiJB <֊0{@:ڎsK89&/nnԜL:gӳ9֎(?9>df(9|Q(@aס&g ۩GԹiGAPF;Fr;t}G@ ((eG擞ÏjRxH^I QJ3 Rg׊zQz3sFs@N;ё\KL@zq@FqGGOրw?/ qϽ/>gt6zѐ{9j7zRuրsތFON(O ?iǥ{t8Q9=8=Nir}p) &w4h$cQ}sPQcן.=?F}?(3fvhJO"Z3(֏Ž@ƌ( dc_ҊJN?/h_Ͼh?4sE@ ތy)>ԧ#֓'"'bdRրʣj0Op@4rOdzяƊ9^j>;h;gѻ?J_֎(֌{I:aǥ/vM'h'Ӛ/ڃ#6{TgO~Tc9QGo (QǿasJ:h&{џjLqڗ98hGA"HZA~4׵0-NSKأh huaK12J@Fx4!#842;{1ڎsRIϭ1 Zhs??Ɓz\gi}ϩ1O֌j:?ҘZLJ=)H9:u'J`&j:\F=3PcJ?/ƌ}~Qz\ZLj.=!u4'sGJ bz3J:'I~}(ތ viqF=>j_p)x4@ ר}(΁ ~/F2(2zfhȤ?JQL(8AAF4я€l LShrzsӿ NT NSG{SsK`@ ֗Rg]tޓRz3J^=hHg=J/ni2((>g=E ׹~?1F ;Q(ΓҗQ@4)h:QwM!#(a֓=h "4Ps#)ԜsF}3ߥ@ sӊUwI(ǷA@&i8ϥ)@B(ϡ4 1}N(WhCڊC&1N}w֘ ?tpK}1@㰣9<Ɠ@'ǦiqԊ?49F:03Jy'OJ^䞃'ԟʎq@ ϧG&G8#Ҁ9<g~c֎}?ZN@ GNƎ=)3 ~@E(9Au֔gځc!?.hGA'ތ€w= 4(#@b?N:G>$qA=J9g2hi J)>ѓnM(P0sۊn.8@!${fhq@dv~g7>dg {`whs't֌\I=Fcހ @'t8h|)8rr2}/^ԿΛK=(y=@(=1IyF1K9#?Z?JL^F)9@ I#P)@=@ jN}i 6~ s@p(΀q)9(@ G>Ԙ=s- RG@~w>~n(ҌN)2s@9Gւh=h0IP}1 ygh{@=)~oZ3@ '=G8~AGN>9~ ('nHx"G^hbz/G(#ց>cf1GI=3袀~g֍PZo)zPFs~4SGNr{ J(@ G4ڀzGϵ.h:ǧR(Ґ( l34 w(}:3 ?1@׶?4q" Z0~c=p;`csF((sGS&Qwi&zR3Hc:L>f Gю=) Qǥ&3Kj@}r3Ҁ }(ќΎ; LP z^)>p=(zh( J=A?(ϠPE-sAL/S@'gŒ{~mڀ03ьq(ǹҎhN >8Z/G8^QRbLPaQPs(}(Ҋ? 1Gcڀ 0h(t€_qFN:Rc=E-md GOPz\c@ ȥ;2{&ݳ@sܚZ1G@ŒQϠZaړ۟L[1Q;4~gBIK ?Ҋ'4C(;RMqFGM`;8 B)hcچCs9c=.h7#?1 ;4?9.}Jo!ܪy-^I0Kx%UQG?Z91-3}ጂ>mpo?ArKx$ppVKҨ^9b\֌*jZCP&L@s5gґȢArgw8~`J/*d_';FA }: 9 \hh\n/cL/^:^8$o'.^Nڿ^ cw* / 8O,#<%vt=0? v4rmkfrX U}9m"o!ܢ&7`-$ JIG7\7ZIHwQ4k x#5{,@dc Q+;H ~b*DS쵌 W`J/5qjݻ9~9'.RKX 14͹_3{Uo!\{007$c*Qwzx3Ilzև΁׌{;`F6dیœfܐmVG^I.SiI`/wx|5RhRA\v `lnm֦@G<WA< <2 ltY85wZ1ޕHe$uIw|Em=jzюN\n&\NKə5o~tv9`KW>Dm[zҵ@O9F3E8kg˓JwY<:։90zҟ7\{u+$QW;&0%s$~&gwc>e.fu{%#da&Pަ{ȏ /\x.:ӿ\(CH/d~vOsϩR3Rh#̛ Ahw.Ʈs~Tq=).gc#H4]03Z@ccG2(- zgNEۛ2jr*r+B_4 F3fł# #ִ:r '˰](* Y8jO@>/u.T7wET$9o֕3BNuV у_Ê.>9y[T| 7*CqVCG\bcl;/nBnlwo 4}~]v 㻸ǜg)EYpXGLܒ=>qqM [8ۓ})vT?f jh8u.T7c崐H}hKL)>ϵ~]v D]sNK˂6z rZ;E]Qw91=E!hGjēБW}?*B}*pm})h5S~'<AꢋʟkG3b4yd6㎞t;Sp;拮rq>ё(`s:c`Ҹ\8K<jp[?&L~T [2Xؐ3$c3u'Ʈm8Ar^]"Hjk` rog {j; L_W+ z873E՞;т9_.U7*vy?ZAy(ݲ3W)>a>Ԯ*ёFBKo}1PVdaW(=v o3[2r8̭'ܽx(\.U*988?8^,26ׁάw4t+5ٟ+~`i j| t(ƋH_FdpYAaL3)'YF>m`[06bDC ԤQFBP .6G6V0ɣEqw+n+m&Ó}r $뎋SbsN\8Sl׌li {.}!RQh T?bNFr*u2zK 4 VjܱXcҜѸ`ILQI r8*'T94]/b݌>sSEFvOP85$,%IểOڧ? h} ؕ#ԉpoFGOJ~?PI .Au 8<{I8+" U$t8捼RP|p4}ÐtVaǵZ`b*0u f֚1=)ŠN(B*N=) J1F;bΐs42?ȥGvAIz?AQ׌ :zF1 NE~?R܎Q(ޜ2>>Z4=O@т{Bds@z?JQ 4ipj^z)'ځ9i;\9ǥsZ=yuO֛8ҁ=:Rm98┏~(nTCN#>z恁G4g;)s Rh:~Lz~=i@ s@$RG@ W @ 8KN>ƃT=sP`c=} ޗ=:wo~tp>8QGߩ)q iߎ? \zh=)Zv?)0 `u(>縣4zQ(9@P3Lgiy`ѷhI.`wc9IwN< AE/IBI,E IZ'ғ*n0 1N=?*: 7m. /&=A)p=? ^Pqj9ӱtF(uz;? nT3ӏEҀz?ʌ`(8@9)y) oKCA Rmx{QǮi6~4q֓90~/hc.8q( L)) >柏h)y(Qǽ.84ϐv"){9:{t2h_ƗhxZ\*osIӱR3r:g@'ڏϱ>'& .z)>o|R}I=#ӟ@]8F9FqhAM'N~d)y}x?&=b2q2pG \gHOu#:Qס?g1G#{Tg F )A Pv}(sjvGF=1:1j1x&A.1}Ei:hs:hz~=qN\wICҗri?RQ]ߥ/J:|-Oʀޗ?&Fs)ژ}Z:G4cւ=}E>Δd4 (ǵ&x{\c>cޓ/Z\z{1֌&wE&/>ߛKL1@=IǠ~_0&Ghu(ǭ'N>_BM.@(M ΎFsi8ހLў}OFrq@NQӽ0Iu}@o=E.aI(){с׊MsKi8ur;POqKI߷@-{R`C> ;tǨ{~R!1GN1KQځϯK@ϥ<_.s@4AE7=hן;4z(Rh@ F~T9?ZOiHNOqiJLgh8h KF=!㹣4@ Rcҗ:u1')ihFz}}M/8Kn8h4PҐ qFqP04!xx4s(|`}~?8<>?;Z;ڀZN}EQ:ϭGb S{րGKQ!b}QHsJ{qMޗs8gւNyli8ZPA@񣏥J\w@Tap(&j:s@٣8=@Ftl{Kө"F S{~L{Rcg?z7~}(p₤7mGy8?J;w1? Ka1A.cƎqqKs~\qhzR7sJ@>q@ Ec)}RqAɣQGH Q3LgԤc/Nԝ=g9ў/AOiIo|R(=8g8.{dQ׎ 0MAM@ǽ/{f@#(=>@ 4"vQ?/#g֎; \FO^b>:sJw8iP})H㠣Cc@ ӎhtѓƀ(.3QjM1@uϱn.=( R ~T`'ҏs4=IFO֖=31;Qרz@ Kzf8Q4m)729?TtPi1~1P qF}?Pvҗ=Ž14PdҌ?zLqVJ?3FA>(9 l旎ݻNy8(ǭ/AIISF}{GcޝLIFq)p3ڀw֏'Z8N@ G 攓J>z~tϧPq})0; r ;h>RqRc4Q߮? w(7*?z{?Z>ʁ >".Gր J'')<(ޏhP )+} U7S8|$M \ >~uH^\b@B֦iXh4'=E/֓ڐ;(I4`Q@Z?*fuPTwTxy4~&'TwG^gTPH>Ydaң; ,$P{K2GZ$&M9qU{"֡KFdFW-l;A%ϕP7 qVdR@9ؠcRJ\IL3:Q΁1G>Ɠhq@q ɣq@lQ?40}~Qj@!Ua(4m^#in\t\Ҕ 1ߊ8h=AJ:uǩ4'>}s@ G'J^(Qa@ TSJcd 8$ ۨU72n96; lNbZy%Fi#) dt2UH"~$&ac wABjDfg9مRiA8UE=܌"ΓRKo`HVj̥X4tFhȤz@_ʌFs@QMsI'IPH|P1cG^߅P!SKۊd6 Ӂh񣧥c@h⎟@nMRB[Ҝ.$,8;sFFl; 9֥q( A tb##En~4dI7k ImGjvabnZNi"I7sz~<2aFR8#i{RQ!Z1&>})~8>ZCӦh( џڌcړЌҏaA'ց Zq09)sM >Qcڀ!?Ҋe2;(AK:Rq럨(#pi0/N5.GhLR}r1C@?8Rcl{ߐh'8 R?(z~t{~91 PORt ќ֓#R|ŰTmhJnNy"`O;?IҔgd\ПʎȠۏj9_Γq)sqHO8}($QOH#Y3n9J\`uhǩ{F1w)8=iy&= ퟯ4c\wFq}h$uaF?RqIր=hϽF?ɠz=(ttv>΂=hiG@ |}(M/>?:”ތcd?/>{=h0\~4w)G>'G$c>Qh3юz94t/ΎCשG_ʌR~"G)Rc<ќPp' Mh3gޗ@ (=(48֌s΀NQE7R`gh'@'4@}Ɠ^OKF@I{ZZ:Sy'}i4gRq`~?J\qGFqߏ!Z\RdGJ_ьu4f(/&s䎴”8⃎ƒ@vhbQaH0zPQz_󢁉cޗ מq֗?:!?\~TgҌzI@4Qu3֓#{cwNJOځ?3G13Z8^Q9=?Z:uG&8hEKsՎM;{_րA'KN{6ځ=1H:;Fh`PX>iXᐆ<j^=Mb#i`wPg+O~*!b9zYn#Ms5` uq'ɺ&!^?JtWb6û*\Bʰ9R߹qRp~lUA9dw})0{4HBg׽GRۚp&\SϔO3!O8-ai78-Κ@\qQFIRkؖ"1x y+9BГ.;:10Nx㨫\c~RLerODw-zҜ  @/j9h,?ɠ8ԀGe!RO1w`t8ުGYzA?V%ϭ'Ҍr1=jD/^#Q7{2^p:>}1ǭR)0(žߝ(旧7Qgӽ/Ǝ?4gf=4f9~TdR)3\ւGs(=QG>BU$KCrIr=*{c >8$TQ2p0x44Y sU9t&T.@w0N6ІGywԏuc,F7c4ٟ2FECBZx S'1SN۔a}(RQZ8 ci,1c5t&KH— N8?&E;@bx28L**{տZ)(YYDH@ȣ'92nh'N=T3Jˎi,,O̸<I gRX3ۚ`I9=&;HMޝ=.1?wά㩪n%=95'݉MRjYh?(M2oUa*Aߐ(14-S%*rHB6/R`R⎽(J3F}M.{g2qҀ zLd`сws.=OF=@2McGNP0>g#4/ʀ!9+IrF<h>h>.xɥ#8~?:Z:w(sZ3J2 @8 I:qRgqKrnQǽ. xOʗ>@9qKIKPdw)7z@ (ހ>dϠyGN@|9_Ž(;b ѐN9IŒ9= Oʀ J21~{Q=&~-߅~4s4z4(hEPPds΀&Onؗc9)h>qFq֌W}q@ hҗgcb~(ѓ`}h@ oƗ#GҀ OƐΗ0$1{QN>aҀM:gG>Q@x:ѻN(pG@hތ*1R4 9*#(RgZ9@ E&I})qϽ \sޖqր<>''i*'~3AsJFz“ Wڃ?6jPG9Ijlb2B?ԟUy6}2Nv۽`"gXԢ3qR&vsPmntDud=3G@2t;AQܰHQ~M(w>jn hN9=P"7m28ёXƪqn*I"q֒lՈ @Ѥ7N<Ւ3^%O!1`zy+2sQVpjʤ7zu-ċ@d cixqq@g<Rm֗$gހuI d?FgiV-FLNhX?Jq/֐c=Ͻ ьALcK|Rc"}(Ϡ LP֔T4`{tZ}F=(؟Ǝ{8P0~>ԛAi@Z0GsE@0ɣ'ӊI aϽ1T!89db#BĐ32s9&OLv8S!6ǖ:梑yX[8h} g1K (l;\$%GDN2ϥ2գXo39B&}E&sҗoƐ84K?Lu@ ~bh4c4}qGn4ƌz?PtGs—٤Q ^ғ(4`d7?M꿍⎽i0iqt{RPGc/O]CG!yǰ~#z:?J\@ jC3Nt>u4a{ց gb>4{TzbcތԙɠC&y>d1 GKzP(i=:zq?Z]w#GҎ}q@@h*=J9?h϶)?J>/>~4^Q֓ zьh{w!@ǭN/:9G&ў)1 4~zbr(КC'_SP*NOJ1F;Zӟ4pJ8zB. 1^(G9/$P8r9=hr{sQ@IMFOIGcK( :vڌ{€AGߥ.'0{R'()84QFho֖zO{Rq@}?Z:unCKƗ>tMIҗ88֗8= wJN}8p?.(;Q-!'~T“{ 2h/$phJ3MP~Bb֌~ Pu(h(v؀h;р{P1>ƒQךLql^ _4`ws{hup;~sߊ0}G^cLfqHG9@ 9=@ Ͻ⓯a@ QGҀ )8/F(=qEG?P!O&?ɥu/b})(&ΌTg=Z: _@>u=hցzѓJ>hj9B9Qq@Ɵ—?q@iq=1M;i~4Âr9Rp6@>\n3P+zϭ'4O8sJO(3J:~vgi -?Y|%5'ڡ)7UO'#? }ɥQKob1q(zsJRH@#I.lTFҝzfLP1:{ \sNzj^:Lx#Kq@F{rh4u=9AzQ@ R(hFN:ϵ R#2) J3iG<8G>dd/?JNt^:f܃lӸ'>+pJ݌97 7 dޭrNp֌q)j2=icJ̩S1jN=Ϲ(s-P nA9cVsh>~t\.Wv (XsRDe#4upXF0M(S$@aڤh"{+6)qqjUCq(ROl桷29=ʧhZ3R@tP:s/NR,1o\i MwaQR݁$라ҋћ-wFmi[Tyhr~`OjދUUM)RQIyRZU1HP6{bdP 3~`{qF?:(ϱ Ns~=FW8@ǡ4cp(@qG@ IcFE&3(&1}y4΀^ƌۊ8CJ).'NRSҐq;~< sLcKȣ GQ>piZO>zɧؤua9]E&.=*\1@ pwΓ>{0~u_j:t9b}sHGG@adfԘh׏#'4Ҁ qG)1i~Q(֗?FOp( /=&pzё߭)tǶ(r=OHqF=g@y'ހ—Ǝh(&=G@#ޓ9яLPR㨣-P{Ka^})9`@94ǭ/4c(99'8 (R}? \~4`PZQJ1Qs@R}(;~`u{f ! N(Ǯ:CҀ94K:Pq4ǷKQPh=*;vAqGQԊ3'_wGOq@ җqGNz?:3ꦗ&@8S#Nh3өzb'8(} ? }(d㱠Ύ>PE'"z_ΌIrh 3'&yRPpyGӊZ>PsFzƗ9RP1}:i4:Œ?(#փOҎP`wOʏƖc(OfA/@lI\O.hϽ' 1y)?K1p=i ʣI_v.}J;џ&ZqQG@|ɣP} +4ތb F}ixQƁisG֏4;4cZOUls;ՏUgUvSVKȋ9 jXYP:֪D;yvɫsۥ6%48GN$\B?(ɧ󞙦Fzw@F(apE;9=Lf0~P;m< h`ïQҔg1P!9gXгtc)#mdb7dP>'F;L[iS3_mcaUd;"x?:0U*jP䊪aaA$J,}"@bǰ}#eb#xy C2$ =1bF `{Ì@<- FH'9M$.y8S"A I{T ӠRAJ_hbix)::tQGjJ^J9_΁AҎր1 'R`g֗?s4qHzg@}GRmiyBh;9ڌPg z攌u=L~ LC@ @'Žhzs@iui1RcfFh'G'sGNP!piÀAcp3[J@ :\KPMg7?7wi/;$jbm~=)ʎ;KRqށ c'42˻k ?pcrshPHޣK\9' &T"}sM$ 33Ko֌BBp7 (~~<Hn"Sx 7cIq3͎>cҩ$HY Ϡy,lg֪~ЙT!?v9=(8`FA|,S:JR.L;sR"ǁMY`Ju' &7U*123Ҹ$] ;H85^߃ v ߝYJLs(ǭ/@dz0{(ڗ&@(';Fh41("}K9"FϽ/#(ǷG.OA'=:nO֌cӏ=1@i6(PҎiJu_ǭ hpy`qրC_QIi\QG>;`g֎{h֝<1ϯhȢCF9q;@ |G֔ԃM$r1F sIP0 )1S4uB7~@E(R`&|Q€2;`WF>&)7vQsKq&Gz3~hȥ=9GQގ$O@qKQQ"Fqץ)'Hyquwў3O'Ԋ:u)>4擏J:t(p}F(M=> RcԜhr7c<▐}xg֓zuNiq=Z:9֎h0)q!#=)s|zbj3SPt@==i#qh}M&})$qA=߭S@ϥ/֎Bh\^~ʓ>Pc>j3q/=M({GNgހ cҎ?ɤPh"9:Lm(C@E FpRqdRb):cdgQ(9>dvҏ'րJ=ӯZ:phII{Kރ~4gQ@yRg4z2=hls):1@ ҏ@ sM.O584+O֧95V0=~ga9>,z8?JxeOq֠8# P"} қ!"6'I(B-oYqE [0xe(n`5ge ֦ m6p{ Liq&Bpz8:1ir:nU m:3FMM|)AL38y" ĸ$c=~ZF 78?#s{q@@c*Jp3R*=j\(S);Tp*c<ޡFF#;T)1@YT#BX19 N[qHA{ȫ3;seh5 o-椠qFGq{G2(s@d}hf44M/Sڎ{t Lz >^)6gi84Pv?.LRy>n;\A~taA* r.5lKxS<4' ՘hd{f*W 1I14W_/1W0}:j` Zw_SM"M Csv\ sG@Vo(](!hCckU={1J^{BhZU]#6L|UUhW#.qSCE(GRN]XF>l^!1Ixϸ}Vur3 {Ӿz(N%*|qE!cQ* *+DP{6*U>cP,Msr3Q 7= BCgl neؼ㯭F3朷Jkps %7`3=8Jqnh .$v=a*\W"\}bI~p;QKOց ]GE~cJ(8:Q1ڀ{`(OLP>v('M0s@FhҜfڀN}:?Z):u4 -&GF~ Ɗ.(& hʜ;?Jk##18'PzK:(t'(#hUdёAPzьpFGjL`u9E:z?籤<9"9ށ8zPqKRu}hxI@4`wb(ҏ(>}49ǵ7OƗQ@ Lۥ&F@9Z\Ss@ >"^qPȥ>pM/4Szv${_Ě^})0?J18zZL{gFOǩgճIF~dRI4g4tjBp{q/`sJ3)s:Ƕ)sƊN>GGN~4si3)r(}ޟcK:zb`'8ۚ'nƗ?J\{O)~AZRsҗ s">8Ȫֈ#jr~gl{L,8)-zjBL*~\tv`֥aIc*g~9Bw?.(84c>YdcG^PYnB7K n'Lj >ǭ zB3Kړ@qIuɠc96~;9phGZN}E;ʃir:f N:_lR@p QP?9GӟƎ4~"1ɣ:" -qG2|6 Z8 572pѴg4.Iǽ. =QK@qҏҎ{Q|P0&4?TGZAF(9\Q֌wis4rOQ? 3FOK)9(g4q(li8'0⌌ S>Uh'NQHQ;QBi*DW'㠣q@9n8F+R)x?@ I@ ~@?֗'4 r?Tcފ3GҀ(>}Ni9&={qQsGdzR@ oƗj3tqK{P~9wϽZ9LRzdchzR~hǿF(ǯ4gRJ1FzƀԝGG494PqF.E'hF3Iz^(?RuZ(:PHьPw֌s֌zL{AQ@ K֎qI;?:1Z\{N; \3~/9&8ѷ}qG4)8Kڔ@1Lڔ)yPQKI@qRފ!擟].hHAǽi)r:u&CA'IցGnJ { Q7B˻nGZlWwHB9={ G#4X1Z | G.W= CvN÷ۯW'8sR#6<-H=  vQThjN3T4^U@&[z (_"Vt_XR&퀞? }\T|ҧ\W!ͼ0@,Dcsn8)~ 6 BA?",flcSO Fiu{8ch򦃞A)s(zTg4PGnxǦ33);P1QKH4>;f9hg׊NKӾMǯ@ߝG (AEc=f@ Q(֤ϯz2jLJ8|})C^=)?, _-—"؝tTcg~1UF^j{f d (WoBzhh*Yn97ozjE&&-'Ҏ?Uf~t }i3A#I|ѐ{R@d}h w恌pxB1KQIyۏ\@ך3QK@+N;:E 8)G)rҀ@>=? nISu\t ;<q40hݞ>tQPd@c~{Q9HO;~;bvzR=?:N_G#Z3M/9l}(>_?/GFzfqK{('AFsրE9h>t8'/q׏)rFyAh2}risJL}Eoΐ.r8e47^" QҗJNƗKhKl~4dzQӦh~tgi0:p((FE{ގ=h(~sF`QFsE{Q;4v(t:2=hϮixi3Oƀǥړ>gڀ џjG@Fh"NGh>g=Aҗu#4743I׊?ΌҗPpz=s{Px돭@E8QZLcGZ3sc>~4~tv((ϡ{Iz -R !;}9#/{ LR@q֎ON)3@? Z@ßQGys] /ϯi0{(*3ۚnԇ.@Ҍ3Pz7J@$b`BFsR({yjLؠ"<8+܄!K?7`MJ 8=j~a׽JAG;셛z P y3ǵE*r;PToy.˶'c'.$a}tF[ Mc {m"ݴ5'N}0b<{ǯ( Rq@?)$f,>{cx'$#k˓gqXG `ӿ PKHBwQh@qQGhg<—Rs:0({zQ;Qځ xO֎(uG$R@c4c2i2hɣ9$ѝz3IǶ}sZ:"#dcl}iA0ߥ:=$gڎЭr"];'(01ҡiC^/\:[CaH-Oi ʗ3 ⠸eBX1X9E(rہ׊Ӹ@-O*pxQ|(O88`AĶE[pLu`"# 7jXԘ|h`)5(c@qo҆ n;ws( 4;=ӻzP۰:8AތҀthZ_QϨǥQ{(ϩN4Eh&Tր{~t`@ z яJN}_ƀ>Z0q@Gҏ?~?'~3F{~Tc~h=@((1џLǭJ?*1Az_i>t(֔ AF? 1QN4cߚZ\sր|т};#sG(֌J0OCFA}(< fq@?`zсdRq@⁓Ru/Iz:tR?8(={?J8>q؂~r> ^)qIƀƎTdIsGEʀ=(=)~sF GoJ7z 84 \}(8أ tgQ~t`b)(玔~QÊ?Gd{pG֏z1Kӽ's@ }ZHh'C@ רH92p(hݎҌv?J3Qɣsʀ lьzb\fP:dShϵJ3OI)8G^F(@1G#ހq(PKߚ01ހJQΗ8}ϭ/>iϧҌc@>4G4r;FHP >wǿF9旭'>sG1Rsig}IBp)^{sFEQ)OG8"?)) b!V |tҟ}EPv1N<$D84a!9 uDZD.:n2HTBDH`<Qv(PN;b $6cmZvC:qfJti\r9}O'␃=E0q@y`cuLw$]@9N?S@PI1'~RSɣ"+  7o7gQ[.Iz^ze?-2 ^8QSf&cҭJIb#1C崌?*]GrdP :SBQRR&:T}ix}y@ -!C9M֎?*(Q{QېMhIɥ@ 84cfL; iy>Jo ^!惞~4@ >1IR*3&}2ixqELs@ y"z2=@ 4j;dRpz}}ߕ&> {E("fr*\:~ut9P4?9~5 ،4^Lch4zP"o(dUc\(8v&r,89>1M0\tjUʸ8"cMDe-Ґ ={\gz@q34oŸOMT!vO0@֡fx(簩GAL~94hTC4x=>n K@)"Bi׊2'#Tjp _8=j̰JrNx@ۍPrLJ1JQ Q׵NV|,]i iELԍE!q+dWV(XK.ӌǘrJ$R:)zBvm93> =R.6H˝[='`# 0 NSK)@ uBXH˖= Fl#Ҥ ݦOIJ3QzB9ʀ9\Py=)23v>PsKQP:9.q&yPttփjL€ގ1b't N=F)pzF=)0=(dN{E,ܿCE ҂;#ӊ_O(ɤ7Ab>Z0}9=GQym`684 P:\w╄ Ԝ֎((44|}i2:Gn*( 掴ch?*8q@ II\fQ;Qt$<qwtۚ]ҎxȤ=(HϭJ0?٤ʀ }(Ϧ(ϯ4}t8/Kی CԑZ^; џA@F=1@4&Hg׊Z0h2=ir;3ڌqҀ:Nߍ/~ߕ!`=)r}GN`(?4c@i| T}GOCG@=@J=w4Q! `R PmޔNEg1_4Qxi \Az~$QJbRsG?Z6z=(ϥS@ތSI.P i=ΌaFs^}+~O4qғ֓րr03ԞdZLIswFOnE&=Z1HhQO4Q~?>j>nZ21:b(ր֌PGџq@^1Γ&J3I4Z^~`Pc_3'Ӿ{Pלc`惚1P0Fx{ހb{ N;Ud ^le=hǡ]]Vm @>nzуP S#PJVyqFb@y*X.2OÊi6.܊1ހO^*D` VXQ& 8ɣFsڑ"'/J:j#wlSnd* Z|veRP:JQ(f5Y3&0Z,Ş=h+.rN*`G"0`c~d m$04u") E!qH?Nc ғ4֓\q4“4)bAϭ=O㊆e0-ۨ=y5ʡcӱʪI^ҒW=.uba}/Z 3ăX?EpT'͜}3M 'Tiv'Pn~b$N1ԉ`{慸"mc9԰G 1N` z)Nڋ:LHvKcHA. XT/ʡG@DԄ s֫$DgŅ4pF'8psZm1}m,#y8۷%؋s.xO4AMj2Nu (?.@б4Lɂc*Lܱ].:uPNҌf:j<+9I+ČSޱ8H$=隙#@s㹠Js)2}0)}'ւ)p}(?si4PA$Ғih?GO_Җp:ϭ&GG"</&N}h?.j># : >q&NpKIf6U6[ULt`ȤmB\Ufփ:k[+Z{׽2yckm7BFiڏ"o?¥$S#Y2c#S->a3MpheBpHbEYUc' ;3ʓqgK4&?S:jg{WzRqBvN(mɕnjoJ (` kb!l)=p3֪ZK+\yg91RAp;P.OI88=H OΗPu "ddTuaGN9ZB>R}ER? ?_ 'Z\4$;J0OP(яR 0;PFF}i:iԘǠs?:1} &Q@ OF}>iy`zTc}({PړZ\J t#4PF 3zG^֓'M1('IϮ>cF=O@bی}hs13I4;9p:s4稣s@ϿG_J})9!*LHc)9=UPs3I=@ zFqԊ1(=@hQLR@Vv;Rt)@KJ(⏚h@ ?.=FGҀ=΁ zqK_wfGr?:N:sKϽǥ/4=@ Լ/AIxL ?֏ßcƀsirҀ Q~tgh={P:CKܚL`h :@4~tqrh΀?t}z}h'FGlPi:8h0}9h#sFGL΁N}=zRr.;PqzSʉwARQI~ B;$:4nw*Ę)>`O%-@ zazQQJn\(CE 0NF; ^x(@&=?Z8'☀`qR?ɤG@,zcC bI*jq5^G3,eAnc zQT(#5?w5Nb dd\E0|p>n/= ;ULEո>)x^rIsn ; R%TU1*PƓ$#11T#UUYZ'v\ 'Rzo`3W"c$JXlb4_,0dh?ӷrzlkx[tɫ5]Dw>W!8\9.,@B‹:mϗԒĀ,V ۜ]1tX4YWS;F=GjPPR>niQUd*78U5 1i!"cQ&ۆjLPL`PB>*H3|(تXNY]}8r#6BDlv9W0+=5L?Q[7GRYykklscr0J{0hk͹84 `,#fcԶ> 0K3R?u4>Rz^z@qd~cs36 ڧM#OlՃV٣iN#$'JC@w F>#"}PFTwZ \sFy$Ґ3rŽKȠ3Ɠ2<⟓OʪK$F! {SJh,rӳUl , r0X ۊ?'n .1F}zg(܏ʀ4w׭s@ )GLQ^h&1Ҏ4>y?Nh_ƎhPTqI鏥 : 3ǡ.;f3Kjo=8 4*#(F)YH=F3PC}/*@5 +oeQQNd22}m4X Շ;c$(lT6UKCpDr3ĂHvcq1@HLU[TFzgޛn{x5>}?V '>4J;pOցOy'FN9(z}E&4;g@ Z)yϵ&:;~tqi:ޗ'8? g4~TO$chPF{SJNhcڏNJ_Tx>gҗ>}(ǽ&=+ǧI J4qߚ1Ə0; 3@hǮMb~ҜPsS>a@'{R~t(H=#֓3m):t\c(҃M'q4l.OzP!`Qߊ:q~4`8jLKthǿ@:S4cڌN. ZLc41I;~Bx~c4{uuhӿ N 8F1Ƞh4P=(ǵRI"D=Wd([ݎ3@\F[nyz~s*Zd+V`~5 8Oun? 4|j٢Lv@)W,쎣2Q(۱O,0O@FS65]jSiaϠj m ʮ:x|qM-[i#֐ȳm FjqYǕ@ %vr942X$䜚uʢE=0XLwSA޷U2iKXQ֑ hmRK1CHBgiC6HNE sRphtOJ8=h{T9=Z=Rm~4>cb֓8Ɠ;{'N3KSIPKL{G=CGG?J9ZNh<"CE4R;M<ӹӺw?.Nx)GLsގ;f1}Fh=RhN*8D,d~Ot'HG@s5,DG'ǣ/5.(,hxGV`9*QĨ1wԇq\}h<ʐ%^x?JR lQ㸸84th:揦MTr&n }E?Jlkg8 cJG~( 1I,RmSc֏1krcݝjhgwi) SsRv Yr`8;&=iy\@)z"~Poʏb€_!c4~1 &:ǵ/ғSKIT/>'=֏(9}sG҃h\Ҏ:)d)9'O/F=P~&'ڀE'&OҝI\P}jpgBPUIj]—8QV\(-6xt |p>f5`9;$x$L1ڌ&yA==GRhj㡠C}Wj |L*~t}@ʑp9PyP:ʤS>TB?$Sm?8vLP7۱ǐ霑VBvQ@ ϥʎ=E)z@oҌ 3@(&s◞9F=/N(?ҁ8?.q?r=ړ8!KǷ)3ԁz\4iߑqG=9?_GF(3ۑKK(sEdf@ z/P~R`t~&G=8<3F>Ɠ'994)phgi4i7 Q >QڌuAG?JL@ ;R~T4 NhK((92)>lBtdzir}(hQ2@ Ҍz(2s-ZLzʗC߭_Ҏ}M=IlQњ>E/zPc<4K'1@>9sяZ2{挜L4}i1G=Q@c(րN~b(8Gրj?)?_4Q('~ (V֡zG0rz4h\t##ڝ w# y1FWOj|J.>z94\.Vd19hbR[Բ4vIp- >V bkh|x վsKi\.G B$ڠI2 j\ϵAK # T>ӑHs&++C#.9e#aXJ^sS\hqޣ89=*mF88RQ;~Fe r9QZG pF:TQ9= aC~S\l;uǮh=}( H7۾IOz|C0ɷ!_CN@PqKo_SGN^1ޗMICK~E/h9H?>Hqߚ2 3@ fg$hz( 8qH~RH@G0Fp8@3A?Eq8ix֗hqڏq_~CE! s@Ϲ4|ð'S~ C Qc#џlP3 1sKLR(vƓϭ&KPأڝ?*LPQҎzgϨhQ9($\c'~s@ F9 p,Ҍ~?JM޽}}?Z^{m ~sK gb})( 3KoSǥ(;Ǯhҁ@QǿF}zQ?OpH#AJhϿ((0{Q掽s\ZL;\qh(=;qќq/^zQ})x׵'|zSE&=F=Z>{O҃(@ KjJQϥctE;LqJhF 3(gd@KA@ :K1@ ۡq`>cМPx4v΀9F=F= );st&R@>U&;ϡ4qPF1ќяaF{L=y(<~J{^1ր\`؏/K&sڌF?Ɩt91Ib?&AF9 Z:Q~pzI~4c( sȣsG'.>PƎ{g~CGGӊ9'ҏ~t`{GZNGp?TNOΊ(H*'hRGGPECǥ&>3K׵zb$ҁ_ƌ^hǵ/G 1I4 vBt4cږ{G_zRqנ:~ LۏjlȅaNGqB㎠Pf˅瓊|2a0i! {)&:>H۞2"yp &hwt3`sQ0>%ϵ&sMI*rC; v5([njD\f4SUךI%.:~IGMD/`,IfLk*9°?CJ{P 3Q_i8$Q*VQ`O=ϸxP &InޗN*LaUQp*M*SE/QITK8eܧP=>TK#9`w{ X —Jk8Lp8gvYR5<=ݓ`<7Ϸ8 )Fң@zwp@=P&GJpzs:&3ҀzTCqޞ%S'Xn`&h}iq֛+m`q֐PMW`3CqS#+b  &9#2 >p84$E/QHVpiY\1@5k#!UriypaH2m HڞFKG>2 $ sߕ/?QB?6T`밁89fmU]U8/7wc7c lqXE*U҈dY~:YH\qLkPWp3Nr^#4J.=8EWf1L2,mUQ@?*9*E!V ~SެdpV |Y`3}C_FƠt } uJ.@ԻF;cS yUqn;{:f; BO9URuG2J q8>Zhw0z@RzR- SM cwMrp3׎݇NL8EUs(0cl~g41 Air3}1ԣ9jL")րO{Ϯ) =.Ghܽs.ꣶA&{ʓP}1@r:4S}.i=y@88?RZLJ;QzQ) nסoJ(@qJ9HE.pq@L~FG֘ q(9vc(LGC*8'N;:}K>4>LQmc#a9&1RPgu(#\RC=)0Oo4cZ:RcKA/)(Ҍb4`SG׷ `c@ \(ڀ4({z?/Γl:џŠLzn=)q@Iǵ-%&\})h@ Ǯiqh?JL9?G^PN?j\s{zQ ? 0:Fӵ.1`Pgڎ1\ۏ<}@9zQ{R ^{:7QөҌzt?SQZ;GxQ@}>E/@8GG>Z^} L `яJ>pFxrz}ץ)4Qc>`яzNs@&&2zʔxQ.3FL(M֌ 1G?Z9(GJ0(fq֗ZNhڎO&1@(ړ>Pwǭ/G^'ci;RGqF PGOj;8br;сi{Rg=@fџjJ7z~Ҁd{R4{g1ҟޙ*2ݽ#;ffCjة~lqɩX" Ia'\`A. WSgn2TҰ ˑbE )ld`Dnq#.F@ R@n޴F$`Sac$@8#gػ 8Tb+)KQV0I#R-p= B&v!ZG@+$yT.8|,NߕA)D(;f;oJmAHSU^ݢgNrAY LLN钗Uzu#*>+cQdj07 >S W`]jz@Ȧ8T*: `S ۚpڊ1K!>U3z";V ̲EDy5ѡphNSO0 ^Sz "Q|$|6zĸ K`pF2*FNÃϮ*|p;G\0=0MO{byM. ?Niy{CZp 87OP 3ZsGOdv^֐O)m`J>H1KƁQߜ~4quOր ڎ=?::t(Quh bQH0z*^)dtϦ(֗'j_“>(=('#PSI?_ܯӚ]{K" n(y:s{?(tϽhŒтw)0}ZԚ^!I4wm;P҂s،z1E ގ:>џN~ގ;h`z;ڎ@*OǏ.;1PRqQ'(ʞ/G_zNiʀ~?MڗRp??J_ƌL'P;~ zZ9P}8g惑?Knh8z(hy>`RsQ_(zv 4QR v4c4g֏΀ 9N)9#KlPG4{sGJ^s֌bbzNG=ɠ3I~gqF}4A(9SFc9h'+@ G8# f(9@>z(EzPzc~_={1htc>J0{Q~}h@CGn(ǿ>s꣞hGN֌}(?ь\Rb_Lъ\U{R֌gE`RҀIN}h)h>^bGJ0=O@=(MN=(=J9Fs@ ϥq~qڏn / j1KxRdTzR} LQʎ}*3("Z@=(#?19=}(!sZ.=M'.;>duq@gi0= L\u >cj>}1Gҏ(?ʙ2@3L~Pdy*@ ӎ\Drʮs=i!G"Q3sa ǜϿU瑖HɌcR@;!!Dq(OlTq7,`ڣ$0͏G#l0ic$Δ)G<E ##%(V;cۃ85<2CJl|^O!ѫʓqQOޠS>Jc`̿24jHs+3@1NYeaa?v=? v;:TprU9iK;F.O4ƍdebH+ބ2,H2Ʀ(؎ OHI|õ9`*!8Leh  v`%o㚘4O-pNTt sձ檿x D2IX].NcY0:0SFs@;PJx8"B]0HLrzeQ0B^F8<4=APPj J*rj|zUA"cs.?)|1s@ (V9RvW+Vx56=Q Sc¬`UلrUQB'92QQqۚ,%ٻJR:Px9* Q8Rõ4!T95Φ>I$Q) q@ c1&HvTƇ;y ݪN=*,6;$* ÏJj$Ɵc!#9 p;s@4zds_Ǝ1F4c4QϮE&G#qH1-\N)0(=x@ќw с^1ގK8cǵ/ϵCK )Eg+4R&w֚=T/AznNqLg< C'n*Cw(@1Њ3*rN3Rܥ{>V&}i3 ^~ '8)qOƐ1GS=i O(Ғ=? )x}sAIRhǣs9 $>$T_kȊa)X(ϽS[dfIZ$2[Y9r>j(.R|#X'IiЌ$@ÐFs@ ьǽ@=@? 2~ѺuQޛ, osN `fQGs/;((Ϧ~㚊I&EpķL-HGz\c4% ,p1ڞց6c&rXUc;K``Y,ph>dp9ځĻGEGSda;6'F%`隁/vIc|M 6-Oq ,ϵY'}M.M.A=1G>;bb"=F;Z_΀F1֓hө8Pqip?ɣo€Š\NhPzHFy)zu'ފOQ 0}Z*#s$ uu h)sBp.lR8E(Kǭ;0wW̐.zbںYԪ9EX0$\{惚($.NM9HbHцdJ(ϱ4u@?ZoTownQE|d078C=EAq JU#þJ@Z;Qw v4~48h1ގ;KIqGOoƏ!⌃xRǥ}iyR(O4QҌ (R)84c?Z;џNgր Zi*Ddjq. 2ygG'=EMIM2 `cI4(>~tbI9b1Qe[RtbCGe]?@1@hFeO.$ГQ-,2)iEY hϱ`sC6X{cژeN7u8< BHR44܃?Kh9J&Ӹ֏0р}iԝOݠx$t3SCGOaBm#~c){'іJ^ 11ڗLR4zG~$Q/^€'8qF}0=)phLsҌ◟]'@r}E/jnҗQKڑ"cҙ#$ z}Ut viqDž;zBU.yCas9aZ5 O=5286ЅO@{ӼU=;s 7+'<YہҗA2˗6oU }c 0ϱg $KҢ`T)7y=psRS#u1PM0ާ)JwZMj뜌22٘K2J!Yd0<z 8!lxr3֚i $*ELʲ. +P|@.?J頙UF5kPGptc8;1S:PJ YOn;t1IϱB)c *nhj>њ9?zB#9zR`waGӊRsCלP!~(ǹ4~A(HGQRԙ#7!*8ZX ^1{ Ur%Vz.0jE+9 hh#07a&EI"@5\ĎXYJ:? _&q#֐#[-t!fܑެdU5x?Jě:O44Y1"ʪ1ȤIȹN|9\*!62nUV8$Ϳ~!8c9U{fQ21a}xCi=Z3@a؊@)TM(֏^~@ Kv>n?.)x LG_G@сZ\RtHb ucQyGdgb8=F!r:QIR&=(}=)~4Pϵ&sKQG`u>9iiHz`ǭJ&?*LԸȤ!8R =xQ~h}Z0KӮ3i~48(ҁ PL w'ڎ2=.E wi>~h98@ozN(Tq@;sI)9ct:? N/N~tڌqɣF@h>P3I.G((qKFh:q/@J;Q9bQ4gB}9?.Oғ ⓵/i3@qIzEH4qgn=O@я)p`i0G9(ch(/nԔz҃i9oƀ A(F@?xj\{f㎿;t~TocP1Z>8M/ڐ@h撓ߑ玴 1O֌{Qh@E2 x+בK1> |PX{@sҌ}*bpsHp;Q SDW+?'٣<ҥJ.߁;sO1یc`es Ug 3NxbY=)S x Kgi\X2g6l(]ۨ߹خ։_. ,HtAsrH6c\=RJ'h[udm>|r :8 339(H* 8SV3P];mTc tgOP]ɼ`Qvo] $v#{10tT(#rK}J+oy!A $ 7ަM "VnےFA*y]_( hLkd.Ӥ8 b7kR|D,p:❜tL;qO5,L9<4ڐp=hȣƀ+ܮunjT$i9鞕$g1)eyKɣpG>Xk )g9U 2x%E44Y DbRU֥\m)fyq`>G2:LZN&-;qWIY6h nT\Wd-UsEx8O6ʦpiKb$ßtV06H;Sp 5]C,=GH<'N(ۺc@Z4EsV$`Q9dIZT`ޢXgHB9mYH='ۚDV!$=SԿ;џ^*]\!h U<9RBhG#o֡)-̀Qw$.O u>("p >З}haޓ/@?aE&~J^hh< 974ECS-N!i\qS3mRĐV\?tT|*_֛+(!wrzT"c.;xmY<ɂ* 3|cwN`%a*$Iɔdgn49ْvhڟ#?ʎN3J>fJR7Lѣ4X$3ҭg"V?g{x!>QFh8M.}(>Gs\R?8Z8> րsQϷNI@ G>JN:`PsSmC 0,c4\;l4S>%[| &Lr1oie  p46g#N㸩"BGZC*o(YwTV|Niq#nq`E+kau,+U9yBL)AdpgR3lge’GqrvD %C0CxKJasSXRB6@hA!ho^1xP0zS޵Q֟v󑑓Ub*+}"5 N{@ EٜׯJ -pq;I/19l& PaԊK0Hq8{DmݰzqM>l RaNxM?jUVDݻ9p!ܾ W[;ͅ< `YPIzрNyz\TӊOiy$vR`_N(q3 >.? @&OqFO-&q$}9 hMaǽF?- lT{#ށiZOƖI^Eizz@h`zQGt4~t1KI((h@!;d{R= }h3`gZC}'/Z^>nR%A#M9hr(I) GzBE|џJLheE/^s@$s@ȣJn91~ʏ“4 \њ9#%.E8(N{Ri3@#E.h#1@E΀I:Rh9֓4Rg?Z1s@?Z>J?Z(h@ǥR? #hQGҀ s(?g#~O@~c~tv&'$ 2q֗c1@ ѴzQۨҁ?&?Z>dzQGz3Rh-&Qס 0}qF^h?*9"uJr}hz^qMPސKӿIf)hM.p37'8Ix8>dqhW1͂ MI0~n<7ɀ9T$=y'Xǥ;>Bf֐VM7*p8rsp9fMr-09\;UQIgIHYaDf.rWH@a6Kr+8.00ԁpNE\p)IKҐz:b}@g"ç4}i6|mNL6O>U@c+׏Niry"\jD#9Y>)#>Ծsz Jc;GҎ)04PiyBdґdlp3;JC(ڒQNJ[>MYqJ64qIF@ccGKtiRѲ gҟ'9oUR,aQ$>QRsp d`稩q⍠ qM;$Dq8uX8ƒt`uw6Fj"cWVdݽ1@UQʐ"W;763gOA۟zB"9WÌ֜y֗rg8Uyw<d n+r4Ɋ\x\* G :P\FX޸5*L8="ӊ/\O(NAɫJ";ݩTc*Rym XwyVF6=KHL$lpl7@&qKҋ:@$(gKFs@>~tQϥGJ(֎hǵKۊIf 2ͷ@捪{ޔ{LRR`vE>1ɧ@'zp),pL TT-cRYGF GLAIS(;NpzbeX$q$k!T )J+crZP#Sœ<剜|T*bNG&uY !iD0qlOG`S\i =rHySc5V#r)ȤN:R^hKz0{P5sVvGrO,,z9JW -q,sFcr>ao`$#i6)w /\ZaS+28j|{~#Z@Da[3ۊ{ =TбFpjyWg>q@t6~~4K F}F(ɠFhϵ(G~Pp9/I@gӚ(ր Rc4NcQۚ\(ϽWلdF.8d;DLpNp{Pd|1YA#4ƻ]áJd1aґCnj3h +,GNGnQQne쑃SOI"JPO1F3MB6xqL25!5>>`p2;ЈРg ʊv}qJ>uc2VIo}ug"U nBF1';yzvJ3Nؘ.(UQw@ I̤֑=gghBw .wTPqF/wg4ٟP:bh0O<J E>Rh8сҐ9";ʀ9qĊ?$uRg&F:=Z_w&PќN)? QPHG&gqz8h@Z3Ҏ~@pzK?_j84 揮h=:Ic@ FhO4ў(IAF8Qހ p(A@FI{RրMG Rڏ€IP914b>iN#4і鶀ϵ% 'Ҁ'"~PI(|Qc@ ߕ(=iGQLz184SAyzb}qs@ OJ?L?{ 3Z;iF|=(ցzGg=h((Rc#.>'iqs@~tg) C@ ٤=)AϵsIAKg֓;Q38^("{QPE'h=((()2}sEj8KG@ t>P>Q1@GE:j_Ό ҂)=i~(4PIP9QҌPގhќњ3?Z1E)(>#8'ړ 'Ҍ(y旯Q@&}qZ;t:;NGj'Ͻ/QӵqHs\F&OP(zPI;Jn1҂PO@>&=(PN?ȣѸg@ >~ʗJ!SJw>ɠ};GN(:/G}(ց9⃟-&}r(`zP4ށ\8s~=B~C\R4RǏ} {QџcGH.~. 0)zwǷ4|ނ 1۵zP08ln=Q@ ֓# 8Ͻ\@ ֎;⏥Q;R{PG'@9⏛}q@`r(ړ\ր (&9\ǚ1F}&O(֗'IaGNq@9=EM֗ @  ϥ/ҀcQϠ(9H}t;MSK8){s']ߕh( ~)H$vǵ.>=8jq@ёHIP QZ;t>vǩ ߍǸQǵ(ǵ&OcS'IМbRd NPR}Z3cQtR`i{rqӿH)rÀ?:^{iJ99즎OB(;wz1z:QsG=v3@ ۧ4:uQP{PA >rGFsJ3܃M=Fh{|])qg P94#! zFA@9&I>أZ_'9giq(H 1ҏGK{Pc#Rڃ' /}h㰣nhڊ0xh4i{u`g2G=G4`@G<<@=(RRhi2E撊1@8~}(P׾hG R~QF3ڐzm֎(GG4K:=p}hǽ.GNPGր Qҏ€G8E 88ZSE&yzf=i:#Q8}h{zi8>Qǭ~4{FE.1ю=dQ4g ? 2E>i:t=ii2Gz@8gޔ}M')pގҀ8ȥy'8ր?.8яM㟮hq)>^ǚ^F}@ F(ށS1h?!z^j1Q3ƃHM ڌc/ 1b|sIP :zޔ瞼fo /M4;gJ3(;~J3_~cן'Nߕ.=8ǭ&KPhǣRIPףdcӨǮ:n~`:●JJ@'F=Z>zzQo֔{R~tl~}E-v4Gi8=)~܊`-!)@J8i?) =/}>Q:t\`{Rgt::'>ҍxO֗tQF{RtP LQQb}G{⎝XFWր֌ǡc= 4ގ(H@ȥϹ81Fڣj34~cڃ֣\}3I{b?J0GJ2sGB(L/RG)y(=E$}iy@  t#=E.=(=*v=Q€—J;QAF>u)0qGО)h*\g(`)q@?:(>4~zqGgh9(~Tt(0;~Q2qL@ h(88(=>mq@Á<(P1K@(&O~ǩ F~7 IJ!㯿J^zg'{KZ=A*=϶@3ց^q)$AAsr;Qǥ# i04dq.)ҀGJ0::ҏ“ #aҎ;y4CHFzYџbҀޗ4rH \zj:qh1OP1(ڃ? 鏭' .}PG~iz€>(#i9j_҃AIHqN>qڎ889&)O81—u 9:ҁF=(h|S@ ړҎs& )N:4}֌OғszP&J\~4=h4`iߐ4:(gKLJ F?PqK@zsIǭ~ёր^4'_J84gG=~s9wǰG˚\qQ@ QN{PsF sPfj3G P׭ 8GRg)9րwlRgKc>QҖ}qGNg=qsQ}h PN(Rc ץ8I\Pz L:\;@GN(i>= >~4~/ ֎ɣP?m#?A?Ɛ0c46AzsKQ@ ZzvQQ=ZNFH֗ B8GzL{?NHM/O Mop3GNLSJ2qԾM =Ahǰ?7=0t=Qތu24gw#К1!>ߍȤҔg=(g4ZJ?ց4pi޸꣯ )JMP!;8J!NbsFyʜi9qGs(8@hOh884g'#4֎?? (ђ{P瞴 Q ׭4cߚ^)0:OlQ?@ b QG9 qGI_IdaH@=8֎q@ȸaCڊI>@< w@ќ~/Z=3>?v}Fs@#GSך^j9zzR8>P0phҎ{rBߍC@簣$z9yE&N9y֌  ݰ?w_qHG>O)y:3ߊh?#AG@Q&>{hϡGQ֥ q'֗\Z\sƎys@#=qs@֖?z#x搃:w qF/J!*~4m9)IA{dP=|挜ɣ$`P`h9=Mbt_Η :h1hϷK@M7BE8H=(x"{G99/4wMpsQQcIz֗oPds0{݈E8qtqIϵuh}y}hw>p(('(h/4g=3Kh??:tⓚ^ZMqМE/fPGEPTJ[yRzTLRtPSQ׽/Qϥ'/GFs@N~$~Q&f4z2~ȠIӚ>F?ҀA=O҃Fsў;F;њNs@ QF 8ϭ&r{xϩ?Ҁ'^2q{O|~'/GZLc9P>Ԡb49uRqhii8ր {1hh{ ? ?=JNi~4tO{̊8=)џjL 䃚14rLA=9dFsڎ(:^ɦ/{ 94~A(JRP9t4 J/Bg_dҗ|:qF~\4MR1Cѕϥ!1 Q:SqQP1sA&9dt@M'>ʌ@?8}M~t:f1hJ?h>QP0w4~4uP =I׌Px!I@> zdGP t)>8aȣz?:0iyhϿZC߭'>`)~EHJ23ފ=@}94?&84p\>ndcŒCPsH4f8@ _F8LC@ ǽӥPw֌4tGh^;P~T R;aOIӶ>&(z]89hlΐ@*O/Nq@˜J8{њ7Eߕh/Ҍ;~{P"f֎(ϯcޏŒgv&v@ z֗("{Q@ŽM{3@ZJ((>s@}補(Z)@qFA#PqG=CGǭ7Kۡ&}3i>џ&G\ Bs} 'oj>qdt揩(y~MҊ@/Jҁ89PǠҌMiw>dwoʀ&CAJL.Ϡ| _Fϸ ◞ތsցNd/'#F(AfIlc ;P}>(FsZ\1JGGJnB2(GnGMހ}@ԟS'Q׽@_z0}.:Qށ?8`~mBg 4v}(ŽP9Ҁ@@MA#GϨh<Z=1i8d4uh{Rm;bK;J8^h9=LPsiG4 A1ԸԤ{~\P =hc=MqT4R~tqPN:Z>`@ϥ$QhIǵ/!'(n@ uQv4cQF GN=zQp1Hrj\cFJ?OƗZ:3U34O=J@I:΀ L{>\4sh9?Z0Q(#.xץ=E 8M OLќ.IfGj3cۊ1( _ʀҎG((8J^=i)qڀ qFEwKjCzPGA^bbR`œGZ1hߛFM.}4c?J1_փ~Tg= $Q۩4QaF{cP1@8wIƁN~~`p2~恀<{})y"&ў!4Sp>K44T sG^ƀsќtwII(H9!K}h9{Q8"}@~Իs?1{qG^Ɲ 2~L'Ō)R);PF}:>P()q}M9ϯJ8G@@n}qKzHASIߚQ})9#s?/>4@((uFG/py4 &s4@4@ƀRg??J?Z>@1@ GnK03 ?:2ǰގ(Q{d ==h4 j9>\@p)I nF1ހu<St&:`( @ 4LR@eE6o4R09O֓sPs@ޘ(Ҙ ۸8tciw\^~9=9=K2} .Iy Rm=@PsIr)@?'ђ;K9&IP0(lQ:f!Gh}O@ d~Fi7BߝH}q B_m`tP7׏z:d1܎3u`cJ:a'@xQJ=3@ (GQw<#GIK{:vϨ4z(3R=0h{ќP(G==cIGPh;֓4?4gq֌c2=iFOp֌ZNS/4cG(qP dџhZ{@-Ks:ph}iqJ2Oj]أ\PtGA׊;Z\: ӹPF{t,ϵK@)1R?'#4nVq@$~'nGJq?JP> 0j0GAG4c?h/#ڌs@ƁqGNM/8r;b 9i8'N}i;* 4t֌PhǨ P=8~—ߍz3ց>ԜԹ?Ҏ}@ Ï):3Is׽0;uQ@']=90;J:u L";?*FG\:11(xFzRZƎ gKGZGj3? 1 'dtڝG@ Ҏ}qE'ך3G(34`~(9bfsQRh`׽p:((>"J?(v!:R>>hހ֏ޓ'Z:ѐGZ^hڀJ?٧w#`{Ϡ?q/@ ~T8\}(Gn9 n8HqHF=1@ 8I~hG .Gri?R@ {gQ@ ):Z3GK'֗>ǭ.1@ģq֌џz; O>G"qAwZ2}E&M)@8֔y@~Rg?$Px\ѻ;N=*)ќ NH֌RqҘѰi~)0 =g?: RFIOj}06}1@G%i3p9n<|@'֌NZ;w fޝ >gHҀ AKߒsHsp@$yҚHu;Ïzc}L1?_j~JC~9?Pi'&(:uH1P?t.;NsȠCI{R|P40}( N=9Zx=1F?/nqHr{Bgi:G'ui:w2G8ϵSwzrz@ R搟\PzуBGl}ip1@ 3ڍw~4P=iyO+Ktqތ!RzP1tb}hlpA=~v~tnP#ԊAHhϵ&qG^4hh I}is~dPt׊^nǡ/^sϩ4& zKM'4~4gր8Խ擯PAzd"1Өd3@N9$R< TLc~= (#`ѐ?&p(ϧ@=MƗ'ހ wh{Γ{ 7vӺ8E&N;gڀ>gFOhv="@ ;xKZ:4c<Iߜ[΀f=yǡ=i_@ z\Pdt o\j_rz֌㩠(ю.}MhoI=OK~)2ݏ@QQӠލ݉ ҂ր&F{ߵ!8?*Q)4=J\G$u~4p(ϡ"}i?:\QQj)8g78 ;J3@)>T|q40[т?t"€hE/AhTs@GҌ0bO(qP!1' ]84@`w4pz()1P1 1\QU'AZ8o8t iǽ(c4v9(sҁ~'4}*\Ҏ=I'.ccIFq?J9Iߦ?.Oq߿F8'}ISEsփAǵA?Z(##I׿K?J&qFyK@1dt擏|4uIzgHycǽ{RQמE#}hǽdQ~t ;qG֎q( dwOFshր4L^(сAyb'H}9E49>O M<٤aJbR~vqJ(> hi #qQ@1ǰJN}@hZ1^}@?J? :4 cIoҌy~G>P J\H2:J:~4}qRs34bHNE/4sE(g4@Hi"R\~TAF)8@ Ϩ> gi2=h=zAtd@ ӷHxh J0=N}{ƀqڗ=(=OcJ?ZLڗH zzQKq>pߥ( iA<~4Rǵ.3F9'>NhF lR`)sڀ FQQ;iH-—wJ19`Z1}F?Z0 O)qH>G?ހzu(? _}G>ƍғ`1?*}@{◌q@ ʌΎ{ \t:})>\ct? R3ސi0Qz1FaR4Q׿FG@y'>Ri{zR&4c4Pv"9 :.}1jQ{b֗Aj^iQǯip}?Z23Nht~t`t 3ю9c҃3؏΁>” N{"iA'()\7ގs:9h &;J:z44e*:s\h=HO捣Jqh8Ύހ{Q~Tr;.=QsƌP &EQKϭo(~&9g*l:~pE'N8\b4|QiGi3<—>Ǝ@OPp:x4w G'&N:a@]}qA;`}ixH9h~׏֗JCϨ41@ \J>΀G>=6ip=s@'~hҀ}(ǡ/n$9u4d (׊J1%S@ ϷG#4}h@{^{G&)1} :i0?N3@ {GF=@(P~u'ϩڗ֗@ lQGQۡ1Ͻ> I'>b挟QIICc@$0 ȣ8(@^3'^CK?td4 N}iy=@\QQlPp)3?.qK=4P =4A@KރoƁIy<}J\{q4;qF1ӧP1LŒj\P!8KPq׊1{I׌8i a1M )?/?:=JM ;h?C@Ǯ?::#Ɨ€Ol~4sK)1h%cҞbƀӏsKO{R?)y(F&zKI<яUO.i1.Gғ\QzP1>:~4t@Rd~@;ҏ“aP >֓ It?*C2p}Όr9?#JLR@? PAr=E 8F=GAQ@1Gwh:wSGN@E!}1@ c=xr &vќs})zj2q@ &s(SFGzPބc@F=~}isG@/&M>o֌ >—P9Iߊ^}(?֓(@ sA= @)61jBá֌8@#?z8qL?h:tgLdt,O&ؠǥQr((җZ'Fh>+z )ўƗݑ⏮S3Aێ߉lƒQP@ 8~;gۚ\q!h'4==?v^qI1F^ZP} & 0z?L p9I44F~ǵqݞB=n=(~&1Pgip1hGOj oҀqE&s>qQcpx?)=:BJ?J㑏zPF{u(EtQ2ڗoG )_ΓR{b?\{v&9 K4{q+ Z2SGFh2;hxQK2{F{dRz^F~$ь@GsH[ߏ.GNh"$i}(簠控n?"(z4ݾ4}. %Ҍގ:Pzt9f'ڂ>sƂ '4`w;b@Aϥ=i91'w=.pzT}i2$ʓ PJ9=Ij>J^}2q '~r}y4n(-OΗGGG͏J94tGJ9җ@ b'99>f њ?@sӭz(׊Al8t/4>4qހ cO}Acc=:у.u=3APz}J1!Pӡ:h=2=h^(Z>&=zQPϵ)x@ ǧK 3@>RuhEގڌڗOʀRqKP0RsGh9܊^=)3Zg?_΀LRu"&. I<G=(3K sGAqFx&nأRpOC!?(=:P[F) 4 \t PHE€ }#wP}hI; ^ @=ڌ{Q׊wG>0G~):tsKJ34M8G3G8ΐlcHcsҎ?ɤ d^j1IEgP?(q@44:3\P?w|~{Qti31@I)s֚2O ~T(8GqP~ KIQ@8Oh"j@q4sR}-J7m掝M.isMzN\=?L7%>t4g=-PFzfҀ-qGOoj'>vQP ;P/ʎ{cr= -?:HzsҌ{RZL?I鞽Nǥ7&3ғ8h)j>{~9A֌s(#d}2Op֗BsRSF== LSu)1~~ @OPF>}zq܊;{zP qI=8}E 嗌ph0s4`iÃ*Pӵ@xFb#=HGK۰@3^1I~#߭ b@zL{}h 88 v=?8g{87dӰ1?J1@84gw٥@ LO֗'BɣRGҍlRm@:K&ցq4~-J0:B?*0q4qhǡ@ :R`ҀƓj:h֗ߏΓ8u4h|sH[h<*SZ0qA>΀{)s.=:_K@1FQQ1IϷFodh~4s@8ȣu{1gz\Rأ'9>LQZ\ 1H3.sJ3OƌP='lvZ?:1_֗@ )㨣v 8 9|{ө:rGK@~z BCKh:@ ׷>Fr)9(s>`Q€Җ~c4LhG=4siNR~&c)?Rڌhp(8qN(4#c4w@sѐ?P9?/fA8QzP܎AJZonRގ~J?_tߔO'\duA\GFr:PH@=8P(TgYqzu#ځ>h@ց {)h8?{T J٣88ny_w Pɤ҂3ց }N'@8ړx1'^zLd1j3.O?Z1@ װ8=G}8RMShK:ŒQ:P 3ZN{ҀюtuhsOJ94wq@ :RdnƒZ?>}Kh(Gn8(=`ғ>(((t~$nG4i .E L~~(iH= :8){u?jNL`??sz2@h#j٥4gP`J)FH4`g!?\RF)p1(#M.~ ~>ր3AϩҗKn=&>zI@ hNQө49FE&)qh'4 1Ҍz4L~=SQ@ ҌzdR{PsҐ(=~ƌPpq \~i>΀ t?4mKI \{ъLci~qڎ>ΗQP& Q'#&=P:Z)/IyhߵG#֓ dQg9{P`F(/N9Ϧ(M nzh}r(yh`zdPHZ0NOҔtJ_qGҀ g֌]g(y(hNMzz?J(?f“oEΖu4 9 %JZ1@ F3ڎc='>Q&Ԡ{ P>jN9㚐@(,SKqR~}@?=&?Ԙ=)H8ipqcуF)Hǭ.LR@ B9~t`u?&4=(&?ɥی4z0?ɥziOŠMQ-'8KgۚAQhڀaF? OlQڀ3~QIǵ!i?:1F8z8K(Pƒ_֓~vNfP0Ƃ Qz)cihNyKQZ3QI@ sK{ѓGQҀ4Ê> x~(ϥal>bǥޝHp: cZLOΌS\{QwQ.= '>JB)ԝҐWZy~4`>P(ԴcQLF=փ⁉1Kr1GZL}sIJ0;P )((=4sQ/@zcR ր~㱣8Z^)?/4N;mj^M.(;zPϵ&)pGR?*1@ mPQ|PV4qG^`sK(GNgJ3Z3?/ΏʀQޗO9Qךp4{? v8cGK) _ZN}O)G=;9'>m?J\wG4'/C␃P(0޼RsN۞hJg^?#8(3N>֌GؠOCFP0#֏A>|E>QH.q€~ R֘ *6 +-粲a4 B6cRNA#$XCԛ@W-mr( J#F wҤ8Ҁ >6)e%X 1^fĞP<jTx1Zpuzrz y856mq@Q#Ȫ@4I:@9!Fq֒-F4cӊN? C%;ߊ7x~qOzM(=c0ȃ>,hZUV# nDA(/րRT$xfA6IGlә:ҀG>:ހsҁGNiۏS@޻Ӹ'`0 ÞXGI8Aj: 8֌Z ʣ%_kn̪v eH{ t{Qc1 * ?Zvcsoq5U"`)R$GAoP:Ѡf>cސ֣˸n^yF# q9Կ=9؟z^ sGG_Lьz@EZ0=(t4~T~SsG^P4? R~~gQ Eb:)ph3P~ƗҀϵ 3=J4dz挌q@\whzIҀz030i3&>@Z;QFE z2s׊\J1:(#)0:PG&~}h M!ط&T橶y|njT/LeH݄O})lR)QZ3G@h4}hǭ~fKM縤/ZNgP y@H@8piX:P}h#HP7@v} gI4FHkU2Kv?;g)y!,jXܜ4S׊BFOH<1sMG#PN*G"n=:`tr8u*]ܒyx"Odgڠd$rŴ6zp-tʪ@;ą* G2?Fp;#j |8/ -'ԌzQ@֒=(@8&Nq>sPJ13Ǹ/QڀҊC){PsGրƏƌQ?ZL4QI;t~4@ ϭ{ӵ'Qғ߭)3P׎1F qG ;CFO aϯI(@j8&hKw?!G<@RO\ƗtF@ Ѱ.00^(' d^.\Q qǶ(Ǯ3HF \`t)2L.~ \)(i*@O`3GׯҀɣ ( tiơ|;X6FOj?YN]wT!0sL|uPy@[oyP:0y)PtbGo0qҭggG'[P '' h #sN7 =.-܎C% Ե1ʅ(jbÖ4M\#4!xO_zPAo'dg5lg7w081OFjHY&PCR 1֙W GSH s?:t?<7^qVTbfb_5!EcL|0?Θ07{jIr19YNߔOZ = $|DodRo\mhHo$b.P,s֒]OΓ'LE>ajrXBGK08+ҥcNw gR@$@s_ )32\~rH=:TZEss6r5`B2+E$qh}i$v QRdzs@#Un$*UoqT[yaTen6jեb4vA3R(I ?5k2ڸ*sN|qH:1w銑M9hSʁϪIRRa. 4ncKH=ii2E@9Ihy@hF@@ ʎ(ydQG'(3qKh?1Z34 J3J3FOc@){P=-&}4ќ(sIh'4P ѓ?hϽ4gwsӥ/?!`:ҫ޲, dOZZk q ЊTC!$g9:.(;ZLQph!{ 9(籥Q9֌J;{Ҏ4dΌ)9oҀsފ(>\R}((wQۭ1;( 'P%!@y\ B4 괟7ډ.=6(?JΨYn֬=GVe#;9B-Nsjr]@*}SCd] .@@2{ծF W5nœ:Q nX-ZX n矘(g(y{T Rn8 3B'HK;(P2Nir1.C,[AL.ceP?\Պ #HG#-&OFQw!2c,iKp%@UN稤srec Ȫb92{$,w;=; }cJh'R AGP'7-b@ޫV5\y?ZHHz`)xϾE/lp()4~${yFbx>c. 'ބHeAisۚ>M8-78T"ۢwudlqݱ166rx Q@둜/?qQOfУi$(cUXhQ v70DSO0!I"F@V{Tt^qՎt-HA9@t)yd )3)r '.}3>7ʀ4vRghj>(u92hŒ(2{gIz@4 @;QqO(8M̈}'J(@)Ozi3J0;F@@J>L|$3!?SVKЩkpE21 ItXP;v9U,Tr) EӰ5Z8w=t`g$@ʬ8$CvmGf  I4[c ,g(pb=EUMR6.U۴8I"+ %RWrKɽ |IY.PjԠ;b<ⒸEP0wU{ynDFAg; B\qLlE'Bob\)MP7:ar%V2U^֝ hsT s|Thظm$$X#vF@Q:Xhs}ࡶ|ycHc+9Ts2!C ޜb@ g$p 5\1mh3A SꢒdRK8²ۅ N-nGwI6%ݎxZn8O2E˨#{o!rԙ,T-q0MX@r,OZGojHnye@LM`,H'isHBqPsKzgߚ*R#,ݏҬU[729F2n?5;h+5VmhQn@>R- 2)j#8һ$q5n$8Ֆ/IrUH Vt{ $iq늆=^c.sӊ?JT,BI慸"ƪ`_&m<I?>9`9FiYS)yGN7d^jI#(>(ϭrzӥ>`QzSi!cIClcs՞9ǹ#13sG/R~R~ ~4{wC1@ Rcqހ 4o@(4{Qڗ4cGK'֏—E7RQHG(,@Ī 89[8RFq*ZlLG:zguca U`%B` y h@ seRN j6ܩz! zwx*$#dE3\,tEGz$P3jD'o͎c#lPrNOJۓiZ'IfJ24lc? >g3zdFF)inq_Jշ۫cnȩU`"MΘS8p;fdU[-Z-˸r*Q2:Վ$($"azTSE# RLW ID eMw`  8Y\S17$& ϯqNqI6`x=C)E`1Yd.mۂpsOp2+b dt| Tъ 9;W!!JFQBYwے?=hr~=j)fes `Th;`(Uy>9GrU0G?JlpU#q$P[̄NN;c?0냊t P*(we\l#Y2 Bj.Y%b{FT"r{3#sC O )Iʙ aܣzGvNy"'yYL+c*LnsG>߅Z>hϮhh֌q@}rz9)>i8>/}}4ҌP(Rd{џ8ʁ Id7C@(z4}K|}((z(:њ?\籤ϥ-&hp(h;s@4b=.=(@R{▏zJ?*^):MdQz8 )8R@E? 8Ύ8h(""J))?*^ΐ QqK8>ynHiGLRvg B=J^}(ǰ.ZRTrsniN:tBqK9!4  ]0(@OsflRc)I~4=}ڧӏCKА}# +4*4H=JaE,j$(#N”p9֎B|,119>—r.=T01mqqɧ` @=0 @cإ>ړ#>*aAU )si4gБ@Uڡ@Ž)=ҏCJ8g"jX[M.WhU 99?A9Ҁ$bT(=AIo-7 <ў MUN'JN}B z44RUœ~>c~gSJ#gpݞx?Oʐy,"J:Si)8aR}T~t?&ΓҔcc>=?Z8HQj Z\)(=>#/~4):Rqٹǭ!QҀ SRj((J8M.93Z;u?mGN(`((s@&G'(%@Ԋ=6}(ǵZ c4~~44Qoƌ_ҌI?/Lc@9֓9/>SK@9\G@G/?ϵ@GSF3܏ƁyqF3ցQ}4d8~hϵ'hǽ.8~u\F ; Ͻ&8iEb #?z2=h3ѐ88J0GbA3>šQK* ij1?KI׵(P12( *^zB*iP ø"ӧFlƁxU?J<ƿ{wRwQQq@i "R}Ma(zS=;94 _Ҏ|})Қ8H@+84΀"0Ű!@TtQA4aF=(6쏽~T"LP"xn z vmS#8A<֊Q@۶\:~4PGiE'( vD~R$Z @4Ɗ5QA,^Ζ*FQu拁B#9ssS(Px(Z^(c,_1JbF9+NҌ֋ԍB$/o>g( ii3J3@T7 }/MG9GqdF'8ڌEB30D$zGK1E \w)`\硣8DE/AKRdpOҌ,PzTPq@'eg+`m @XSg(apS ĜF85'Gooj@zԟJ#ր"h:cId9rxrk #֧hzQp#1Ųn$X&iqyA48-&}*_€Z2: ? 8ތ4RE&>v)'G=iO?'=8'=?:zqZnx'f@ FOhIP2H~@'I=ϵ)(ZҌ :Pa@<-%4sKH=?ZNhv4R~/P} G;EӚF 稥~4ƒN>G/2 Mˏ֗3~4w4s'8isE}E'E/߆(~}hPs>}:\*NHhd2h{ 8R#''1hs'Rc$ʐtsҁ&qճG4 .IWJ}>9hGݠ`88(.ю`z4':Q=(:_izǽҟIPGhKJN?ɥ:dfcI/F}4ƌ{ 8F(Tbq(w@ F!qJL^}G>=JW&Xw(Ҏ1ր9}? Z(w3@v@NxE781pp .qGjL0>QJ(uR?6`cƒhɠ8QB[KԼ(:ҊUa@Ni8Sc@R(;4gFG h ?ߍ>=@s \уޓڀ '4@| :R`֗ ֎ԝz\{22}'hr=:7I\t L◟$P!izRgޔgҀ 8(`&{~tQKǥ'Fڎ:Ҁ'# \z(9ƏΗh:(FhP?u֓wQǮhG@|cIL)0{Ҍ}sIw4p}y6яjAAKʛz^=8z\^)F; wFxIR hRIѓ@ QC1Z0GjNcӊ?]“i~4PIN(/I;=hQEǵz:Qր(Ҏ='߽g΂h8~ LzGN{fހ T~b{@/K!B8&'mL~44NN9"E4 ҂qQ@ӏ4zb34rqǵ.?t" .= b6ry~iFOQ1>''ڌhǹR| }:1@M "sש9qށK:Ҍ@<}#J1s@:{@LsQߞ$h#(>ehv QZ994vƎs4g=I>9zuA3I.N1ObE/' }(ϩ=>{MPO?@`>qяsQ&~tdqF(ր ף?_ʏF{NoҏҁQ@?*\s@@֣9 |qؚ2~QKB 4:ҏz?@ ۑ\}i8=)P80(rZL:K9ŽJC׾hsFi;旚?(*CZ_@ :qIߧ4zҎ(𣞽SKߚL Rh;\8냚\_Ҏs"OP)qi/Q|Ph=i3sGE 8gғq&$w?u=h~qր z揨֎x)i94qGsKj3`t◥'>it}hPG9h֗Z2hIޗvtLRg@=H>`zP &db8 /֐gЁH1;`ri1ϱ8~iq9Pv.8GMfP:`=ӌ~tc =(= @4gdqFAh=Qs ?h/К8Q| 3N} QE%ztizQ@9>Ը(Gz/Iߑр{?30IzthGcҊў3Γz@jhyI>^Z0?TZCҎ1F?:_Γb撗?Z8}M&=Hi~>s'K|P`ןsEс ((>}?c 㨣L4ǹ:A'9 }hi9?-!$uh“ iF;Pc"`сڌ``($q*8r{c~F#@ rx(:`PA@8iLt:7~g8O|QIǾiy=KCўߩ4@ jLǽ3ќ4p: ь4r{⏩.=::v4}(1Hsڀѓh"{џJ2:rҔHHIj8QzN@?Zp8j^P?T}qA-7?ƀCG>CG>d1@F{~4gPɥր}/\QzRzR@sE'G@Oqw4 vcځ Rg=~qFxҎ(RgJO>RIE 9 Gz\1PОh*j8ϥ1xA>i{F?wR'iH>7qM.I)poƎh:x'h= }KϠ>~T`94w#ځǵ99p=H~4sIOz9j>oQj\zq[':@ 'G (3iGR)w @ קI=>G&}G@ hv挓R㠠^}'I;ӚOr@ Asv}sF&:P{g q@ G4'(J?g؊^ԛg@F8@"'=G9㧵:{Q{cs1@ GOҗj@{@s@(HI={I8R ?€@ >dcAB?Qڀ 9'gǶ)xŽZ@g~TGFq#hhϽ'4PQJ9<@ sI}hGI4u@Pf&<4 \I?J/zOƎE3ҏLJs@ /ғKGozNOJ*94RgJ3ۃ@1@J=;R)94 \`u}M'^P=? ~֌{uo1@?Lѓ@ ߎ? 9J2hR8Asƌ2==KFM0FqR:R籣(=xր>dQ41zRbL4c>{(_ʐތ›ӻy\8?4 {d ;}(ғ47>zRr=M7vz7@o 1M}izsF;nϵ Fq֐@(8A?^Azh9=y T>Qz:QM!(Z2~z>.)#s@ RnѸbќRn#& zf$ʀh=>sS3@ϧ~t70i?Gʌ'QѨ S_΁CM8tK|z0>8&O@ lT&ssKt>tTr}q@9G^)0=xsEvj;4~44s0;`}h Q⏧JS@ p3IQ:*30:>`u0zF=s@ ǽ/sQb4{K:Ⓝs R~u=*Cԣ'F}@ R(ө?@"=iOրZL{(sG4Pu?ps zP(֊C ]Z1uAF?TpqiqM0+N/Ҋ@ >\RR`֔<@ zd7g0N~#`NiNqc!1J:voΗ_ʀofq4"O3@p1aG 4@)I4qh2=i}}ڎsG^3ьғd/Ng03ր3s^FOeǾy'ړ—zJ1L|rOĚwIq@ })TsҎ aKϽ&3ץbpM(Ǯ /ۃ׽QuirH<:0{ҟz9=B^O\Rg؊1@ȥwc"4)4}F4(q2 (4ϭ;BiGN6ќT(9_ҎfߊLb4gڀ>PG{ZN(Ϯ(EhϨ4~ ? 3F=P zQG@׏ƏК0}@d{RQ1M?ZKIF G9'gL\`wqQLsqu4t?J1GF=/?t=h@#4c֌.sZ3@FGΎ/^hbbiI((簥@<_ggdgR>{q)GяzсR9KF}h bƐE⎼@tc'4ggL 2}F(tZN P bqӊ1K=}3@8 =&Nqd)y1QQBf= %.hazCy@{ќzR4qP0*N`hǡK? 'ϭ)' LAni=.OC4t;џCG)hʓB`s֔ N{'8~w&TO)7)w)P3qG 挟j 8QFH T2;Kc49# J1ۓ@ ߚZ\Qd{})8v=l(Ϯ)vQ4gҀ9T:Js'f[?G\RQPFMGjO`Q}(;яjzqI0:A;QJ^Pcړ9?;sA"ʗ9(iy'hǡ.xI}.}hcht$}(Ͻ<{R} 4`E.=I;~"Pz1@1n}(9'y^iK@#ڌ}:ʐ.p=)Η9a\ 9$Rdt@`qE}h3p:j'AJ1@Η\)c.LB)$} &A~sIݨv=i~S'|t|Ps|\z8ԜvTQG~)pqLPO'sӨ@ ӿ>=MG?u|f}hg>s“9xN=P}'8@@ 4F{q@p8o)w>x t)çAhF3h> I/>?'z^OJ׏s(֔qi 9<[<ʀLץEzQj\RwLvRF>Ӄp?(rc@ Oht_z^:Pu8b(vv49ۂ)qSKdOj99H'NP.39>whǧғ}iC}(4F=ȣt&?(>/Iz{};&qhͻ8=E8#PzΗ gBh$ 1CFG>4c?jZ94“h 3KތzsK(H ђ{F@ ӹ4ڌjCA>}4ZR=&?h)٣=I;v/QrGM9@x'?JOK0>q{P>c@җh)xI߰Rp~;ќ!tF}JA1@fҌcJ8z> 0I(!C^z&8SA~^ԣ>?SK'INP4_'@/~fތzSh֌L~ 1/dџSQr182V4^qIsM-U;!{сz0Oz81: ^4ђx @GoƗߚiPA#M2O|1wfќv($P (Iڏ9bʏHOG=yA{Iz9($bI{J0{@ B)xzӚ1@~>RM4Ps@G_FOn(8Z8hissI@1?-'l*1sK~i0(QGnԴb ? >Gџj&=֗֎?3@uF=:џz^E֠9#(#җ8&=hJL(֣z\`Ps1Fa~4~9G4:NJFM.LqH@JL4m'Kڌz L8z~ǽ((:LS)Œg0@ R&28#>賓CH~s@“*_ƗI^iG=J1Nh>gƚO<~q(LRڏ^8^r?::})x~":h)2 4#oQ)y'J:ӑG@;v(;RFp[t 34 _Όx8PcjC.>A)0}N> җf;Rgu9Ms})0{?1ۭ(z `T w3OhltsHBHxK8>F3ӏj2=F短bs~?&1@ZM<RxmZ\Ɨ = >g(6o)vNhE'=I#Rs?Z8җ؜c=3jN}mϡ& 4ӓ.F;qG?.sB}3A$zQgӒKz&q:N=9i3ɠ?CI@gFqȥiq@>Ѵ{s@ yӠ;q>nΗ=i84`Pj1т{P掃84c=}ڀڔvm&?P=? 1:2:>~ҹ@ j;GZ9J1E&Gy_ҌPQGN848ڀ wbv>{~\QF}(qڎ)?:^} 1FI=c@QƎ}(Ǯ(0})qF=(#M>qQPG;р;PZB>/KIucc'ڊC0G?ʌz ? 3I(>/CGQלQwv8 9^}(hތҌcvK@ >cRg@?.p=? N}h.sYR`f:df}({Qip)1ց 1LzI;siJ;t&IQϨPFj|}(Ҁ~tǷ@pqړ@4u׊3 R>KրRӊJ8GR4~Tbh1KGHQPF 4&G#9(89=G?q=h>liG^j`4z(=Nip?ɥN-DZc=? Oʗ~tIE>B@QK֓'}sƏ~TA~>p~)G>.3G4 miO-d(\ќ(ǩLj0:J=i~?Z;4cԊQgipOZ1@j2;Q@'Ҁy=9SRf€;Kc~P{&G\ w^'sF)yJN;~t`@ r(L\~Tu@ $Rb:яQ@q.Fy}@ n(֌㸥E {~~GKgގ(33@th8=ix:8Rq94`zGP:Qyhր zhMhPPGⓞZ=9J7{T ?Kb(ғZQ*8I=Aӿ NO|҂}G@>~b߭&юMz})ޖ u4L3(Ҋ@ƎJwNSvڗwQ~4P1|KFzR&LуҀzz -43ϵ?}@v${ь##L{}ݹzwGݱA?ѓɣ= 'z=(O?Z3x#F=?€ zΎ}@2 '^;uғSGooqRGRRssPsGG_ҎOAFxP@ 短FO@ ӡ=.M'#?Z6KΔh+F94gϩh@pi0)v34'1sKqF{q@ ?zRяz?O—Gn4H9Cbؠ>q44q848`v'g@GAQ@ ?/J9j_ƀt}\f砠{H@xhr{@GLJN}h??ΌgM_€:mG/h1F=Z?(ҀzwF=E?@ ֥Hi>:Q1(揦i(ϩ'@ 3Kj;q?p:cQd4MzPӡ&OF=)y)3h1A-ڀ]H(hq})@ G4= (#~4thP:d>&;qG~04c׽Aю{QGSF>sȠZdQb Rp:ހM(/H>{~dzʏ'OJ?2(>q|1_1 ?Z?J@=ZM2;g?0)&a@9ϥz:/)8^@)x4zL/IKz1?Is@ )8@P`gKGOjOKs3@?LQb)?LgKǭ'irq!6qFG c1FFzH E!:PsF=sAuǥq@~<ҎHr;QoŽ=?:&?iysGaϵ cF\hǶi1yshP1]qրR4&=MGoҏ4c<}(=@`FQ!ui,QFy@<Ҋ>P~^i1Rt _Ú(⁉9hh1ƌZ\QOҁ ր=r})8EzqF@G(4^?T}(ѐzRjoi@R@Gc@=ƀ _ғiE^'n.(ǵ':1hϠсZ?zLv4cރaE'4|ELP^'ր:Rs3旜sAϧG=p)8J))߅!h@G2)p'Rހ=ih>.h>JAӊQǭ{zLP8ғ3@?/n)1&ԏQϭ'N _ʌ((ȤK@IҊ%2 8}(#>>tr? \`rց zix>p:.h#qGO8JZNF GP8 N)x9(9= } ٣L)Y@n=F)hspi9`#>=~ԼcGӭwQ曁GӸMdc4c`P1@{ӹIj9ZLBi:0}hIyGz E'>hzPQF}taF3ȣAy'wnp)x#8s/4>}(sIZ(&GM'pOփӥ78x4@ җAIE)xz(}HsڍяcKƀ P~s 1>8PQ= J_΀ b;"T~c@xǵ7oʀ RڀߊOqԿRvhqG~'E/ZOl 1?;P׿ւqGQzNy)}z:sb8?J1(4~tyө?ǹ GRn\O/^}qFh;ր 1KR`R\tLӠ/8sIzQG8G4v(8(=iʐҀz?L=/CF=g}Z8'N@ @d~>\Q҃GKLRcK{gf Hϵ!&Rh:׸EP}lQ Ҍ{IR=9B?.=9t(֓'恋'}9旟N)})>E&>P)p9hȣ8Eo€G~?Ia@(NZL{QP1s۽挏CBEG49'Ln?>zL/@^t" g P1x??\G4>`gir=@ Ϩ~F~?J`&N>Դ(tg'h@RgrM>s^J)?QhIu.Ivp(Œ s@ ^H1F9(sF9֌zc`zb2?Tu穣Ɠ'Ҁ2:g>P(l€8^J9[Pǭ)0~cڌJ_Ύ3F=J8=&>0=i~4~sJ0)р{ޓLQaR8(v΂џ\ќ" zp{sFsz u8h#Ah`ugGA҃9tRdv|^@ ~E#pPJ:p:ќu<{&LP r=돥9M/iE.{(1?Z_Ž!h(9zJK n1FN8"=/|g4z(4 pzzt8Rt?/>?&8R(:?KCF{f {c4')rq@ `2;sGOSIӹh<񣯭/4Ps@RZ2 րFx4ҔgҀi3@r)i3@.{bҀ'Ssڔќ'KAqހOOƓ{Q:\J9B4t3K׵ v)7{^ zf~vFx#рtQL~c  14c@4旟Z2=Zt9\ѓ~3֗rF)Q_.y3Mx#J]~AIߕ4`;t=9ѻIuGbIqG~cuA( ?b(G#h~h6(9 ϭ.{b?'yQPs^}iqi;v?JL\֌@8c~P11 S89&AK֊8) & ΀sch? 4{Qj=?ΌL} hGj\4c(ҀN:b ?J(vt~?J9sIe'>c'ޗ?\ϩ9@ d@þrh#ѕ3@ƌ֐ǨPF);)p:s@Q4gޖ÷\Qy4u)zSwRҟ4 LzQG 23N~t3F}p~@Z\@֗Pr>((ϱh?LtZ=ڗ>~uAސ Pw44cO@8RPh?8K”P~F=@H:HO4r}3@ Qڎ'=.^hHKϮ(>FI/)?ZCSR9@1ڀIRKΌ{g/(٣4QK׽'~tQڗ6tE'>u(4ǭ.)1?:_NJN`QP=9#>=(#GnhGǵ4`~TtF1Gq@qG9Rgߟ/9C@ 8Kj(: ^i9&>)Nhޗ'oJ: 9>QsLSJOoI^ 3׊\<@ p:_ƌcG=:q zjLӱIB} ,=;rh#<C =x;RN4Q/ $zHuǩ'N0)sSgҍԀK @c.}E'899i0z\})ʀz?KzQy}N~nJ>ڗTQǯK'4 \r}pO^h&R}??2{\gPGlSFA8K1@ 17 uh Pr(>s@ǽ/O]@1րF!?_ʓ׽!#cA ws'B~t=:s^O:qh f{㹥>F3PpzsE)s@ 9SzN(cQǯ4Q9Fs#ǧh9݁@ 8G>r8ȣh#?Z;g7uKh9GiFOQGC>cG4p=1KG^ʓލF1?! u)y=G=qƂ9F}O)umGKZ)SKp=qF@⁏cF}3x|iwIӵ(ΐ(縠;zw֓>}(Gdc'E'G@֓؀h\=KLҏcbgb#ǿ:}h^h:QSK׽&swdt(;&G@ J2? 0;QށhZ2?Ƞg#րRw= :恆3҃N(@ǩҏ“ȣ'>(_S@ =FhBHsҗҌih?1OGgP >RJ/׭&}"@z^z9=Jvh@/?4Ns>Լ} LÊkʉ7>dBsH8'dGzpib*bҀ=(qGh^J?@ڏʓ/zFIw< f ֏?:2{f{8(Ϸ@Z>Z\bd g^}2:KtAQIߩR(c4 AFEZ?֏^@(?Z K׊P=)ր=(֌E-'EPQA8haG9bݨI~TgތP:TQ&ix4r)(8—qI4x)_4~PKhZ;dԊ8<'F1< 4_Rd74Q֓9#g@ j1ޓ~)r; ߭/ i֗ w4tނ34n њ7ϮiIǭ{t϶~PjOJ9.րzFsN'hրLRHt;R~R {RGw?/7chGAGКN=h#Ii?/ʀ:"p(ǿ4v$Ҁ{tcmhy&>d=q=#7?Z?)R= qix)2Tf @(&1s@;ϰRtcҀM/>IǯB=ir1“PJ\g=~ǷLPrJ=F=@)=*Sh8u4c֎'^'ޗqZ\c_ Q-'_Z^;fo@ 4)8=? ^;bF8~TuG_Z0;|`i?ǵ.Ah1~TPޏ_Ҍ@Kw|Hs.O@ Pu'_9NWN1ǡќZOPy94у@ ϠwNԿ7msKoʏ΀cG=ړ=94{RtzuG4c܊9җzP&p \O“wG Q8qjN?3is@3GNGHp}3G8m@'ڐGKh@ (<h2˚:)xOʗ t|08=sGG4J3E F}@ Ɯ'^E9 ")}ҁ}(րo4r;@g3Mށ L}(:@/Z:c= (i Q(ZMwzsF?hZ!.? 2=M c9 _€=>d4{\&OM/_O΁@=sHz@ {ivPaI(X/^x1~fz`vA0ۥ 1QQ@֗F(ȣ>}h(qFhhϽq掽A|fzRqށf}hs@4d:t럨9P!xҎ3ь~i1SG'h sʊM4s֓րGAߓM:1 '\׸Fr8$RQ^ )2yGҀ3Gg(ϸ4{<ϯJOlcE4d~`g="\L—Ϡ>=: QȠTrN)?#dv4g'1hҗ4t?@ <` 0ý)8h44ڌ `{R84zR=G֎:Sߊ_SAG.1΀3=0(:KҀiqAqRaO8IԊ33ҁ_֎q&Oу@94h6MG.hƀj_^(Ii2G(J2}s@RƏ@~h?\R94zsF=O&z(Z?SI)qG|P=(9@}wQb|ryg֏Ȋ?ʌG4g4}(MSKq@ ~h KP'4wK9:(*z~hlR;~ʔΔ߅ғ{)NqsJi.L٣ȠԿ2: f {R :{ތzQQh>ci+М{ӊ.)2{RuցONTGF N{@'ގ{Kh~G8~T(=s@zfQǩ7 ;ϠRsri3A>g}h11: ^{1s3؟ƌ|сGK@җgAb'F)p{(qzLҗ1ҟlQױ׊@ߕ?J\Ҙĥ?&NEalюsF=Ȣ}(Y~XzZOl8O@ GI΁ gڊ3O—>dP~?K(Ҁ>>Q'Z1ZZ?AIaG=E(B3Kߎ(8\Q/q@ QqKI~tRcKG@)`t\QQP3уގn~q/F?ڤh\ƌb}vƀ>p1@0((J^q@ E A@}4s!<|ÊQ?:8 \{RpqM <Η@΀ 8Pƌhh IE.9 hr}I(dRJ>ƀ"/=gr pKs@<OҍW`qgތi?8ϥ/9z2Z\PߊLҒ;Fm" ^~cގc9EÊ?hzu$SҚҗ v&~/?֏3GMGlJ Ps1G?ZN}E&y~zcs9@:IhOҀ>O4qK׷@ ֗4j8<ŒL(fAF)hv?Z_|fvcj1:4waלch3@=hPڔQHNzi9=AQi{tj3GAtQ>PQ:QؠdzRh'(h&I1KN}Z3G=='z\;:8QҀҗRg:@ G>,ё@ lg=qj2=E> :斊?*ORs>^Nq/jNz`QcҌQ>qIj=)3߭!PzQcҌZ)21Ҍ=(8?s҃ 3*?*Nz G^iRO_ҌZ:vɠړ'4㠣Aj9GZNZ\cG?RR@ AҗCG^ gqFIQM4dbG4}?Z3@}1FOp?:> i1h 3P?ʗ?Q!Q֘oΌ@ 1ZC Խ;E0q4sh?*(ϩ ўzi>NKBƌt9>SsqG'Ҍސi232issqLQ:0z?4~ {tGF{g4:B QӭGjMRdh}f4tJ(Pd *^~uhޓ40i;ҏ(:J?8ihaGI)ySG^(֓2? ^(i uB^89PN;hhusd9i3ϵM(ϭ&A֓<4 3A&RhG^~8RpJ2}Rҗ'Sѕ$r3KlPg#GJB(&c(#OI:_h1}hKQ=hǥ#t@?FqF\Rt(8<}sFG\r{R⎣?h?Z^})>Q`?J:PLi>QP{`K})2O821җP 8I ?ʀ`('i:9PuqGҔ8BgN 9gLx?)sIM.V;cOF;gZ0;@ ױZ(ԛOaN(ǭ'8Hi9G4~ASA>Q\Pc#{~4uK`sF;`i0hi}LRL?|r@ yڎ;zPcK:QL~zf)h9(zt4bz):sKӵ>أg#ցƀQF}h϶EϽ`qQہL{qҀ8CQ㡠Ohbhq)zNڏ~hH8 GQ11A^!>cʌv⃎= &=E.GFG@:c'GQzQh@}&yqKQ@~zۥJ1RPT0ftdP&q8 N=GAf(9)x'>}h;P!0q?G֗HG `w4vQh0GA_fbgGA*:@RA=8^qϥ?Γ;)sGG^tw2}(FHA4>m~T{Rc)_ŒzRyBcI:΂q@==i~LьvyAOOƗl~h =Ӛ >Ϯi:iGRs?:6~4dFxnh1FG4?Ish3S>uP08J8sP \}1I@ ׃ғg#;~Iߥ\z)3K@'^sj^= 'F}{Rghi9уށ9qFqK@ 9ҌQj(FzQG?QGҀ—=)q@ ֓qK;1@}qT_iAsp$p~`gڀ>أJ9=G@u/(ҊLsG>.Oړ'҃EzL_t旟LRdߍ-jLr)yG9Kϥ&{c?h 'Qb {KۧHp9zP9Nh:v A8z~4H:擑ץ֌ 2z*N{Gb?h()y"m gQR!ҊY~fSEH<80!E;w94Rp(1IG>4^O4r:p;֎P9?J0{P1ٹhyz\'Qɠ]4(lь4v(^:{.2z=y~h09s^@s@ Iփ)h8=(ϸqў٤2؁KQ@49dgǦiGҏҀ޾ԝ:Z:u4AsPF=M/Iϯ@_Z?: Ql}(h;њ98(OlqN(ϧ4:K8$}(~g(G(2(=-(;4)3ӧ֗_€ 4s()9/֌I׵>Z_tsh(t~4fŒ3^((}(3g4@44gsG4F.(?Ҏ3GN(?:C/gy/440srzRc=ixp:ڀ==hM܏9AG>Q4QK@}G &3({c@Ҍ/b/H1*^> 2=E/IQ*Qg?g}hrhǹ'ƁI~c~~b}hc1z(yߊ\{;M^:;җ7wR?Z^?N79s@w&z:\4d&iq)ьw=?Zb32Q>@ Ǹ(9 N4>s}(wQӵ/FGr(sIǥ/8BuQсG@3K3\P!)Aoh#4qߚ;Q 3Ha1}xvQ}? Mր }qKӱ4~8A׊^ԙ:09'\q@h>#К:{Qޗ€1G4c>@t~?.h8=֏P /^ƓϽz4t撁@ q@nsRg4dtpixh: QF}ڌ{PNzc;ёG^;KIFGqdb€ (KI {~p=*8\f z֌c~&ڌwEGFxǠ/>QsF{ O9{f€KۊCG@i֗8h?:ZLwL;R9>;~ ;c;ۥ/c@Z/G'8>s@ P(Iǽ/{0ڗ2zRIsF1b(9aj(98ޓ'ozb /SAu=y}zP}ihΔtQҀm?Zq@\G'wɤv]bF=(J0n\ !y}3s(p:L9<|@#(huMJ_'~3J7.pr Qƀ!"(^R4pF2OG|1A⁌th?J\L2=ޗ#zL/4dRRhL7|N(9h=;QQ8AGnh44C^)x>ǮM!y=ڗZ:d9=OI1ϭȣ vi3.xRdzb@ Ͻ9h,zLRPgir:`#(?7 ?.@i8@}/h{3KꣃE"ʌQ"Z>Z1gzQPQ Lz984\ _p;ޛΗך\z0@>ɥ8b dws@q@?( w{PƓ8Ghy`t'!g4c!4uݚQr;sG'~ԀƔ{sFH8 qڏ +8@Ͻ #RpO>џz\}i3*_Ǝ}idƓZN93A@ )3tGQǭ&3"J:R`{ѐq @ÜOFOKfq@#4dъL u4Fd=sFy$#uLKi? P0џLQz@1Җ(#\ў;QFz0;jZN?z\wKFAG@G4d_>n(Qv'?j\hɣ'@ҎŽ>u zsE'/O] : LPǥH2zqꥤ4 81Qb悽zށKzcҏ4c@ R=.@Rqڀ }EKץ8(0 j_ʏjLZ1I=L۟.x&ʗ>Ԅ zZN3A'ގݩ(@ GրsQz8EAEgE~OaKIR uIɠG4f~d\2h9p;TvKR:O)0=N=/Nc_~T>E Izѓ3 4`~?=:\tjSIsR (Q׿FA`34~GBvG#/ךCuahԚSӵ'AAϭ/IKϵRixQqF/! u} (> Lz iy֏`?/~яr~sߊ3ICF{:\JLqށh~~4~bPq—ꦏ#i&ʀwjLJxpHKfA9FGc_4i3J3h?:? ?Q@&sԜR(ғqџA>h<#=4@?8>ҁ?(c֓P<=0A=(+ɣo4b{j:>gހ {1FA>dv֓:1(ir};(h2= QKqL8CRю9iQFyRcAzQiy'LFE E&H!'?J`qIZ_sIP :`~`E LRFG4)&{Q ǿI?\QL^E&Rۚ(b^ u*QKIz!i;ҏQgJ>Qg^} )1Pǵhێ?Bh3ҏԸ.8 @ IRzQIǥ-ށI_M'Z\gҌ(>cI@4 ~tug>@n; 3/ԊCd*N}8h9>JS\g&Fy$4K@ 94R揭'@旷CF=1&#g2hR~t`cڗ9&=Fޓ#4\Pƀ :nӭ;(IϿHhR:Z8j3Bhr:tR~AG#Ҁ џΌ  أmҊG&[uGj㌎3ڗ9?:b )q1 R#~BIJ^Iϥ/ 'iy&y|߀Z>Ra04}@?>?3Kc$tKj%/AJ}zQj:whhyާ8׌4)=qqGNQKjci>ahP!wPOa@ G4Ƒ|ٻw ~}):FCgK_ʏIҀԴH$\җL~T>,d)7M;Tm4h>yP{~~tgGzhpN@ɤN{xQ@$QǷI0qLwϸJilߝsh >ʌ٠Bhv-4:A>Թ<џj `tؠ֎雾mq7nE%~?JLRo\dF}h~T"-:ѽN>aȠc~yҙ ۖ^xxZCIr28.yhd𢖛}JOhϡȢi22yy(QNJB֌恊NG.M#ր ʖpoΔ4uGNw C;@&}.@$q@ E/Q7#c) 2O)7JhE!'=FZ3hIn()Y)AϦG/ⓓ ^(x#'҆ 2{3sOZ c(J8)~S@ L;cҗ?Ҍ''E}M J_ΓpH&}zQ:Rړߥ qӏj0}S@ 'GџAGրv)2z14Z?(8Q揥@ \֌נEs(?ZZLj9==ZNt0Ώ ;G\PJ?})`ۯGJZ@'8=KR~t{*:xZL]/(GN~?`v41sqF).*TswoJC@)O4c! r()dw4ʓ>3J>Q€Q|~fhi}IwzsIFy)hgތZ`&FK?1);ϥh=h֏v9*L8P39sM/(ǹE~tds@P=< Z)4h GZJ34~4~4OZ?JL籥Ϩc/I.h(֓pg1@^QhM-ғ(tcސQ@8RQq(89AzQ4QRIۑŒu'9 z@ G旭'P0M֏EI)h!7KsF3@ȟ%HA1ulS>ԤgQт>V(v~T)1M!z9u# 3h-VF{{/.u!H@iM3$A֩Aw7_k_1Y \0KrR{j/Pَj[Gq[ V[Zd,Lw9.Ye :֪.Ѭctn]STdRZ٤^3!_J 3H&picn1sY)ZMa)!H3FFp9 '%b%O*oJE\_9U2rA#ȁZ!fkE%vHNCrv w _4䃌"Q)r65bՙԳntM1Uu OsV 5e}*nԛQ?qяSBF1׎9R e2ŶaMk`㊁>Y9H#?T[[ 4w;#6!B'UrPNJcl7MjqY*C]tjЊO`UDXmH !=%~F`7 Ce'#@h ei#`ۉ=95)oJ0@ V"S>NJ~hArۆ,2XM)Jr.YWpӾTm2-_0c&ᑐzc5goٝEٌtհ=)՚[Յ.f+[' w>I-"O=iwO߽uwV80)2`RpͿwZج`'"֫Ï5<v\웂-qeASs,n05d ݼn?{K r \ǯP\T/TvS 1izp9Z!MAt>q멸LTs q[lx>rc#M ٩P 7LYZO).@ L?:⫤XPHS=g҂=EAs$Ź^FIw`d`u(@ sp);LcWӰXL}(*^Reۿ?F\J[`%j η;w p~J? i,5TKzIH9+~4ujc1Fr[F 0} 6R&}^F/Ι;H?` sШA1dqPyF$>OO##bJ8by[!:3LqsNOb{gT0Ȼ=AgwD 3ԋ#N495YːTt2ȥz~uȒn\psޢBKlnqSJb8(梊II0i쫕RR>Q_ӥDG00M '?H=j)n$KD$ tqT#w]rqs)$Z= qT]ʼn]:>8 ȡ%?/#MF%Ap;Rp{{c4_zN1QƀtG0MQ`3`'GC!b"d(j-%V'*E+_ $uf,`zQC/Ȫ@#ByZVoG>Elv!' <`%#f$)>V;FQBH6Vx=8)sC9[xR8Q>QS1ڎUg6*3Vs4sފ[!8l;Ho ݷ❘X3*(_5'=! >QR(J2ޢi.;4}@JOq?A@ 4qJ;QǩQ/NԘq@>mtڀ?J9}((1Q @>($>9/PN0EcxQ@ 8 G=f0;Q)8х~ ϿG>#sG^ p'ЧiO?.IANOs@ǾO֎izgNhq4gf}h&=(q@ށ8@hqi=.z111GCKy z!8(r{ LKt֠s/qE&Av挟^(@x}ELџʎqIn( ` JPUնka nHAzss TSZ>2~rҜHG &6Nq)@ROLt!H0z*|{ V1V^1E2:=ԼOeCc0 &I8  9s? B"[izf!Rm~^;KqRC1M; ķ*jO!|-ApQӸ=B:ʐҔ! Av, ZEvR;vǂ?Z}%vWNjS4b ;;W8OT(r3Pp ziX,ݓ9 \s[_ qHR! #U}6ԹG㊊0}j\~񫛢ՌzT׿u*X'҆ MhQ2wSg?C)6;p*|u(\0PQX)A t]ccozjݕ FOtj*OcRq~CKUڮ dV3T#ގ(4ϱAzp(Jj8kG,-vx@($#r|jn"\⇰2ȑXзϒx%cVRx%L+gNc"%8q*O“ʩ%a ܬ~fevGZ]wL:UGpD {~Dx &I<*I ˕ Vy_#A"4bN4<*ObIt!|d68,zrh"lT(_l'nM֠Ufr3gi`t)F4cP)y @G\G\&APHpNW<15k$zR"WR@ɸOZc+j"FT#Kp@*8Mv'L÷*Yʬd:*1Oj~'80 cjLtȤéZ _1eq3H}̧1~̓p8F$Ek#Ojpn>xAUJGx߷GZ% $`Ӡ@:x3Uo&}D= ,0܅9})C©Br$QT011-F$(! `1Q\e d `zTCU0;Vdn$1RAUwȄ,y8sҪ3āMǘ&S8+(gʄ1?Y;Kɱ9#4,@2eAؓӢiq95 k\nG4X@l4ۤqT '5d~*7u994|凯[a.Ҷ=LTp˗cH8i1ș?7]㚊ݥh#*\mhda\I8Y>U̅ewd Dps2HHvP wqGz@'֣K}9OZl L*wg?H8OƛG4g0ek=[?U籪#N@lR!X8Fٺ O*'ҫ&"=hiXңwnҤ`9<^)903PNNCޙ!v/j `Ăf:Ub3:m\y4ٗ),3BiKI>\ !lz`ԑŀW'X BH%b7Jx<|⛽Q6lǮj!nPydBDͻiTI^:K^&qDX!n xv>a/"Ы %H͸(O@jj2,0h[,')ztsLO.2Y*2ikI^Q~/1+< iKc4ZB!?OB3zTJTs,t8Yҥ$B0:7+![=\SG"WwnIT"CױUr`p)-ɴ@қyjL@t,[B稧掂aU c7>2Ie r{֐v1BeF>SA~]gH@Pp4ǒfFJg8IrOD[\l.7ߗC ~ Z^Ԅ&}H֗€z_L~4`ih&(Ԋ^V#AvPc-'Ҁ QQ3ϽԜw1@>Z3GP0h82h${Qǹ#8qItH~q>RKR/֝!8nhuoKE&6GC\ҁG;G=hHhEI~4PhcMсKրRGN1G4y)ܜ4\џl9 b?ȤNh{sFMޓ΁ ӵǥڌ1Ehr?F:QjNFyKIg}1@ J3aIBh?$tG_51}*S j@s$j\qP*$SCQop_'H di6bNsCQBy>Rx9J- kc4&q}h/^9ʀ>6uMT 58pwpzQ q$W@8#=L9[\$gSrzPshߥ!NgsKxQ?1qHHr}`0T0=s sj(nx j(\v˻#hn:c?ku Pa?ە?Z-P4 >P<1PAs>U@Ls=s@9Kb(.p) r҈¥_5wAF:㚚8 I8AZo04'`Wd8Kƥ#j\\Hw6@= 'ʨc6շ[!()cUD5`) p)dҊ^1@u!2{G2Q" Hs1Mtܸ)cbٳ=*^?Bҝj9;@lT#֘Y@~T$*)#nԫV>njIp 9Mtt Si8T*:(ӏKv4]ZH19''4eJI56yP;W#&TTQ'#4n˷8=i\F˝ϸ?N`pa4WcBm40bcU\{㓊T2$q@#eFE$4QVlaoQS*8+1O@%GQQMQUjBQޫ}"(r,J<,c†?)8< Nj$P "bN@?Oj6Y7eXT~T c70y ru8Jsǯ91'"9Xr[<ǧZx$iU®9Br;TP Wޚb9cSm>ьI9/5hCȌc`85IFGhS\:dR E0dr}ju03ހQz1AF=sRx,)Ǟ٤ )%!w0,x4;2FA YlŇj~Q/;"fb08۪eKMH@ ;nq=*|~dO|qh f0N(\ƪ.k|jE"XP4FM~RH \999 [L%}ʻ3i Sy`x%Il⡙&,9^qB`IFuR70ϱ \|$bfƍsJ,GPsN9'F3ۚ3.(&̢yBL#KzAҗ*!UY<$f202*4{r0F֚h* a&% `x'K$i*l~G#(7$Vm@^2(80xC$tjH@ rG9?ksHPq6@t′y M@r%ҕl\'>YHy[֒I.h=ѫ$j叩$.X'DNF<"Y'w&0e8`~."FEUL{t"ݹp@<1lLs FhM#wˎ8Ħvޫ$|@?Y)lJЃ@ȉdzSӵ.O)?*\ZB'4~4s~Pu>⁏z\i ('ҁ 9y/4}2E'^98 ";QDxq(qց_Q^1Lz1LBzѷ S|QO@.^c(֏Θ'9}hր422(ڀGA?9FOsqG>&@hs@iipG)qFӞh>`ɠzfihhޣ'i2?\QzP@*?~Sh؃QqN9ʏ|1@ZB4gS4>w4Tw~j;r?JLc4LCҎz0}pE'>c#1KӮG@ ^12:ߕusQ~4cFGI(=MF1>ƍ1F;3hP;~'@pEJ?֌}hq(!ϭ>٠SIzA .8ܚ?zu/3@}qK)8.qRdzsKc>Ԙ~bu}hǭ'SHr:Nǽ'm.BiǯZ@8)Jۭ&3Kv40Qȣ=退J\wz? QցO?J^MN>J =GQP=wA Cך@AiGQQc^=h|/h49ž=i1c<\`{=qKr)~_JN)h@G)q3IZ&1@v4(?#@ WFrèm'w@J;}hsNz >.)N:QTǡ>`v`G4Z0;(8==)zt{G>ߝqiyLѷ /@Z?JLuzb~h1z;J\vQa&1d4\RnhzTtH[Asڀ{җہGOJ8";p: q!>xǭ'Ҁs~{b~dznd gIzA4chǩ P084gNhN{ch=?J?:C){t4N`zP/y~Q)ҊQz}qSڀGA?_sK4'l4 3ʞiҀ۟ʗGNsIPH8H—~4@(MG@iy$b (w=Oj u2{$Oʌo֏9@HO $Z^}1G^?ZCQ=P9 }M&1M.=q@ Ph捾Qs@ K:j1sKQsI<{8?:?~A?Pϭ&zv;ct1AJ8뺗@4s鏥RHS4a#dڗOƎԟ2q8<(Ƞ'4gs@@'֓>攏jLH@}Gc֎h)~/ѓhJ4z^=sE%}(?4dzMޔz)$t44@bt⎴s2ޔ`'ǵoJ^xg(bq48sI>~z_h,?>'J3h{ъ)9n(cf1^{џB>N'=J84g`(\.sJ~P1:E.>8R0h1SG'E z~~4uGJ^Z9'Fq~ԜR@ß& LQڀ9}܏\P)yϥ/=LCp= z/>g?j'R4d϶i3ьr1-&EzRq1@ =}h<ʀO֎GɤFqGz^xLx4dg)}3J3@ sу돥=hߊ@'=}E/ 13@ ׿GNҗ>s(i4s\4ܜ}2qN)23F B|J8&M (ڌQLџP#?_ƁyGE=3@ HO֌3@KgQ\Pӿh<qGL zѻ~y8F8ǥ8QϯIϨ(r}3nG==?QK|~={~94r>fhT{FGz( py{:sF1ڌvipAю4qRtcAA_wJ(?F~)NhsFsB3@)> ќu}h>z?L3?Z\4cL{P?/z1sF=(Z^):1'>њ('sHO7E3 dѤRPr>K:@M<ҏ2qj/ƛn)x#=s@L/җ>ONwhsiA=?N~dǯKL@ OznIN?#<(.A8"~“Ƞj\#hϽ'Ԋ9?3@^3@>cϯQ€q3x7€; }),g4@qM9'_P})2F(,=F~ 88֗w)7wFNJ.OaJnG{Pj;֛ #FF=;'N4r869G@{QtJ#z\Ut?\Sr$)1PG_lʀԜ{fZ2{#o@ Qn}9@4`aE4#ގ(((yIJS@G(GC((4u|P@9яhutΗܚ7€ cOƐcJ3@ -'t@^(@R8; ^ONr;b܏lQӮ:P28R~4t@ E'G3.q֓G9drp>&GS9bht4;=h(6 ^i8sz}h&ojROj2/IA@ Qǽ-/hf.&4'M&M/=R`}>b)VRc@)32i@@ '.}zѻڀ{ 2{FAZ'4r(cGDZP1:Aqџj;t'ގ@ E&=2(E-&Gc8{P4}OF3K0q(9i0sKFG rz—4tA#Ҁb=(jCҐ; R;jL֎(dR!>Q֗> LI2{:3c 'tE.i:vFq_pti3\ڐQϭ4>xj7s8֐sc4tLQ{tg"v({}.r3Fs9Pw/=r? ^}:htZ^i;@J@;7'~LM(gzQ@?ZNŠnjw81sIy@PP 2}{>thR)x=?{Q:bj?_4{;*\xc .!l^FIt~yLPڌHG=:~0A$yQ>rZ IQ@KqPFnw:2x>\~;b #>j9@ƌ׷`G͜g.\P3y=ix Q:PIqGOj1>tmϽ/ pT}h##">cڎhqQHy@ tpF;h=G6:сGщ>i:8~=~`)1Ӛ683~7G?* (9>lIڐ#w"'Ǹ)xݶcF}G>Ԥf@(xIv9ECGN=h4`;Q0sP9@z/J=@Pr:ELQǵ" j)i0}hG֗z9h2A—H~~Zǵ~gڗnh4Lm.)>~7{0q@ u}:{@Mob(#hFOfOʀ zG>4n 9?OjCZ2:RcҀZ8")xHA^{R?0ތ{ўhJ9ϧ&{j9'׏j3_S@Xg(:RI:@җAcA߭4tGnGqژqGAR80\Q\`&(ǵ-\L vzN(>c~tg~tcHgڍԼJLSǵ.n)3Nc? 1qGOZ8zRc/ϭ0G>qz_ƀ= !8KFyrzŽ{h1ߍ( 3=J1h?OLAq*8@w;qR瞢9`p=h v4d=h)GN&GҎOK;'N1K aK@ ׃Q)1@ Ͻ/zC`(Ǧ( /l}hh;qK'Nǽ!f=ǷG x>br0y;P~#q@ =Z2} 9MR=?*\Q:~AΌARaPjLq~(cڂ}`RR}h?Z0h9' Rݣ98'^=)zsϵ4.} &1~{h#ښqM;FsE (&;ɥ~49}sΌQ8@3ӃJsOсӚ03@@h923K 1f zg B}hzQdfu$g_nhpyx4}9O@ h?7F=q@=dj\gi:>g##v@chO_΍''ҍQr=G\w߿@/o?'n?Lds΁>@ s4 vF3@}'23#4mE(Ph}(9*^qIhhs֗l N;iǐ8c?'OAG818Rw֓8=h}KlPZ\ u y84}ip1hqP`g֌)up(q?gu>d߅-&)81@ F2ގ9yP|{A~0('CR3R;тH=;&+qRQPs@/KyܚS۟&>r;Gj@v4i3R{@I֗sz1"|RcހCF}~c`{>إPT⌮qOҀ:zJ!I?|lPF='ގV7sK.?ts1iGsIKҀ—w)0)v>>Z\)9=iyG {4wQLPz{IbhqE=:3j31EQޓ/@N6xPуKI@͎?Z(Ӛ3OO֔}1F= &1hiqۯIӥRm&OcёE'$>):QQfx J\risIP/(2s~ Ls@`LсZ9}(~4gQP0?.xsciE/׏Əƀ 9h<(~dv Qj3(0=p;8@ ~(t4g=?2@At:8QN;f p~$~4q?J14jOP4 \#I}h1@4~t~P1*NOJZ1@ רqڂ?Tsߚ^({R~cHN=(hgϱ=hLr9F(Qtǡ4r;qߊ8GnH1@ 9 #@LӮivmO@hϭ/LQ\?-&j1 [(Ͽ@ӥzRƁ KΎ)0?@ ^i:P=8(@ ޗ?JLz?)ʓ0S9?ʏPǭCFFqPF3ךZ8PW#\tE'@ FiqF{fKJ23ӟ&@4Q}DdG)=ƗIŽhZB(?Z\!Q8h4~Td?Fh9hl~tNzP1r;dz9Җ FG"4Rf{Fq֎7=GJ;SIQE7i)v?j_ğ=Ǡ"{Nր!!Ҋ&E4(Zi  FؠP#zd]!c)xqwPp=~QNQn}7oF=Zv A?4{Ə€y‚~>Ԙ=@zJ2OҔzh_Jm/48n}h6PKOp(8z:c~Z&Wz@:ӾnIv׏ƝFG?ObvE6?OT sP`s׃N0ʁ I4`zP䞔`SRdҁF: 4cN&</ShڎH9848$z9;sG>{Ks2=*sЃ@=ɣ!? g41h:zR*0@GAt4:9*0AgLCA֌ד:G=@i9l;R@ 8Ǯ?*pϯJBq \HxGH1O/P}c8?:;{hE.}Aΐӟzv?i> is;wR~: Nc=IGQ~=1@ j>(QsFM'.GqIp3K۞~/\w&s\sҔ}1?Z0=(H"#Rғ9 40pN)Kvgۚ3*=QKNƀ {`QQ:z(=(~tszcf;Q(Ɨ?.}E'ҔR(@ ֙ϵ/LRRƀh1Iߑ@\⃎x=KO&;i8"ȤȤ2~t\dF=M'nRn@hҌNO.\?J1U ֛Sp>L/>;GтzR =({ u.8PQ1җRMҐq7 >N\})qPuqҗZ0:$u֗4gJhH}4 R9P>cӊ62:g= ^g#@zғMPS8t{P0.=FzsGKҎ:\RTLI-)~4сF?)~B(As@ "{9>c?^O.'9wlp:@rE"SJGdPF?TcQ@.(GAP^>` MJ`JGJB}(9 `FqJ>S8)sG_ZN; 0֌H>~9v!~SIyK!O?^~=R&qKր:QQ`14GtQ>^4;^ B3ތQ鏭/JN}sKPcGQv4LP9-t@'h 1 q?J>i?ҁZ_1cK 9 }E.E!> AA@pQ@}ь@ FANi:u@;QGoʁ0P(P!hhaӥ4 Rh9Qh()0>/?OPڌc4u"2?h;h#3dOfE:\nmҗ` /'h?ϰӏRi>lNiv瞟P`ta}M;p.}!`ހ*>S`a? N:рAǷKfpKϥJ3h/=%hyv4}ր T')0æ(#>Jis@<@+(G#-&9ւH&>FshslQryE'yTg(/@8z⏧@O^1F}F9gڀ sZ?1Hq?*9LҎʞ?'j\~4cg֎:>ƀ>y:gN>QzP82~QRЊo9Q΀&=qlя@ aFxPyҀx$/AQ;QH~tcP`u{ǽ.}F(R1ҖrGZj?3({Z^=ҌRgցN(Η>q@ Q)Qh?>j\ё@ ϽIdPҗpCFGb(:O> K8KjZ;PqizӿFG(DZ݃sԚ^=i9hcJ@ `u :Qu8֓ڀ OƎz=ъ4O֗'xҌ@88€@~-ץLG.=MPp}M:uFsIn3C X8<6p}h1тc=H4@W@ '"hrGQcz_ƌcA}A9P ?Z? d >`(@xRgZ: c0;8\}h N 8L}(vsF(b9}h(#hO4GoA@}h84(Fi8(~nԇތ^}h{Ɓxh6RcK_IуёG@?TIvG:f}zG&rx4z^;Q:FH= OqN&sڗOʀh>}Aݽʌ٠9旟CItQש)z~4{vҌ{RhZ9^M@4Q NhT܊??ʀc&8Z1N):<4ryRq{sKaF)p(ױ!'>(r&s@*93 /4?ɤ8=ϭ.sӊN)s@ zӳv΀?tq@=9>Pj?*nvP10qp*Z'J\{Ύs91_۹8#?Ҍ{Ɓ IG~~zRtF;t 9|QAQׯ@O(GJ< \;PrGAIϧN'R֗&=ph8@ F >}9>Q.w Q@֐jQFǁ`.#dhb?xP!>U٣C4nhzf?tN8?zgZ'^٤-&) \G@ R:FA*:@~tҗP84s~4`Zo˻hIgcoV >sB}}&=?)@j3HWMMA`)?JvGG4h\^?ɤ8 u&\qs(ҀJBxOZ Kҟ^~zKa֐)0?PqHh" R~rh<J?4PKqNSI8sԙR֓K_Ҁ 0(4b3aO 1(=h# @ q/&Nx0<~4to@h}Nh @.({14܊N{8@ :g44Rhw}iyZL44#"ړO@ h'4u4稣y'EƏgZ(z89hv>v&Kϭ'QIj\FHQϥ/>z)~v=3 Ҁ'c⌃ޓ Ns֗P0M 9hёhǭ&@hnf/Cz/nduN?ќѻԏP0XRQǸhǡoLѸgM/nzR4gAӽdzMJ3Pp;!ҎqAsGs^%cޏFG("O-?QH Q( `cF=3?:Lz€4ug>(ҁrh{PPFGi2?3#\ Lc֏ւ}hhiqF/ԜzhǹiGL>'?{t#ҏʀz\{GFG4`~4q'z8P>~}iF1gc{ր 3F}QǽqҎ v?C?(APNsގq#ފ(KFhޏќt 8q? j1FaKc?J_4c(֎(ǵvvh8<@ L?8I穣4mdߝ.}#A/E'4p8K?9)9iqQۆ@$u#h籥ǧր h }q@u(ހ(֌@O֏Ύ=hh@##h'#F@~=hxRu>޹t i:}r?*\8iryE 'J)sڊ{j_zA3@'4RBHuq@443@q9PqLxhRh~r?TsK N:`ӱIM(9/9HsIyAOKϵ qIK 9~qҗ:ю?\Qv鏥13IO3@xqIRQQQj4>oRd(hE.r:uCI4,?^3ӟǧF}FAH-'zOh9hO}sG<oo΀ du?lRqd{;1F(>cOZ1hz&W dd:k$#R`?Q@ shc$t&j93j3QP1ii1h/ϭ'@~G~t894QB=hZ:vAE&isy`:t4Q3@iE 8=y=M/?_Ɠrq@4RaϠ.>t4C@r4Rdw _\&@zHg-~&M)ⓟj(RI3;@)r{gF}4Rgh(yphh8)qOΎ;2?tsQG_J@'ii3ZR~0J^34`ԼvdKM٣֐)30=?*>~4LA?CF{KSH_֗ӟzPh#~Pj0}.}IP\{G~/ZN"-1L8rh?Zȣj0zJ3K@qOaڏʁOhbl~T~/~d|݆~ =(LLR> Q97z2{~4 8h~s@I;ZJA=y-'ҎExG ^K8&(~tsI~4?QϠPQFG4(8Rr:(zuh9)y>s?:1@ EǽQϨ@ 4;Rڎh?8}ZLQOƎ LN)6c~?sF=ގzaƎsK(~(/Ҋ@;|\MK84 '?M(ef}7p'qր#g&I~T`RcG#֊~4 t4~tDX3?J(^J2qG~Ť{R~f()8J32iylށ}(Q}({><{g_/4Z^@ <CGAIZQZ_Γ#h=*^=iipOƌL2y(I=J:RE/q;я(@Iځhx(),!3Z?*Nhsɣ>GGg>sގz@+F`)*L:ʀPӌGQG>'b'֊8уgK3΀ޓHcR~8;9:NbcI؏])'(Z@x'AQg?Lw84'oT:@G@gڀA(F:3΀7s@aF@}@~Tw'Z? (1\fR(ha|2qR?=(h9@PM.:уIs#9B29@ ߥ):q3Z_ƌq׏z18)s(Ic(h{ʐg(:џAJGȠS@'&G֖dNJ%ђGcя >Rcdg(㹠A׾h҂OҁuFh!.HRCRG#RFsNs~s)pG1PR( 8:ёp})c=x`&}h2=ϱϱ4c4 g׊N3G@ KL8NQZ=hi=*?I@M0nEZLg~cތ4ǵ? sK)>SOҎhM.~ȥϩ4Lڏ@F3֏GAёsihcќu8z3ڎQjc;qҐiz(/JCE&G)P3FG^`u4"hɢzP0GndP!z#IsF~PO~ ? Lz@ǩ4N(:u4@ ӟւGz^zL|Pѓ CҎzсڗzqF>/l ɣKg4rhdcsIM-B}EޔsRtCF1GOOOZ(QpLsR)ya_J(ï;KRҀ~1z^`&'g逧I=hJ90yc49c4GNOŽg4)xJ8€sh@=z~T٠Bc*JO1N)S?:Lzbb{ RqR /׊1Ϧh簠f_ΓIg}(I#ޛxLSQ@nSF;Z@W_#9J:Q@P=3GNHF`>9=)3@=ɠ@  O8=~gPSFOM:Kӽz?) #l!2Gj\JL/挀{TL}h{`~g9PxENp;P;Bi=@ GZLށjCNiq~t{1^1#IzQ.@QF}4y3Kǭ/JN(Ҁ@1ƃ)~lRQ=1тzZ^{Rcր;K@  t2z(?w?c(r1F>c:t11)sF34g֏΀{Q֓>dKRc>)7Hq@ sKۚo~l~r8֏֌IFsGҀzRg=/Ҁ qI;s.}N~vQ@HG:&>Q:PQ&>' S4`R@gM&}Hi{v$P~B= $s}MqAG=q.?:9zO#=qIZ/IHg֝ҁ(:cޗL=h}hG"؊>)x4 LGA ;җ>98@3G" sץޓ(~T4zQA@ zRMN7tK@&sӯր1ub&֎/4Fy?*=8\R~րތw'h(p9hu&@룿4piF;PRzf!>Ԁ9&}EQ\a>~&~)G=f} Ng׊? яAKۑ@ 4qߏ`ցϵ4vG J> Ͻ'=i9/Z&q/4J9z9ʎ(yas@▎i:u"$}(KP|џz;I^4{>`T zRR1(J1@4qzNԹ џZLQ@=iGt;P\RqF~iA/N4& :bJ3 8~ 3#9i AҎ0c(Ǝ=h3Fq'~?Z=9Ǝ= !,8gP0>gG>g4s}>_ƔPRrE,G@(ǧKS3y)Af=ȥސ .=Ԝ֐A 3iϦ(4}p(2qҍw:7s@?JLGz{;~d/jOɤ vA'g׊1sPƗ(#"ہK֌F=hGM{Ru<:6t4Q}(u8h.lRgN>{яNh8=MzGG_J\f~Y')i:?/^gx?*9u(cfƎI'#֎OҊ0OPIlIv}J9E{cRbPbRsE.hIs.()9=p(ؑPM}h):RHN?tQNIFO)hғr1K&py>{T`sM.@ )ysڗڀ G?'@ GRcJGs@ ILъ>g>)? Q@ q:G4tG>u@::u&}i}izP(j1@vڐ:0/Ҏ{Ru4Ҕ:/I׶>j\~goQ($GF4 ;ќv13!x^}()2}FlъM?9h=F=h~q3O#q~"mma\>4l K)pi>F1֣(d1RAG?zΐOΗ'?!>ƀLRg4s@}ҀO4~i@ 1KE%/E&I@繢@ aF =?J9/?@ >߀*1{qQvn O—Rq@ C/Ҍ@ ϥ4Z?*(Ҋ9>ԃ>ԸQ@p>sEsG^Q@s?!EzwE~TQPGQz(h֓AG`zѐzdb}(#=Os`&┞Psڌ{Pߐ"8ʃ.iQ  z94@ɤǰA9 0>gFA?J^ |}(|E q*RB)ztqtApgJN@ HtӨ}IR;QPip:4f`;`g?J~ryȠ!GJN;TvsIFx<”6zf Q(j4 RoH3@FFB#gFN: 1M9)r+s۰@3H8@#? 2=KʀGgI @ۥSAh9"bgӚL/f ϦLуPx掾?Z:vKIcFzǥ-Ґq8SHJ(ONsI/d?:NAN{~=L) BLP}.GJ:s@Z? 3?;Q11@ ړ'ހ8I-(ړ9@ 'Ҏю:b(QQ@sAE_ƓqҊ8= >ϥ@)i(/IKj9ŒQ@'^C@t{RzbGaKQPg2}(sFi2{ʏ€'ҌNI4sޗ>cڊ1R QGZ(Eh{RvI3PQPьvf)1 zR~ QƗ{dpy斓ҁg^擟]/i9'1ڗqGn)3FM QRQF .}_ΗJ!ݎ)sMGsףj7Qa@4~c;RdځG?J1@ :RNy1j9Bc9( 4c9(ZLzuE%-s#֍A@>Qzќ4c<9>~f`p9J_Γ;PNh4r:RѓQ@ZN1ҔfȠ\?NGG.(1?ΊC9Q1@9Fy`PQLюyhzƌz 2z`9_ Q;ѓҏ0뎆A@ E(9S@ xތ@>aR3G<y .3ځ@Ҏ(<GE'E; ϯKsg٠>t(4t_ғ8 (}h ^qi8f4~hN(:2qގhƓh8ht}K &)>)}14Ϩ '=E/HW=Qƀ 3AQӹ&Z\c΀( ?'M/^(ڌQ@}sQ@֗}1J\Ru:}րLINa~4gړ?ZZ?Gni8/ 1c?JZ&8Կ&|qI'ޝրK;ʓ4}Ԅ?:4:sѐhJ:zb ?:> ZOʌ#=phl@6yQNMQ@h߁9r{R㸠2:@QK;րGFzbsȣ>ƌ (gьsFy@@)hϵ&y91F3)>>FGqK114t?:ug N)߁z@)>ǭ/^Rw13)dќzRuoʗԝ?J=((JQGj;P12OCKA%&ќZ>sG^t`1I\ZQIڌ('җjNt@ K3;tB>~8=\ctpz@=hh? ^i9qoʃG>Q۽.>ttF}x&ڒz/jAG>}E 1G}R~@㩠gRc9=3IߜR\ZLTғ {Qhfq}zP сh;g?.=)zj@C'%~SEb)xcR@۞)pjh ex; ?h8L&NqsiAidIu&rI֗ר4tsv#J'>!??Z1){џrE&{fhw.<)9gԚvHqI==#mhis)@c~B{C; 1*8<  폭.i3۷ҔۧҀ}9 ]Vu.'ڎEإ~q/SFpqNO֗RzC]Rg?ZN>֓ǥ.dt֗#&Rv ʏ^d5(qG> iyhϩ&Fz;QKאi3(h:tϾ>粃ѓ! ѸJ^tq(hOCF((&=)q@ ֗m&.=Afs0cG?|&{Pb1G__ʎ;KPsQ—@ ߎ(_~TtsF3@ Gi0hh*98⌏J^~P?Zq@=p@I:~(ߟz?1Is@ @~4`@yA⏛P3:LzqF1@:sE}iz@>v{ƀ#њOҀ wȣ@qFFh<4gƎts0sҊ>?!y?SGJLh#=ӡ;_?F: > —u&b*>т;b:Qc'd ^b _zQs@_Ǝg4@ 8(;_҂=@} &N Ӱ{Q=(R~d wҔv9?ŒҎq@ ߽{ԘGGNh<Oր1$v@3yځg44Cj3(F=i2=OҗJΔZNSE&Ol~& P1_ڏƎ}h(s@#GփӸzс q=( 掿@A(GF3h=M(G'Hg)>0HE!B?ZpL9Pz3#j2{`@P=F9F擧J\Ҁ}%(ɠ>擌w9)i3ihcKN(cKzJ3t4(}xZ(;яJ:~t;Q~f\Gi8y=#ڗ((iiQs g4NO^n}hIϯր ggF=^>d׾~~u(?\qG>@ уؚN}hx_P1=J1ECGN^?LqGяΌqRrq_΁s?J\~TQϭ)ih> 0{(Z=hOE/_Ck&|b;ԉwQڧH ǥV`uN9> =UcF%\n!h}})wcA$}=23:Sg~n__qNݞ> xE&D0^@3}.GcQ#\2€VJ2Cwc}&s|{MΎ; 4?0g9qN> .4FIz◀rځ㠤IhsA))b?*\('ZNOZ44*=r)28g#_ƌFOր'#.}h(럥&8FʀGO֗&p?j^1~TLށ4)yaϥ&=Ҁ8GҎ{ N}?Z0=(1P I~gIϵ/Ҁ q-'>} Ҏ(ځnGG@gF9i#)h !?:Z(1٥⒀4q'8GQ*^>=4.3E& \QJ9)9P!1jL.h^8Pp;~ȣ>~dџ(1? ^ZO48Qǩi6Ό{0=Z?4R}E]cڌsN}()q4~#4g?7cKx(h9掸K(:hb{RdzsP!~4f=M-b>rGZ)硠~gޗ'34sO(F?:.);8_bpOz^): 2:?.sh(җl}(f?: 90E'KϮQsKڀ~(9 F)h&9F=Z\ ?MR;181Pq{Ti0=(ޞSz1֎݅;^?LRh&ӑǦ(8\1h1F= 8g돥/J>cSސǷҀKFE# éwhh{џ^4A>Qӵ2tQ {Qhy'1Ґg=88t\סP ^E {b}/@^QQ0K4~u?*R> LJh9u>j1G4|hE=ƌs1J?/€?;~Aǽ&A:=~Z^iR94'@Ž}t~4Q@Q((ϥҁ9s(c41I/'ǵKZ?~4QOP1r(=PџcIɥ~QF}? ֓AqGK(a?:0OZL)0}s@=*1F8>t .3G@2=FOɠ}1IƖX5a(;GHPt T H@=G~7` 0ss@ eŽϭKGAq@Zv3F>m*6ҜtHtb,v/==9ٞKw֏zi 4Z)@)zPͽFϥ?)ͣ(|SE3h"PvG|Sf'M=y“ ;Kx4S4c֝G@ #F;8LfoINP)y=((}q\J?*Lq03KzL ()(s⏘~Q@Γ'=G@ǾizvҌz}(i*2hҏp9AcGii? Qǵh8@lb9Z8Z\c.iph)1wPqI)hJ>43(#iy'Ib)~4(14C ?> њ\(2=x#֗4~TqJAÊu J_F3׏ƀ (u(h8(L_ΗQwڌMRqڏ/4gZ\⏥R{斓sGjZ(:QQE&1I=q@ &GOp>? Rc@ n;JW= :rMJ1u_K}(J0h~/֐PzG4c=E&(iqAIǭ/=tL Q8()ON9KLҔQH;џQKP}i}hǾhZZOΗE  )zS??iNh'4sqKIǷ@h4Q& cE.M ьѓ@G(ϵ/=hGN}(j=98/=j>j>Pfi :}hQj_Ҁ4c( >4Qj1FGF>ϰRdP?,}hA@=Q@٣hh?KF=p84Q (0֎:яʌRA⎝:zQz@o>&h64;u872`Jn\q cQN(8 7J7 `&>ppIMgssܰS2?J]€COpp0Ɗ2e}(2)QeiQFG\wⓧz\]€ۊ)rZ@7b+@ ?J\b+FV?\sN|?2)?tހފ_4G~Fё@ ~&ireh1Iv||hF){ǭ0)qKǩ~qFG7(dz>_z`7ԿΗx;Ə0ǠNhh~t#րj;tޗS@ `Z8cڌ)~Z@3z8ZO )~Z>_z@7>ߝ.8Kzf>n.?ȧ|-09qG@ /@ )qNޏG|Rȣzf1륥hbvޗ"GN|N;;Q\-'Nb"@7Z0=qNȣ"Rp)q@ Ӹ#ހ\2(􆟑FE6S=(hu4-/uqK@ ǽ;"CE.E'׊vE&GRnp#Ҁ]ҍҀ}sF1KpZASNI@QRzQڀ&G''-.I@/R6 (paperwork-2.1.1/paperwork-backend/tests/guesswork/color/tests_libpillowfight.py000066400000000000000000000103461417573700700302670ustar00rootroot00000000000000import os import shutil import tempfile import unittest import PIL import PIL.Image import openpaperwork_core import openpaperwork_core.fs class TestAce(unittest.TestCase): def setUp(self): self.tmp_paperwork_dir = tempfile.mkdtemp( prefix="paperwork_backend_tests" ) self.test_img = PIL.Image.open( "{}/test_img.jpeg".format( os.path.dirname(os.path.abspath(__file__)) ) ) self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.config.fake") self.core.load("paperwork_backend.model.fake") self.core.load("paperwork_backend.doctracker") self.core.load("paperwork_backend.pagetracker") self.core.load("paperwork_backend.guesswork.color.libpillowfight") self.core.get_by_name( "paperwork_backend.pagetracker" ).paperwork_dir = openpaperwork_core.fs.CommonFsPluginBase.fs_safe( self.tmp_paperwork_dir ) self.core.get_by_name( "paperwork_backend.doctracker" ).paperwork_dir = openpaperwork_core.fs.CommonFsPluginBase.fs_safe( self.tmp_paperwork_dir ) self.pillowed = [] class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 10000000000 def pillow_to_url(s, img, url): average_color = self._compute_average_color(img) self.pillowed.append((url, average_color)) return url self.core._load_module("fake_module", FakeModule()) self.core.init() self.model = self.core.get_by_name("paperwork_backend.model.fake") def tearDown(self): self.core.call_all("tests_cleanup") shutil.rmtree(self.tmp_paperwork_dir) def _compute_average_color(self, img): img = img.resize((1, 1), PIL.Image.ANTIALIAS) return img.getpixel((0, 0)) def test_transaction(self): self.model.docs = [ { "id": 'some_doc_with_text', "url": 'file:///some_work_dir/some_doc_id', "mtime": 12345, "labels": [], "page_imgs": [ ("file:///paper.0.jpeg", None), ("file:///paper.1.jpeg", None), ], "page_hashes": [ ("file:///paper.0.jpeg", 0), ("file:///paper.1.jpeg", 1), ], }, ] promises = [] self.core.call_all("sync", promises) for promise in promises: promise.schedule() self.core.call_success( "mainloop_schedule", self.core.call_all, "mainloop_quit_graceful" ) self.core.call_one("mainloop") self.assertEqual(self.pillowed, []) self.model.docs = [ { "id": 'some_doc_with_text', "url": 'file:///some_work_dir/some_doc_id', "mtime": 12345, "labels": [], "page_imgs": [ ("file:///paper.0.jpeg", None), ("file:///paper.1.jpeg", None), ("file:///paper.2.jpeg", self.test_img), ], "page_hashes": [ ("file:///paper.0.jpeg", 0), ("file:///paper.1.jpeg", 1), ("file:///paper.2.jpeg", 2), ], }, ] transactions = [] self.core.call_all("doc_transaction_start", transactions) transactions.sort(key=lambda transaction: -transaction.priority) self.assertNotEqual(transactions, []) for t in transactions: t.upd_doc('some_doc_with_text') for t in transactions: t.commit() self.assertEqual(len(self.pillowed), 1) self.assertEqual(self.pillowed[0][0], "file:///paper.2.jpeg") # algorithm may make the results vary if it changes later, but we can # still check that it actually changed the average color of the # document self.assertNotEqual( self._compute_average_color(self.test_img), self.pillowed[0][1] ) paperwork-2.1.1/paperwork-backend/tests/guesswork/cropping/000077500000000000000000000000001417573700700241535ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/guesswork/cropping/__init__.py000066400000000000000000000000001417573700700262520ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/guesswork/cropping/test_img.jpeg000066400000000000000000002422011417573700700266360ustar00rootroot00000000000000JFIFExifMM*>F(iNxHH02100100( HHJFIFC  $ #," '.0,)7(44418=9'.<2432C  2!!22222222222222222222222222222222222222222222222222p" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?( (((-%4b3KIK@Q@ ((((((ʖ v+$& F`?Z3ggT3|r1c=h#UbI)SBD*#d#= #)Rd;p*J)^D,$!Ns5}_F$?Ü6ShFQE ((*L+&}XOƾ >/Gb0l{F8Դ8KE%-Q@Q@Q@Q@(1N4PHihZ((((E? fhttp://ns.adobe.com/xap/1.0/ C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222p" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?( ( ( ( ( ( ( (B@@ E*r(h;E7-N("((((((((((((((((((((((()2)[4PQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@1?ֹ>(((((@dPEPEPEPY':O_zn DžjuPGf(Ө((((((((((((((((((((((((?4PQY ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( b}@((((^((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((@74PdsJ9{ӗE֍H"r2)4E( ("N(((ݚQQEh^QHHhG4)4(ȠȠFGJL\Zn : ( (ޘ/@Ns@(":ёH`RpiQEPxNR& 4( (< )O{<֝PӭM@2M^MPdRpiQH=)hniN &"p(ȥӨ(" ( ( (ȥni4($JZ( ( ( ( ( ( ( j{袊((((((((((((((((((((((((((((((4PQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@zQ@Q@Q@Q@ KMPIwIK@h(("i {{P*L^:Ӳ(iNn)K@Q@sEր(ORi4 ( ( ( ( ( b}>((((((((((((((((((LZ() -Q@wQEQEQEQIK@QEQEQEQEQEQE&E-Q@RdPI▀ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( n :(csҊ\0hOJ}QE&Fqޓ~{SҟMNȥS ()QEPx"=ҀphR4n :)4(QE0iPDzҝH 2(ȦhQEQE5=)n %\0INȦzP474/PpiQ@Q@19H#8KM ju&E-7EPEPEPEPEPLPw>ojuQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ QEŚcsҟMRdgiҀYL-Q@Q@Q@ )uPEPA(("Ҁ LJ}QEuN ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (s@&E-7@EZ(N()2)i8;E&E(KKM AJF :7474M@ ZUuAiQEPRdRE@A@ EPdR)CtyO((((((((!ޟH>PEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQ@(#8@ Mߞ(jw|Q@ )Oju4(@Q@ p< (@B@JPA(nm8LdRM:4\uPdR)QEQHH((((((((((((((((((((((((((())i27g;f:M. !zR40'+r(NȦ:hAF;ҷ4֝EQEQEN (IJph)n 9x(((Ө (s@Q@Q@Q@Q@Q@Q@ KH>PEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE/QENs@Q@f(Ө ((QE74Q@ `OJuPEPpiQ@ /Q@7EQE7EQEiPEPEPEPEPEPEPEPEPEPEPEPEPEPEPx(((((((((((()?iPEPQEQEQEQE fc M8'4PEPphȥQEZe>9O/h)QE)+sMPGZuQ@Q@=)iN(E u! u9|b@ Nȥ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPL}3[g}Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@EŚh@:((((((((((((((((((((Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q4QH=){ ( ( (gyӃ05tay(d.S_'3pG+|=.Ycnz 6-.?2XG^8+=,(EVQ@Q@Q@Q@Q@Q@Q@Q@Q@831O i0kُ<?[^"LZt~cӒ@qskzi+m!HN"L>Y%ӭbGK}j24đ€;pyhQ@V\N"K@?$W0KIR}84Z+.oh[8ZY.mB|)UTRKݓy>* b$ iKd N)QEQEQEQEIv6ر$q\]LJ#pg_iN1UF >&<[S⥏)2|0du]=K)K8!Mp`6Զ&3[_ޒ{Ir) yT}r>׮MqNs6GnDz13 w[t{3JKtP~{WKO8%߇mP\OxGG%}tɭQˮ 9_rxS_`Ђ HZAGXQEQEQE!oՂ+h|ۉ0v=9]sN JĄPXs׎Yu E(t_Êք&Gd0Fdr>njZ@B'Z Y!`9fIWPfy k}kH3fc#C}jOŃ-I83yҴS>ͪܡUnֺ"6xjnWhEQ@Q@Q@Q@Q@Q@Q@Q@Rdb<=cF$UBpH J%vYF}o|l!,{rXuN}+WI.eèo4.)һp]yq^y㏈颷UqJQ}MKW,'HUJU_`pk'}ih=:Te(dY!GA)T\>56|RP<=GĚk_ޡu#7$OnA ᴳ,h`#=W|?h7 d~uv' |/f2;_Vjea#}KY0:iuqҤDH\j*mBE >8Qjw/nt]*gt}O-TbuNU68|/4(-5(7^!%-n_EwV;mWZED (Q@Q@Q@Q@Q@ &EdfvzWV=#JS&t81'ww]E ˅6+;ÚD$}R+ϕxAZA[VL.x_xo@4I;ܤ -Cww(*?w1O%bxo :3,|ߕܒz"W/׈Ou$'ԁZ %<%i͊ $RAȮO;׵i͜`Ax&4/+Z{2b:OQRMGQl'ʊ$A~WvE+5s2Va1r{GIhR6"VKUVv-CCW8 z Vm34}t{hb(ϭkR ZGMqtU,-I10KsT|I+_^N0Iw|1]'޳Ka8l=:Qmi UW,Tj͌V.-(*q~'Vռ?gRcu$ޡ9QVe1kxxZX<ͥVnB=KkOƬAͪ v陥 %pC*L T&箷4)31j$0+wb׷u\ITW3ḮEE{!Ggoxj3Ft-=lz Ӟ8(*(hN6[ICsEACsEQEQE74QEQEQEQEQEQEQEQEQEQEQEQEQEQE7EQE/QEQEQEQEQER@ EPEPEPEPEPEPEPEPEPEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE`dZ((7}ȷEʓ:H`*#Z>Gk=&/ߚ6P?λ@m|#B[zP^}r⥉Mfo4}k pG; CiKy!ܲ8U[i.֎Fj@G5><д{e<=}#oԀEZ<] $2\n?W${QE7x۞qQ:=<5xſن-3MIgPsN1l .]ivY! Fs=ooF{EHu? %i,"I%biD+g]Ȼ\J`wK!q'~\ /N]յ#c]W0λd#(r2:ը}$QHX>+-@ E1$W@E> ( ( ( ( ( ( ( _Gi7r6Ă*yŋ$l;l\Ic#A^H.AV ~ O4ǩS{~5^uDm~IMkFV(tiA;g@+iu"L=a{&>i+Z+At충7c_n($0n|Imkė%rKHҴ܉?;}:nt_LܙRwJ+9I((((((((^Y$_i<_.,{ z1?!ҽ0.yL} IZSv$4Oj Z淨H޽I0kww ޒ"lZ&*JU*6AETPFN$}WGK[@g.F-U$Xdž{7c1J? YBH"r݂NfZ5샃/`zqֽHm4*8V6FQ^ 5.A-VN<9kwP@:T~v;a" ǹy/5jy5T``c$>̐.ˆmsJյ-'OFE,\^xC-B6XZ9OϽL!mXI%m h '>u.ޜ[_}ާg@WK!q].dXݩ- Djt2Um2'G|@Ԯwž"ЧP"lЎkh|M#%̎OE(-BHKZLv~"#1<-C9.GMw>i> wZūf^,ʲJ֏XM_UxuR9e Ez9`ps^y=6^1 qnykF=1$Cu5-x营[{uRIaX/_8lu) 7ILG1_zv8Oj.Y4Ov3š͵+v7KHOG q[R|\$sO%Zu)$P?5"QvTUݲH]Oͪ_~tK'2?xZQީpwFiUOB M{6VPk^ Ǥ.5y2G dF#ǽ]YEp~ [uǙg[u9 oYSa!>k\V#ZȪ7!8WWphztsQw{9'Twu]Hu'qҹ=. jkwpW&Hߢ* 8|Ǣ7Gƽ>|EbJ9k[E^bMpJM{4?h(8OQ #λ^$d`TøwM[wYx<5j%H*HZ(w>VhmL ,R$6sdzuaMoNJn1$L"nQ+!U(Oq֘7[jv"y?j? 6Un%ج--I<1N f)O/kSBrS9QEQEQEQEQEQEgo4˲!砦hҴحY2?^U\d },ER,2IyOhȅCZ_^jC-YpȿֆXz[k6'c?kj>_*J >7'MBH 1ȥ995FJ2)OFGNWD1]j6:[ɧ*ͧQ k 6Ihlכ?m? ҭaNk lmR6aQ\tl>9܌Kƍ:UlUhR@tQEHZ/,VkeWJss8oc^, I[Z0r*nVdQ|aNP״7Υ!b[$+ڑG%|Ww%Y%Oc"xJxFc+*m r3MXI Yse͉8>x+Ώ=zngM \w5邐8JZ@U ԴM;XΡy| %?kR`T QQ<ן/|neg}ZHIuC3^1MTzy_Twuy98}Hx >TQR(ntihew69IӯmW/>v V%SdETQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEU'0%hQEQEQEQEzZnQEh^h< E#Pv 21Ԙ4΀xRs JPΰNj@AP23S 4qFGB l0RpidSiphQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE(^(((R7JZ===Wږ ( B@Jx':#";?4PE(((Oz((((((((ҖEPG|QGf"N((EP9 ( ( ( ( ( ( CEQEQEQEQEQE/QE5AnW);1ޝEG1Ԕhӗ(x((4((((((((((((((((((((((((((((((((((((((((((((((((((((((((((PEQEQE74QE"4Q@'@sEPEPEPKEQEQEQEQEQEQEQEQ8(((((( ( ( ( ( ( ( CEQEQEQEQEҖӨ;ҀIFE6A 4 &8zQwZ~{PdJZn>((LZn : ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4PPEPEPEPEPEPHyiiZZ( ({PEQEhx"@ =)AJ =(Ԁp)G=)~h@NHҫ2Oq uҀEP9(()2)iހEPEPEEQEQEQEQEQEQEQEQEQEQEQEQEQE(nhEPEPx(QEQEQEQEQEQEQEQEQEQEQEQE&E-7@Q@Q@((/QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQ4(P9QEQEQEQEQEQEKLVL>(((((((((((;(()ZZC();1ޝY ( ("NE"f;7)OzP;ښSo(U+0i Q@Q@Q@Q@Q@7~{S((EPEPG|QGf ( ( ( ( ( ( ( ( ( ( ( ((((u4PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEPx/PEPEPEPEPEPEPEPEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEŚ( ( ( C((((x)KEL#)L64`Ө((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((M-PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPE((((()FS>((((("@((T3Q xFi"9(QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE(^(x(nh^( ( ( ( ( c@(((E#sn)iݞ4ٟ;(ݚC"Juj2(h( 0Pxs@ HR iPEPEPEPEPEPEPEPEPEh((((((((((((((((((((( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (TjYqU/Z&yk< ʫ](ij@(((((((((?4QEQEQEQEQEQEQEQEQEQQcc?I@((((((((((((((((((((Fp)h((74QEQQY)=}h^( ( ( ( ( ( ( ( ( ( ( ( ((((PEPEPEPBEQEQEQER7ZZF@ Gf(((@Q@c~sIG|Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ +ƏyK!Kx:]1޺֙ykn\Р7~:+2&u 4HjkFM+Uz ]O|pEloI'l4M F5.x~tc**G*WZɹ+z|f6$}Z۹Q(((((((((((((((((((((((((((((ER5[-&pFnOhG▹k_xrOhX]4sG*é28n}QREPEPEPEPEPEPEPEPEPEPx4R7+K@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@#}oQEQEQA (@f(((r2(^ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( Mf~lg(5ѵoΟSE5׶Q2 [ᴓo"gY-:nr# {fb5?j0jVu'Ĉ#itͤkӮO%H:jweأ` zS袰((((((((((((((((((((((((((((((GeD, :<0۪K3{/]B|(ս1g~ IRP]VZҦX+$S6 }]# {WGSuŻ\F3"Ү7fI[PZ[̡pVtٴ ivw$۪8$·7QH((Ȣx\Z( ( ( ;fL((((sE QEQEQEv(((((E QEQEQEQEQEQEEŚ(hq@Q@Q@Q@Q@Q@Q@Q@Q@Q@#}奤?yhh((nh(((((((((((((((((((((((((5,wZ9h-#C^^}RoxQ>c oK១[OE5]s dU/ܛi,#~ ybaĊT#ĐGiX i'z-yQXQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEr5u\xqiYcbB+n|Sڎ@s.=z{V1!|LOž6<@nN~F8*xWJR_UL>[J ۍs\GF][HRS鿱pZC;@P8Q\Q@Q@Q@Q@Q@Q@Q@7EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE#zө4( ($uQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ahCbg5ތQ4h#^\O;YaukxMap&$u UiK^yco:60 ZeQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ge[\r4(y?{WU%0y\"ֵ؈$HTe+4qtWDIX&lG>:H#Gдt>/*GM qqTQEdPQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE@Q@Q@Q@Q@Q@Q@P9;(((((((()@+S>(QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQET705{y=ɕ9v5Ly <(EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP֖)D Qy+$}wԵqDt6L6ou^zQE ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ?4Q@Q@Q@Q@Q@Q@Q@ wHI=PCf#vii4(((p(-ɩ[ޑwo h؋UzQXi"hhdY!9 z'^iZ[?[&sZ£G.//ׅO!M B2l?0xnC1j}kLOc~ :mh:(8ŠP?RQE#X(((,QY ( ( ( ( (Y2I G#]NftE6f$=8MG%CA&&q[,b&-2sZEn;U`r4|-:e6NkAE#Ҋ@Ś( ( 4PPEPEPEPEPEPEPEPBvLr+l:\DIvzGҪ%'h֭q۽N#%S\].F>Ѳ1^UOܵ, _9Vzxsy=V▘_ gvknz%ㄎ ̌ 5*f8eást}!Sx]OB5(e=+Ojz$-X^z^V#c<1yQ{x\tk{lH4wQ@Q@Q@Q@Q@Q@Q@Q@Q@Px(((EQEQEQEv(N ;?4RZZkN:#8KMju PEPEPEPEPG(eG' Oyc~AF@ EPEPYr٢ (sKMPFzuQEQEQEQES꒣IEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP񿌚i#<2=^ks:@~#=c~袊?A ( (&+#pF+j=6|1Ѿzcׇ>Wg N/3֤A((((((((((~T'͙N Q^w(j׌ӣ$gОI(bdv>oiQV.rkBPXz(:]TdNȤ1((((((((((u$4}W8'=x]䷗ <ף|Ox,#ޜ*M(((((((((((((((((((EPQEQEQEQEQEQEQEQE74Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@1O_QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE'Q^zE\,:׻q^{?ók(Idz=uME9QZi5矤7ʹEP0P#pyd.ʨlNf]*= x_ u@|#aOM}#(+%sؖ(傊((((((((((=kv j-GC\)5 ?i2^'aoymsբD@:71Mb~Sp EP1G48'58Rҏ2^Ǭ|1v: 'm* @dϭtxVMw>O((((((((((((((((((((((((((((((( (@Q@Q@Q@Q@Q@Q@1O~((((((((((((((((((((((((((((C!r6ATPSirjzTO-~>^tA8aw6tdUOq"_;嵄\h\}RJ+Tx# ˁ)p3W?# ymeS܀?WO~*5?ՇckzE 1=zN}zW?_]O+KtkEXC㸎cjJm9 hK8̇c"#Sq]q>*iU-QE ( ( ( ( ( ( ( ( ( ( ~ vSմ˩i/uvŸ&oJtԮ <7&Z] ;}xW0[۫k^3ҞcU1]=Lq?.|V~3ŽL5jv羽ԋFTԡ. " hǷQתƋB4M(қ o1E Qwa* ( ( ( ( ( ( ( ( ( (25u=6[v]Čּ#PJ% zWG|Uס2${.q+9lx٦ ^*␂*qgp`vaҪٺsmJ2ٟ,fGF L!B .̈H:]'8- {/.uF?n~u+穮V&4hOW/KRZБ ¨%W}pQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE( PY(4PQ@Q@Q@Q@Q@Q@#_-#_-Q@Q@Q@(((; ( ( ( ( ( ( ( ((((((((((((((((((((((((((((((:O1CCRt?/ _|zꮡ\WDHw-QL+~.Lè(sEQEQEQEQEQEQEQEQEQEQEQEeVZM ոe?޹k߆SǬP713𳒮VW6{j}+Vᾑn;z qFjb>G-/VBVqh)3Td:(PQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE((((((((((@A)h((4QEQEQER?U?UEQEQEQEQEQE hȥ,piQEQEQEQEQEQEQEh((#8KMߞȢx\Z(((((((((((((((((((ހ((EQEQE~dgc@3Dss1r~4KqJJ*Ş9|-nR\vu5/벲ek<5]\7mswt\jsʡ}kuYķ|-Ok-#L)GfѝևK_dۋf>,>ZH2I?C_1Nƴt]nDfZ«:hRC9MXak{HlWRiz]!zXҝLaQY ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (s@Q@Q@Q@Q@Q@Q@Q@Q@}JQY(((sE QEQEQEQEv(BCsEhx(,SKA@Q@f(Ҁ((s@QEFE`~oJuRzмQEQEQEQE#+:?ޠEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQERdgi;@##޾m>⻡{\ƝU܍ t(E|wp'ߑs lڊEsZf)Jes`bQE=NM9Hi rj:ziG23B=ilkp7{}&9Wepn(:PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPBEQMWW4((((((((((((((((((((((((nh ( ( ( ( 07gEQEQEQEQEQE~#+:(((((((((((((((((((((((((((((((((((((((((( cͱ,H%v1VuAR|Fm#ŗHb1pXe?{=tݬyS1R'"1 4( zQO'`HtIn [as]ϰ Ax ;ȦV! rG k+j:5OP+S( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ((^((((((((((((B@Q@Q@Q@Q@wQ@((4PQ@Q@Q@Q@Q@lG(sEQEQEPx )23QEQEQESO09OQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE%rcحOZӣϬ8yOV%}"--;R(-vU`((((((((((((((((((((((((((((((((((((()=}hEPQEQEQEQEQEQEQEQEQE/QEQEQE Q@ ߞ((((:($?@(((((((((((((((((((((((((((((((((((((((((((5򪏭>MrKZs-y#W>j>j\cY2=QL((((((((((((((((((((((((((((((((((((((((((((((((((((((((_)>((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((eާS( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( kzL~ g( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ^IQ?ޠ ((((((((sEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQES꒣O`%Q@Q@Q@Q@Q@Q@Q@EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPQ?U%G' @QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQES}2@Jq@((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((yTr ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((*9A+IMr64( (Ƞ" ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( OIQ?(ȦE. :"Z)2(Ƞ(((((((((((((( (9O#:"`Rڀ"VMOL> ( ( ( ( ( ( ( ( ( ( ( nwQ@)h^((((74QEQERdRhQEQEQEQEQEQEQEQERdREPLnzS4`cNn :(2(ȦdQ@QEQEQEQE@Q@Q@h((@A)hO0}ƀ$(((((@h((((((((((;LZn 7h֔r2*;;e:4QE&E4`Z klC@P-?塧y?__ҝIih(((((((?B?T%Q@=(޾  4@ %BpH)V0Yc9ACy?ji8c4AOxYۏc@UQiZC|7Sh?/})ZD0Li~TP_~YKwW$¦oGʃh3X??rx~u®y)b[;<iGmR/܃?!H.83VaW{b2It7ep>,>d(2=Ya򚺷ISég4bbMKixWu[|*mQ??*gaϏߡ}xOJQ"xS.)1}J<3җbѰn'U"9UIy?:ڀt՝N%Wץ%"R@&IKMQQ?@ȣ"<)r(ش4ւ:fdcPz]~u7Ŏ1,b:>O֜*I֌Iր%޾dc=ҝwFPy)>FA'?kZ- Y޾ٿ顧oJz9 ;cl9 )7E?Ӷ7?z9u4͍e_ր&izO:?Ρ_L?S?ƿEK)O:=hԧZm@A({R(!0hَ*?%$~=%G?)@<})a~+ +tAA| c4v0= A)\ugzoLgruDXSt4'ʫi$?Z ((g|IEB֤zP(=i<(ԙU=ijvIBh_{zTT?GT?QnOzCz'F$ߜq 7ʖ2H?IEG~yݧ PCQJWi&~SKfld}3 TBa?HAPE4P4PE(TqޝF>ldqH?>@A)hzRE"Q@ h((((((((~RTrƀ$((,E&EZvEdSh( ( ( ( ( ( ( ((" ( (Ƞ(zREP)TPqҥ H)}@ ޕ*J8=:S-28s@QEQEQEQEQE@7EQEwh((((6--PEPEPlZZ((((b@ ^)4(^((( ( (b{Ө ( ( ( ( 0( ( ( ( (7}Q@dPME. 4`{S4( ( ( 4PPEPEPEPGf(4(sE"-uQEuQE;ۚg7)EQEQEQEQEQER/ KEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@EŚ(((()Su#Z(EPN4(#:(((((((((((EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEր((((((((ɢ\ Z( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( I>ᥤ@ EP)pi)QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQESpw)Q@Q@ |y@((BEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEiiEuPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPx((((((((((((((((((((((((((((((((((((((((((=}[ޖ ( ( ( ( ( ( ( ( ( ( ( ( (((((((((((((((((((((((((((MLYEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEh((((((((((((((((P@Q@Q@Q@Q@Q@Q@Q@Q@2_OOMP9 ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( d_OMݠ/JZEK@&E-7EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPL~4I?շҀKH)h(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((@u2o9zR/JZ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((()ap:?o ii-QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEo-#P)i-&gZ03(((((((((((((((((m8QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQG|PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPI'R?o Җ(((((((((((((((((((((((((((((((((((((,EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEEŚ(((((((((((((((((JZG("2)i4((((((((((((((((((((((((8 z@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@#R?o-Q@ phQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE!e>Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@HQҖ ,iP9PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPHq@ EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPCsEEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE -#Z( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@#ZGQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE74Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@#KHp@(ȥ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((-#U(SZ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPHvH3K@KL^##H9HQEQEwh(((((((((((((((((((((((((((((((((((((((((((((((((_j)V((((((((((((((((((((((((((((((((((((((((((((((iipR'ݥh^{U슒Cj@Z(@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@#ZG'ݥD@ `z@ xJAi;PFÃuC>J=*@Z( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPHv >ॠN2jf (!zҞ:XҀ$KQA銐s@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@EŚ(((((()7+@}KHpREPEPXҒM0+)wz}(aF3K@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@E#r'K@Q@Q@Q@ (#Rphʘ=iҚ/_\PP9 ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (2h--"}ť((((٩i֝PJ=(hsEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE>'ZZ(((()4(&RN=U"RZx (((((((((((((((((((((((((((((((((,@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@#rP'ZZDK@Q@Q@Q@Q@Q@ERph4@K=@ME QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQPEPEPEPEPEPEPEPEPEPEPEP'ZZDK@Q@Q@Q@Q@Q@Q@ َ'BՆA)cSqTRJZ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@qii--Q@ ( ( ( ( ( (xQ+zTEuTԁ֞##Dx{SRQEQEQEQEQEQEQEwh((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((( ZD jӪ,}(Z( ( ( ( ( ( kzSEJA :8u= -@g=HQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@pR'SpiPEPEPEPEPEPEPEP{Ҥ+!j`@GZ\E" QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE"}ťOQEQEQEQEQEQEQEQEQE`ԴjOJ9vc43Q4=Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@(nh(((((((((((((((((((((((((((#V1((((((((((n 1{T0(jQBHzuh(((((74QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE%>"5EPEPEPEPEPEPEPEPEPEPMQ@CԴ{^(sEҖ/M$1Oص1+ۭ*jLbFGLbA;P7>㊛z##(ހ%5 GZx((((((((((((((((xO'.E-2=(h((((EÊ(((r٢ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (\ Z(((((((((((((((((((((((()KG (ps/ b( ( ( ( ( ( ( ( ( ((,Y((:)- QEQEQEQE"/-DRZ^z4NȦ E=x(HRQ:- E*=hPA`zq GJEF )@OCRP`Ӈ#"Ephh((((((((((((((((a":CLȠ .EPEPEPBEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQY((((((((((((((((((((((((((((EiSLO>jy⚀RZZkzPҖbSO;cT֞P3ցJb}׎)23QEQEQEQEQEQEQEQEQEQEQEQE(((((MFť"pjE R0'%>((((((((((hqEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQ@QEQE;?4PEPEPEPEPEPEPEPEPEPE&Fqޖ (Җ ( ( ( ( ( (L_j}19B4;QA. :LZbS)K@,E74}3;Jn =Ah?>=QEQEQEQEQEQEQEQEQEQEQEPEQEQEQEQEQEQE}7EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE/Q@Q@Q@ PEPEPEPEPEPEPEPEPEPEPpw:((nh((((((< )d0麤1Rpiԙdq@ nzS)POQE7~{SS?Z4PEPEPEPEPEPEPEPEPEPEPEPEPEPEPE(((( L c)A-#r.STX4)}#ր%(((((((^(((&Fp)h((((((Ȣ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (,S8%2((@iԊE-QEŚn :gpiw s-Q@Q@ÊLS Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@EPEPEPEPEPphP@((((((((((((((((Epih ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (#RTquJeI@4`/i}QI@ xȡ@ȦT94OJhhEPpiXJxE:L23QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQHHhȥ4(((((((((((((((((((((((((((((((((QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEG꒘_}#O L F@(4(LmҐM+X8AGR`9ZŕB.3>Qn¥R*:Rn%Q@sP7z x@ J9må0rHF=(֢yzszQwfKMPIKMȥS( j:c=Ƞ($Ը]٣f;у@A*;袀 jZkzPgҚdt1LMSs@ [TEG朼u@dQ@ Q*0*7#/';co8cl9 u1)RPQEQEQEQEQE֖(((((LZn :74PEPEPEPEPEPB \F&ۊv 4@OR*6F=9x@BEQE 7EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE1:b}z@ w"(((((xO=Ӛ\H\E@=zEPERdREPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPMQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@R$R+SK@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@zZ((((4PEPEPEPEPEPEPEPEPEPE((((((((((((((((((QEQEQEQE1>O'(IK@Q@Q@Q@Q@5=)P{Ҥ:Ӳ(hȥEPMQ@ QEP9PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPUi q2UL P` ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (Gf(((,EQE"x ( 4PPEPEPGlG (74PEPEPQY(/QEQEQEv((v((nh((((sE "^(^(((BEQEQE(n>}ާ(J=)> ( ( ( ( (n 'l)&wOEiEcuQEQE"4Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@vQEQEQEQG|Q@Q@Q@Q@Q@Q@Q@P9(((((((((((((((٠)23QEQEQEQEQEQEQEQEQEQEQE74Q@ OS}2PӨ@Q@Q@Q@Q@Q@Q@1x=)MَҔBH(h(((((((((((((((((((((J);-QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEEÊ((((,EQEQEQEv((sE (^(nh(((٢QEQEQI@ EPEPEPEPEPEPx4#rŚ) -#rԿŚ(( no cropping should happen # automatically self.assertEqual(self.pillowed, []) def test_transaction_with_paper_sizes_2(self): self.core.call_all( "config_put", "scanner_calibration", None ) self.model.docs = [ { "id": 'some_doc_with_text', "url": 'file:///some_work_dir/some_doc_id', "mtime": 12345, "labels": [], "page_imgs": [ ("file:///paper.0.jpeg", None), ("file:///paper.1.jpeg", None), ], "page_hashes": [ ("file:///paper.0.jpeg", 0), ("file:///paper.1.jpeg", 1), ], "page_boxes": [ [], [], ], }, ] promises = [] self.core.call_all("sync", promises) for promise in promises: promise.schedule() self.core.call_success( "mainloop_schedule", self.core.call_all, "mainloop_quit_graceful" ) self.core.call_one("mainloop") self.assertEqual(self.pillowed, []) self.model.docs = [ { "id": 'some_doc_with_text', "url": 'file:///some_work_dir/some_doc_id', "mtime": 12345, "labels": [], "page_imgs": [ ("file:///paper.0.jpeg", None), ("file:///paper.1.jpeg", None), ("file:///paper.2.jpeg", self.test_img), ], "page_hashes": [ ("file:///paper.0.jpeg", 0), ("file:///paper.1.jpeg", 1), ("file:///paper.2.jpeg", 2), ], "page_boxes": [ [], [], ], }, ] self.assertEqual(self.pillowed, []) transactions = [] self.core.call_all("doc_transaction_start", transactions) self.assertNotEqual(transactions, []) for t in transactions: t.upd_doc('some_doc_with_text') for t in transactions: t.commit() # No calibration defined in the config --> no cropping self.assertEqual(self.pillowed, []) paperwork-2.1.1/paperwork-backend/tests/guesswork/cropping/tests_libpillowfight.py000066400000000000000000000144441417573700700307750ustar00rootroot00000000000000import os import shutil import tempfile import unittest import PIL import PIL.Image import openpaperwork_core class TestBorder(unittest.TestCase): def setUp(self): self.tmp_paperwork_dir = tempfile.mkdtemp( prefix="paperwork_backend_tests" ) self.test_img = PIL.Image.open( "{}/test_img.jpeg".format( os.path.dirname(os.path.abspath(__file__)) ) ) self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.config.fake") self.core.load("paperwork_backend.model.fake") self.core.load("paperwork_backend.doctracker") self.core.load("paperwork_backend.pagetracker") self.core.load("paperwork_backend.guesswork.cropping.libpillowfight") self.core.get_by_name( "paperwork_backend.pagetracker" ).paperwork_dir = openpaperwork_core.fs.CommonFsPluginBase.fs_safe( self.tmp_paperwork_dir ) self.core.get_by_name( "paperwork_backend.doctracker" ).paperwork_dir = openpaperwork_core.fs.CommonFsPluginBase.fs_safe( self.tmp_paperwork_dir ) self.pillowed = [] class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 10000000000 def pillow_to_url(s, img, url): self.pillowed.append((url, img.size)) return url self.core._load_module("fake_module", FakeModule()) self.core.init() self.model = self.core.get_by_name("paperwork_backend.model.fake") def tearDown(self): self.core.call_all("tests_cleanup") shutil.rmtree(self.tmp_paperwork_dir) def test_transaction(self): self.model.docs = [ { "id": 'some_doc_with_text', "url": 'file:///some_work_dir/some_doc_id', "mtime": 12345, "labels": [], "page_imgs": [ ("file:///paper.0.jpeg", None), ("file:///paper.1.jpeg", None), ], "page_hashes": [ ("file:///paper.0.jpeg", 0), ("file:///paper.1.jpeg", 1), ], }, ] promises = [] self.core.call_all("sync", promises) for promise in promises: promise.schedule() self.core.call_success( "mainloop_schedule", self.core.call_all, "mainloop_quit_graceful" ) self.core.call_one("mainloop") self.assertEqual(self.pillowed, []) self.model.docs = [ { "id": 'some_doc_with_text', "url": 'file:///some_work_dir/some_doc_id', "mtime": 12345, "labels": [], "page_imgs": [ ("file:///paper.0.jpeg", None), ("file:///paper.1.jpeg", None), ("file:///paper.2.jpeg", self.test_img), ], "page_hashes": [ ("file:///paper.0.jpeg", 0), ("file:///paper.1.jpeg", 1), ("file:///paper.2.jpeg", 2), ], }, ] transactions = [] self.core.call_all("doc_transaction_start", transactions) transactions.sort(key=lambda transaction: -transaction.priority) self.assertNotEqual(transactions, []) for t in transactions: t.upd_doc('some_doc_with_text') for t in transactions: t.commit() self.assertEqual(len(self.pillowed), 1) self.assertEqual(self.pillowed[0][0], "file:///paper.2.jpeg") # algorithm may make the results vary if it changes later, but we can # still check that it actually cropped something self.assertNotEqual(self.test_img.size, self.pillowed[0][1]) def test_transaction_with_paper_sizes(self): self.model.docs = [ { "id": 'some_doc_with_text', "url": 'file:///some_work_dir/some_doc_id', "mtime": 12345, "labels": [], "page_imgs": [ ("file:///paper.0.jpeg", None), ("file:///paper.1.jpeg", None), ], "page_hashes": [ ("file:///paper.0.jpeg", 0), ("file:///paper.1.jpeg", 1), ], "page_paper_sizes": [ ("file:///paper.0.jpeg", (256, 256)), ("file:///paper.1.jpeg", (256, 256)), ] }, ] promises = [] self.core.call_all("sync", promises) for promise in promises: promise.schedule() self.core.call_success( "mainloop_schedule", self.core.call_all, "mainloop_quit_graceful" ) self.core.call_one("mainloop") self.assertEqual(self.pillowed, []) self.model.docs = [ { "id": 'some_doc_with_text', "url": 'file:///some_work_dir/some_doc_id', "mtime": 12345, "labels": [], "page_imgs": [ ("file:///paper.0.jpeg", None), ("file:///paper.1.jpeg", None), ("file:///paper.2.jpeg", self.test_img), ], "page_hashes": [ ("file:///paper.0.jpeg", 0), ("file:///paper.1.jpeg", 1), ("file:///paper.2.jpeg", 2), ], "page_paper_sizes": [ ("file:///paper.0.jpeg", (256, 256)), ("file:///paper.1.jpeg", (256, 256)), ("file:///paper.2.jpeg", (256, 256)), ] }, ] self.assertEqual(self.pillowed, []) transactions = [] self.core.call_all("doc_transaction_start", transactions) self.assertNotEqual(transactions, []) for t in transactions: t.upd_doc('some_doc_with_text') for t in transactions: t.commit() # those pages have paper size defined --> no cropping should happen # automatically self.assertEqual(self.pillowed, []) paperwork-2.1.1/paperwork-backend/tests/guesswork/label/000077500000000000000000000000001417573700700234115ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/guesswork/label/__init__.py000066400000000000000000000000001417573700700255100ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/guesswork/label/tests_sklearn.py000066400000000000000000000477411417573700700266610ustar00rootroot00000000000000import shutil import tempfile import unittest import openpaperwork_core import openpaperwork_core.fs class TestLabelGuesser(unittest.TestCase): def setUp(self): self.tmp_bayes_dir = tempfile.mkdtemp( prefix="paperwork_backend_labels" ) self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.config.fake") self.core.load("paperwork_backend.model.fake") self.core.load("paperwork_backend.doctracker") self.core.load("paperwork_backend.guesswork.label.sklearn") self.core.get_by_name( "paperwork_backend.doctracker" ).paperwork_dir = openpaperwork_core.fs.CommonFsPluginBase.fs_safe( self.tmp_bayes_dir ) self.core.get_by_name( "paperwork_backend.guesswork.label.sklearn" ).bayes_dir = openpaperwork_core.fs.CommonFsPluginBase.fs_safe( self.tmp_bayes_dir ) self.fake_storage = self.core.get_by_name( "paperwork_backend.model.fake" ) class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = -9999999 def fs_exists(s, url): for doc in self.fake_storage.docs: if doc['url'] == url: return True return None self.core._load_module("fake_module", FakeModule()) self.core.init() self.core.call_all("reload_label_guessers") self.core.call_all("config_put", "label_guessing_min_features", 1) def tearDown(self): self.core.call_all("tests_cleanup") shutil.rmtree(self.tmp_bayes_dir) def test_training(self): # ## First training self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.fake_storage.docs = [ { 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': 'sklearn and Flesch are\nthe best', 'labels': {("some_label", "#123412341234")}, }, { 'id': 'test_doc_2', 'url': 'file:///somewhere/test_doc_2', 'mtime': 123, 'text': 'Flesch sklearn\nbest', 'labels': {("some_label", "#123412341234")}, }, { 'id': 'test_doc_3', 'url': 'file:///somewhere/test_doc_3', 'mtime': 123, 'text': 'something else', 'labels': set(), }, { 'id': 'some_other_old_doc', 'url': 'file:///somewhere/new_doc_2', 'mtime': 123, 'text': 'something something niet', 'labels': set(), }, ] # make a transaction to indicate that those documents are now in # the storage --> it will update the training of bayesian filters. transactions = [] self.core.call_all("doc_transaction_start", transactions) transactions.sort(key=lambda transaction: -transaction.priority) self.assertGreater(len(transactions), 0) self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") # XXX(Jflesch): use upd_doc() so it doesn't try to guess labels for transaction in transactions: transaction.upd_doc("test_doc") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.upd_doc("test_doc_2") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.upd_doc("test_doc_3") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.upd_doc("some_other_old_doc") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.commit() self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertEqual(len(self.fake_storage.docs[2]['labels']), 0) self.assertEqual(len(self.fake_storage.docs[3]['labels']), 0) # ## New docs self.fake_storage.docs = [ { # old doc 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': 'sklearn and Flesch are\nthe best', 'labels': {("some_label", "#123412341234")}, }, { # old doc 'id': 'test_doc_2', 'url': 'file:///somewhere/test_doc_2', 'mtime': 123, 'text': 'Flesch sklearn\nbest', 'labels': {("some_label", "#123412341234")}, }, { # old doc 'id': 'test_doc_3', 'url': 'file:///somewhere/test_doc_3', 'mtime': 123, 'text': 'something else', 'labels': set(), }, { # new doc on which we will guess the labels 'id': 'new_doc', 'url': 'file:///somewhere/new_doc', 'mtime': 123, 'text': 'sklearn and Flesch are\ncamiön', 'labels': set(), }, { # new doc on which we will guess the labels 'id': 'new_doc_2', 'url': 'file:///somewhere/new_doc_2', 'mtime': 123, 'text': 'else something', 'labels': set(), }, { 'id': 'some_other_old_doc', 'url': 'file:///somewhere/new_doc_2', 'mtime': 123, 'text': 'something something niet', 'labels': set(), }, ] # make a transaction to make the plugin label_guesser add labels # on them. transactions = [] self.core.call_all("doc_transaction_start", transactions) self.assertGreater(len(transactions), 0) self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.add_doc("new_doc") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.add_doc("new_doc_2") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.commit() self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertEqual(len(self.fake_storage.docs[4]['labels']), 0) self.assertEqual(len(self.fake_storage.docs[3]['labels']), 1) self.assertEqual( list(self.fake_storage.docs[3]['labels'])[0], ("some_label", "#123412341234") ) # ## Upd docs self.fake_storage.docs = [ { # old doc 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': 'sklearn and Flesch are\nthe best', 'labels': {("some_label", "#123412341234")}, }, { # old doc 'id': 'test_doc_2', 'url': 'file:///somewhere/test_doc_2', 'mtime': 123, 'text': 'Flesch sklearn\nbest', 'labels': {("some_label", "#123412341234")}, }, { # old doc 'id': 'test_doc_3', 'url': 'file:///somewhere/test_doc_3', 'mtime': 123, 'text': 'something else', 'labels': set(), }, { # new doc on which we will guess the labels 'id': 'new_doc', 'url': 'file:///somewhere/new_doc', 'mtime': 123, 'text': 'sklearn and Flesch are\ncamiön', 'labels': {("some_label", "#123412341234")}, }, { # new doc on which we will guess the labels 'id': 'new_doc_2', 'url': 'file:///somewhere/new_doc_2', 'mtime': 123, 'text': 'camion camion camion', # accents shouldn't matter 'labels': set(), }, { 'id': 'some_other_old_doc', 'url': 'file:///somewhere/new_doc_2', 'mtime': 123, 'text': 'something something niet', 'labels': set(), }, ] transactions = [] self.core.call_all("doc_transaction_start", transactions) self.assertGreater(len(transactions), 0) self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.upd_doc("new_doc_2") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.commit() self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertEqual(len(self.fake_storage.docs[4]['labels']), 0) self.assertEqual(len(self.fake_storage.docs[3]['labels']), 1) self.assertEqual( list(self.fake_storage.docs[3]['labels'])[0], ("some_label", "#123412341234") ) # ### del docs self.fake_storage.docs = [ { # old doc 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': 'sklearn and Flesch are\nthe best', 'labels': {("some_label", "#123412341234")}, }, { # old doc 'id': 'test_doc_2', 'url': 'file:///somewhere/test_doc_2', 'mtime': 123, 'text': 'Flesch sklearn\nbest', 'labels': {("some_label", "#123412341234")}, }, { # old doc 'id': 'test_doc_3', 'url': 'file:///somewhere/test_doc_3', 'mtime': 123, 'text': 'something else', 'labels': set(), }, { 'id': 'some_other_old_doc', 'url': 'file:///somewhere/new_doc_2', 'mtime': 123, 'text': 'something something niet', 'labels': set(), }, ] transactions = [] self.core.call_all("doc_transaction_start", transactions) self.assertGreater(len(transactions), 0) self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.del_doc("new_doc") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.del_doc("new_doc_2") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.commit() self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") def test_sync(self): self.fake_storage.docs = [ { 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': 'sklearn and Flesch are\nthe best', 'labels': {("some_label", "#123412341234")}, }, { 'id': 'test_doc_2', 'url': 'file:///somewhere/test_doc_2', 'mtime': 123, 'text': 'Flesch sklearn\nbest', 'labels': {("some_label", "#123412341234")}, }, { 'id': 'test_doc_3', 'url': 'file:///somewhere/test_doc_3', 'mtime': 123, 'text': 'something else', 'labels': set(), }, ] promises = [] self.core.call_all('sync', promises) promise = promises[0] for p in promises[1:]: promise = promise.then(p) promise.schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.fake_storage.docs = [ { # old doc 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': 'sklearn and Flesch are\nthe best', 'labels': {("some_label", "#123412341234")}, }, { # old doc 'id': 'test_doc_2', 'url': 'file:///somewhere/test_doc_2', 'mtime': 123, 'text': 'Flesch sklearn\nbest', 'labels': {("some_label", "#123412341234")}, }, { # old doc 'id': 'test_doc_3', 'url': 'file:///somewhere/test_doc_3', 'mtime': 123, 'text': 'something else', 'labels': set(), }, { # new doc on which we will guess the labels 'id': 'new_doc', 'url': 'file:///somewhere/new_doc', 'mtime': 123, 'text': 'sklearn and Flesch are\ncamiön', 'labels': set(), }, { # new doc on which we will guess the labels 'id': 'new_doc_2', 'url': 'file:///somewhere/new_doc_2', 'mtime': 123, 'text': 'something something something', 'labels': set(), } ] # make a transaction to make the plugin label_guesser add labels # on them. transactions = [] self.core.call_all("doc_transaction_start", transactions) self.assertGreater(len(transactions), 0) self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.add_doc("new_doc") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.add_doc("new_doc_2") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.commit() self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertEqual(len(self.fake_storage.docs[4]['labels']), 0) self.assertEqual(len(self.fake_storage.docs[3]['labels']), 1) self.assertEqual( list(self.fake_storage.docs[3]['labels'])[0], ("some_label", "#123412341234") ) def test_training_no_text_at_all(self): # ## First training self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.fake_storage.docs = [ { 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': '', 'labels': {("some_label", "#123412341234")}, }, { 'id': 'test_doc_2', 'url': 'file:///somewhere/test_doc_2', 'mtime': 123, 'text': '', 'labels': {("some_label", "#123412341234")}, }, { 'id': 'test_doc_3', 'url': 'file:///somewhere/test_doc_3', 'mtime': 123, 'text': '', 'labels': set(), }, { 'id': 'some_other_old_doc', 'url': 'file:///somewhere/new_doc_2', 'mtime': 123, 'text': '', 'labels': set(), }, ] # make a transaction to indicate that those documents are now in # the storage --> it will update the training of bayesian filters. transactions = [] self.core.call_all("doc_transaction_start", transactions) transactions.sort(key=lambda transaction: -transaction.priority) self.assertGreater(len(transactions), 0) self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") # XXX(Jflesch): use upd_doc() so it doesn't try to guess labels for transaction in transactions: transaction.upd_doc("test_doc") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.upd_doc("test_doc_2") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.upd_doc("test_doc_3") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.upd_doc("some_other_old_doc") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.commit() self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertEqual(len(self.fake_storage.docs[2]['labels']), 0) self.assertEqual(len(self.fake_storage.docs[3]['labels']), 0) # ## New docs self.fake_storage.docs = [ { # old doc 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': '', 'labels': {("some_label", "#123412341234")}, }, { # old doc 'id': 'test_doc_2', 'url': 'file:///somewhere/test_doc_2', 'mtime': 123, 'text': '', 'labels': {("some_label", "#123412341234")}, }, { # old doc 'id': 'test_doc_3', 'url': 'file:///somewhere/test_doc_3', 'mtime': 123, 'text': '', 'labels': set(), }, { # new doc on which we will guess the labels 'id': 'new_doc', 'url': 'file:///somewhere/new_doc', 'mtime': 123, 'text': '', 'labels': set(), }, { # new doc on which we will guess the labels 'id': 'new_doc_2', 'url': 'file:///somewhere/new_doc_2', 'mtime': 123, 'text': '', 'labels': set(), }, { 'id': 'some_other_old_doc', 'url': 'file:///somewhere/new_doc_2', 'mtime': 123, 'text': '', 'labels': set(), }, ] # make a transaction to make the plugin label_guesser add labels # on them. transactions = [] self.core.call_all("doc_transaction_start", transactions) self.assertGreater(len(transactions), 0) self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.add_doc("new_doc") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.add_doc("new_doc_2") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") for transaction in transactions: transaction.commit() self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") # guessed labels don't actually matter, it must just not crash paperwork-2.1.1/paperwork-backend/tests/guesswork/ocr/000077500000000000000000000000001417573700700231155ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/guesswork/ocr/__init__.py000066400000000000000000000000001417573700700252140ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/guesswork/ocr/test_img.png000066400000000000000000000107061417573700700254420ustar00rootroot00000000000000PNG  IHDRdÆ bKGD pHYs.#.#x?vtIME vtEXtCommentCreated with GIMPW.IDATx{LSgǿU,E`Vo+dTQScj:ܘHy&Bp@ P"rSN[V~۾@O=PI9ym H0 0,a0 aX að@0 a^D"t0H$TWWDF㴊wE`ʕH$Xa\P Y|TUU ^ƭ/3ӳG|tE! PTy D"8>غu+juXɓ@^^@&aܸq8GOFqq1+`ڴixwo؜WAAƏo/'0uVNJT*RL0AAA?/8bb鱐@UUU^J(%%<<#Vz=EDDc6ץ+9/׉' ڵkkNq@PLL jT[[K |Ҧ7''… h4Vھ}`^8x F!NGZ-eeeT*%tUn؊+ݾ}52ܹsT[[K? $JI:Tw[bat (jnn8M(''Ǧ7!!ٳg;]QF{>}О={:U Z<<<ϯPݟ8}nZiL&3φ!C7otZ]Q ///0;BTVVbɒ%4h5>>>3gtKyC|hq?3`{^ja޽{G [liwh"uhޙe Zvռ=*j^VVh}u8HsSj*#??EEEχLiӦO?˔ѧO|w4i1cr<*SPP#G ;;˖-ԩS'O… 6l J!k9Tw[bHؼaA:ð@0,a0 aX a0,:T[q{WP@" ##AX 8 =l'X ÇQUUNddffӧOǂ 0zhJII"a…x7-sÇHKKCrr2 \`(J̙31c`z{lm<}||0qDbĈul 55YYYCRR~mQqۦ] 8~8qx{{Cpp(D=i`RR]hRH& ?uTjZ?`޽{ ֖ե]N{mz)qۦ&"ŋ_.e=ڑ@P\\ݹsbcc ͝;믿7o^ڿ?ɓ'^?[%Kʕ+d0.\@X\mGHܽ{Wo>H[پKNúuP^^^K.O?m+˶ݻw>deet(//ݻq&CG|===i&ڵkq:<>y(]潻i&@eee.vV5@`e~"T*-Ҋe%=..NJ6#m,`"… ]~%ݥy4h~ )) d9s&@[W-uoT*&O# @p =H:4w^̙3s2 3g΄\.Rtg/r1 ׮]ȑ#!JqVqA#ܽ{PTtӧtR,T;<],"BII N8B៽0;Hg'z 0,a0 aX $,, e6Xi,^U/jaƂrZ| 0 JBRDuu5baSWW#Gbxys[rssm۶+qIdddd2ƍ???^FBtT*`޼yp8 %&&Z-vAt۷o1ؖիW>e@iii6+R^vwϵuwtħ7%%<<<8pnm۷h4R]]ϵ}JKK ^Z0_ܹs[v@ӔQ}}=FjTRRB۷oN鍉!ZMzjkkIPϵ-&̝;hCxSSyFHw@ǎ+`@-$ٳ]EVͺsrr)voވxPPtիyF۽azEW#WvȐ!7o\#>d29ba`֬Y,&M777ڵ/_Fnn.6olԖi^k.~VT*Ehh(֭[R7n_K:tqͪ5*b䟤-nbtt4rssqeT\.ﶊvaGѣG#$$6l{_/_ޭ[`;ÓٳDh4VrO/i;0az{dd$ `Y@^旡b)StkŝkzE3|zHbb"lق 455A qa_^[c; 8r_455* ;wVouO/ӅtJo|?`5g/hfѢET[[t޼<@ X ޼(//GRR233q=!$$'N03vZ@*ė_UV(**B~~>0eL6 e.Č3?[y `4Sr3=ϳ644رc8tKbUVVZT]vͮwl0c5j"##qeÿ ÿ a0,a0 aX ah dARIENDB`paperwork-2.1.1/paperwork-backend/tests/guesswork/ocr/tests_pyocr.py000066400000000000000000000161341417573700700260520ustar00rootroot00000000000000import os import shutil import tempfile import unittest import PIL import PIL.Image import openpaperwork_core import openpaperwork_core.fs class TestPyocr(unittest.TestCase): def setUp(self): self.tmp_paperwork_dir = tempfile.mkdtemp( prefix="paperwork_backend_tests" ) self.test_img = PIL.Image.open( os.path.join( os.path.dirname(os.path.abspath(__file__)), "test_img.png" ) ) self.core = openpaperwork_core.Core(auto_load_dependencies=True) class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 999999999999999999999 def data_dir_handler_get_individual_data_dir(s): return openpaperwork_core.fs.CommonFsPluginBase.fs_safe( self.tmp_paperwork_dir ) self.core._load_module("fake_module", FakeModule) self.core.load("openpaperwork_core.config.fake") self.core.load("paperwork_backend.model.fake") self.core.load("paperwork_backend.doctracker") self.core.load("paperwork_backend.pagetracker") self.core.load("paperwork_backend.guesswork.ocr.pyocr") self.core.init() self.model = self.core.get_by_name("paperwork_backend.model.fake") self.core.call_all("config_put", "ocr_langs", ["eng"]) def tearDown(self): self.core.call_all("tests_cleanup") shutil.rmtree(self.tmp_paperwork_dir) def test_ocr(self): self.model.docs = [ { "id": 'some_id', "url": 'file:///some_work_dir/some_doc_id', "mtime": 12345, "labels": [], "page_boxes": [], "page_imgs": [ ("file:///some_image.png", self.test_img) ], "page_hashes": [ ("file:///some_image.png", 0), ] }, ] self.core.call_all( "ocr_page_by_url", "file:///some_work_dir/some_doc_id", page_idx=0 ) self.assertNotEqual(len(self.model.docs[0]['page_boxes']), 0) self.assertEqual( self.model.docs[0]['text'], "This is a test\n" "image created\n" "by Flesch\n" ) def test_transaction(self): self.model.docs = [ { "id": 'some_doc_with_text', "url": 'file:///some_work_dir/some_doc_id', "mtime": 12345, "labels": [], "page_boxes": [ # Should be list of LineBoxes, but meh. "putsomething", "here" ], "page_imgs": [ ("file:///paper.0.png", None), ("file:///paper.1.png", None), ], "page_hashes": [ ("file:///paper.0.png", 0), ("file:///paper.1.png", 1), ], }, { "id": 'some_doc', "url": 'file:///some_work_dir/some_2', "mtime": 12345, "labels": [], "page_boxes": [], "page_imgs": [ ("file:///some_image.png", self.test_img) ], "page_hashes": [ ("file:///some_image.png", 3), ], }, ] transactions = [] self.core.call_all("doc_transaction_start", transactions) transactions.sort(key=lambda transaction: -transaction.priority) self.assertNotEqual(transactions, []) for t in transactions: t.add_doc('some_doc_with_text') for t in transactions: t.add_doc('some_doc') for t in transactions: t.commit() # first doc already had boxes --> no boxes or text added self.assertNotIn('text', self.model.docs[0]) self.assertEqual(self.model.docs[0]['page_boxes'], [ "putsomething", "here" # unchanged ]) # but OCR should be run on the other doc self.assertNotEqual(len(self.model.docs[1]['page_boxes']), 0) self.assertEqual( self.model.docs[1]['text'], "This is a test\n" "image created\n" "by Flesch\n" ) def test_tricky(self): self.model.docs = [ { "id": 'some_doc_with_text', "url": 'file:///some_work_dir/some_doc_id', "mtime": 12345, "labels": [], "page_boxes": [ # Should be list of LineBoxes, but meh. "putsomething", "here" ], "page_imgs": [ ("file:///paper.0.png", self.test_img), ("file:///paper.1.png", self.test_img), ], "page_hashes": [ ("file:///paper.0.png", 0), ("file:///paper.1.png", 1), ], }, ] transactions = [] self.core.call_all("doc_transaction_start", transactions) self.assertNotEqual(transactions, []) for t in transactions: t.add_doc('some_doc_with_text') for t in transactions: t.commit() # first doc already had boxes --> no boxes or text added self.assertNotIn('text', self.model.docs[0]) self.assertEqual(self.model.docs[0]['page_boxes'], [ "putsomething", "here" # unchanged ]) self.model.docs = [ { "id": 'some_doc_with_text', "url": 'file:///some_work_dir/some_doc_id', "mtime": 12346, "labels": [], "page_boxes": [], "page_imgs": [ ("file:///paper.0.png", self.test_img), # new page ("file:///paper.1.png", self.test_img), ("file:///paper.2.png", self.test_img), # modified ], "page_hashes": [ ("file:///paper.0.png", 0xDEADBEEF), # new page ("file:///paper.1.png", 0), ("file:///paper.2.png", 0xBEEFDEAD), # modified ], }, ] transactions = [] self.core.call_all("doc_transaction_start", transactions) self.assertNotEqual(transactions, []) for t in transactions: t.upd_doc('some_doc_with_text') for t in transactions: t.commit() self.assertNotEqual(len(self.model.docs[0]['page_boxes']), 0) self.assertEqual( self.model.docs[0]['text'], "This is a test\n" # new page "image created\n" # new page "by Flesch\n" # new page "\n\n" # unchanged page --> no OCR "\n\n" "This is a test\n" # modified page "image created\n" # modified page "by Flesch\n" # modified page ) paperwork-2.1.1/paperwork-backend/tests/guesswork/orientation/000077500000000000000000000000001417573700700246655ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/guesswork/orientation/__init__.py000066400000000000000000000000001417573700700267640ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/guesswork/orientation/test_img.png000066400000000000000000000710021417573700700272060ustar00rootroot00000000000000PNG  IHDRa hiTXtXML:com.adobe.xmp tgAMA a cHRMz&u0`:pQ<bKGD) pHYs.#.#x?vtIME /fg6IDATx}y`չ#F1U@KPF( 6?"Wq)JբhnUD/D@D@d4Pʾ4VUd9d3g$7? Yޙ3y>5ΨuOWs51p5G9j \QcjWs51p5G9j \QcjWs51pGaŏWԤVyԂqNg]scCK^ .9]7qޠ!\ yg网Q#]#k5БMe.^1: ^/BzAؠ?Qk{CT!~콱$=d(y 04W8h`< 'IJCرepOIYokZ77*Cٽǵ86x8+v0v1}b7l7S~ l~2W;4wxW6 ^dhXN5{UCí#jqgeuPo`{Roظ_`6pjp3dk~k V wᰱ0#`kRm>1c,T|<3mר *P[ 6"P t@`"1A<~-`W@ -ƥʾo ,''n lG} y6pABqJo<F@>.2mUC6}C^EcaT;!prq7bCs _!!@mD(Y4F,Fډ!h;k#Oo}5t\֗y$\NvH.{x}*yox]B`Kyۧ"jzb΅H"/;nKe< CO9z'֬3ccþ\ӽ!+͟N:k@Fd0(}q5c(Je`:]绷X mciറ֪@ [;-֗40vB5}U T| 3VTzcu.ȷh  D Jňl[KE?ϬA[;PeE4M;dٶ޳Ҵ.lc lIa4턢6/ݭ2֟g Ȥ/!?v{ fNvRjo#Ҋ2,Pt><(\$:{+=!(iI/v~CCmR(J}yO3mOև;ps%\v}&y1wǠ=?ڨLBe~ZɂYPb}IxP(tФkP0u8R6G{͟wi|+N~0~q?zN+xʺ=ŗcw^+ ~}# o{{gxZRPo`s;4B$q桓͎Wf-=Z@G~rY QG/#Ccfm`@H2IaیaP}E^y4TG NB[ V8R8(;0 ݳ0|@YNVk-X᠇3g\͑|jV\#>~ؠu"w4?Cp _C{80>do0#ơ,rܸ"}2p۬#ڣ#dD$Uݗt|~SxP'N S߲ɸ؀SX0vt* 6=Ӂ^Ej%Ȋ7*\N֩9' wNJʼ8Q{k3zû=,}ٍHc߅Ё,I;遵0xomә +ި̓$YDl@G S=-OaO B|qhشYO+ +ިU0eo#_:N[^ i'kڭMp>ᜂQ禍p? P(n^K;\i'O™/;/ |NXҢ)|kq+.PH{h>gHX)Gn@~܃O~(md95O|F`\ת:$D,F!2Ѵ dˬr ~NtAiG8T!(x@v'=DS!X`?֡NG"Aܿu6|IW_KXIB1˙ʸ #9#$-X e / vB좲]霃CBH+Uwt{gZH)s}20L_98D\B@t02獦=Dd-8,ח.'VdP;Jjw-;> HӞi eoTdP;YX;$&ί,-~ֿ)O{%Ȋ6%"p݄⎍opN0㪤d1vR$4홖 +^f L&tWNNB XkC:z:^vL՜ό =2KKU/2| >fY*fMH)o^xΪ/V&̃բNnK{ BH!vp 0nu~pP˄e^hnT*b|Yvă՟FgD N?.5'S2]_;Ș tw_]Fh&-]P;OiI$aCiX&Pl_}&8C ؽLa\l([7)0 n_>@ ?=jE7IJ(HTo1(p;o*l Ȱwe2&:?{M-5BiI*>=y,}>^DOBm%X)lЀ')Z 27iFqC~f=PBiǠ6إψr(|g3(cl\#A*%]t9zBLHu ~‹%d{hȽULF!&s'_rKG;_ `{&Y4|Cpp9 ̉;:2 1S.Ldy";0ŋ{vǎ҈p!.Q 1ވAi8唬ѣcm ǹKCvoT8ۜ#cwO!E 61Тʸ:rWCǝwF!mV^qyWFQΛ=eǎF?<3q&ETsLk;DBbĺ7ز"r0,k//`d~sp#4L} cM=Zq.pݡʾBE8!~ƓYiBD !!ؠQؽa{m;fb ">ᖆt ~22Glt9|F9_ `PLGɓmƝtL&F{PgsqWDRlѲh,67&MCS_stk<\o}"$8#jPә(%r|h(ד20ȠPYG1>RO~ $ ]rBمW{ÛYH(|*l2Єgʥ5ZO5 ccWmr30`<ėFo ]Թ/(tRۋ06?yG7t^VlD04)F#G?\6x4";/K'oB,x|:F?tR$~hJ /|PH- CX>.BXlm_k:1οRhݛ A( BKfbrXۈON~H]Xr G8q|e]Ph( (P_~h(2R>t[ я $UuM󂿽L 7uA9Ηx*B?T%I{cl(4OG?Z8Yt&6KU_ڪ+>bo~^PA(*5=$l e&gUhxXFM20e4>*u/e K6ұgN"jMH4OG?Jť8U:P#\Q$ ݔ+ Pi20"p_DA,ԥpX!dǿxta+Me&e`(I1Y3NY݃/Р: 5EےCLs*ox񲈓8傢{ VbX/YEbQ$+.PTup'oMj2>zw1uq;c>?;rvZJaMbQ$}cwErѣ3MCN!|!MHj_".L΂N50'\]?e;T:i-c5zxF+``8~Anѥ8; ߊ~X\[^܉$ݴ[h_'oD1b,FQ6a5.exc &kB8BbO\ܘ~~B݋;)T/V̓ZTKql?alBr l> ;5}ݕjdH76xܧX#876;V49Ƿ !7'!ꄱU=~VS G/DI3* 3Ƞ嘷?%?-KHKCg$'{5 =bVPe=SW<$2}WV'@l[)|/Jz$&̢`jYqVSv .E2J#4ķ5CvD99 Z9\u1HC[\}Ƚ lWZst kq)/akݮ74O]X~ne6U0 %Sh׀U\Ujp qaa`2saY9[Q1WI(L6jw}c(ApEh;8 k&L{K |KV>OvSv`Naqp[ruvЁ2҇F?(3S|U ]uZ_ލDCQYk0xB5yf^Kp%kL;@[| .v&V|}3tmHJWt-'#\ڋT6pWMAaMp;4kR#陻+:~˶|lCbZe``?%ei3}/w=rϵ_׺٧[q;#\?l˽jje-PuWFe 2g^<,ڇcͩrO*3!Dx[ֹCO>0.]nl`.ڇ*.)ϐև·@]UtGz'yѥg;íLMdaOY08zee=@ai|Bùdl?̦8s);}^6ùW=iSq?ͽ 5zg}ydI2=@`K2^%H,ıoZoPsuh|{õxJZf˽JjN~̂ơ`g_׌sWH 8!ZSnVD BWICq`3*G.ɦX?[u&NLX#opY}_vWRqu}Sћ4e{pX qFMV7 l|A)W.-ZRUʷJcќ-4HueL=˺#\C-ǥ Ȩ,A)(uhV^OgJKuW3W e! 6H)90sB cmy~/oz~ARqY,u)%{ gcQZQ@4}Qחb" uiѽ540g n *P68Wl^ia,>\R΂U{lҘ>FMh䜫9VpzFYRI @i¹ʳ(Rmϰ[>'Sևyp*<B @&wB]eV7nҜ ÿ~RqQP U%JNv RTb0Qc;'G8"0PH** H=+*1{FVֆ!HZ%wsaۧLIszfA@@;S}(+kY1 F,jaߡkpzD9Wi<9"Ԋ-e9(ʆ44BIQDpo.#.}ٍhI~>7Ȇ@3Ь Q$\us-C4ƁV7 V~ZI7j-3 x2݅kgf,, aHv5TUq=7i,-Pܲ6>o4d6<&p|Q$\+A4cEO}0-~ %+Tw +'E[ݭ.uAj_ '!Y؃Bd*Ł er!D^y0/o0Xiܩ V v=&3p K.n8,2JWmdFו뉞RϪ,Q(Ł" )]QIUՇ1LɎEBWJeT3{_{I5VI]IB138/ǕBLbEՇɒl#onw30$ ҁVQ%f=.e Pc|kYޫ!t-(8Y7N}=*6Hi0A_ 3!Iu3KBcxuM^3N㟨Q6k>`NO:Pi1%%%f҅JFϒ 9?ւi2Tm.+ 6x 9?@:M)+|NV~rY QG/()\9={q|Γ.N Rr:~*(g6N>6`ړQЄcҶhƽe#Y.^7)9 UJZ'GK{"|+P ]nKd$N[<+b !DȊNcCY3# eM~o0gH)( M89uZH/1=Pa2vOI!}=Jf|x >-xɦ1(n_D'c]> ;5}/]4 -e`)Ӓ)@\oHqpt:8 /$'KhVwKj4X imK N?%!9DS!X$7C R:<, fqEPdFJ"S'׏YaL.jJ.% ,3:8_|v^uA8\AT2\#< i%h;@GoD>0 G2ۅϕnXؼOt|'bzDSvl;=R;)~t(޹B)77LvᓁhLSs0g/F L: t'{ƅ l.xDPuOjew{9`>3e;é/-_T\C*g]c-W%轳f 1xn)[yo3F9߲b`k{g!t9P8='Ymc<{G[sIRe0[˗k )a[ۚ)9͍>YofG6; ơ4l$[ԫD4k7x$D ug;[PK:Jd$[׮o6ym*c`Ki;Pд![(IFoBC2VSM2¥#Zo@YDdw.7/8CE Ly9 4HٽП~ ~#YON*hp۠, 8Ws }yWJi$FC/ ^fy|4 Hf?=e+ycKU1a^Ima@iDl ?-~duwߩ7Q{6״͞tx7$ i6/b+tXieSo@`w0MD1ow;2 ibGD//]i-@ŲWRIgduhFZ,@-(߰3Bb:MSLNg LiJWd1 G u J!t)SEA{0]AEu1X += Œ7&a3gxԠ[<=^RX۷Ԇ_ҭa*Cm0?A x'ZEpB)Fp єgfH$!`Zo6l#Ȳ13=e`*NSlh$FCBIUhΝ̪,i);p [SH$!qƚLg8?÷Gw}7F"i448k2dWT;(>J70 -YY6Fɺ<.{<4+GfIC 6s("YktF" $KTsO*&{xt6O aJEbьgZyA!PU N@WP"h܎ i^-`_e Z#PѥAYi^-`”)20 92 *DL5y&L1Z#`ɒ_3o~ђtьL{AY'>c:Y&-`S.SA =.xգѳޭ䫠DL5ic@RQv'KW.~au&C/Jr7= m,+bt6O uI#\E^K/1hmlj]T8xps%Vӌ89(_4%7T|s Dֵ.`TEe?Av FMJwC`V.HS,}ٍhIF"E%q90EDL[7*=2EIY›}h3^ZϷ3YP ieÄ7hEr=d}l6`TN*CZr?:A/$a%Xz4a1 /J6.7),K)T4/5al@ vd+=٢zFiRܴ؀`*mGagcH|?iJ.DSV3gδn+~LQv偲 "Ք9bB}>۰P#; B$A`O4 !p\zzb hRQaHZ8dWQ>ôc3Vڟپv5uJf&>!G./81!cypO[Úd狙/$$-Q!\g{`go0 R * I (.YEbQ,Jx>F>'L1P$ꕴ5 ʆ'\km)*pRL+Hhm&ND2m)t]\K)j 3op(a:o2 _ڌ!!~BQ٠ |B ([h!wqv@''MGM?g.~"hC/Nʚlb!& ^cβ?l#K>BX|a* #F?#g_ 8Y~XU2:SJ!FAچ18>aDOZBy|Ʒ" V +zv{ݴ)W"2x8}OY'0h1¾[^wwkhW+cME3yf.egBL9s)[[ Vpa?;,#x2 t^m0m"vcYvX4XW.hRHB(Mǀlm8sE,^rה5Y:x_E?vS!pnUO:* K{ YXn]IN2ekӁ IRp4a x*[!A IٿdyZ|ͲTZB cU5P7p}ugh˿k!0VRJ?[cFBl024NY҄~*rk}BダwN$:fsc/F0G;(.Mm T-4=^l1|;4v7(N{!DC8X~ ' IfX{AcZr[ V&q񷗃kS^θg:8&{ws' fpqyt `xK˳e@EeE+9(7x0u:6>+>1j*rSZ|u1K&za($zʻ4'~JR<+oF{gFUX;0T!@Lt] Nǐğ tܿ%ӞԺYݟYIu?hZAH"ov`^pЃʐ~JR(xfmWwPۓ8bqgT?=%XƖ{5ugt%=c` j?5q{&v 'TL¿&] M^3peyfH-8"VY YNϠ$&ww޺I/Im5]d/ o=e-W$:op6~-T`X gT4gPq5P[aAnP;i1ˣXv\&"-J~]ߪZn M:7q]|O eΫ0;ok=Z UpׯRJ T{`C*vP5 ,@Ct˺uOeH((k9g;ѡ ،R8{*CxKAY9ۉUR8b9Li$S(@10@!I3-%T{ָ%@Y)ۉE` ҆*Cgٰj-xZq5lZg| )TR$<--XM&O5|s*DJOe)ݙJ2 9\B뗆{#SoLB#TVExflZH~i޶=*CץGRTNlK)3l?T\2ܨ}ڨ:_{@WW10F\T\ԽM+--kc [oZ Uu:/}[JyV})-s?LېSG7Tp[l[2'  Oư |iUh}"Ot*NGs{]/r/Hྪ~fFb"gG?)ܟ{_1-;9X)o!Qí#c(:.˨$C2w G;ifS`"Q *C8 (;uhb~ݦy;|iUR2G*v]9cՒɢU=ER ֗2G ◿?e+C}P3 %7Zby63 lXeX@j7X R=7~}8 ZȩGxgVlhJX" !-: :C"۷z{/mp$USk: āG:LztQf,EI1.xhUXtsAy;*IH,%#\]l"f~6U03o yg6T.w06Pذf4‰jy3ܲSA##]h`\](b3巫kTMJ"@g邱 .ZĬ@"pL҂eQPK݃1+rX, 1W뼈Y;SQF.AHUԛC!9c`_fzٯ4#y 򮔐̐uЄ `L#!&jW= pcYMPuJdǴy,&jJ^20@1 ?y4Վ5'sHT;N۪iF-";˜ 1ӎ-v84GݎJ)Pu9Վeߦ76g;kyF'c95i꼦vLfe%268oyr+Vda*JZ)!INݼ@1FBkA9+F Dr< Ub1 S@W~,1 †vtF)c7yčcF韼tL)u~D;^%XWNAUH ӀYKjhQԠfލ5~0ǡC}<|1ZboM4)҂ځ7IL1h YR &6=V&Anio^'[e~/=]X?kЉMq7n8ʺ;:]\1& bF= (W(AF^{!H[ar tۏ!oxɸY,?QS\ 3XN{\z},f_Q|A98!؆((ANaS^Vnglz 9;c](d`AZZG1}ȢGۋ(\tlxtz`W1|lv/+% nߍ . ,U!_ʗc`:=CkL{;\^DD5pw v?r|e G!^4cJ E ]([ heY%zxgIKl~tzpyp |LVF?TdU[^U /OVm(+f؀erVpicb Ie2&:?{M$AK>=pa;j\dw9{}{x'7}|2Q T a<3D'O0_itz@ ,`SF7쵕3@1ܷ乕J(/,;'&/;*=<K~J@hVhK/J乕J8rR_f*k▊א/Ϛ6ОtIpQ\^Y; TиH@aB|Of]#d*^`/_]Q.! t&MC%zx&'k_DnDKBw]awI04D2]F/c`<{Tڅ9C?kWی*3*g1]:څ4.ŝϤ+L-J "qz0^XKC@NߙHt|ZOA`!%-& r|{"YkAS63Oޮ/~VJg BaX-(C[oAMS&{8 +ʊWtAscZtQ4Rq%=t__t)Vρ-*4Ձ ؀% Y}B`MMH0OǺ2ыL]Xr Vw26?|!xRB<)VKD N?GVR!ؠܗmlv(z. /\j'Be:BH+UwpH76ٹ4RC{ug }]f;'0;"*(Q1@_q?i\hFZh2Xs{g|/aCʸ5W!%ȖiqN!Mn^w;b43`:dTqa"Zy;]ejL=C8 ӌ*뵠գv )2o0u?:`2!DفTtL)wQ~(jQ}7.Qi 8MxUkA.m!9I%RdT7A4Q%Aԓӫ )W!Ft9$y4MϬ&q ifC)6jjU*O"]F﬏7l֑fZ JY.[c n_Rg^ mrߣ"Wo2YK, +&Uj9aIGU2M s> oke=ZUO C~5Ard.}#daR6*Srd\Y HgEa紪hVDfwwO.bvϼ @G7:Pt"ՉDG% r/sO@v\"\8Tr].˽E>[=,x㈂ CȢE~GE>˗nL$HVC?:&hh m0$ڽ.]#޶MڴǕ~pE|ZIx @ `*d` l CBQnыrKsVe'@vl;C4@ƦV$I[j9T;ȡ.L%7&*쌍ؙAtCbJ{6H$y[Z|i'@/g LYH`{ѣ3q K2gwMׅ4_hޭC2>\ar3^[Kp5ܺIx?x!.ᄦX1ۅg 2vsc BJ(҇-5vRU-9C xt-!$RDv4V{eP?Ǟ10\ e6~_% :ef=JTCk Fdô:,V:"u >®܉C(QMa|k :Pey6 ͆/Cϔ MùW=i5 e">W1pk% Vr:@Gq6i]$xKB.Ыe@dG39GջFN#}#ӤW-ZJyxS&a$+I{{қ78e[sͽX*h N$IᜂQBοYmcݶ4BBd7XAsюrF.[)+ʹִNəonUe;x^20`(W Vo7j͚ ^*ɪ9V}͡ 1]%2^XMM^[h`'Gc`d5fЌ8:ͥoQVϱ B$ge$:f-rF"/|-bh\{‚E366"!ĥN{oQNf;خŀơQVuYQCb{1؃ ؟$)?'?nQ ]wqA!)؎֟g` Ŏ?9|\OBr 1cpIN_(ϒ 0E1ŭ`Λ} s_񏫥Op4y!_(dsXH[ܚP/-=b-\:2+{ՊW4f(ȓ^MBtH35/7zvl9fwEnZ,PW6REITl5U'@e&wlvY };{U9gp|%B593Q'jy:_IXMV58-Yh|l/{ {ԫi s@^MzTϏSS [0TףxƷl05X?܇MOpl'hʎB<Cc@G+/c\TLD?-4eG)!ӷƀ[_&Q4vpao (ϋ[@h}w75K<蠕 KÔI y}6 0ݚ𼘂[h^^@̆R/mkx7c#IO&1p:w/BQu 2=y?ҡٳ268rW[>?a.cPܾ\zxh&rLc@ߞh9o`RB$VQG:1 f≗ #$hP$VGqQRзOP_P$F}TrO EJFJg5+`Xd:GU՜,Z)]J((TLHyE]QUGCjOTE*0L_}&8|c..N+G G=c`Ń ;rMAvyud=|Qg uP0]nAE4S>7)n,A͒9Y &}3aD-]h6pYjNVA$߂a}Wu*w5I (=edyjuPPťie=C*]^ iNЮ(GUT eć.9WeOaiʲ@WP.\u:KzT}N}( Hb@}!_;K* -7la))<$}*4;7hWΡj,ʆ|͚ ^ʘ U-1K V㵀V6#S(׮PL=:(iw&0!O<"O!PL=󌁃0[` w;Sx?%$CF+ <>~jW&nZ \`EIf.Kʳ )#^HD1lB`D ,b=^HPV6voM|nuL&&9u =8DA){I>M|_PZ:ʫ\HJk#)\ F/>Dq~uP^R NN< YuZSj ^һOL&G JL'p9+; Ӿ*׹Nau[j:(iuE`9Y-CLU&mV :(iw bJr}j][iU'mV{l_ќVi_$PLAuc߻Qlsh{z}.ρ'Cs%>@.$-3DT`Qؓ,:h8(8:o~hzׇۄAC0mF ؋ÆsDf/hgu}qvNMP$;o0]`Ȧ M z{ͼnV)Mm-{tDlgThR?n:?_Ao6!I=*L]Czol.ZE}uBEZϯbs(^t.~$H[{ v P*>P|pF9PyoW ߐ1{P*>P à@s|(iZjkgUAqt@16_Rԅ4'Ŗ>ooh{G;Ob*r%nk;i6 w8('{y}ɚW7Vi+i~Adksnh@gfͫmVI 5MM:*r5*1[DLJe_5=$l e&&ˎ{h Mjk/y׹@HUn2m9IwQ \ʵ2-G LFNݲof̪UXr(*=_Ip_6Cb_"j4DS/τ"t-w/ wP䪬Fwj`(x&܂Q L`@jg.7X#F' =6JE׉pB%@gu @^HX;]chlw:j6PaE]I[W7XA^=xYAѨ*E55ˇ>#G9YrkWGv(,OEԃ 7XfeTr k5w!E`vd[n +cT#P;?jdr8evQQh8e# 7J4zƢD?h>G|>8}ZngpF`pJi:F|jQ\Fy/^88/:㹢}vP`=7NX 'uokֹZR"cdT=Fy2u{GaG52p>6'P&cz:YN8>Nt*ŁKD}:vP0;N?ntMO3mOlJ([7Tڠ+_fz}5v*PcjiR5G9j \QcjWs51p5G9j \QcjWs51p5G9j \QcjWs51p5G9j \QcjWshGeXIfII*0tEXtCommentCreated with GIMPW%tEXtdate:create2019-10-16T22:47:22+02:00C,%tEXtdate:modify2019-10-16T22:47:22+02:00IENDB`paperwork-2.1.1/paperwork-backend/tests/guesswork/orientation/test_img2.png000066400000000000000000000022141417573700700272670ustar00rootroot00000000000000PNG  IHDRdÆ  zTXtRaw profile type exifx[+ YY16ˡHA~h3=F%/(2ƟMw߿iL!y.9GTR኉G{[ѫ9C;r}cz}P! =pǐYxx=sq{џ$Y3Y˜8傹sLl\{zCヌ[!}y(p_%c^:UbVh;< ;ޒ;#_ʑOyxts{rgƯr^|S[לՔ|6uoqϠwh2+ثtGI4QG] 1MJԩҤ\Lʨ6e3O>KI`5:xZmqq"E$0/f)%^XU 'u鴈`:9 7ȅ~+j:لVFAMb~|_ }G i,+E:bKGDC pHYs.#.#x?vtIME 77ItEXtCommentCreated with GIMPWIDATxӱ 0g~Xᤫ$՗ A `0  A `0  A `00 A `00 A `0`0 A `0`0 A ` `0 A ` `0 A ` `0 A A `4_8IENDB`paperwork-2.1.1/paperwork-backend/tests/guesswork/orientation/tests_pyocr.py000066400000000000000000000063751417573700700276300ustar00rootroot00000000000000import os import shutil import tempfile import unittest import PIL import PIL.Image import openpaperwork_core import openpaperwork_core.fs class TestPyocr(unittest.TestCase): def setUp(self): self.tmp_paperwork_dir = tempfile.mkdtemp( prefix="paperwork_backend_tests" ) self.test_img = PIL.Image.open( "{}/test_img.png".format( os.path.dirname(os.path.abspath(__file__)) ) ) self.test_empty_img = PIL.Image.open( "{}/test_img2.png".format( os.path.dirname(os.path.abspath(__file__)) ) ) self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.config.fake") self.core.load("paperwork_backend.model.fake") self.core.load("paperwork_backend.doctracker") self.core.load("paperwork_backend.pagetracker") self.core.load("paperwork_backend.guesswork.orientation.pyocr") self.core.get_by_name( "paperwork_backend.pagetracker" ).paperwork_dir = openpaperwork_core.fs.CommonFsPluginBase.fs_safe( self.tmp_paperwork_dir ) self.core.get_by_name( "paperwork_backend.doctracker" ).paperwork_dir = openpaperwork_core.fs.CommonFsPluginBase.fs_safe( self.tmp_paperwork_dir ) self.pillowed = [] class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 10000000000 def pillow_to_url(s, img, url): self.pillowed.append(url) return url self.core._load_module("fake_module", FakeModule()) self.core.init() self.model = self.core.get_by_name("paperwork_backend.model.fake") self.core.call_all("config_put", "ocr_langs", ["eng"]) def tearDown(self): self.core.call_all("tests_cleanup") shutil.rmtree(self.tmp_paperwork_dir) def test_guess_orientation(self): self.model.docs = [ { "id": 'some_id', "url": 'file:///some_work_dir/some_doc_id', "mtime": 12345, "labels": [], "page_boxes": [], "page_imgs": [ ("file:///some_image.png", self.test_img) ], }, ] angle = self.core.call_success( "guess_page_orientation_by_url", 'file:///some_work_dir/some_doc_id', 0 ) self.assertEqual(angle, 90) self.assertEqual(self.pillowed, ['file:///some_image.png']) def test_impossible_guess(self): self.model.docs = [ { "id": 'some_id', "url": 'file:///some_work_dir/some_doc_id', "mtime": 12345, "labels": [], "page_boxes": [], "page_imgs": [ ("file:///some_image.png", self.test_empty_img) ], }, ] angle = self.core.call_success( "guess_page_orientation_by_url", 'file:///some_work_dir/some_doc_id', 0 ) self.assertEqual(angle, None) self.assertEqual(self.pillowed, []) paperwork-2.1.1/paperwork-backend/tests/i18n/000077500000000000000000000000001417573700700210605ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/i18n/__init__.py000066400000000000000000000000001417573700700231570ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/i18n/tests_scanner.py000066400000000000000000000030021417573700700243000ustar00rootroot00000000000000import unittest import openpaperwork_core class TestScannerI18n(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("paperwork_backend.i18n.scanner") self.core.init() plugin = self.core.get_by_name("paperwork_backend.i18n.scanner") plugin.keywords = { "centrally": "CENTRALLY", "feeder": "FEEDER", "flatbed": "FLATBED", "left aligned": "LEFT ALIGNED", } def test_i18n_source(self): self.assertEqual( self.core.call_success("i18n_scanner_source", "flatbed"), "FLATBED" ) self.assertEqual( self.core.call_success("i18n_scanner_source", "fEEder"), "FEEDER" ) self.assertEqual( self.core.call_success("i18n_scanner_source", "feeder toto"), "FEEDER toto" ) self.assertEqual( self.core.call_success("i18n_scanner_source", "toto feeder"), "toto FEEDER" ) self.assertEqual( self.core.call_success( # Brother MFC-7360N + Linux (Sane) "i18n_scanner_source", "feeder(centrally aligned)" ), "FEEDER(CENTRALLY aligned)" ) self.assertEqual( self.core.call_success( # Brother MFC-7360N + Linux (Sane) "i18n_scanner_source", "feeder(left aligned)" ), "FEEDER(LEFT ALIGNED)" ) paperwork-2.1.1/paperwork-backend/tests/imgedit/000077500000000000000000000000001417573700700217235ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/imgedit/__init__.py000066400000000000000000000000001417573700700240220ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/imgedit/test_img.png000066400000000000000000000107061417573700700242500ustar00rootroot00000000000000PNG  IHDRdÆ bKGD pHYs.#.#x?vtIME vtEXtCommentCreated with GIMPW.IDATx{LSgǿU,E`Vo+dTQScj:ܘHy&Bp@ P"rSN[V~۾@O=PI9ym H0 0,a0 aX að@0 a^D"t0H$TWWDF㴊wE`ʕH$Xa\P Y|TUU ^ƭ/3ӳG|tE! PTy D"8>غu+juXɓ@^^@&aܸq8GOFqq1+`ڴixwo؜WAAƏo/'0uVNJT*RL0AAA?/8bb鱐@UUU^J(%%<<#Vz=EDDc6ץ+9/׉' ڵkkNq@PLL jT[[K |Ҧ7''… h4Vھ}`^8x F!NGZ-eeeT*%tUn؊+ݾ}52ܹsT[[K? $JI:Tw[bat (jnn8M(''Ǧ7!!ٳg;]QF{>}О={:U Z<<<ϯPݟ8}nZiL&3φ!C7otZ]Q ///0;BTVVbɒ%4h5>>>3gtKyC|hq?3`{^ja޽{G [liwh"uhޙe Zvռ=*j^VVh}u8HsSj*#??EEEχLiӦO?˔ѧO|w4i1cr<*SPP#G ;;˖-ԩS'O… 6l J!k9Tw[bHؼaA:ð@0,a0 aX a0,:T[q{WP@" ##AX 8 =l'X ÇQUUNddffӧOǂ 0zhJII"a…x7-sÇHKKCrr2 \`(J̙31c`z{lm<}||0qDbĈul 55YYYCRR~mQqۦ] 8~8qx{{Cpp(D=i`RR]hRH& ?uTjZ?`޽{ ֖ե]N{mz)qۦ&"ŋ_.e=ڑ@P\\ݹsbcc ͝;믿7o^ڿ?ɓ'^?[%Kʕ+d0.\@X\mGHܽ{Wo>H[پKNúuP^^^K.O?m+˶ݻw>deet(//ݻq&CG|===i&ڵkq:<>y(]潻i&@eee.vV5@`e~"T*-Ҋe%=..NJ6#m,`"… ]~%ݥy4h~ )) d9s&@[W-uoT*&O# @p =H:4w^̙3s2 3g΄\.Rtg/r1 ׮]ȑ#!JqVqA#ܽ{PTtӧtR,T;<],"BII N8B៽0;Hg'z 0,a0 aX $,, e6Xi,^U/jaƂrZ| 0 JBRDuu5baSWW#Gbxys[rssm۶+qIdddd2ƍ???^FBtT*`޼yp8 %&&Z-vAt۷o1ؖիW>e@iii6+R^vwϵuwtħ7%%<<<8pnm۷h4R]]ϵ}JKK ^Z0_ܹs[v@ӔQ}}=FjTRRB۷oN鍉!ZMzjkkIPϵ-&̝;hCxSSyFHw@ǎ+`@-$ٳ]EVͺsrr)voވxPPtիyF۽azEW#WvȐ!7o\#>d29ba`֬Y,&M777ڵ/_Fnn.6olԖi^k.~VT*Ehh(֭[R7n_K:tqͪ5*b䟤-nbtt4rssqeT\.ﶊvaGѣG#$$6l{_/_ޭ[`;ÓٳDh4VrO/i;0az{dd$ `Y@^旡b)StkŝkzE3|zHbb"lق 455A qa_^[c; 8r_455* ;wVouO/ӅtJo|?`5g/hfѢET[[t޼<@ X ޼(//GRR233q=!$$'N03vZ@*ė_UV(**B~~>0eL6 e.Č3?[y `4Sr3=ϳ644رc8tKbUVVZT]vͮwl0c5j"##qeÿ ÿ a0,a0 aX ah dARIENDB`paperwork-2.1.1/paperwork-backend/tests/imgedit/tests_all.py000066400000000000000000000026021417573700700242670ustar00rootroot00000000000000import os import unittest import PIL.Image import openpaperwork_core class TestImgEdit(unittest.TestCase): def setUp(self): self.test_img = PIL.Image.open( os.path.join( os.path.dirname(os.path.abspath(__file__)), "test_img.png" ) ) self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("paperwork_backend.imgedit.color") self.core.load("paperwork_backend.imgedit.crop") self.core.load("paperwork_backend.imgedit.rotate") self.core.init() def test_get_names(self): out = [] self.core.call_success("img_editor_get_names", out) out.sort() self.assertEqual(out, ['color_equalization', 'cropping', 'rotation']) def test_all(self): editors = [ self.core.call_success("img_editor_get", "rotation", angle=90), self.core.call_success("img_editor_get", "color_equalization"), ] # the input image is 200x100 frame = (50, 25, 150, 75) for e in editors: frame = e.transform_frame(self.test_img.size, frame) editors.append( self.core.call_success("img_editor_get", "cropping", frame=frame) ) img = self.test_img for e in editors: img = e.transform(img) self.assertEqual(img.size, (50, 100)) paperwork-2.1.1/paperwork-backend/tests/index/000077500000000000000000000000001417573700700214105ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/index/__init__.py000066400000000000000000000000001417573700700235070ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/index/tests_whoosh.py000066400000000000000000000076151417573700700245240ustar00rootroot00000000000000import shutil import tempfile import unittest import openpaperwork_core class TestIndex(unittest.TestCase): def setUp(self): self.tmp_index_dir = tempfile.mkdtemp(prefix="paperwork_backend_index") self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("paperwork_backend.model.fake") self.core.load("paperwork_backend.index.whoosh") self.core.get_by_name("paperwork_backend.index.whoosh").index_dir = ( 'file://' + self.tmp_index_dir ) self.fake_storage = self.core.get_by_name( "paperwork_backend.model.fake" ) self.core.init() def tearDown(self): shutil.rmtree(self.tmp_index_dir) def test_transaction(self): results = [] self.core.call_all("index_search", results, "flesch") self.assertEqual(results, []) self.fake_storage.docs = [ { 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': 'Whoosh and Flesch are\nthe best', 'labels': set(), } ] transactions = [] self.core.call_all('doc_transaction_start', transactions) transactions.sort(key=lambda transaction: -transaction.priority) for transaction in transactions: transaction.add_doc('test_doc') for transaction in transactions: transaction.commit() results = [] self.core.call_all("index_search", results, "flesch") self.assertEqual( results, [ ('test_doc', 'file:///somewhere/test_doc') ] ) def test_sync(self): results = [] self.core.call_all("index_search", results, "flesch") self.assertEqual(results, []) self.fake_storage.docs = [ { 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': 'Whoosh and Flesch are\nthe best', 'labels': set(), } ] core = self.core class FakeModuleToStopMainLoop(object): class Plugin(openpaperwork_core.PluginBase): def on_index_commit_end(self): core.call_all("mainloop_quit_graceful") self.core._load_module( "mainloop_stopper", FakeModuleToStopMainLoop() ) self.core.init() promises = [] self.core.call_all('sync', promises) promise = promises[0] for p in promises[1:]: promise = promise.then(p) promise.schedule() self.core.call_one('mainloop') results = [] self.core.call_all("index_search", results, "flesch") self.assertEqual( results, [ ('test_doc', 'file:///somewhere/test_doc') ] ) def test_suggestion(self): results = [] self.core.call_all("suggestion_get", results, "flesch") self.assertEqual(results, []) self.fake_storage.docs = [ { 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'mtime': 123, 'text': 'Whoosh and Flesch are\nthe best', 'labels': set(), } ] transactions = [] self.core.call_all('doc_transaction_start', transactions) transactions.sort(key=lambda transaction: -transaction.priority) for transaction in transactions: transaction.add_doc('test_doc') for transaction in transactions: transaction.commit() results = set() self.core.call_all("suggestion_get", results, "Whoosh flech best") results = list(results) results.sort() self.assertEqual( results, [ "Whoosh flesch best", ] ) paperwork-2.1.1/paperwork-backend/tests/model/000077500000000000000000000000001417573700700214015ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/model/__init__.py000066400000000000000000000000001417573700700235000ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/model/doc.pdf000066400000000000000000021522241417573700700226510ustar00rootroot00000000000000%PDF-1.5 % 5 0 obj << /Length 809 /Filter /FlateDecode >> stream xuUK6WPZ+JeA[`- +q-Rt Zy>h曇曇aW W"?'YS%RYєɾKd_zI2/cZHN]PH>?77<^lE&*i?Lu#ǍLU^uV-ē%&KDblZȤllW7l+p&`s@1sÿi- =:rMw%ҭ;zꭉQ9ݥ[W^9]0@!N=zy%[z^dGU5WR(D-H1eb^rfбO"|ZO3ApDZ45a<ҐDzyw &Y,b%[L - '}UB؊:RPz_ːiݽюjRLMznxT>BZ5kB*0,Tq%hݑwl`)!bWח]by,,٥h> stream xwXT?*X{b[,(أ1h$FK4` 1D0"JҤH.^ܗsQ\aײ;gΜ̙ QSSᡫm۶)S 0sΟ}… w}5// @ݰagk߾[222Pۃ Ek׮=wwDDD[AAA..._-[FxΚ5?,))A,V^}Tjpᆪ@Nm\nddɓy~Ν)p-@yxx,X\<goK__\ܒ.] 544QyՒ%KxA̋/P-2:?R(enݺup&l+W|g~]vfUTT^1h __OS\;ydsH5kְ`JVVvƍO> ?@z… ?^~_+KFEFFRL=:twLLLPEoԷk׎٩XҪAC$''۷ONN]xq@@hٳ\.* 7 8}K)..޽{7sIOOGsRDnpݝ/h|^,774_hQuuuѣGJJJT}Q@jiii"纟6myÆ kۙbbbԨ$]vussC:|0/={:͛D.yAA̙3<ԍ: %rss=JM'&bٳ :upu)RYY˩H>>>h#l'Od0a2224z=`'Ι3^tJUUUpB*X#""L t"1cƘtr[nQ'Ņ:\hhdԩTs84H$$#ߺb DGG'\ɓ38p@4K;p@*&VcA!O4]Ϟ=i+(嬭ty"[HTk׮@°v?YEEunn.0c ?ƍCBBZ $Fdd[aaa|egg;::VUU?)Dԩ*Pʽz ryQi/^ 1;FΒ%K>6--*??/mիW/Tۺu+͛7Ei?FO>|œׯ_:88ձ;⾧4p8:t-.e>}4?ΏqgeeE1Nn>;E|jjjrrKÁPRF%Fe...޽;hAw_58{}.B|xxsuu;i @ZYY֜9sW/]Ė-{kl:;;|2,,ᘙ7;hۃ&S^^,$*yYY2C;bs)((4`ccSPP-,,h]2УGc9B_t)ŋ)Y|9LJ}z2}:h ƍL)lf;b-{_7nLGGYXX󖇇$ׯSʹOrJ ϟ?C;w._OO bjȑXXXquuuss0J9j(d֭O:,//Cqњ ()uUUUeeeKl۷oVǕ+WT6?== b-@_TTԤ lllrrrxhkkS>/_DJ͛7S={VbĉtFFFhP;qqqѴk׎ IIIWHH Я/).]nX޽{Ѡ vbbb(С w 3f̠ wl!>333]\vv6_YY?errOLL ñОp}CSL ;udcc`~~~ݺu|hȠy  l)((חbv//ߗ?{l6޽{dI_Q+Ktޝ9[jkkCCCCBBRSSɓ|JÇSkiiI̙3 bgǎ|_TTD|tt4pvvpE{A K5kPH޽ɓhV;ѨTWW7|?777445""JKK544:::51m4jh333 8gұ|7hV;UUU󑇇GzzYeffN4MCԭd߿?˝;wX/_fqt9 j[RR"t&|]]]Q]vg\#t,'OF8*//0`56ln伣c.](!CDEEJ%[!QFF+$777:aÆe@LQh믿6uoߞ6mZNN*SSw]2ۛghY_/_f؟u%KHsE(--ի޸qSXXH :u 𕕕)733+++CI ~Z/(**BHz%p^JW_e@2\~Nq){<6mܹ3/ģO$uss+V>|"匌믮.sQ:---,HݻYӧOMPUUL{;v\xANN@={SNټy3.Z$?@gOVWWI~yܜ9s(@MH$~Q$p&OLcjj-VllDDDYY)SЛ111RIEJp\Hhh(Z$:;vhkk;&MtQlL 8tv 1OB7}t^?rSNp\T2JHHs%X[ 86}ZZݻw544( ܷoumm-j@~X܏ܹst 6l@5kVÅ[$022_^^૨l߾| F3~ +Wz*$twҥ-syy9wܙwԉB~ KJJP"NMMb}\.6y‚nݺ|EEu١ DҥK_QDDDQtС m y6`yҒꚿj}{gDNN.%% vMӧOQܼybhPT&L^wp8nnn>>>ׯ_^B_WW#F~4h:{,}Iw!Gzj:sΡA@Rm۶NzϜ9[XXdffWWtf.cC//BUU ҥKtҫnnnfffyyyfaaÇ#""sSH/--E+ OOOX!իE&My)StgggϞӋ,('''441888&&4rG@+YYYfdd!ݻʿ{n&H0 WUUOz:OIIHB~ kkk)? 6.<;޵kZ@4}%ӥ+C巶FScK)++ ͧS+++__d. )prrPV9N i%;::RtRQQ @|FQ Gg׮]2::/**{+$$$<<А>ɩ0(D4uwwgK(ԠBDم qFq,_A %A[M^(ZOII 655`gee޻wmnjC~1BUTT7TTT{0dccC4b+yYY-==EX-r4''Ғ|?^RRRXXXYY}]vѮ4}6{ҙCgn߾=77u%@UrH: @ٯ\f[WWI666A ulmmūSLb_p-/44~Zt+]^^ m!a<ؾ}{j;^xCӗ6uר:cjcǎQ}||x$/ٳMNNAAAh ͛3gΰ%͋Iqq3(q0H]&O,Fef3mڴ ٳgtEʈ#]=z$=|0fr!:j=z p-`rڵ{UUUZZMjj*|)JEʑ#G]m&E-((`DB[b߸q p-*ҥKW]]E􊊊'N(**}Z[[ (THqrrbWlMl\.Đ!C4Ʀ `kkK>|8BDFFg(j:000--+66lV|THܹ35(333SQQinnV)Ⲝ*CHH4HOO߷o_vddd6lx񂚘i4w166533epޘ *//t[fNT__O}ZZZrr Lש>}:e~MTfs,Ux_| 4"z'oHJJJXHZD߿g)aaaqqqVVVYYYNp ~J[PP"h޽:k׮SBBBR =(bHGFFIVSSCStEshr>}Ycdee/_ްsssKJJϊbv;;;苊0͟Iَ;5/^~b/鴭#GR:f(!!!)777z=yyy*ITTEtdff.^]t9}ݻw]vL8}Աc>|Z^^N322 I9i$… |5c j#mmm*TݻwGTٴi Q.Hn>>>fff ^ $@ W{,""͛7=CYg555ߍSK]zUS__?w\*:iCJ'|澿8 BO<6.;{+qٳ T 7oҥ q\999QDssΔ wSݾ}[tnPa4 m455fȚ*&&љd*L3/qL&uDGGO2ٳdĉÇQ~~>/^(,,ٳg]ݻàAĢH*j)j/]]ݶ-IXXXǎ$ׯ_G8}t>Bck FUVV99gwD(\ӧV^^~ڵ+;tV4uuu+))n۶?oe}jggG>|(ccL5sza .#ZlYQEVb}vË:Do=zގϷ|⅗WUU=ب<Νp CfΜIVe8|0@EE%##-҆BQF)1S?){\w"[c,%7n| tRڵk+|||Xp=z&**jͲlM6f׮]ӧٟwzjV={0x Bgnn{&y,,, ?N zO]\\ZdEEEMԶgѬ.7776シ}}*ȑ#t8֭kfdd-[XDOV\˗/OVUUU.\Ǯ8pA,OԈݺuKLLlRTRR]hN˖-S{OfbbR񿣷i&*zVQ\.WWWEÇo8 ;;3t=CgDD /_x1b35"#dL6- GUYY9ejI&iqq{pSz$BڧONgfV766*++۰a@)ecƧ%WTUU1=7nȋ.]¢"jiii_}˳{:ԚԦ~m+/}||>-Aqև\|SNlv;;;vaȑ#z_uQ RSSBfndbbbddO}}}يis*GEEV4%c~… _z%ΝM֬Y觮GfN4 Duk3g9%%޽{:::Ԝ5k޷o_eeeު g>}Ə|oFOO~g˵kإ!SSS9HЉ̙3ؖXZZ>yɩZ i>t*҂ D߿|(ܽ{W2:ƟB &~͇Yx1mxwںu+˶k׮{KM&<<|ƌZͤE]Schh.opZtblggל|(hťb '~k]__f'k׮---x hsׯ_hcǎT gLnܸѥKvwu b-33SGGbא.\sSN<~t$aaa)))oEDDXZZ޾}_]f͐!CfէO .<|=wߡ@GSNR噛SOg#okkg;Nd *++y;vCX⤁Ճ 9rdSXC=lD_|/\~=$$IypWgl޼s-4c)nSSSMMMTNO& cbbݶ]>>W^WlՀ{у_$}y[i-&ck7:yBBBYYY100ɓ'-_Ϟ=V5Þ۷+YYY&cƌ~ͦ2cu&9⯾ϟ?oDGGO8`߾}h*ϧa:-oSGGG i_z"K ͛7 THPPPϞ=CMHH|VRޯ>|{SN.]{2|pɓ't~v)iܸqIIIhffftܡC662/^˗Z!lzGڵ+`ԨQMږ=>j*)Z-IIIZm^[[[v9o1$^EEpرc&IKKϟRI[t)ɓ_-...liӄʕ+|?H*IǏI͛+"JIIa;Po4HgϲފM@QmRRӧOJKK+**֖_^b6vhhh+˗/;uҗÅ hm۶u[` Re5kE{DJKKc{>8==^}||322\]]o++]&SuܙzsQ_5HJ uֱy޽#8qRSS֮]Kg'OnR<^ZZcccC|xx8 UUUt MɄ.1bM__5+ʴߙ3gS9^JP>zt›̙3"^f###yyy*ŋ[|]eooO纲mQR?٥Q\ommr޽W^q8DeSPP:!vޝ=S\[s[ $CXX͛X8ߧOh*9s>;CC53pSSӗ/_Rt_QQQPPM\TTԤo@;wZ׏v:qMȂz pOcjԩi rrrˋEs=l)S @;vNqRw)~ӳ{[2޷yaTTGaޅ6c^ J;v,qذayyycquuMMMͥOe@|\r7^CCC\n7Ϗ ȡ/"$$Φ-yZZ<(("x͍P|a{FuuEh=z􈋋k4Mmmwpp03##BxTXXԩSY75jTdd$3aB~֬Ytf_~QlYZZrBӃ߿ŋ*((`SVPT۷g)ro4MMM HRRE Jf 7w@i߾=u]޹sGF\hQUUџV]jj*ffftRM1offfDDE?s OMM-H'++FTTT|/%%C޿v3g˖-KKKce׬Y#I,@Jdggw֍Nh/^ؚCɊXTTT>Lѫ(tڵunWUU9ƍgjB???ggڦD\UU[ݷZlIEEE+&33dѣO/ ڑ't&x())=,]]]6ɓSRR'LVê ({)Sxv}vggg.:eصk;n[ۗ.]}ihhCNNҲ322@t|7TVVJmU$%%j@\DDD8qw޼РAΧ^/**ha,ߴiVWW[[[~J?vXv?3|"EEE .dc)F,''G"٫۷+**N̙sݒO˗^9@===ƍ655*++DFF͛pm%%%=:dǎMuQTTDc k73d8q⣓eڵG__v&meiiӬ4O~@@ER 7|͛7>MtT__o>v뙡@<_DAA!--ݻE p:L???oohAwݰaц _ JHHpss322h}޽˖-=z4J>dRիlŋBuۇ6@QJ$EfffMZzrӧ[O~866-A) JKKɱm۰9_6:dIKer)ۘI:`K~H 222 Əϻ}o>OOO ))_zthQQQ&(//wwwOHHζ^KJJRIfccîPCJSRPTT|y .D+~dEwisnݺqY^Ϟ=fRUUw`nnnZ\\, F4-tƍ+,,D6QQQcqw\\\ƆHC\O`Gդ<Em;]o~VEEE_ێ' xgg犊 AvPVVfbbB?,??VUUB76zV ..o߾lťZ\K6//K.t<@)Y&=bkSؕS<Yӧ҆bĉbu6 S<Aa R9\.8%楥***zH9tmp^xBQ$66󴴴d祥'$$X[[yvm6:yyy* ZVڸ{s}M?p@С{vJŨ'\MMM9GGG6X鶴3$$JM\%E -))yyvv6G533CJ;;;xgѓ޼yÞp8bTKy͚5mNˆC<{ ]@t0>|mSSS###}||I^YYIqA~~Gx{{gffR|RELjTژVX)4f1"##.WE|ݭ[7?v0uT:rĉچSPOѺ qvv.**rttԻS8⒞'-##C֔6ڵ_n]uuu߳gOlov}zg̘1UWγ<==!33S^^rpuuE5su;;;7t?\SRR( w % $uGSJQoٲS,Ȟ?~|FzsbWck׮9sFvI9_@8p'L HG 2]aaa|||tt4EI RW^&#Kyzz*((((URoݺSlx̙3Y>}kĮ~*y~!88rh׮Q4222tB.]$}rr%xmllJKK_~]TT$HAAA~~~|+++S?S ܹÂ={|ٯ_XvkӦMYD [ Ɍ3y>6X^^իW$p8 geeRhOI'8q"'UYpkkkٌdΝ9s&}jll,vUڵkBgCʡgϞ@hɒ%t޷o),, utt|CJr? ??ȑTaÆ 2H]]]=zO'Yhe.##s%c{yy]믬fΜ)t&γ|84IdddXYYeff`!g͚nƢդ6 KO<ũQ;v?= ㏬eddQ?3el2IQC錽W^ϐ`(Zt)K.h/rĉ--[n|R&$$PۋT³gX\"t>,fAFbw8 t~˖-l/^5=[l555:::I/@QʡCcMnݺ j*WMM9YM2)--ݾ}; uvOwWٳ?C[[ĉ۹s ֭[GҟfKoəG]][3]FFƍ-9=YW߻w +R cejjjR/_l7oޤF. lmmt,?X+<**ٙbSbŊ1cưhQ]]~z矖HII2ڵǏ܊MܷuVqO6+ $ U~~~)-@۪yѴiF &LXnǏ0֭[wޥ_OO'N߿͚5s9r${l 3~e˖ر)Wҿ#GhwhQQQ|r0{NrJΝۤ{꧟~*e+ЋN:yFx~@_}/X@QQ3Ph6h 7mt1 _$@ii:ں,,Le/RTUUnnnl@'/..Lc! 555LϦUcrҽ{w\ MC bÃ^t֍|jhhPNB > @FFFVV]JCCC۷8Хr;vbccűVL%~?~Ȉrc![[[ Ǎ'&l* 6lٲsμ~׮],UAA˰Dz(6pi"M/&222Lد_?)Uxx8{_Lkl۶Yty.Xr~::-DGGE_|A[Qt?vX---www'k?|p1U|=U v5gƀ/6H\výRذp[[OQ*CC&=/jԨ o9sׯrֲAYYYMPKKKuĉ LIIa 'ׯmngg~ <***-4ieeeΝ;-^ǏS*5k{zҤI-S&3c >mػw~׮]V… ;ggg+))Go"'Oж}vuttauuah3gδl455)ۛ7oi{Ġk׮M}o9<[nbǏZHsΥl=z$U7zv„ ;yٴ[oaN*Ķ\.wڴi]ZHl>y1ңGRS/^45gvfʕԺt"lpppKgϞBL':.\@?9rdYYYr u ϪUhe˖Hy\.{<99YLΝ;+$??wBs޽;m鉮 *VVVm"q|VOj&cǎQVVVU]]][TPPp/><''`S?~\K9|/L\\eաC`:nݺO?S2%%%:d3g3/_@oذA^^ﯡMy{.b̞=[-{fϏӧg^^ Cjjjج3f̨0W^]@/b#GR,?`&NxaSSS:DOdd$E^^^.t&7oL֭[¸P>#Fr;v,**j̘1拊̼SNUPP.*++344?>ty׮]^^^4;::^ti}iN6_8MA.t&__$ymNa,--$&M*=z4ˇGϟ?oR- oܸ (r߽{7E,By=x𠴴qqqb?aggggLSS~~dxf>O)e\v-ŕ+WsGΝϝ;"C#5eee9NjlK]jkkks8`s763*?<7߈ue5D _WMMO^:k,ݻ_:t(00u vȑ޽{;~xAA@ⱅћ9ߟ2zω'(ueVWW[QQQ}1Q ϼ|ƌ޽{oDD}}}l9s`sDFFFjjjk zzz)Z @%%% 텘ɍׯ9C:DI@ ~H']Qpzyyy[6|!CnЫ?9 w*@'/_d?~<|pVcRԏ&`l~s2a>|XwI;wNkl4[؀fJOOg+u=͝:uJVVszڽ{ݻw[g|NN΋/#""bbb=[]]}:k׎֜@bsf| e2|p6ߺu+m~%FkkC_ɓ'HQQQ b+UV}1}/7OO Offfz{{dgg(%}Ė&3gΤ|Rfɒ%d+))ڵ+}-,,Τ mq=ދ/f?}[*g6!CZd>>ǎk8>|(tL2**חby(INN[bK.9qV$' i~zj-[_E9|Bl_Ӷ_QڼSNm kkky߾m۶UVVsH.K>˳qY[[ ӿyPUQQZQQQ^!/+/[N w^bKd ;#q}bbQZv VWW̩-Ν;-$>}=NV\)Ȫnnnyyy;7n !V^:Ν;Qcƍyʞ~(֭[7}t GyKKˏNѰ}v~j}͚5LgѣG-]C^^^c?vA.\ RdΜ9-~ˬ %Ǐ6j7П>}J{bwgϞرc#FdW^}W#ӭ[7%%Ç߿MȲi4o;wN*!??hɒ%􊊊TO HBBœ9s؎֬YY {8q]5kVAAA柖o޼yh ̻YAT),,da6dFWUUӇR澟Gfmx쉉/n8`ʔ)&{?sU:ux@j=}?sf!P>111ߡCڣhE4h\D18~v|}}Mؽ{w6֭[6//o^D}_J t̙~xN:+-m̙lw3fHt=wcccCCCSSS?z|ۿ Ś5kXlek׮ɱ<==5m8dȐ?#Ui:yaޟ@]]]__?.. 33slիWP$ޫW/6uuu9p\77x__ߴ4>)9əGBVnn.1ygz˗/Ax#yblOz\)))}]ǎQtaɒ%ϟgW:w+6;;ȑ# 7o@ʻwn҆tdii.#v5/>VL} u"w711aܿSN G3':˗/iӦo:vo߾ѣG3:u+4ӧl( ߼yCSddW]KKՆ§va^P￧@[99֮]K9P"޽{ 4i۳gvtt|' }4f̘5}USS=]|@9rwgeeeDDg~yUq|}MXc7Kރ5hEJ,QL`5*v#t#R^}8>eY.6|wwfv2ߙ3>kb7iY8t3gl۶m֬YzҴjjɒ%˗/_6jԨ~um߾w}7ye˖8p,44T :::(ԩܑaaquuQqgϼ1 )S sEø}͛7ͥSNq02@v 즇s!ڵGEGG+!񍍍׬YӳgOߡCK^z5""⧟~s9Ñn*͛7Ozhnz`FFF<&;;R]ӧ666ѥV#99k׮qD/1  ȡĠAԒsrrhw|۶m_|0&,Z4i” Zj]v@ 0 0 #@RR1~We>6W|tkt邻6ydH8$$$XbذaJ֬K}333!?~ -njjzuRzzzTڵk_}Ø1cAO+1(?))>777&&&""ƍaaab&'OL;ٳggkժ@ZSÇR6l<==ysNeQ8p͛70 0 24$NIIyWev >}p! EGhԔۧ=_G^ŷnzС?JfȐ><<PVVVPYYY—%{{{{ccc2ȇGɏ#~~~q۷e_յӴmgU ,,W_~@U߾}K\O,YԭFOx< pcaaT&33"T FA]ȼo ("خ]$mllp~Ę44_--۷o?|$$Mҳw܁ t֦8.ӧW^^^xC# N(*44(MjhtRX2m?~³fff 4@-Z *I{_~@cǎ>4{E%KpdaaT&66@cr +-qV)y$_nJu$k׮N[׉@]&N(}xݻw222򂃃-,,nݺeoo燼2/ y7;;Av(LGzeݹsL"#߿?- {yyFfHY 4-[իWs?daaTCn*1/S.777uxRPܶ>yʔ)A h[}}zq  zׯ/=th CCC!ܹ) ɉ2@mٰam^8{ZZZ-W^d3|74-vZSdWO;Snݺ}ү_?$;uꔲw{#0 0 2]vb~[[ۨ@VZZZ(O>˗)= 7mTfryѤsI`eJjj*݋K#:::777433{pHo={Re,X է/&M2}---[AYS/Bxʎ;hL6m wHaaFe.^Qȑ#Kc4ngg蘖::888;;KVi| nO7ҙ1cܹsqjʕV'''ZȮ]6۷ouuu֭Ki8 ^LBXXT$999)))AAA...%(ӿMC5nP>M||#*99YSS~/1pݺuȻ|JȦ?G ;5BڤFf/ 0 0Lyɋ-RxRp b>|'hͳG$::QmyKjN=8Sq100 w]tWIƍ' n+[h/^@_rӧ^^^ #'NE?j(sssa gϞjJB7nܠ UMMTz<_WB684(u?0 0 0uT ,/9qJJ ?ngg8pٮ JuttU?MptuuK/^/-[eL%O(O2eVzBOO:I5w7#"""&&FayT^z6&&& 200y(MGu:{l\-B5kH$''KMԩFaaw@Tx|qӤIMa$%,iӐFdN>)@wGJ|HKK˨?@]VV-S8ZK♢uر2BDZn*mcxb3gwTaaFhn)$!!6b_5͛7sΝ?>Sy)])8 EiPzuTO JCFFȑ#%t"ј͛7lffXbyrqq9x𠚚ZZ$hѢ:r`Xt"K<2^z˗/wss@Wq+(+V0`ޗonݺJ7nHڴicgg'SlrrӵHr;&mddRR߿? 9{,>Q{=+DpOGUV &O=aa)L:Zy<݅V\r$UqSvAY2KGhѢ-[?~ٳg6#gggGEE9;;_z^`AϞ=IHhҤɲeˠ:!ݻw秣rٹsgѬY3mmmVB3fj@^paҥ:u.5k$?Eg2d}£~IfSN<5kl۶-LMMsΑ@]xZiG:MbnݺќZ嗷؇ aa)6l 5ZB&MBƌSeqhH@VV.ZoF~R bT߭[% >\MMmСk׮Z(Ǐo>///YOڻv⧣G, t!|e/^a/_޺ukժU}QF7oޔI kii͝;WJ x#) GqPZo߾SL?ц9Zj_~/ӧ۷ogΜ)^ S3 0 0UBs2j}IlF}}}jh(dґϟ_z[ Y+ .ܱc$RxxH7woVf0 MD -LjjjڄJ Dp+**&E!Oυuz|LÇ|Go6[ MNN] ESN-]tgbQ333z$&P24/EڵKJJx2 0 0"144$m;a ׺uf&''S(?#9rݻ 5jٳ'E_w J,_*-?ydR.]" 0 0H0r*{nڮ+>R2|W~/^xuXw!~:&uԑ9jhhxرM6ihhP[nrGw+u`ȥVI&ŋ̙#FC&2FϤHKKKdfffI=N^Sb"sN27nL@i~av&3Z0 0 ÈgU>{Lbbb֭B0mheeEF5W^MOOwpp? Y ϟ?jyNrUea"t==R>O2ESSL?Gk]p/,,~֭[CP.a֬Y9?Ɍ_믿72 0 SqIII111(Krք*0}tNJw^n۶-Z`ժU~~~999^^^ 騲@Γ렠 GGG%f]]VVV"gff&L^r;_JLL200$\x(W222\rgt½aa˲eȃ~T___rZ &&&JnJfU-'77 b$&&F~=7/| ~@2 Kl߼y?ZJ[[شiӒ%K&N8bĈ~ʕ>>>b۷Pq7p@+!P5jL0v _/_,> JL3 0 T(Z1`LJ5go~ݝ&F>DGGj咓C>?~,Ғ6V+ kT~³?b >}z5k֌9rvrrrRf?tP\W___ׯ_1|?ʓ>nܸ۷omN82_WW955LmE6 46mcEWG;+K[n0 0L?g7oNkF`ԩ2Zj T+V(H*؀=z@ ̝;T`YYYW\Qh;i$" ~@8{~V{ЪcǎʎIbf _7..nmбcGvM6%ǎ#'S|3!ϔ?i0 0 qegF$'X\xEF$ \00&OwV2mۆ߸qR@.?±^|I޿I ''O3R3f z¥K%Uœ9so߮# 'BNBM6x`???|~멟hѣdEyٲejjj-[Tk֬٣Gٳgkkky{{KMlB{Æ Uؑ&jBWaL\MaۓC!3n8\}{U##CSZ*ũd@N,XP5ʗ^z999ϟNIIQPAAA&QhbxlllBCCϮ]Jfƌ הKGGG]]M69rt (~ëٳ"K~˗֓Ǵl־}{d_''sǏo׮B_@Ϛ5k۶m%EƍF.7,Ϡ C2 0 S`I:>EPxO1*=PvI`1Nh\@=e uuHPK0777)g++G"`gg0Ann.4=11ܹsg͛7Q1X?ر?!!!aΜ9%Pyhp0 0LEq(tٝwA{l/Z!B뉉LpgkkթS'?KرC {_̙34'5PSSi/=Qzueػw/7RT "͚5.aawa̜;wN8YZZڞ={ZjEjժOw囹FޕT+紺tȑن.]leprrڽ{?>ݺuKLgK-ʕ+¶- ޽+!11,W3*nׯ}yٳg_nnn^;o>\wΜ9)͛7G%xe>}Z#$E>>>rWooo6"~@fӹ[2 0 S9s&3cK!CH5lp8X䕉ЦM k&L UsU7!!vtOMKh333.ֿ|R[I _+++ʠPB||BܫW/^g$}Pڹs\\\ J-$++nnn1:tݻ@W^AcXwիWctݷof͚I;|0Uk֭@ /^tpptґ#G !+55ؘ ɏ s***v߻w_;<WL*CJ=O'j \5'䭮!.]*lpN:Ě6l( 0 0Gaƍo*䥅K uM4I|w;v_t.]TQO۶mϹВW^ݵk?O VZ6m;V5;)M C'|Ybb"ENN9R_x,F__>$$Daooo͛+ӧO9r$sG;/3:{Y3:u*չO>EeQN˖-333#FoZ ~z2U իapl߾<ޓ$DDD@:ijj>\zYH תUB7˗/Ɇ7lPclA)jΜ96l722zQlllU'A&H"ܿ9]p9݅˹zj666111...8+'ʊ&+Fc2yyyf=a1&&&~[\xM$`ZZZ w6mڠ@JOO'?4ɑL(3(..<3aX޽Ø b!|}}mmm ϕ?n޼igg Dc-O͛7ˋ/C#F?4'.Wl׮ ,i_MrAAz뎎ˉ~@o޿Ra۷o'&&ZXXzJ8FW+W[sС5j6td(4c zۛmᑟMk<B+ !C"ѣG+9ſ3\eʔ),Y ֭[ݒaa; hqhhhm` ȷlr KOOx.8ĤEkq =ĸ u5/^zjaa!*ζKII1779Eq4iwQȓ'O &U2i$缼wŇZmڴd%c"""S͂}(- )ގ#֡u֨+rrիΝ;,^Xdz(hkkklrtt_ǏWh^Bxxx{q/_޼yS,H|||PP0J>@ϟcii9|pOݺugϞ}E1 3.Ѹqcd ,xAQI tuu-6y ȕC%ДHbb@.333-ʚ4--,Q wHaa*hԩ (ӈ|FMʇ}W%-@۷oPTzc]K8.**8p ~رcx Һu7o.oYv;wޕdxMlllhg}A Z!/_SB}\cǎ"W߼yW_I.:ʨ^:mܸQY#S+2 0 S8|003fu2e p U_p5\!W\Cݻwgee%''~%&&FYb;wUW^c æMCS|ѣTŋ˴!](*M˗u۷op 0[[[ꓑA>/IdE!5k+E={p={ [kkk#:wBaa*fܹ ~ndY#RRTTԷo2 ϝ;]O7h0!!\AgϞ̔/YEHѽn: & ˤ3BwЁB;1~qzziY_ɖ\QF"_NKZ~}ሁ;v }`;Mq;aa*}7gÈxӦMS~^~7 _hf||Ap"WTT$AݝOv"$ܿyP`YɛGuРAHqnӦM¹6mJ^/%7oraa>֭[OAcӀ)OOO|^\${E0QxQpG!rGtt+W'JA5KǏ<rttU޽+2ѣG)"x` :6zڜ9shh֬ٲe tꑆCSp<>_3ruuEedQGLbK[n¹f͚dSիHгgO 0 T,Y@XGALaNlO8:L2oGٳ[>'''??ܹsbġMMM-,,ʴdS2-YCG/)۷oAeuOǽ{֮]+Y|' cO<)HV{4?ԁD__z'NB&M^xC5)),eMʀ_{h+L mFxaaӧOȑ#bcR...AAA bcc###9_ru9r$ߎ˗/6lw^YЅq]X}PϗCYZZIJ?jƌ$S @&A ЅV\5>wO?D^BǎqF3}EK(..'H=/T5rۗɾm۶KM, k,Yjtjտ$0 0L`̘1\xL^z_222_333q= pҥ[C:L6 7O>"a~B/]$&/Err2 򄓓.yYj└Z!ׯ ƛ7on߾M ||2-=@.**»w L H0aBllx,--ŋO"#2=zNb# I_)Jĉs 0ڵk,)&j0 0'Bn0zIYT1Ȍ2)o޼:`ɷp-ځ)2KPPܠP߿ogg'Lh .S%!g_zQSjHrբE3UoߚH߫W>|s???vxc_ԩgٳT ]'B~DGG?z/Y;[n9tPk׮|YÇp i;0 0 S!_>F//Je]"пU4r-tqq1Ǐ@/Ot钘QQQ <*;wǓ_4iE+:c*=}%Ee~zEnnn999"EyVVuxQ%;;2YD&|GK.E`{{C@`_jƷo޽{bZODaٳg.L￧WnڵٜѸy&<%:Y#DXXtKK2-!44ٳgx4/1tvv/1LKK;1cw մԩSuٲeb \pȐ!&cǎqea#42...`>((H"޽{F=Tz R|Y }}}w>|*T(!!Z~aa!CdbbQHVVV߾}izW~pׯ_Aڣ'߸q,ĕ4xYA}YAdQv677`ԩCm۶!?lRxM0֪UKY]v!iӸ3 0 mqK6m_VVV2 PjF޽Tnp߿k_~EdϞ= B`tZ1;^!=zvvv*@׸>~Xd- QQѣGIB݋*nlllnn斚*fk׮EEE\xxzz~~,Cᑞ^[`Aݺu%rSN۷oGeee"%ʡmKS–8K@*L₳ 4x' 0 0/L4*ёq={3֮].\,Z/D 0 |lܸQ8x,--9.U_v 6lߑJ 5sadAY;ζzⅩiYcISXXeη~KF濒  gBj]__E|x L.5 RWR5--!!!/s+ݱcGIS4lpccc)ȍ#G#FFe hn܇abb ŧp»2`>M {ꅻO?ehhCvP7bZd?>?y]v-7F2M*uΜ9$핹aWHLLLpp0zxx8>1M$Ç!! DHpww9l=JCO>urr(|79_V-uuu===ϙaaa@d]|w.?w9 9 3 0 )C>?}41qD#;wNU1~߼y/^\tIZ̠ Fe֭[uO:uȻ/B^GGG|/_C\rQL4LOOP5777xB)<'?p- ૮nÇ)> t!It邯׮]k?(LM܇a41V95ƌ3TJk֬[|y|||0b{ Rd~ ^$EEE6m"2lذw:z2ޖk֬!iQ%@j\ `*5$D!# 1`h2mƿueƜ={7s С!֠NNN܇a4Õˏ͒0nhP3E:y͵9%&0SxḿGz y~ƍrÇϟ?^$m%|g21p@tJ&B555;MBff&!^w&&&ûWgϞ-G"ɠ#N [nEّ5ͱԨQCYy{n 0 |9sÕ#Gʟ stttuu})g81Vz @P =ߔqsk׮-oR*yyy;t萋HVVVqqq<111'[u^r)~w)HrmkkJnSSHd%\aՓkjj͛7˗D)c+ԎkTm _&0 0̧+)<_jjjll9UP )++K*~s3gJxDΝqshJG2!!!44޽{!!!򨳺: (?%0"ٲe zѲeˤn۶zWf5srr=z3v-ݽ{/gϞcě7oO;5k&Z/̄ P>l{~QQQ h駟u"/YdQa4ׯ+b!1TwvvƇ+ g(p6_3tZjAl"w Fco)V!UuwTAa䵿tqS$9I3Rq wwwWA ǪCO6lW9y(\=`JJJ:uz,׌3[ŋ@ZjjjBY<Ϗ10 0H '... @-Ǐ1BM8xe%88&hHW {{>}P[H'L9e ȟ:puM6//a[[/^O Z8[Є 0 0 1Pi֬; u u=As?&)SPPu5jF׬Ys۶mbb]}x {ׯ_ѣl|˼233#VNsvv&w("ӧO+66޽{xi DU'ժUSSSӓYzӧGEEo*tu\k׮h hU/HU8D&a,7Q vyq7faS?w^rRR[zEiĘrB&dz>$ WylRz 䟍.tCk2Gza֣߿׵YXu9=4Lz\) faa!RSSΝl٢0^… ~YjN]VYzյc| ?V} L$e̙ 杼u%8s ,-_\NHH@www#dzr^>Vk><8/^2dń} ڿq O8leMҖ p.Dg̘A_iC͚5srrGVUٳhCe 0 0ƷpBjUPP`nn.RWa\cǎ <ȑ#bBҥ&&&>?TQX:<6iii$С|Ϝ9uOnܸARw"Cߺu ك iJ,UZj0V-H?cEʝ;w ȸx1cAh+W2YFYQ \Wxwg{ݘaaOtڵO2FBetuuTq LN81b;йssjkkC@8 ,b兇;;;_paӦM&MꫯK^ѣutt:b yuS͛7WX]]]7<oH`aDFFRhO7.$ä޸qcѢEdp+z/_ݩS`m۶ , FSo.u5a]&HcCCC?H U"cթFRR=*[,yzzvMIIQf@[BRpر8?L;::r7faS`$)T&!!x|kyMLLv9w\u_i`ߢE#FXȑ#V ={,-pbccc&M*ӆUNƪ˳=tPYHZZ֭[Gum!!!;w,zH~WGUl._L_) r 7 uQ~gUh+aa!C'#FLv0"͍pss4322233gggg܇FQ%%DFF~%]vIf 0/Ǐ|]e~VVV$G0TXu:tҒlDž &M͛7!ZsիF9 g$F)[l)&QQQTdvvvd A6233ϟ?Kqdڷo}aaΉ'02۷nnn5=zٳgSдil{d޽/qIMx۶mqڵqĈ$V]ӦMbСC<[??cǢb{{{Y2>Y>_PJ&:2tdݺu:|bi=YTZZ͊'&&r7faB+*K wwwccc 1~ǘ#""D]C˖-Qkkk5 T(.!<n!YzӧڹN:ȋ׸ޢE nҤʕ+q\z%sƌgφv.5=:uJ~#ѣGM&oE!ѪU+U 3+S35yde ՑfEhb_5j$M@#YٯgݻݘaaHxxg%(3—!Ɯ)))1"JLLǨc0t-BhcFFa$ 4e[񶇜_hQHHpe/^=;tP cccy{-[[ݻkNR/[b꣪ToC5j(q)$0`}-((/qYXrΝ;o 0 |DvءPx ;~ZZƍd>}_EFX… .a3(|̺6$'5^yIxB߼y3//O&W&L㣬oB"ٰaàUϟ?,f4x޽9YjЏ(,,<}%߿/gig}OG~G.\ȯ0 09AM\o[>|r>,,,66ƦT;O-Zlذ0Uׯ_#_~.]Č=LY/^PF+<2d%bڏ4nUݷlҥKC|u8 3 `kk:uT֖ܸq#2Μ9SY]"K_ϟ?z.B(O7n\v 0 0 XvrK(((GB mڴAF 0 F%~[hqQZ|ə5kΝ;*w"?DF}||֭[źuVOORe)dի[j%ժU3f555h+022¥`FiemIwww2{PfO7o}} ])=pV?@aa… x`<}GyaO{_5;Dݿz [GGg„ ccc)7Ϝ9#8//OWWwĈ3HobbxFI<ԩS/]$1 uK.. Wx[ZZnݺО"EgiE2ҷo_!?ϧ{ta={)wfaa><5khÇH:D0 SxE&M#WڵP߭[&ݦM===bOOÇSe&$$@M2ɓ*MOO722 TWN5ʒI]u(?AqThҝ;w"1c)t NGoߎSL.m۶H6p@S ,`y 0 |,KOgWqP}aC׻woתU+^ёC'''C8p@bN =Iرcӧ?^MMz9 hhhX[[*H~ɒ%ѣQq޽ /L'M$-tvZX|9}0ȑ#'O8ddd`Ȅ* bC{իB򫯾O LUZZZP111+Eokk+PHZ###uuuz ۷G !Ʌû۶mC_~E渋˄ 444ݻ7Waid ?~g D_߼yf 2qYdNxK3 0 s!_~%_zGҕ+W({fcnnN[SUW>,,lĉ$-[pɞnr/m_\\|ԩ߾}[Y(᧟~8nD.]oߎslق+WIMMEEIj^CWӧ{Zg?$:xb1֭dgϖ9N2g} 0 0ѣGS0 maŊJdYjaaNtt4$ g422"u5kՕ0`޺uKrjZGGG|`` ^NMmVSSEUrvZɑ/_ɋ+޽{bɑԩ}V2iii_T+ڍZ@3ɚ5k&J8;;sfaIcoNǭRJJ Ewo0}mh u)5/ƎK˗SS+733ĉdN4nbѣG4B!( L޼yM- ,Y$88ɓtdƍgڴi׬Y-4  (իT+y/ǏqT70 03gbBK⣯:u0LDezBCC%iBlkkkըQ K񸖦Ĺ=򪩩I&ʕ+Q-[+zsss1G?|Ϟ=zR>FFF޲eKI<;i~ӦM%^ɆMٖ|IS˻,a"DFF%2$"No7aJ Mp $ejjJ!w2OHDǎ8n)b111ѣէnݺ2ٱcdHc~6l: %t1BΝ;#YϞ=eKcayE~ ^~NL (o0 ]fSQQQd#ҥ ӒY>acc3l0ɓKs2VU6o,ǣF(zMMM~7n܈M4_===ժ믿"ŋ]vWGjժ InLf~; )4`ayJgyUq=}5bkL-"bh;vc**HD (ҋ ~q{ٻ\01:?x={g)3NNNdٳGݳ~PPϣ7 ]0 6m(+D%,#^%(+WL&zzzRb>}@&L OG $ׇ 2DXu?|2/7oޤу ;wN: )LOaE^˗/ӳSaa1k,ay󐢭c33| (in tHWWט__~C֯_ aV`BЕ+WbS˗/` `#A l|>}ӧO k\B⬡!h``|*33ؘ)2rHo@ 8STZ56zދ QqIK+.tuuқ4it[[[a<xA!wI-[K.iP`VVԳ7n8;;?yŋДܹ/^UTT*hyMC`pNKK={?^ 9I B_~AAAsJJƍ ԩSM6Id2sξrn]vxbC*Yl g4hgϞG @z,zd+_BСCm6aF%G4qKXPA}||"""`ӧ0WI\\7:uj^yI>'%% ^Y=puFt1bPF Urx+`jii 0իj/?rnݽ{w!%--ܹV[TB]غoҤҥKa&U $'N @_&Q Ga!˥ 0 0&6-WSbEȜC4.򞞞3biиH"ӧOVȁ\7-ZHKK橩j"*/_L^duzzzH.Q 2SgLۣ3 0 `#'aaaAAAʄ@BPڵkaaJ6nH߿/۷$Uϋw䏌޽{~~~.888>}&<$-w^i퐐i!322`5j 3r@N`x0i(]`G=zWQ^״mR>W@1͚5C íyaa>)))uԁѨQ#S&(`I%mw_v-SX9H?˖-k׎dذadn^ZP LZjW7h8;ULg=z: ::DFFJTo͚54!Fٶ0 0 _޿O@UٳgN Q4Ϗ&\z,0_򺺺j=#]_o߾w… ";%J8vQB" l߾]ڠ ccc---2ʕ+[[[ WZzꑷX1۶mé.])ZBʡC$H277רQo揟aa%++kРAh+J*+u4i&a>Ui*Ud.btҥK0O87.^xڴi0~awΜ9G{Lvtt,Y$ꏿ^^^jÇsΡ3g*G_sZO^zYf)\C^<X)cʕ[nDW~aa3n8*2.@R8セgpvvI/ʼO{Msɓ')))^z*_zu9"=88O>Uf |̘1 ^K.M2jժUB=Kh駇N)3rHd+]8(ca)ЅhᄈTرcȠWoڵuf1 |\Y&ytx+//M4itԩ;bĈ޽{7o޼lٲK.ݲeˡCΞ={ v44h-LQ Owo?tttWrrr\+VhѢ4t񇝝؀bN2~fgg\DnݢDr7aa h&SNǏ+͛gСp233'ȑ#b+Jխ[W---)Y$s&M:uԿcΚ5 m ]\\|}}\xz]]]vW i&@IIIT=z2#Gk?/={o߾={*UJOOExjayH={t?M>]H) 0 0o߾M1 9<7n¬!!!~r5kRϟ^zhѢu_g̘1jԨ>}o_Z*3dȐL С>>?Kx.k'jkk#q˖-a))))L=ѫW/AP‚ BCCJR e˖aTZcDDAӦMѠL2FFF...޽.͛7/^ sիW-,,o_5uÇl *V(4WZDGGy"GGG*Y쀮o߾Hiժn'~F^:𳳳ݗ.]ڤIdrȏ!Q;>ػw/7n 0 h[lIS׮]SSN__˗/o۶mڴiЎ82a&I^zpKp!C`R89mΛ72ⷀ7ǣW  1'_[OCɩ_>>,vo/URLMMeix)g׮]/:t'Ғ͛7s`a&ݻ5jHZzo &&&ݺuSn ~Æ CkC%ڵkժUvtȹd ba6m'[n]&*^? 322 nmٲw/iIs^޽{שS|gƍ“ !EQQ͛7adtpZZZ?~,}>Po('&&J\8qDr'Nܷo1p`aF>o߾%wsr&NNNЈ7"iʕ+פI/>rӸ8O1 cbbB7Df% ܹsgK.-Կ+JkV] gΜYblymm9s{?ddH~ /YDxpׄ%J ]btg /iӦHĩ&FYaٳUaa߿O>##|M߹sdF>>>0MMMmŋ(y=0111e˖DTU`ZjMQi&gg甔?qsqqu $w8pss=uԾ}6l0{>}۷:tM߿O|eL% >\|?@رcdll%ކ_jU}}}e+WYYY2k,wAqƉuč7r`aF ԼJ*AKo Pp/a L:e˖I]paڵXb]tYv-,}`oR(q:k G9<,BwMII72UHHf;Իǫ===/_kTn?~چ)̌v~VX1;;[pZj~n 0 HCJH޽5zjYa&_Ӻk2ǏSODX /III_S|6^^deeYZZuȑ#B: pX뀀U5^psΡ+_x,gϞ{)S ~oMJBD8^0 0999"H*hllѣGk[eјCBGmΧO.ZB c7XO=:: ""iMcǎo޼966Vl)Vv->{$֮] NhWB eʕEQ! C 6l0 0~~~f (( ѬǙ3g2 hÇr=i+xʔ)hT^}ժUJqIخ?|ǎ>WyEM4Eezzzo޼X"mll R֭[1cƬ[ _wAD6Þª{e˖FaHMM]`wU@Ȁ<ݽ{_/0fC? 'O=N6cN2i%˗B\uC]ha+XQP i^J)l}TT+WPϟ@ٳgӐx#>>b 0 |~?p@*UH8p!_SSS%3 h@LL M߾}[,… 9.]HzW˭[PHZgϯ<;vǃˋKؙ3g{3G)Q9s ?2h)U}Y'N [lϞ={*_;f/3RҥKgddܻws%%%ɉXGЏ+#3WIff-Z"M6566~ꕲnjj&3rȶmۖ,YR_X1z200@QEDDBKaDH9vhN6Mz='N@x^z5/_&gϞ%a O4LalHOO,(,LQKKKOOςxbʊ pttAKK )۷o(Dkee%N?r7ṋCb14%4‰CjC>Tآ?@> X"0 GAцAPL 7h/r)H,XR?da#Z3BwuI›5k˗+y5%K1B[[[na\C/ZJu?~z,^urA~\5vXtF6l8q͛7SP=bz̧KQE–nvZ,]lYܾ}[-[Ϛ5KP;afpH[.bŊCHapa"##H:˗GELl;w@e{. ~ƒ`'c R_xM9M00gݻwgΜV H_uϞ= @8vXtt4Յ  21ԹsgTСCaрW^ k廆tZs\%IIIP}||\D8;;/4mށP 3ٿo߾ׅ0aȓ^^=x䐐v k);ňSNԩS+T@\kkk:HII()SF!\Y&99V}͕g!s!v s߿ΝqƓ'Od:̄ر566600 c_hm۶}www!v 0fAT\Y _%!޽{s듕E[i0Ƭ^pի~QVݻ7CIAԩ=TneKSzbǎHڵriʕY;;<ӧOi]Ν;A@a`H@ϥѣ.4OhAZJ.]> K_N h汱P!7|||p iݻn ڣ5}}}---׭[7~ CsTVM[[{ʔ)[lCIdۿ6mdŰ5+0--), P ŵ>0 S@W^|FW}X_Tmi+tiRTeM6!Ø1ce?t?g߿^; z믴_8KN]*>fRhE}0?  S0ѨQ0'ZׯQ'z+++ooo/^#ֽ~ed[$$$'O\rԨQGE0L^b@p *LЎL;{lDDD_A8pҥ a޿?o<2򬰤G:z/ `ݺujsgnm۶Cܴ +W $$H.C1m04謝RGnիN 0VZ9b 3}s΅~,}堠 4s .-- m߫WfffB@+ί۷oO0jժb~…gFXׯyDÆ 7mSX4;ŋ5.d_2a/__͛Sߡ<9մiSMkU'8\h l7j(q!C[C%aɒ%y&MB?.۷4\-Qƍ1?~|$%%y/Qif*;;0FGG# {g #=,}wӝ;wRRR`'&&G;uqqquu!ر~;СC6jǏ=ѬH]l2]0 0ϟI]h%JߡCqmڴ f'Od!z* O߫W/ nm۶!ʼn&# 2w͚5L=Æ ÃК]]]ߏ6mHE0ҩ+W| F˔)#&MEDD߷o ?Z44El oO8 Zzz?*u2dHŅBݾ}8v z5 m⢯/AbP>SN7oDFFJQIp+W4V]+oJa/sR,x ,ٶm[!4J"{ر6lSXBPwޥKnݺ?~ŊAjC7NO( OVcB&[A'{9о}{ ^>١AŊaл̍7N[ؿ0"^ի'J*faAG~(aaa0!%ha Ŷ sN\KBݺu ?={ S-[Vx:ư7OeSB-03g0>%jŐf @2*{ӧO@jY__0 |i@a#E?aPL޵kV0˔)WfduYF rJ+++pSNmݺpΜ9SLA~Rbd.Gƭ *OƽN8!׭[FA^\8dQFIl%w[XXP̚,Awt04kLmTOOOzta)>e|(lG붵UpCGGGh=a_z BMNN`G栠 4C,///8\2ɓW^ʌ\/=L9&q9XQF`o rC;MvMgΜyA||<$~~~ !lQTVZ/_(0Uܹsa7iҤrʴb_/Yd՛6mڷo ƇU3f@ϧXkk׮]v BzvpĈ0yޱgϞ4bH7 'p!ҥKU9(@ 'N>`/_v֥+o3PNv1ذ0iiiGyٳgh&0 -,wȐ7nDDD~.͎ka߻wuz[(ø<33B|I!TؘBRk*)TNW0W M=XXXdhΝ&rUHHC\[{j za/ dt<64g({6mZ=7n\B>Xh9oAڮ]Ғ.]Z<~@Mȫ suu%^p%H߃;ȑ#f(t ر#O I&3a]xhӦM4^K4@记llla#M&<ܛ7o^~ZkttXmɟQ-ڇ;w@vI^A޹ssAYYYѾz.W>0 ݑ&  :::B|kܹs R s3 0tCLC =uԂ {jժ0/^xj՚7o޿ ;v, !Zvs~޼yYf2g̘Vq=p@ʕ+yHw!]tm۶͛TVV6mlڎ$Q޽{U(MLL9Sv|>h]:y jԨ橱f777ĔbVlkk{U;;;iiiqqq!v._L[srr` ?( AbHMIڹ}~3=aHUPHEf!q.]zALL o&ADDĝ;w"##O8!@rrrNH1C0 eBM֭>__߃矽{Uz%=_jUqՎWC7339^9< Pp8`J޷o?||XxN: >njXIž{l߾]ᔶt) OnZy]:Z>gϞ XH#и]ZC !֭[h0=zV)S@*J |}ܻw(P:s̀ n{쉋Xۿjxڵ'O\n:lrqq&0 aR_qȑstʕi20" AammݩS'ymkprW>u1jժU^姧F9HKK(bhhHNJ }ӧOSk׮lm۶UH'okg a"[h|H0 ݻhHi<|q i$|`P`y&++J'gְ_x3(NNN'N;G[jUpppauX`a…yϟ9CA6BSXLHc??D7779^A"##Ja/Z2~x!իW#3@Q,XKSuԑjii[NQՁPZ~OEDDЪ 3gΔ~qrrr5˓M!@NJJ" 6DӧOo l0/].]x"XyJ w;|G[~9K$B^ke:::ʯ ˗//0rutt/_nccYc@kz4~i`G0@=/h^](6:Ӽ. wߩa322h!.#drDRhk.JW5ifff3@_jժ5o޼|]x-X ]aqgTTǏ+00\]kƒ%Kp &G0ߔ_ksrr`Cv w$_ʼy&""v s6l Tadff2u֭00v:jԨKٳ -zAD.P˓syU'py +WD[n|ѣG0%_&Mgϖ(/[LF6Qvv~ƍH޽t4:|ptZ?tP@oT|˕+RuaBD@@b/dptt/rD/ۓʂ8 r٨Q#AdjjJ `J;;;KB ϟ?999]]]]\\^4( رc=z eU@ 1%?߿U7˃?0zʒ)ND7 ۟r 䧍 n yrk< DS*"aaph5#1c8"?C~PKH9s挮>kbݻvB9Vyϩ!*E1w4H.xQVf͚*ܼ}ۋc␟|qx =|4h`LƧpsqaEʕ+ BRm;HׯC!*WtRަ04h"gaHAqVx'Ow><<ٳVVV N!H(W- `aЌ͛75|͚5E-*K8B),W~=T3nݺ1"} Y-(lUH- a "}޽]s߾} Ŝ:x%JuFqvܙW%K*/9BFWZN2m^濽=yE} ^%@XekH\Hqr{U k7,v`>C ܴiSaǺ8 VTۣhn/љ;wG ŋGW1 @[ى'O ˗/P$/_ _vwMKKGGG_0lذ/-10 dJOIKZu]`âdFPՎ_,;u?c Rnݼ2 %ʡuJR0j(2e 7n}I׊"L:UH}k֬Jp%SڵC)LAz" S+VT/W\^p5Ƙ---j4ۡ+y0%F'rrrhP.DpnO_/Q~BqncH[[P۷=ED5L0?IxxҥK#~Q֭[w.$!BaJ߽{f>|^^a=511qttogΜIիWW0 0E+d.pD7o,XiJW;wU"ӳF\[O4Mmi3f̠!;ZE3{lh޽=z4%Ο?W3uuu;UTOiAQƍ e S___H7̇Rc{=ru 899h9rdO`aWAŋ`C744;~ӧ[ܲe K.QFM6KŽ}?\H3gwսJQm###---K*KjժE_iCJhZK3Mᑓ= #ǃ1 iiA8uM2+G5klx Fzg]^'O:uj֬YmڴQ)1eʔA3wppȯLZZin 1Ԫd}M޽[p%1wbnt W2kk+|8iceeURl\};w IΞ=vtݻwEJAAA|j Bya,gϞmll:w~מ={I (ZgW^ڳNŞɯ]/o߾jfb8gee:u[nk׮8p~x|΃NWaPU)t(QZqÆ ߟ1IjժE%/Fbݻw!Bwss۸q#m ӧ֠@}?3f Ie޽a O<څSN5k|mhhhڵ˔)#w+z{8qbҤI;v;v,ݼk .`kkKjԨ奜ֽXBm6+$Xl-{_tB([ ;w䄃 *r||Z܋/ m۶>nll CZjdâ_Q'=z4gΜ2ePKo``TWzРAT:"a NDD:qfmmm 2E &CE{p!Z' QtLLLt*AIOݻ !!!]M6թS̙320 A(ӫ|ݻ׮];9QY 1j>|pq'O+Z(2H0l0b^kHP;RCwuvvqҥ՚E~ǎ8|ƍơ<~>sL\Ə?21cPa.7n<|ŋoݺ˗FB=9\2ꩶ===d611s;aglx!6dȐO0Aa~3ggƍ>}J~_/POi*Dt;F4XP!e˖+V } ٺu+F &SSP OQdiӦID fҤIӴi͛7Φ)D`}UPADrֱ+ĉAAAIIIwޕ$y,q O֯_Ц$."yxcǎi'ǐ4PXٳg?0 vMC+[vmll +o߾:::cǎvڽ{tpp rr2㔛YXX^f zѣGw vfU=z턄V9j|u4$<>~ΝwK%:|kVVVXXعs`k* >>y0 0P|ԩ_H}233ϝ;t>}@F wޭr\֭Q{hтJo?~8jť E0{ 0h@ $@OQij(Ǐ'}ׯ_S 'N$/ .tl` gPP-Æ :o<"РA$hlGSQ0""P/Jkk ɗi^#y 2 #q(vzL*JwCHF??{]zw >++kժU#ڵky0 0 *+qqvvb+ ]zʕrֽCx ƍL2+#xS&::&4zh{x͛7aMSK[[[333tGW^}I}ʛRLw'OBרQ׸ 0W̉'H.,Gv59.\Nз*m۶2*͇&MȬ*Li0`ʳƍc Դ@hm@```~ٳGN3FHN~79Wsb?&_zC)Z tTv͛7WXr_Gի' 9o:u*`hJժU_B-vW^~LLfO'}^μXz dggܹ N6-5ܮ\Blll bn7o޼|2`(NBAC-p?A`B։#"##۫W/@+Wn̘1gϞ`0 հuVT;LHjM{I))ѣ >pB_r'8@Sfff֟pvviu{%?--M~a, +*н{0BHWw[l'œZ>k,>/Aa EgΜcÇK )߆ A:"vl! f駟$7\+ҧu̸V<^%P!7mxe̙3 è%00)Lf8H(/^TCH{ĉ ^Ǵߟzr5 HEbii }0۷/**?*a8rt1r:DاГџ;w65zck֬yfya \ {8j9>o)PX%wZ bʕea]HФI*2rZp%VXl:::Bʳgϖ/_.%nݺE"vm-[' q3j!G#HI. {Y ]EA122Txɽf[n?ȒD͚5qSh!tla>bC=ҥK޽[8ݻwwܹ|2uRcnSN@*R:i4<{laѦMk >0 sq*f߻w/TbXݻwY]v2dȔ)S/^ w֭O< SEdȓ:DU.\8}t(]t颥{){NxCǎezB'у˹ߖ*U i*U/^H<6mڄITE̙3GYYYƍwEG?Ο?͛Ψm_˨QӧOrR @)':3ZmۊWWZ{_nv@ttxjɒ%)|+Ci?ĉѮakhMab5kFW^-q%dzxxܾ};>>Z1n$tAAA_kD޳gO }=!hazKZf 08fff`f )w=n_b{-y+cVyi}^ VVVsЬ\`x吩 ^3g޿]|*UJp/,iEI܅惫W Ϟ=ϯ_毝fbxKLQÆNj֬Y/LMMQ2)1~($55:::ˆYѢEuuu---eu!F#]%BP*15k2dȾ} 3:%KА]5._J>|.!!Ay?6aoSN/0A~TGDDDtM#@$CQ &5Sv „ y76lۅ^888~˜Î;zꅃ+ 4jHF4 !{FvF˗S:uuV뒤 K)%`qŽ.^x51N%Mvڙ(ĨRIZZѠAz=o0 `jjW7|pQ^=Ɏ+o-[0x`W|xhܦM={V â9sآ/a)yǎz.… ;wF,͘._yZjw8p yqwww/XXX(orILLttt\z5ma[_6n(sĀal߾֞+WVWWW4W > ‹/6\t xTٽҰ|==22}+.ܳgٳS~}qc'V:n833ڛ`ѷo^!(0 sY2;{ȕ鼷oBEG竰vN ۷/U9Z<_OO[ sڵñ[94)EN'WD{;wj ŋi|+mmm刊G^qa=ܛ7o~JB|OՠW% vвuzUq|񼊝3g>}t ! c2DPK, q+͍Gн{w}Bǀj ? Kz[nedd=ZXX(  6l֬S%Jܹ3/ ;v(0baQ5ȓvݻwCٳΠA&M4sLO,\pzҥN.cǎ%_RȠV6{>VVVҗ/^?MB X;v #9MOO Sj5 g#N OS`Ĉˆ'=Nϵn:[^PxȖ+V / V̀anݺNu@}!zuHIIAM ^{*gkkkdd4dA473auq᪹sJ yvRu_({ eB,Ǒ#G+m^]]({왥% ^pAB(Sj-Z{qpp@ Cbы7lؐ-za&/УQt.]B _~k֬9|pp|V999n Fe׮]͛;M)hPw.dz.ߵk-m5xxx 4H@SѣGIpl29_xAv``1p@}`nNwm>iӦ "`XbyҥK7ϨJ)}$ hq9fm`lcߙ#TJZ35 dXB)JQ *T,1qrs\:s^s^@)'pB1{ET"}^z%ǜ`cc?bpe"~ Ѹ C:SӧO1e1\zCspkW^}D |#&(L;;;?P/]Cz4!tcǎ壋C>ڵkoܸXg uÆ  ӧC|Rkhח`j9nA^~ׯ_׍7iŋ]vݛ9sPyNN =< 3e"wduʣǜ98$D!b-.\(,ÈݳgPޤۣA{{`GFFN6VZ” 5;8&+P+11qӦM_y" ELB+:88k׮,Ke *!O=%!}6ٽ{FGG+&0a*F "s5j| )88888N?~\:S)!_d:H}9 m̘1>|600`kמ;wnLLL)w)// %,wܥc SL3 5'N`_>@nccC,mF۶m+Rˠ"^&U۹gffRSS ) ʕ+W/_&_yo߾=nѣGi8p I&= .ˇT.aÆ֥K_b)?!Ƿ'Lr! nݺW!00՟>}zYy3#/YD$t & 33cǎ Iqvvɓ'$$5dǭ[ɅshB4<<]oDɡCdc`fƍ{-eF:%''/\ouZJ-"77U &W~m'0x{>uDe˖!X;IV:5\x1to 0 r+ڙ3gׯ5jԩhI9D@ t? >rݻwt֬Y2M322.WDȑ#C aGT}BA\D P ڵk7o~ |}}ـLkB0ЪU+>9I%;$ȈV=&۶mSƁE5y3!!!??۷_UT!АfJ |qpppp#_>k.vGOe Te-[2Cww[ 8?qě:u*?w\Ã͋_|aKKKj:+i5,㭁meeEqqqt)t$+V4mV%L6Ma9$=zDfҥGAԯ_?7n@j@eZ޽~" ˗C(>\^`j輮.yŒMz*)) )d߾}Q;0'W,>y$MuCl>},bز}+7=Cbbk ~~~PxJ"jʌ~ҤIBFiLyYIpp0hȯGGGc}❒WSLA?LHHtđdMށuzz:s^hD*LAPЖ>}-7n<++ А6jժ% Mpv'~w0hAgRӶm[Ih LS?Aݻ'ӃȡFyƌfݻK,FͧN81rH e11fdd@<@S0/zL2Ht-E@@?(~Ybx˗J5}}v``  cǎ}%x'2F%̙3LD'`M6\X ۷~̙ cƌ!%[Pgر?k,\UU~Ξ=IJ2(-ׯ_q(oѢسDEEAr\" xuXd:*"(p焃C8PإK*S0gΜ͛ 7:t耵*񸸸/^z,8,,_ ۷ٳgW^jϞ=|qppppH… /^dggc5~'*3˗_ؽ@ZOLLsTto,=2R:L@g 7(ƓG^HӦMxb2KkӦ{hRAΡ<0tLB\*TTEm ȈF2hSvq뻻kgddTt5'> *`S@.dr}||8'j'ek(UllUȲbL8˫񑑑ǎ'`,@=TrB:u*5k]҈[ubʕѣe Rw޽{e߾}7k֌G;wЉH<HO>ۉ(+CHPB=P>9ׯc /^7okpjii c婅ISXMMMDOOD5Ϝ9CYhRGa>}Zf{ӱclJ)ցe2\p99ZhaccQU2f0U0nܸ#G[ӎ;( ]҈_y?7n(3zaa!{oҤIxxDr8qڵktRAf֭7o3PyLYٳg⽥kҲJ*O(SDn:VB!W./0#E LJ-sCg͚|*ȋ{8͛uttf̘<ś\~7ggSN 3`rsscccD/^JpAf(T y|mܸ1ue 7n >%UF SG6m`(W\nݰf2>~xiP e6uT333>)!X@_h]WOڦӓXLEX ?Evӧ>| [M61JP:񮲤xhwPoee5qDa.]HΖ5k֠rY < kCɉ07,,,,--)j… E7(6aÆd]GwG?~\X O{jj̠Of?MŋdGeiРZ2Wfddxzz7#ڴi)b˖-5EѣBFO=JΐLLLھz}+Ebo>2`c%>>>D+W"oRXvjOgݻw'+7o+&0bZC333A(ѣGŻJ={$;jժϨQ_h7n VH)p# Ql8(I~Gpab333CT/GzzzJclK<`@@@aa!χ+LVŽiXP0?'|_K;PnDVNuჍ!//~M椤;v`V *k̙0M۶蕵5cZZZ3,LtY>88888$pu "A%&ƆnݺQlj BLjXOQ4۷~sss2syMFg"~GGz֭f)ܹS >_R۵k'~6qQ|@:tIN8AO@8p@ڵksMMMzxȢrg1-˕+v/pgΜAaPPгӈWJJJNN%N233T~~K: ={$[n BS4SaQpemB=<~~~jff3f'*3]bРA=Y4i҄ {رcOO>rq+u֑ jAAAPQFaB(&zmmmhW50yaܗH~  >OO'hnnn͛7+㧟~v%[>8$ȡB AAA`ֶþbASoh;p]vM2slOʕ+wzժU*j}!޽keeE;/q/9! 9;;+ @)x5PB^hш#zQV4j֬ٸq_gϞzzzXбN6 +-[ك^ݿŋlSA35~H\ Ϟ=tnTvuu 3oʕM:t?Na._,h/\eq_&LOsQjРK'xo"? 86rp`<ӘdgRdʅUU߇<^@ԒΝ;` -Z`3hKrssO> b IMM5=<<2220eL81ʼ;?(ۀtR2G&;ڡCB+lBhQLKÇ-[`\ofΜqƣGbOOOvHLLd Jv...:::xj=%qppppHX#*UJBB-cKjժ:t4hVLTmyS XjԩH.`ll,sݧOK.WhtOJUz¯('=AI!_|A X1@}t`gJBBERkK2?@'ś`|s)Q+8,6砫nnnI5^aw;> !>jcDŽw'۷P:l%o޼q>=n6>|@:0/q.tFD"dGVVh~rr۷)>f'NHd5i$2IF:|qa4Q(d8=t'''|-R|΢Wo޼yРA:tP_pAɻ\qǓҥKezrppppp@^;udfftΝRN%ŋߏMYhWccc$L2E.D%A)f/sĬ&ׯj.[@M)V<;|Pc?xd*T@Zɓa2"+BR`` t]a!%GKKKܒV|z8 kd ­/|2:ayZZ&@۷o'L Ryh阂@;$6 Իwӧ)~ceyyyӮ|xZ⡨5 Ӌ .55tu9###&&󀇇3Vc҄cjjd;X1UAM%''Sd?~<N,իW_rݦȌ+Шƍ{) dee`zrJ0ϋ-g]]]B'J.L_Eǻק (ԭ[7"\!Cdggdu <'o} [S^?88bϞ=%/^LP}?k,y]]>==:6۾t^yFSaa!͛7333_.S; E!',\H;T0({077.*RRRIp 5Qrcƌ`EѣF˗|,qppppH`ܹ߄ٳ544KIׯ?<"uvIHxDիW?`+VdN[^= CR_~E*A C kvvȑ#}jՠ~C,Tzv#HjYo߾sa9ܹs P$󧥥[PP0qDRMLLC3֖0g /uKzE-13О.zB1KiӦ*tؑBSʊb%tB*Kri$4i"3"/ő iF322-[ֳgO>9I' X9

xTrۥ}h~I5]-~*<ؓ?u?u) vN0AЙ'sдLѼ jg):ai=I&7^?|Tz?z(>l! Sd@~lH^M^=DUy#څgiOU%-iN_`#̣O->lhŤ8Lv@? 4b9`MB?\ _f Fb9 -n F8G5\{o6=udM^l`o]S4$B=E ;H EelOµy "rJ?uQ5{( \S.g)JJHj+VpHwCQpX RjX\J%KOQJu7Kٷ34^F;#sƝb1tZhR\$7,ɖR& .T˸V-ze΃h2[&b#?;,|^@ z9퀻@_}Hv2|3xpM`nyw|Hx)yo #[4XUHyHHrEifj±ں":wy7҅DY9SF!ﴘ@dhVMXU[.{HsE\&yB)v[Z("=`MXifF|3 ")& S6A*49X#s@O=R;]uo;re ~siR[Ze4)brVhy0(DM+jha֦`4$!xe*+66 RߢgLA =^|u[c0x-n?$V~_խԢo78]N_vp$dž$cؔ4q0n%l|dV&+ZUKVެr9` To|Šv-Yiw1tb,* #U\ m`91P~[-N\?c>fzgFJ(Iu$Du@At&yR, y:`_AP܍.j*&/ppp%Je*~/Vo>/ᩀ dT\6~͕ '" xQ>fI2[ T/E*':@*YyJzCIeޘ (w'3fﰋ|G_b)tͰ$+Qs[ؓaEg(+_F/%UáЕEF 9]X)'$FTbE$_*?R-dv\&0\.CE/(xV(nx/i8rL 1 Z %31/W(.^H[1?^(JuЪEKS z:=xbDh@8VKS;x4`kE pk[ϻ8Z*Ve9X0˂am>FUӤȹ$Ui810P.i/xyat`/t?`y},\v_V;My\KS $}]vƃ<["c0lSLNJuy !s1(LXo 6A$B[NE(9#-$R %Sx.&+2ɿ43byKIqIxOH{JeYYԢMcYuӵG^g%/ _:0Yz'@HY-Wuc7rcI^p ϒ/΃=l8rF:6t ,i { [8(aҭaCY4 X\8>Ǟ',o}X|T%C[isXbZ~"i/!Ʃm5-jN __"J$_ knؤ;)GP%'!( NcS9YJʸPl&"RtW?6ӯ\6%3>QWhc#o/1|o@BpW:S=m9}su?(dq?g"A8åc,(q$j{%PuQj][(0PG`ZDzmezIa&<3nN\ f,GMۜUY7o~lBs?([ 07/Z8[udG,RaA]i0TjuȠh䪍3g|JezM~**O&ĘD,F؈&E$TPnX$ol?縞AĜ;ww[vS H_{gWngeg"tUge* t<"e0/o E$8Yٷ&"V韢?)Oin-X3ʺ,6vυ Wa{[Ek[+Qe\W>7zI9e#8.Aߟ?'foy`i3 ]L31Ҫ GȞv8h#^|m /f]6$XW0T:ʵĉ /DEq׈PfDZ=)Rvh_ wIa*ZIYR;Or ھQ #ImDu x&"bʁί 'oSϴ$n7ND`>VͺSy"|-g#͛o/.D``fY9̽(C=eg0{9`gʸ?;OT,,C5K[d9r!l0s"?ӰkA^9lnz S8MVw, l&*[6| Ę1o^_ |XHi~>~q2h0ku݄/Pe"FbWF#90MrYPReƢx)Ase,4'(NH1ѕ\!O4KS;{UP=luzVSDӴ1oPoɊT:]QL"*u,<vx;'r9) =epro} NN g[\߉E[vNPBPp^WV*L]TVoe}ykbEY{FS4{?O ,Bv/XYԼ-&5|;34#Γ=7o0Os,y *'eNIJj ?-e(ZxT?u +_'OZ'ɋBPs1s ;OL2TnFK-fz `|Pp~xn9zX_ ,x&^0EE=&BYDǬ:.jZ @n'0cQ?$Q>`xf)UwP4F2'CSܕE 7q-,r(5!&OqANf{)&"&jh1F~4@DmD?L)Rb{jupܙ[ 6t=E p?j |Y8U7Iӧb`UwjZwZ-#2E=X-}^ kFcR5Z,ol__χ2:0tƉ8:9j{N#>v@S~J(U̍Nfh,-}-F#{7Bz}/?0G\[QR[66-]~{88}"=<b2:*䔞 tr}HH $Dؓdl#hܔHLH0-yo&yDs`{ p_B@f\"2/ͽ&}g1_pςsL(I+ {{g2?S]Oh@^N{8Dz$b ge,DGJJ(LV"Hyܕ 2k(q(LnnY̓2) HM;x+80Y`ZkѤUvc*UVm]$eϫV>n}̢4pѺk1QRuzE哊Ce]F++Kck6G$$%٪/FK kkG'-OK_14]]Q^#1eQ$l_$/Nw[L0㰊؍5[K?Z:,-JpN"ʹy>J<,S wYG(Ni0B= H O P:)9!~?QgÚ@zXGpZ``uNПG.- _p w"G}p^\t;ƾht&@j&'ʰd&ED1 ."K֎P]f\4??~w?lWm-ǟshEm[ lġ㢜MZ ҉d2iG}4㫬|R40͎1LWV<91#vLՉ1uZ7rfZWYCRhg:㙮yΡ2s&Q8ROJ~~NRb?!κ4h1Sٳ?* tft~dlDfh r:āҮhXcx_^bY,R)ʰ&.^Ir_L)cO )CfHHCԑsXDѨgtbROTV=űDD+r `tK`$l'ޅMDf%<5ʞj4/@U갿Pq oo=<o ,vil0oyOW_SR!VqBHSvYhB] O9 塕TiWLOgʩUlreRC=V/K}0x,F?0rMx}g#M|MFYZ,L4pU8$G(F|0 /,-x 5ڞx:NܡkC/},-|#xma7^\ ΘCp̖8ْEE jǞy浻\nO  6 e6 C 8WNZ5-˾kqM&˘2q^eu8]piQ tRh}x̤HPlw 1jOgO v.< yD?/죭z!C|r#k3VJ:rO&z$x[X'XtAT$,,D׭7ݍU#deHwiAU{Z=u`c)m&sP'7BA@p])A.S:T7p p\JuXԓEufxPo`c=yhl 7ѿ3Y2/^iZ@F}oN-J:ݴ7 +{&ӭ%:aknklcF-ǖdw(n'D.D"W JDylYRUBFId|̗`g!e/2^|[=;}QE,FݏQrrޝ.8AYYDRj#~숳/EYh~|VNL籈Yt\Z,U"yLhD8|+a;& !jR$Yf]I 14E59 jm){IC :=ޖ* ;m`+-I8R-p8/@ ^ ,?a_a=dYF)Ym{H2(9>L'ҕKJH,mC5Q MσNX燧~:?W=jɼUС0MYD? owrq%%K.eYSuRB'I#páDOH\,w+"!Ks =09ܐ 7I(8 }-\!58#"̨ zf4N`gk XD=tCf2gB"Tגf[9?8y/2+Ww~r_i55/`ե⮇!b;VBGG"1|Vb_q8mKn.3*N (RLp(xKgN`y*;K/RYǧ|T5e2 ,P֓vP6u(>b/w,^DBG\ҼUSc5?l>nh$_)GxMJ0%zIS4Ŕ:\zh~+-L|@\(̎ЛJV}lD;Wv,jZ#xIb[_PwiJ;/U"k1܅1 Axt̛%\96- r33ovc_\IT6ȿ{R85D)'cZu_^΋g͸hEpyaѪLh jE§;k.9ٷpkV7gU<#/A/Ŀlv7! 'i jziHE#+ &%#)/w.TRSIck.۫,$ɜ!+ݻj= t-%d@Ǒ$MVvj6HX lT?7p,vRnpdB%y︜V):bSrbuV֋pSV&;m)Smj{orLGA"J8Um_]DzF=#{0r+q%Re1MI.bo bra_h֍V,cIS*_ʸōhf V5sbQ9~P_PP '*ip$?Q,&3}Nd)X:$T%Z1)d|XbWWb8C@"\BC3h쀟.@؈˭v̡UWMLx,[b=MXOD L=ڀùhOj3_,?%ʫNM 2@J(=a9/+fߘV-CQazسDZرD, Ѯ %DS᪄LT2CizY\^Ի>꾧aCYhU:9"/{7u{,fP%H|ނ8'p3L>h(>5N3`LéGSksn~u*<ʴ`3:qؤ׊&μ6F#E=<b*.׏t e KђdWfd̸{+mãKK7ŋxFVo|;"cᑧoͿ=K~r`第V1j׿4F!x}߃7]oxg^Wm\ƫ5$<|@/Yo:GBJzSfAq׭Grvƽ83JD1q< ,Ԫ>rclɽSY9oeTKU=ak! JK5X?GjT;w[Lٗn1u lu}``} ll0,۫vƽavU8'Gx*/}HsP~TDFq(W ǮQ/7%VJ_r }DL l?c؂ zPh|D{ G,$cIpa6m‰y7 T4o"yPpKZEAq8_)1(V>/7h`7 $w|!F2vMm-PrkU>;~]:7}xhǯiZ?U}aFSue(Pn~!=o݂W+oo,@eGyӧ׶yԷC֞&{z]gիz߫VO^FmOw3yϖ8g2}x fwVOwPf2G-6 mMUAD2#11')؉̐dAXJC3G(*\A>q-G"{c4;PMqN=#&|GqP[E=47DU٦$R=K,|vȽ-${TG'm%I_cDztT6&xr8 y6fxft؁9쥒 1I{%k.PڏPz`*[tRpHa4 D}H9Cʆ&*CCS< G=9Q#у\TW/8K5y~\fg[o9+A'JIokZv.baI #eW>{7fV4>$efGR3?eI ܮ*<g Dե]@S0pX̦rq!YDOa5ўG?RFKry<7Ng'Ypm6\I#!ӡu&`8x(i'?PGL&-H0B#(NR16IR"2o' 涉|QI&.җ ރbLK7+*ƄرVۧ*~ 7yym"NV]zip9kW՛eT̸ΌuAW󊘑KII\$"K fg}*(Y4f1NT܁OZTϫBo^.׸w 0 zj): sb'uC9Yw,?VWQ߈b4wԟ]F(hpX^{> W;FUyT72nwHh˫enmݟ~[c_4)nJvNxrW]7v{-CƵz͐IR|_5P5jƽm\pdɹ~&N;%\}A" C$ ^?ȯwR|Aǧ𩌩tTLڶFk]7VitQ)ZLUJje0m)JF ݪu:s8'쌛κ-*}^-0`ܬV]+:22nh4#$UIanigNJ2:˸Y O;/KpBr#Z%a*o1yuN f=u?UMVaāxDEmQfuIT+ES',~QNg]* D "jk y'bX=Y ,Ea D) N^'Nծ $~1BA W̵n2ӱkݫr,]pU0VW vAz"\$thzJ|O1r@8*r= X[ml|A;cH"GU1z#8$҈L~MI }!\Ts}T[R_*$԰۽_$8lj|M=WK_u>Mk> 0ACkq?}/fn|æV;} `j ;NHw# T-}v3*݌TwT-Q@@_,pB>k_=blZz|u'h뀩=~/h?[[ЦӹԈR lދ UcoQxˍMVӌϿU_f &JY,_55V ZE]횏'qlpZ Vzz"?DE! [*-OTI[jH.CR?\$GDi'MQ43yltW倣1\zO,DkuU%EDXuΩãUQr:2%)ch޸xT}2h=xؾ̥=x)-սE64uvm4Q*ڨ=yuC&A?O4Pa<WAsk_uS+Lk,l'd.@ ȩ)%AQa>'zc0"YvʞZʺUT a0khy.Xӆ {R`Y7dЍ>4LdBaq"'>ަHFC*ed4F+rۃSAf4‚ E= V J5PٙXw/Ik+sZ+7$+eO vRJGpƽ$ЊYK,U $K^nrRJ6hXۏI#vDa͡Dy_sLH)&bRs 8EBtU{L1&%N UeZ`RTY#AG9答Bı 97}85iFu Xb*Qq,DįlxbV[UXK4*`xlac^0xU"An*`aw:Lz hlvaTQbV*h;]K)?hRp8\0d!U`')z%>+o~{p ˌYz\E\1$5q$E tRL() R~\ TKry2;E`BKnY^ B!%rx`EnO$LjV-WD&ļhnOK`ϩui.Q}?#RU/ "QmjU/w]KDQ=1) x'b P'rDeggyym;&O!.H$w0b#V+6N杬+E{˕-ZV}珫 J c cZ 0<,މ2.64`F݇7x;aʡW|^@x] e֥[G\yq֝?.ϼc'5s?i$tWẗ́C5K>P{i4h|ë07nL^˸6{K}EnXN+R ?I U]}HKF%0kR3ejj(lh%f[\$-GPc.)MY\Z26 qZ(~#E5l ),.i4S4>`Ot"1yB'S&ҡbLT}pLJ]r.pPJN*G0'DQ*C @,ɜV\5Ưy9*ud2OO]>$p7a%z K*06H@VPpXk>T%/$o dy&"|L I4iצMфAZUDRbHJ;}rMaNcztg(!f5~\ ,jWnl:":Ygk5D[D<kȮ<_ c-FPgWtT& YLjp~Ge8auxRx (i5HVf6'anjl: F 6>II ЁvLei ժd#;S]]]DS0dmkp:p141(9k =& ~"c'ܡ^!Klz7}'˥[ÞǴsh; I%$j.4 lEq+PpBa)b4Jފυ4Ön6޲1[_A{ zH{k 5 nƣ'!RsF"2OPQɪhD)ڹݾ^Cӵ\Y9I U UTOVCZnkɸpCq B.W灴)mꁨLun#,|Lx \ͅOӍ Exzk,(t8گ$L/MPͦ}\2q! FB!mj|l^|p.@~ wjSa! j.yC DyghN C}MJuP)<+xL0zt@:fi#x/ Eq7fK)s(xbeeC%E O]ِN;"N [7Xe7u.U7IR0_hHL': >[ǯ99w\v;ƂB\E ~+8 (ZLDF,:6ymz~;xJexB8InnPS)ފ|[,Fc'_^D4qcd09n8ۿy#3zh(~y/_./ߏb^U嶚Yf#KrjB~ FlZsh+,A[A_fv/^f2Qb0e46kC_C?"Hꟈb$01ko|O@N| Z,6)w]e7% lgp|&֭D`{<<!vi*h4UۉGMD>cUrSN1/㙐T lsO#EV8IJ5e9/Dw*O) )!@kk@| Nc6y7]%`BtU< Г-}fI00v<)ʵLy&TpqQ=Xw*vvf``0,p*M;S Lݺ &/кQ)ҳAVظ!W3c`YqGMME,rׇ ;@39oP #RM.VPOc,?sb'J&:%\~niӓ"! vBC>+Ͽ#L$6 beK0~ t~*iM'l6 !) fxcaJ#N &-+IlR-u Kl@{GH,d2OS g+a{qQ'%ŤR=eqѨfӫ] fe³|aEdu; Qr^ CDj()b6tuy'<)c#n,4"}OZތD6#4ٍF#hC<R9vo#IV&q'Q#)A?ȅKZ)QpWoTU9}OHiV(\ .f_F"Ɍ;"-3$3@z6hjQr+|^`6OjN_8.h3o& R$*y;+.yiTك6 |*%=;4䙗s= -6ܟa8s)N3PO#+&k^k!%pyj){H G:hRo3\KL.@LƓٹ/7m'!((` SbBdBf*t]Yb'1b, &qo,յwh!4Cl<%ʜ䏽*.4&XWeKx\:14ym}@t /5`D8~t,*J3NV6@#k֬bRZjq/·wCL^G猎_Wx/Jb-jkV(-ad$΢$J͠G?4Jgnl7h?Q)\w͊x4f_|z4&ENUf="3r֟[_ASEE]K5n*,j%VU f㴽rZu8]/)/TB=[jjh4' w9 `qqQW-%y.و +6%9rUe?Rr[|dyYT Ċ1= O4Iz${` JN c8JAaORh/zqEBT< N$R?#ˬ,O$E**Ud]ʃ~<ۃJVGo2#r@(#7LOJSI̋?r^#sBҲm_ )DX,Dt)Ի)(NRaNBPٯL?4jx>B$MI /\r z.QSl84C@>EnfgvMB))겘YGobE׳ZfPHFġ]T$Cq"!k#̷4*Qpi Y=_Wk,NM82U[ʿ(<2D~.p6!&Ǹ~p/8/EvZ8DZh\khpp_TPZmF'?I$\?~+T܃Uo/( xДk8n0F)(N#+b~ K²r*WD[RP}jBBFhL̲2^NξCb1ځ"";4uHשbB"P /N&(:zb<{mc4uu-Q@ (P/.X& ;vmf 'Cor[-V˽/EݰiRnT1~uq#p]u,)]5D.PvN+6nbX MOC%ʒ ̋7XɢqNwgua[/,iWƀ&&-UsCK9_6o|x*')FP;wgii/,0bYLΆ[ZʶvhW+{0^* FƚvifMa|DPٓ:dm [ԴiN\׭clUĠ\Vu rQ=0ل` 0uÊVT>biLM-ŗ026ثy(ھPTY] hyX0i4\S6*1BC!ju>·ViZSžJUf<˼CviQR@M HS̿]Pp~D`aS)(c^=g6O(4~\|DŃ'YV6+Ű(HCjsQZ%7:0\I|Sqx,"gWU}zݠr'NE#_bɐ&b)|. EJ"Q}[K0 90K'M{qN WgDZ883Qw~& g/=铯$q!YiiN0qS_>>kAc2-o|@^df\gsb*RIJH #K0MHr@CZ2 ) hr0BY:f>mlxvSŁ&TrDz Iɱⅹ͓2q_&()O/(??&`$˴ps[ vbc#R㦔tm88kTSMX=ɿHMz%J'5"%e3N9'NN#O.@r*\mlx"ƧŨ fIΌ|I7;T/,vbb"I_}1%QoTV`~$5±?JS.)zhҍ:oxbr׮SIùt.W;pI&%i.)GuSYolzp֊;Vk+& -LL՜!̲ؐD׭ 41 ;W#w #'C/')+}L\Y"QIN~RϗyUa)0#,3l@<.nGgrT0UX-$n~03/ 6gϼ#h;\z7m Cjnz-(h'a" n@ '2ym2bX2zXZSb0:Þc#CV,YK>0kėmi6Mۃ{fb#!t.h؈}hpٞ ,V>qܭ+pj08ڬ3Z K[6W ؏ ۇr.k" (R5\? 4Kw^(R, '.תIjd/юͳ%nz@o*tլWr}v8- \EOfN*׊:w>D3뷊ÖmلR=RStf(U#:Tvڹlg(J.u+uS:J2=系.sEp>5!90.g텣0 j)Hh/,Z310ItA2{ԳO-r)`01K$5 \P[A`#6I''G;0ɲ鵾-)@%-Zuߥ̟0+h|}Ö+7GW} ,7{$ ? 'v.'v$Z;-ݖ8v ^Kq 8ˇWwJtf,0Nt/|Y"Dq%-]:mt%{%viſ\^. |#=4ucnZ:Z$x[,x UłxRS.2X>uLZ)2OE8\EU)-/Þ~6 )}Ezd.DDvZTڋ..FNF :H6_'4؊K,#\8?]dbB-5Ф@Zr!L?IU⇂፟< #J)k@ZN"$RhA IO~ %DZPJ2ˌHfp LPpÐ?ߜ[/8#jH-,#Wү֍7?څg.Le_=זY^a0ͼr^^^/k゙|sd|?{Wo8owzN^qٌ[مg.ityEEF̟~3|Yy¾9.X(I MR2@vBMGEwjC`p7]~6 \$>f1"|*OW%qzҙ蕒Fp2Er/@zYkVᴢ-yzv\%|T.2|zzm ^ 9/RS*ɲki"Fx=RW+S/N$).i|Yߋ5H?Ϥ y4Q ?GO%RV?-lGҪdԅ(wHBd>Ӥa/@9A^=z0;( DZ\H:47O;] %nlKa]#P"i>֬P Wq78z|jf Nn@/)} öuګD\#;f3Qu rJ$WM^ޝʪQՎ* J"M_^?ThW᪓4Knmkhv!ʉÖi>i`>/-=doiû$s t^yBW6p9mYh?ZߔqC-m1)xQ9٣?Ʊkr?\0p-'5?^0}Y⍣XˇWcy;U,?/(z#i}P\~N|ھ*)+ݑHr.ܹwLZ̀0ABY{FE hԱe3'vj33oLSb|Zf(NV+X*GKqfymN?¼yK\pf#Ocz݈b^@U4.FD)m`LO") uvu(bK՗WD"ՎKcn$)rډ+b0l\R l0jOQW([[R"l;F9y C0Xi)1w*:_y_U4m_xCϤ.ohK )9636A yC92o!SW+j9٭ O#ܦR[h HL*۸xzBtҥB d29zDeqU] :m?;>vd"ҳke SaQviO`x'8d:ML}pKP28cR4EHw*K{/}>&p j0*;0ԌN7XAqq:&Y@Y蒂2;襸++N?t{%#gM,TRcq탔ޓ %/2ΦHHCHG)T̩.é&,[H4_ -72Lm(=U}hν%Ai{9Y%B=LLxy$}{KP:SP 70q@Y$7*VШΞ}P6/t ~IOJ̭r$ixOPDdqkIf( ^; ZfEШw7 %QP%UJH?dC ʪzTrmCC#MIokGmLA^k N(]CY@h$Э9&dM z TUmcGo菚hh\n mpDEdq ob;I91  tUG/ZX섮G'<7EQ7(;\}ʡCy-T-?ĺP?,k~f|HT7$v]G7U4<3FvuCC{d|躼J:y{8#gk\~?Zլ%_x5㓅}I?Yp1{W?"/&{{?{оV=rY%A`xdv؊7}1zRDB)rXB%"``+*Okj|yAX[>?Gv7îQ%uGIӹ|+`a*1]3/G~YثԽbR#:pwΉuHZ SpZO(flS .R]vHM8a>}$e'!L=F) ( ,bE֯'HASj\v"u." ?+pU,'4aj6Xkf)A= `4٬` T NiD/NI{X_Z6R*G|fR"ED&I쨾v}0KE>O.XJD9p"Ͼ0>=,ۑA*zMK>[;s=R?c@zʑC;).,SWkthlhfnUGV"rYfRmG[;L$+l %-ԸWҖ׿׭3<)H"Z j /^ ~-Qn˞ xN21ܲZ+^I-T)n'q;I8[hx\Xiilx*Q.ֽ"c'y/@^ϒJ8ZhB@$luW@\/4ljx4gKKz)0l2ay[w?v~ ~ ~|0ڞ/Vu}IemWJ5 ~Yݿ;/4~5Zү5,~)0AmK?;?I|S,0q϶Xm_lj|׏3nP#s`x?q:"@ƦM"g5RX* ./~SK b!aif,=yc.r"˂tgR 8;A`^b&c!uJ,r(N~!L)$M Fʔ>TZ9}$b(3GEQ|9TH)'h! %5' 5ς4:,WvXX0k=c9uK+<"L^Ayq?z0^VOOP){ xjqR!b5"&6RSLQ8 농 p~a gjkԥ4rIhhIq7|cl֩.qh[>y i("jwYV]p'L Q~+iA)k=IKĄ2)"r&[]!= 8qxgϼ ][Tt(3fq=LjxVnj!/ +H[M( EHR-3T}$\hTeoiFZ̀=}|-Ԥs'SyI+Ţe ?Ke bfLCOUL3ػ$41 ]i?\N3vBIvptWJ$#I!dzdEX^螰<4"u}͌;8uMx2Ӥ0n6[x(̓$\h0d;4 hrkM0u_n&4)CpJ!aL CYD7$ՋWʼru;XL;puqx+fWΡY׾ ӕ uCx20hҦ6xdebsg({qf̙a^-ܷRT_{p%٭6l,.٥՚xX,ڼvn7gׇW5'-Z>yvݎjC/_o㟩-SW}J?w?K[{v5]󥝟2 ^vBw_w_oSэ?oyV,u?ˑe/05sh$Olႂ⢮rBQXa4(:44vOfdnϼtG**49)yR XEaLl"<:xZfčWUΉt*pDr.W9N{DzXpyND2ɳoOEN>Pscã&O_wVEN*~<DqksQQI'~ED+38{p8Ccc!Kq ̖i83ҸX<&撾rpyR@0-1BH@ie9;}odjOJbՑO7)a!Ƀ6Jgy(&JD k5/0'ԫ%/8Gxx/$aZfz%%̑o:]“/n-)Fv2͏C_i硃œAG2+C'xbO/-i/OL~zL hѕzh݀}LXw0~(Q ~ͣhQc_ =w_gc/o|6}bs߭ߌ_TVEgt]uŭv~Jmߔumw_}̷o7E3/rɟ}?=_Wp55R;TZހib tQHEsKM_u")u6<l\Hc 8fbvxJe5k@8MMOb)UcåaLm r5tPVc/65mB7-%뿼Vx$͈9JXV2˱Xaãm -{5(t,/sm+@#@P&rQE8 {VUYLXisL;PMBrK?sw*nO@LC`nlzګfd7IkE:]-*jh|}T#+}O0T|`mzLS5 ( s^"pR-PcpiH .USo?} I9F~hޔc Bm#`<"-fCs{cK+G"a2H-:D!sb&]snӨ(cb8U_R^ 14K4?4l `ЪDSb7<D٪ qx~> +9x.0*ThxV FՀQ1 N3 _Y,3Qn [5mֻgޖ&ە/kox#kg|~ƿ.~[/Eg_o/FrsM~$[~l"FLo|z@l͐~۱V?~P~oq&>vc?w=XJ5r󯿓WT?()3^Y#uM͚>ԸmzɺKǝrU{DaIJL)t܉V"ICJݥP@I&N#Q,@xZZ>~*bC{AC(C2cPfwP(%&#{`;%B<.$1I>awH61f1'ԁ6, ,MLd1OŸ0a<,. VUQEQ8,K.oS:fd5ܨz+*p3f>sſD 5S*U_Nn+FF.3S()LєSgY+dgS , hqQWaa@#' [7G0RTymќ603,)5-J!b (QQb_q -ӥ]8:ؑ=2=>iA2QA[HC璗5 nS3{ۢDMEaݖIm ^>?UONlaqd䞓_55T)q &''gϽP]54XQq L)sys@V OdˀQRe͢?g@zpV풢4I1,AƓ;:>z%X&sR;FZQA+_+6z6SKO_՟مTtPP?6Ǎڂ) r3'sgCH9}^ MJ\7iVI`-B R <_y` sy"szX w025F΂ۏJn\='m,pKʷLVpb@,,w[o`2.*:.e*U0 \[3eeBJQtCοg1Od\f(ʼU8w܇rU=yw>8>paijԽELQ检흄roge"$euC\K(:̌QKV8+?VTR!qODϮdžqWWZGg?_Xx_V~9kG~5c_6~z˶b1 FoF~>o>g4g}ke7K.~%#GƏ{oVg'g''9,s'=IҮNOTI"E⾂A vXoE3twuI%EHQ(HQ$J7PӓHjfs{xx{}H6?8ڼ%WMu|ZBs6lZ>x%OGϏg(fW^qgˆ,lt;??_`Ex ^7RUEJq2 5 V yٴH֝Ζa1s^c32,.K~n,/ʚ~D(ipZ3L fNL4AQ~7 -`UՑܨi` R F dN^: "iPGYm$' FFѯV b3``Ŧa 54ܒ<\|μ ZJG,4JIMKvR_3Y鈃'?) LS;c0 8`,aa!ɊK >ii}I[F=W6, -aNv5.%g@rd c2F[ϔKg>€d)}\O$ U̡Y=Y) ƌ !2'.\bbVb$K&UM0{(CFX[4~_5FxeGJ/C`)IaAKb6 uٹL;xvO7;OӶ/_J`Ɩ׃ _o_mo~O5UW~Cyk_fku׾bm?o_?i[mŎs\M6?{cN<^Gʹ9^wA/]?:.~VbVNY.tlsL|Zp*Μ60*&A^4(v;) M{=Zf,bp@*H$KY~\?K/-.S~4C[yj,^F6M=}  fOI::R$D4&)i_oGџv~3i%KvLٮ{VBfMN \Ossh&l"5/Ji`ŨMw' '\0\މ4N\L vJh%_!a| nvビ5E3B;f="lFFz)F0m`I4~G%TT*pݰ,)9jŻ$͌F(XDYz| ~ETe#ۘZ5rۢvaJ=T\hfq'2Ǖ!/+V3Ik2giD-±:Ս&ㄪ`qҩTY *Y6Ʃ*)7{^BKU.b}-%~?W!fTW_+)RhhYV# )CݢXN;"1{k.HBa(?%jE?}T0[^sG? KG<}?ɾЮ}>kKv=+6dmJ;Nn^ ZUOnTU]PK>|c4kOjo^FZbj&ݝQq<#&.. G)^.dv^*CԯIIl,4KmxOLEY",VCHR}zcVR}u F̘V8&Hvi {pa0䟋 ~h!mtTtVqka!֭zS_#? az(˫bRvY!ǥTzfE^%lJNh쭰ʕT^Dx46BU_#术`mGSĿ"cn W[b6)r$7?;xo'?oyn),ox|oO}[v8#<¸oޫ~77g恓N#|@37Eşx`_8 Wk` ~[sD 9=+O)<|]xv%ːsJASUr(%ЇTsOWU_&?be?fOmˮ+j[|*+zK/oECo߻UȊ^@hpP ڿ~Ed_]݇3;/0vIRSWlg,7ў Iɭhf:8vl;7Cf$8\K߶شX XKxkx|Cwlv|Fu۱MMk{lׅew'Rc)[ہc~XĶؖsفs؎KG7Ý[?Nñ?>ޕnfB}0.w혜p}ҺzbC(+2y>և]w8v_/m^wo]z+9_*JƀpE>saZ: P/oԷ7FwRmYRgjtܣϸ7 t5UMW\&mυO$e$W@_WmG]^p_A,#[K?+/O]"Ƕ#Gf#w3&u-S:'%gL=orf|K'"9g]sۂ3/Zg<wkmoߝ&Bה1鳍G\t9n6mTq2p-O;K-_ W1{$zW? @'H; }0p~ǧώ'u%9?]HG*_{.sOk#327w*VU\)6\\o8^S}WyY2P$_J6֩maǀ@E1^4ef#y VU`R*-sJu'@ʜq<þcOIvt-3X=bB>sV a 呈5IqS(d1Ov,(Yw ]K-`L նu`FsHWıԉGOt>=\rϴlyk]yhp)ٱzkcέG6wl8Dt42O{MN>nzSnҶuqX&k^&>l^lAgLi\̹I>Q뫫1y^'Y+{1cϏ$Z$k=rt=~Le;b=WU;6E~+k#zdRZ?~3:Xn5?+xwp8h,Bf[b<",,e# qt=+)ZYgDo cᏴ^[d$S^W;̟\ʩN/WgE)e3|˷N{<#3SnxeX\Ϙ#4VŵHIOɠ#s<dS#hnjQq'躬`SZr$#2*t (s21@] +߿".:㧁0 Aֲ^&opX׍exs'E &ryE@Fp BN<, A\&v0  `+flOZehGtd4|$iwRY ȴp+JAq9MaN8m\[ i` "+bu:Hz!l9SwO189 rdXU4eLj!`lyp~?;e:uQ'{O71X@XgĶXv}6TnLbqэO?a(еr!9 YVCh=MJFX2 dYn crV3аX#P 6(nG~~̀ƏGC'"%͡𹚪>q6, V^eu_WV]Axb23pAMiIwQ\AG?Fti2~Po₼Cg5}9,-(/=*68\pNS?t@ y?!sho>yy/n:v8_Q~ 7["H_w![X#.ɑ%䲠C^/OH' .BKrK`]tmfÿs9~rGqĵb5*wyei-:֒R:R]zz2mϳ6] F~Wk^-l9 z/˔qב8.~5|V/ 6-vȯݳẻ_>i1ϗ+Q}>$Ϸgv7?/GJux4p]dBv}oh띿7[*P&4 [exo_|쯩/fYJ[ǿa>OZ~ 7֙O~l"<4 Cx#Jn=b!}+*^]G3915}d0m )=O=SuIr"ЩYPqm‹N= @Z|@y^VM: F0@˯zԃV^j1ǁ-5 wJ/YSmES꺛X ǾW^8|4D,Fj /w|d`%sт 쳕dC ~?;t r o1XWϲW8Zmm]"/liYoh9ba}%8yN!v#]6y$!]:⥐ Dn)`o=#j6`H,( dAK$Lrf;i%7^6 XPAlf,49R*(b 2v bAO9hѫDFփU9osBUa"O=oҗ. |EJ$.*CiM[ R?_u@'(+w8R]×09%l_@A=l&_U:Xd=h6bCظMf+Mq&P(ՍV𬪎YEigcwHw!7weխ~ҒGQvsH#ub۩v#Y&T vsVז`q•8-Q+Tmq<_cJ ;œdV p(Yc::k!2 ‰ǡГP)Q;371Hn%c4$<ȭ?G56SmFN{ =r.|=pcw|-<Ϻ ﯶL~W"z4ѠRn#ŅtFha9iC4l/ }<=m{'[ftMѨ酦}˒0AN%Rl ic5oc΍\w{ ga=s8 ucfrS5!J@Wlb 5Bp+bXt KKsV !鳕MDjh'ԧP($'edگ!xILlg;lH?wvK=ng{\1Ɓ qދ.Q}nmÃǽ˂<.E)iXfppvhд g1ʭY(8M(ﭮLᴙV3%h=UR'C K 'D#Bf9\3'Uu7io`\S{i~B{^=ؤ%ecQ6NͯDx 5!vޠƔsjR  4Cj:аa#)+h ؃ZOuՕW}h`!LyGr<ijdCh3G1jY64 P)S @+ 9F<0K)N; |i- VFPdfjG϶:-JԙȄ$ ,I7lʇ I5}J*F?5Yc. Si_Ї4Ft‰\3EdASqF2W>}gE{ĺjNޙ%=l2 _d<6ƈbh%ݕQ.ZբǴfeul4H zJYʷb pxr+>MMwpLw9-e`b-h<2i8\S % ugi b#u clo6O?=W_B R uuTkA/`L",Bܒq2!?h/{\6N5uute'VUՋ͵Q3TVzy (ARW^Ҩo!GPQlh%ƬGwqEwD۝XO#_+be(v͓fkشZ|<E'EOg_7!;Yg7S. co)(pJ6많E9T^4N}.BFPhq1e}\ZzQ%ݔ,*$r4pcDBkdqɲ˽0<J\{lՎx)^h]"|<wR8-W$fӴ 17e2'.šC*a{y}xUezU~.{ܤ<.;%iWjzEHK5f<2_6h]ah>F[!%w}T ׳ݰ\<-^*9OOuC5,}m#oup+bWW ګtxb VhD"~HLmu!6퀿ày$){?`/~5Yo08T;kb"Г3Q_Zim׳oH\xZHtqm!~lgj1f!Ėc-66ɸ7dfaAFoRב$hbVpVu: K)Qidxb7^ MH:g"Z_1 }U&$ ihᦁ[ 9㡳퐢n2hUTH5AtJ[NǂģK[8"+E`/7b"PKO6nG,Ӧ9G+ixu"m%|L3E$õ\/ g{e?}3ǹq-x&;n??ru`urr{4I@q/Zh%7[xh =#񆱽؞_I}q4+6C |] .i{І_.?C:2)C`P!lKh܁k $Zdk_86n\2 o{#^l0P{}yoS?tTbMo݅hp@%/[Bߏobmg1hH;a띈}0XSL]Ih`ɘk*)Axqb{PޟkNsMd=՞*/&_/ *jwy~:T Q7f2äJnsw*xf~x߾wZ**>Cea\FFz ,;!qO5C MW!6%%l+yC]7j9!ZЀ Bx3G;|cIܠޒUw,RwFB= =x4οWi(UuĀDB/!y(؋̓oiꭺl8+g=ˤ]xRqwՠGshu/ 1jDNBz ꩍ 16nf1Oٛ6BD8:)} ahGVT}SNIY- __DZ ]9ϱ#:aq/R6^{{n o [Ch5,äX(slcr{l2e3Mf*X)6PF6t8Yק]i?$^*|'.Sg76S. ~?"B ?B]aqVCodR>(]{ fp{ަ#,K]zC=sCk$)}(.JrHL=aN)/N (&(ں$릅)iKjך[/1c,&8֓cbz\j< >ۇ,~Gx@#z] /Er'PAT\qث!h 9ysQX|<^4J.ѵm(m #Cad>2 E|PeT[{@ /Ms!9NnQsKQ@b}3J{s&{3IԩHDÌqiDQsAƄ#}ku;Ju;(YLPK&ЈA?j6N47(Z.f`ӒC$[hF<}vQ*x ' Tm1YQSxΓ~DNlF 5֩r|V yXSuAq1Y00lP./tUE/BʩR$0M?y'x; #yÃ>λQ<  w/ n5p> st1)CmȏE!6 ?Bx|bbo YUDheKߟn37aoKj6It[&d(F4$u[QCzBsi!ll_m=يu[q4acD<4ZgM1l{G1ZDӱk[z 09uZ3%i]F_w؋2`^]tU}8g^w9}UՕ}{ cǾ}Rr?.z 7T}'qP){kbK yo,.+Y3%hp+`mUO/NqiaǍ$?+$^ΡoZF5.&j ~B-3RS0jDS=W$ D =8$6`AF0˴|iGUK=DC8|d >F0ˑ܊ >LKd9ĹU$YkHЍDC;^! eoK싮-!)ie2|%*-\@'P9]!/b,5i_נ7%;Rx꣑u1%aOjBL}9ߙft rZpBKwM\[D)`%62x}3SzqJ3 Yru x%{_ᴿ0h8]3t@PUe:V74) aԍ:wKK.@c6O a<]_GQ}]Lj4Mk5+jinT7{w>UWDPPC ]‚j 8SWKzճ?K,3GNGVfg"f`< msȂyw=K^ϒ y˅*逮wlO>/u4iS ةZZ#]atT~Ayn]u [~>LX-?ONOBWOgC_@3炯hL2^*>ۆ%x5B۾wuKuUģ|Y_"*SgIա4h=gR8IsIXEQuUU^f2~+c$M+l\Ҹ=3ܼtdHg6I,HJȳ K guMlczݝLㅇ)u#MBB 4diWMM_E&ݰ4a4ȨkXK1wg~1~pD)LiL6%PYG:w}BSHYqJ5)ts˹Z^QqA576`I 7>?xFIq[;:xtjHU"w22O - < L̾u#vGxBy-W! Zҙg}fr-ee*4i7Ju΅KPWPEUxRPqrlB*U}PUCpOQpDFղv=(.i ) W28S*U?NIxHMQV>AkǛܜmoY\V tה%)ÿ{˱aF8tt$!e4j2CP5'I bawvAM;u [DZk4_0ҳ6[rTKNKebH%Z d//3O7$_{-'w],{™CgJvi~N3wJ݇F /vf!v .pD~Y?#Cx1 оÕ=$dpk©aT_2r.:e1O F"047 ~]4+,R@Y$O6}]wľ>d+IU\4WMo-}M)ٲ.y9s+QXmĨhj1QX026N\'O,~Z$N?J,滍 m9Mmmq3zݘ>_RԭjG?Y(vo)jcK>QZ܃jOrcҊZ{!Æ!_> VPs5>5i[NW8(^3hٻdx'T}2bX$K!DH/~}1x-~x+t{1d/<N$k>ګn+E`%NK]G[Rܛb]] HOGi= 6)$f1cb@gI'S\$WCLb.SG"AD2%F^r $A`$ZKX/| NZZ ,=bIV2bS1CD4+ǰ_dB)ϟF!2CRbz%HK0*OȆ$d(B_>jE8៟iygۺLyApj4'V2H˯ wMR?bVkۅ-Rh0?=P= R+11M  w]?jXڊqB/RH?>~\`=݁KZ>\E6xǣ)(@RM+!IFĖ'x:k 1}y >eP`W[,2J9(Sȶ;'W.)Q֒9cYEZ΁OwKKXf{A=wkPt7{B@xxJuu߷Whk*2s:|c[^#S! cl4^`ڛzBgڋP;Xp5x7♎9_TV)#PD;P(LОN&ck!c_6n8ev׼e>A˥/ȕ!s& ~a 'a [ؖ x G1pe-%*Iv?.UB'=/&OH +Eo\g]F)hBZHh0bO-|4%$]dnۣ`)0\CsD iegL'v7vnyğ?F$DB_yvA_!.Z]B~!v)4 bQ5b^G{ > RZ$ëOys%]~o_ #cbk7@fn{j Сs{Ǎ4Ցf椐 ),8L֜#7 O2gnG/zXk'}o -*@ Tt$1 0pJWheq\(jouq6J$P8H3O ; ΧV*]g zCnW"dl SL}J!6"KG-nd^l옳Ki֒ܙ}aRY,L_2kB.<M,HB=GV;{oɌ3?na0ʈe*90U֚=!.`"`Dfg$Dz(wdG@4쎴-I⺇B_4'"V'-8?bGEc bz2OBa=AF]$T D[z`)a!h8z(Ħ,l%+:7j(s={lu}-kyE+oaYk|ϿoT7U_{Eˎ.8\9ѾNV].,8n3G#] DV T3 SzLF΍dl)U$GǏqٙ^z`>8mcI(#P*a@!1YUQ !DQ#BD7D`3'͢:m^hGxԵo ?`@F.Df1:MƘ,!LyR\KgHf+ 6^ b%HscQ!j4캔6o}+폲!n6`hu//.)>_Rt LIIy :ZՂ~wN;kEaNdZPP9믿wҟX#=_*Jky}BqS+83%$#`ղmxpSd…C11½!eD^~n]:0p{p|>3qȲ e&;T l?Y٭ Xvs_C6I4Z۔6*9lIM=vؗJ < N10V3]8}_;v!BVVg-cȀ.&d?4Lf" Mxm aS?+TDZ0 ؆]xeFD9Ks$:3"?و?"V p8]\@J/rP Y&m{VT)<E)>EHJ$%ǻ@ :xfS..=}?]_sw^l@u-p'?>LqN cJ#Ҷ7UnMuFy uU_$ $r@J|r*E~\Ws+hUu:Ԫ>uUJ-uFzFیG@Ҷ*EƲ*efe %)(KQy3GI{1zL"^.q@ c3PTf!=4wgYߖQHԕifc1 ){Ruhq\3^\W{2坂AqXqdIB5\ߎ+cޢSeCT9"F tbNhaӮN9Ŋwd_>zJ=v\=VP:(WZo˔C!l>?!ϱH+|؟b$G (zgK 3@xx ۽dL aVU5e\ǻ: ^781JۡlVƴ6f9 N|%/Z{pcZ3#FݨmX݋wH_F|DCg<$hdG3/5ě}[:J% ˱74O]1G0B=FbjW!CUatdmQ /xc(+i#}:ygUU7:SŭhuO]%l raoFF;|Ps/sg]A)*8ɰ٘5`LxoZ(b sPl[y)ϱSkY^9ׁg'8v\kb_N%P'ǶZ;A9Z|/;S8{YVLwXy_?BanB f]dC F I#!}!0LNU <_~?yMݮ9c`MM?(o}Y>[Yc8(D7ɗ9V* c\n2F1PaTGW_f HvV{q wwTDD4Y(]$ee(罏0׏TJ~Yg3 VʮT4OWG?1MR|?7XoҊ²"yGKoUGŠҪۺ ܬPW +OvwC޾Swynw߾* ,uO ;tNJNu{w +qX=W|S*)?J{+Q1i⊏59p3+9.q#ЕRzs9#v3iCyss^,s9TֈT--c/'ni^~SpU 5 t6AN4ħ<UۓK;C.c~*&Ŷ16a )'.0ݼH0 HbC=,*RܪQ<_USuE M5ic)8~ZOޗBeXVF **QxEAk0BP.+^Uc(cjaN?j+zэ`B/ĝ+u=U45_y}j[PJq4Ǹtι9?#Iw`x@wAF9Gq⺓~>529[3`'LZlӶƙF|/z_eO?/1qLQ ,DEU*eCT26iV; ~F7A uRqCV&Ԫ[(,LQ g~yy1k6+*o**oU RѩQ]J`pf`bP;KZJeFգj'Me^g8֓ N|R\|WW~\v'8n8[x;yo9΢gYwM1lNR`\KPdF(Lb'Ft3L, eDJa,Bb"\_IcDJ42]XJ䏢$b3vP*A@)Wt.i( YC7&Q Hvňr39x @0){#%1"Jۄ(iv”ܽÃ9OX cT:Ƀ:1֬=&KF,.]cͱǒ?HvLw5wګߔtKU#ڶr9" ǰ#$h7⛋iCuI=RJLV~#Z&:F#VQwgG՗AF(T-6LVc,$PaHQP%ִ&#[q{T磑 1_4 6(/Tcѭe(?EːpX+TB_mCITH\IK! KJ۸Kbx%f*?TR 1q՗qj&Χ<ӒI#kMS ݋kHf %~+ihOK)g< B:u/482Oā(|z!H/C_q:/>}@j>OF-YPOai7F2P2U7j]`~ӵP_7 X0OZ̓z0 syFoNc`wgU450vJiA MwG6lE9c?|^f-rC~梌R”́%ڏlDXAǀ% ^Gߗ=JktԔj\׀5\wY@8!{{Il'ySi($vj`5Ǻ;]d,ze&{1yGLX<55Z3|6_7C|f9 Szr(O@M_7`>e{  i՟4=DOl?{((#*wsU mh\0/f3)b >pQg Ncl0IQ68W_O)B4 Մqq#.QxR< b2* <>8)nܹU2 ) C xHчRh5uXLDŽ5bf?bhYȺCL~z'dOf#X ީdpA>|S 1CK1-:%H 5CKN23$hL'X3\IGKY5KsK1t--:eԖLO5 'wƎN*(NfS y9[ <]{E']?GZͱ̸|?_(8LYq[^E:E[pR]ݰ8ZTxX?JSO=Lw"mZR`MW_B+c,.n ZL܌$a]ӀQb ޿Tyc%Il#Z1.&"֑gFh Y1*X{2l)%QX/IEU34+AS"MPF0K MJ{>qcq夋MKag, M{P Gjw4iBcXF#]u@l3;"qf&qjF.7R}7cfsOȟlٓx6G +|R)@L*(UerE/\=UvhQ~󭣭_~G n,yh_~rS`iw.*.,(\K=Eyc@I 簖ElR\{<+ +0Jes3Nʜه&t7إOkְNiug &tr|V;"KbӊN_v۬RѝQVU(vOS^7 iFԏaB4dÈ[T)שּׂaa(W *knJ1&`MѦV=@꫍B#V|]}M0dj A%_3$E󱚫A@o YT{Y6¦(X5用,bbuJ J,aj>UՐC<"> s10#o/xV|":G"k},iۣ;%9-T|3 B% }La<7vЗ =.ԗ(lI}Y(<W|[r *%n 1X)b{0<^d ωxͧ"*-ԥsɐ~;dI*'<'&=$ssAb8=mԲ#g9kL3JUlGq\];툰>[_?j02?? -Z@\`rc|P&Ӹ}yqͦf iw]PLt# ۴߷ "Aiu-B"gxO/x Xl5էG@`-LPdGdc.f8˝0OSkMpt<DbBr5=' ai(#CTyvϲs?Zj, Sϴ9ps7NY\v: o_n,Ѽ{Ŀ"Bb' K$?TvS41:e}?(fu8<{NFsPM+iNf 令z,D@V{9䟫ʅ [!nr3臫VJPgh01ԏGUWXMl4L`Zs -[DCmjXL8C ( qMЪ>ICYޮc+Wx⯋WeVie<6Yu@Z[TxV{Tuuo"S(!+kzqoCu*+tB~FMM?FO Z-*jU(j2-n`X)+݋ N1щSXHx ͽYu 7a䲢Wv,Vۏ@Ên9 12HJكF-Ը0rH8"Y60 9zuZP0yP^Ø(5NGK&\jf4yҷ4GQY?'GcQӍxdQrǝCb<݋5tJMӢk-}G%q[vǼWrߕ⁅mo-ڌgIT+ONǢ[1όXY{)ȟvL|( s9eQHhMa7> asoP}AGܳ}B ݂#D Zc5r%]/~;ַE{BHKQk=qψPߙtIqSdl=#oG2vKިJu0wĥ UOŰ^ojQ~xM"&h25. ScȾ?}_K9s g ?X뛗[NS?Ǩݗ5.7ӌe9GCϹqBx^-N$bBR&[H!r`̑pH"r RbYuo,*bR\"y>L)Q3'CDqbE8+|=-+mʊ7ԍW\쩬.uTwpѐSU]= dz&DŽ*֫ u{;]o8v 2 ;JsNyc$?,)h:ʮv̓<3D\x̲9'ok65gҜl,ٚ%hVrd0V 6@6zR)A4Ft;mw# Q3:<&_Ďf$:yy@/zd{\h&|e_2N] x- =@&y^ ڙ1>b ’_ %6M"MR{m0O0y>`w*{:wqvciK9\;!Eq?.N=wyv2 G,槜ZSN_Ϫ!Tx| 7&(i rJ(3RJ&}S8Mxr'. CNgvh-O|go1i<L-(=JЬbFbVh0M6gH9ejy zOO&>jJ_WRv+n$N|=uY{Ey'w8~ }`2z_jv] M?ɔ3xMOOfT|QqlЯ|; q{KQ1YFLv\#8 MqA EQ^vㆠ-8)-.5JMXemj-qS$]r^+)nU{8ʩ"~z y7n zET8]V&+m#ۘKpt.#I4C\6~ c:IL; P=~/pnj4p]r{S  1ja~z< Xb2 JE׏QT3 ̿m ~@ޓe .p ow:1y9'`|A-٦m91&+O%6]1]}^ׂ>|`~Dc^t5F.{ ԾdƟJI6cb2ߥZ$AO@E E@,6,?zwn n l_ U-XRy p x.޴:a b acHV-IѨd3@;czqt|4[L _D%hƲ;%fL:SeY ƍ: ۘ)(iUe]N0^BzҒ @ɥM)sτ?^WsHY0:v-6.qxV5:4Oeev8oy F)xoֿVão?m+o>M~ y?f6M(*ȸy㪫uD0ȏ.(8CQgj8%D |K@θ@F3$|6+bdkDkj54űbw\ -7ܟYibwcyX&9?0.H2U'/S>a9.ROL}fe$q~tfa|lyl<(mh“<(]3BGssAn?OItnܽ@Xg_`״/75-z n,Iw=bN72֠4沿٬^;лwǣpE2mȉ8kSw/8S i{],@׬:iO7iO kҟ+$30 fsl@@s2̧"+Z?VXJ~5P bĥ1Y"I^+/ua|e+:IZC< z ̓$)*4>gqU'/^NyiGL?Q:'%@xqJElp 0` "Majt!6uL18ilFq8pm $u'>nWGh06fi*\tPd+} mcu遁G-CL(kvbr 6 6bA@8Z G29AS(۷}9T .:nϽ>Ȃ@َ [g-uR@K$EG{ܫqY.`Mꗖ ΟxgDl3}%%EeNY~hutiv}6ߺd$|_) Y'_-wis&KHqIfuObm.pM[03?jz(#/l`;X-ءjLx+leR\5z}8 y#UJazB{ /U"o?X$qi]g^K)i>j#8yŧ U){(xR3[QSսrEt( k4ADTv{U`{ii[yYG< ͌ _CFK _bj:jvB,8v^D6\n!wǢcbZ'M0(Aȿ}yo+lCѷ,6awis ? psE V=;>l לy朳qtz6av\v,ci+,3z~/j{_m6'Aϩ2iey<&፸wY4 Dc8rƥІT?-[яW>%!;g'dbߎ"1__™ߎ%LvUFD/Z,Ckw/H xӲXID/ѶT?,yD`iQi.C ^cw)z@^bzwJaP <\"dj#6\yڱB}|p`9h0)!uyh7 V7tGd4D( dq H)ƶb0vLDX8K煑fezO>lAcQg-a#| *vPHa8D6\a3B$n=~*kRO?>Y{~ \H8U pf,!]u*Jq\6e/kjϱ%g]O}x3k &gz$|{칣q=aa'hZ`(}媡ZD|\([+7?+(9Y|PsKc%'eok}ޗ/}=槂?Nۭok/.w[Z]"}hkRQi/}z(i?䄏eJnn,3a?inj]0gҾE}7d!n\/)RWWGwJ*5U* erՐaV]3Z1 rEтv~\^5(WM%xq)LT-90+,:(R8-| O/hzK^s$I0K ^]|Ju-b~ժ^XR09a+ȵ9]̦quX?v%DTv>,5_5!+A﯋JkWC>,C<2tWnQ3d(f$naU{T*,?̈́)[VzB-6GJ ;Fre74~v]PBѥS=zf{ZM\MI#>2{8Oa@2Jh"v*e7\/-nŁ Gw>*(`~ aF]}Ci' ޱ'ѝĈGz u#A_V U0֏FqbG/H]3)e7\\r=4anDR1-zS /s7c~ ;0¤n 9_,`^sϓJH9WA(&}΁ǒc4n}揬BeWd/<FB~SӢǿm~:45/:tP)71Qa]T ?(/P]ŇүGLRM{kМXQ2Q] O%E?G\ِr<0!T~=hշB B?S|CԵFҁa3ӟj_t10(V] { @_E=S#IZ{#!D+(3QsA\nװ bbLg+ċr֞bⳫ2()0w 6sfc2`\Q^(=G?Ø&smtPo# m$1ɦ6昅2)0^`_R/NTJQdKrwI쑕SM .P(,IfpH HCo0M HdjQ]|+Xdm$p,8Y0lRh5ŭʪn a^7\QށEEYf[=o G5`hJ;,ՙ&; ó\Pe9k4 1Q#FBn L{Ɔ)yԍkG ݅oo+Nz $3yIfd%فLIf뤳$lsdi-II Ǖy-%XCދ:sxaK7uI;sP\ '05.s@}F-p1< ֽeF7US6O%MYY"G6fJ)> tѯ "2Yꁵ P=E9~=W۷O˴ݹ fw;ԇmʊ@-OZ gv&=sO?xvg-|l^k& yP<{Q2FU}@(Z'tY4e% h1ں磌f26, .Ž/n4;[:%J <5: p8jptb,*wp]d3A<'GJ HA>y6wy { ޳^|ڤHb&Jcc4(GeT@D)p-0Y~ݱDy09v4{(lbۣl%,l("O%ֱ"nId^,Jy'p=y :`c#F" ÍRޥTtc| FT@M='.T=USW5(\^W+|lҨ}qde(*U/gtS8N7:3hz _?c뀇]9{k$|>q.npsGhZ;n'\> ^V)%7Ø&7"-QE,{B؊/7J#)Ȓ7/yfk#P#˙#nf{_ #xȑ#c.==ǑF#SmGȯ41ّq.)8:j#SwO>:?ƎB㑟11Lsj'tݑ#Pihr?Va9L=#b*pGp?7Rd`R;g~#|Lo]q.-Moo {go-7N{/ - fׯx,۽;VT]|}.dNiu󞗡,Gutb3Dhc(C̍kZa;3<)s50QVIIsY*9{%%Nx&N2@`%+^t 45zVq*q%7j8psM$p UNsΣ͡D"#Pc(^m!v|?'G@>60m`{uC o!9 V"~-LLƻaC3rΏҽɇo·FR˜eRw<%<Ȭő3|H ā]fUtin 2(%_/g[2V<'.mE%W=jhzt_:5Fep`6y󟰏%"ݽG$7a'qirzdqJj-g4d{D4l@Yi10+LDv_Xp LzYi)<~I'k6I;F:b*:pfUU7K67 gs(8+ \?1zJ|8#tTaSaK+v|b+d(rV3QY0iNAܮ&C"wg Ye`~r2G\ǵ! {,LY`H|ZAC) _IoY Jދ3ꧫWB&ϲ߹κ<{n #q !1M X?j|8Y&-1yqn5܉B`m7F]wM)g=a;emfy' }L˳ uDwlȿ;{$`79E?fOh:MzV#Lei mÊ`w,,isCqHw?%`ųy\5Fv xr7$W<YgZQcLeq!dWG"\_*>7@E,~p"ǽIib%a #u#]sCm&QbB ~F:SIHp$b_1MLa"I6Kp rn`{Ly4>pU(0XGM?Fy  “u0|VzjiupH[BJ[)|~f5quwpJh xFÈ7jY@Ӓk:PM  , c EF'+m};^pQ! +*ob_'_u瀐O -Fx|[UQFR5)[Uu SeI"TTU]:!5VlmR(m+h0M7qG^ܪz睏M򊛸 Q,jDW!a480:(ӜaM\N| n=ur&Јn⹔29;Ha*puu^w[;E*& ڱc72:KUwʊJ)9y]'tz9F ?Ƴ̈́مkgEEeJy }]ab _(TgXwI^cc0%5xzi9VVf&1 Yo),/^7ϤRMMښcG33Hx%%bI7Y$U~i״̱4(l.rRV#b۔Z*T%h}k x ܮd 0ݱH?C[ڱ Џg?jo#?^yľ|ٮ=JHO2?3u)|ݏ]lqQ 6?ӗC$(k%õ![C#p낺M0^ _~!VjnP4w([R~f˭l.yeo }Bb[d+X3=(=֖)&L$ZІZk0|[#'>ME3W65GeUY7WC]" Rtac(o(#{T,Xy0+:I Z '/5g3uv=Q[򉼼STUݪw`QS$MHkZsܷ^m %kj>WʉF(uMXϑj xHI*7_^AC@?^Bku/У +ޮQcyI]ͫᥗ&Np>0;:Sڄ1 H7Ymjac齫8w<7X'g"F^s^d?њi 9>}ԁ88T1"K)k>@Ltθ(l` ?9i|K̰lS EQ& Rog[naM1d?"Ϣ@$Zη"5 :蘭Vdfk;0UU70mL4OBY1ifMW;lmZHbm`8}:Zv@=(ҽCcv=I%[ƙv\}!,9\{?OQ` wP(GJOΖkixxK}˸d>s}MIQ+h^&?16W]kǦpCD;Jly|Nql "D)3a;^Zg)YZ42Bk<>4]k/sfYЗZHu.LKA< $n8爦 '|<4N圇sZ*)icT.b9 I_([b$;O&6QV( t4 F?{=Gpi'N|j\|`*ʔ,L? p\u%X31mOOLw%=[vtn)ܲrUDAHd}EL*={lKJUbF @l$XsȒ% Y#}sĒ{XXOs4޼$Ǯ1sy'1o4[7r}SNߔ;M,&"!3sٺTz#AG˽{^On>!m@o!`^+Jyؾk/|-}1mRJ) vӚ5o$n>J|-QF=U;t*[H6jΧwY!-E|ظ1ĐN>Oݯ4 Qk'y:ыc)92t3vGG7h Ye9 f=iaL8odsmi'@(Cu8vM6l\+~Vlݮ)At͡vc&j:Ia QYn8 zjR1XH&kp0IMm6FGx?o}]y6l%qdC( O&5ϐwS|`=3[lHfv&͙eDr;[Z)ai vتi.Ci4E)R/tϤ[yZ  vJ'c%J7@`')Ha/בg8 pLz1`*aCW>5??t?o[%?|塺ָ:5^GfUf cō3zoԫwRVGhc"gP;DPpkKd&qGJX7RaC} #kIŹ.?I B zZFyR+=dOOc)!n/Q WkԽK.ͣ4>N3Y&*e$X$rd88%t%xON#L [~MXn4CS?"}Zߜ֔hSkXtCb5 l$Ld|"a4ѻmNׯB Y[{m=WDrUu݈eQEG EqbD8i=~di'MI4 bN%/+/ 4g1j'bkvȡW̤64rL^b/8$"Z6"PF\cld#EG@a#EInddgXqKZWen,Ř9nEӲdeF@Xp[<>{`?_\p= fϨi'CPAD_ ĖËz"tO8hVvxL{3v/I=\!/,oE߯c'9֝ly#;ֺ CJޓ2%J;J?uxS`ESS_qtɩsR?u\gcE}Gȥm{ j OmTz*ytMPX+-KjY!a2WQĔ =heV;L4:Gg{GQiqlmbEryˤqh` Rњݥ['zRf$. @42ANm<28(~EJr0C.E)F";'P:48rlō+ÇjoT씔;%8 :\\~˾kT_S46,)9XhVECK3 Q!HWLr!G *C4˃JUVvĉ $Fjl *QuucgQ XzoAp?wTe~K;#~َg??$~Q'[i4+0L ~zIwh1Ozt% x@5tp>B.N8XVIGh {_^xF(-R#~KJȐz:xE-ꗴwYfW$&"i'l>׫̟k,*^{wA$r_Us0X>rM쒉KrO]8g2 =zz@*`<ilJՅ>d[j <)ܭ60Kߥ4|6 }!UA]b6VI^2HY`=j{ECpɡ`:5N_Us ]ayE tրr/%azM/~G0)>ZĨ (#R:ҳEGF]Fwo1 ?'k{q?%)ъ=~2 ç]^ bڐVd K i*n(H'g aZ݋pB S"JdxN:he.U0Ju! mKE~Y:.W]lPK3o +6=Q/_lb[Z#-ht(_i=갂?]O.|O(;Jl7+ʯXiΖ?W /*5=pQĉsV(p+Yammc?-jC3U@"ꑯ11 b:wZ2z e.ckoyO$ φUj\H>*hxX8z+*1积=-*bTcUE46|~qԢMo6?ʽCP58kA[l^ ~*jiY2ѕ$>0a&'8afMU}#I2Z-ؠ7ΘYK_ ;.yh2r% 겔}&5}m;} vF"ADNs'EZFr-8\H=J&Ǥġt/3BOpp\N5h.(%q& W8 1)=W#;`Wqi_,L~\Yڲ G G)/Z̑h̒>SD;QLHk gI(K'Q A簌bŤS[ILi5P4,l6 ;(Z[(Ȱv9d8eC"Vu6dܪ+n*h{muꚫjuKfr?ؠiq`o>J (LP+W/adrOP b6 z#R l'v }vZYDYOUu JR&3-^B*3M{ekwN8j&Ag8Mlpl6bO*_ Z>`pTl#'߆Lx%i?;?}NG֬ Xt4xlcM+O1 W r~G+ hlѺcQP-F# Hܭ{_Ds]hoК]nmށ`X}{y+1o9}~\a~~Tp颉fJi\~5*m[{ eB[~#k7YogV7y/xVnFe.#/z ErS8IVTK@\K<E /'`<x3BvMGAw$'Lu2[T&vtʩ%,4/&9+-=4]y5_~=yW™_ }$Xw|ד $~%~#㭖f?cLƯmL7~Uω xꊫ˷f Օx#ְEK:=l TUסoD8hR; k{oxxjUSsUx^.ٌ\7o#jó+=qΠ*P?4עd?!ޯ|-g)&"/i_˾7>" ):m?^^]SA):e^{gn9W?=EeJf)gf^Bh( \%tG4dq?YA̗͠UY8+|yx&fv3t4ߪTa?c]@,Vcʋ&=[PFO_G:J*=:AkKG/N"?[#yݔ|<Ϛ {0^/M1G|9xJ_c_ꋞ/~Woh֤{hoE[D|ޙn3"g|I@vN:q>on[hx`wŽHY4ٚ:toJFkݮa'tg+$ h4}fM[76 6ڨiw ĥuJϡ;@{G&*E~Л&1toaUW\I6H_2TSK?_hq:]x{zm?f 75GfUǙ>t_:oڹm}̟B5D۟s^|'EӠ!4BP껩*qy ]R g1Lwt["/$f䅭xt]پL@`QIn3ﱻs7cuߔ/4O *mLF㠎d𡯢R?73ޢ#oHn)'kT]CHe&ǵ Φ T]axz(#2p){tQ y!zw%oHyIwpE9kUujr4Bf7Tda,"? Gc'9c)wee4^\&>i+|?DKBs`1جlb a3'Oxo DW5ZM2f]]x%Dk!BWfwlQ\hUvK*TRB]V1fijw?j֫{\)Ja4OjթHH⪳iH8LaCKӷ@1/`T/ Y-čDcVxn7#B'UQqjuWZ_g"aPtz-Ng[>v\VrkUWx@sI i->P]YVzK/5{<ޯY@X(x#-)crgqH?cc_Gb-G c[3'pez|58sŰ4Q /߬s75/FbygX1SN' QUhƴP^(x/FS؋e&b` ͝w>,@zpt3O,JOe>sꋟҿxͪ:R)/|K%?ϴ'^&2FUTd O_l"?=˅aXQVįySG(*6iW-4,-9PZrEoX'M?_z:#X"Gq7&'|h~4hO]x~9ʛ%)F77/fsAS ),X=Z2q]2$12FIIy4w횖x}DS+.2).=sw{,nD ON'hKO[IoAMb x7|.$Y5h$bM mG.8ŒF%r,B"ǔmÖ2"ϵX!B8AH e,vɦ7RIg\l ;ϒx۷С!WqOy@7;x>`DqQ^B ]<@f2[GHP@j=7{ K$24tM/k9%v4vNO"uخWN/L~tgҾs |4G54_;TVmv'SeFkR_p[ K h WneLQk_g4A ѰtGQ^~A?Pr&R~d#VV\47e_l'vytuʫ5՝~jUwQ;ž g%9<_1O 0'v] XIuҊǔu0ɍp##], 7ѾGŠ聾)-auCq(',% BNxϝfMEG\uIGrf"L1?׷q*` Njd*'aln^9PIo /"Q?YK[MV[8.f3{zJnDm, g?ZڪkG,%%kEEoTNJmڭoe3[W߱Zm Lebz%xXx GK54$VUi[<@e'.jK)86Уҫ;ntݭԲL_.Zj=]…DZV.Aӽ>‚*4}~?%y1G$5 )yY9#-@;RMlRY@04Ί=v cEP@h?Y' '~(#-Z;>xˋ}|oo3B?m ZQ`CLiC\YxЂc-^]t:- $``DrJihR_ fSz]*Ut&gɎTΜp\v4^4xo;`sNM?YWmŮ:q0NR;7LAf3E hLo%iegv㩍ORV^O_o]^kgO^z߼okžǂ-Iʪa^O;V^~ }0ZLSp5 =K/~Yg|@e jBwYOK/xL_ ?z݊^rOkyKޫFJDwуGy;oE_O?ONs/=#Ft9j2t@A_`[c(ЗPHspD%Dд7)ޗL<Lǟ7Ov 8Mjv3m7#52m$83>;8'ۀCT(.sxpEޏ=N3$m[F $>ba;`+gTD-ų3"FEE fJթǷۨ:^Dz-9G#,zmtjePt!-+D U*+.LfQ[ãʡfbiĶjB%N{M. ,ZQrQ;a/:kW ^T_!ZFqxAJr-*/.?xc8 r"rB,u Ձ[mwe_< ]x%pNxh*`]rMH(btMl44Z"rI;sOtVwߍ.oVSF m2Oo,3<&)bɘD,?Z )Y b:+&l%xT'%Y R1CvEѣed>/ AmYq' P`҄7dOHߴzxVFaZyWW>e(C\Vq&r3d{EI@E۟tOFA&XE T/ Eb, bH ,m0!$\?RL٧eP`v'ϓffkMO޵=}O>>[$ y:R *م }mK}EL<+E" !7KW-ۄ'Fޡ{hX`:2+EO.1.,! [#s!uC}} :Y$$@[ۖѧK'H!v`S:p=0x sJ" Ntb-]%%CVI)g(@ y,s .x{KF;{_N(OdOL;hsФ̲t\ GO D׺.JN9xl_j#nȂ#khZEMl5bIRS Mdh;Aς-&"T=\@)jS8a,cOƊ}p [4Lmk|9;d WiZ-{;5%횦AwF݇~GGZ#.<,"oHe$ܲG<ÎNtL`N&+BѴ2)U(4Vn~kLr]dzK$`bpMc66XR};q>`f4e2 -cX`M=Vĕo;~'9VOZ?[>Ou4.(JՍgf. jMktMq"kɗs0M&mLC +)XVmr̨#$U͵-b6QG_^Ӽham;w,N,cv6]pgE v-CLƥ ,%~ ߞ!qCrf ' p$V-x2b˳6m3gYH2Y65or@WrOc&2FXD"boz3)I3$wUN7¡E)S$ Tޛ5)ăVuݥ%n_Nc*FUT^AMSFp$],f2 +ӮVT\vLwd$V]G%_1);qj‰طa<8|(XUiTd vHee4|% (bm`I{Yފ]ڂfA 4z@ hж`0л9 ]AF7Njh҉E/H,DOdb[BYtRV .9cѵH;> wQ|B="}p#ާzqJ8tm,#@XoAtD5Ds(}8 g }LÉ2ef2+ꤣiM"M}yAab/¹ȊvwϮE>уԩvOf q^<{;i#q{A`/<[BK-XDD\f,L _f pX@d"h߿^n_o.vg.ߛ`wI]] qy$7)W5j*d+=E6?B0ÝGFcuR՝8eWWzzh70?9aE#Gf|DjvfcVʼn( (RYqQ&Š?[Y}}=v;arༀMVɥpE8P l@|:]sвFsZsxDPdݑPuY}@qyj``IwvL9IVu"&{Ί^"]Nwg$ōzҤŸR,! 7QZkdM@L=6@1.\OT]fÝk- ӜEMZ@$ʤp:QԴn49PM;˰K>5R#'9e`? J?\HG cyͼv~7r.(ISy]Y%1 _P[h%'x$n3W 1Kx."#,X Td6 G͂(uCA^^qY{A I]M:KMRr1FwM4@Ҧ%gٹ aңt%x[KK/ͩ@nj1_=c0 b;ռdȓNU $Rz- TV^R*vǾf%g!T3蛊^^\Qy%La+aG<Q%bꪫxYxjTS0i #qEg^],&ԈzhLHZhLpzgr6ש^=_1gz=I#fh"?)sn4چI'99#ydbB7 J']R%YC^cӜO맱9]-O!DyPAaW2 0QCCm-!~r2NuXl%/ڔMYBbq n2̖B((P¨@ni 4~bST>·-ʯ=Clo܏qN%VD"<۟hӆ<6|]e]?c$${ĝag*yF (\L  EC" |>ˤ)LxsL.nJY,ekyfR +׾)i*ˬJ7/{^ۯQ6ź7)r$> wE](dB*(NeS\7Qqp LdPTh<.21/ mtD*-=G*@yE(]:z]..XmU]7*+oV?աCoѢ3'J/?_]qK%czF݋#Wkb\UBF#贷I[&&K;o NFJc/% Hrv =;z:n L2eFIFp VӡHrb0ka// [Y6m29*Rvs?Ѹ54;8C2j2 ;LD%-lȘR2iBՃkrFS#.n&8Gu :2SklY( UCS=juwى54T^NcJMD97&~6xu5쮥 #ppt! Ix{mO3*uD\!w)(QX̼BZƾzuW80$BuxqOTRNC FQpO5jkd69xy\RnlvM8􉢯Ӻ%LeM+ڟɉg*q29Zr(h#D:.WFzQiW[sUZz!N~a3Ah覬i 9=*?^VMm'bFfJE@3'(aYovxy%14f̑X`EEYv;Zk6SEI,%!N4joZ,CF Ua&1Q:NXR ;賨vA?#{)4B y?-xn,bv4^b*'SKhl1-ՕPm, L"1zme$O0w?@t &gAPeY(v}%%>GŌR׸:X)J?.  ]IdAӧaQ)#֛QFLxQ GK/`ΧBFF݃ 䉋-)yƉB+i NgL*C ?0v8-!wYy t_eIeyglAh瀚ȥ*1^;4^w挺_=5Xɻ:"<~\}=e e6,:5uC_s"5Ać2pϒZ9UUW F9;#o!3ozMOvl Ȋ/@ R@ZNJN7~Q4J 3rM ;p㖙K؈AǶק,&CHaּ5؏y(lPX$s粴'YC q n b60dqYf_Or۾NǩNr4c,,dDKBy,?3yHvXFO)F$p {"q+À9☉T ;"^RJ г% />cGg]8qx<+]B`|%i &/e${bKt.@C Ab |5鶏:#vl2a!p.XH#0Z G,`A?_nO&툽h*+ʄblQPY8aM4w3[Zȵb,摚"vNaw%4 knMJ%G ]I3g4Q*FP(\)m,{@EZP}|AӯmE p W4Lxlhl fpY5 RIhŃ\.:V@4d2 6h8࠺^78+lJϘ<%Sɯt&`7bB5iy~[Rtrn:kյ8ҩ ;f{=&qM wj eS뎬w&Y督y|jȰ}7mB$I|YjZFQ|\К#qs~_ bf4 Ϳ\fPwl-iqᎪ:^$HSoXu 7Q!W'Ye(IXdCxu%/P ZXFyQ~$BE"Z딡2;|{O=OXCcwb\qX1[w?dJN8rw-Knಏ"HO(O87|4ᰏ9 87,d1"V]35D"lY#y࢟܄qaGɓ|IdO +x.4\v⒕/9[Qzv2S[{e(/Uw[rO$a?uCLn]d:Qkd%/~5\,jY6[G@PT2bftݓJΒu)C13T``XT SbK4 0CdbC؛xtCJSRs~reep(mc?꧛i7Zj+ON *{bqhB 'j_(&}2f/NDl3 Ċ1LA"k(#$"3Re~mw|Fkv;Ng}3^vQ@h2KmwbsTp}FbFAx9RH`]2=(ݵ=! s|Cm+# 횚h.)mw6MsAj4O+A RuqH#Jnu[,5՝ht;( d2T,Zʊ@GՕW9訮&HCD\Pn b? Tפ&ftu/9gXmu'+`pNWKtUp>Št$}mJz⚣i _DM:nؗqEw V>thTzl):mS}Bu5i@O,9OWM 5ʌޮ`KSrh h$3[lrY@RyU ;Q\Zy^tf -)xy|"yj#ݲ%wO+AF ~#yD M3+ӝ2+X(sΐ ˊ"% GHVW^CӁ77}`BCZzcg#euRyy.UmgU%m}oyh\e2EGDOWSyAӫ:U pΊaJ#@:tMo ƾ5I4Mo}>Z| 0?9&uug77yYcS]/.>(1FΠeubNg‰%0M`:btgj+,I7<@aB`>(lR)oBKaD?gxS h_,Hs|sik{Ծ+JNXdT9Mw*º χƈZ.2*lsX'ݹZy0abk &𼪸0Km D2x@eNI@QELDZB˲n 0 ~-u _f8|_#.MQ#GR3}(|7RA<80)HBeWd^ 4/zAV!^. w ,%SmM){ `!jI&d{) .3À=xЏU%BC$֧s^N]{Ͻ{q[sһX>ϾlWܠZ}^ïOFI2O)瓔-Dʗ|7 p7!@`!7ZJUuQ +Y)9{Vy%t8k`0?( 82Ž ns<`(99af{Qc5.\5" j[g1aёEDyV1UAtA.=~WTYq^ߏ- %\&lf xGpSTu]M fPs0DE: @eEio[!pڂ-PF޻řfg1cFlw7bcc~xfN۳۞XmnwWJ*BH2I2I2$$IH HU{ PCxZn sw g&U@jS*N;L7mlXnѰD Bz8hN T@g~'{upp6 P[?4\2#UNJ?wm.8}M`2i4+Jza*EXjHzyY_C֦c>fځ`n8x$ %%kسA?`jӈNwJ? ZkjڮS>?%?D6"5qy#kjZ=^p.* .^q7@[whm%E j0nB$\hza =/j+k4_o}W!p'aE+J\IE2;3n8Mk}B @ ^]'clN6\/G}+2IE6Cp4M$w_؀>j IڿD;mOt00e8GnԫJr iWݝ:Ϭl+j4LdgM{B2ꛥݰ f3 $tf-1 eR :;1TДX,ˮrjRsI7)qD[[Xx>uھP%a75=@44&)-hz1uWT޲Z S-5 X{ xwZӟkb=oEu4۱o6c1=}yji޿#h-S){Z;I1U|,k-O9%M>I+O%p,bэ'(o>mRK5͍< BO&Z㏋ /. .*j`YUܐS'/vUW`kk@e#7,3OC3)RZrEX&JKz14+,8{l8N{9)a8qS%W8Ή v$πc1 IUUe~Ed$ [}ƻ:eHˏy"MPfnnaabM9ՎR%!y-<)ɭ^X^r 7A y .*|T\a=ge$|eO| b+ vb'uEk#@Aaxgc-qD9w-lٞ0,+fN=_XFcK;^Izcit;i5oaII*F@n5zTD׼ybIi]9喿ʲaՁVD\H|@G*.&*{Onoܕ:zNK&Z>ȕĎpǀ0d;r%'0Lc+\U"E|b_j .;(>Sk^tzy*`.64ؿ)55T?/횶Ԍe4W͖V˨>8lTGh_zh~ Ok1:2/2Jھ#k&`-*:oѳxp<~)Y;zAiwdȌÅ)x<38D}(Vk3?NG[tI2sC+MB#@Abz" fMS_8,P= qd7 4-9+jG?sd,93g~r/"Q/2ڧ?pb.]ދ JGvvQoE/_vv}$ٹ]B ;ӄ^r.)sC/:9Qc//2DM3NK{$zϵXfg.)`§2n&L5~%Yi4?D!)FS<(E3ȼj4e%EmLHnpt%*5JXp;7 Z|{$zgg=M+X׷\|;WY*O~,Mﴷ=ɪ7Hkžvrq}<)gXF9 O'dpagYF+YJ:%eɰӿS @r6:]6p MѰ K,LӃR[B6Ў",*`pd7GF+~!zA-$,Zcۀ?OCiWn@`rZKk`k,<$e,MR#0eQ gb?ei|Mwt}@5N ~NvP]aR1Z0Fbq`'/8%m&hǹHJ]+j˰۞櫩y_ߥoA4 2M:JPƣPT& 'MYM@(PO$ pV8KMPCn?R4MjMu}cF*Jk-QpLh~v~% vmSV V]NmgzBԚo%TcOT}Sh:Htż>em&={R4v9]0:QN| 1YY9 |8x(FUפ$3c%fVG"^Dc`Yg5ٌϸ,)9zK4'KD2q>|+3ʖhˣf& WdJ+W8yw/Pw47saM?om{D|ixWBp rFzAV\%9E͚N3Lr@TSCMpp#>[ ?dΖRMt8MҭŏFs7AUNE ^?7ZV&޷Z`BޫezfP5{ROݭmO>tD z4[:M/_v hyT&yuDƀ.m9#xtP-QQqfEP1UV^CAUgB@sM_z|kc1k)%ŗ񉥠, 8 MYFe/8+iH_ CpED[̦$yK̔ROΩ8{&|vN<5qepig{㚅g1Vl<'pz[XpA /|f%o-(tͅ;qM(HEy&eIkI= ca9M/l2oTz79^`cT=rN3xO\WƏ%QǙtça%?@;db&NΗHmB3`~)eIB D$H.Z u0Q+3#\z1/W˧'S$](`-vb%L)*>/P3g_C~FR QLL6 5,߷36GT\6Pvr ~ 3!,!`@ᱡ4ctttxjP?֓ر翖hrO?Üv)ݔ<)YSSrε6lDu֎{%%W{I2n՜p fNb?Ԝ)גPi\Q#bdN]x2;R8r{[јc-cvƠ14sLs3}8 z*m^ǮCn,IZo%SBbF%J%q _ygTj ;'Jz)j@ix)'NcyyNEai$IY<a&`AaM|;q`!xhLo.bT\|rgYVIa#>_3>Rϩ紺*miehD;>lW Ų{o~)4İn5P{Փ'.(PXAqpp2xCч+*zp>/MEyvREE/j4=h-cyEwqч&H}}9[Ϣ{ cb3 bR< 0~ς=)[lzɺp+܎Y65C@`퉏6Ye4/u='笵vm$vI)p%Ś؉8SQ.+[/AxY񍴂 ?$FfjO霪ba4h?mc Ƥ*)yspۈW㉧cpɱ]m?XFncJ<8%V" 6.7;TKDX(^L$8zyJ?;_A/_a_4p *^$UԾȷ'XVok{Jo[OTIY|S1tǀ-- 9b#wHkla9<\;ën9W蹟,,iKRRrRׇD1 bA#+' fOKQ$PL;  {| { rBh2 SL1 HI3 W-93er[iZRßrI#)bOYRR7G)[&1C0RlH~{?mΦ#XO} a@zfIDV'\-AuJi^efY±Q cP4jZz,Ĩ6BHBME eeܧ1nSkfzm8jDT"dV4GmX3/eGpzkҩru,FKSPj;b$sYF9-5/.׸3!QU.]iᓔ~q'k߀ۅd#pau<$])z(>KBe37XhK[R/gvR3:]q cı+6}1Iӓ|9M%>BҔUk'웯 ¡{ЫG>|;Ia.YD|)]ϑF2$T~TCb[։KAb&A}F-/[` #ɢ>2\FXhqM,:YrInF+e%.\yC%vN_TtGMչ #,_.Iw~?3!4?ysBE (z[y;?OL}b)\b໹ gIY]2 +>KY#cGLn: h4YfvayhK!!7^ 1 Qۃkwێ3M2=gH\-y:S:l=H%sܪb'\p-x%әf}غAe +I&"MUՑy6O2}* ^?)EǴ1 $aNj׀}v n&EDϳoH?wf\:E3ZLL qVg~|UP{ G{2I"fy&,vC=p)lXM.}bh's`#ؕD "~c q.P3?%pmʅ`+,'X3aX?!E/ݳ:9PH_V 9IJG}`q1 AT_FPxH!\`H#90~2 W䂜4]&)uki)3ב{ωO/m%#3Pճ<ӑ1HC?TygZZJf zJЗOxlYHFtZ wSiz}9̖^5j"d(4C1¢?`{^ ~8%wvWq p:7)`j87L$ԉ6;N]ஙE^#YYn]W1$IWK&`psZ;Ae@%dt#u}UlCU[x_L~W2anwv^,Q's{{ZQZm+TBGJ)tJJ*eq YH5I'uCJHK"X6zj .&X7cLلb/,w-iojaHzS+YH*BmYSmEϱcZ+K%#h^`y>qLA8a]jo 31)*Xko5ӈQgdw3u!dpJN^̟[3p4ނ7KI:4 .u̔\4[n ղY'$-]r1͝nTV+!ZKZW}H^yMiW:UCi`\4ن߉>VcckVK]hx/#$3)ژnG1t9MUCN;1ᔨW7 ;\戮X_lju@4YCاS}eFs cn:Z^xMJuBDçJ.x]yȑw.t $>8ox&&AJ2#.^(=@RzGHyKK҇yۉS0uӯӃ;;vT}UQjRI]1,.\p/<+A[jL%%W1R UΘ#MQ{a-_Jb7 J2 qf+T%A.9 Vc1Yp F"b=Ri% L(!,3ha :~K|7X9=P2e'|߼_ؘKK/ RbJ|WPQgoBB}tYYu['s:ѾI=STt+dg|*R4'9~W} K {)uϵ3^χW%Ƹ?NDhi^m/6Je@Vcno_">ed8Mxj, 5.c>pD@r"N ,vLh3KjRJziI˱S"rId`T-=uWh:^GǓz$8DDF{ UXeb ގ$m(]]fVpZVih5tAޒK~ B*TJ,$6Vt|`@ '/o1/R**6-F%˽`n)_EUq ,^g-p<kVU6iwLh4}p|܎* ` 2 =6N:Yz)i 6,IAџkkI.*gRW>M=m'W]x t]UZ_qk{NϪo:n)s$>HcE &𡉽{LerVWW@Ծ ˙._et1\O yQ?P4On2-}fKqN8|B/mQAʚp`hB80Z֭7O=)Du[A.A{omY9{w~ ZIJ=m{eq=U91Cu[. s_@zPHsL7`lqǏū7_)KUY 0͑5x+T^DY༶C'Bz]oӀ!WמxuF.!dvn~،a\Rk-JNT]/hY|ʹH}gW۳/Јa`a'E{f=SY #'Uc}זva 0"gbsFxL9I%l?RhhĘDSV$\-#357UsmA\X>1p ,KM|OM3v !~i d\ψqBfbDJIM+\MIJ.hE҄ptD>/4]iv*"2QrYMkR,wiiy/a #rn2Ѹ٢L9WNsR_9~(O~vp'otS? SjNuvpE'#5\B-nQI ]e=4j}yRN$Kj ĝH pYm #7E J,8#./:BOX.\g2dÿՂ+l36ߪbR+8_ Gn3y걪l jE =~ _,j+zT:Ll^5gi4* E }xA2zW\j+!CܶUBh yB_F`_ Nn yYFѠeRKbg8[] eٞ 5 %WرR^; В{@u73,,ʩ=gQ|'34pr0%{f06dͩ6x)_g ٩oNה䰉>q?O%wUʰUWzDKK ZSKl5HV~x%Ț#C%GޓbG:$%j&dL3R!ƔdWI|J)<8n+ l* ӛC.?FDjz/N@ZD?LqY HIv% BR2, _/~ۄ)H#6(G Z>=y垡,g9SD8ϻpNm :_a?N' HIք5+ '~bu0-v0478gAeT$r5ԓ,/կoEHԿ-Dױn8Z-F. 0Zařp"ˁNⴅ'Ըj1aE؎_d*)guIGvWo O g-P' ec<0n:c ኑZjR\;6e4oVh%ruN_U5Pc6U`t& } Þ nxLA] Bgתh~Tnq1eSXYY7M7)*/G5t}|CͯUƸ]1&ux} 4Qe  7erVXyh u; לAwb;xpxxjxNaj#-DȚ)4Pfs^8lKSFnjNNN8b<IȇHo}$EoTVTH`e&JUYpɱRs[hSuR K%gSsk<Ķģ(59YN@uʭʐ%\k~$$V#iիp`]w2j3Í8'&e?ѕΟ]R$)Պ*JgeYʯe`32n0*f#чʁJJJͲXu#k7.adiM~)aײBszZ;CV;^6He )Y׎ZnS3.[<['[{mx ՛SA9U^COfo^PiG@`%[3GTfȻ 6R37GրQqhtCN#qP3pz 6)qb> &k/3Dee?|,3N0JNɏ;o zmVWҍW2E+!My<_t8()95cNQP=)YnҊht:gK4s8j4oh?ȱ w**J|KV ^CphQqz|Kړ˭myzK8Ck!ad}:NŤ4DȸCRD2ݏK_s&?#}H A6H x)I;%SNMxjgd|>gIZgv_չw"_Q?V( .s K y>%_rO 폯EJXoB0I>2u1,\K5mhXhN]WW˧2cc :ЌKP$"t@lQH79u27b{=*h)/8e=L%3g-% -(bÅK܂󢅹F֒(1lRMc/D R̖h顽8( 3ȅ=D" LᒗXf䊈RNFx`ɔ2O;w>ٕY1 |Zpǵv-p]*MgŐB* .{mO|,\i𱇖F1qq `@p[,И}!lǙw<9\]c} y?Qq3a_N{&#î$rތ?~`#8%ͨ\51b^־DQz)K?+hiLAI΄g; Iu]Mc'z-SZ1ksUFK4NJJo 4Vqnђ^a 6Q124ɲmh2L:fSftXo<hW1[J5&ˤ93' [%\{Go/ӎ`Xo^ 4,sŁBҰ 7J_yC։. ~i5׀0NZI Pz|ASz>t q8@~鞃d-Qg7? ᰤPyk"uſy[^wJ7_/,f4MfOi[Ͽ>]ڱ;G_?dW^F?Ƈ_oRXWicW?rz(ڱK%| _ ],.,8/^tM-FΏd( X,s ol4 j+kL7RJ|xS[JYsk%o,]vKȄgKVRqR 8]?<횃Y`r>=<ѐ.5#zuyԸ48`,>S*6 I#VkY"pGe=5VdeS7=$_9|, ?)gv8Ŋ4|A{Sw'MXa^jϼ@mPP2ۙW?OOdU'jS{!I1lrT *BB_uO[c/^e،侪@&,8*6^5N<p:3ϱ sSR_̩;Sςp9ynj1%YMh ڶg?7l)T_`$'DWj)p]z2m` ޷pTUzyY *YvfsB~T%n CBgcuebYkId+*zp|Ǩa( .YkoIrP.|+Ъ0|x>#6VW~Pp_S9W^;kw){݂+G;:_cy_ёKNJz+K .k(HwGN\-)eO^ ~TV5Ti4O*FJktäjac^{ͽ@]b/;q*µ0FsF4r]Du,ݹ '{E 9MIR}HGWO::U [ ʋܒor\*?C[:>pJ7O͵Vm_::z:muT׎ù3;lYS5g2O< jad򄟨֛LʚqlOic)i\k0gOJv?.Xv5gH4̚u.8hI͜dk&F,T\i6j5!w[ suI&q0p$kL#Cyà$0h~WGX2.x(>fWOGK O^lA5 bna\j`eC 4}V;2lx8jM4 ˦bisFI/n5?g~"rНt6~.xÄkQ>_gf mHWy;?`Ҁ$@§HP9Z9㷰߮WOǢ(Q^^z2Ûd--p ط{>{Cؿ`ˠːuEi̭t;VM(+s: 'es7Vn`1cz(acC`]Buw?$$3s.p܊JmI%6xAEK0a͑--$`G;8)CJe >QJ-Z`_$.`mK)-}st, Un66߇@>TH>Ԍ 79%BXkvk7:%m'HBb'aºixT2>wXB*vZsOcr[Mv[TW%hvrPF;% Jٳ_w^^(jY{O!ذLuJS'/cr횁s$i.jĤ3kdK+k-Vqyܳx"g6p(iAkFqθCsHxUSޫ]~߲Sݖ] ᧊x%^?RG гSo)#f_w瞐^p7-j9ܴ [QR%{/ʲ{`:?@:%',Xv'Sէ: 8‚#؏< d.I>>jiyRJ'GF}vJ5Di9 >ς2j3 b tB x*z|FU ÀN@0S5fRs.)D%<4]-2x(\s^[t?ºCe6n_(LV;sQ^ jq^4 '7͖)i2˦t3uu\~yE) x.;)m7?R/՝ U0H+3X,1~DcsFGTU爀=3&Ap(^ 6,+\B.< kMÛ\J]?_fѤ$VW2y}sgڊÌ3se+ډ,a65gUYbnq7p/<(1Fi,s $S-" ǠOڒ>@R]$<}:"H3U(`sU h% Gʋ;2^Y ^;P[Kx7);ûw8++[.*OĀѪg}@>tQe\'*c0jҠZT՞X/sܛeIl+Yn&="G 28u9S{ş2ܚK(|>[\hb4I1$)6%'݁ǗgoVf' +ãM~c+΋:d6v A @Z+ 5t b} Ͱ̫|W5wL8wRߪ_zWժ{q6TN48H}`o/;D*Mt~>>ѥzd5] wLqE}e7~-I,NmQGTL1=P %T_;"A.gɽi̎8lEMA=&YqÁ2T *JIPWmM&fInTs>9G;t"G\9ZȎjI'#, )8N.ᱢ“ݯ\?#ǻ.(/H²J~wU`/+y8t s{gT%,%H^lTϿ8ք UER1^rQy3Ssាl-.Νڀrqq8ھӡxg:DIyC }KO&qj/%]YWT^??'?4\V~bʜՆ!k-x7.tiUDO½*x}뙇@o/)0܂ou^Z1yڭŅ{;8p\&IPLa9NSe3R&,RsDjioN,'sSP&&D!"- cka8tBu<IJgq^O2VFQO93oU"8FiU*7vZ UCRmo=*.b K} ɓq,d&`QFMryX>_q^edke倁(<F]\Ee*Ht-^;AA:UQXO:$:3F##sYA@ʧ*ǯomApߒ8?ſyuR@q%MstdU] W 6y~# *j.#ԟlXmƫjؗDX^x5t53^ux~{p9X'9j$#ǿ.%1Prn>nspϥMӑ X"򰵦o?: eFA$SQ*$Ie 膄Y"-_퓟_ASlN*u y .J+t)GbȐп3?b7Rmsn=IGZ!N*כڛFV}OP&_"?9p9g9l /LPϷrƍr[$BG%ϕ}' V5)ǀS^چ, ;c?ڕtzw_9oݒ~_x >RzRQ~׮ѵsJT_@9CN}2Bw *ֲQx:a2Ԕ,Ds[ET{뜦uB =g Հ>zAMXwM3'5=o8@^6ȦagҤ,tҒ+8\Y pu9{vsǽVEq.9v?'FȤ4զ-4/%]7ڼ\Z%1?khXjjg-DuX1t/Zm sgλW"J 8P9ayS(j;W6hs4eR4ũ(_X@:2 dAd)PhgIM[p?CM+m8ǔp\!j 5\_m* .S}{pKfMw2[nz}$5:,Q"nmm )5ܫ(mc~\Y}O?OΣTGyWv'%{rQz%Z:tZ~O+%/m:O#Z;z`=U<.ul +jPckReZ&}0u5īε˱ݸ؉?lb?6OU]il&~痳n _fw⍳odQلoUwdt7J/]Q Z׻jQ^@ νyB?"Ha٠FwS,85X^5~|44VHp:8GeTN"3[8fU"VLv5蛇ӑ}2k7~VGD#@fhC=ݱZ8\]VY WD$M cc[q4D#~{LjLB-W.Sd$;w^To$XQ!ڞ=Q@^Tz`gtN~g=va<*-{띷ԎxY`DtG>g68~~2^VM`Scyyo~PSvt s!Hfea{c$F$FwZkcDb+<{|D-M#ifަr-Xj/tLdj'j ?^``: m|DօTӄ7Z;gR;i+f__h|W?J /g1ٸg*^IgZ$ǃwb-Tͥt^ wGQլw,f8HnƍZ*~1~jmRgS6ٔRD\['}+있+6н5]k͗ڂ1ڦ= Q9'v׿xJ?l.F _;zXi_~t˯z;_o8~_^z|+^wwJ?Źw[-ؿH{B-i%'Ogo"!yr-抹l^®L7|zMpaQbh1& 7(-X\^mFp,ud**PK3RVY]=RXpddԖN 5;9Y|Qcm|VĬGr`y^{ywKI]V$R]rEyCyT! ]$UqjAn։(EMWHm[pjLH53/_N47,[mS UoY R~%Nږ!ҭ~"̢OW!%eUvg\q8cf8DBxhܩPq-kNO ,,83 ^\qfX^*uw0.P}2ܴG{@ bGȑ(R3ټ_LZ g)xG e[ 46B+i$٦i/*,mD ډqi=ۃ ky298<;s8-WtH+ד4->= 6]p .x5 $s0(⛪qtZt]~6jzK05ʁSW5e=5M/ ޚ'/Onܩ[|G$E ]ѓ))QeMQ~N~M(oyw?^Xz :!^[˅w[`{U=Y@}ѦX#@k: ;3'<`uihXL:v-V8<ҒEajiO©SW*ux)` X7]pW\C >73[D6$)8a b Ζ[o"0aXpu۟fo&KM߭=jyZsc~9Z`ngӭ+*.fRґlz3 LJ:~/_4ϦOҭtVͳLb-ٶ O3~VgϞVG7Z>6ᓿtg;"Ԍ>Aޮ%۟xImǿOlX[F B*zo>G)dj_,N)@pF8m㶅hl nՒ+xoz&VE.C# /;.x' ϋUͤ@;^BU3k[2l K @9,ZMoK/p7s\{KB˗V'WlI%Quigq.#/8D`TB辏4WXt@X7p2}EJsCr0?"Kg]x*I- L1L\Kެ%{4 ?-qWN:yDLMjLf@NY[響GQr_vˮz9f? 2ltXUl_=ӬrqE _ڞȼHLʥr+_Q-NqLRSDUc8]:k.aZ; [<4?0x wYq4 C gі5H4`<ʀpd5}Uն0|S\X=q,?>,!Z@tlm{WB+?+P@X6uya`7R1n&n J<KQuV|[:Cp Ph38} 5 .Sc=I0A+_HWDrT(QK ReOy#K\nb͸4eS8Ve<9HH_%׋4^C+R!zg29Jc ׽Ɣ.4J|φԤ:ysmļ$n@ Pi ! w .#b,MnZ5wms~\e!r ws@+<jXg "W9/W=$_>u #c60'ֲTwWM}Zm?Vvpjk쑉(THɢ~ -RɚcPc&my /B~f&;|8KKQ Ƶ9 eBgj4R^?|ҫWk5ЯLõFѻ|9qrbp-Kf(|NG{ܪ&~)UU%%q'MQJdL=ƭc#LFir6:/ s.D+T DvbVgQ RFqaa$Sۈn|LԼԭf!9@ЕK,T?W˫sh1,'VJΜt8&kͷ$V&F;"ZF<9&Ç 1|{KyWoIֳ'ӗ '/u.~w koD+R?WЊ;11fy\*,aK^{pM]; 85kz+uݮild)\|U{Dg6UUR>՘F06'5+&MJ0XTh*+$< -K'uU1nca@ "np쵓j%arc6\qg){_-Qk-%&񫫱YN1}:J6B`c]JRn\c''Mqb;3xOOtK֋A<x Ҵfc;wjٔ(%!$Ňĵ93Tɰ>9wX@S鼼Bco:.CAna!>|9^lR5}9;?huP Yߦ su4@܉E |u@XLc -y'Fcb%V_ /u+4B؁iڞ VukWH6 wvc "<-jОq%]~tn6 x<38&:17E<l,2vf_Nv9x3;p+V |JIs+_w)ZN~~vp:UňV?"GpaScX5,KdSMklzTpt(CTdHC@pI>vrHkZ O?p1rQP_.>Ԑ/aO#Qx1\R"'}( jdQyiX_J|I,q YTWh-BW:N.oqɠU`S.Q381e9Ob4m @`bR a|Vr!6nBRdi?Rmv{(Уta| @T=XEP6FO R=Ec6S: fћ4WB#,8*=9 31 zbFI}e3jIl?*lAj1<8s_}لm,XTxlY3TF՝HkeϊfgjwFe U­DZ!UJV-)?+zCmqm-OTt>lNsO~156> }l$ MGσfCS]3xG'\ . k򐠨} W,eQTmeaYt*VX@CbP]"onKl{K9|]Qt&US+[|,yxó\Qv!`_q:&  yc롊%)/UϪk7%KX$2T܍QZf!؟B wE~SxyVH't P\r:''a tEp隠5fA* =- ^u)USzcUo+p1܉Fx#J+aXq'>s1њIdkhJ;ıB\UDh(< ֮:% G4Ӽ*qF~A kp?%5MeYz mhi\"݆{յd()N.@:ִYRatBrgPYl&R]Ǐ OIjblq:'+AZ: O@Z.hhAYxWqj8›0̉^-jc]G`{'ؾсuOXU{4k)թ熂vd }}Rs0[9N/Vi] q~^Ƃ\;)`O'N\<3)(n0bΦ|Uh{~w )Jj:[΂{ _'VUѥbDQK"<.sQBdǵ0i j Q˸,j) bmU%7ͣ H&:~F Ym1J*(!p9\{isgՄ}B+E~ctUl#\F.x bN}CI(N0cb'VE«Ѕ>P4b @ʶVNRC&z6@7^]upom ~+&i}ыb)hs: oN|䴏k5'|]/H~?G_p{ߗ+o{{H2l0n|"MǦ *Ikc In,JI.Cz]7ԐQ2n5c 4c|xmC^hYGң>z o^HL# h 2h*Gьdb'ul(g4ϣ"ǼV밐ҏhM(;뮑0nJuWtExڨP%#+7D Q4vL!X!pM%"}ѣTLi-/ |JfK B4#7[5pG7qbLkڬJy8[-b! w$^ zH+x R&VFWE ./+/ .Ow9Dvu0;]"F0aqFK#:].K1ıb2:u'=oߓ?c~Vd{<1J IJdU\99R&PTtjt:'1 Xf, { 9D(,ЍH(X0zˀ@Y6"`ϵCaC֒!4fgs'58 P~Q#g@q,yF>Y6rr?1pUVޑ"Hs*ppT\<5]KKoښ{TˠEdʀiڤdF}8œ =Ϳ5(U&u͏(:Y<_>XTZOul%OՒJ ?BWT5ԣ|?^ߍ<1'I L70|0І1bG@f+c20NX&`E$X)RϣyūOݮ)~4I)+NF#`Emd}l=SWDXz: `<] *D:OܚAm:kRd,hc@AE@LfgIJ#%HSRΡ"W o 'R8LzM/HőjmV"S5nl4\D }j4 '/c&Y F B^>#xߎ|n*@:@4DI퍊C)R2VuwŅn[Q~;lLc&p9\~^ɔ}&[F!yJ8|]4)a DA҆J:4H.{bac/>w_T6KԀ Ika30=RK"7!_TAXAOI+lVS&ퟞa=_{M_mo~> fTk/I7-t/} H8Ci\kkKQJ*#FP|sRﴏnt{tN~wQG͋\=x~nd -sϩs>(°xd|Y*ጥ]'}C[%H~)B'2$9H1>phIKe+dG6ۓW;.ի DZ$TTjԳ %:@H`^\@tc0"T/(Rӗ]бN3\ٗ =؈ZwUVyG]u[xvBξ, UeRVtS.+.@ _Ԝ@d]EԠϿf"FFq);}Uu{UTM^wOiunu`=3?򩪚5$D]ִi%N-M4EMl#eiM-70<1iDpm+AϏHo&RfSs2IP9@%AD<]!g^R2=SWgAcĀ vS'>% g%9-;coƙvEDt"aj(hBK%dc n)Ex' VmQ DIcmcP$ rUVhB;"Lb]ΤEW YB:OR6w|3E;H(6}__:dt==\C'ٗ^I[CFb26۝jHv@u3fD0j2uE&FUս: +Z T 5zH^T!F`xZaæ.>]_ڶUO7{d˪i_z{2Zӫ 1~G%aRuױd$Wfۨ. _1'W,DQQ?F.8;ׅ!fؓ}((?PxXH 'FN~|`<qn2d6Ľi"_4IeRܔ;q%y~1 5-Mq!w&|õK #]DE{PqZMZ^O8 t`Ekd+CKv01uG+W)}~p5gU@ mdpĂ^Fq֢* >qCkMc\{0 ZK8lwג:s۟g"JIf|a:}w)s$O}T;q[w`V#X@"]فtmShHU'2%(:<K0g' O⦝OSE'Nn)練GM-+.l$ b3м ocށvkCWXgkf;^?]5sCLĞu9)Gg|b4h;QR$n>י}7~b7|lt{]d(Vgj*1:4G"C.&lSGdDF߅#cAf2uXs )ȔdִK+y"-4J4r*\(W땤FB<ܚSWB:UjĄ#L~ɔ@@ud]SXuz'_'}t"#4P蚕u!%VxDγ5(H56>S<1ʿMYO2p9\~*S%%;KJFT ',Ppq_)fm y0GlU RAKm-I"P4N^g. r[4!Bd:6[~"sLE:yOOHNT') R;e(.2^-4Gy |]W>qݯtL7N&QM;e+ --fD uګz|EH_ҫ-hTri% P<%[6 Ƿ٢IB4O(Ol2>_QReu ɐV n=\&2'ŧ!; O*)v"2Cʾr83sx򬾁txwE+½aw 5x#3Px1[eRTJ"3tѣz`h, C"b0T "V>W0To`&B&:G)پۜr,";g@~@$ILVijH2 f`{_1 .W|HG۬Ik/&_ "BF[H2hw0waJ0g)fe4G̪X_:bikgw+8%qY_zcY]r{ykZo~|Zۣ1P~GJSxﲲ.Keѣ@K|YN{}~~=恂k )^ ;At> (jϿ&Kx#qK>g|,nh@R /8DX.C9ʊZ`80O,%%#'9qRrIQ[axhXyw ;*<k ᭓6*NS;*  !웭HA(|6vN٥ VB%n%.jDv(wwk!`tSK߻ɉ4))؜ޛ`Vrm+ټd=M0A**S%H WY|[V, bO "[sj 2wێKYԺ2Cr8 Yz/[;WWiYsxƢ*":@_SiC=)4?xT6+23}6Aӌj?ykQª1RBK )EtO65jhך55S1ӞE|@3-4P5{?@ÿJSg%?/4-[#Oۅ㾡z7Owp,|Ⰽj .,q&dbY {Nb'pΦ`^<ξs6] eF o**1(24&D0tѸ?/'$#" ID"K|BC_'0DꂫtgBEA ~& xcj[ƾ{oKZշ~;{Aw_Lc!:eD7S|+D\pVhc82n؋6P2Pbpmlgj2yfm%#ds<&SC87boz Gsy>,zo|,Teo",+ .ETn@ws2JUkѪrϺK>Bel=]ZH3'\İÌl } !uA3 J$Z^\Ik׿XIwu)ƅRJ}&aG2"XT\3J睔Q2]Vc q xS:\!}R-sAXʾLr."8/^tH,C9&k _ӗ$LtmcB.us'ÍWjʪ$SͻRX؋#7emTR lY4l2dQ=]O~Xw)y@ dZ نhtg%V1Ʌ5<@Bܗ(I@T264KFeeu&/awq**H'tѤX S áxK"?TgݶaΓx=pXkSi:<3鑘q~ W4rY֩mO(}j9\(Xaf7SW\>o _K\rË O_v,{ۏ4~RXW.gpq]S:|p)y :R{.?dtf0!zkVG7x-ZV[w7JG@8y -#(D+mTWm8`O1UHbLoY>Z$ٻ ~B (\=|h[jkUL|BxY ÙJZ}}@;9d1x&#&&",b='1u+Z^YԎg¦Vu{{f~Az%#-[GNxpU4ɺl20zqkXQ+c6pj7n/?f>1b.+]dOggt l#8dX{P_rS?R}bV⟵X/5~|⵷#9U3USA"HHވl\()馦_6ذ1qRhOTkh$wr6YҼ2`$]vX#Z=i|D#+SQvbPN4g;rSonCyYkYy Qg䙮3y7eG= :C_tLӥ^{xGvdr;eTfaiZVF?]rd2EEU 07J(,7 N'm#SxDZfFH(ڔ|J<^rnvIN+ }Z˲/ JFos*!ڄbU<ʢ BVҕOs*L֗X#^=o~Oiz}yW޼/-=7;_|+~+W>x=+K7{~ʑί7w_;#u4|T݋@m;w%J*$O\s _ө1vt- o;'J?CG/vG)}b7)NH)ZnlxPA64<϶Ns8&asGҬ(ר\#Bo~L;Pf+:xb--9dz"UzT>3mC,ZhgM.%'Gmd{52#( 9f&  =9W=mdlۆq@ 2M@NcA¢k@$fS? $궒OִjnwK7'+p-4m\"tqhX<+ZΪ9CΑfu9b;"hd\Z^~b" -V(|v8~0x 6"!0pip&/H9rpqE_= _%ǨN{EL 7W8Cnl|!),-Y @ Q,yvFs@ON>A4'4xo-ZjXa9}XtOrߥ\sH<.u&-Z VU9b}=U笎IKh14-QU{d5۰k 7MTcd1iI+}d&PPVF0CTm[⠺19W$dbKZ^=MTe{H rW#e5IdTӆbG~T2)ӮP %w?Ouԇ>?PГ! 0#˅iY]76}gq }$.8n:nzLV =(uuHۥwV)ll+ɋKkn&^6DO*JK'pixO++44<٨݈f%ZDћ%΁3iѪ=Ďes me|}eJ[Y5DɛM.V17l;wv[|:\Yx/zS ?>S{"޵jW% uX|>:ˏ",&aۚY:D9n;+u[1hEkj_Dz>buK4Q\<(v6`)!!نO$Vκw ky<{d*$݇RZ{z' OxVvQ|%YN56qdnɿ!ϖ;[p IDtg9~K~5}& _IHMt b (.M,M$_`F 5w_'L 5*"jƿium 84UF֙MZdOX '`w&! `bԮ^cЌȎn2]&eݐ-F*wFYmhO%Īj1G45?bi WYjS~Ib6'Hgn_)H̐3P6eۮ|#@wHEgөrv7MImfz%!VZwPh ")Mv@)* P\$@"00JDS2U F.)"Z-u6UHlMH'-v@#mُ ׎O+mM[ZTFV-At"hc[l]7 Jo ᰘEɽm}t=+p2s{-hN`@α&bE+'\ݰ+FqiBGc4݆Oevd"bzغ9YSiC#2 7_7_%U]4ii%Rx|m/CQW2|[Ԇ2}VjntwY>ݡqxn}VDW|։ܭQ,1{~nOZs* m`! =\~/-e*̊(y .Daw# @,巢UP|ˉ/uZ>gyOS*Q,;`~Ύ~珞:=Og00ݣ*1,KʊZDsG|LW~ؿ}mXGUS.C]_g}<䴑%n$9 bk>lYbǞy v8b.omyt>u;8JF5ٟ%谼ab%cMˠ;kb j֩0j.{d z tBo~vNө־0E(>Hp5?/O1nDcM ̊gwumtOe~P< 9t  .aEgEYX8*&,;Æp@tI>$"yΪ;)%RDym~I_>71 VI5%̸IUNōR ^ka:p6qDcB։%Hl<8^n$Ժb3oU:p\+NFV$8c΢EEh6i}wO'מZs}CܮZGD<*)QOuHU9q($(PX8I\;W a"?ԏУ/YTxS{:^(7Jsm };%֑xKIq9uuۍ>DnUyGf<1+cgQiHpOD-*ąo4kA{ƙe;+KLN0xfA/-3_$8c_=c"4P~{s[l0ܠ3>3k~@ɧmv.߻տ_4^S;n/-ԝ:ˏr`ڧHL:AXe]6ENK'B%I * fϸIWM%2<,I\0Rʹ?g BQ2u鼽u#%bI$ AOMk+y"RyRJb gzJZmQ$lV}ck΍ӌCx9['jT7+#+@4]BWk6Qj_dK MnS"GAhձ,|Qh!Ծ( #/p>1tcBc>rTK[r`ξ$t c}1g*]hr j4B?ن #F'*%9WKym%C@,&c" ".ر-NdOq?J c}eާO}h Ҡdͣ]Sh*[OvOWi+̰*~km& ʩ8h]VUlQRK??O Q zxeqaꟺN9pNjL}٧?Jl$bN{G(uc@ݞA 1wj1Z,xG>ӪQP2T1oa] 03flG@chs7#*XD2E DWY^<ԁzJ 1]IaS92Fi9PD؍}JX覡q l(k7nh| r"{W}`f<_~$ 2[W)wG8m#5{ڼ zXO00;zPx'7 fpk(@#hhSbͬdoߦDz"L˿R>"̈4J65}ԢZ 2Č6>ʄWxMJI) +25_UuP*/CHiQv8x ~hxd. h-9˲Cօ*(8U%NxBEďD@6./SVqd_q!{\'J_LebME} ~jgB4%2[ D߸.fReVKcD虭dy _N/N^)QMBr{川 thDaBeB739.qChOH&h.VhD{ʦ)%Tn#"P.u'74m χhrm#/PD]%6^wΧZ=7Ppc87<Gσ]r;3z\ eah-*0h40G<)o.OMWRZ:)F{h˻kb˻*ݚcž6*?vg4fOhzy/vQczX~w_;QUʅ_7L_~9/S'ꍼH.R&3\$uu="JvPm6?h+ 9}M&M'aX 7n>yS{)Ǡ1Šc0$ %l69G;qdC셽8&6caozy9?r-E-D"wL9@ )bMڰZݮPcvBc1jjR,\ ᖶ?!v3/xgy<3x^OcUUw[۶p&Bfܞ5 N$KhG._ [-*F' `R٪4,ϟLrZSiJ#[jˤ֔ &mHID38J G_:!n8Z \` pZ]2dG%{ɰ~ 'I;?{ {7ds\OQ2r4K.,rMԡmi4vPuFm(ujjaďdoxP`IoG6+M-O?ollj?hXNN~ ?~So[oyW`ūu㉌)}LgJee+ OkSedUWҞd$DnzLmOZe'5eõTƕ(ޙfV^JsUqTl(ʦMo~AjL,},>/Tf?\cǘ T`w-bXAΜג;^:-A| W^}XcB({R{E,P^>zE8&nZ83Ȫ^ߣ_4ſN"zS9NQ"g 'VlnZm/ /I(TcE!mOyYV'n*;K4ژ JDh@% F2AWPhsAOrąS/+%5qi\ĞP)o3k:7ֶD3Ƴ;w-D3ԜO*oO }A`;Nv&ek>M7PpQ{^8QfxݱGUw%uID7HaW%pb$YU&sYbC} X*QF2^ J" Veh)pǿq?NpKRsǦ}vŌ@!k2羳egN/C0-YKG,fT䇻 ,:l1X, fެoxX]}󻽞.A=ZsN`T|{fcGE ڟ~/rH")p'z/J lm[0 ECH!1I+syK?)Ө09>cܭ4rNqSrm0pzL컕ƇLK A3c*N8l[56mf-E"*P vbY)m*yX*v%P81Z, bXtjĿDcZG+g UXlXcUUwn:"u?bG U$DhLΊ>b(94ޖ")]գGbb}SIsw8E7]q.pxYD+#^,oUlrUYwA!:=\f~sMc6ЙK^4IIHZ,43[lzgd_Fj657m >Q}biS&q&x;9W-B"*p~s5?X8^!+0*tvȉ`hZE}}9n:t2L1 Qĉ NǘCؕ0}7n`y":^Ї쪏x6ܩ]ħ򊅦Grj8,36H< gf"_>Hf>Nb^aw'q9W)]ZHn+0 kQD܃'=3z{nea)%.ͫ|i_{E&Ps^lygg:is@uvb# jF: (^Yϕ!¬ O@b5;69vPi_b2UZ+4PD SbA#US}5WSJ)T(Ily;R0Ih^u]x ތlZ,Ǔ#6O)̟Ocޯa Sf~Zpm&ɘy*"2D*mRO$+޶KA!-EQe-dʑ)iQ9E=K;Һ'=Bn[RJ·7Ei߉VˢSqv\_D(G{2Db+&k%BTv6ULP3Sl_(3azzoK1t> Gc≶PBL^LVVx+r;SB; ޲ΓMk _$*|7tcv*0-^ZZ2T$")S >\KS!3N7$3>yD T v,RD .o_')&v6ơc: lQyBe䎍hV }nS=~i>]yo6hk{ KQY/5Fzm5wi,V~)ʙv}HDRv 9fi5I('Zg{"J#bDڡ&٩HsѴN2 gw?"bR($uR݀jhnWеO}pl6*@]RVdC\Jn/}A'c맩+U9"ondr{8~|_k[Ie_Md˯7owLس_PSDjhZXRatSZR~D.J0bGHJ\P_k:hKƸksoi5* =hz'_6xC&c[(+ůPъ{\.otU:ANOα]iFδ<]I(2-!"wİxyMQv pA[S}/Rs(6c[N@1^XZm|=ky#;EEE$QD+^1YX.$0U~~FR%f@m5!)55I\%ᄔ/}z) EUؕkf+@edUJ}ep1|뚶Z' qgpq\tnǓ) }Yɔ:'NqM^c6׵Moalǟ$O}>ZSHc0\G`3?h|?#9~fͣ_{ '_׍7Gƽtu!z\g/`oju,EԜR4P:0_ 6T}aI$3 I\`C#T;ĹI& y2+<]rCUzu+=ņ[ d9D"9]+%xq9v\hNܘH #(y}A},vDԇ h;Wld/*. wHϤfZO[J@Cˠ|l{octOb|o9K=YRh?WmMR?c}^xP]`[XT{S jc&FƆin~LsqMg}}HxQM"SyOp=)FWeʐiB2-Od]~Z_7n$ kgN[B0RDx`?31 i7}Why뛍Mr{)=튒#/I@>@Y%~s\fi um39}T v[_pjP]UkULޟ;Ǟ_~ʓ895Rl&?*hNL®`p=Gʺ`\?/2ە(@).1S۹QjX8)$$3s~B1t!XKnbp-u?@ +$q[bUl+ڇT;Ső+-RdΙH*U &sqzlPb6fd2(r 7 /9I| 5!_9aHCܫ):] "Gb*p9LvxĂfqsѺH1u{8rk").ׄ P 0*z%;0J-ׇ)ʟqV"ڧŃ-l'.K22O[1CE"+i2kV2L'ZBi,H?ѣOveb׎&v h~ *+-3XĂ"4]SjEmvSF܊fCOĞrr"~ U^Oʛ{E ukA* b974aR[:%#LMV󉱠oۃʯ4Lm3#4MI$mSwk[^-)E@A ƒiM&Yg^VՊHIS$%DQėl'sqqIpxF3w8Ž?{~>;)x^Yy/M 37`1.dÿ0`.ƽWKȷ0Gm4߁}ot< (8"dg@5躛"ƒKV `FIi3CSlYeސuLiwjXdaSɧ,Xb+{%1?bȚ+nG D\ _6ڴ!#4b&,AFM P&ެUqzj6HUO%v(@:x-.xoo˃te P=_8>s@ \cxSTԅβ}}qo_,}0trqQWn)m0S쓖>-1Qc.?_f%ByB[rb"fs}wi%sY (-+X^?I N:y X8 D3GWx>gGN\\& +`a#| wzzl!WO}jY3l\1lUzWVBroUYw6~?靯;o wGPf(|S_?e4|/3PVix~ W8I7(YkfċLnzL,M2!' 4o@CL.6G9q] T,4뻰S1FDCfX\GZyq SU@ p@_ *K@@~?$]b (tAr*gmp;n!B-(+b_)]tdդuζZKg UTIiKh>/5XuTu=sZ$#$"[IQx}"} sf /gcKZ\$jx`W)|ۣfn;KPP[+X3)P4 p js3ySt5%uh|J=f^jEpNZQKd^*Z\ո9Dg+͚[70Omwuf XY w% 5"Ed:E)~j7w,.ePhHDVrXMp#w¤Aäj1r7^cou.lU!xZhOf*?Oaݼ1vey! o7[95#}KDX6F,ID0'T!I_I|_to=PfM,1lœ)Ҙ`R8,Q #뭘US}_1#& օIjiJ?/ոfpL}_uAJ]n #>wsQMd)Nj'ʤ6%W1!>Z¢TBeБԩ5B]57:8F K9J4J9\XqA- Q#1,DJ,wE`JOyE6_ɞ|ϸ4p#RMeFgS[{ =iρӰnԀq-"-*,'&Sԣ- ""cB~ qrȻF}ډ- NFd$z):|[*x0߂lVkX`'5`h5Pr.8'"K{c*ŋ.wl^+{)y &^c`MR76]J =xSDɃ^Ԍp'F9W-e⋨-냙szEHL\ZV 8NJ H]<wcz.z*D+xmKjS>` Dﲙ3,BeTD XSs R^I+lƕm=l[A݆XVNPϹKݮQ ʙ9'iGׅ΀t[([ "I ;Sw0q{Ay\) OFkh|8|蜯b37x1'm-#lopySi/ >$~+Y}JV;m?ۧGQKI gsϞa }S#7 ˀU->|V_YD>kA%]9R tj$R%,SN[B`Vb aI b&L Qu9XTVyG~4̘). ͂BQmR0LTa8QU`xM*'ţDsoQ㟼Qo_>Vbſ|s-h⑦/#_z5v*G[u Vd~/TD7/$~#q/V*3B{+:3"F㕀o:x.LiPOtB,olK-V`}hd) 5'w{vdJ$vhHTeE7Vĥ驸U]3LƣKLvcY>\K2.XwКCLf8Es!`^_`G\r0FJdq%%K1Oy}S?e sr7$\nVm; 1}ntSJ')J8)YRLbM澚yCIv^]3+ K E 彩QU$*KJ)']o¶1Q39Tϑ3EEVpu$Bt8 q7Ha2Sg0rYrԑ[IN۸.|[4ɍ_π- x$IۚO?@A{O7(,HjsFu#\>,xsAqqNwAșR&ڟ{$VꜜpA^ee}XM_t2i5/<6@K_A2OU ^ץ7\$B6Iʾ@Ym!IgwR0ٱP@5L6"\ Q}a8;orR/0B/IX旛Z>e#yԁaÿ:.|S񽊊 &U?uxNzK(?-wz]V۠#`,hqEuBqNXOb `e9 o#ՈwƂ5ЅAc$ ) "Gܟ{2ˮulBAUrK.NSdKpV L}yovwtlPSs'% NwPp}ث{ X_cMP܁t7JN_jiL-ߡH/V* B?ĭ+<ڵ^Svpu;,Pc(}Ydb=j=h%=J/JD$!@}ÿ́;JEr{m ϪlHqjyZql-VcGJ#)?Fb+W\HlΊ(_54v$_L?3q8_ g`G#ܞ}HoDQ̹?ohTYj%n^4g׎HrYTѴuD]Xb1OoX##ulkf)--J2ubmMrx#".[L}ui{-gLEJp, ̵ VBXPf*3]$/Ubv̜X0KUML׮rQIr8j%ٰ$ǃs 룱s w)*t@zz/lCۓvDžo??Qإï9>RҞ>ӧ!E}oQiqD/ǧ4/ Č*_4sbޣibZQy,:;D*&5Z9Z< (7?u<ذ> t‚Ee˔kS2Sm.imJ4`έ-VQܾ d)5-h"+$\Ũk E8CtB V"%LX)@R|U/ʼn5& ZH~n5 8"S#HdE _AEP'y@ފz{eS#8xG # `6`]@?.щN}(\”/w~)=܎d峖\xlX z*inb SbR|],,="Ab۬p-TQtLbhʲ2ݸdTS@-Q ,X*:Ozwzg Oƴo2I,*z&E*v\Kͱ@wU7n7Cj_9MV6SizBz8~cePCܓu=ߕ̙t86y"Iy#o~󯀦rrСY1^VzEw L}01Q8_>5?+`*qJ7A5{mMW-~Zs%nMEhbRt?wrsOt('Q.,dBq40LR$_cԣ$dGT1V3/T9kld \sc<+gl#΀̴C\4VMngW0{rrNZ-Wݞ똅~;.Qpw ,\33k.w/~Ei$j$bLCzףD"K?(f؟vS>6jXR1-&ԶugPqvTnG(T:PcGLɦ-&KrۡFjYqȖ>hoelP}B^}q|ƞמ`U=8DZo?9q͚|ٱ*VsAOTX |W1:%=)9qDWe O&rӒx2eY)gLa6KQwݣ:)B}}bP]ghIY>Sʛ>J{-~D5 Ý&&gl>mdPˀ|Nm@츘g*BSZC _k|km_]s-_oT4+7e0\$ei(.h4^Ixup2UQ9]aQii/>e0/}bЂ^GQ=zCQRJzI42< =JYePd 9#@be/]\TN}q/'Z[WhR,bJxM=/?';Z/Z}~UB7IVXZ$%IngRz`h!l~nV߬ rjR5y Z&{mfm#2D]txOiԧ9lJNNq5|Wy!mOHh@(C);[!Mn^zU{:M*-c2}tH!S2S1#QB& AV7H/T t?[&hrK1B6{X߰j#L_,-j8ڻl~p9V%cVxs=XPl [vc%-4A\ڶZWw2jP pjK=$6[c3# . 2c+` {4,Bh >Μ-Լ#,SOfG|]9Z|,~|p3 >Ţ^_?{ 3L>҃Sޫ[ '^ *b!v&'K^8`xZXg &X^iehf򈎃G4EgltB`H}[ZJoe2y}IT 4@=$F DK _#gx$j,&Wǖ8:ت[Δ#}j`w cx?b3ĒWk4+xE8 $Fr` NK"QsL;mq9f+{|_زU1DW$aט| ]cL,ݿ$ ggJ pڋ9xVx{/ (5jۢ{`I4}'a&IvսocpU>Le}$QxarN"ν"4X{A+,b$GńWiCt^ٰ԰EqLnU6mAͯ=+}wM/i_>WCCPT6N6"m#OKZshd 'Bl+ʷD˖PP%S#~bW'nR#q\n)ZI_Eh֫7qm}a46ލU1--)'mTCRdgi!NqNN&#/B b|װbMc1BlbE߆%_N2" Jyj&Jlk7DHh@+lcWF~ٸROy)O/氡ܗ[Yu Z1;C֕h4`jT$z$Z4ƺDH/DZ'α^Afą%IqoECg6z NT a\*1P6A`{_~뛴Ym~Hxru>t /+'+YL/a[ ;/-V3Q8>jEq};m*15h0tK^xgU-u ,u oaw%RS/, /=sSrd"e1J4]jd&%w=ŔLo"DBՈY($]kL偅c9]FNt=Q.߬D% em,̴#p/8ؒ*ik6 ?v@z.'Ţ~:/^_:(_.\*z}ϗHk 0` `>p=Rd#5-ygS%Lg_<{%yLɍ苺H2 xx,6뀮S<$:EL_~ f9si8]WUV;eW.xw-I/+[?=K%T^ҘWI~ &zn[R#ӸUN!74O&>AUSFJk3iw\ Hrqc*H⤨I'aĪpsܾsU>Jbd-Tf%!ҩ"tˬ,F$r rA*-ETٖ#DMI4 (k͛Q@Yg($N5  +{ܮq_I7AIxRS=)<Bc,Q[;d"X)?nu/Җ. *CsQ]$i"Z%ki%zPmnu%h@eCsYdrtSK&guLK@D[?kbu1Is)(t39VJ1!e;,JkjIxMb8,44܂.I wS`(k!! T';z22/]d__*v✩]'|1V4ܭ nb+~Ù#_ $v/tݠLp>sBD<$h&m4<1q$E#j&me Шi EfZxny(dw@8|3mtx$Aՙ)KOyUⱿV ^LJ%3Lsc >J4V&_`(nTHs[] #HK@;nL|?AwK_?1:}5qXQG*8Gw!_s/6cc[fHy:RX^_8jǬ?ZG6U~#gD(_ۇp*o`]Aԕia8%l;!oIb6.Cy$W4xfr&HJvX% D,`#SڧM.ؑ`[Sv)h] Z]&ʾ ɛV7v ,WN\QR&q:v_ 7ZF\WX֕T&L%^E/5 =cr<F:NUV֋~7aA8I`ykd^6-Sޤf!*Q6c{7Y%]ٝG+3]?+,E2 A`s؆p i0`u)8V{<0f'P&6N>$u8:3KM*;jk*XS,D&)-CN[&bAS翏q^- M]EtbyG}j7{(KY^=9P'lU CvC˒A WqSo~ _.7aeEphKXd%y/١焲4[i/MupoW2Mҽ gQٵ&DzkEFzssýNq-?,-BT οI4Jgs={_`\#Dbc.'evs@iC:Jz |ԋ+,(t,:ET)%:z\Q]i]ГsjD架=v5LxI%0l*{< ;`27ox*i2]u{mቿY;@ԝŮ!Rw-:.BJ )6Ng+JJzLf'Ā:~hwA|(agH]9Y'K2"VfX}Z~9}/{YM}EQhJ]q0=a,de Wy(zh&@ ,n VGϡ3N(8P)}d8140pH/: &&C,RM'r waRs@PgyygRϹZF/f-1q؆hx6lב ej=  8 'QgoY mT2-x*FEĿUξh>z2L_B Z7rV~1\9gޙd Irᛰ3~is|xf(c<Y¿ɼ>Ds.SX0`ϊ`Tor&RC<$7_C0 0I: xNO*ƒ{N%TϢ@v=kz+&UU`5eՆo yKz>S:oݦ}xLRU3S2`!䆿*ՈA1ۊ7)2 [syD4r);!{raGpwovзb1Zj_8L \mࣴ>r$bo"pGGj98s/-A! H(#w_vf[Kf|Vo+ޯw\;GcĩH"kXIpf%*f˷u,qf3}*P" 0 9*ʉ8#Jh\bqv,"9 >c9URr ƛI}С"wrrsY=74@n(~xFR7DEV 5̉RVzxKKkWXxAouEz@\1 D0FDPMjf0B?UYB,ܨ\-pAߍr-_@;eeWlkZ6? n\jg`fk ]Qqe4zQ|&yb1%ʽm^ nKJ3$%Vg746QJaM6/,E/'ްLR|Ʈ00p\?+w޲>BqrC?YZv +Ah)÷aXnTf .`9\W8|Qq/1tlF4yݣ5%gWNigY'b{뢻Z,v8,0zE3Gy&D+~ 2%f Jӎ`$( /IQAΣ8whfFFH> stream x  o7Ζ endstream endobj 12 0 obj << /Type /XObject /Subtype /Image /Width 353 /Height 224 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Length 27689 /Filter /FlateDecode >> stream xtcי}9gLO{lhٲlɒ-9V=lAV(E+*W1 AHH/"ᑏ("XY.ヒ{7=v&j L!E\MUӉo9r=TjfY^Vg<\H-IJN`>nԚuJ8w2u>cZ"=<|8<f;%"زM{N$I--.U C,vCŇCRva4uZl b1_,Syva0O…rp2 EzzAf+ʿpy|nmT/sWI!l:i3rp2 Ezzf^ Eҙ)ϴe)TdgzfKDlw^)QWUY.vCU0պWDs(BCO|aK="N{fS+\APU/6.MuMQ̽r;piHj.:"vqr4uP!_Rm^`\ݝ* R|aDy!_Rm3Si3ţ=?}:нUWv(usއO]wMwo~ͷrLG|?]t՞oWX _8-U".}hؓgg]3_$׻|QE8}UN_I}'?}w.DT"bjUg|i:XDpQ4W"د~}bOc_t zW\63{a1ګ_||xPƃA9tuBؠSyB.9׿c{nW !j7\IW?8w趻>}sBׁfP3gNӮ5D_{yf}Wn_y-w}O;?/;[\GǹdO0/bcܬ}.0x.A+\.Kg<żˬc=&{Ycs" _CB>_o MM =E^7L2GYD| >O~si\+z UkavFφu:xI_o'fK!bW/K:f79\ۈ6} |lc.NjUMclunv_"~[ItD|'bؔBfjlP+a<*XKd|[=!$=)[azֵvD?|—3LY`Zo@b{ÐZ_O̐Z) =8$jf l B E2dGaW0`Kr%GH~z(fFH"ݠH"89zHDFjHER1y!LtHUP&+E@\+sW.JBP`R4h3"(:GϾE/XL@?)$ihŜ >)oH~NpǞO#D cPץOc,"&))V)dn3&D%rFVUo| k],yP9"XI@ڐנ}ZTCaB`ݍ΄FB3"XʃD h!8 .\32*([ZBɑAEVDdſ,t&cEP3X TSYH=D}9֊ s<Е^6աUDUݛնB$GERbD8Ba! 򫈨tJ:pбjEP.Fِ^"Dԩ*E X ,"."zk ACbEz@ēO>sn:uU:jxWn-gP̓Lc>ziTD3#|Cn5pa5vGt2jAf>pǹd'N~'~hమr`- ##mg_t–s 61\TPXCĕ:v{?|V9DmBHZVo @`e2| foNǟǩSMu+"n؄~6ۨߨ뽚c' #l^X ,lT ;~D*/*" 0"vW,%@(qIc}},6tH^4IPɑ[s/HEV#> DaC? uJB}:EaXD cփ rD$apL,s"n0> b(:c3$?yp~!?KDR| ^SK2ϜD}<>,e<,`eM%ꔴ1X*̯şg rU.ՏMVM!& ߓOO=r{A"ӧ' Un+OSf3{fv.Gd5I?C,<KSYQ+w&_ȯ{Wh<í.m/C#e BᛃjsϽSH+ P͠@D(J[߉QvY ɲ٘GkSa||_J9Ht$m[}x<{gDK >Of^ZYqaRVy:Oʏeo#ۢ|ws۹߸]V[]P $ ׿>D > ;]jmj͑tn_⍹nͿt+t@tT$f̍?k%w\s~ ȃQJ\ѠgmC#[E/J#ԜYZ^D8el%|gfD<> OPbtgDK =\K}O𫮬vf2X ߩ a̿XM[^ho!Ϳesʁ͈x'?MO$[2kݕڤA s$I/B9x8k^9Txh:hnxs Kx%s[7ꆳu_Jq5"6О.ӶLW867({tw.Й$'2w&7nՖeݕ@g_I-Q>=%2e<(pϾ*Ӂ"X܈ q//,WW7UR-G46О.w[2/W <7sPa˓]{'^Vh"8Ptgw]Im09-rK=7?\5DήaWS B!"[̑㿤^);^)*;!_|ZCĖ.%)Eg[fh3/j b}veŊU -رg :vk" 7o~Ni">pK62. x؋Pa?k_K˸v@ j6_Df2당%82~|Nۈ'rx&$Ng3?_tXM+=/2(B!sxHQK-oJ]Uhi.!ӒU5\ܓK(4GoBS8fѦ/ѽ6Z -D'?$~ӟ?"-ASE,"JP4A)qPd!aDdH] 9r[ʰa_6"!0rPx?_; 'Êgk jC_B$G UIn~g5" ſ;c'mmW5h<~ '4_رu70΋@!] ӳHot4YX6q÷V' {c"We Ýy?" r ɑC]IThlH$g_o>w㷞w3y8D7ݗfQv^D5k-a_$׾ޓ\Gn`p6G gqyfQ-h%~(3"NץH^ jqH z$`ՐؖHQ'I" &"8P)@-] ~IDPt(5PρP"phl跄HN be?'g9tM;#)RՊyS[vG4qDĞ^)Ì`Pm?l"6fjנBRG}e閛V|nr?򟰊FK\B{W^C@p4XJ{Zh;Qw75z\o?TXX.ñ. ]_:M5W+S QD E!Z%"@YDd[ ?>ʃ4߰s@|-;pu^t=K~7g 5Zt!;\Eˇ*"Gy2h&'ݯfҙpkI4vD?2r,;m6{#qL&3;ɴB@h"buq nuD*7-'NuO:* @d4=)H<(>sT"ill2 *ܹszyBD@HE 7ֹ6/"Ϛ>ߴ^3iL& |>/ju X4 Ӥ0&w<&bg VDnwXYYI$z`h ϝmhPeܐl6h:!ȭ3͹\nO 8/@gΟ?WS.^\\B"""WĻc'p]DWcD"vfgC@  _f[6YØp@R%0@M}ӧwd*[ ˷ paXvF]V@H4zY!Ar0fDl>ttI4*讯k6crrshhT K_yϟ 557>uꒈag8pzYdz>ttw_2KM\neɁ9՛,%־:ncR  B4x^R?3l TjzN7(MFӮ)4#3e+JIkA& |B!yHza|GGWkL܁gd  cW(> 90Gz͛R6a$3uAqU| 2G#ʰC4Na4HB D@OLYe) sM o\B!A:>_[q=Kms|.YӄaY4--ەjBj#$?fElhb<_e?:Vf>dD*Ed37̴T&UJNwf6T"'ӾP(K"B82? l6<[۹q:2x+t7;;4Ʒ+j咁uB"}Xes*JY_FBccKWGW{+#knj1^<544 "iWgOG[FΝ>دAO8:X<Υ~ܚZZ˅ G̮ uu;Ǭ 90GEf>avS 8UFjuD"1n9LJgE"dzިTj?Rd6L6˵5;Lw \ʩ G=^/wTR\~ î^VH0 3Eód4cI>L*3tJp1tq42d6ϸ\>$@X< F,JV"i::^ͶAīqtQu\;L79%6mKǬ 9HDή .LĄ --}}aTV&U |FN:="g}C}OwٳưXlͭ #H6'X^^>6.%:7A GZ-w-%nŲC>fɁ"6ag})éeDPU*L&H!R&Ud"k2ZJT*;1.H''u~xt8Lfm!ɸcw86'qH445l8d0/sB@6ca?NO" BdJO&H$a6CAp)2h*KNDZ'iR39ihuD |~sPP*/g/ǥ+z|FD1 G1+$H6{WP(r\* !๳5 p\q xhćFgχB~H4&`h_!(_<@_2VqU0zrNR0;Һ!>fA"ݻrC_>=>.ZbM T*JMLF\!Db_th$1<< `zB>nEul(Vg;MKf[<|lf0z} |`Qkնڊfْ̰Ex< |2tfX"EVmʠ'%H&@"b)ryr0H',Lel6GT8c;l, jwHV== +4:gqqqB>1z A(hlx'ޓL&˩g_ 0K0w} Gjۛۺ:{.?&D: {z~dhL e# Vа셫 U NF#7܅ [^kB@ Zۘvb6Mg|~F7YCչ=^ծT*uh2 v8l t-CD.vG^G4F48 O|Ge\M vǬ 9G Q,QUpaJ8\V() #a82Q(m~yw"##L!L&hzzj#^ɔBiz`Y!ArQ8{vkp2i\tuBu={|ssKwwwKKX,tBP(lkk/!>wܘXEǏw?44t"bnnG _0յsٕ]W6Iifv+h<$Y!Ar*VD-nee8;zJ哓z01H~@n7XV\n"Qf8 J Z .ɞQ*7_`O;:=Lಊ$+$H/ 6,x d:KDEeJfpUW):Ą*H `usm;6͢Te,n+cVH\Q"# "2I0(664A'x<~SSsGGؘ}H0jJq JDX `K\Ƭ/b ꅜ+fBʻ++v1 *c!x݅:/$ql6C̙3>l9Iͤ?==35|ZRanD|k /]]v#Lz%cVH\Q,P4yj>v߹iQl 8V qAʲ:̎hd+]^SZڶbٯ+AJv;+>/T-M$Wۊ> ]yш' Jm @ƦΎIH GsS x/IbX(xUo]jdIR٭',V+<LhTjl;iBj[s,F4d,Pe2;vV럚Ym`BxHٝftXd2*T"U** ♉;"$]DZkv{4=lYO9c/Ǭ 9D0E?Th`lg|x[>X` 2T!)p.`8SIy`)Qq,KN׼dݮeX{WVA:u8\]m}\K}]ݝ|U"9ufdhT6.MDbLᛚDXygGwwM}g~gT *onk;_[fӣT4$ 7jy= SRFhT1nb$f,a2YLMϠ7#Ar D8ͤFEY5;q" L&CGMGIS,34B$GN[z^Bת>АpttT.g65VvT'TԐXrL*O(UH9D 43!"/18Dd2vJPh4711!HHAGLwejjgN1|rjHM^(vwwgaZ~OO@ CHwWhhkiU+U6U>( $H "DUSbFvJT, v:bETa]$!)&0G%0Pҳ+ Ar(֭\._Q&P(YJ`F MzÄR1>.1,2|RHaX ̜mSAZ6|! st!O*ú*R5Q J-j w7 uuu:nddףb0==qRH>>B$G$! *:W, `BpbSQi(Qq"%q*8&!fYhh ArD"(XJ0|(A+(Өx_bm^Pj4:aJc2zc[sK_o`r:]jl'2&*[!D Art1ϑ,"(p4rU>TPρ]!l6C1 w} hMMMZA:. C}u CH$q4VDŵ"w k;G,eT: |2o̤ip+K3RD"H!bd28;uJ/z$t45|!ńjxhbmNRk6l6'xqVjӡRj$dQ5p"x% %B$G 4 9bm<]:E2'!I,&LFPCCCKK\8u>Fx vN eR{o=0 ш$G hJmYp(lH'@T"D8Fp"GC0h,Gb``CP0tx&H$G 20֪ Is( ! #b8ɂQ=P813 CHEDdX,'f7;5O=ioJ>j.4"ٸC-BϯRj{:{;ZGff*аL2!h_ЂQADռ:OSs\4W|J @0*?Ot2L!&uA`0LՍDc& nw  BX|ixH!Dpt.G 8$\N8NRng vl'2~ yL iBܮ4M! mE0[E0bq\P|1O`057?Ffl@T)V+*N/tv+D2 Ev-K" 9`Ds!geojU)v:V["jqqOOD":;:BL*fqD62$ Hұ1qk[H$ngklisWVV4H\}Dh r2_+ggO<twwD"J]J*o&C122*KU*႔*F(XlRnJ!~j`P'Dd2j& %`ﵻқ--6ҧ+WuݻX;8[6D@r"b+G5'p@09p~'AoN#ewmkktzuN6#752(}!"Xz$2? c PLտtŽ`LH&4tTvOU|:MgW`{_@kjmeGt)~$&_(0SS"\ *Ŭd5)*pǭfR6U)W I\Ljd\*HBa]]L*%pjGyNWWg\.oL~t\ˁjsh IjJe m=]W P/aWQ'&X7qg645;,B@r! ֧h*>Wb5M.G hӮ(:̝bd5`moQAAjˡr";*KʤF% [FM%6mBtX`?,NHRZmVI R00" 9Və lE[טHT[StLS'G#zQ¦fPͅDX,q| B$GsE:WM,W_XRQGQEQL2WjuSN'$B0P0RE D Arv s >X,69Xm^ϴFbfFR"x}ޙ1H4fMFJVmxXV" 9VMuT " t݃Cw#цh$o՚Z>!Bc{zn@ 8{L{{;B$G/z6LagOa b-̦8FVn`ܞuD!\K&S$F@:܌kV9ᘜ4T( dz,&P4?-˴IvR5۝SS3Te:! hKւ7Qe2 yjB>(>yzddDPR,44KHIG"FĢD<8h=}áH<Gb&CH$'ҳl0 AfD|(RH^/c^PNrv7>YIB2l&` 7%TV4v?IH!Dd V DooH4DE"1gOw|G(zSG<63>&J.q;Dr7""FuDTgl"@cx2J$Gh$& 7{BI`8 'p8ʾv'|; ` 67\6'E,HbQMP 1b>QLٮ4z=h(dJry8vvK""M͕$UbT.dr+|y+1],-(U\ BDBy(/˥' TXDO(J,3 @)7CSKtlXK9\|@DT|1]]%V<żg04"D r4X TS"zK_{hd ZO6.[q2L&GFN<-C"ƤR(}ID0[0,Ls9R2c+yz1GhR>ezi(;EJ/r,_©(Ԕ wv?Zն%{[O?nk@/|㱾'v=}cCO=pOC}mjN:LO=q<]&\|ɤu DfW>a7 jt4$=2?\""^adL}#/ nݓ 7~A^]# nm,D]|ca^Wk@WFxmU6r]^oh_HY_ nŧr1g"p~x$=NmP;6:)46K"qSsˀ_.S8q* >s*t::U*B$"0X:FgqH(ϑtvD,1fTூ0o%uYt{%7>2ig])u_x7o5p{塀^v+1rPS_\?z*bnj2.)YƊqhÏ zn舨UT ٙYfjۜVB!z-V[(?'F \zD#[bfbR٪҄^n2 W)ٻT*ťb~ e=o] ,M_*ؾ=~Ӓ kEq? [E?{/\'hMv޺ۆ;lB_w }QpiIr>U&e\["7-QX[X{xT>xn~^k#ʅ_ ~T>tl.>/+­`vԟ\5cT\6-qVN%Ԥms`o~ۃ}WN$TsJ| SdlmGޕ9`DhG:>.JR\&?uht㷷'[WcKƙ+s "/ K,F F^_{aP#s"F e/f~M?ܑ21Y1}Qw'{g\w&Ў<,y~:~N/ >?sFqԣ]_v;k7gYS.r~ml4d8_ x_X1}xvsm87 ?Zb#X)!rx܀`h4`0d4nϬJ,.u؝33z1ƿ]-3b!3X$gb>GxP($^F*,d8o1brT|u%ۥx|}w~cCq_sߖ phNǃ'u. w9j(eȮ|Fh \1osNoF?O^C>O-Ts?*}/s2Vlù, YG|EgvxFĆ%,%ع;tIP}!ҩT}(O8#R-ݕ%$9)l ϥËd:rSD,O% Ij^ue\_NI-Ŕ+YbF .esi&_TKf 2 <6SΖb -P\x1I97H )[@g,\qOhN,Pyݢ/SLڨx}`ï pOG9ߍ|.B6}e)VX /".C1adz"8GcJP(Ni.'RiN78ZjYL6c9 d rĸC:4LLCK"b~.SʧT>>3W'Dhئĩ:4RF<ʭ: *ehֳfߢmaW*hz~^}ik01KQA7M;Mt"B)76˔f¡7kUÕyu^q]ǘK4mt3ݜx&]i9&ZIȡcB&4E7)2 "3 [BͫN-jRmJդq;M3NEV[yj2o"&Js} /r%vsf@u>V믺̓1 jD>XzszԿ 2L2ML8-wEE Su/ϗJt>4cyμio_~!{.m{mk?16y@0 3>>ZId* ` H&SqD"P(*IHVAn@+Ώ$"9* yJCn{9bSkeqY.qn ]NSSp0$vJ+;ERSҭt&~w$HRϑsXfBq!j'ufhfVefݡd&˘ip N &fb!rt~dN"$\Jp HREP`NpI0G 8Eq8T\1Am|I^V{H]=WOL=3==W}OϽ{j^)^XB3*}}ィ G'թQHFc}hynn OF*#l\O\,3 Z`eWED9|P"f.5$#`Hd.^JyD;ۻ.B%Y]ZWm,Co6[g Bth[|zMۀ7@aqDW|i.ٵ- jTz%;ڂ &ʛ=),?WOn7l/W_C!-沈_.}#0s5dA+鹅q]"|>~|l=pa&&0+䑙~Ⴣ+ˣ\]8LOpO<.y=#=S{vO:>j4KsV MT-3,C4/Nx`4QTiESl8Ytaw5|u^w@d]{9IXRNez6R3J^Vu{Sr'$Ȭӌ#^0΀*UZ+J>t֯}۟7D Rxzs(cKg[_V~ A󽯶>Ve @dQb]&%eA"]YUM~dj$LT" J2A㢊. NOLRvĩc'O..,813 BkTUՕh$V GG`DLol pX*ws/e? ~BM,wՎBÈ@}d"J28G} -u%zoDBxq#hG[)dDcSFP`hk?;"8Sl ; +`wO;Z#ZhZ1;D/H8H~EQjQ-4Ppcyk9޿_/;xT+ʾ;@ G&$a䜄''T.NP:$pY ™@ X!n⮐gW^K!0ͷ-^eD4 3=hkMy*vةL* R&)Ko%sSfkv*Ԍ\2.JWT'@8bIw? րa(0(7W@P@C(4-~\q ( F}DL껻I$&Vڼ:E{}o|FD ;ud#G?5{q^5mec}{j?@>=5tR$UEO<9008>rlWD)Dv> .8"vJ.H A !M/HѴ 讑enYʽׇBR*,hZief}D Qi{hɸ= JWfveݕ>׉vM5?!x+L\)I  Uk7899FDEɡx \aD0 pjh rB!!h4&@A:}&LR#"-Hi4N\qgΜ#^O^~!"W;{*"U܎ngg&NJŪ%fӅro`,1ׇ6_(1g{K\$ߐ ov<@G_6~"H#h'PKa b8' IF1H}BDn7;V|>h>"Uu#n~xi*CTB.}8"NTط0СS{{ll|vfcCOEӏaʃϻ=??6Iw+lDh"p\YۡFp %]N <~iE㺇@{Dbs!\-+"67!"Z^76&l@4D(ѝ3&QBz$X7*8̅ӵfUjR0HNG+`_ן_Å 'ȹ>8JF4ڭVwev>;L dZO b)V*Ur3}\ǯQ)ZDlDTE=\"NI JIKv=HAp|"АZ+靀ד^JhA-H^( qkkk+++4M !*:SxG x($8_`D,&cթ4X4hێ : ) >GF&Mzz,U̗G+ []Z}IFzhM{}ϡW^5H$ڒ =j*rH'*W >JHyi4P8#o7]y_@QU/ܾdq8WT,iTDL:xb>E|BZ$d, h$+"ȼ "A ESpsx7"ևL_3K"$s7I""0H^D8%AꓮKP ^ဨ0s@t:}h4GDnfDݸՊrtthqq=cã#G8z}'gLM3'N <4=qlǯ$s$Q;qv@:H&Һ{~h N~"lnUz ȬdYAL&S( JCEm>"vs"b;%z_j(jHT `(;EQYԐD<ͥn9Y}hE9))+ƅ;E2@C(Ò.D@9؅:pjtSW255$+@Ek,vt $B\Q ! tWF">"~c_n܈'w g,Ig4jFΞu顁C MNGģ 0t٣GN2fb||ɩ_ x %!I$?.lvDZwD*HZ$ $! t3"@ҶɯD>L2A&h;8IT#SԗoP2\jO:z b$#;"jaR(Jp$K֢8a,A٫)ɒ r)o**ŒQ"mȸ A)*}ˊO@P07rhw6‘XX?4| 7"Eb iYGґXs"6~&CDi7 "4KDwIVp( :1̒ )lTjgWvP:Ȩ(xhv"Jk^@Pf>p8 \.PP(Pt:It_Ȳ,:,괁5yy0nQ=U\AAb.˲R ڣ]y&[SK)A xONlߓ˨+" %[jdc?lvGDޮ[+Ea88$YxM{38Hd70'e\6Adex/e(C!N$Ath0L#aKn6ύnvըU8]hV 3^nOUҨks]$|:Aq; 5PBD@bh8^䤞4=Q!Q VJ C.(8I@>PX<,sm6Ms^e7MotZGؾ>"n>t A(q:&FV2L(]+Y,Nl+ӳEWgkH<4[LXc=w>t񹻓g:vcR^U3dZwGQȕL\p̕J= II! #1QR\n2FcE2qR2kJ4 Np8Ս["QUd(N dGnyrDĎ@iPd,=DjTE}fk|u$7?Է6=ߕJ,>paȞȏ+CD}D&)jUDV[Z8De- OP~5Td-d/jt7BQ)] 0>bIkD jF"*eSXu*zio46ιfQ.oZ76 :M>}Dq#₊XCf;.JJv?2~Z&Oˌߛ9fcWՍzأ'*-ԏ#~c?ܻ:ժ7xz&/zQ6ɃN7IŒ8 ̔`xe.c;I2èlT(4-J\2&̙1=Wҙ\<?_pLV0Z.Ui\DPP>"Dy'?cyի-~swYr-i;-/{~d,-_߱گ[>aK0U-o|?XGV=`y-O_4mU{=ˇ?jy,o|ef~k^*^oސTE<#5jj5ՄYFxE3>NB!˩\bIKb"/..----//0@^s]O.z#$Xʯd`RaHNl2r>juZn#:A6}?*g$k4c4(Wi~}җZ<–%ˋ^d[_?i˷}K>0UEW1OycUDmK"z)jS?9PzLa=jdL [-'L>pW)Ȟx">/]`]| endstream endobj 13 0 obj << /Type /XObject /Subtype /Image /Width 179 /Height 94 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Length 768 /Filter /FlateDecode >> stream xˊPWdTp3)B AP_c|ŅnlEwf4T&6Mbs䄙d)v+ ` A `0 A `0 A0 `0t-^^OU|y0)70x,6! Q!0A `0 ADŽ0 MaȪCM,>=>bT~T*V˜\<c:v]"la}43 W%ưbq8^**z>\W a0U 0X|0 GWIbwrbE{[ ÈDwqS[Oރ Cu] ~? Mރ zi ?8E)mfAAW  ԁv_X@ 0ljq endstream endobj 14 0 obj << /Type /XObject /Subtype /Image /Width 136 /Height 182 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Length 13728 /Filter /FlateDecode >> stream x $Wypx0#l70kAlĂF`X|` l {qwI# fꞞʬ2/{KQd)*+!W/;-ОZ9{#j4$QԀi$gAPк]R[jw;]P(zDZ\&ņ&7,YP`0SC&\ںu=Fiw BLRL&ftvc}}lw:=>_ Jm6;Zm2Y"7 /p0MT*H$bjiD"C X,^/UjfW(f"rY0V+KX\4SBct 5 BE&Eu| b@? ޠOatX"2${0z ̈́tL`2^(v8\D2Ɂ)XzThool|~ŪT].l,1#?B]xGxnX^^q;;;BP.ݕH$t\p^ws}n! Dn:$/14~EQ%&F}ID//T./n31h1ît:\*{YAvzt^o+$f M@|:LDfBRA4h/Z!ieKrTH$z*/--\.R) A] @R<7,\."2@ҁhmooo2fRDtWasB j i2ZG1AA#Az=\z}֨kδ;] h4@\FLY]A0)6ATl6<>xk8 67Ө,[H zl0C_@do467`{] ׉G\[Z6 \"xK*Ku. /4k&e5Pk=NdVtڽvl@k]VizQ&a1ˠc~-uC5>hnkX6[l 2F1(4TY,BNi6C7A_dKcЧ ŕ$r)bH6jt- K2*LF)>z WRtZ\Rdeeueeh0>Ѓ;bD(D㏑!!8>zWDMѬwXVWXZVRcRV+U DTEnZ**,*NUqd-D!*3bZJZA+jtJ/BJ沙b lNÑ܁R$2R+|K° r&[J L)-%SD:c8.j"FM(SRlee],907ƎBKM&S(JR5KKا Zp@_^a^ЭT*vM"cvzhR.K"nt4ZZ&Oy^D X$/D 0@JTG^X06V ip'+5k zlyAK/Gۧ T*% JBG: pXF[6\D2ᴘvh5z^93&f+@F8̱X, rwz괻\^/ 8\^1g{ިɗ.-J%bh767j־@@d9$43A%\AZAA@+*hN'äRixKMh,$zvuDCК?ѥjuŚL@qYV" sp  \`..,Vlt[nafiX"3'Ç^b}ʪB"xL"u: /jh1  "`ЙLx_t\,ٸs 6*͍MtTrтSR(Po0p~_4Ur,^K>'X"ٗ IZ2Å8Pu;H W#cb%Xbf,Dnt`@MR1\ J&IӛfZDBb<\.*j6NFǬVŝw86x%2 1Fp [[]H$ntݝŅb@pF-.ZbR/67b1Cy045V`afcIQ8 mb|0AH%WEΎ^ktfG"L&jv g a̤CgPv.-MFH$$bљ'PbؒJ왳Jl6m+WWz:Kd=v< 39X & S ^**+U RZb8^G}VX*ʥrmhŞ -N,HNx*y~ux Ruz]ٱ;>o(Dn r ׿MVk':v^L>B\LdZhgWсd=|Nc0L,[2gp9TIP> T~rfDZFYX\K&`KJ\gR19~,[2-/HeGE#h4Zeȴ PYh,H!'1$ʖiмY8Z.*Kdf=PFȋoʝn3G:T,58iFATΟ9ki@d l1 ^onEloxcO|vNez~쨰0d.WH2iBͮD$ C\"ꏕ+tkPd^\.!/\+Mۗt*h,**F&e3d<C{`,K[ͶH8 Q4Eh.o.>;O+&@8́. K 4*GTml:Z.LFRycz]2s69;O>d\hOB: X(P(Gv'H8Z\.Ǣt* gp$ Po.wcTp)BtCxh8bB\8l`0Ljk;te"#hw f}1C V x4tFasssyyl0yJ% 2Rl @,dqapJeϟH$ V;H( LX>_xy ߯>9r94Ϡab&aYZveEz~2w=a솽[w;:u <ʀ}@K6X0 p< @ݦE5K^Kq{ sA%JFj^lo,_^D.^<fls7׶͗o! v˫6IKt~ ,:h7ۍΊa|"~xpҩtZr3%ϝh9Ml |+Ё?* 2~kf.ꕛ`EuAKtrQYv6rR,"8`ЪZ\ ~s~_xߛd:05pj~nRz ގGcRN; e:,`z~YưyO?\ԣ!5\*Q& D, -5`ER˅2x´Y=ou;bު>Vx 2 x_e7~7'L#y!l hnm>ye:ݝ]rp xuiil4?#Hl6Ѡk?l{'N~}dߌ}$7NZ>J T(3МsB{AOk`eRt6M$TNZD"TVRf Ȫ5}uA%%)^(**-8p()y\p+"L>& 宗\t/AOs4C7𙧟!F+-L _t6L]>L84_|JRtZ=8ZrLѬ,/I%btyh4/-] F/D?0DzxtvWa_p?K@?G vfҠzFO3wOs9(> (M贉fUumDڍN̦]k7fj}RT;XN3 BX"fZ )QG60_9H7^8c{oRdрjTr@d FaՇO{l)եN2Y,WŶ)u\Ia8n NSyk\=G FZ-[[8[ˋ˗.mlr8BY* !=iy\t9ӫO'RRtP0jX <:.K V*T.obJ^\-,[/a7=Ҩ7lq׵Zf96.+IVƸ P"IW|Kdy4sl:aT (l1K3h7q\h&խ'3uIc.LqI1r=#N@cG^Szlx(b:ߢCzIT^$wpADW>9SbKZS6ԣAܪ3ɺN*Z8,Zގ1n65NJD @ރ~ew..ߑ\鱣Sd #X/h˜Glor?y?^{eT 4Z9AD%KMu Uа? r~DH>I|c ?\1G^7xos{s[?ՉHfenG4g2Wi27z /7_y%#ijDwֿOuQ~nC'13>pV+_9 '庹lA'my?rr|ǀݝo_a}.~Pz(%^b%!+Y*}7/g}"X.5h]wڷl 8z2i9r3tc4iW2Q9ff1 <$:R^̫Wq5da:ͧV>Eo/oW}^ ؍U o4Z:xr< D,b_ǯ~υˑ֨˕X,vDZ,}~h2Wb4r8:v}}CK$W< "ȝj{NB_/M ʌpn_D"FEZ}"K@ LuZ.I}C~'\˅2\1ǫѠ3ôVÙM} 74 !=6Q_ѩ}rU!WlsBQ4_0 &*JB)ږ~'{O=뽽{ۇ~/?#63h@.J :`0XVDb6㱸Z<.GRIjr8ۇ˗93f󂮋1q%-*{3_*(.NwuQM*AE$LVj0|њٯA&P.X\~\yCPЩH^ʦAz|>auXV\Vi|N`@|Lp/Z)WU Ջ P_ER~\np&:.;nz].'wڝjFV-mCo!'_/lɉ<3+Oe\ fk shE g݁\*w:Jt? `3`zd+ߤhufU ,nuE"dybU*X<DzX ήB,[䢽q ͣU*ՉD$euuzټj1[A|>?mmm3-H"m(1c/?v=~.c|,8Z0P\B:8^oLqrw d*z}jOݗ?7/#Q~2a_[] S)VTf1$7pD"1ќ˕ӕ5rm] E+SPĠۡgV.*d Tu{y5ID>:Dv&nLX5:uc֬UjaZNSauFM߲G\eo`=\NwmFR9,(N1 dZLv,]LJl0 BMsټhiZ zJ\LbT""hK82laaA mBH,8/S(ଢ଼>}㭭T ;>lotpʕW^Eh')t<Lۃ{=PNsH[p>vo0>kg?VFhBy^8DKd.k\'.ܤ?3NIUV8{ݭT&ҸpHLC_NJ…J"VJBnf|-d~eo&nVn+ER`^lTU](GιҹEj['.CqΖ7d&/6 .t뷛孓H%zDy ~;G|EvGv[9WV~9-ŕeʩ*4[-\trO7I#hܽjeTҩ&{l7cmQET=,Fi"趪:֭dIj%i]0Q`3U2nz`6y`}ѯ 5kzVo2r5W0P$H|t2ʤ./;d2 F&X,1ǓDf2OiZ[[ݐejF*if9{~ޤ3Z͖?n1[ Th4?F;&%2L'>#z292G\*k2ZTZm]pT7!XY"e.C\ FxN%3@(ɧә|\0 GLj0Jbbqf>'KjXb_^@RƧ܀X,i6ә̅s :Ve0_lvˍTd.^\hxKd6.G*U*5oгRIg r̉e}]`R Y-rѨdžGGA.;FT B_,VʥZ4z}`awWʵ\J2l6/#rT%ry\pCL+JO&Z="͟3hemnnr reyU&9tQѤR(A͟>c2Y"cCŘ1f\)i%G BL4*@RPqHgL<c'>i%Fo1S d"S5j4,+D:in[-9dZ4k f#zN=3?"z\8%^776mt8(:>Y"r`8:?}(x_D2 2D%O e/St"LĒb:ડ'd2dej R`2KUBPծUꠚ*Ŋ˦XY(]k-֌Fbn{Ec-g2]vl }|_$R`4ϝ= L*IL 6V/DϜ g4ZVc<yчx$O!Aじ/'Cxd!WJdX(AșJp*g^ֽBc*P\9兢)ZϤsNT l1~^`TTتժT/ϕἾioմ:B!=7j6 & " X$Z9KnSecG {k[֘Lf WW8ݝ]0.JuscS"t=T~Ygwcum!7f3Kj偣L$uN$jXT"I2l6W(}H( f$Ep$͆|ɇlPdVF B AxɠF5TlۉxTAK-3%\V㲱 c=R*5P ^tY*?tB*ӧOT .BNPzKd6?(fF<bBKǓd@pŢ Bn[&Չcpl BT:HRVA (!Dv6># Qf9AՇjʑT R# |9 й̹ p\rѻYgt_ s}x/tpju-@R"d0<OhBX,OB}QHO#rJȱeWjxD|Ulks%U\!"ɩS Z}Y#ΞK2d~ U)ۛγDfckLsSaQeK(d&F_;5hw]}ݕ^w:hz=w ě^iwDf+d~.HA|}9>|#n>py˙?vddvD4Xn~| E04je /do "!ڏ<]t;x%PYDe)h *LrJJ$NX$fd*Jb{pЦր~!C3?7R:k贛&X(.JTDZ:h9 Y썹 i?rF08Zj 豋ˤBy"h$2ckkfs=\p8;\B.;uB%r\.* :^nK&t& g !%~r< d4 {> stream xڍVK6 WxOkkl hP,=(c[YYAd&AD}?,ɚr1.:QMl?NRYA "V6N>IytmV3j7`,og킨:Ia&IŏcKfC줃^ɟ|Ed&d1*pY3:FOjpOh^hufT=O 8Nxr:X&2*:+JE&j'~{ ̳yQ++%N&YW}eɺʤ 1"6x\B\c+x538q7̬IX`Au=Y gT]i998Dw}E/wړ)o *WKA=G;)!ZjSnXgGOd.Y6۱Gg ^\bXW$(8'.r Y?yJpKutP.VN6?tS$Nn׍7nc01$\q( GŠ$eʮq2L4, , tѧ϶"vf;H]ĵXR E3!2D-Chw h2a9c5ph>1~ 'fTC[)r Pu 2Hټ# ;P;=Mo  최@/,OGYgEwgwEӭܷhNˡ H]:P|U=R~}"D63R{&a礑}ə#! mC"͋AE)+. ߐW~ٹ 5kV2;٭W e|ws56ߐ:8gF o΋g?zo1{; (lL4w!9%^gh1t3;M\ @GIh7]}M='\upgwҤ@`d$_fa> ~?;j_xXI~O{BzFV% B3 endstream endobj 20 0 obj << /Type /XObject /Subtype /Image /Width 189 /Height 387 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Length 7279 /Filter /FlateDecode >> stream xYT2g9s<͙2w&7EQ,EܢFbQ1H%Kw_\XiP"JK2_k١mwCUuտӿTLf?`0L&t|XR;E{[kSSCcC=_ZZZ0h|[CuJ vG6u[rrԇ1FLz5lj7b~k}j|O`>`l 4\tiWlzV?Y#V?5M꼜W#>@k3GF ş.xcGK\((t؞x| '妒o8'6&`Ջ:Ԩxv?{[gGN0)O퓧Nܼܦ31͜5h4;q8?~ќ JKKѧlFܐNflpn]U<-6~W%\)Ӧ'vΞ~cP>1G/^ݙ$$X} lzO橲3\NCS%KA9xe*niO Fg>(_575qy9Ms L6?Gm`x6t܋͵k kujUV}chlA#5Gԋ-zI?w.Eo0q{zJCvSS]PA1xie6X/0yCSk*9)i\P03#R\F/H|&nW_ӗ[s2ON&7|N1s9$^R`hbL/M2#g\S"߷pjĘpDu?6c⊇lBggGFjӥ14buى[߳Ǣ]Ns|s6& Dt xf|\.G4͗lw34gSW[I627 zE'K/]NI6~En65U.lbҘvHbM^7jf6ZҘK1t6q4U5&*ӓ܌d-/ӈAPSNu ]sS*ŔXɨ|Zfp8m4%f3R|ܧ.P 4{<0MO+ Nzf*N'vM Ԙs޻z+gohGLHO+ `666bu$}>_TQDjogs) ^U"ڳdlp:][[DJƛLUO>*,+_ɼǏ&o SVUX !/]&es&ӣb{qI V؈Tx2[9v|Y*qM{G~i٣loC=}vHrlTy5o_|5wJz Б>b1d6lDoSeoEx@ |cX]}r55[-- =M6n1_Tϒ+0R$v|2ki)Hvttj&Y9x)_l܌MUu'+Wy5>舢bb=C?vǟ$6@ 7n;3gmNF6.fِ ِ ِ ِ ِ ِ ٸld a ld ِ lX˼JOD p?6z*.7 _UDܬ܌;ŅwbfL/aiY@kYí/ufOy?lT\uҲGDfVXUzQ=;G֐b5a)]/EnQy$&gl1fl䪃b0oi;\ 6AkjCt6|5vy]8g`<,v3nXVml*Lrr,[jnƱ]6rAgFϖ]T[3gϞ񹍹q?6rAhOa0:[7gH$ORfwRiwzOţkNvVa>R del GWU]-Q(VKdfA|kw`m\{q"0xH:P a*;Poؠ'hE˖9>,YP6v|Lښ8U\RrnwOq{6ˣbb=C?vǟP)_ka->- !!!!!!c ldC6 a ld a ld ِ lMpX; H!!!!!q{yq=+Ȇll+׮/]ކl8H)·l*Ø#}&M hu0g /w=2t\P={?.㻝 |/=Cl!؈6uy+:J6N qk'FCK}CCiY@k;C` xt8Vkkk۽wX!**&v'kjj=63O6rEGi`*sTOD,D;R,9rU?|D,744A*X=#+KBQqZ6R4FJzigܚ$"hJeTIY-F%_tl6U~nC6dC6dC6 !!!(6 0ȆA6d 0ȆA6 !0ȆA6 a 0Ȇ18,Ò܉K$!!!!3lB%Os6Q?>$`\5z%Rdlll*WS" uqA^xBAA; %gcKPJrA%6}|^mRvT2lMB*y*T^e?_Pp6}aP|žsM`4*4hdx6r<*Z>hkkk~%Cl+yUܾ#?{9D)4hdFU*w``H#^oRnN%CɆI ِ ِ ِ ِ ِ ِ /d ِ ld 0Ȇld 0ȆA6d Ơem%1GZ$ِ ِ ِ ِ H8s5q 6V+:'ٸSodAl\͔i_|;+v0oKW>_o :.(xמϟ?Aņ-i(ٸ ?<ڼ 'C`HKŖ>+v\Qq6ze ,jYV+ 9i55Jgl"k J6.FW]Sǰ7o5`Vs1貭"k4 J6&Y9 &M AYS(ٸ;3gmN&"hW?U%~'mȆlȆlȆA6dC6dC6dC6c ldC6 a ld a ld ِ lͿ:,ǿ: ɆlȆlȆlȆlȆlƮҫڧՊ6kGގlU:3vV߂l\z"`ӫR\UOQn9s=}B'Kƣ'NN>Cj==0?vY*#WԖVO-Nl+U7Qӵ޻w?nwCCý EEb>;Ҁծ0-H{*Uds -8d+-`?hKld#B*LeCR ǰ**TFۆG-SZw~o &YYPdc0i HNlF+T ݸf]/;-"JrIl(@poA6`%_6;R0Ɣ*Klٳg]Im)@*woA6a#WF6eeX *SOPO!D0-HN[ \dC6dC6dC6d !! B6 a 0ȆA6 a lȆA6 a ldC6 a N6g%1lȆlȆlȆlȆlHO6d3 l~--D`7^:+vNiYW_ϧȆl^fXU8T*]=w1z)؂gm|tL,j566X}l**²*Q}phPkIۥQ577ဧϨ6.Ǧ˶dco6R^a^jǏe<{ :mIS~J6˦W]^r<}O2b$v?yi),t6e Unk4GQ1xL `ډR}l\ !!!!!!ld a ld ِ ld 0Ȇldl}_NlH!!!!Sأ' Z6QmȆlې ِq)gH1qEE|M[qĨ?ڞTKK+8WT<;Iu>EJei444;PT$VSvp<,x-^aߴy선~;IOR}AǕ}|p>lq؈~OR]Nې ِ ِ ِ ِ ِ ِ/d ِ lvfO endstream endobj 21 0 obj << /Type /XObject /Subtype /Image /Width 379 /Height 165 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Length 17269 /Filter /FlateDecode >> stream xdYy,0/ЛbABШ[h YFnAp]UtOo#d{qmDdVk/}=2JզDsqs#~;6"!ΞPO|{p N,}y8n<_d2g)<=}]NwXQ(@rvvvm6V#ζNIeG`cXr"~.iuZj1EbBwHl6hYloQ*Q"j\);n5-?+ÃCJmwvvBZ\)O`Yfgmp+PRR@ G;n6cfX,\68d8p$Z֋J$DB.;HQ?`+V GD< S4L& `XzH,KDbH4Z*Ul6;aZL\ 880pV 8H m#$$jc$Mzs<"^ӫVw:x v7菡p4&A&uvt;`2Fx-F ~o jfX'N/MNnAp<MT FnԚv/cYE9;{05v0Ԡ׭߻o6~r|S^mmlK$RN3ӓX"K\.ЁZ(F.W b vgj2qnK-Rlsml6omn FHLzFqg{ jk76e2 ل,e1BP*A((@8\ԓt&BTi6[pfRmԛrR4\ G0 Ǖr f\.zl0+NF=.'HrBTj58μ'%FCBB!m4͐$h4j w4$ZNf 11@t{}P&]ٙyR(/ ?JU8a26$Gǥho|y>j4On-0jrhoT(e'c.KV8 ́ 3@YFGi6 ᘤ`Kj&}$I([lXd0r9 4BD선1ufE~T+ MQ\Yp/1ðAB4 ǐnJw݆sz5N8@Zc6x+' ʼnJdgJq:T}VmomтQi|H$`zc._lZ vш!XB@*6@B3+~H۳\rAkp-<`0Ȳl(J&SJ >9;쎷8q>|^ UpZtYU7zaf4=xE)dզh EM !(*_(ᢹB:$MqE4Y)WI z !Dt$d:._>N L '΅iCq&JHķn4`,ɉ|}}]ﭯT׮{huV%FLvEz4ݽ{pH,Fur۝pniR (RѨ].=E:vccSo02lgg 'Uq 7Qa\d`"(xX,L=o(r) ^LH$r^^, x$+@T*[tah<SZzRd O&RE!v9x8y=jNp.&Fkް zh4qn5X^krX,v{cOFd{Ą6-~r^A +NP0LnOh8G7.&pd2 ƓpTT,N+pgnS zwv+EX|tt(2T*}dBZ̖@ E1l6V`0MFt67hf_f1hQ[[[jZ$:[&3׮]3`nߺm;_T*wwvŢ#N*.6ǽ^l1&c nB2eghji5bXm)J`ePd,Vfs\FTy1 Y |i:?5h5B\2\P*\.ZZf.-WJp*6_hb4r, \^)O}~#fʞrySt?$r˥J2֚TX,m"XR_NgYfNSD"Yk4{A2d8t.[,=>_:zP(i[=;3$@|Ru6s8szN/4/zH"v]rt$ zݍ7jPeYZRv P#ZZ$~ vImnniJ\V_@2 _&N ;bĹҖJxA{r?@97Z3N{ί=O )\6_@(#t'UoAĀc iZtA颃|r[4 B `SPB3v>uΔW$I퐀AlT",Գy'Ε륺,r@FhñFv7_ZVAAuB.7vޮ :bh ry<{z^*zF~,9lý]nHx(GfQ1pp\q8ėENZDcd Kr,EJRVͤE$D r)T:ɤ3|1H&#ZN3l&ҙh03QT2 'T*5\&-*֜ fx1n{ssm8ݹsnݺZOv^w6nJJEGF-R_&Sv7 zdT(499a"ёD~ Rj4fyW.;6 &[V$ uh0pp\!3^^ UԚ(NH$Z,TRm粹t2CSl*j4BP(J%RN=gsTjw*ZTdsT.bd@@9U(fz5%lv , 1pp\MZ,U eE1c #x`> cNO/Ly+І_}:Mu*40YPhLH2ʹ*v 1pp\-KnN;OTJ`bl6p{B 4 C(y}. 'Bpj4dBJ|>lhLӁ}٬X*O&qX\QttL&y`0! 8)82`Ĺ7 X&5[>7sjs}ۨڬ`uZjeixMO?7DcvCR_s=nb؀5VV^ncX"ǒ{Nv-L`1̓h$MmxhfӜ򎉛M4:$sFs9ATuKB)v192ep9ZnvjI07_b}/΢drr-N+h.LV1h:m#q8-FL&H$`4z[=zν;Z :N.VZ.O9펣Cl U1HE- EfFTIu:=%KF ~_&>;_>FѯgXV='. ?_5pKZm/ v2p[?Rh(5rp1@D"d^ӑ'Q|*"E(b:C |~ 2[ 8w_7o[׿b``D#_yʿ~J @az.*#2 cH1oXCooñ/C].g?'}~? / ?q8c"/Kd֫uϟNe,fk(~_px4t2L284wAgh r+/r\`_T* B 8j(8QK_p~|بv[P 3J't$ li.A MOӡ'XŠoHҩ^t[M7+ q3Ah%)i4pSZw:&fA(2-`X*C`R6x5@x-_soKp 5r2U:B_/| -<;/=O~ /r:p~_.fxm'u:]$9MHX&@l6 Zd7+ّd0?Ѩ>t.q]%p$y]>ZzZ+?PTThjꂂ"* /lwx_~*p>Fc9h.ߩNs~u {b)_TSEө%4C:?P*P3I2PwZC,IXsRqWM4 ϥzYUA] Bo3j4=3.i~~[78px/( h4Rb H4JeÝT.CPd,/}utCkPvD"i1Yb8 k<&Jj.[t{΢lS)pv\+R RcU#Tn j6^rTXiXs;NmI:av1t]'C\i|>`P)2  aXGbVq]Vp}:qh~omm9v 8V8z8M Ib Iҝ^l6rrh0O˕j&#h&/jh[NdjZrl\6רÉ]PM`:о.Մ$NDl\||>UAB Q8G6ࠄG;6u:4;U+5 KR`Ip0S#8HEZoҹX4'r\{/G<A2P("L e$FtXTF2q'J!?پ gcm`od4[?y].?٬wD~o(O߸h,R]Vzlx|h4<qF(m*8@o: ղTβY:\P8\"@ -Ȱr,+,.$ kGQ Ud\3xoΘٌq-&q29PPIFmnbն~=͘|v2bLzN8 ^C;;7Ȩ}T2Nq= yv`4?vq(8  fgpA88p1Ϧ;#DY-N^v6t ~ptwp[]ɏ͉t+Az#lʜI(_-Xȶ>P[;ΥN5+)z7{C. 03#C&af\CCD1\[Ҹ;mi40`cUc4`aTgd[_+DCxFRj0u 'àiMRLD\`p0~!3L1ppXj(^-b']7jCEmW?Քj˾|s7PlO|IBpnRɨt]ŵ7HLɝ^w#Ew~k퐫j_{̔']N[798J  (Nm'y}h+תʽ捯5Uƪq{?${?"FaI_~`ͽ k|-۵{ڝվfol?6V}%Jݶ[;<58qo}u 8V86pиI5l9݊IP<3,SA:LuF +t/1ي3tT+̸15Qj~3aը_cNQԤWBU^l&S% 8V8=;m:D*?%K?qz[[9g&U-|v>S4*zlv7e9~̷m jz#(nфכjcRyM]+U<Njc1Znw|~ Pr;~ow'O&RBNo3?LZ*;=bBB 0d2bq*XWAh~`0en=N^;:< C6YbBA]^yHl͟J?ٌPh4^ n7NϢѸ'NF47Z fD ܥX,P L$HD.g2d,,lA=^ ) ˝88p™=zMg.N9 'rf=*G8s̜*`ꌡ|Vs.Oaݙr󰸁py<;rA_7Hb0LHQO2V N,62l\q@"yLz5{Zl2 b(e Lmls`r܌:Vkj\.ɦgiu*̫Z< cB/h)*hDVBP>Er#Mb2<;{j N(x> Cp+F [4NFTn(a Ʉw[df dFq,h:hVfh,R'L'r R6% Zb}1\,,n3CgB APhAca5cnc0ppXk_l,Pܐ'N18& ;zAp#,7g;Pjq< Kpp2%>ݑ$iv ~# <3n3Wk(GGT>_\^l y<Vm򀞁 jC/G&Nrz#%V=%ɚ'CDJh4H5`8OzV5E"h$f5[ق\S8mcwot B`@фB!V+H6R,$h$;QIK%NS,TZM#\";>>I';;CϷa5[ǔ[,6 bqlpH8,:<Ã˅J硭X.V™ը7ηٝ2UZ׺;x2<8e)ڦ 9,zhi+r0j] <7pz o:f*'Ͱzg5]pX1<[ )TV(јN5LWwD{v <IJbSG@{m thJ(| *Db$I]^BvjD"4h Bv ~6Q>*q=#r88pLΣzpb&V(`|XT"%bQ:'vg\i*n@d"$l&YlfYV!t ;8/I2vCRCa@APjpmvU:pTp(J0ppX6K9 }|lnǰS()'~?iXDClzZ5ifΗWZ.h,A A$Iu=x H)+T5rC],o[EBQ Je 8VG<4hH޿&V Ivi.T`(nB^~ݙM P\08FgvcrNA Ā NT? l2Zv F#(OʍFD; F1|&|>0ppXEvcJ|<?}ҒX!xq8H qf3|& `L3*$Z>BKoAa<w4JL&7荱hRAAa:I: 7l>K!88pp.6sƁ  78 ͦR)vDSŃ #e18`JFDP hÃC"K'>[ɉt\G#q}Іfଡ଼>>>j}c[w'I 8VK\n1G W9 V՞NO "@7U|>?)\X(gjlz]ЪP`8F[yWd͓VY$--5< ;~ /A188ppfGwQ(JT(IcZ~rR{& 6E%w}r:].Zwh%/ʢk"1%F {;Ӑ9ILm MO H 8V g9\rU:s;K~ O:lhy8l&S.Wϗk&$BPcp2ф !ɞ q ]P4=z8a-Y ͞l % U {m-]ˤb: RG=_3_8-S7VhN$[:p)rvM[PeQ`)lQ"+*?ߗ_g91ppX9ff.;+]u ^҈]ʧH 9E\_I"i(Md(c3{&8¦u`䞄=L[6j!TO3[x&TO-ڙ0ppX!,opV4̅0'AbIRp5XhYs:Ѹ U#;G^0;Ho?%x.Aa.(d}[[j.m-]B" 'fHk,ד5!1ppX6Moϛ2KFP&,ر%oU>eQj8Z zEA88p7 &L:c^/p T3f.s-i^;NgpԤ– R8n#REBbFhoqA!h[+::Vm4,nϹYdFc7 G 8V z8}Uf1Zq /Tչ(r/DJyun V %`RΥ~=VTW:om^-0o0PvX4[x+UG@H,aaJ1#Rvq+j˥#[4UykBS`Y( ChǙ,|RV88pp& t>o p4pi Z쪦Kөfeb65spXiKs #/[*B"]S衵L#ˆn$, HzQ(61+ YnY|9pgajə.zqZP77q=68V8~!4ay@p|cZiٛB2:M9_ |Q|բJhcs:UF/OZ̲=*f+ty^0D M@1ppX6:4{.,)aF:VU.4 ./.=策E'V88p(p.3Gh]NDE,ޢ-Y"#at(}чp5|/\c/_vqNk;zu|cV *.`:yq0W>8詿_N[>xR希+\Z>¥1ppX9XyĖ+_DxyA[ z(؂pVP/"炤y^ B>QK_^_p#Z 8V8g.+t7!ȼ|/{!\nɹ_yHayO~VbZ_?|#>,chs}?yEpRm>q_?L8~F"p>  80p0ppyy 8p 8pp>}9pN uD8cP0;|Ѧns{ρ\?y 8~f;myz`~myǴyO 8ޗR= s0ppX3jZyρ8a~my7yFE88p_n0ppx_K<" 8ޗm8x8p(ڀp^xOā3"80p0pp穀'g.p~6É,p>s??3[ĉ?8/~O?{'NZ\r?|#[aĉ? `a'U /¹W._}]KN[(0pp\!û><.4>S^xm1G8ssxKKW^ȫ'c籤Zgr.[/ĉ3M<?8ޗ'ΟNY[1 endstream endobj 22 0 obj << /Type /XObject /Subtype /Image /Width 96 /Height 106 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Length 1893 /Filter /FlateDecode >> stream xoSe#ibk QnC C@`$a@:eN^okv۾윮v'}|}ߦ Q2GJQ?3Q~ ꧂~FGI vKyq̄"֛(I~QVVv*QO,)c33=7N*@>e6S?8&A 93d21GH%GL2!!p 5AG|>2NcX4R%йT1%ʂj3T3#$~J!d@e<h !Yה' ˀB H#a$KU[d7OH~Eh~5ADH8$#N.CcC6&&d ђ-4? Sx@M֏M&A"UAG˱*Vzߏ#U3 MUA8Eh`$#Sx4όf@7Փ\[D~ 2<'T~έ*k۾n@B/CM)Y Cu NO?{] ݪ"x)ɏP~fPȢ -:90q zuA~HCKNE;$׉~PKQ)&P~~rRP?CԧP8~jW'R?v 9$E~YU~YtFDR+3LG׬[/%:~0Om#G"p8ڈ)k?i?xm(s0s ʝ(i'4O=6KL3ѧL65C -.R &b1_j׍q>s?Iuemƥٹ9DV!V+q<ħ $ʑ_˴f~h~bg45o/:YI7#,|o~eSo$r8,>Sǻ9Us\W6?X|5 ҙғ_O- B?잉̒ah'Uaڼ*G^Qކ J.?E'~j/+qI"ZlSЪ2(9xݱkw0zis\XpBO}z Z.F=t(4_^iww_LVWy?vNo O%~A_43 "VU=EEi8(ڵwSJўCߠR?CP?JC>Sɛ-%("o:Q?uCԭJoEբC~~g_@F endstream endobj 23 0 obj << /Type /XObject /Subtype /Image /Width 193 /Height 86 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Length 12540 /Filter /FlateDecode >> stream x}[vW%g1G k]]EE*9QP1 JT4d23fpwuS3Cx^j+uuOkg0ifL 35z5`3=3tƗjg7D6/[` [g=kDZEg"q"7z;[ӧ_ yM]m"?]b qdhά|!*a;$ӤBNAAAо.jn1P)qM`G)@!܆VYD)r~!%ȢIt5<8'72+P0֋YDLЕ]-`i rjg&[h48r FvIaB\(%50P(ЈOTg0}WPHl@ C (٠H\#Z-/A@:gRh-H`/PGqK_A!E.^znví۷m(nJϧЄ)[ L#>B7Y0_mZ|%)Ν[}Cœ')ش.}31)$j05鐋(4EPrgTlg|ДqtTl<O=I_Ť# x7)r_TV~F.>h'RhwѣQNAƬC)qI&x陙AaI⒒> juIz:Z~fR rOm9b?X#P…ZzMtl<&Q;Ŗ3&?nڰ142Op}}߼}[]s?_ PaI Dݓ?>|'> YD(Dg))Dǽܽ}^zW?vǤ#F(;LDlt9˕ͼ#W)PfOP#3QW_(cRD MU#`(a:r]G.hT:OHr mm- 7^gN_t3Ϟ@8S)|IJN-ěx;/"ݝ컷o mAt)l_J @;/qlM1P.4RE& Ht"*,.b 0 e3 GƊn/ԩښK2Ã0׍L"uPvۈfW}nYG)4EBoI5S)4EQA xCM!U1%T22Yhhg|J/&4 wDԙqIL\I!:(.KArS&o. $T$aèT`{&=6!!k Y3} ɪH 8LliXt|<ʵVW={k޽}z 72<83 8.i B_P0\E, A'C[~lǩǏK$Ⱥs760aNSw`݀ܯI'O:k;&J_߻pGp"'7iu5h={K!Դ4`=|T2KG]={=~fïC&LGattTN1?F2'q_.c2Op>@ &G ,XI2-@A„Ό*( VlzU 3-PXϟ?kVʸ{y?QbۧUNY y R\ox:rjlȤP(\? F4mw`?hc(4E/?>|@>W&&L7AV($D!Y firTg0}|U FL# MQ}ץq ٬B>C 1Oڡ~-`Z"R0Ӣd4յ"ku~fR rz(L_B}BN)[BnQӓPJBF)Sb^(`yEg-a3)ɠPJ=}̤C! )tT=(QҺv>F!ȅ2PjA]vS3>_j&M?I > sA|?XS}|6T.CB8)* ofZڙt@ςg@aO9SX]W >,dg`B;3&>{"f0f|`Pȩ07g§ ̈́ ̈́oB/^xGKJп’Bŋ@( @BP\R@u Xn^~QT.Fnݢw ]8-Qx}Zt¢BQ!"(б&Z/Gm@(tG@#*Sƕ"lAPB TYaK JǢ{4iU. dX{ XTRTXR((F7^X\P4(R@_.`5R ǒRt,--)pGϦУ 9> c8R$((-E((Aƍ" &\A*Pr9*I 3-$I6EQ(MNgK6BdQ3AT(xȧRtf;AAH!BcɌB !W6jT"N@edS+ȢQ:W TިF4!ST&fc?,,bsFmN[&=M!UDⲂ-4rmtgDm *YEl!B9BtsBJlhj_!,)CpRs`Q4T;D52gšJ:fZzM p`O-_Ɯ)V0DK>"JO)4F 鵏(dd32ƅI4vMA=i"#FItx10c:e:ͲnKR&ʩgܮ1PcQSK)lۉeی?QRR*2tF[G}ԧ D69M  Q(OW{*  Hu :&nBZDbx 1Zr2je)HO%L E"ZbtHN/0ׅnbCe2t]k5l}UDI@:K9WT+cRcKiC%vKl#1dGE |F&S=G dO,OHBѝHģ2OuAn<}J]Dgёq8:&x$&DW,Atxg)qpD;bh3;eQx{֖G- k#p#:i[@sU*Bvxg<­>w]DWHtP;'{. ":.x#P kH|e!X/Z;}N.n_Y vi.-2Da/Ǥ;*ɣ0|p`{LDsT`(c9*_.0&/GChЪhՓh oWQbHIQVCtl1$fиry )0멱,`b|Wu︋qϰcnzr8MÂ7&A&f|՛EBDƗ Y>7Ä=ud+gwg_roD:KL25 4/ [ ^/%fnQqǂl|Ǚ|aW0ePwދ= }?˦b$>9H⽐R $(AmqQп8 Z ¥X>E AD_ѢM͒K9mM[dPb o1(DJ I^|®TYJun˻i[@h3YO󗤧dص#4XkOf9WYRg{1DŽԐ?U쎎KuO9(- }6>Qiu]so q> '#xF$ԧ$;=${T8%fo*|0$$[W1K]_]z;w2︫.dn8{;>8((b˚A~9m=ܞX=̼B{[x*->r ?%|P# bhR@eoXlӵrEu7 +m퇚x:GEj\f}v@;൫n$DxF z xbr <%W'g֬ zf9+bK2[~KsנĠm څDs>,">b3`D*2j~x!{ɦwQmLpT{]HQ Kb/`T9C!jADj"%zPF.UB]X.UtΧ&@gl}W^uwtF,s hp,XrZuw3q vJFGؔqmGtYξfwol}:UT#ý}=jJY ½;Uo{,_w ɽ34;0uG\v.07\/ $cke VteÁtyici?L47 Ķyti9>n9%yU)d(o$pK#vُh΃ZWJ ?uHy^SR RJ!M (ri! d p5]R f}Z !z$߀KgmH.4iZi13j{mެSXFxf_4Y`<#0qqog<̝c_5zo4+1M?g^_yUC\sgH~djf~w.y&0ři`󼾦-wD*4㚼45ꚽj2}`!]E34XH:f7p _aB7HW*}1z1|k* 4FP5SNQ#rb6qV)#JR&# K}<3 _ND.דrzQLЋ,ydcwdpQ_  جSM֓!0Z4s挊4)$sI>!֭+\p!k{Y?F aQN]*nioVmgNlQM#G튯l:tgާfE[eKOK{uKɭ5g8yyp>S_ׯ謘x{Sۿ\«vΗ ^fۜpuutY kB[pmL; \vB dbUbCI~Bo>{}`Qh\os1+9{ՙ앉χ;_pm3[ Z<;~]5KϜ"R^VxƖz0bHĕmcUDZ2lDH>K3]+h+ B0A0po T9WY94<,#!K_? ?B-(_l{z癴_.pmE*6xD//j}d+1;}?_TnʯXSmI#9F~*+?&YҸu#2Q-+N=۱޸ֵB{vziBz, #MGtzn,Ht<}^Ʒ zfoIN`WtcChMHoq}ݦ03=wrcoݚ=<;scRy孢{RRUk0+۩jAS#S] pn*TJ9n{8ݹzn.ylٔ~S藢ܵ-Vgw;rpQեK~! )W5uE mzyJ68IcaUr)Jrb1V҄5dhȤ2Vw)_1KJ[hkII!']GFZrğrҳ JG=\h@( )|=J7$__3i6xRk o6T3"0W"} g.ST0Ky/Ksq)ԅIWX7fp! #piѐtg45 M{„4 r͆y>¨d+1Agodh>Ǩ~nWgc0꼫3k0}W?oP*0l4x[?t;3":2nۣ_ȩ'h%EMߖ]ވ"kkL4ĬE-ǒ&\;UEoiRYzA wA;$j(QM(,JE&!Qrl-&Pk6v59<݂=ߔrxWS?>)#Z1 th050DSq3H=-0zMHcg-X/vn5K=b"?h{G {_==($*ɣh̍;8הZ̹m~ .-UK}CƸT-vSW~ ]p{\_B\tλx:}gTXzܫs*sRv|׬JE;!͎ HQP:,v4~#)#0 {xIcVI5|aϰBv>E|Z:J )f?$")lܐJp$?PFFͦ?l ùF] *rG&' w{{PeP Um w{Gk7לNׁhjyh)YȄFюϬ0Ohw2ۿ7$갷oLdq>+Gܹi=7ąZ ,Ⓗq]Hcy'N-]eV_ %ͭxIxbœ=*~OX1 e<nRɵ[؟ivꠠriEwewPd ɯ\R2k#EOJU\Co>GfBWd.M,b$K}* a9`6M9OD 9 shզxp› xqnq!x C68b]E}~Wr1}#ޡ7~j^$W߈HX\`JBcjBcGeN!Ź[GDaIN , K|ř7[7e㊨xћmWWRC wa#.enK9.xc;݃κ`,ZÝO=XW}׷/-.LNr | l!њJN"GN6!dE raE9 ;T^P;x(FXK=PJEPb(* i*Y#"Фڦ~{~̳!T.)y1:MƏw@XizXaOmp."XO Fb 0?`ژC o~"ߓ:,E D͂ _ĩG{ǧnȐZ^}O]Iɿv|}B]m_SHmɬkŋ}R'|;ՇifۄTX[^4G#1h^_ FL -]x6ZiћiM@ k#QI:HȈ (H-$Bhݰ1<(ך߹)"ٓS&Kv{ɿ58Iɖh8Ϫ;slK9v$6^m C]^1ivIo|~%.Ңe6rL G ,O$ΰKܛM?\:S6 jD9wAa5~ U5Kƥho뭨l][6s^U5gC|™[WFvx>a 4[A$jM_qw(ҡvo TzȯG' pRF0VF]Rh Gʂ8b] %i 4 њl9ߐ^զO" яH)P7`:NE fBHM?c"3\hAQӭbpLJ۔x15Ho1G|o]ݖ|xC{ϝ-ft2cb~?CGZkW tsL]CCG"/R}֒uHfL3£%/hZd,9>y |Py"} endstream endobj 30 0 obj << /Length 401 /Filter /FlateDecode >> stream xmRMO0+iKtG5YOxP(v`áo޼σ}Q OP~DKQR+0YCask]AԙQ(n s!fRiaԵ .:?0b> stream xytwq]̴3mM[V;jU.(VCZZZRj)MQAH"A5Ȇ,"+9I/Z7<s>lj:}uo fe(9U:CLeRKT?S*2[/VTڐVV?Ijnc_اUꢊUJu5TYTJvVq}ԅL{4JxF^?^ڮYޓQSrtHM>UcSԴqjIrKmuPn?uRLbJ\Rz*PWQu2&e) U35kV.v;&muSeܢ\WYULJ}JelQ9{T^{UEeeuk;,)lKI ۡ<LWU-T@୾VS67= nk+ݨ:HeD*Iji'գj:E|X}jURN*j*FmSߩN!ʩ)>9'T'5IZũZU;Tח*u>V_<:Q)ʨ_mOIgEw@**4J7U*:Q͘ZgUfW*STf*eV4Q}|U[ "w@cU'95RFcճ-ϔ*Y^UISՖwΪe?[m|TEYGߥmWcUjjߔ'=0BSՀ*N>D)c* gUZR)Q_zL-W¦FyZ?TF]_kxKb6P@2BuuQ Go"T/KS__Ub|ګcUjn |\Y&o3.Vƫ vjLխ ꩌEe.~~WhJآ~lEڡ|O̤qjKk5jo *&cղT"U5D_PSrW(uTU= jYjAyR%]W ֳ吊hncZ顪|12?T_}~y93+%)V%K\ە[8jw6U'jǪ͕e*QO}SuHqelٮq\\*? Nj΁EkJ_c?|u#Kh"(#2/yl UؾcYZqzHZ;瑯;ӾP>WOVUTPMTo*yZj}S=/rKճ^lxul꒨|Q3U*P|I*@MuF܋ rcgmKUt6Ik=j]r@qn)3w s)tץ{{= aE]|M4'Ev 1=|GӦޛSpx?澮pbtwfܾGG^pyHGJ=L\߫܏%Z?0j%X{?!ᝎ*N͹tu1@ .X^2'UB+ghtz[#7T7=slVBzճ^grU;p[IVQ̢#.-1fS  _19nĥ'gfNŖ?GMvVLi_W=XW op%kwl{sfeX ^flMʲ3.*{?3h; '#(bpcyzd:makrM KI0Ճ=C*qeP}9|kgu "- my}@y:ngᗿz5W> 7ʑd?̢#]*^9aV=.s{1̘nν۷Ss5=}s /jhƒol}r9nܿ3YF?6ݸtRN[wm  7Lo3ӇS/~INŖXlDm!.F{?_y nQ֋l;J\=x >xNHճ9HQ}ޠsiqUO֝y-F[}}@?Xx Ϊgѩo/߸rO~%zrKj9n -VDEojmZm޽WJvZ6zה3*M1V;2if'::JtħKoslbf[j]`6thjdKKJ;̽Ϙchl`P}:[*OW׋'Ŝ r8s"63z -_Y{2u}ywnճG;.58~qذgGE>h)S9ps-9iX\g{p]O#%㣜yG4x#lmq(Ӓ;ըB *}mNhs{d's%tlRFfoݯxwԙoTB+y{o03z 5W_i9u(a 72Šr;ΆGK=yCm˝G:XvV=Gص\wXlD'_ыSc^s԰sSlTrZ].o<;!+快Qk[f9!GN/H_q [kQ|F1N<- vdܷںwSW<٬)\}\z$1j]~7ܟG'^ȕOp-"Ǹj-ί# gfB^Jjx#75ڰ7NS&labgmqm\ qxz^/yt{\}Y\|~BLa>wfY֔;zÑGϟV)joIVcLf.?~78'K-L6#"d5EKr3^P#eW-.zYsFM<~՝. .m|fk-yUOu:^wRƥmKtCmijZz70gEDWLLd-cjc3.ɁYװ6M+,2)7590b/Ԏw9rBtN܈Ej  ˦)nY??D{e=7-+ϥ%_OJhlxv)ism\<| $G^k{Z=p[l65|_K^^˟yp6mT<z$伳&֎KT^RSKYs؉=Șb5mѮ)OYM̿M%m2(}joiO\xuVzM9ۛ`۵Њ|׀YEm.od/$[eÛ.Y?pOUՋnyZShŵW+ONi8!\w2_zf ׹5fњ\baZאsi^z{aPsJϋfTD8v _5F\}+$V]9j)tNm?2zFW\mTZ%Leu7\k~\oi.nz` endstream endobj 31 0 obj << /Type /XObject /Subtype /Image /Width 450 /Height 100 /BitsPerComponent 8 /ColorSpace /DeviceGray /Length 66 /Filter /FlateDecode >> stream x1 g /gZ"z endstream endobj 35 0 obj << /Length1 1887 /Length2 12235 /Length3 0 /Length 13408 /Filter /FlateDecode >> stream xڍP !w w  ܃#szYݫ{ݽ u- k%P.Rss"h<#A`g1܀o6i 7 p r 8C -@V"D#vuyz+ 'batz;=|' 7;+V h݁n^@k%T-ƊDжxx[oG-x; Ps:EV 9V;D ?-N.ξ g[ PUf`X8[Apt[xY-,JJh,*>w+7;;Hfgk)} 7[}\g 2=]tA@9o&l@;;; Xٱq O'].2 $w / o"$5` 9# ݿ`6~?~m¬Ύb6)}e}9KSRgpp88|<ͣn[? 6`_r^ \tcvv_ϐSGAd=E[8}fMvR$<,vAm˂| +/rA<-v{.+m&tߖq[e< 77 _$Qs5):=BolnH(/?M_H&_`ؔAoqEoqE|6ƴ/b6A `M7 7_|/_M?MP!M?]}SypK:Ǜ8J{sߺx;7z<$zoe|'ts{{b\=}VH `+_$Y&Dfi2Xݺ<j?mJ|Xݑ_!?nknOx x2KҜ@Z,>hFH¢-٣Hɏ^{@Χ[叱=Z^%:qơes49p,X>s7XyILH'J 79֪9{ HoƦi%KbKmHMZD-\(z+G'F ~JULG%XilHt&o+y0+Br>x<-\lT3:?ّ,^ 00}wp@ ]ckxeQi7I9g\kmSQ)*vyBp槹zÃ5X6{^UsVLQ6]<#+Y}0J8 ˑV GՖ[XF^ݩ N4NW̼Ci]+i0P9+R zM$JZ#K/7ulGwfT8CPe䄲I4Z5l77e;*[о֮p)CŻb驑:qjxKOU^wLu$k+&위إ{_Jz]%rwoB&x'KpUOeOa>XӲ@Vr9] ?d\A;%~{P#h [㬩ӸV>xE^+° .e(je{”*`d v3Ny/X3bU'ZQ; ږiI`0) ]:` 䜙zwrOnLl66~Z2;>y>p'+Cτ4#' agz]αS((IW4"|VMFX7vz7KqSu Yjj2V]jQ~6i{ޕ/7p&US܇Ydq\,4Z+o(7d3S8 g [!Թ2L!za.N !\Usj(3,CDIwZvG+.iD zeB@- ,%o4eru ?K<(S|%%5NM\Nb-K#-8/E7NOn^te]`:%ѳד DZfg_{t omE8#Aj 81߱*_TaAgmldĐ RcfےG&7~a]ٰ멚lnLJx*xًg@xF^q2aRZaD :hoHVw\r%ڐtY+ νTæP 3 t,٭0!!UԴk3F5C׻[9kt jT7f/уi$W.P,2o]G]*}\ϙECVqNHb)7ueO_~PWXKܺcuט+WoDg9RS68dLT4P+xWYdrI~9DQDyWW^MY/y (v=Dw7 *y(K'4P+䶇Z̵7.c#迃 Gׄ ?6Z?FZ?~衢"L`;"^جD'"`U5{4 LRvD>ׇw~Vq#Z:m<əW~<8a!`ʦԑI,ƅջ×~KEjAq^03Hڈg ~ea+[ʆ\O*@^θrkgMa\,N ^8-qtWÚSC ټl($Хޡ%/;vpHI:crtF}\7:ڕn)l>zz:zA}#(x{ք>lIp҂Xl{u?2oG.o!A^p᛻>r\{kE΂1Ha_,bnLX,P(tgϵ}C!d2"]9pR%&E}ŲF a;?b= nYDp4[2SF+e٭--O_mLǪW2^MX-KDiVbuyOcv#wLX͚ܳssI-<}Aucn/m4\O%y%뱴*K?ʇ?yZn\݁$ͺ"eͦ3nTb;>x-3pfLޥ:E彟9Cx:خi69#E qDkd8qiT%eeǎ]Gr@oUTeJIiMq6c *ËAK;n ?)j;o:lԋ-!=*Lthx~-Lg4hμ*F!OGo}`X䗭*a 9cG+觪Kbo;:L8HB0=4d-D/$M_շmJs@,b:fT_Av{ cK /T6 IϾp[ gruG~{BV$-q'g {g'rM9Zύh_~F(a$6(*nQ֡_X}G=j6+Ш-N ˬ䫪f9GY(䴿aŪe 7L MȂȐZ矫xߨQ,>N_& OQF@`<#g}^'g @9k6Dl7F N9Tқ:-Uxˬ!@96?|޲)ebs̟cGVg032?N,.u1Pif/ Iprt#7pi c{j ytJ2(t.){(dfR hhOߟ,ddV){JFaBh3B8b>yC[ Ia l'!8/U2~/@¦c w<&)2tubC#3/JaPֱ}koiK*SQ'CuI"_\s'ԞTF:y ᅯ9])D@ė8]]dy}djS5a)ޒFK@5*h^"'dѭI%+;a^zq=d1sߵ./Unӧ5\M"@\ڍM\R>z؜ӛ3]!'W{LT=?tiR.>ښI =K= [Jl¹oӗ t0;stԨ}#Be2 _ֶUi5L'cie> :c~_dH׵(!ͯd(U|J Zȳ\ ANHE,%-;nu+1+Oe{Օ0&/}&͸v cl#yJ(ߍ6Ѿ :ҙzx2NkzۈdCa`4`BңP(Qd;C"hG QQj@h1'CMJj jᥴiMx=g VyMLR;S=#46.)Y&2#~~ZnFLP&Vh a(`{XoDzE>&Pi:9O]5C$:ꓱkJua?|OndVswTQFw˺Mc T(ɡJP6c oӶG9e2^z2u$g><2 =izr5U8ܰJr%TY00)Q'$ҶqX !GWaSBEޖ>WxO_؜Ҿ~$(q#e2d{4ygQ+XaSY?7r杉ePOuv_B"qOOMmNS\yLjq &81<0 ʖY*F!ɑx0pEl?Gw(%cjaS<.J8UZԜԶF,5' 8)>W7B?p^AF}T uGX}Njߧ` j􈭡Jz/c =\"d}af̜UeFJS# {Z *}+T/ߏ{- 8!fjnQD3ɵרFu3uSTr9G<.)r 7E2LgvA [l<&]zQԌ֔fE?lu~~9-}R{ A5z>lHOG|S*€d"$bU0k6H9%NH =Ĕ߂ "W$ 3x[79)QR1!O}paN΅⾵ZX_ز1ͤNTiUq}ާs&A[zgɨE@ yw/970>ZҐL.ĚPk:hh0VM:lNd뒺p21| L[7 `d$O C2 o$c0&$b| nb8h+ yck_M[,POZG’:簠OcYy*~\9>+匜:ObY} U' Y !OCwk}~s?,_1˺XpLE88n3*+0d?f@b -(. st\rNƘlaX}fyNsvHK4DvW⪰S UpVp ϺЭؙOfpf.:$Yf_1BTOfp"v̾iPy-fϒIJ](NJ-*s9Y5_B:^C+ 8X\ٯv佳^I"Cukmt]vtsn.5sv$y~q_!u(XI0;4՞e pj8B1 `Bl Kp𦡒ktl8^oGx8@~4t%1Ϗc!tGJc4ŸLlʛp(U0S,y^'PUΪ? GR|]+E h!I e'q_jGB$-H^ \9o*)lH[;9/qKQn}8M|SʱX$A\G#TRDSS?c9uk}s,a揻anQa:)3!OG LRͤvP{p96 GN9,)EciGJĥ8-5IAק;Ҕ1"g jX:e 0@t,shr &w-Hu őH7A_ h'!NW:5C:{P:7Qd}Ύ*oڣ*,18k6@co2tk^i .NLYXN; "65% ʜ8Sè/3F' ;9_ gĤu $f$lg2N$xhV#lFp~ľ*,_ژ  hk5c`M^etvY"S;Rw͢Y +T2$k2{|y*G`GR ş`O S?:jlx?$Oc 5O?-A(L 8֤y}ޚ-)8ޝţY>e&}Fv!:RM Dy~"r;6זة&ALO]D^T΀WЂiIt$L1r+S ٫2)3Se a?/:&]4>򡬽bʌl>3(4k#I2Еɦ@pD'7NdK2|%x 8g30G`uwL9s?Ch4RG7jhl\˯i]Ō==V:v |JD%d5cR_w'2&~! ŒGUt+"5 _o76Ax:~_ H,nGkFxCImh%O@*- mgI&zl}l,n99-d`4{+RۋVsjWWP?xěY [4םsTlj'xy}'q[$~~^1BK)^v9nח`8@^ZkGJl,F}`nŒ^*0ߖic DZQ=qi.e)=9(V*n>R:'l?i~t!?3s{!;ů)>X^,,Imi'>Q9w H|,"TK5Y詷h|s,^e\kfủPTEciʂ`7H`4:"7Y WCjH_u*S33wv8](]ڣ\ɗ!)M0D@;UUv^'R?n{5HU\ݭ9#>X74v>kIuUSdz {nq, BbEʿVGPʙa44Ro2a) _SU2:6 x{kYdxLeBx[#U"C QP>f G[V5Fl"B|r|e~=RĉP7*yM$xR@hܾ|'[BRS,!#]LUW"~(b7heuuf-09SGw Q݊taF QV(<+|, $dsC(λqwu C!pA?FևhGL&;au;nD+/S v/C=.7Й¶dmsb45$:E?P H.\dO0c- ,yE7 Jp&N~0{aH[ sW ~C(_oA9HM5|;ٱ;1ZewXXNzQ#}wrw||4,Jh\SjZنʝW*Zl/ZɼW>7:bNb%=(ڗ"9',>O'*=>S}9?b1 ER)[iI7 QĪgy@"FWH2_ 5ʎ~Ť2dԲSQk5`~[sM69̴5S=sWJ^FCM W[j&(=V凐¡BW'2/1Vq'5"T=!w.3nG+b l!CΕS(2b9u.p7&q*M8{LBp'_PF&w0Y)Y>UE}q:U\!eipCA!QauDAMhEm\f#@݊If;t@kˏW=^l;fʄHc҆c Cel9b]_&2J Q "K7lГa rqԓ7-@ׯzˌeL&VnB{3g o`U9H?'ozppY@d !"gMq_ ?v"/+V aD>W"K+M +VLƖ: Uŀ+i>5œ#BMUWڻwB͔úݬp撃߉:A"2B-P_%0;l?c#L;jk 9MVʮ5OV;FýP; ,XD:EͻTKxDH8X!sN }M݊5!sE&gI`~2y]WB)IXS߁ESG|zt ǘBZ(N⏇՛j6ԧ1RI1:{o J}5&·vJvOk<}]~H~Crt+lG)ru]=.JH g-ǻϳj|ՀB2'mY`ά *IxF9|Jb daCeH`:c-EBi\- O&i$D|Cz ݮ ,ɍg!n.Π1C~b_}LN83H 3Db{_z{$Eq=;wdHVGN7,@#<o&#~xgv$7ÉE>$-T(>v.ɝL*bLBsO$fE{K+/i-&uz(;KFd4+d=M2#ڵ 13.`C U=Uar{/J_?Wh.Ɲ`@s( ̎ł"3?Rx&>-QĈܚ Y8brBV ,e٢ȉB Vo([ stUtuhcdwQK?}"W  R>}lB:r}/Ƚ_Ȭݎ2EY@ co|s$}x*$*)b$fge^1 _/HFy.C2N "0&泖>dMqgsK2Ttî*(.;9:Aucc0]agIg*^;{. 4Xch_+_1J.rx8Ftza ֍WuYȱ{n3:r"Gyz\3>語O̭ ˓a`xLǕ&1]Y-BU6 endstream endobj 37 0 obj << /Length1 2278 /Length2 18718 /Length3 0 /Length 20067 /Filter /FlateDecode >> stream xڌP\"Cи{pm N n]; wwwd{5>ӹ QVc13JقXyb ,ff6FffVD uo1"& dg1G1M&n ~S:[X,,\Vff9č]@fF- Bda ~;?jS #` ڼhjl P3CAo 212819Z\A`K* 40@) Ǝ@d uzp5:쁶2=ߵ0lv6657uZA@<# L05dmlfwI[NdvbtY"_4oU5ڂO4}+;ӿ:dkfWfL gMDd@0 tL-WwdK=- 7dޞT/BdaLh/|G@mX}6^fv/ݿ2NT ```5d\oKl w5+ڷ2'b\voS Pr=ffӷ?,Goې߀$VSQۀm6P{[k*@6W+6~[[ $ r)52k-P ׵`xkѽ緫m$V653kX98ƎoM~CO]4=&F[; -=o#_0%0A&?$_ `L2,A&??S鿈Sb0Ao'Ao'AoZϛzz4/xәY5?v$66? _[mao"Kk7,:Kw{K?,dg~?[1>elVDv0bs;|+@s)˿+~ &SX&q3!g{%{gay?jV?oNN@v-?o'߷ud᭴ou|;SFVJl[ooV h0kgdUrW%ʰ3*0EB.2=`F$q"as\x^O;?DZzEj{ԅw4?C7CRd;8sPEsr)]Q٭Cz*d҈/1ɘ!3ӢL_Le!zExꬳ~X)Sguj%!FOŞ,.Xwk/ BO^fȸϚV Rmv^ŲEߎ.i0t$U¬if $e.ranL3Y Fֶnc]Fkgb \/'D\g G Q~h`p7ύvga_7ԹrRjwRJ*ݚB0!;Vu!iFRxi/|r/*XP;ZП/­rZӪtG >W%h2ցF}ul5;h*MhH+ p8F7%2?; xNb3QΙ V4fbhǔBoOǏ]v%v4[UOxk/dˆ]<9pfPYHpn2'p~tcK yJe"/Q bUbLQ&'~ ߝ6}q@(\#oˡvJYp8UU)qWG*nzERns[ !ZtT_NOpۦ[Qk&'fلy!!\wNZ݀Pk%i|Uu%H5oT0 }pDVMȽ4R!l AO%_tuo13S0 QPU!] O:{Xԯ}݃~n'x_q;p8* !{ty6%>"IUS/"<"~MJo-m9j@jzɸAꓣZ*m"|K0uE'#6gi8:%eƶEZ7pRN1#gлNo{քbޫ'{q;|ý%xðFԣb04+Uee\h'H<nܙ|ĤP 8D;Imaw5},{k;كx pɄ+k'%kƺawn~٘^ᴞf1>j^q,\>mHN}ׅcIM=6P)d)|Y}`A}U↩B,[=/KJΣωA`^A w8*V5 T5n;t5̞P(|{qg9J=o#U~zBQɡ8_ʧfrX鵢=aL~y o 6-wǤWGȿ"0 ȶIB0XKq>X|QFDUrge&D}6ﹾS4! LQ|pjT ydc} f/:UYGL|mgKn^2C\dvĸ];A2KpJul`P5T=9<"30܋ki~8\:|]y9Jû{C0A2xdR~Z{x* Wbߴ@1M@taXFO @v9ðJe6N㋻1@AY[df] lm`a j<ĻXAqi܏[NnI㳕9c+(*U]ch'%=ҌrdL!դ)_aZN]l!3$4Kz.EH[xϧ02MI`L^fl0{%mJ$Q+8MMx~ol2@򛞣#u$ ?0!o?f7vS%C2"\)?G*3㡪sr oeu~ "!vZu ѰEdKFsڞVKFl^P.F?d}׫Ivr;<ra%*SNF ->Ce4m=/vv_&wHZLtF46up!y^џxk?A_ M ╾ܽD]aT? 0%X漿 q&Gq2A$N᧨&Sp]C6a-5'-o>I0مʯφjDmS1W}?m8:i:W.Z'N&Ѵ?_FG-@EJc1h6ѝ.r~Wᑋ BV+1Dj1ߠ N:#|QLi%Q;y&XgO(2ӽtzk`;~0׻#bo'LFg9hẕ& Kduu]ay[ V ?xj5؆:}MaZH2"q-Bi؞A~?TH`TWLi{6T*eg8gbE`_AvQ E--F8IfOwzp_i*-zm그yeĪ^,m$7mAǸ<Ю&=EmA\8MoemL&ԏr.OU~&VBOսNgOWqy%ЛcϏt $>3 i"]RоMT OsjLH9 /nX[OxE4$ŜrN5)(ͦb8M?RK' r <~!cg  ,2!?2. dG@)L^<ٚ#c:e?ہ"ast8:M ˀ{s`[ TbF!X5Ebd=Mۻ;aI$=:9m,y*>߱XfWta&[34{H#ѡh8l"vKDzxlfmTp.%ТCoȸOyݙEH sRǑNS^2T1trN/+o S>7hC.Z@g7bك$۪LaXG@hmPy!MkXd%O4=.wt.; !mIԕN&+p>CZ"j) BtrK裨?`u$[Ynw,C{jrA VQqdaTGx6yRtWi=4d=[D*G=cc:\\/J֊I[DoޤPeޏl"ˏ>s`tǁ;Ɋ"ӓTkKVg~S ڲ0o5`ȰMXL8@l75 Ɋ~8ev%HmNbƸBm;_i~+-A8.%ѕu?WL#m7$W4d6PE<q&V{;5Kq&~?Y NCD6XռKF !lXnۑ#:6gqEUS5__E4:Q$\:tv{dJ3.Z8ڄ麸pӠOP1/ ~NibuhtT|HXX1^g#,/oхK41ՏZCd2]mN2Ai.݊R(: ,u+U =|]p!} G _\iɤsmD4 NJ3246"$Mq]'y|ʪU]bWLϷ; ~FcoN(*{etL=(}%&;6 RcUj=܅e^,f<(ݬrCd)f2$T"T#|NܪZ*FuY9ъ$?/ۜX 4m$FLt10#o|Ci[gZOW|=JNGОf>e-e ytڧ]ΫI7rŦm5})YNY,o\"|<ܠA.le]Q W餙B@-t3E8@ pA,p-s֔;֒{Ջwn(-.ze[x=[zɐG*9C[=V9Xt5nH#}4J4#00p\+'밶I:M;Q*Vlc`PiIg/C#G|871FbSgr2B?ȊHȱH4l%<8ʒ&)ʒ'7>Ea_0=[ q&q`תSvјrqi Smy^OmW+dHT,-d*ˇ4A!T[LIQQۙs_b#"]t b)h3DX.<nƁt\.yĻ:Rk?S^p|ǧ{xOqh2Wyy9܃|{گ cU{XTO2N*x0o]ŁUd7K0I)$ǵxz \Ѷߧ9ӶѺb0k/ɶ'_n]:AH,GLUЖ 7,FfyDxdyפW0TO:)kʧ;`ï{aS0[鄊 B.s)f{:|},:<.`bkqma' 뚙kwN@xM˴jnI%gҧ, 0LQy7$SW<ۘNTQ?u[ 7٤N _ !b,˻,x-vˊ3{G|jq[+=k*;V=^8UV~^v^΍ ydHf~٥rFa?9~}$S[{By ĂF+g:j}@l^uZ:RKǸaLB5Mj؛A|҃ǥZ#N3=RvS2מ$O;c^P؍Z{pH"NZ`.ueI NyYDv8is]s Y\ ȑwp#XH $=>v OPY*AL"%f+ gb.΀ѧqL2jI>4 h,35`-bm SZꍉ$2=˚m T%#u8WP,Zň1)iHK?=Nm||{8']}It 2ۊFֶW;\l-J-˂T&{'l'^+0U*4g H,W>B[!ְkyK1{q)HZ{!q{gC~8GK*\O߷SUw!fIbߖu~h.\p3tG}gtL-YgM͠;=N>X0NFOWiIجT1 ؂LWs7Jv&ϡFǭ ƐUs+WELi@q[ME-% ^q6=14_\~>%FbL޷UTxB\g!rhi}-I%q1KxNN&Ҵ8z,w?fuW,`e&*0ؓ8xc*٪'=9Go>ȸfd $U,vƧ 5(.$~Pծe7^|6,U: u&O6Ä{{**%[L\K/8 :^EiksSL{Sqmid6> kA׀{j-﮶o-I e8Hr|BII<~)0[v&Z)?Ӌ}۴Jʀ7OP\+0a-!N.p 0 Ua1\b>y]  .wxGOUfM5( CR8}~8›32>Ǡ_CU sc-7 M}@RPcz{̦-@{)Q[-uHyc96|MKL{X8{hLZBe sWF V'XxF{eOrB̈́A7ݲ^?1JRW4<(ĕ\iA4&g &+D|؍}qVHH| jCcs#+=NhW"TӍW }T^_2g ֦;DۮVCEQs?m;[%g5S!;ݧ$e̾4fcW A?s:S B" kEXWuvH27r PJN.>CBZc ůZ>OL<}.q̂"hn/Uk0H >>M\}ԽN+c3 N^9bTq%$D!>P5W  "tv=& "1T8vP妯?YǦP-iYDE[kIS5>T3E>BGLo1pSU^JtV1fT'=(Jwoڲ؊jUCԿB>c<1P!{x5M,ϏlTk}Xj0g=N ZL6a]"Z&(i);2q!EANA>M!V. >Jq~U%Wnr{Na7)*C061ẗ}]u@}aWU4^3Jsf x~V=@Pc1.=:|\;z#sZK.rQP@ 'yZZ^m =oqK&w"CqܸQMֵXOy LYUuTIg`[ZUsYmJd'IƄo#)l۟dOUΞLa`\ :#6!,7{w!-EP-t|>]WبI25@a|۬y.E4[JBp&qEH@ypuuǷ#”ϻHR+'Jck  _ 6q0Tc[ $͊Գuswnj{+ X[i,i[Xﯜ:hvgp]=5nIQvVE?y>QOPlI FD![>Hyqѓ|:cE鬢dVpzU8iGQяfi9,2-?\V칂bB? He71-п, ! o/P2^װ8fƗH}̹fu$oCl>wŰ?sWeҫ\X+l7?J7)2o?y[Yol1ÐWI[Wr@ybHn*9J$2:wCOr"#0U%'2"/&Vv4 xwU|Y7ԼB5"e򐿌i (IJPP b5NZw&2)$Qr&ZJ{<}:r>=ersf[`ŦasmnDEYMDF0,Cw=b^>ȃ+KIy}s1Y/#V{q*v>و2m3Sb4Y~ AID_Q+D6_u(f]s%nXá'srl9nH8 |$JҤcڬf™ә a n8e VbRK62?tRSw&`kwt >B)⊴weFtDO9;4qW}:1'R㸒׶jtiMX? Q |Hgsr )EMe>0w0`]ɐyy¨Xa#!&ӓʧJNv8+f!n{u9޺.>v:IB ;:Y'U=DDD?u<+ڼvKˌ $|I3ʥ}lhгQKB3:D}w;0XP K~@"8E;oN' _}2c=bpˊ0=CFfmz~t"7 $8@ӱHjѲ*P^>@Cl6)S븃 ƨ'~J. =v lHI%.N#!贃X)c)tyTϕ![JE`^oÍGeii~`: w`E.a5B]@`\aJ4Y: /2.U@B ,"X7Zڦ9#F2} +pМuW ߥ ʤ6Z3w3".k {SN>g kK2dRӛu ?tǎe0?9ݛg. kEB{%pAIB14Y .g"%h꣚s{$lU/ lQ+ uf@TiDl("4!RO+ 3PdЈ~@^5Hkz_hw#]#< # $sø:H'v3&8?7*=q)~O+kdsiZD铫Sww\Sw;<B5݉aJo`@~xh *#`DϕQ\lvice-nlj)$Ѥӛ!/:dZzvCn *Q6nW^_0Ԏۡq? |<hz7ZiVGjϵW6:U]\[te%+׼ig?:aF {1-MB30!K 1]t-<uKn6V?~Bhwأ"RwS۪c7; 0ji3A-{ث@gs CQ&͹wtMi{pblX!4bM5x9v$Q*a~w=bkrYC=|CՂ~_ #z-<_Q-j&TO׋GU}}Sw \F?y'[Xbt6W?2sMw6,Nu r2.zTZ{jl^hq\is%hy\;LVyJk0)G4 s($sm#!_w=OdT`ы (5Y 0PGW?~"hsc3 '5 l8ipmWLz1#]+-ݰ 윗tחMʮ[Xo๨OU-uv:1ŹI='u:ajnޢ{,%)+>*N:fxĄ tCm᪡zN2 5i5{w0〼P#;t6CJ]hqYU#!4rMIվpR (:mgj8T Wƻ%al/=CI߿R%Si 6#X?6FRb4I36KŁ}RS̫7W1qZϺip?c~%yxtn31XK8 y6^H . ׫o>Jksc|JS(x0&QSt4AYGܗpu cy{IHY3s!HN)G+g$ޑ 8X]5J3,?<+ Eч ,&ĿʾϨLKqo[ N.1Gkg \DVL 7oe͒M!\lr42Hodp>!$u#Uz&De?Wt&_A ŭI fHE*ckS&!2dU~'"^¡-' NG~n׻qlcTK|բič :0~|i^~4~gXk|DcNBi(],QS1oyj&hJHbEAunS+x?_ X;!YƦ[ґ%/_)ADi"]j&i=\AyYYt"{ս[0B9M@i,r!i).U|tY oŶSic\sa. f B-,E)*īL5 a(V(BP{_8 _Ɵ6LGlDrwqmf"c7keP =!DaV%ti;E8tdawd8:}|9zR44M}D#9I 5%(*`wh-~tA`^r̞)J͙XK y64e:K:m0 C(dsy;Wg %$TQc))ni7es_\HSL- \뢭lɌcL{^I+~h[}x RS }/߲9 |ۍVT7?N@8ͪ* \D0YpЦ}TpA;@9ܫ >TR:`rcawlpqTA\Q>+#v~ )νaVvߠ2~әRH*Cmr`0]ֳ5:"+n.yJWr1Z8_H08 D0~BcaldQ{j\o8̸rd>WW<(%+"2 g+0֘S7tn0iN+ܥ` qdF QmNG +*_&F2;#[Cd$%Ec-_E+]ViTX ]tE2"m`IJG6 %EP:*6"<#[ I__,kTHIЪO@i=M鍩>̏,> k.ogHj#eKw|uͪVӲS %6 7=a0e_*QD{j۔(3M@6d}2aweGy)j - 7`/Hf7}F|FV O#8 waզ+f $F߄Eп!g\ \.[rC C%"dQp!}' z /⾤Xd:W!:-UJ* Dg,%'Vm#n-zQn}#bLy4᯽OjKOߕ慔}0\+֕ݬ>+haѓÖ](`Q jʌ"Ze BUqSWOok"D3P$'dv'NSw.hK :s@E!1 /'4{!`fYtĚ]eWY,26¿lW*I\yhkt0RG|~dmzO(T봋g Q(b,Uҁ,/R\VdF!;"987uD*+2\Ǒ+.WXP3Dswnk H0\ ޙ= $/Hр~ Jfaے쳧үf8oVJkG+i_i>i#d (<{M/.hԵXJz%2OĥQ]_D|G1Gҍsy*}($>oS%? ԭ- ^܎9B汞&"J,nf dR׸TGCdQAݶ^s&`kR=^ AyPN!B3a'q/yzp/ gV *ݙͰ@! =q"(MIosS9ߑ-RiTܸ $*|I 2~WWs"SVAFQԘ4 DFCKPNJ!Ey'OcNa鷳d:CÓ:aHLvbB]Z|%d P;;\OQXٷnCt숗NEN/KH5.z`+\#4,yշA0>e#2R$*oOqVN 񈡖v Y+`̳Ϗ ൸-xzORLDV* \ D?_65=l9(F i( .lu/;5{@d [kb5w?J?g4X XL-~:+F \:8`5ޒʫ GK - -DT틻hw"M;r;"1tcnGiYhd 44"+E$8~\6gM#u]/ߊNMSJlBap3 ;r_J_F9Q:`ymSD;Hدu?hv~O s=uVJۺvK "c|!qvԨ.1GKgZ1Ȼc2|UjNv͊>労[Kd>aKʖ,xnzq=ܵȄ5x`f*GjI`Z!W!AQ02w2'@MZ&Q2,UY;7Vۚ[y fMf^nuc5# pȅTuE3O =ї?t(}hۺ3/MrśQqUJnFq7~ٚi-m{rWM1@“7+C##iJڌɟn +džM(|`|s GkԦC3o> stream xڍP\.CpK`; ]klpww.A!8 GrνUծ{PQg3v,@~;d9Pih4 6ͨ4Z`G'Ԏ G0& r~)A.6vN~v~ uH\!f%V<J#pXX:?/O)O8@1@Ζ`MA6u)_)-XANPG afv; (lUƊJа8eW;g lbgvnv7d^\=`\b~~z9\gG?PfSg bf_!}Ͽ eiJ3U}Pw ''p|; . 3sa׿ϟ`w.ehhh?!ݐ7 xMx֬S`Tm_36W<bv6n#I6S8Z%욿bV:A~*v |ϓej|s8=+ <8)qpAg!qps؟G G6V;s<9l*M!3L؟7|*6 0íyl6A~ 6?  ֘8:>_ܷ?76E [UTl Mlk1x;a"gx%[Ҧ~S-rXsj˽Qv W}cb5/HX4Dx?:xkX7~‹w#^[8: wS4y&ӄH,('XSWcO L>1E^zkӞNDDz^)^%Y8 "P) F:%"imC?}PN<[%jQ~hʸw 5۪@ᰚa]}v9/x14EƾtLG)avqW+l?ȗp[Uy/qQ%k9GqYk nmM􌴆VQ,:츇L[i4Fz굏_,.ƫKG61P[.MK+F ˉU:R9{DIޙ]N-]׌Ϗrd!J{A5&(+HopH`DJg ~1tG']2ZFԎ(OleH&k^w c88=PqҚw.|Om/jޤl7HhbVдvRmT |̆zbM4p)ɀ~y<]cQRW`-<7 >|\_F(St׫CmB/P?ιANfd1/38^)w.̏â"%WPX< ߫6'oڒ``Jfz0\zy)i$ap_ǚ_`j6Pʱ^78QNncɲo'(\/? 3E/ԼjLHvv!SVy)а׋7D3VnPi4)B,24 'Ώլ(̃/xzS ϵanp|Pyl<v A|=AߺSS4'ǜ u Viި,([a4s : `B lgT´X&VBAѷ%nں]+}AGgSLȌs|X:e~4E8+ 3kuxU2]@ܟY74L]"WIzKZZ{fA|w!z1/H; 0$^V,}|Ic:8ug{%_uo35%̥xYk0䴁^+2SBw_W]GSY,#`}P0Z tq5)^68r;9n۞SHr=wMgP%OZ8 >ۍGVT9RwVE!P[N6)&׮P~ZyhҢu ?4Q[?[04DEGS^y<7wY%'.^˸(_MW9.M:Bj)b䝕Zӯ؄=Mxsv䈐#!]ͮvօaBQ:eͲ&C)#J~M-2m[knDU)G0 rϞk|( +a`yޜ5fK`?(=ra?_)Pt/V+;H'40d PjXB5ɧM nuHo뷱t$2rt7);?-ICꑕܸt oM+_CL[ƌ?QƓ$GvsxF-pl;Z!֞ҌWILu/(vEWY*d&p+%UDSsmR›'r<~݋Zi?Cfbuocj@e</j=85>ks!L 퐿u z?_dv#9a:+6Y:#a,*3/΋="VWVfתsp5nܥM[%׮l :?\UqY vX;ZYd'7z0agDoJ=i12 i3Xᩈ6Tlv,* i8?È*G' orjn=FתqoNMׇA7q](~N60Diғ\h%'"BE0%_h۝pS毵 _#5Ӭj'؇zQͦE3(c{ʃJ:UBurQmt=DJەJ\Y6 ȁ&XREDjcld)G$;JJT'pQ'CW4;n/2L%ZQ&$pߊPSdBt 8x1Zα)@z%5#ߜc"j傞C_{ K1o;:jz}c̭8@`eKm QJVoDlr W4Wa M|<\&.ޯCQ,v2hcEљObܚixZC,e^lصjF 4YWh֊5#B9cط44-Og&W !+l7'V!)L8*2{16xWă* v|6\JlT]AНƏN|i%B5]?z }Hv#:^&[N5Oaɦ+.Qj{SQ^eŅJ(uT5_/`"" yeeVZcMd:s?U>u@9'âB!u3yeq:tbl Ӓm틹.{1(Dr:M]u_ZE@V> ~ c& 28N>T r[ ZDzZ<zߒDγA23 }Q:rmЉ' z5H,D?>y1ER˽^1ڻ~#Chb-.oU/I-9[׮UܪETMC$At 嬒q$ 5e_z)Ҩ:3iA>KV4 AsH3ހ F/2z^$61P#Tpg(`7:Tχf%9"T5Q8J00So-1:rTӑjklPX뎃f3W7clݰFK6i-= z7xE5OX#AbJ]/zTqBS=mwdgysL_P=lߪ=nf#] U"Jl g|zArK`d۶qy}Jtbm)|(.#q2 SÂv󧀷lPۏXVv?s&m*8'#$2bzlGSa7W.ZiWH3uiY?d4&w~c2l1]/q"٧o]K@?SF!R*]GI,GӲRePD. !U]ۘ7 x DᵻtHGzP*+R=7l9Yme~K}YF~H0oȴI+fɞ! tϻ'm͌XEPXO WU̶t1%hoa@5gR~E;Ҥ&N#?HT<|Ws yUh`jB՟%1D yT3(6=t:"shWKޭԔ˥8Kr_ǡeh- !΋lo_}ŅyQɅ te=B+f`B}T)iKpnOe,ힴ\$Zq2 kܓDGҕ^ /.zsq A>n(ϒv)kGںbOjQ؍dYOX~$f}:Vf1<0IGSd#TkmͩNtdsǪTGYbT|#+&kl˯Zz|45^kOHS$PGW=+9SU|X=GI RaW2i}al 7! \`;?U(: pTnjUGkX2Rq M<,tskMܜmv.̀||+iXl{,!"J*C(WٚЗ]A2F<[u "0D`CJQZ&Ǟl\}tain!Ҏٱݝ)gUl̰v,C7640  z y B4ـy-D&2ebꛅ#F*W\=-"!U4\k/N^H{϶3rY-y-ePTd%sI/㉒&ʽOErEfL/ ?Y Cs,0?6%PTpBiZ,TjR$x9b=TFBvHQB{(Om7*4k{J6 ~H«ϾiQz@|^edĸQ.yMGoTKCƁʉGXo6fz }yJa`cZ` ~pUr.$,M;%0 )P.X]@li^vV-HZmt4&]id &ޝTޖ1aLXFvbէ{<~ҳX-/|u@`ѤmtչYJoͅf %ºN>&u1VWPH|L΢ɪ6eʍ u VI1 QLYϝ%X`@3g?EY >C4K7gra-AÔ򾀞ymʯ#]|'V7Ѓ;~8"ʣfk>m/E`/̛`6ڹ:ڌ(d[6"Aզ=̪^>f:>D2DZd[<5?[O4'L?jW3R*wr]0H|F&CX=$[xӪ3@FP]f)(e;S+YicM)s'X'k#nhY@cs'(Ax6Z[ PF'sfEoMSNK!/<&S%i3X@Fh0.h=rv8U2mT_K=2aFaWέJZ*Jp)qQI'6Jٌ7iJY*t34=&Y@kH\ͽ[d&0MLI{Ce)GjTޘe.M:uQ EW$nfZ,ѨB5 ,7N{7~"u ?rsgo-~f~Q=SkTJK/L|0𡛢<+^6ȓN k>do=t~Zv$\(<흮[VlJb9鶝:OE)F.+LШiǩ g"Sa%6Ѽ(VW sW>Vry>np!,;7*~ڀ*7#"BӞFax4,ި]eR=#Ƨ/n/xR@;݇<ýoՋxʨ2=2KUy7Isͅ{QN]-v̓HeOKwDy"|S,,K[ q5@oה;zX)YxykG+:z ,\/efe>w][ ;AjKA}JMhAV$_ ^G Jy=leYOu4Y}ҴVU@4Z}U+>F:P{'\q{Z*lmUT<12 mdY$k r1Deɛ+fTr@cg^ݧQPwKDjJW2.S:{zzhئBՂ%41[x01E׆~:6,i8Pߗ vp {<.55.IY,TλaeD;6p[ShFXyu B~#Cr)F+Q^ *VcE=ys=n>aj #i@#_@'F̌|eOklD5;hӘEHS6nIϋ,Yf9^ˀ;hMI| jwb Ra *Z! fovZH[ 4S0vj#KIĸ4~ LOi=+E^K> endobj 8 0 obj << /Type /ObjStm /N 20 /First 142 /Length 1225 /Filter /FlateDecode >> stream xڭW[OX~_$T n-]ă, ê9Nr@-HH㋔$-i/IYCNR+H+:T6X|gE@SXC"OZ;K!:Hܽvw}KQsVhz7oYy\m7}&r ?\@`((9񢂗%)̦@!XՋtul2Ik!Շ4=ɪR/ tiYV\ͦvN瀸Oic:;h ,oQܨa`2"hZ^pdry=- p%me1 BP-n/f R15z:柢Dx3eu)d,_VG6>bJˤgShܯ3| =%ZTO9c1'5a` z@|#cBvQLI!w!#V>(8 $jTfªWFN>Ȳ/K׺fIJ[uc9wi+D/sk-w=Ѡ9LWy6.mʠ@&?)JdQ(p4LbUPVaV\C]}VXbO]j+xN=,ah^)rad9e<>9,# vtm:umPYJɬf__'^|3*~}ƭ6b3elWvl>9w|t~@$4eGwC1e-l=jU;wF endstream endobj 43 0 obj << /Type /XRef /Index [0 44] /Size 44 /W [1 3 1] /Root 41 0 R /Info 42 0 R /ID [<01A84DEEC3BC5545350F3FFC3626F108> <01A84DEEC3BC5545350F3FFC3626F108>] /Length 164 /Filter /FlateDecode >> stream x?aewXkBt I(U"Q() J7eG+8@8RJr?YA7uFښSΔƓ=5TkPuDxкRTbJg]2hYJ(;TV>? endstream endobj startxref 578293 %%EOF paperwork-2.1.1/paperwork-backend/tests/model/simple_doc.pdf000066400000000000000000000230121417573700700242100ustar00rootroot00000000000000%PDF-1.5 %äüöß 2 0 obj <> stream xm 0EY 3I2`kpWw>vbQ3%K<%*&ߒ`Ցr}Ji|xD@ !Pέ&UX\|d=pWb}q/#AQ,J%a]V>DhHN )nH&;z7ND endstream endobj 3 0 obj 181 endobj 5 0 obj <> stream xzkxU=UzXl=,$+JQCI+~۴B,ْ`[BciZW] @~=DN3(y\$Db_/7+]!|S,eWɗ^!=r|I/!2AnA[%z L _Q'T1"Qay9</(܆2J L/7-+6r&AI<^ z 3?6KF9 __l)r辌cSv9B¿j@BMjodcگ}ΫvvZ[wM۷mݲq kUk׬.[_:fH_+j*Rɋi2юJ1ZUFD>ʋRwtP?*^\"tDsx s yN0[V ?/~üxWSX6JoFg"h# [-ªJ2[GP֟ہڶͳ )(őEcb*wVJ"-TiT%?&Ns35H!ElegضDsXoo#6Bڹ'O.AT ᣥєH$PdZD.W}=3L7gf dQمq{¢)2 Ѓ{:ŒkE,ȏFM~&Ϝ }[9aOrÑĆ8/y2zD$\b(9J^d+DԎ )$`~tPK|?GyQKcC8\U"gɹL1XZ\͎e92?S왑-))Mf] ǵ7ᔦzfV,];ezZZ)QPe7PXr " u:X] g/# Bisu fvjJ=,w%7p8F kHu]M~(j=ys:^PkkcX}puee*`pX,W;lf kf L 9 h#PaA:Kc@v>i9)@2J%`ۡ Wyٰ`no<ݩl$5':~Ugǁ|΁{[j7pVOc-;X}WYJR:ŘUp]XSX\-aYCeBaVo9m1hr@!)K:FKcmCjL lu6يqh+x=ۭ5o;xGiK}B1ld9*uK lF$h׉v:   [Luwy@ x |?W;G!+>;mv5nJS|[).!"ki HΚa66Y'N8NH:4W,DL7c2Z'fl'odMO^pO_e]y))pn)ggbJwRV;$,@YL"sŤNm6kt:FV;hTr O8'L;!ㄘTN^':u'䄼9'j3TC%B 7,%Y.4N|/)k)uSϿ)_m^f|ϽS<ƉlqaN'. X^Rr\NW(k5fKPXTnݐqC nnx /H\$#X)u5{ 7[UVQλu7pôn6MoIQ6ZV|Il0_ɁQ(4^AZvih~OV4+Yϗz-/|r k7y\H: Y5:^=<p2zazP1(1=+z!Aszp6Mz zHȷmReI^YqB)R$z ] C-h4"Vl iRj԰ЁCmmA,ZmzxPmUs61BV=6kbŸgJb\ǁ9xqp9 8XM.sOs|Gp&?Qp 69XŁ2⋵3Fa`q([T l<`/jiuP!GS y띱*5oS^1uQiCC]gg%XهVnʬՂ`4f,c5BaQk(r6@^q'M:\̹2FkVjUv<0UlvEխ1L\ ,km췑`:n <0`sM͂iO5=<Pyx じz=z򀋒a;X|)/Du76R٘? (!zN`_gpIqZhnbw=R](\ip'aɈQ31zL<;ɧA(8y3Wv+B)V(جt`wnɔN:{o]nx0o7Q7G[wfSdFX0JXHeѻ%fC1Vl$0KV`=p 4x?=mrAZ!^xx>Ni)JXh\?Q~̰38Oy`{r \?oǙlL<.|c==RmyAXe(#u>ݻ<`+ViZ8-bX,e^5WG6ӓɕ5dr"%]wc}΍G"pa`;"lJľClk>܏o>y*y'>DD&AlHX-f 5Ψ0c快Zӹ!stHЯMR+}Ŵ\˟přE=`J%=9ŨW#]͞{TBWŔ{Oq5b7[j=c(l(0qV֊#.8Lp@p,=o ʹX陕ݽ߹ƊL}㍫f?=Ȏ 5̍ƭd/ژ b58XMHlb̒4 7LEi$2GIddf~H_DDPjld4ï*kkxoIT43.lYVAL%srz`\{⩱=hjGz(>*~95TZjVT7\.gKQ> |bx!|*>2S{P4ߛSP<"s"3^?KƆ,rGO&~ _dds4}ect%ptlh?Mxzld2- Y3ь4x&56?1H Xf;Dtղ)at*?6L%PCx|;Ƣcc6MEc趱4:OF'ڦRd-F+htb,qO1G4@|Di<ÉˌV-|81ApԄ'ts&g\t(@Zrxflq>V$+ Z";J(gJKY6Hb_FL>(VJ23M>xj}OQu9HP!ď2&+r_L:H2 J&)+4n4W"Ͱ4Ϛ!eT+6r]I#emܥZ4s5h]/J4Hf[lr6a;CIS_V1 {Ehz-Ц/fu'0BJ"d7 >Aoinmd!~;"~ .^|6݅x9j#Үv%JOmB nWAķMi6IҏyTi8???g0ɱO?\,>}EくC`p₦o{#{{W,5s5sɹ]+鱟2y{l)0>} =y9vǽG>~5^|90w"sy]dpiv\2Ӌw.xߋ7{݋wv ؁}*#wNyNvcw0O8wIʽ d:ѧc4 .,[ dvowo{ҧz&MXmۍ#tb.>vU؝roG&h5 B<_pEg31#}ゑ14f5c=g {:łе"-HO{[$}{NGIS#pC@iLY4L)| DOX4qJS!6B!Bޟ&C"VBtZQGg endstream endobj 6 0 obj 7404 endobj 7 0 obj <> endobj 8 0 obj <> stream x]Mn0t DBH)I$Qi"cV陰ΕՍYtn d,t浢U B[/ Ce1σ͟M[ipGY[YDAQ fl}˟} b%[Q6 \cnQTz-0YlJ/}Rxg;=q!kh΁GOqBGd=D]+<ˈ}2OwΚƄPw|ؔ9ho; endstream endobj 9 0 obj <> endobj 10 0 obj <> endobj 11 0 obj <> endobj 1 0 obj <>/Contents 2 0 R>> endobj 4 0 obj <> endobj 12 0 obj <> endobj 13 0 obj < /Producer /CreationDate(D:20190902205629+02'00')>> endobj xref 0 14 0000000000 65535 f 0000008715 00000 n 0000000019 00000 n 0000000271 00000 n 0000008884 00000 n 0000000291 00000 n 0000007780 00000 n 0000007801 00000 n 0000007996 00000 n 0000008384 00000 n 0000008628 00000 n 0000008660 00000 n 0000008983 00000 n 0000009080 00000 n trailer < ] /DocChecksum /72B44A81E5EEF3D98178B73CE31BA672 >> startxref 9255 %%EOF paperwork-2.1.1/paperwork-backend/tests/model/test.docx000066400000000000000000000100731417573700700232400ustar00rootroot00000000000000PKf0R _rels/.relsMKA Cl+"Bo"3iA PǼymNAêiAq0Ѻ0jx=/`/W>J\*ބaIL41q!fORWeS.IC iW)D ^dVNx!1_Ļ97'd,Ӧhp]5nTQ?PK]#PKf0RdocProps/core.xmlRN0T*`%'*!(q365Ďe-{I mggRud DEy# W(riFCЬȹ`KpQ0ҎrSbsIP@V0*YN Rll;LZV:D8+Q(vnIC.QcUq@e~hr ̃ n9Gef$NILKrM)[o eA8n=#pt /W6?v1՞f/W>x C#]R29i0*[+:¶kyFb/} }zPK aPKf0Rword/_rels/document.xml.relsM 0"ަUnDp+16 (z{Z(}1/__]m,IQҦp(%INR\ vDnyP-2$֡^R,}ÝT' O&Uʀ7m]k=nHA>.?|m ?@IwPK/0PKf0Rword/settings.xmlEK0 D"SBkRbG +73z+E"#f 0l7%>jn)Ȃ3ReW.)hf'.C.ܣHhέl #AW/?Lm#iiQOrTε m]/PKe"PKf0Rword/fontTable.xmlPAN0 wz[gXב$N+!ȡٙY?=6*9R i_:T}r(G'YWwˡl]jQʁ!y 1pHq h! 59JBCěw@['NE!Ug2 D^={'HfLGvkqkD<OWǐ[ ɮ`&}d[=O{>Ο:?xPKUPKf0Rword/document.xmlTێ0}W 'j%Q*R0ƀDzMXHUU`9g۷wR$'f,Ul) WM~6(P@n+d%A [2IJrjBVdu) S9c֠X F痦c.jiz yNl?韤KT{06@~Rp5d B_\?|nd7 }E9X{qzgϗ/-lak@'6I/V01hwùTv6W/gw|aHkR t- xm!YT_HiX8 R)9 t.Q꾓j-=}(6ހ [qڄ s,AiPKI zPKf0Rword/styles.xmlUmO0_{IAjJP7Z8s()}i|~:(Utcqx0dRcv28y(q,VHçJ!Eivǔ.:05rJtE4.ΤHK 'q RVRΐAjL.T]2} ]e\ςs_dDTin8^3^bk~L-G@cq%踼 : -.4! c134x|$!RM%ރq:͛AQoCr7\?$eƼr0ĸ:nc)3LXgTlʲ"C,h¶$ %vpIm}ejq]Py-~!;Ak>s ̾>4>OgwxhgkIUfʺ9Ǡ0ܣh(^<6{=ƢRۿfiM7-lUtˡ6XIU0ymfD9D>ؕl]H}eo\ζI'`L54Pe?+V<,Yx~beЋ/J6ekw뽦{_PK-e PKf0R[Content_Types].xmlT9O0+"(qa@%1B0#c$BUh ﴜ|Tm9$͍.Sy^d+ >] ҄`)|f,hT)2j%F! L[ؼ ׺'z/JYJi^.snev|#?ZzO@,#^-Cbn'$3S@c8O0|ýșcA-@) is۬}qbS4Gѝ)_702l-T(ZyQ\i=&I=|a_Fǻ_N>PKBS^PKf0R#= _rels/.relsPKf0R]#docProps/app.xmlPKf0R asdocProps/core.xmlPKf0R/0word/_rels/document.xml.relsPKf0Re""word/settings.xmlPKf0RUword/fontTable.xmlPKf0RI zcword/document.xmlPKf0R-e  word/styles.xmlPKf0RBS^U [Content_Types].xmlPK < paperwork-2.1.1/paperwork-backend/tests/model/test_img.png000066400000000000000000000107061417573700700237260ustar00rootroot00000000000000PNG  IHDRdÆ bKGD pHYs.#.#x?vtIME vtEXtCommentCreated with GIMPW.IDATx{LSgǿU,E`Vo+dTQScj:ܘHy&Bp@ P"rSN[V~۾@O=PI9ym H0 0,a0 aX að@0 a^D"t0H$TWWDF㴊wE`ʕH$Xa\P Y|TUU ^ƭ/3ӳG|tE! PTy D"8>غu+juXɓ@^^@&aܸq8GOFqq1+`ڴixwo؜WAAƏo/'0uVNJT*RL0AAA?/8bb鱐@UUU^J(%%<<#Vz=EDDc6ץ+9/׉' ڵkkNq@PLL jT[[K |Ҧ7''… h4Vھ}`^8x F!NGZ-eeeT*%tUn؊+ݾ}52ܹsT[[K? $JI:Tw[bat (jnn8M(''Ǧ7!!ٳg;]QF{>}О={:U Z<<<ϯPݟ8}nZiL&3φ!C7otZ]Q ///0;BTVVbɒ%4h5>>>3gtKyC|hq?3`{^ja޽{G [liwh"uhޙe Zvռ=*j^VVh}u8HsSj*#??EEEχLiӦO?˔ѧO|w4i1cr<*SPP#G ;;˖-ԩS'O… 6l J!k9Tw[bHؼaA:ð@0,a0 aX a0,:T[q{WP@" ##AX 8 =l'X ÇQUUNddffӧOǂ 0zhJII"a…x7-sÇHKKCrr2 \`(J̙31c`z{lm<}||0qDbĈul 55YYYCRR~mQqۦ] 8~8qx{{Cpp(D=i`RR]hRH& ?uTjZ?`޽{ ֖ե]N{mz)qۦ&"ŋ_.e=ڑ@P\\ݹsbcc ͝;믿7o^ڿ?ɓ'^?[%Kʕ+d0.\@X\mGHܽ{Wo>H[پKNúuP^^^K.O?m+˶ݻw>deet(//ݻq&CG|===i&ڵkq:<>y(]潻i&@eee.vV5@`e~"T*-Ҋe%=..NJ6#m,`"… ]~%ݥy4h~ )) d9s&@[W-uoT*&O# @p =H:4w^̙3s2 3g΄\.Rtg/r1 ׮]ȑ#!JqVqA#ܽ{PTtӧtR,T;<],"BII N8B៽0;Hg'z 0,a0 aX $,, e6Xi,^U/jaƂrZ| 0 JBRDuu5baSWW#Gbxys[rssm۶+qIdddd2ƍ???^FBtT*`޼yp8 %&&Z-vAt۷o1ؖիW>e@iii6+R^vwϵuwtħ7%%<<<8pnm۷h4R]]ϵ}JKK ^Z0_ܹs[v@ӔQ}}=FjTRRB۷oN鍉!ZMzjkkIPϵ-&̝;hCxSSyFHw@ǎ+`@-$ٳ]EVͺsrr)voވxPPtիyF۽azEW#WvȐ!7o\#>d29ba`֬Y,&M777ڵ/_Fnn.6olԖi^k.~VT*Ehh(֭[R7n_K:tqͪ5*b䟤-nbtt4rssqeT\.ﶊvaGѣG#$$6l{_/_ޭ[`;ÓٳDh4VrO/i;0az{dd$ `Y@^旡b)StkŝkzE3|zHbb"lق 455A qa_^[c; 8r_455* ;wVouO/ӅtJo|?`5g/hfѢET[[t޼<@ X ޼(//GRR233q=!$$'N03vZ@*ė_UV(**B~~>0eL6 e.Č3?[y `4Sr3=ϳ644رc8tKbUVVZT]vͮwl0c5j"##qeÿ ÿ a0,a0 aX ah dARIENDB`paperwork-2.1.1/paperwork-backend/tests/model/test_password.pdf000066400000000000000000000214351417573700700250020ustar00rootroot00000000000000%PDF-1.5 %äüöß 2 0 obj <> stream {tJCP$ N$eFY/JC$_4Jw.Ƀ%F4s$YpqYYnlj8ȦڥiGAYhs|_<BWߑݏѷ endstream endobj 3 0 obj 146 endobj 5 0 obj <> stream `fy INWNvON5eᡪ\& a$?r>dbRL!l0ccO!v7@P4;~}e"/s>TK\3 ރ,kW0G?m5\?帱fL5=!{UÎ^#b%{/r\zc5[/KuYW!8tcMb[dq[-K%aJL MS3Լa<%LmAt5 (qy|%.lo Xw*a!IO,]}I@ l#<"ķcf,',vSP>jjݒjcᘇkpv4f]o`C>_Ή6 ރB7 "ςّdtgStz&[;qre\\3̿b O! Z6 9@z9GtÀCLF&C<, .mz4`3Dz(+M,ߓG/}VU# ?oXXB4cqDqKCoSY8ݤZo'ƥZP!MJ,FI+%u1e!d߶{x.B7 e.KCA BD9K"9e1e0`H;kZOEn;J[)DVń* A.w$LfƮ<RYX5myW$,^/lo6^k91d!g1x@Zd6Y V&WEQ ' Hyeb7~=,Ryq]K1te*2_zQBtBWM"++螰#5`5h #[|ffӻ31/u+ĹL qO=+@$]UHp x;D)U?)wDX] l`IDW?T^{.vFGO>|S`>ot284y6,7N)埿x4?0u#)r[*vE=MixhL7ܕ&w6jZuBt/7!xXݓN@YRAӹo屮$Gm\B9Od0K6vAH_n$޼MulM<#~PAL Zbǚ2yyH{Dߵ{kuMۏ0y' }I8O~cY NX@Ct.Ռ=g ! LKJW5G/&s y3ߥGE !M00]UE0͸6=bԕc+P*B+@c>pG(bgM$AtR<.k?w]X}ȋuIeISm80x`\k8rKTih4S}B9Ӽ~so4P'i ?t`R-A 5͐\ hwf&Tݼjw=pp9X/rD*\yx&tٞ}rvmc3x ˳3cд[4q贖Eyf=DjK:yiO9>?eʯse+[n@I'llǓuǹ3LK=mh-4>'#붽b${uқ{Sz4UdT-YIՇE}M;m}`IxsSf7E޲ `]>(2[WGǗmdz½w_hI+QuGDRjBg,Ę ݀% ~w|5D8Q&"M9F eRU_8,^N*p=f6U.ǰ 5x9x2uR]7@#g { kUF v`ڬDQY|1<esب+866ݻPSWt߷wOJOE{^uijP[Fg/2goLF a`eQr^.k2݌tK:S^̼l.8{|y6P嵬b%}y#xO%O xZ 0}g 0Cok;-C]*oX 8eRWW>I v׼uO/o='-N '9 {.l?UVl6HW#&AS xW.l^rwؤhm!I 7ؗ8e*"zùEIjhjo LدL2H aW:dAɢ!gݏ~m[7y 10M]C/0-μ, Tm2 yq[s3u4~^=&.ĺm_.HP1ak2x7a7ӥuo瀰xfs{GhII,DJhxZ֕ͧj|;6;&RL>KMC?ALK'0CTՋo}tcҫ|5>ƭ-Qww缇(nҐ:Ԗ Sn.Zֆ߁y} մSʅ' 7:;h\4lLa>LH dJѰڝ0ށiE|]6b֠p N%[`h?~voN`Q"݊w&IcKaJ)e ۷nsY%0\/`rG,sDW8}=灠f@Z+G٢x66 G݌ <k\Su!PO9` J8lSgźũR[WAG>pτjxѸx N3){4dqFD{+'(OެmkM.(UX8ޚ]jPJHmDѣS]SB=ݳ΃8.'y_p9=xG1Pm;;EXwfp5MC&"k p~n)=Q^| EJD'Qv1 Qh'Z+ʢ+ yt OxqZRtݒکQU, 4KC(k|~bx{jC[^wPp2??yA>%D3o){3Z}J2VB*lqsˢg6d,ޟBn(O kT6{Lu0jaBQQKْÌV9 x!Dˆ*V&(gOԶ舮ά*"޹!'w`>.E7قEU1%3>d _Y~&+h sᱭ|KZiCU>1ޤӚĮPLҺ]B̌KH6b۔WI:{ga} ~m3R5.Ă7,M|9%4oO0:AB:˶U?Rd=}PW d7癭}N{~}Tۈj%ƙǭY}U_ΝD"~J4RjJιo8q%`,ڐ(2D{k0 fy۷xZ8Ev!ֵL B8>n/~\O4_ydD[pXM}ug+\;Wr:rZ??%{D֩X!/OHv0g?EHU]_Kv1BhN_7Y!?? g[GԚl=nfHÍҀ# D!Dž!xZf8o5QLE!BY|E+k-#GueQ,O^GjZ-2+1>IH=Y03؁" 1?Alb=8%e\Zz}a0w$ c46Q)NOp5dzRgr]e<5U'sA|=ØOA$5&N$ܮXwϤY|ţSX ͳoyw&VjXm,8i|-Q4S 9ua8(5K Zˋ_4jbF8Il9z^6BX;9B@-p(i@#P*R@Jy?7HN,,e쳭o=/ U"C]FM݁q(ߵƒ@L nBkAyrdj9< ׏ˑ\N{++\g ӀeXZx'n4_$2&`"1V9)}qWQ8Ɛ^xy Uמ$@T5y)6"wPGkƻݦ>KP8Q&j&7ÌWzw`l5ء)C'%ZԽRp|x1-aFfN⍚PAF<kv.c"Nϭ7MnVXN S0IVZ `o*xRLzL$s-\UpG:V?;77&X7PGxK`Wdw{XY?@W?~__h72Ѥ\?oIPdwrl0TgҐ IT́A1/ l!)46 Bq4rw2 ߏm)6{-VOpu$ghaMJJ zi*'6 .Y9Stit>«m%x"<? endstream endobj 6 0 obj 6557 endobj 7 0 obj <> endobj 8 0 obj <> stream ]­UZ OiѼ6z O]"+E)h RUQ&QJcG$E6yr/(Ja2=SÒ[-X(d,k"̇/ejpB!y9}kHCd:eN 96۪@5|o.E 1%" -p^C5#c> endobj 10 0 obj <> endobj 11 0 obj <> endobj 1 0 obj <>/Contents 2 0 R>> endobj 4 0 obj <> endobj 12 0 obj <> endobj 13 0 obj < /Producer<0F4AD7BD3225496D4174E8D76D12897CC877CEED24DC9DB568A5637A671CE5E2> /CreationDate(}y:s5ڀ\\d"!)>> endobj 14 0 obj <> endobj xref 0 15 0000000000 65535 f 0000007789 00000 n 0000000019 00000 n 0000000236 00000 n 0000007958 00000 n 0000000256 00000 n 0000006898 00000 n 0000006919 00000 n 0000007114 00000 n 0000007478 00000 n 0000007702 00000 n 0000007734 00000 n 0000008057 00000 n 0000008154 00000 n 0000008330 00000 n trailer < <4CC2DA594C5DCB199E0D108A41E1AA2B> ] /DocChecksum /4234D31F6DCF9FDC719B74544277637C >> startxref 8470 %%EOF paperwork-2.1.1/paperwork-backend/tests/model/tests_all.py000066400000000000000000000776151417573700700237650ustar00rootroot00000000000000import itertools import os import shutil import tempfile import unittest import openpaperwork_core import openpaperwork_core.fs class TestAll(unittest.TestCase): def setUp(self): self.int_generator = itertools.count() self.pdf = openpaperwork_core.fs.CommonFsPluginBase.fs_safe( os.path.join( os.path.dirname(os.path.abspath(__file__)), "doc.pdf" ) ) self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.config.fake") self.core.load("paperwork_backend.model.extra_text") self.core.load("paperwork_backend.model.hocr") self.core.load("paperwork_backend.model.img") self.core.load("paperwork_backend.model.img_overlay") self.core.load("paperwork_backend.model.pdf") self.core.load("paperwork_backend.model.workdir") self.core.init() self.work_dir = tempfile.mkdtemp(prefix="paperwork_tests_all_") self.work_dir_url = self.core.call_success("fs_safe", self.work_dir) self.core.call_all("config_put", "workdir", self.work_dir_url) self.doc_pdf = self.core.call_success( "fs_join", self.work_dir_url, "20200525_1241_05" ) self._copy_pdf(self.doc_pdf + "/doc.pdf") self._make_file(self.doc_pdf + "/paper.2.edited.jpg") self._make_file(self.doc_pdf + "/paper.2.words") self._make_file(self.doc_pdf + "/extra.txt") self.doc_b = self.core.call_success( "fs_join", self.work_dir_url, "19851212_1233_00" ) self._make_file(self.doc_b + "/paper.1.jpg") self._make_file(self.doc_b + "/paper.1.words") self._make_file(self.doc_b + "/paper.2.jpg") self._make_file(self.doc_b + "/paper.2.words") self._make_file(self.doc_b + "/paper.3.jpg") self._make_file(self.doc_b + "/paper.3.words") self._make_file(self.doc_b + "/paper.3.edited.jgg") self._make_file(self.doc_b + "/paper.4.jpg") self._make_file(self.doc_b + "/paper.4.words") self.doc_c = self.core.call_success( "fs_join", self.work_dir_url, "19851224_1233_00" ) self._make_file(self.doc_c + "/paper.1.jpg") self._make_file(self.doc_c + "/paper.1.words") self._make_file(self.doc_c + "/paper.2.jpg") self._make_file(self.doc_c + "/paper.2.words") self._make_file(self.doc_b + "/paper.2.edited.jgg") self._make_file(self.doc_c + "/paper.3.jpg") self._make_file(self.doc_c + "/paper.3.words") def tearDown(self): shutil.rmtree(self.work_dir) def _copy_pdf(self, out_pdf_url): dirname = self.core.call_success("fs_dirname", out_pdf_url) self.core.call_success("fs_mkdir_p", dirname) self.core.call_success("fs_copy", self.pdf, out_pdf_url) def _make_file(self, out_file_url): dirname = self.core.call_success("fs_dirname", out_file_url) self.core.call_success("fs_mkdir_p", dirname) with self.core.call_success("fs_open", out_file_url, 'w') as fd: fd.write("Generated content {}\n".format(next(self.int_generator))) def test_basic_nb_pages(self): nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 4) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_b) self.assertEqual(nb, 4) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_c) self.assertEqual(nb, 3) def _get_page_hash(self, doc_url, page_idx): return self.core.call_success( "page_get_hash_by_url", doc_url, page_idx ) def test_img_page_delete(self): nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_b) self.assertEqual(nb, 4) hashes = [ self._get_page_hash(self.doc_b, page_idx) for page_idx in range(0, 4) ] self.core.call_all("page_delete_by_url", self.doc_b, 2) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_b) self.assertEqual(nb, 3) new_hashes = [ self._get_page_hash(self.doc_b, page_idx) for page_idx in range(0, 3) ] self.assertEqual(hashes[0], new_hashes[0]) self.assertEqual(hashes[1], new_hashes[1]) self.assertEqual(hashes[3], new_hashes[2]) def test_img_page_move_internal(self): nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_b) self.assertEqual(nb, 4) hashes = [ self._get_page_hash(self.doc_b, page_idx) for page_idx in range(0, 4) ] # 0 becomes 2 # --> 1 becomes 0 # --> 2 becomes 1 self.core.call_all( "page_move_by_url", self.doc_b, 0, self.doc_b, 2 ) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_b) self.assertEqual(nb, 4) new_hashes = [ self._get_page_hash(self.doc_b, page_idx) for page_idx in range(0, 4) ] self.assertEqual(hashes[0], new_hashes[2]) self.assertEqual(hashes[1], new_hashes[0]) self.assertEqual(hashes[2], new_hashes[1]) self.assertEqual(hashes[3], new_hashes[3]) def test_img_page_move_internal_reversed(self): nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_b) self.assertEqual(nb, 4) hashes = [ self._get_page_hash(self.doc_b, page_idx) for page_idx in range(0, 4) ] # 2 becomes 0 # --> 0 becomes 1 # --> 1 becomes 2 self.core.call_all( "page_move_by_url", self.doc_b, 2, self.doc_b, 0 ) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_b) self.assertEqual(nb, 4) new_hashes = [ self._get_page_hash(self.doc_b, page_idx) for page_idx in range(0, 4) ] self.assertEqual(hashes[0], new_hashes[1]) self.assertEqual(hashes[1], new_hashes[2]) self.assertEqual(hashes[2], new_hashes[0]) self.assertEqual(hashes[3], new_hashes[3]) def test_img_page_move_external(self): nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_b) self.assertEqual(nb, 4) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_c) self.assertEqual(nb, 3) doc_b_hashes = [ self._get_page_hash(self.doc_b, page_idx) for page_idx in range(0, 4) ] doc_c_hashes = [ self._get_page_hash(self.doc_c, page_idx) for page_idx in range(0, 3) ] # doc_c p1 becomes doc_b p2 # --> doc_c p2 becomes doc_c p1 # --> doc_b p2 becomes doc_b p3 # --> doc_b p3 becomes doc_b p4 self.core.call_all( "page_move_by_url", self.doc_c, 1, self.doc_b, 2 ) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_b) self.assertEqual(nb, 5) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_c) self.assertEqual(nb, 2) new_doc_b_hashes = [ self._get_page_hash(self.doc_b, page_idx) for page_idx in range(0, 5) ] new_doc_c_hashes = [ self._get_page_hash(self.doc_c, page_idx) for page_idx in range(0, 2) ] self.assertEqual(doc_b_hashes[0], new_doc_b_hashes[0]) self.assertEqual(doc_b_hashes[1], new_doc_b_hashes[1]) self.assertEqual(doc_b_hashes[2], new_doc_b_hashes[3]) self.assertEqual(doc_b_hashes[3], new_doc_b_hashes[4]) self.assertEqual(doc_c_hashes[0], new_doc_c_hashes[0]) self.assertEqual(doc_c_hashes[1], new_doc_b_hashes[2]) self.assertEqual(doc_c_hashes[2], new_doc_c_hashes[1]) def test_pdf_hashes(self): nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 4) hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 4) ] # just make sure all the hashes are different from one another for (e, h) in enumerate(hashes): for i in hashes[e + 1:]: self.assertNotEqual(h, i) def test_pdf_page_delete(self): nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 4) hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 4) ] self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 0 ).endswith("20200525_1241_05/doc.pdf#page=1")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 1 ).endswith("20200525_1241_05/paper.2.edited.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 2 ).endswith("20200525_1241_05/doc.pdf#page=3")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 3 ).endswith("20200525_1241_05/doc.pdf#page=4")) self.core.call_all("page_delete_by_url", self.doc_pdf, 2) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 3) new_hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 3) ] self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 0 ).endswith("20200525_1241_05/doc.pdf#page=1")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 1 ).endswith("20200525_1241_05/paper.2.edited.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 2 ).endswith("20200525_1241_05/doc.pdf#page=4")) self.assertEqual(hashes[0], new_hashes[0]) self.assertEqual(hashes[1], new_hashes[1]) self.assertEqual(hashes[3], new_hashes[2]) def test_pdf_page_move_internal(self): nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 4) hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 4) ] # 0 becomes 2 # --> 1 becomes 0 # --> 2 becomes 1 self.core.call_all( "page_move_by_url", self.doc_pdf, 0, self.doc_pdf, 2 ) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 4) new_hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 4) ] self.assertEqual(hashes[0], new_hashes[2]) self.assertEqual(hashes[1], new_hashes[0]) self.assertEqual(hashes[2], new_hashes[1]) self.assertEqual(hashes[3], new_hashes[3]) def test_pdf_page_move_internal_reversed(self): nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 4) hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 4) ] # 2 becomes 0 # --> 0 becomes 1 # --> 1 becomes 2 self.core.call_all( "page_move_by_url", self.doc_pdf, 2, self.doc_pdf, 0 ) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 4) new_hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 4) ] self.assertEqual(hashes[0], new_hashes[1]) self.assertEqual(hashes[1], new_hashes[2]) self.assertEqual(hashes[2], new_hashes[0]) self.assertEqual(hashes[3], new_hashes[3]) def test_img_to_pdf_page_move_then_delete(self): nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 4) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_c) self.assertEqual(nb, 3) doc_pdf_hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 4) ] doc_c_hashes = [ self._get_page_hash(self.doc_c, page_idx) for page_idx in range(0, 3) ] self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 0 ).endswith("20200525_1241_05/doc.pdf#page=1")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 1 ).endswith("20200525_1241_05/paper.2.edited.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 2 ).endswith("20200525_1241_05/doc.pdf#page=3")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 3 ).endswith("20200525_1241_05/doc.pdf#page=4")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_c, 0 ).endswith("19851224_1233_00/paper.1.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_c, 1 ).endswith("19851224_1233_00/paper.2.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_c, 2 ).endswith("19851224_1233_00/paper.3.jpg")) # doc_c p1 becomes doc_pdf p2 # --> doc_c p2 becomes doc_c p1 # --> doc_pdf p2 becomes doc_pdf p3 # --> doc_pdf p3 becomes doc_pdf p4 self.core.call_all( "page_move_by_url", self.doc_c, 1, self.doc_pdf, 2 ) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 5) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_c) self.assertEqual(nb, 2) new_doc_pdf_hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 5) ] new_doc_c_hashes = [ self._get_page_hash(self.doc_c, page_idx) for page_idx in range(0, 2) ] self.assertEqual(doc_pdf_hashes[0], new_doc_pdf_hashes[0]) self.assertEqual(doc_pdf_hashes[1], new_doc_pdf_hashes[1]) self.assertEqual(doc_pdf_hashes[2], new_doc_pdf_hashes[3]) self.assertEqual(doc_pdf_hashes[3], new_doc_pdf_hashes[4]) self.assertEqual(doc_c_hashes[0], new_doc_c_hashes[0]) self.assertEqual(doc_c_hashes[1], new_doc_pdf_hashes[2]) self.assertEqual(doc_c_hashes[2], new_doc_c_hashes[1]) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 0 ).endswith("20200525_1241_05/doc.pdf#page=1")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 1 ).endswith("20200525_1241_05/paper.2.edited.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 2 ).endswith("20200525_1241_05/paper.3.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 3 ).endswith("20200525_1241_05/doc.pdf#page=3")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 4 ).endswith("20200525_1241_05/doc.pdf#page=4")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_c, 0 ).endswith("19851224_1233_00/paper.1.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_c, 1 ).endswith("19851224_1233_00/paper.2.jpg")) # we delete the added img page self.core.call_all("page_delete_by_url", self.doc_pdf, 2) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 4) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_c) self.assertEqual(nb, 2) new_doc_pdf_hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 4) ] new_doc_c_hashes = [ self._get_page_hash(self.doc_c, page_idx) for page_idx in range(0, 2) ] self.assertEqual(doc_pdf_hashes[0], new_doc_pdf_hashes[0]) self.assertEqual(doc_pdf_hashes[1], new_doc_pdf_hashes[1]) self.assertEqual(doc_pdf_hashes[2], new_doc_pdf_hashes[2]) self.assertEqual(doc_pdf_hashes[3], new_doc_pdf_hashes[3]) self.assertEqual(doc_c_hashes[0], new_doc_c_hashes[0]) self.assertEqual(doc_c_hashes[2], new_doc_c_hashes[1]) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 0 ).endswith("20200525_1241_05/doc.pdf#page=1")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 1 ).endswith("20200525_1241_05/paper.2.edited.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 2 ).endswith("20200525_1241_05/doc.pdf#page=3")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 3 ).endswith("20200525_1241_05/doc.pdf#page=4")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_c, 0 ).endswith("19851224_1233_00/paper.1.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_c, 1 ).endswith("19851224_1233_00/paper.2.jpg")) def test_pdf_to_img_page_move(self): nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 4) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_c) self.assertEqual(nb, 3) doc_pdf_hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 4) ] doc_c_hashes = [ self._get_page_hash(self.doc_c, page_idx) for page_idx in range(0, 3) ] # doc_pdf p3 becomes doc_c p2 # --> doc_c p2 becomes doc_c p3 # --> doc_c p2 becomes doc_c p3 # --> doc_pdf p4 becomes doc_pdf p3 self.core.call_all( "page_move_by_url", self.doc_pdf, 2, self.doc_c, 1 ) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 3) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_c) self.assertEqual(nb, 4) new_doc_pdf_hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 3) ] new_doc_c_hashes = [ self._get_page_hash(self.doc_c, page_idx) for page_idx in range(0, 4) ] # in that case, an image page has been generated from the PDF page # --> hash of the new image page won't match the PDF hash self.assertEqual(doc_pdf_hashes[0], new_doc_pdf_hashes[0]) self.assertEqual(doc_pdf_hashes[1], new_doc_pdf_hashes[1]) self.assertNotEqual(doc_pdf_hashes[2], new_doc_pdf_hashes[2]) self.assertEqual(doc_pdf_hashes[3], new_doc_pdf_hashes[2]) self.assertEqual(doc_c_hashes[0], new_doc_c_hashes[0]) self.assertNotEqual(doc_c_hashes[1], new_doc_c_hashes[1]) self.assertEqual(doc_c_hashes[1], new_doc_c_hashes[2]) self.assertEqual(doc_c_hashes[2], new_doc_c_hashes[3]) def test_img_to_pdf_complex(self): nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 4) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_c) self.assertEqual(nb, 3) doc_pdf_hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 4) ] doc_c_hashes = [ self._get_page_hash(self.doc_c, page_idx) for page_idx in range(0, 3) ] self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 0 ).endswith("20200525_1241_05/doc.pdf#page=1")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 1 ).endswith("20200525_1241_05/paper.2.edited.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 2 ).endswith("20200525_1241_05/doc.pdf#page=3")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 3 ).endswith("20200525_1241_05/doc.pdf#page=4")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_c, 0 ).endswith("19851224_1233_00/paper.1.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_c, 1 ).endswith("19851224_1233_00/paper.2.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_c, 2 ).endswith("19851224_1233_00/paper.3.jpg")) # doc_c p1 becomes doc_pdf p5 # --> doc_c p2 becomes doc_c p1 self.core.call_all( "page_move_by_url", self.doc_c, 1, self.doc_pdf, 4 ) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 5) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_c) self.assertEqual(nb, 2) new_doc_pdf_hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 5) ] new_doc_c_hashes = [ self._get_page_hash(self.doc_c, page_idx) for page_idx in range(0, 2) ] self.assertEqual(doc_pdf_hashes[0], new_doc_pdf_hashes[0]) self.assertEqual(doc_pdf_hashes[1], new_doc_pdf_hashes[1]) self.assertEqual(doc_pdf_hashes[2], new_doc_pdf_hashes[2]) self.assertEqual(doc_pdf_hashes[3], new_doc_pdf_hashes[3]) self.assertEqual(doc_c_hashes[0], new_doc_c_hashes[0]) self.assertEqual(doc_c_hashes[1], new_doc_pdf_hashes[4]) self.assertEqual(doc_c_hashes[2], new_doc_c_hashes[1]) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 0 ).endswith("20200525_1241_05/doc.pdf#page=1")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 1 ).endswith("20200525_1241_05/paper.2.edited.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 2 ).endswith("20200525_1241_05/doc.pdf#page=3")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 3 ).endswith("20200525_1241_05/doc.pdf#page=4")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 4 ).endswith("20200525_1241_05/paper.5.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_c, 0 ).endswith("19851224_1233_00/paper.1.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_c, 1 ).endswith("19851224_1233_00/paper.2.jpg")) # doc_pdf p5 (previously doc_c p2) becomes doc_pdf p2 # --> pdf_pdf pages shift self.core.call_all( "page_move_by_url", self.doc_pdf, 4, self.doc_pdf, 1 ) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 5) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_c) self.assertEqual(nb, 2) new_doc_pdf_hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 5) ] new_doc_c_hashes = [ self._get_page_hash(self.doc_c, page_idx) for page_idx in range(0, 2) ] self.assertEqual(doc_pdf_hashes[0], new_doc_pdf_hashes[0]) self.assertEqual(doc_c_hashes[1], new_doc_pdf_hashes[1]) self.assertEqual(doc_pdf_hashes[1], new_doc_pdf_hashes[2]) self.assertEqual(doc_pdf_hashes[2], new_doc_pdf_hashes[3]) self.assertEqual(doc_pdf_hashes[3], new_doc_pdf_hashes[4]) self.assertEqual(doc_c_hashes[0], new_doc_c_hashes[0]) self.assertEqual(doc_c_hashes[2], new_doc_c_hashes[1]) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 0 ).endswith("20200525_1241_05/doc.pdf#page=1")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 1 ).endswith("20200525_1241_05/paper.2.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 2 ).endswith("20200525_1241_05/paper.3.edited.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 3 ).endswith("20200525_1241_05/doc.pdf#page=3")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 4 ).endswith("20200525_1241_05/doc.pdf#page=4")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_c, 0 ).endswith("19851224_1233_00/paper.1.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_c, 1 ).endswith("19851224_1233_00/paper.2.jpg")) # kill doc_c by remove its last 2 pages self.core.call_all( "page_move_by_url", self.doc_c, 0, self.doc_pdf, 5 ) self.core.call_all( "page_move_by_url", self.doc_c, 0, self.doc_pdf, 6 ) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 7) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_c) self.assertIsNone(nb) new_doc_pdf_hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 7) ] self.assertEqual(doc_pdf_hashes[0], new_doc_pdf_hashes[0]) self.assertEqual(doc_c_hashes[1], new_doc_pdf_hashes[1]) self.assertEqual(doc_pdf_hashes[1], new_doc_pdf_hashes[2]) self.assertEqual(doc_pdf_hashes[2], new_doc_pdf_hashes[3]) self.assertEqual(doc_pdf_hashes[3], new_doc_pdf_hashes[4]) self.assertEqual(doc_c_hashes[0], new_doc_pdf_hashes[5]) self.assertEqual(doc_c_hashes[2], new_doc_pdf_hashes[6]) # remove a page in the PDF, just for fun self.core.call_all("page_delete_by_url", self.doc_pdf, 2) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 6) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_c) self.assertIsNone(nb) new_doc_pdf_hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 6) ] self.assertEqual(doc_pdf_hashes[0], new_doc_pdf_hashes[0]) self.assertEqual(doc_c_hashes[1], new_doc_pdf_hashes[1]) self.assertEqual(doc_pdf_hashes[2], new_doc_pdf_hashes[2]) self.assertEqual(doc_pdf_hashes[3], new_doc_pdf_hashes[3]) self.assertEqual(doc_c_hashes[0], new_doc_pdf_hashes[4]) self.assertEqual(doc_c_hashes[2], new_doc_pdf_hashes[5]) # remove another page in the PDF, just for fun self.core.call_all("page_delete_by_url", self.doc_pdf, 1) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_pdf) self.assertEqual(nb, 5) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_c) self.assertIsNone(nb) new_doc_pdf_hashes = [ self._get_page_hash(self.doc_pdf, page_idx) for page_idx in range(0, 5) ] self.assertEqual(doc_pdf_hashes[0], new_doc_pdf_hashes[0]) self.assertEqual(doc_pdf_hashes[2], new_doc_pdf_hashes[1]) self.assertEqual(doc_pdf_hashes[3], new_doc_pdf_hashes[2]) self.assertEqual(doc_c_hashes[0], new_doc_pdf_hashes[3]) self.assertEqual(doc_c_hashes[2], new_doc_pdf_hashes[4]) def test_img_page_move_new_doc(self): self.new_doc = self.core.call_success( "fs_join", self.work_dir_url, "19871224_1233_00" ) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_b) self.assertEqual(nb, 4) doc_b_hashes = [ self._get_page_hash(self.doc_b, page_idx) for page_idx in range(0, 4) ] self.core.call_all( "page_move_by_url", self.doc_b, 1, self.new_doc, 0 ) nb = self.core.call_success("doc_get_nb_pages_by_url", self.doc_b) self.assertEqual(nb, 3) nb = self.core.call_success("doc_get_nb_pages_by_url", self.new_doc) self.assertEqual(nb, 1) new_doc_b_hashes = [ self._get_page_hash(self.doc_b, page_idx) for page_idx in range(0, 3) ] new_doc_hash = self._get_page_hash(self.new_doc, 0) self.assertEqual(doc_b_hashes[0], new_doc_b_hashes[0]) self.assertEqual(doc_b_hashes[2], new_doc_b_hashes[1]) self.assertEqual(doc_b_hashes[3], new_doc_b_hashes[2]) self.assertEqual(doc_b_hashes[1], new_doc_hash) def test_img_in_pdf_move(self): # Bug report #245 on openpaper.work # JPEG goes at the end of the PDF self.core.call_all( "page_move_by_url", self.doc_b, 1, self.doc_pdf, 4 ) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 0 ).endswith("20200525_1241_05/doc.pdf#page=1")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 1 ).endswith("20200525_1241_05/paper.2.edited.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 2 ).endswith("20200525_1241_05/doc.pdf#page=3")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 3 ).endswith("20200525_1241_05/doc.pdf#page=4")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 4 ).endswith("20200525_1241_05/paper.5.jpg")) # Move the JPEG around self.core.call_all( "page_move_by_url", self.doc_pdf, 4, self.doc_pdf, 3 ) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 0 ).endswith("20200525_1241_05/doc.pdf#page=1")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 1 ).endswith("20200525_1241_05/paper.2.edited.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 2 ).endswith("20200525_1241_05/doc.pdf#page=3")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 3 ).endswith("20200525_1241_05/paper.4.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 4 ).endswith("20200525_1241_05/doc.pdf#page=4")) # Move the JPEG around again self.core.call_all( "page_move_by_url", self.doc_pdf, 3, self.doc_pdf, 1 ) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 0 ).endswith("20200525_1241_05/doc.pdf#page=1")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 1 ).endswith("20200525_1241_05/paper.2.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 2 ).endswith("20200525_1241_05/paper.3.edited.jpg")) self.assertTrue(self.core.call_success( "page_get_img_url", self.doc_pdf, 3 ).endswith("20200525_1241_05/doc.pdf#page=3")) self.assertTrue(self.core.call_success( # bug #245: returned None here "page_get_img_url", self.doc_pdf, 4 ).endswith("20200525_1241_05/doc.pdf#page=4")) paperwork-2.1.1/paperwork-backend/tests/model/tests_converted.py000066400000000000000000000054601417573700700251730ustar00rootroot00000000000000import os import os.path import shutil import tempfile import unittest import openpaperwork_core import paperwork_backend.sync class TestConvertedPdf(unittest.TestCase): def setUp(self): self.test_doc = os.path.join( os.path.dirname(os.path.abspath(__file__)), "test.docx" ) self.upd_docs = set() self.nb_commits = 0 class TestTransaction(paperwork_backend.sync.BaseTransaction): priority = 0 def upd_doc(s, doc_id): super().upd_doc(doc_id) self.upd_docs.add(doc_id) def commit(s): super().commit() self.nb_commits += 1 class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): def doc_transaction_start(s, transactions, expected=-1): transactions.append(TestTransaction( self.core, expected )) self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core._load_module("fake_module", FakeModule()) self.core.load("openpaperwork_core.config.fake") self.core.load("paperwork_backend.model.converted") self.core.init() self.config = self.core.get_by_name("openpaperwork_core.config.fake") def test_on_page_get_img_url(self): with tempfile.TemporaryDirectory() as tmp_dir: self.config.settings = { "workdir": self.core.call_success("fs_safe", tmp_dir), } doc_dir = os.path.join(tmp_dir, "some_doc") os.makedirs(doc_dir) docx = os.path.join(doc_dir, "doc.docx") pdf = os.path.join(doc_dir, "doc.pdf") shutil.copyfile(self.test_doc, docx) self.core.call_all( "page_get_img_url", self.core.call_success("fs_safe", doc_dir), 0, # page_idx write=False ) self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertEqual(self.nb_commits, 1) self.assertTrue(os.access(pdf, os.R_OK)) def test_sync(self): with tempfile.TemporaryDirectory() as tmp_dir: self.config.settings = { "workdir": self.core.call_success("fs_safe", tmp_dir), } doc_dir = os.path.join(tmp_dir, "some_doc") os.makedirs(doc_dir) docx = os.path.join(doc_dir, "doc.docx") pdf = os.path.join(doc_dir, "doc.pdf") shutil.copyfile(self.test_doc, docx) self.core.call_all("transaction_sync_all") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.assertTrue(os.access(pdf, os.R_OK)) paperwork-2.1.1/paperwork-backend/tests/model/tests_extra_text.py000066400000000000000000000014021417573700700253610ustar00rootroot00000000000000import unittest import openpaperwork_core class TestExtraText(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.fs.fake") self.core.load("paperwork_backend.model.extra_text") self.core.init() self.fs = self.core.get_by_name("openpaperwork_core.fs.fake") def test_get_boxes(self): self.fs.fs = { "some_doc": {}, } self.core.call_all( "doc_set_extra_text_by_url", "file:///some_doc", "some\ntext" ) text = [] self.core.call_all( "doc_get_text_by_url", text, "file:///some_doc" ) self.assertEqual(text, ['some\ntext']) paperwork-2.1.1/paperwork-backend/tests/model/tests_hocr.py000066400000000000000000000054661417573700700241430ustar00rootroot00000000000000import re import unittest import openpaperwork_core TEST_XML = """ OCR output

ABC def

""" class TestHocr(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.fs.fake") self.core.load("paperwork_backend.model.hocr") self.core.init() self.fs = self.core.get_by_name("openpaperwork_core.fs.fake") def test_get_boxes(self): self.fs.fs = { "some_doc": { "paper.4.words": TEST_XML, }, } lines = self.core.call_success( "page_get_boxes_by_url", "file:///some_doc", 1 ) self.assertIsNone(lines) lines = list( self.core.call_success( "page_get_boxes_by_url", "file:///some_doc", 3 ) ) self.assertEqual(len(lines), 1) self.assertEqual(lines[0].position, ((10, 20), (30, 40))) self.assertEqual(len(lines[0].word_boxes), 2) self.assertEqual(lines[0].word_boxes[0].position, ((1, 2), (3, 4))) self.assertEqual(lines[0].word_boxes[0].content, "ABC") self.assertEqual(lines[0].word_boxes[1].position, ((5, 6), (7, 8))) self.assertEqual(lines[0].word_boxes[1].content, "def") def test_get_text(self): self.fs.fs = { "some_doc": { "paper.4.words": TEST_XML, }, } lines = self.core.call_success( "page_get_text_by_url", "file:///some_doc", 1 ) self.assertIsNone(lines) txt = self.core.call_success( "page_get_text_by_url", "file:///some_doc", 3 ).replace("\n", " ") txt = re.sub(" +", " ", txt) txt = txt.strip() self.assertEqual(txt, "ABC def") def test_has_text(self): self.fs.fs = { "some_doc": { "paper.4.words": TEST_XML, }, } self.assertIsNone(self.core.call_success( "page_has_text_by_url", "file:///some_doc", 1 )) self.assertTrue(self.core.call_success( "page_get_text_by_url", "file:///some_doc", 3 )) self.core.call_all("page_set_boxes_by_url", "file:///some_doc", 3, []) self.assertFalse(self.core.call_success( "page_has_text_by_url", "file:///some_doc", 3 )) paperwork-2.1.1/paperwork-backend/tests/model/tests_img.py000066400000000000000000000022641417573700700237550ustar00rootroot00000000000000import unittest import openpaperwork_core class TestImg(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.fs.fake") self.core.load("paperwork_backend.model.img") self.core.init() self.fs = self.core.get_by_name("openpaperwork_core.fs.fake") def test_get_img_urls(self): self.fs.fs = { "some_doc": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", }, } nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", "file:///non_existing" ) self.assertIsNone(nb_pages) nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", "file:///some_doc" ) self.assertEqual(nb_pages, 2) img_url = self.core.call_success( "page_get_img_url", "file:///some_doc", 1 ) self.assertEqual(img_url, "file:///some_doc/paper.2.jpg") img_url = self.core.call_success( "page_get_img_url", "file:///some_doc", 2 ) self.assertIsNone(img_url) paperwork-2.1.1/paperwork-backend/tests/model/tests_img_overlay.py000066400000000000000000000063711417573700700255210ustar00rootroot00000000000000import unittest import openpaperwork_core class TestImg(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.fs.fake") self.core.load("paperwork_backend.model.img") self.core.load("paperwork_backend.model.img_overlay") self.core.init() self.fs = self.core.get_by_name("openpaperwork_core.fs.fake") def test_get_img_urls(self): self.fs.fs = { "some_doc": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", "paper.3.jpg": "put_an_image_here", }, } img_url = self.core.call_success( "page_get_img_url", "file:///some_doc", 1, write=False ) self.assertEqual(img_url, "file:///some_doc/paper.2.jpg") img_url = self.core.call_success( "page_get_img_url", "file:///some_doc", 1, write=True ) self.assertEqual(img_url, "file:///some_doc/paper.2.edited.jpg") img_url = self.core.call_success( "page_get_img_url", "file:///some_doc", 3, write=False ) self.assertEqual(img_url, None) img_url = self.core.call_success( "page_get_img_url", "file:///some_doc", 3, write=True ) self.assertEqual(img_url, "file:///some_doc/paper.4.jpg") def test_get_edited_img_urls(self): self.fs.fs = { "some_doc": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", "paper.2.edited.jpg": "put_an_image_here", "paper.3.jpg": "put_an_image_here", }, } img_url = self.core.call_success( "page_get_img_url", "file:///some_doc", 1, write=False ) self.assertEqual(img_url, "file:///some_doc/paper.2.edited.jpg") img_url = self.core.call_success( "page_get_img_url", "file:///some_doc", 1, write=True ) self.assertEqual(img_url, "file:///some_doc/paper.2.edited.jpg") def test_reset_img(self): self.fs.fs = { "some_doc": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", "paper.2.edited.jpg": "put_an_image_here", "paper.3.jpg": "put_an_image_here", "paper.3.edited.jpg": "put_an_image_here", }, } self.core.call_all( "page_reset_by_url", "file:///some_doc", 1 ) self.assertEqual(self.fs.fs, { "some_doc": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", "paper.3.jpg": "put_an_image_here", "paper.3.edited.jpg": "put_an_image_here", }, }) self.core.call_all( "page_reset_by_url", "file:///some_doc", 1 ) self.assertEqual(self.fs.fs, { "some_doc": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", "paper.3.jpg": "put_an_image_here", "paper.3.edited.jpg": "put_an_image_here", }, }) paperwork-2.1.1/paperwork-backend/tests/model/tests_labels.py000066400000000000000000000126451417573700700244470ustar00rootroot00000000000000import unittest import openpaperwork_core class TestLabels(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.config.fake") self.core.load("openpaperwork_core.fs.fake") self.core.load("paperwork_backend.model.labels") self.core.init() self.config = self.core.get_by_name("openpaperwork_core.config.fake") self.config.settings = { "workdir": "file:///some_work_dir" } self.fs = self.core.get_by_name("openpaperwork_core.fs.fake") def test_label_color_from_rgb(self): self.assertEqual( self.core.call_success( "label_color_from_rgb", (0.9607843137254902, 0.4745098039215686, 0.0) ), "#f50079000000" ) def test_has_labels(self): self.fs.fs = { "some_work_dir": { "some_doc": { }, }, } self.assertTrue( self.core.call_success( "doc_has_labels_by_url", "file:///some_work_dir/some_doc" ) is None ) self.fs.fs = { "some_work_dir": { "some_doc": { "labels": ( "label A,#aaaabbbbcccc\n" "label B,#ccccbbbbaaaa\n" ) }, }, } self.assertTrue( self.core.call_success( "doc_has_labels_by_url", "file:///some_work_dir/some_doc" ) is not None ) def test_doc_get_labels(self): self.fs.fs = { "some_work_dir": { "some_doc": { "labels": ( "label A,#aaaabbbbcccc\n" "label B,#ccccbbbbaaaa\n" ) }, }, } labels = set() self.core.call_success( "doc_get_labels_by_url", labels, "file:///some_work_dir/some_doc" ) labels = list(labels) labels.sort() self.assertEqual( labels, [ ("label A", "#aaaabbbbcccc"), ("label B", "#ccccbbbbaaaa"), ] ) def test_load_all_labels(self): self.fs.fs = { "some_work_dir": { "some_doc": { "labels": ( "label A,#aaaabbbbcccc\n" "label B,#ccccbbbbaaaa\n" ) }, "some_other_doc": { "labels": ( "label B,#ccccbbbbaaaa\n" "label C,#000011112222\n" ) }, }, } core = self.core class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): def is_doc(self, doc_url): return True def on_label_loading_end(self): core.call_all("mainloop_quit_graceful") self.core._load_module("mainloop_stopper", FakeModule()) self.core.init() promises = [] self.core.call_all('sync', promises) promise = promises[0] for p in promises[1:]: promise = promise.then(p) promise.schedule() self.core.call_one("mainloop") labels = set() self.core.call_all("labels_get_all", labels) labels = list(labels) labels.sort() self.assertEqual( labels, [ ("label A", "#aaaabbbbcccc"), ("label B", "#ccccbbbbaaaa"), ("label C", "#000011112222"), ] ) def test_doc_add_labels(self): self.fs.fs = { "some_work_dir": { "some_doc": { "labels": ( "label A,#aaaabbbbcccc\n" "label B,#ccccbbbbaaaa\n" ) }, }, } self.core.call_success( "doc_add_label_by_url", "file:///some_work_dir/some_doc", label="label C", color="#123412341234" ) labels = set() self.core.call_success( "doc_get_labels_by_url", labels, "file:///some_work_dir/some_doc" ) labels = list(labels) labels.sort() self.assertEqual( labels, [ ("label A", "#aaaabbbbcccc"), ("label B", "#ccccbbbbaaaa"), ("label C", "#123412341234"), ] ) def test_doc_remove_labels(self): self.fs.fs = { "some_work_dir": { "some_doc": { "labels": ( "label A,#aaaabbbbcccc\n" "label B,#ccccbbbbaaaa\n" ) }, }, } self.core.call_success( "doc_remove_label_by_url", "file:///some_work_dir/some_doc", label="label A" ) labels = set() self.core.call_success( "doc_get_labels_by_url", labels, "file:///some_work_dir/some_doc" ) labels = list(labels) labels.sort() self.assertEqual( labels, [ ("label B", "#ccccbbbbaaaa"), ] ) paperwork-2.1.1/paperwork-backend/tests/model/tests_pdf.py000066400000000000000000000132131417573700700237460ustar00rootroot00000000000000import os import shutil import tempfile import unittest import openpaperwork_core class TestHocr(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("paperwork_backend.model.pdf") self.core.init() self.simple_doc_url = self.core.call_success( "fs_safe", os.path.join( os.path.dirname(os.path.abspath(__file__)), "simple_doc.pdf" ) ) self.full_doc_url = self.core.call_success( "fs_safe", os.path.dirname(os.path.abspath(__file__)) ) mapping = self.full_doc_url + "/page_map.csv" if self.core.call_success("fs_exists", mapping): self.core.call_success("fs_unlink", mapping, trash=False) def tearDown(self): mapping = self.full_doc_url + "/page_map.csv" if self.core.call_success("fs_exists", mapping): self.core.call_success("fs_unlink", mapping, trash=False) def test_is_doc(self): self.assertTrue(self.core.call_success("is_doc", self.simple_doc_url)) self.assertTrue(self.core.call_success("is_doc", self.full_doc_url)) def test_hash(self): h = self.core.call_success("doc_get_hash_by_url", self.simple_doc_url) expected = ( 0x7d2ffb0e8ddce8f7dfbb4a8dfc14d563b272bd47b1bafb9617fbfd228bf2eecd ) self.assertEqual(h, expected) def test_get_nb_pages(self): self.assertEqual( self.core.call_success( "doc_get_nb_pages_by_url", self.simple_doc_url ), 1 ) self.assertEqual( self.core.call_success( "doc_get_nb_pages_by_url", self.full_doc_url ), 4 ) self.core.call_all("page_delete_by_url", self.full_doc_url, 2) self.assertEqual( self.core.call_success( "doc_get_nb_pages_by_url", self.full_doc_url ), 3 ) def test_get_text(self): text = [] self.core.call_all("doc_get_text_by_url", text, self.simple_doc_url) self.assertEqual(text, [ 'This is a test PDF file.\n' 'Written by Jflesch.' ]) def test_get_boxes(self): lines = list(self.core.call_success( "page_get_boxes_by_url", self.simple_doc_url, 0 )) self.assertEqual(len(lines), 2) self.assertEqual(lines[0].position, ((227, 228), (656, 281))) self.assertEqual(len(lines[0].word_boxes), 6) self.assertEqual(lines[0].content, "This is a test PDF file.") self.assertEqual( lines[0].word_boxes[0].position, ((227, 228), (312, 281)) ) self.assertEqual(lines[0].word_boxes[0].content, "This") self.assertEqual( lines[0].word_boxes[1].position, ((324, 228), (356, 281)) ) self.assertEqual(lines[0].word_boxes[1].content, "is") self.assertEqual( lines[0].word_boxes[2].position, ((368, 228), (389, 281)) ) self.assertEqual(lines[0].word_boxes[2].content, "a") self.assertEqual( lines[0].word_boxes[3].position, ((401, 228), (468, 281)) ) self.assertEqual(lines[0].word_boxes[3].content, "test") self.assertEqual( lines[0].word_boxes[4].position, ((480, 228), (568, 281)) ) self.assertEqual(lines[0].word_boxes[4].content, "PDF") self.assertEqual( lines[0].word_boxes[5].position, ((580, 228), (656, 281)) ) self.assertEqual(lines[0].word_boxes[5].content, "file.") self.assertEqual(lines[1].position, ((227, 284), (588, 337))) self.assertEqual(lines[1].content, "Written by Jflesch.") self.assertEqual(len(lines[1].word_boxes), 3) self.assertEqual( lines[1].word_boxes[0].position, ((227, 284), (371, 337)) ) self.assertEqual(lines[1].word_boxes[0].content, "Written") self.assertEqual( lines[1].word_boxes[1].position, ((383, 284), (431, 337)) ) self.assertEqual(lines[1].word_boxes[1].content, "by") self.assertEqual( lines[1].word_boxes[2].position, ((443, 284), (588, 337)) ) self.assertEqual(lines[1].word_boxes[2].content, "Jflesch.") class TestPassword(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("paperwork_backend.model.pdf") self.core.init() password_doc_url = self.core.call_success( "fs_safe", os.path.join( os.path.dirname(os.path.abspath(__file__)), "test_password.pdf" ) ) self.tmp_doc_dir = tempfile.mkdtemp() self.tmp_doc_url = openpaperwork_core.fs.CommonFsPluginBase.fs_safe( self.tmp_doc_dir ) doc_pdf = self.core.call_success( "fs_join", self.tmp_doc_url, "doc.pdf" ) passwd_txt = self.core.call_success( "fs_join", self.tmp_doc_url, "passwd.txt" ) self.core.call_all("fs_copy", password_doc_url, doc_pdf) with self.core.call_success("fs_open", passwd_txt, "w") as fd: fd.write("test1234") def tearDown(self): shutil.rmtree(self.tmp_doc_dir) def test_open_page(self): boxes = self.core.call_success( "page_get_boxes_by_url", self.tmp_doc_url, 0 ) self.assertNotEqual(len(boxes), 0) img_url = self.core.call_success( "page_get_img_url", self.tmp_doc_url, 0 ) self.assertIn("password=", img_url) paperwork-2.1.1/paperwork-backend/tests/model/tests_thumbnail.py000066400000000000000000000020031417573700700251530ustar00rootroot00000000000000import io import os import unittest import PIL.Image import openpaperwork_core class TestImg(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.fs.fake") self.core.load("paperwork_backend.model.thumbnail") self.core.init() with io.BytesIO() as fd: img = PIL.Image.open( os.path.dirname(os.path.abspath(__file__)) + "/test_img.png" ) img = img.convert("RGB") img.save(fd, format="JPEG") fd.seek(0) self.raw_img = fd.read() self.fs = self.core.get_by_name("openpaperwork_core.fs.fake") def test_get_img_urls(self): self.fs.fs = { "some_doc": { "paper.1.jpg": self.raw_img, }, } thumbnail = self.core.call_success( "thumbnail_get_doc", "file:///some_doc" ) self.assertEqual(thumbnail.size, (64, 32)) paperwork-2.1.1/paperwork-backend/tests/model/tests_util.py000066400000000000000000000067351417573700700241650ustar00rootroot00000000000000import unittest import openpaperwork_core import paperwork_backend.model.util class TestUtil(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.fs.fake") self.core.init() class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): def doc_get_nb_pages_by_url(s, doc_url): doc_url = doc_url[len("file:///"):] return len(self.fs.fs[doc_url]) self.core._load_module("fake_module", FakeModule()) self.core.init() self.fs = self.core.get_by_name("openpaperwork_core.fs.fake") def test_delete_page_file(self): self.fs.fs = { "some_doc": { "paper.1.words": "abcdef", "paper.2.words": "ghijkl", "paper.3.words": "mnopqr", "paper.4.words": "stuvwx", }, } r = paperwork_backend.model.util.delete_page_file( self.core, "paper.{}.words", "file:///some_doc", 1 ) self.assertTrue(r) self.assertEqual( self.fs.fs, { "some_doc": { "paper.1.words": "abcdef", "paper.2.words": "mnopqr", "paper.3.words": "stuvwx", }, } ) def test_move_page_file(self): self.fs.fs = { "source_doc": { "paper.1.words": "Aabcdef", "paper.2.words": "Aghijkl", "paper.3.words": "Amnopqr", "paper.4.words": "Astuvwx", }, "dest_doc": { "paper.1.words": "Babcdef", "paper.2.words": "Bghijkl", "paper.3.words": "Bmnopqr", "paper.4.words": "Bstuvwx", }, } r = paperwork_backend.model.util.move_page_file( self.core, "paper.{}.words", "file:///source_doc", 2, "file:///dest_doc", 1 ) self.assertTrue(r) self.assertEqual( self.fs.fs, { "source_doc": { "paper.1.words": "Aabcdef", "paper.2.words": "Aghijkl", "paper.3.words": "Astuvwx", }, "dest_doc": { "paper.1.words": "Babcdef", "paper.2.words": "Amnopqr", "paper.3.words": "Bghijkl", "paper.4.words": "Bmnopqr", "paper.5.words": "Bstuvwx", }, } ) def test_move_page_file_same_doc(self): self.fs.fs = { "source_doc": { "paper.1.words": "Aabcdef", "paper.2.words": "Aghijkl", "paper.3.words": "Amnopqr", "paper.4.words": "Astuvwx", }, } r = paperwork_backend.model.util.move_page_file( self.core, "paper.{}.words", "file:///source_doc", 2, "file:///source_doc", 1 ) self.assertTrue(r) self.assertEqual( self.fs.fs, { "source_doc": { "paper.1.words": "Aabcdef", "paper.2.words": "Amnopqr", "paper.3.words": "Aghijkl", "paper.4.words": "Astuvwx", }, } ) paperwork-2.1.1/paperwork-backend/tests/model/tests_workdir.py000066400000000000000000000124131417573700700246570ustar00rootroot00000000000000import datetime import unittest import openpaperwork_core class TestWorkdir(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.config.fake") self.core.load("openpaperwork_core.fs.fake") self.core.load("paperwork_backend.model.img") self.core.load("paperwork_backend.model.workdir") self.core.init() self.config = self.core.get_by_name("openpaperwork_core.config.fake") self.config.settings = { "workdir": "file:///some_work_dir" } self.fs = self.core.get_by_name("openpaperwork_core.fs.fake") def test_storage_get_all_docs(self): self.fs.fs = { "some_work_dir": { "some_doc_a": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", }, "some_doc_b": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", }, }, } all_docs = [] self.core.call_success("storage_get_all_docs", all_docs) all_docs.sort() self.assertEqual( all_docs, [ ("some_doc_a", "file:///some_work_dir/some_doc_a"), ("some_doc_b", "file:///some_work_dir/some_doc_b"), ] ) def test_storage_get_new_doc(self): def now(): return datetime.datetime( year=2019, month=9, day=4, hour=13, minute=27, second=10 ) self.fs.fs = { "some_work_dir": { "20191010_1327_10": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", }, "20191010_1327_10_1": { "doc.pdf": "put_a_pdf_here", }, }, } (doc_id, doc_url) = self.core.call_success( "storage_get_new_doc", now_func=now ) self.assertEqual(doc_id, "20190904_1327_10") self.assertEqual(doc_url, "file:///some_work_dir/20190904_1327_10") def test_storage_get_new_doc_2(self): def now(): return datetime.datetime( year=2019, month=9, day=4, hour=13, minute=27, second=10 ) self.fs.fs = { "some_work_dir": { "20190904_1327_10": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", }, "20190904_1327_10_1": { "doc.pdf": "put_a_pdf_here", }, }, } (doc_id, doc_url) = self.core.call_success( "storage_get_new_doc", now_func=now ) self.assertEqual(doc_id, "20190904_1327_10_2") self.assertEqual(doc_url, "file:///some_work_dir/20190904_1327_10_2") def test_rename(self): self.fs.fs = { "some_work_dir": { "20190904_1327_10": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", }, "20190910_1328_10_1": { "doc.pdf": "put_a_pdf_here", }, }, } self.core.call_all( "doc_rename_by_url", "file:///some_work_dir/20190910_1328_10_1", "file:///some_work_dir/20200508_2002_25" ) self.assertEqual( self.fs.fs, { "some_work_dir": { "20190904_1327_10": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", }, "20200508_2002_25": { "doc.pdf": "put_a_pdf_here", }, } } ) def test_rename2(self): self.fs.fs = { "some_work_dir": { "20190904_1327_10": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", }, "20190904_1327_10_1": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", }, "20190910_1328_10_1": { "doc.pdf": "put_a_pdf_here", }, }, } self.core.call_all( "doc_rename_by_url", "file:///some_work_dir/20190910_1328_10_1", "file:///some_work_dir/20190904_1327_10" ) self.assertEqual( self.fs.fs, { "some_work_dir": { "20190904_1327_10": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", }, "20190904_1327_10_1": { "paper.1.jpg": "put_an_image_here", "paper.2.jpg": "put_an_image_here", }, "20190904_1327_10_2": { "doc.pdf": "put_a_pdf_here", }, } } ) paperwork-2.1.1/paperwork-backend/tests/pageedit/000077500000000000000000000000001417573700700220635ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/pageedit/__init__.py000066400000000000000000000000001417573700700241620ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/pageedit/test_img.png000066400000000000000000000107061417573700700244100ustar00rootroot00000000000000PNG  IHDRdÆ bKGD pHYs.#.#x?vtIME vtEXtCommentCreated with GIMPW.IDATx{LSgǿU,E`Vo+dTQScj:ܘHy&Bp@ P"rSN[V~۾@O=PI9ym H0 0,a0 aX að@0 a^D"t0H$TWWDF㴊wE`ʕH$Xa\P Y|TUU ^ƭ/3ӳG|tE! PTy D"8>غu+juXɓ@^^@&aܸq8GOFqq1+`ڴixwo؜WAAƏo/'0uVNJT*RL0AAA?/8bb鱐@UUU^J(%%<<#Vz=EDDc6ץ+9/׉' ڵkkNq@PLL jT[[K |Ҧ7''… h4Vھ}`^8x F!NGZ-eeeT*%tUn؊+ݾ}52ܹsT[[K? $JI:Tw[bat (jnn8M(''Ǧ7!!ٳg;]QF{>}О={:U Z<<<ϯPݟ8}nZiL&3φ!C7otZ]Q ///0;BTVVbɒ%4h5>>>3gtKyC|hq?3`{^ja޽{G [liwh"uhޙe Zvռ=*j^VVh}u8HsSj*#??EEEχLiӦO?˔ѧO|w4i1cr<*SPP#G ;;˖-ԩS'O… 6l J!k9Tw[bHؼaA:ð@0,a0 aX a0,:T[q{WP@" ##AX 8 =l'X ÇQUUNddffӧOǂ 0zhJII"a…x7-sÇHKKCrr2 \`(J̙31c`z{lm<}||0qDbĈul 55YYYCRR~mQqۦ] 8~8qx{{Cpp(D=i`RR]hRH& ?uTjZ?`޽{ ֖ե]N{mz)qۦ&"ŋ_.e=ڑ@P\\ݹsbcc ͝;믿7o^ڿ?ɓ'^?[%Kʕ+d0.\@X\mGHܽ{Wo>H[پKNúuP^^^K.O?m+˶ݻw>deet(//ݻq&CG|===i&ڵkq:<>y(]潻i&@eee.vV5@`e~"T*-Ҋe%=..NJ6#m,`"… ]~%ݥy4h~ )) d9s&@[W-uoT*&O# @p =H:4w^̙3s2 3g΄\.Rtg/r1 ׮]ȑ#!JqVqA#ܽ{PTtӧtR,T;<],"BII N8B៽0;Hg'z 0,a0 aX $,, e6Xi,^U/jaƂrZ| 0 JBRDuu5baSWW#Gbxys[rssm۶+qIdddd2ƍ???^FBtT*`޼yp8 %&&Z-vAt۷o1ؖիW>e@iii6+R^vwϵuwtħ7%%<<<8pnm۷h4R]]ϵ}JKK ^Z0_ܹs[v@ӔQ}}=FjTRRB۷oN鍉!ZMzjkkIPϵ-&̝;hCxSSyFHw@ǎ+`@-$ٳ]EVͺsrr)voވxPPtիyF۽azEW#WvȐ!7o\#>d29ba`֬Y,&M777ڵ/_Fnn.6olԖi^k.~VT*Ehh(֭[R7n_K:tqͪ5*b䟤-nbtt4rssqeT\.ﶊvaGѣG#$$6l{_/_ޭ[`;ÓٳDh4VrO/i;0az{dd$ `Y@^旡b)StkŝkzE3|zHbb"lق 455A qa_^[c; 8r_455* ;wVouO/ӅtJo|?`5g/hfѢET[[t޼<@ X ޼(//GRR233q=!$$'N03vZ@*ė_UV(**B~~>0eL6 e.Č3?[y `4Sr3=ϳ644رc8tKbUVVZT]vͮwl0c5j"##qeÿ ÿ a0,a0 aX ah dARIENDB`paperwork-2.1.1/paperwork-backend/tests/pageedit/tests_pageeditor.py000066400000000000000000000143201417573700700260020ustar00rootroot00000000000000import os import unittest import PIL import PIL.Image import openpaperwork_core from paperwork_backend.pageedit import AbstractPageEditorUI class FakeUI(AbstractPageEditorUI): CAPABILITIES = AbstractPageEditorUI.CAPABILITY_SHOW_FRAME def __init__(self, tests): self.tests = tests def show_preview(self, img): self.tests.ui_calls.append(('show_preview', img)) def show_frame_selector(self): self.tests.ui_calls.append(('show_frame_selector',)) def hide_frame_selector(self): self.tests.ui_calls.append(('hide_frame_selector',)) class TestPageEdit(unittest.TestCase): def setUp(self): self.test_img = PIL.Image.open( os.path.join( os.path.dirname(os.path.abspath(__file__)), "test_img.png" ) ) self.pillowed = [] self.ui_calls = [] self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("paperwork_backend.model.fake") self.core.load("paperwork_backend.imgedit.color") self.core.load("paperwork_backend.imgedit.crop") self.core.load("paperwork_backend.imgedit.rotate") self.core.load("paperwork_backend.pageedit.pageeditor") class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 999999999999999999 def pillow_to_url(s, img, dst_uri): self.pillowed.append(dst_uri) return dst_uri self.core._load_module("fake_module", FakeModule()) self.core.init() self.model = self.core.get_by_name("paperwork_backend.model.fake") def test_all(self): self.model.docs = [ { "id": 'some_doc_with_text', "url": 'file:///some_work_dir/some_doc_id', "mtime": 12345, "labels": [], "page_imgs": [ ("file:///paper.0.jpeg", self.test_img), ("file:///paper.1.jpeg", self.test_img), ("file:///paper.2.jpeg", self.test_img), ], "page_boxes": [[], [], []], }, ] fake_ui = FakeUI(self) page_editor = self.core.call_success( "page_editor_get", "file:///some_work_dir/some_doc_id", 1, fake_ui ) self.assertEqual(self.pillowed, []) self.assertEqual(len(self.ui_calls), 2) self.assertEqual(self.ui_calls[0][0], 'show_preview') self.assertEqual(self.ui_calls[0][1].size, (200, 100)) self.assertEqual(self.ui_calls[1][0], 'hide_frame_selector') modifiers = page_editor.get_modifiers() modifiers = [e['id'] for e in modifiers] modifiers.sort() self.assertEqual(modifiers, [ "color_equalization", "crop", "rotate_clockwise", "rotate_counterclockwise", ]) # rotate 90° self.pillowed = [] self.ui_calls = [] page_editor.on_modifier_selected("rotate_clockwise").schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_all("mainloop") self.assertEqual(len(self.ui_calls), 2) self.assertEqual(self.ui_calls[0][0], 'show_preview') self.assertEqual(self.ui_calls[0][1].size, (100, 200)) self.assertEqual(self.ui_calls[1][0], 'hide_frame_selector') # ACE avg_color = self.ui_calls[0][1].resize( (1, 1), resample=PIL.Image.BILINEAR ).tobytes() self.pillowed = [] self.ui_calls = [] page_editor.on_modifier_selected("color_equalization").schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_all("mainloop") self.assertEqual(self.pillowed, []) self.assertEqual(len(self.ui_calls), 2) self.assertEqual(self.ui_calls[0][0], 'show_preview') self.assertEqual(self.ui_calls[0][1].size, (100, 200)) avg_color2 = self.ui_calls[0][1].resize( (1, 1), resample=PIL.Image.BILINEAR ).tobytes() self.assertNotEqual(avg_color, avg_color2) self.assertEqual(self.ui_calls[1][0], 'hide_frame_selector') # crop self.pillowed = [] self.ui_calls = [] page_editor.on_modifier_selected("crop").schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_all("mainloop") self.assertEqual(self.pillowed, []) self.assertEqual(len(self.ui_calls), 2) self.assertEqual(self.ui_calls[0][0], 'show_preview') self.assertEqual(self.ui_calls[0][1].size, (100, 200)) self.assertEqual(self.ui_calls[1][0], 'show_frame_selector') page_editor.frame.set((0, 190, 10, 200)) self.assertEqual(page_editor.frame.get(), (0, 190, 10, 200)) # rotation again (180°) self.pillowed = [] self.ui_calls = [] page_editor.on_modifier_selected("rotate_clockwise").schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_all("mainloop") self.assertEqual(len(self.ui_calls), 2) self.assertEqual(self.ui_calls[0][0], 'show_preview') self.assertEqual(self.ui_calls[0][1].size, (200, 100)) self.assertEqual(self.ui_calls[1][0], 'show_frame_selector') self.assertEqual(page_editor.frame.get(), (190, 90, 200, 100)) # .. and again (270°) self.pillowed = [] self.ui_calls = [] page_editor.on_modifier_selected("rotate_clockwise").schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_all("mainloop") self.assertEqual(len(self.ui_calls), 2) self.assertEqual(self.ui_calls[0][0], 'show_preview') self.assertEqual(self.ui_calls[0][1].size, (100, 200)) self.assertEqual(self.ui_calls[1][0], 'show_frame_selector') self.assertEqual(page_editor.frame.get(), (90, 0, 100, 10)) # and save ! self.pillowed = [] self.ui_calls = [] page_editor.on_save().schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_all("mainloop") self.assertEqual(len(self.ui_calls), 0) self.assertEqual(self.pillowed, ["file:///paper.1.jpeg"]) paperwork-2.1.1/paperwork-backend/tests/pillow/000077500000000000000000000000001417573700700216075ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/pillow/__init__.py000066400000000000000000000000001417573700700237060ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/pillow/test_doc.pdf000066400000000000000000000230121417573700700241040ustar00rootroot00000000000000%PDF-1.5 %äüöß 2 0 obj <> stream xm 0EY 3I2`kpWw>vbQ3%K<%*&ߒ`Ցr}Ji|xD@ !Pέ&UX\|d=pWb}q/#AQ,J%a]V>DhHN )nH&;z7ND endstream endobj 3 0 obj 181 endobj 5 0 obj <> stream xzkxU=UzXl=,$+JQCI+~۴B,ْ`[BciZW] @~=DN3(y\$Db_/7+]!|S,eWɗ^!=r|I/!2AnA[%z L _Q'T1"Qay9</(܆2J L/7-+6r&AI<^ z 3?6KF9 __l)r辌cSv9B¿j@BMjodcگ}ΫvvZ[wM۷mݲq kUk׬.[_:fH_+j*Rɋi2юJ1ZUFD>ʋRwtP?*^\"tDsx s yN0[V ?/~üxWSX6JoFg"h# [-ªJ2[GP֟ہڶͳ )(őEcb*wVJ"-TiT%?&Ns35H!ElegضDsXoo#6Bڹ'O.AT ᣥєH$PdZD.W}=3L7gf dQمq{¢)2 Ѓ{:ŒkE,ȏFM~&Ϝ }[9aOrÑĆ8/y2zD$\b(9J^d+DԎ )$`~tPK|?GyQKcC8\U"gɹL1XZ\͎e92?S왑-))Mf] ǵ7ᔦzfV,];ezZZ)QPe7PXr " u:X] g/# Bisu fvjJ=,w%7p8F kHu]M~(j=ys:^PkkcX}puee*`pX,W;lf kf L 9 h#PaA:Kc@v>i9)@2J%`ۡ Wyٰ`no<ݩl$5':~Ugǁ|΁{[j7pVOc-;X}WYJR:ŘUp]XSX\-aYCeBaVo9m1hr@!)K:FKcmCjL lu6يqh+x=ۭ5o;xGiK}B1ld9*uK lF$h׉v:   [Luwy@ x |?W;G!+>;mv5nJS|[).!"ki HΚa66Y'N8NH:4W,DL7c2Z'fl'odMO^pO_e]y))pn)ggbJwRV;$,@YL"sŤNm6kt:FV;hTr O8'L;!ㄘTN^':u'䄼9'j3TC%B 7,%Y.4N|/)k)uSϿ)_m^f|ϽS<ƉlqaN'. X^Rr\NW(k5fKPXTnݐqC nnx /H\$#X)u5{ 7[UVQλu7pôn6MoIQ6ZV|Il0_ɁQ(4^AZvih~OV4+Yϗz-/|r k7y\H: Y5:^=<p2zazP1(1=+z!Aszp6Mz zHȷmReI^YqB)R$z ] C-h4"Vl iRj԰ЁCmmA,ZmzxPmUs61BV=6kbŸgJb\ǁ9xqp9 8XM.sOs|Gp&?Qp 69XŁ2⋵3Fa`q([T l<`/jiuP!GS y띱*5oS^1uQiCC]gg%XهVnʬՂ`4f,c5BaQk(r6@^q'M:\̹2FkVjUv<0UlvEխ1L\ ,km췑`:n <0`sM͂iO5=<Pyx じz=z򀋒a;X|)/Du76R٘? (!zN`_gpIqZhnbw=R](\ip'aɈQ31zL<;ɧA(8y3Wv+B)V(جt`wnɔN:{o]nx0o7Q7G[wfSdFX0JXHeѻ%fC1Vl$0KV`=p 4x?=mrAZ!^xx>Ni)JXh\?Q~̰38Oy`{r \?oǙlL<.|c==RmyAXe(#u>ݻ<`+ViZ8-bX,e^5WG6ӓɕ5dr"%]wc}΍G"pa`;"lJľClk>܏o>y*y'>DD&AlHX-f 5Ψ0c快Zӹ!stHЯMR+}Ŵ\˟přE=`J%=9ŨW#]͞{TBWŔ{Oq5b7[j=c(l(0qV֊#.8Lp@p,=o ʹX陕ݽ߹ƊL}㍫f?=Ȏ 5̍ƭd/ژ b58XMHlb̒4 7LEi$2GIddf~H_DDPjld4ï*kkxoIT43.lYVAL%srz`\{⩱=hjGz(>*~95TZjVT7\.gKQ> |bx!|*>2S{P4ߛSP<"s"3^?KƆ,rGO&~ _dds4}ect%ptlh?Mxzld2- Y3ь4x&56?1H Xf;Dtղ)at*?6L%PCx|;Ƣcc6MEc趱4:OF'ڦRd-F+htb,qO1G4@|Di<ÉˌV-|81ApԄ'ts&g\t(@Zrxflq>V$+ Z";J(gJKY6Hb_FL>(VJ23M>xj}OQu9HP!ď2&+r_L:H2 J&)+4n4W"Ͱ4Ϛ!eT+6r]I#emܥZ4s5h]/J4Hf[lr6a;CIS_V1 {Ehz-Ц/fu'0BJ"d7 >Aoinmd!~;"~ .^|6݅x9j#Үv%JOmB nWAķMi6IҏyTi8???g0ɱO?\,>}EくC`p₦o{#{{W,5s5sɹ]+鱟2y{l)0>} =y9vǽG>~5^|90w"sy]dpiv\2Ӌw.xߋ7{݋wv ؁}*#wNyNvcw0O8wIʽ d:ѧc4 .,[ dvowo{ҧz&MXmۍ#tb.>vU؝roG&h5 B<_pEg31#}ゑ14f5c=g {:łе"-HO{[$}{NGIS#pC@iLY4L)| DOX4qJS!6B!Bޟ&C"VBtZQGg endstream endobj 6 0 obj 7404 endobj 7 0 obj <> endobj 8 0 obj <> stream x]Mn0t DBH)I$Qi"cV陰ΕՍYtn d,t浢U B[/ Ce1σ͟M[ipGY[YDAQ fl}˟} b%[Q6 \cnQTz-0YlJ/}Rxg;=q!kh΁GOqBGd=D]+<ˈ}2OwΚƄPw|ؔ9ho; endstream endobj 9 0 obj <> endobj 10 0 obj <> endobj 11 0 obj <> endobj 1 0 obj <>/Contents 2 0 R>> endobj 4 0 obj <> endobj 12 0 obj <> endobj 13 0 obj < /Producer /CreationDate(D:20190902205629+02'00')>> endobj xref 0 14 0000000000 65535 f 0000008715 00000 n 0000000019 00000 n 0000000271 00000 n 0000008884 00000 n 0000000291 00000 n 0000007780 00000 n 0000007801 00000 n 0000007996 00000 n 0000008384 00000 n 0000008628 00000 n 0000008660 00000 n 0000008983 00000 n 0000009080 00000 n trailer < ] /DocChecksum /72B44A81E5EEF3D98178B73CE31BA672 >> startxref 9255 %%EOF paperwork-2.1.1/paperwork-backend/tests/pillow/test_doc.png000066400000000000000000001263371417573700700241350ustar00rootroot00000000000000PNG  IHDR L $IDATxgU5^DT"RR4^4L +*1sQc49(VD;⨈H3( "~?kãkg2wD#' 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"&'i+Yf:tUo/҄ .]ӬYݻ_~eymQQQ~~w߶mVL!-HT<÷zuR7瞫)Uh7|S7֪Ukƍey :o(u{TDɕ|a/Bk׮^{5\3waÆ|Ur+Vu]gѣ_V% nT1YYYuԩ[ߦMއ,GKJKKmԨ^{պuܪժU뢋.^ۿ7{oaΙ3gͳg:twQT!0,]+cڵkZZZ˖-w^%L$~ (9r5kn*9rXs5{J_~w{Qɣ[juG~={ɩi^z|ӧOEQGC%7r!?eae{n߾}׮]+<*vamڴ(##y{gÆ sss222222?9s$y晉Db˖-EEE6lXnڵkWXt?~|\:̙ӺuxsΟ}YEtՄ9P}W=w>u]W%?7n /0yDsO<1p>}Tr%zwN;+Ųe~r۶mSw]y 4Wg}W%ՠAoɇ5iI's`׫W\0iҤ~z޽u]7iҤ:*޵lٲ?OS&p@%nעE? P"!kqJtWάY:wNTk}iiiquOd~zO>dj$+l;>o뮻ۯVZ9´it^݉GHZhQ=[>SׯSR簳mذYlWuM/;;w]_N0J׿Ox뭷;JKOOoժUe˙;1O͈|!---##e˖Ur-[fdd$oGx≥KV,w';Ux_uUjQQoۭ[V)J߱sU5i7@MЪU [wrԻRc=xqx;wn1bŋӧO>*ĶO>KOO/zD"1qO?[~}nnn&M~pڵjgΜx+VX"==aÆ:tڵkvvves͟??\TTZm322Wj Ǎ7eʔKnڴQFmڴ96mZAUlzU%--^z+VHU{Zjx7pC6կ_7L$7oW}W良*{!ˋhҤIO=K/|Խ]wb%]P}}J<˨QnYf7##W^9묳50UW]5bĈ%Kq7pM7ݔQ~ʈ#>xq:f'tRnذVZUlWZZڹ{wo9pe*<*[▖vڵkWo9rG}4ve˖:b# 333R.-XN/qoQQߟ׿5uywN:u/r駸~GQ_裏{W^=}!C̟?hÆ yMC>K<3رcKaÆv%G6jhڴi{ϟ>$6mtٲe{+A}ze׿+eԩSS{+sԳ(+^޴iS{$dɒY͛Cf4hP⮌xhg=zN:moX˖-)S$j—U._|E^x˽{eee&^Xkv۾I&2+`W;CƏEˏ=k׮?9}YooXӦMŋ5ڼys[n]z… 'L0ux{a͚5kslR3>%\FQ͚5[n'xe]֫WvD"7īж+W5ڰaW_]׿[hSPWnR.ի믿^3&_ꫯƌ1cƌ3vڽ{K>cׯϟ2eܹs5klܸqӦMiYf֭j׮]wKxyƍrMJyg$322ԩS^-[v֭K.{b*XG-VLN|ZZZïO[vܽ{AB9_;x֮]ܒ޸q:uي]jZԩ/ׄ/D{ǫOZ̙3SW>J7dee 8k4hЋ/tԽ'N<#zYg8kwqSL(==^xaΝ1ۋU+37cƌf͚ 5˪O/y䑕<`8}٧ǩ.U{(ti^hј1c Pnx͛ppw& _E=c=va]5RMhMj-[L<9^m׮]oټy_ZyU{!YFF '0p tMk>#͡𩧞J.>]vYUN򲲲˗W5˪!~gӧ'SCT "uֽ{x{e˖%?jG͛WU _Vzg弼Tdj{ؖںuJ{ 7^ŋ7.^nܸqV:v/O8/ _V 4nܸaÆū]vYչ{ D>իWU_q' nEEE͛+6*t '/raaa^^f͚5O$u];7<ӥݴiSranM4gΜYyUFFFbŊ ;ssss|ͣ>ZU*bŊO>9fԩ3|ڵkWjފn͛7o;ܺukUM|P zw\rҤI=z7:dee%'M4ymL:\,QÆ O>d{֭{Wǫr /PU*H$^|.]L0!ycƌI} =_.\p?ȄO`Æ guܹs~饗&׬Y+"93H$zSO}׳g7n?ܭ[.]dggWrݻwK7nܲeˢEl;:t\޼ys~z5r'_o?#;Q>O&(**9s-[f͚M6o_߮]_WKSOϺuV*{1bD+k '-~g̙&MMf͚䮌Ν;ڶm{qǕx'Æ Kniݺ^z衇vڵ~Qmݺu̙SL=zt|]~7`ݺuUּy˂>W_MiժU߾}۵k׶m۶m&ۖ|p]FF)rG4mڴ𫯾zW(7nvܹ\HMCE_~塇~ <'|7xavᇧ^Ck׮mӦ͒%K- ׬Yze˖yǏ/vgȐ!j?rM˖-{7f̘1cƌO?tʕ:u۴is'h\PP_N4)>Z٥릛n:ꨣ*6;hbKSOoYyرcGkеkN:o߾}:tw}2w'6f̘7|39m۶~ĕ|P=6nXPP0w~ݺu 4h߾\[Yք9}3fXn]F{9sdff>:ujٲe/&Nt+VԪUaÆ WJ|PޮTTTJEQ:u2 &L3Ϥ]xn lI #@`D> 0"F|#@`D> 0"F|#@`D>LfuOzx1dggTŋK&LXtiNNNfͺw~%o;x}m۶d՚={yml֬Yc:AJK$=২e˖+OŋkҮ7poykժbŊ:o-**JտvU[׭[o߾=\uM 8# O>daaac.첌ʜeҥÇ/eyרQvŊwuٳ{q嗧Ubofڵ3fׯߜ9s{:1s̱cǖ>u'tRޛo9k7n|(rss̙yٳg:;Wkk;waÒ%?O/.e@&M.JFEr-+W,qoZ=FD"qO0!#GYo$w'k>ׯ_)S\{6mހO?1:ꡇJn֡C:={vrʕ+SI銊>#<_.}dݺu;vرc>iӦyÆ 333lْ\s=+vjVZjK}\>}Jye~r۶mSw]y 4I`J>ӧOxO?]`A_o,^xԨQ˗//7[Q{.|Q5ho[zzzEM4+|(v-ZP $"oEQ":th_OFQiӦ_|,y睵kF7+V_W_;fܹs)#?x,HjiӦ˹8%:Cj#ߒ%K|ÇGQٻwO֭[r)/ĕ ~#VSx93m(7^7qy%O?t|wI&}߲e˨Q(:7n\O;_ H-6mz>Ν;s9DKK=EEE*v7nʔ)K.ݴiSFڴis7mtEEEDb{os֭SL7n܂ 222ڴisgb[dرc+Vdgg7iҤcǎsLNNζ/)s~\lNH$6oJ_X&N駟~wׯmҤ~8]veyv?/bٲek֬c=֭[~-Zp)r޺uɓǏpD"Ѳe=zhѢ̟O@t}gϞ]~m77hG5kl{A%.^oРAr 7ܐH$}m_ҷos-YdРA'tR)]gjwuWzvymm~D"1mڴw޼VZ~e8Cʚ5k9Sb ͽ~b/GDQt衇~2p̙3硇:wx&ԿRP;6r8`{SxWK9uAAA>}J3;;;???Uޫ*HL8+lذD"J>ԧOlСrK#W\+DQt駏3&駟ѣG/7nܒ%K(xcڵI& B-[~;GݰaCwyxԩSG/x{(1777jd+ؒ%KN=ӧGQs{999SN}Ǘ-[/3fԨQGqD'Fz饗[^{ķiӦ?k׾KPvEEErƍ(J^q˗' 6<Æ {۶m/nժ[oZ>={r!:uJ=o~nݺ͜9?OW^^x^o0###0//qež/y9lzz/ˣ>z^zӇ 2m᱇z覛nJ뗿eN6lzYfq"ؼys-т N8엪_Z`7T*#;vްA%s9KjժGۺk(^r{S#ĉ'ئMrF::uD{%_/)v%_,cǎ-南q ڵkmڴi{ϟ>$6mtٲe͆ ;|W>裙3gN<jҤI<~֭+eڥ䋢o;vlz?c=z[ӦM.]q9x̯~O[oEQ6w܊M^ɗOG~\W{ɬ6l8(zK<OVZ혂(zaÆ}3f z3ج%KqDl0/O6m֬Y%{'(jݺ{E7m4dȐ_2bĈ(N;o{jĉsÿe˖}_Fvn +5EQԼy;,P~a?s9G}t=k?JYrѣ+?kJ[cƍ>g?Y<l+"^>|+J9O?E'{<2_ߟH$(ٳ綗r_Zw1Buy>-fS?/NKK;ӒKJfbLF^sg>g}{rW-Zj۟}ێݻwڵ йs߫W-[nD"qW'aZZ#,^7iӦŋ+p R'c:u:/5k(KwA%~˗/?c~M'|/wر'tRFxpJ' r)j%OӦM{왺7MhѢw}7^>|xEyyy^D"Il͍Akݺurum;+m5|c9w,wݩ|֬Yݺu;餓^zחr¯*^MjD>–z΂3f$̙FQtdffWUZ;v&o?سg!Zf֭[ծ]ގo=m CuժUێӧO +WL֭[{h7__\j3oڴo~cǖx+V$x~U(uY'w|'"* ykiܹSN¿W5kRWsN:WA+䤮nذa1^xaZ쎝cƌY`A=ܝ4]_wZZ zk׳g1t'x":uwy%$##_~ ǏEQ>}v|wxu87nlbR/{駓O:tuֱcǝ:ϼzsUםq ' 8p7tS}ڵ$^>KuǫO>9s>ڵkZvԟ.Pɯ{ݺukŋSw. ;8S4h堃aho~SlnpΤN8!^~ q2UwM.pD_~yzOȑ#(Sɧծ];5RwWUɯ^z$8":s_}ҥ:/!;:sR~_矟-4hp'W&###^^bE%G{oG9V#F|wO>u֥oѢE|QUܫsݺucǎv{PڵkqX7bĈW^yegFIK$=o Eoߧt[no.\ظqŋgff2`ĉaÒ[Zjշovڵm۶m۶ŞX6o޼/࣏>zWKڵk۴idɒqFZfMڵ;rٲegy㋽xK>} \;wqF/'nK/=CvZ~(lO~/v[E999-* /\=غuN6?{ov)HƎ;r5k$7vڵSN۷o߾}ws|cƌy7۶m{ǿ(L8S8∦M~WJ7~w;w\?qn{ _zzz"Hi[oE%5m3ۺu̙3L2zo?ϻuVVN|={?KVv'5o޼쯺[(K'yNjd}^2~f͚ӪU{;*-[ c nZPPPPPdɒ 6pG}t^^^^U*uoܸ`ܹu벳4hо}>8zJ~8?U4hpo>77*vM0gIKK uSP|,??N8 T{(uq'|v[/nѢEVVւ 4hP*J>`uo޼.R͈|@wG8uAժUoY#/}- ,86mt51I<^ڵ~#8bƍo߾x;?UVuOv֭+}G{ォeJS]'՜/| +WۼyW_}hѢk6mڴC 4IN$@`ܮ#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|#@`D> 0"F|=; u;!3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$@  `F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`FjHݎ@oH>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`F3f$H>|0#`FiIENDB`paperwork-2.1.1/paperwork-backend/tests/pillow/tests_pdf.py000066400000000000000000000015741417573700700241630ustar00rootroot00000000000000import os import unittest import PIL.Image import openpaperwork_core import openpaperwork_core.fs class TestPillowPdf(unittest.TestCase): def setUp(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("paperwork_backend.pillow.pdf") self.core.init() self.pdf_url = openpaperwork_core.fs.CommonFsPluginBase.fs_safe( os.path.join( os.path.dirname(os.path.abspath(__file__)), "test_doc.pdf" ) ) + "#page=1" self.img_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "test_doc.png" ) def test_pdf_url_to_pillow(self): pdf_as_img = self.core.call_success("url_to_pillow", self.pdf_url) ref_img = PIL.Image.open(self.img_path) self.assertEqual(ref_img.size, pdf_as_img.size) paperwork-2.1.1/paperwork-backend/tests/poppler/000077500000000000000000000000001417573700700217625ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/poppler/__init__.py000066400000000000000000000000001417573700700240610ustar00rootroot00000000000000paperwork-2.1.1/paperwork-backend/tests/poppler/test_doc.pdf000066400000000000000000000230121417573700700242570ustar00rootroot00000000000000%PDF-1.5 %äüöß 2 0 obj <> stream xm 0EY 3I2`kpWw>vbQ3%K<%*&ߒ`Ցr}Ji|xD@ !Pέ&UX\|d=pWb}q/#AQ,J%a]V>DhHN )nH&;z7ND endstream endobj 3 0 obj 181 endobj 5 0 obj <> stream xzkxU=UzXl=,$+JQCI+~۴B,ْ`[BciZW] @~=DN3(y\$Db_/7+]!|S,eWɗ^!=r|I/!2AnA[%z L _Q'T1"Qay9</(܆2J L/7-+6r&AI<^ z 3?6KF9 __l)r辌cSv9B¿j@BMjodcگ}ΫvvZ[wM۷mݲq kUk׬.[_:fH_+j*Rɋi2юJ1ZUFD>ʋRwtP?*^\"tDsx s yN0[V ?/~üxWSX6JoFg"h# [-ªJ2[GP֟ہڶͳ )(őEcb*wVJ"-TiT%?&Ns35H!ElegضDsXoo#6Bڹ'O.AT ᣥєH$PdZD.W}=3L7gf dQمq{¢)2 Ѓ{:ŒkE,ȏFM~&Ϝ }[9aOrÑĆ8/y2zD$\b(9J^d+DԎ )$`~tPK|?GyQKcC8\U"gɹL1XZ\͎e92?S왑-))Mf] ǵ7ᔦzfV,];ezZZ)QPe7PXr " u:X] g/# Bisu fvjJ=,w%7p8F kHu]M~(j=ys:^PkkcX}puee*`pX,W;lf kf L 9 h#PaA:Kc@v>i9)@2J%`ۡ Wyٰ`no<ݩl$5':~Ugǁ|΁{[j7pVOc-;X}WYJR:ŘUp]XSX\-aYCeBaVo9m1hr@!)K:FKcmCjL lu6يqh+x=ۭ5o;xGiK}B1ld9*uK lF$h׉v:   [Luwy@ x |?W;G!+>;mv5nJS|[).!"ki HΚa66Y'N8NH:4W,DL7c2Z'fl'odMO^pO_e]y))pn)ggbJwRV;$,@YL"sŤNm6kt:FV;hTr O8'L;!ㄘTN^':u'䄼9'j3TC%B 7,%Y.4N|/)k)uSϿ)_m^f|ϽS<ƉlqaN'. X^Rr\NW(k5fKPXTnݐqC nnx /H\$#X)u5{ 7[UVQλu7pôn6MoIQ6ZV|Il0_ɁQ(4^AZvih~OV4+Yϗz-/|r k7y\H: Y5:^=<p2zazP1(1=+z!Aszp6Mz zHȷmReI^YqB)R$z ] C-h4"Vl iRj԰ЁCmmA,ZmzxPmUs61BV=6kbŸgJb\ǁ9xqp9 8XM.sOs|Gp&?Qp 69XŁ2⋵3Fa`q([T l<`/jiuP!GS y띱*5oS^1uQiCC]gg%XهVnʬՂ`4f,c5BaQk(r6@^q'M:\̹2FkVjUv<0UlvEխ1L\ ,km췑`:n <0`sM͂iO5=<Pyx じz=z򀋒a;X|)/Du76R٘? (!zN`_gpIqZhnbw=R](\ip'aɈQ31zL<;ɧA(8y3Wv+B)V(جt`wnɔN:{o]nx0o7Q7G[wfSdFX0JXHeѻ%fC1Vl$0KV`=p 4x?=mrAZ!^xx>Ni)JXh\?Q~̰38Oy`{r \?oǙlL<.|c==RmyAXe(#u>ݻ<`+ViZ8-bX,e^5WG6ӓɕ5dr"%]wc}΍G"pa`;"lJľClk>܏o>y*y'>DD&AlHX-f 5Ψ0c快Zӹ!stHЯMR+}Ŵ\˟přE=`J%=9ŨW#]͞{TBWŔ{Oq5b7[j=c(l(0qV֊#.8Lp@p,=o ʹX陕ݽ߹ƊL}㍫f?=Ȏ 5̍ƭd/ژ b58XMHlb̒4 7LEi$2GIddf~H_DDPjld4ï*kkxoIT43.lYVAL%srz`\{⩱=hjGz(>*~95TZjVT7\.gKQ> |bx!|*>2S{P4ߛSP<"s"3^?KƆ,rGO&~ _dds4}ect%ptlh?Mxzld2- Y3ь4x&56?1H Xf;Dtղ)at*?6L%PCx|;Ƣcc6MEc趱4:OF'ڦRd-F+htb,qO1G4@|Di<ÉˌV-|81ApԄ'ts&g\t(@Zrxflq>V$+ Z";J(gJKY6Hb_FL>(VJ23M>xj}OQu9HP!ď2&+r_L:H2 J&)+4n4W"Ͱ4Ϛ!eT+6r]I#emܥZ4s5h]/J4Hf[lr6a;CIS_V1 {Ehz-Ц/fu'0BJ"d7 >Aoinmd!~;"~ .^|6݅x9j#Үv%JOmB nWAķMi6IҏyTi8???g0ɱO?\,>}EくC`p₦o{#{{W,5s5sɹ]+鱟2y{l)0>} =y9vǽG>~5^|90w"sy]dpiv\2Ӌw.xߋ7{݋wv ؁}*#wNyNvcw0O8wIʽ d:ѧc4 .,[ dvowo{ҧz&MXmۍ#tb.>vU؝roG&h5 B<_pEg31#}ゑ14f5c=g {:łе"-HO{[$}{NGIS#pC@iLY4L)| DOX4qJS!6B!Bޟ&C"VBtZQGg endstream endobj 6 0 obj 7404 endobj 7 0 obj <> endobj 8 0 obj <> stream x]Mn0t DBH)I$Qi"cV陰ΕՍYtn d,t浢U B[/ Ce1σ͟M[ipGY[YDAQ fl}˟} b%[Q6 \cnQTz-0YlJ/}Rxg;=q!kh΁GOqBGd=D]+<ˈ}2OwΚƄPw|ؔ9ho; endstream endobj 9 0 obj <> endobj 10 0 obj <> endobj 11 0 obj <> endobj 1 0 obj <>/Contents 2 0 R>> endobj 4 0 obj <> endobj 12 0 obj <> endobj 13 0 obj < /Producer /CreationDate(D:20190902205629+02'00')>> endobj xref 0 14 0000000000 65535 f 0000008715 00000 n 0000000019 00000 n 0000000271 00000 n 0000008884 00000 n 0000000291 00000 n 0000007780 00000 n 0000007801 00000 n 0000007996 00000 n 0000008384 00000 n 0000008628 00000 n 0000008660 00000 n 0000008983 00000 n 0000009080 00000 n trailer < ] /DocChecksum /72B44A81E5EEF3D98178B73CE31BA672 >> startxref 9255 %%EOF paperwork-2.1.1/paperwork-backend/tests/poppler/tests_file.py000066400000000000000000000046661417573700700245110ustar00rootroot00000000000000import cairo import gc import os import psutil import unittest import openpaperwork_core class TestFileDescriptorLeak(unittest.TestCase): @unittest.skipUnless(os.name == 'posix', reason="Linux only") def test_fd_leak(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.fs.python") self.core.load("paperwork_backend.poppler.file") self.core.init() self.simple_doc_url = self.core.call_success( "fs_safe", os.path.join( os.path.dirname(os.path.abspath(__file__)), "test_doc.pdf" ) ) gc.collect() gc.collect() current_fds = list(psutil.Process().open_files()) doc = self.core.call_success("poppler_open", self.simple_doc_url) self.assertIsNotNone(doc) new_fds = list(psutil.Process().open_files()) self.assertNotEqual(len(current_fds), len(new_fds)) doc = None gc.collect() gc.collect() new_fds = list(psutil.Process().open_files()) self.assertEqual(len(current_fds), len(new_fds)) @unittest.skipUnless(os.name == 'posix', reason="Linux only") def test_fd_leak2(self): self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.fs.python") self.core.load("paperwork_backend.poppler.file") self.core.init() self.simple_doc_url = self.core.call_success( "fs_safe", os.path.join( os.path.dirname(os.path.abspath(__file__)), "test_doc.pdf" ) ) gc.collect() gc.collect() current_fds = list(psutil.Process().open_files()) doc = self.core.call_success("poppler_open", self.simple_doc_url) self.assertIsNotNone(doc) page = doc.get_page(0) new_fds = list(psutil.Process().open_files()) self.assertNotEqual(len(current_fds), len(new_fds)) surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 200, 200) ctx = cairo.Context(surface) page.render(ctx) new_fds = list(psutil.Process().open_files()) self.assertNotEqual(len(current_fds), len(new_fds)) page = None doc = None gc.collect() gc.collect() new_fds = list(psutil.Process().open_files()) self.assertEqual(len(current_fds), len(new_fds)) paperwork-2.1.1/paperwork-backend/tests/poppler/tests_memory.py000066400000000000000000000031371417573700700250720ustar00rootroot00000000000000import cairo import gc import os import unittest import openpaperwork_core class TestMemoryDescriptorLeak(unittest.TestCase): @unittest.skipUnless(os.name == 'posix', reason="Linux only") def test_leak(self): self.tracking = False self.disposed = False class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): def on_dispose(s): self.disposed = True def on_objref_track(s, obj): self.tracking = True obj.weak_ref(s.on_dispose) self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.fs.python") self.core.load("paperwork_backend.poppler.memory") self.core._load_module("fake_module", FakeModule()) self.core.init() self.simple_doc_url = self.core.call_success( "fs_safe", os.path.join( os.path.dirname(os.path.abspath(__file__)), "test_doc.pdf" ) ) gc.collect() gc.collect() doc = self.core.call_success("poppler_open", self.simple_doc_url) self.assertIsNotNone(doc) self.assertTrue(self.tracking) self.assertFalse(self.disposed) page = doc.get_page(0) surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 200, 200) ctx = cairo.Context(surface) page.render(ctx) self.assertFalse(self.disposed) page = None doc = None gc.collect() gc.collect() self.assertTrue(self.disposed) paperwork-2.1.1/paperwork-backend/tests/tests_datadirhandler.py000066400000000000000000000050171417573700700250460ustar00rootroot00000000000000import shutil import tempfile import unittest import os import openpaperwork_core class TestDirHandler(unittest.TestCase): def setUp(self): self.tmp_paperwork_dir = tempfile.mkdtemp( prefix="paperwork_backend_tests" ) self.core = openpaperwork_core.Core(auto_load_dependencies=True) class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 999999999999999999999999999 def paths_get_data_dir(s): return self.core.call_success( "fs_safe", self.tmp_paperwork_dir ) self.core.load("paperwork_backend.model.fake") self.core.load("paperwork_backend.datadirhandler") self.core._load_module("fake_module", FakeModule) self.datadirhandler = self.core.get_by_name( "paperwork_backend.datadirhandler" ) self.core.init() def tearDown(self): shutil.rmtree(self.tmp_paperwork_dir) def test_init(self): data_dir_hashed = self.core.call_success( "data_dir_handler_get_individual_data_dir") self.assertTrue(self.core.call_success("fs_exists", data_dir_hashed)) self.datadirhandler._delete_old_directories( days_to_data_dir_deletion=2) # after deleting old directories with the default value, # the created directory should still be there self.assertTrue(self.core.call_success("fs_exists", data_dir_hashed)) self.datadirhandler._delete_old_directories( days_to_data_dir_deletion=0) self.assertFalse(self.core.call_success("fs_exists", data_dir_hashed)) def test_dir_hash(self): data_dir_hashed = self.core.call_success( "data_dir_handler_get_individual_data_dir") self.assertTrue(self.core.call_success("fs_exists", data_dir_hashed), "directory %s should exist but it does not" % data_dir_hashed) workdir = self.core.call_success('storage_get_id') self.assertEqual( os.path.basename(workdir), os.path.basename(data_dir_hashed).split("_")[0] ) data_dir_wo_hash = self.core.call_success("paths_get_data_dir") self.assertEqual(os.path.commonprefix( [data_dir_wo_hash, data_dir_hashed] ), data_dir_wo_hash) data_dir_hashed_again = self.core.call_success( "data_dir_handler_get_individual_data_dir") self.assertEqual(data_dir_hashed, data_dir_hashed_again) paperwork-2.1.1/paperwork-backend/tests/tests_pagetracker.py000066400000000000000000000064621417573700700243750ustar00rootroot00000000000000import shutil import tempfile import unittest import openpaperwork_core class TestPageTracker(unittest.TestCase): def setUp(self): self.tmp_paperwork_dir = tempfile.mkdtemp( prefix="paperwork_backend_tests" ) self.core = openpaperwork_core.Core(auto_load_dependencies=True) class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 999999999999999999999 def data_dir_handler_get_individual_data_dir(s): return openpaperwork_core.fs.CommonFsPluginBase.fs_safe( self.tmp_paperwork_dir ) self.core._load_module("fake_module", FakeModule) self.core.load("paperwork_backend.model.fake") self.core.load("paperwork_backend.pagetracker") self.fake_storage = self.core.get_by_name( "paperwork_backend.model.fake" ) self.core.init() def tearDown(self): shutil.rmtree(self.tmp_paperwork_dir) def test_tracking(self): self.fake_storage.docs = [ { 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'page_hashes': [ ('file:///somewhere/test_doc/0.jpeg', 123), ('file:///somewhere/test_doc/1.jpeg', 124), ], }, { 'id': 'test_doc_2', 'url': 'file:///somewhere/test_doc_2', 'page_hashes': [ ('file:///somewhere/test_doc_2/0.jpeg', 125), ('file:///somewhere/test_doc_2/1.jpeg', 126), ('file:///somewhere/test_doc_2/2.jpeg', 127), ], }, ] tracker = self.core.call_success("page_tracker_get", 'test_tracking') out = tracker.find_changes('test_doc', 'file:///somewhere/test_doc') self.assertEqual(out, [('new', 0), ('new', 1)]) tracker.ack_page('test_doc', 'file:///somewhere/test_doc', 0) tracker.ack_page('test_doc', 'file:///somewhere/test_doc', 1) out = tracker.find_changes( 'test_doc_2', 'file:///somewhere/test_doc_2' ) self.assertEqual(out, [('new', 0), ('new', 1), ('new', 2)]) tracker.ack_page('test_doc_2', 'file:///somewhere/test_doc_2', 0) tracker.ack_page('test_doc_2', 'file:///somewhere/test_doc_2', 1) tracker.ack_page('test_doc_2', 'file:///somewhere/test_doc_2', 2) tracker.commit() self.fake_storage.docs = [ { 'id': 'test_doc', 'url': 'file:///somewhere/test_doc', 'page_hashes': [ ('file:///somewhere/test_doc/0.jpeg', 256), ('file:///somewhere/test_doc/1.jpeg', 124), ('file:///somewhere/test_doc/2.jpeg', 257), ], }, ] tracker = self.core.call_success("page_tracker_get", 'test_tracking') out = tracker.find_changes('test_doc', 'file:///somewhere/test_doc') self.assertEqual(out, [('upd', 0), ('new', 2)]) tracker.ack_page('test_doc', 'file:///somewhere/test_doc', 0) tracker.ack_page('test_doc', 'file:///somewhere/test_doc', 2) tracker.delete_doc('test_doc_2') tracker.commit() paperwork-2.1.1/paperwork-gtk/000077500000000000000000000000001417573700700163355ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/COPYING000066400000000000000000001045131417573700700173740ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . paperwork-2.1.1/paperwork-gtk/ChangeLog000066400000000000000000000424361417573700700201200ustar00rootroot000000000000002022/01/31 - 2.1.1: - drop unused dependency on dateutil - Doc deletion: Fix broken spanish translation: Wrong Python formatter has been used - model.help.intro: Handle gracefully the case where the package maintainer forgot to include the introduction documents 2021/12/05 - 2.1.0: - Responsive UI (thanks to Mathieu Jourdan) - Add support for password-protected PDF files - Export: add a button sendig the document by mail instead of exporting to disk (thanks to Guillaume Girol) - Add confirmation when deleting label from all documents - Add option to redo ocr on many documents in one shot - Add option to move a page from one document to another (drag'n'drop isn't obvious to all users) - Page menu: add an option to move pages inside a document (drag'n'drop isn't obvious to all users) - Mouse middle button can now be used to scroll in a document (auto-scrolling) - Add dependency on Libhandy 2021/05/24 - 2.0.3: - GTK: (may be important for package maintainers): "make data" --> "screenshot.sh": Download the test documents from a Git repository instead of a .tar.gz. See Gitlab issue #961. - GTK: Doc/page export: Work around bug in Gnome platform 40 (Flatpak) (caused a full crash of Paperwork ; Segfault) - GTK: Doc/page export: Fix use of GtkFileChooserNative so Paperwork can exit correctly after its use. - GTK: Doc renaming (date change): Prevent the user from giving an invalid name to a document - GTK: Doc/page export: Handle gracefully errors (for instance Permission Denied) - GTK: word boxes loading: Add missing progress notification - GTK: Import: When importing in an existing document, refresh correctly the GUI is the target document is currently opened. - GTK: LaTeX User manual: various fixes suggested by tomf on Weblate - Swedish translations added 2021/01/01 - 2.0.2: - pageview.boxes: add workaround regarding unexpected Poppler exceptions on some PDF documents (see https://gitlab.freedesktop.org/poppler/poppler/-/issues/1020 ) - add a command "paperwork-gtk import": Open the GUI and import the documents given as arguments (thanks to Guillaume Girol) - desktop file: associate "paperwork-gtk install" to PDF and images files, so users can import files directly from their file browser (thanks to Guillaume Girol) - add plugin 'sync_on_start': Start the full synchronization when the GUI is started, but only if the configuration allows it. (Before this plugin, synchronization was started directly in the 'main.py') - advanced search: prevent the user from opening the dialog twice (not supported) - Help documents: Fix for people who seem to have no locale configured (?!) - settings/scanner/flatpak: Fix the dialog explaining how to enable Flatpak: Once the dialog has been closed and destroy, it could not be opened correctly again. - drawer.scan: Workaround regarding scanners reporting an image too big for Cairo ImageSurface (some Fujitsu scanners) - settings/storage: when opening the file chooser to select the work directory, pre-select the current work directory - GTK: boxes.search: do not highlight the keywords "and" and "or". They are used in Whoosh query syntax, but it's useless to highlight them in documents. 2020/11/15 - 2.0.1: - take into account that document IDs may not be the expected date+time format (no uncaught exception should be raised in that case) - doclist: Fix: When the document list, scroll to the currently-opened document. - settings/calibration: Disable the calibration settings button if no scanner is selected - update notifications: make sure the funny strings are displayed translated - Include tests in Pypi package (thanks to Elliott Sales de Andrade) 2020/10/17 - 2.0: - Full rewrite - Use of plugin system of openpaperwork_core to split features - PDF can be edited - Pages can be reinitialized to their initial states (reset) - Settings dialog have been revamp - Scan source selection is done in the main window now instead of the settings dialog - No multi-scan dialog anymore: clicking "scan" always scan until the end of feed - An automatic bug report submission system has been added 2019/12/20 - 1.3.1: - Add spanish translations (thanks to Iñigo Figuero) - Frontend: Fix multi-scan dialog / scanning from feeder - Frontend: When looking for the scanner, if the exact ID is not found, fix fall back code - Frontend: About dialog: Add patrons 2019/08/17 - 1.3.0: - Switch from Pyinsane2 to Libinsane - Replace application menu by primary menu (new Gnome recommendation) (thanks to Mathieu Jourdan) - Remove documentations 'hacking' and 'translating'. They are in the Wiki now. - Fix: Make sure the main window is restored at its previous size correctly each time Paperwork starts. - Settings window: If no scanner has been found and we are running inside a Flatpak container, show a popup to the user explaining how to enable and configure Saned. - Drop custom heuristic page orientation detection. Only use tool orientation detection. If it fails, default to the original orientation. - Fix: Ignore word boxes starting at (0, 0) (Tesseract bug ?) (thanks to Jonas Wloka and Balló György) - Install icons in the correct hicolor sub-directory (thanks to Elliott Sales de Andrade) - Fix warnings related to regexes escaping + various other cleanups (thanks to Elliott Sales de Andrade) 2018/03/01 - 1.2.4: - Main window/pages/mouse handlers: Fix infinite loop in signal handling when the mouse goes over buttons drawn over pages. 2018/02/01 - 1.2.3: - Flatpak: Fix support of other GTK themes (Dark Adwaita, etc) - French translations: shorten the translation of "Matching papers" because otherwise it messes with the UI - Export dialog: Clicking on export->{document|page} a second time will first close the first export dialog. 2017/11/14 - 1.2.2: - DnD: Fix double-delete: when moving the last page of a document to another, don't delete the source document (it's up to the backend code to delete it) - Flatpak support - CSS: Add border around application button to make it more visible 2017/08/26 - 1.2.1: - Add source code of Windows installer (NSIS installer) generator - Scanner support / Multi-scan: Cancel also successful scan session. Otherwise some scanner won't allow new scan sessions later. - Remove gi version warnings when starting (thanks to Matthieu Coudron) - Documentation: Add missing stdeb dependencies (thanks to Notkea) - paperwork-shell: Fix command 'scan' - paperwork-shell install: add docstring - Fix dialog 'about' 2017/07/11 - 1.2.0: - Installation: A new command has been added: "paperwork-shell install". This command installs icons and shortcut in the desktop menus. - Add integrated documentations: + Introduction to Paperwork (added to the documents when Paperworks starts for the first time) + User manual (not complete yet) + Developer's guide + Translator's guide - Text in pages can be selected - Text in pages can be copied in clipboard - Automatically look for updates (disabled by default ; see settings) - Send anonymous usage statistics (disabled by default ; see settings) - Export: Add simplification methods 'grayscale', 'black and white', and 'grayscale + soft'. They produce much smaller documents - setup.py: Properly package resources files (glade, images, css) and load them using pkg_resources. (thanks to Alexandre Vaissière) - Import: After import, propose the user to move files to trash after import (thanks to Mathieu Schopfer) - Import: Allow selecting multiple files in the file chooser dialog - Import: Clearly show in the file chooser dialog which file formats are supported - Import: Use notifications instead of popups - Virtualenv: also look for translation files (thanks to Alexandre Vaissière) - Export: Remove the text field + save button ; request the target file location when the user actually clicks on 'export' - Export: Display a notification when exports are finished - Search: When searching, display a search bar to browse the document(s) - Search: new keyboard shortcuts: F3 (next) + Shift+F3 (previous) - Settings: Remove "Disable OCR" from the language list. It's redundant with the check box above the language list - non-Gnome / non-Unity environements : Move the application menu to the left headerbar (top left of the main window) - paperwork-shell chkdeps: look for libsane too - localize.sh: Also include the translations for paperwork-backend - Settings: Use pycountry to translate the language names + remove 'equ' and 'osd' - GUI: change the way left panes are switched to reduce issues with GTK+ - Internal: Switch everything to URIs (required for correct Gio use) - Devel: Add basic command line arguments (thanks to Mathieu Coudron): + -d for debug output + -v for version 2017/02/09 - 1.1.2: - Doc date changing: Fix for Windows: Don't display the document while renaming its folder --> it keeps a file descriptor opened to its PDF file and prevent the renaming 2017/02/05 - 1.1.1: - Fix document list refresh problem (mostly visible on Ubuntu) 2017/01/30 - 1.1.0: - Windows: Activation mechanism has been disabled for now - Workarounds for Gtk-3.20.x / GLib 2.50 (Ubuntu): - Work around weird behavior of GLib.idle_add (multiple calls) - Work around lack of refresh of document list - Import: Display how many image files, PDFs, documents and pages have been imported. - Automatic Color Equalization: Reduce the 'circle side-effect' by increasing the number of samples used. - paperwork-shell scan: Quit after scanning - Settings window: "Source" becomes "Default source" (cosmetic) - Export: Don't lock the UI - Export: Display the progression of the export - Improve keyword highlighting: Highlight words identical to search keywords (as before) and also words close enough (example: 'flesh' when 'flesch' is being search) - Optim: Document list: Only display display the first 100 elements of the list, and extend it only when required. Reduces GTK latency (GtkListBox doesn't scale very well above 100 elements). - Optim: Improve PDF rendering speed: Let the libpoppler take care of the rendering size (see backend:page.get_image()) - Optim: Reduce the number of useless calls to redraw() 2016/12/04 - 1.0.6: - Diagnostic: Limit the number of lines kept in memory (avoid running out of memory in case of endless loop) - Diagnostic: Log all the uncaught exceptions - PDF import: When importing a big PDF, clearly show the progression of the import - Multiple document selection: Fix the way Ctrl/Shift keys are handled (bug was that multiple selection mode remained stuck sometimes) 2016/11/22 - 1.0.5: - Setting the resolution on the scanner may not actually work. If it is not possible to set the resolution, fall back on the current one. - Improve tolerance to crappy scanner drivers: don't stop if pyinsane2.maximize_scan_area() fails 2016/11/18 - 1.0.4: - Windows: Fix import error dialog - Windows: Fix GtkLinkButtons (didn't do anything when clicked) 2016/11/17 - 1.0.3: - Windows: Fix opening of export dialog - Application menu button: Make its style consistent with the other buttons in the header bar - Label list: Add a button to delete labels - Label editor: Fix the reloading of the label list when a label has been changed - Label editor/Color picker: Fix the switch of the mouse cursor to a pipette - Small Paperwork icons: add a discrete blue background to make the icon more visible - Main window: When on "new document", disable the page number entry field + the view settings button - Fix icons (application icon, main window, about dialog) 2016/11/13 - 1.0.2: * Fix export dialog: - Don't use GtkWidget.set_visible(False) / GtkWidget.set_visible(True) anymore to avoid weird GTK behavior when reopening - Fix endless loop that occured with some versions of the GLib * French translations: "Scanner" --> "Numeriser" * Windows support: Fix translations support * CSS: Add small padding to make sure the GtkEntry and GtkButton in the header bar all have the same heights on all environments. * Fix menu icon: Add PNG versions of the PAperwork icon. 2016/11/10 - 1.0.1: * Config: Fix pycountry db lookup (prevent Paperwork's first start) * Pyinstaller: Add .ico + .png in the package 2016/11/09 - 1.0: * Export: generated PDF now includes the text from the OCR * New command 'paperwork-shell scan' that starts Paperwork and immediately tries to scan a page * 'paperwork-chkdeps' has been replaced by 'paperwork-shell' * Export: Add an option to automatically simplify the content (makes it smaller in size) * Import: Display a popup when the import fails * Page editing: Add an option to adjust automatically colors * Page editing: Fix display when making many edit operations at once (Rotation Cropping + ...) * When starting, instead of displaying an empty document, display Paperwork's logo and the version (if different of "1.0") * Improve zooming/unzooming with Ctrl+MouseWheel (try to target the mouse cursor) * Allow scrolling using the middle click * Support for pyinstaller packaging * Fix running the OCR while scanning at the same time * Split backend and frontend (separate Python packages and separate Git repositories) * Handle very long label names more gracefully * Word box highlighting: Highlight correctly all the boxes * Fix spinner animation when getting an icon size other than the expected one * Switch to Python 3 * Switch from Pyinsane to Pyinsane2 * Fix file descriptor leak related to PDFs * Add a dialog to help bug diagnostics * Replace gnome spinner by a custom spinner 2016/04/06 - 0.3.2: * paperwork-chkdeps: Fix check for python-gi-cairo. When python-gi-cairo is not installed, sometimes, an exception pops up at an unexpected moment and the script remained stuck. * Add Dockerfile to generate a docker image+container to test Paperwork 2016/02/25 - 0.3.1.1: * Fix crappy dependency list 2016/02/25 - 0.3.1: * Fix label learning * Fix headerbar widget sizes 2016/02/16 - 0.3.0.1: * Fix Paperwork packaging (.css files were not included) 2016/02/15 - 0.3.0: * Whole GUI redesigned * Added: dialog to make advanced searches * New dependency: simplebayes * Removed dependencies: - scikit* (replaced by simplebayes) - numpy* (replaced by simplesbayes) - gir1.2-gladeui (obsolete) 2015/11/25 - 0.2.5: * Scanner support: Fujitsu scanners: handle options 'page-height' and 'page-width' * Scanner support: Brother MVC-J410: set mode correctly (value = '24bit Color' instead of 'Color' ...) * Documents: add support for new label format that will be used in Paperwork >= 0.3.0 * paperwork-chkdeps: look for required icon themes * Fix: work even if the spinner icon is not available * Fix: paperwork-chkdeps: work even if Gtk is not yet installed * Fix: PDF: reduce file descriptor leak * Fix: With Pillow >= 3.x, calls to Image.rotate() must specify expand=True * Fix: At startup, when updating the index, prevent infinite loop 2015/04/21 - 0.2.4: * Fix python-whoosh 2.7 support 2015/04/03 - 0.2.3: * Whenever possible, page orientation detection is now done using OCR tool feature (Tesseract >= 3.3.0). It's much faster and reliable. * Fix doc indexation: last and first words of each lines weren't split correctly 2015/01/11 - 0.2.2: * PDF + OCR: text wasn't indexed correctly * Img doc: indexed text contains extra and useless data. As a side-effect, label prediction accuracy was strongly reduced. Rebuilding your index is strongly recommended ("rm -rf ~/.local/share/paperwork" + restarting Paperwork) 2014/12/19 - 0.2.1: * Settings window : add help links * Install process : - Extra dependencies are now detected by another script than setup.py - More missing dependencies are detected (aspell, tesseract, language packs, etc) * Bug fixes : - Button 'open parent directory' doesn't remain stuck anymore when using the file manager Thunar - Settings window : Fix the way the file chooser is used (avoid selecting the wrong work directory by mistake) - Scanners support : Make it possible to use scanners even if some basic options are missing (source, resolution, etc) - When starting, don't remove empty directories anymore - Searching : Make sure diacritics characters are not a problem anymore - Import : accept file path containing spaces 2014/09/21 - 0.2: * Improved search : whoosh.FuzzyTerm is now used * Label look has been improved * Menubar has been removed and replaced by an application menu * Label prediction : when a new document is scanned, predicted labels are automatically set on it * Pages are not displayed separately anymore * New settings: scan source, number of orientations to try, OCR can be disabled * Scans are displayed in real time 2014/07/08 - 0.1.3: - Fix scanner support : don't try to set scanner options that are not active 2013/12/29 - 0.1.2: - Improve scanner support: option names and values cases are not always the same on all the scanners - Multiscan: fix multiscan end - Translations: add german translations - Settings window: display correctly Tesseract languages like 'deu-frak' 2013/10/03 - 0.1.1: - Page list: fix display of page list longer than 100 pages - Scanner support: - Fix support of scanners returning the supported resolutions as a range instead of an array - Fix: Always make sure the scan area is as big as it can be - Fix: When OCR is disabled, fix scan and page editing - Fix "no scanner found" popup (partial backport only, still slightly buggy) - Scripts: - Add script scripts/obfuscate.py - Fix/Improve the output of scripts/stats.py 2013/08/08 - 0.1: - Initial release paperwork-2.1.1/paperwork-gtk/MANIFEST.in000066400000000000000000000002421417573700700200710ustar00rootroot00000000000000recursive-include src *.py *.glade *.xml *.css *.svg *.png *.pdf *.mo recursive-include tests * include *.markdown include example-paperwork.conf include COPYING paperwork-2.1.1/paperwork-gtk/Makefile000066400000000000000000000072321417573700700200010ustar00rootroot00000000000000VERSION_FILE = src/paperwork_gtk/_version.py PYTHON ?= python3 build: build_c build_py install: install_py install_c uninstall: uninstall_py build_py: ${VERSION_FILE} l10n_compile ${PYTHON} ./setup.py build build_c: version: ${VERSION_FILE} ${VERSION_FILE}: echo "# -*- coding: utf-8 -*-" >| $@ echo -n "version = \"" >> $@ echo -n $(shell git describe --always) >> $@ $(eval branch_name = $(shell git symbolic-ref HEAD 2>/dev/null)) if [ -n "${branch_name}" ] && [ "${branch_name}" != "refs/heads/master" ] ; then echo -n "-${branch_name}" >> $@ ; fi echo "\"" >> $@ doc: upload_doc: data: $(MAKE) -C src/paperwork_gtk/icon data $(MAKE) -C src/paperwork_gtk/model/help data check: flake8 --exclude=_version.py src/paperwork_gtk test: install python3 -m unittest discover --verbose -s tests windows_exe: ${PYTHON} /mingw64/bin/pip3-script.py install . ${PYTHON} ./setup.py build_exe mkdir -p $(CURDIR)/../build/exe mv $$(find $(CURDIR)/build -type d -name exe\*)/* $(CURDIR)/../build/exe # ugly, but "import pkg_resources" doesn't work in frozen environments # and I don't want to have to patch the build machine to fix it every # time. mkdir -p $(CURDIR)/../build/exe/data # We need the .ico at the root of the data/ folder # The installer makes a desktop icon that expect paperwork_64.ico there, # and since we use the same installer for all versions (master, testing, # unstable, etc), we can't change this path yet. cp $(CURDIR)/src/paperwork_gtk/data/*.ico $(CURDIR)/../build/exe/data (cd $(CURDIR)/src && find . -name '*.css' -exec cp --parents \{\} $(CURDIR)/../build/exe/data \; ) (cd $(CURDIR)/src && find . -name '*.glade' -exec cp --parents \{\} $(CURDIR)/../build/exe/data \; ) (cd $(CURDIR)/src && find . -name '*.mo' -exec cp --parents \{\} $(CURDIR)/../build/exe/data \; ) (cd $(CURDIR)/src && find . -name '*.pdf' -exec cp --parents \{\} $(CURDIR)/../build/exe/data \; ) (cd $(CURDIR)/src && find . -name '*.png' -exec cp --parents \{\} $(CURDIR)/../build/exe/data \; ) (cd $(CURDIR)/src && find . -name '*.ico' -exec cp --parents \{\} $(CURDIR)/../build/exe/data \; ) linux_exe: release: ifeq (${RELEASE}, ) @echo "You must specify a release version (make release RELEASE=1.2.3)" exit 1 else @echo "Will release: ${RELEASE}" @echo "Checking release is in ChangeLog ..." grep ${RELEASE} ChangeLog | grep -v "/xx" @echo "Checking release is in work.openpaper.Paperwork.appdata.xml ..." grep ${RELEASE} src/paperwork_gtk/data/work.openpaper.Paperwork.appdata.xml endif release_pypi: @echo "Releasing paperwork-gtk (paperwork) ..." ${PYTHON} ./setup.py sdist twine upload dist/paperwork-${RELEASE}.tar.gz @echo "All done" clean: rm -f ${VERSION_FILE} rm -rf build dist src/*.egg-info $(MAKE) -C src/paperwork_gtk/model/help clean $(MAKE) -C src/paperwork_gtk/icon clean # PIP_ARGS is used by Flatpak build install_py: ${VERSION_FILE} l10n_compile ${PYTHON} ./setup.py install ${PIP_ARGS} install_c: uninstall_py: pip3 uninstall -y paperwork uninstall_c: l10n_extract: $(CURDIR)/../tools/l10n_extract.sh "$(CURDIR)/src" "$(CURDIR)/l10n" $(MAKE) -C src/paperwork_gtk/model/help l10n_extract l10n_compile: $(CURDIR)/../tools/l10n_compile.sh \ "$(CURDIR)/l10n" \ "$(CURDIR)/src/paperwork_gtk/l10n" \ "paperwork_gtk" help: @echo "make build || make build_py" @echo "make check" @echo "make help: display this message" @echo "make install || make install_py" @echo "make uninstall || make uninstall_py" @echo "make release || make release_pypi" .PHONY: \ build \ build_c \ build_py \ check \ doc \ exe \ help \ install \ install_c \ install_py \ l10n_extract \ release \ release_pypi \ test \ uninstall \ uninstall_c \ version paperwork-2.1.1/paperwork-gtk/README.markdown000066400000000000000000000000731417573700700210360ustar00rootroot00000000000000## Paperwork-gtk All the code dependant on GTK goes here. paperwork-2.1.1/paperwork-gtk/l10n/000077500000000000000000000000001417573700700171075ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/l10n/de.po000066400000000000000000000614211417573700700200430ustar00rootroot00000000000000# German translations for PACKAGE package. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2021-11-29 21:07+0000\n" "Last-Translator: LAZIC Anna <0.0.0.0.0.ffff.255.255.255.255@gmail.com>\n" "Language-Team: German \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.9\n" #: paperwork-gtk/src/paperwork_gtk/cmd/install.py:40 msgid "Install Paperwork icons and shortcuts" msgstr "Installiere Paperwork Icons und Shortcuts" #: paperwork-gtk/src/paperwork_gtk/cmd/install.py:44 msgid "Install everything only for the current user" msgstr "Installiere alles nur für den aktuellen Benutzer" #: paperwork-gtk/src/paperwork_gtk/cmd/import.py:35 msgid "Run Paperwork and import files passed as arguments into a new document" msgstr "" "Führe Paperwork aus und importiere die als Argument übergebenen Dateien in " "ein neues Dokument" #: paperwork-gtk/src/paperwork_gtk/cmd/import.py:39 msgid "URLs or paths of files to import" msgstr "URLs oder Pfade zu Dateien für den Import" #: paperwork-gtk/src/paperwork_gtk/main.py:201 msgid "command" msgstr "Kommando" #: paperwork-gtk/src/paperwork_gtk/docimport.py:86 #, python-format msgid "Don't know how to import '%s'. Sorry." msgstr "" "Es tut mir leid, ich weiß nicht wie ich die Datei '%s' importieren soll." #: paperwork-gtk/src/paperwork_gtk/docimport.py:105 msgid "PDF password" msgstr "PDF Passwort" #: paperwork-gtk/src/paperwork_gtk/docimport.py:123 msgid "No new document to import found" msgstr "Keine neuen Dokumente zum importieren gefunden" #: paperwork-gtk/src/paperwork_gtk/docimport.py:163 msgid "Imported file(s) deleted" msgstr "Importierte Datei(en) gelöscht" #: paperwork-gtk/src/paperwork_gtk/docimport.py:170 msgid "Imported:\n" msgstr "Importiert:\n" #: paperwork-gtk/src/paperwork_gtk/docimport.py:176 msgid "Import successful" msgstr "Import erfolgreich" #: paperwork-gtk/src/paperwork_gtk/docimport.py:185 msgid "Delete imported files" msgstr "Lösche importierte Dateien" #: paperwork-gtk/src/paperwork_gtk/print.py:42 #, python-brace-format msgid "Loading {doc_id} p{page_idx} for printing" msgstr "Lade {doc_id} p{page_idx} zum Drucken" #: paperwork-gtk/src/paperwork_gtk/print.py:144 #, python-format msgid "Printing %s" msgstr "Drucke %s" #: paperwork-gtk/src/paperwork_gtk/print.py:179 #, python-brace-format msgid "Printing {doc_id} ({page_idx}/{nb_pages})" msgstr "Drucke {doc_id} ({page_idx}/{nb_pages})" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/copy_text.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/edit.py:30 msgid "Page" msgstr "Seite" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/copy_text.py:30 msgid "Copy selected text to clipboard" msgstr "Markierten Text in die Zwischenablage kopieren" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/edit.py:30 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/actions.glade.h:1 msgid "Edit" msgstr "Bearbeiten" #: paperwork-gtk/src/paperwork_gtk/shortcuts/app/find.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/new.py:30 msgid "Global" msgstr "Global" #: paperwork-gtk/src/paperwork_gtk/shortcuts/app/find.py:30 msgid "Find" msgstr "Suchen" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/properties.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/print.py:30 msgid "Document" msgstr "Dokument" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/properties.py:30 msgid "Edit document properties" msgstr "Dokumenteneigenschaften bearbeiten" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:35 msgid "Document list" msgstr "Dokumentenliste" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:30 msgid "Open next document" msgstr "Öffne nächstes Dokument" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:35 msgid "Open previous document" msgstr "Öffne vorheriges Dokument" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/print.py:30 msgid "Print" msgstr "Drucken" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/new.py:30 msgid "Create new document" msgstr "Neues Dokument" #: paperwork-gtk/src/paperwork_gtk/menus/page/print.py:49 msgid "Print page" msgstr "Seite drucken" #: paperwork-gtk/src/paperwork_gtk/menus/page/reset.py:49 msgid "Reset page" msgstr "Seite zurücksetzen" #: paperwork-gtk/src/paperwork_gtk/menus/page/delete.py:49 msgid "Delete page" msgstr "Seite löschen" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_inside_doc.py:50 msgid "Another position" msgstr "an andere Position" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_inside_doc.py:53 #: paperwork-gtk/src/paperwork_gtk/menus/page/move_to_doc.py:53 msgid "Move page to" msgstr "Verschiebe Seite" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_to_doc.py:50 msgid "Another document" msgstr "in ein anderes Dokument" #: paperwork-gtk/src/paperwork_gtk/menus/page/copy_text.py:50 msgid "Copy selected text" msgstr "Kopiere ausgewählten Text" #: paperwork-gtk/src/paperwork_gtk/menus/page/export.py:49 msgid "Export page" msgstr "Seite exportieren" #: paperwork-gtk/src/paperwork_gtk/menus/page/redo_ocr.py:50 #: paperwork-gtk/src/paperwork_gtk/actions/page/redo_ocr.py:62 msgid "Redo OCR on page" msgstr "Wiederhole OCR auf Seite" #: paperwork-gtk/src/paperwork_gtk/menus/docs/properties.py:56 msgid "Change labels" msgstr "Labels ändern" #: paperwork-gtk/src/paperwork_gtk/menus/docs/delete.py:51 msgid "Delete" msgstr "Löschen" #: paperwork-gtk/src/paperwork_gtk/menus/docs/select_all.py:51 msgid "Select all" msgstr "Alles auswählen" #: paperwork-gtk/src/paperwork_gtk/menus/docs/export.py:51 msgid "Export" msgstr "Exportieren" #: paperwork-gtk/src/paperwork_gtk/menus/docs/redo_ocr.py:51 msgid "Redo OCR" msgstr "Wiederhole OCR" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_settings.py:43 #: paperwork-gtk/src/paperwork_gtk/settings/settings.glade.h:1 msgid "Settings" msgstr "Einstellungen" #: paperwork-gtk/src/paperwork_gtk/menus/app/help.py:64 msgid "Help" msgstr "Hilfe" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_shortcuts.py:45 msgid "Shortcuts" msgstr "Tastenkürzel" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_bug_report.py:49 msgid "Report bug" msgstr "Fehler melden" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_about.py:45 msgid "About" msgstr "Über" #: paperwork-gtk/src/paperwork_gtk/menus/doc/properties.py:43 msgid "Document properties" msgstr "Dokumenteneigenschaften" #: paperwork-gtk/src/paperwork_gtk/menus/doc/print.py:41 msgid "Print document" msgstr "Dokument drucken" #: paperwork-gtk/src/paperwork_gtk/menus/doc/delete.py:40 msgid "Delete document" msgstr "Dokument löschen" #: paperwork-gtk/src/paperwork_gtk/menus/doc/add_to_selection.py:41 msgid "Add to selection" msgstr "Zur Auswahl hinzufügen" #: paperwork-gtk/src/paperwork_gtk/menus/doc/export.py:38 msgid "Export document" msgstr "Dokument exportieren" #: paperwork-gtk/src/paperwork_gtk/menus/doc/open_external.py:36 msgid "Open folder" msgstr "Ordner öffnen" #: paperwork-gtk/src/paperwork_gtk/menus/doc/redo_ocr.py:40 msgid "Redo OCR on document" msgstr "Wiederhole OCR auf Dokument" #: paperwork-gtk/src/paperwork_gtk/actions/page/delete.py:85 #, python-brace-format msgid "Are you sure you want to delete page {page_idx} of document {doc_id} ?" msgstr "" "Sind Sie sicher, dass Sie die Seite {page_idx} des Dokuments {doc_id} " "löschen möchten?" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:116 #, python-format msgid "Move the page %d to what position ?" msgstr "An welche Position soll die Seite %d verschoben werden?" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:135 #, python-format msgid "Invalid page position: %s" msgstr "Ungültige Seitenposition: %s" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:147 #, python-format msgid "Invalid page position: %d. Out of document bounds (1-%d)." msgstr "Ungültige Seitenposition: %d. Ausserhalb des Dokuments (1-%d)." #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:156 msgid "Page position unchanged" msgstr "Seitenposition unverändert" #: paperwork-gtk/src/paperwork_gtk/actions/page/redo_ocr.py:100 #: paperwork-gtk/src/paperwork_gtk/actions/docs/redo_ocr.py:92 #: paperwork-gtk/src/paperwork_gtk/actions/doc/redo_ocr.py:105 #, python-brace-format msgid "OCR on {doc_id} p{page_idx}" msgstr "OCR auf {doc_id}p{page_idx}" #: paperwork-gtk/src/paperwork_gtk/actions/docs/delete.py:72 #, python-format msgid "Are you sure you want to delete %d documents ?" msgstr "Sind Sie sicher, dass Sie %d Dokumente löschen möchten?" #: paperwork-gtk/src/paperwork_gtk/actions/docs/redo_ocr.py:83 #: paperwork-gtk/src/paperwork_gtk/actions/doc/redo_ocr.py:97 #, python-format msgid "OCR on %s" msgstr "OCR auf %s" #: paperwork-gtk/src/paperwork_gtk/actions/doc/delete.py:78 #, python-format msgid "Are you sure you want to delete document %s ?" msgstr "Sind Sie sicher, dass Sie das Dokument %s löschen möchten?" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:405 #, python-format msgid "Estimated file size: %s" msgstr "Geschätzte Dateigröße: %s" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:628 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:108 msgid "Select a file or a directory to import" msgstr "Wähle eine Datei oder ein Verzeichnis zum Import" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:637 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:128 msgid "Any files" msgstr "Alle Dateien" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:715 msgid "Export has failed" msgstr "Export fehlgeschlagen" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:81 msgid "Keyword(s)" msgstr "Suchbegriff(e)" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:102 msgid "No labels" msgstr "Keine Labels" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:142 msgid "Label" msgstr "Label" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:154 msgid "From:" msgstr "Von:" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:160 msgid "to:" msgstr "bis:" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:270 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/name.glade.h:1 msgid "Date" msgstr "Datum" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:302 msgid "and" msgstr "und" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:303 msgid "or" msgstr "oder" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:320 msgid "not" msgstr "nicht" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:344 msgid "Remove" msgstr "Entfernen" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/scan.py:118 #, python-format msgid "Scan from %s" msgstr "Scanne von %s" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:77 msgid "Import image or PDF file(s)" msgstr "Importiere Bild- oder PDF-Datei(en)" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:78 msgid "Import file(s)" msgstr "Importiere Datei(en)" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:122 msgid "All supported file formats" msgstr "Alle unterstützten Dateiformate" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/title.py:33 #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/name.py:59 msgid "New document" msgstr "Neues Dokument" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/empty_doc/__init__.py:89 msgid "Empty" msgstr "Leeres Dokument" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/boxes/__init__.py:83 msgid "Loading text ..." msgstr "Lade Text ..." #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/__init__.py:93 msgid "Loading page {}/{} ..." msgstr "Lade Seite {}/{} ..." #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/__init__.py:622 #, python-format msgid "%d documents" msgstr "%d Dokumente" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/thumbnailer.py:116 msgid "Loading document thumbnails" msgstr "Lade Vorschaubilder" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:247 msgid "Renaming label" msgstr "Label umbenennen" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:294 #, python-format msgid "Are you sure you want to delete label '%s' from ALL documents ?" msgstr "" "Sind Sie sicher, dass Sie das Label %s von allen Dokumenten löschen möchten?" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:396 #, python-brace-format msgid "Changing label {old_label} into {new_label} on document {doc_id}" msgstr "Ändere im Dokument {doc_id} das Label {old_label} nach {new_label}" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:462 #, python-brace-format msgid "Deleting label {old_label} from document {doc_id}" msgstr "Lösche das Label {old_label} aus dem Dokumen {doc_id}" #: paperwork-gtk/src/paperwork_gtk/settings/update.py:60 msgid "Updates" msgstr "Updates" #: paperwork-gtk/src/paperwork_gtk/settings/storage.py:84 msgid "Storage" msgstr "Verzeichnisse" #: paperwork-gtk/src/paperwork_gtk/settings/storage.py:90 msgid "Work Directory" msgstr "Arbeitsverzeichnis" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.py:64 msgid "Optical Character Recognition" msgstr "Texterkennung" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.py:76 msgid "OCR disabled" msgstr "OCR deaktiviert" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.py:168 msgid "Loading ..." msgstr "Lade ..." #: paperwork-gtk/src/paperwork_gtk/settings/scanner/dev_id_popover.py:67 msgid "No scanner" msgstr "Kein Scanner" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/resolution_popover.py:123 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:66 msgid "{} dpi" msgstr "{} dpi" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/resolution_popover.py:125 msgid "{} dpi (recommended)" msgstr "{} dpi (empfohlen)" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:15 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:1 msgid "Color" msgstr "Farbe" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:16 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:2 msgid "Grayscale" msgstr "Graustufen" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:17 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:3 msgid "Black & White" msgstr "Schwarz-Weiss" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:62 msgid "No scanner selected" msgstr "Kein Scanner ausgewählt" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:66 msgid "No resolution selected" msgstr "Keine Auflösung ausgewählt" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:70 msgid "No mode selected" msgstr "Kein Modus ausgewählt" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:113 msgid "Scanner" msgstr "Scanner" #: paperwork-gtk/src/paperwork_gtk/settings/stats.py:60 msgid "Help Improve Paperwork" msgstr "Hilf, Paperwork zu verbessern" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:33 msgid "Now with 10% more freedom in it !" msgstr "Jetzt mit 10% mehr Freiheit darin!" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:34 #, python-format msgid "Buy it now and get a 100% discount !" msgstr "Kaufe jetzt und bekomme 100% Rabatt!" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:35 msgid "New features and bugs available !" msgstr "Neue Funktionen und Fehler verfügbar!" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:36 msgid "New taste !" msgstr "Neuer Geschmack!" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:37 msgid "We replaced your old bugs with new bugs. Enjoy." msgstr "Wir haben Deine alten Fehler durch neue Fehler ersetzt. Geniesse es." #: paperwork-gtk/src/paperwork_gtk/update_notification.py:38 msgid "Smarter, Better, Stronger" msgstr "Smarter, Besser, Stärker" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:40 msgid "It's better when it's free." msgstr "Es ist besser, wenn es frei ist." #: paperwork-gtk/src/paperwork_gtk/update_notification.py:45 #, python-brace-format msgid "A new version of Paperwork is available: {new_version}" msgstr "Eine neue version von Paperwork ist verfügbar: {new_version}" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:64 msgid "Introduction" msgstr "Einleitung" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:65 msgid "User manual" msgstr "Benutzerhandbuch" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:67 msgid "Documentation" msgstr "Anleitung" #: paperwork-gtk/src/paperwork_gtk/about/about.glade.h:1 msgid "©2021" msgstr "©2021" #: paperwork-gtk/src/paperwork_gtk/about/about.glade.h:2 msgid "Sorting documents is a machine's job." msgstr "Dokumente zu sortieren ist die Arbeit einer Maschine." #: paperwork-gtk/src/paperwork_gtk/actions/page/move_to_doc/move_to_doc.glade.h:1 msgid "Select target document" msgstr "Wähle Zieldokument" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:1 msgid "Export Steps" msgstr "Export Schritte" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:2 msgid "Quality" msgstr "Qualität" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:3 msgid "Paper format" msgstr "Seitenformat" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:4 msgid "Export Settings" msgstr "Exporteinstellungen" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:5 msgid "Send by email" msgstr "Per Email senden" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:6 msgid "Preview" msgstr "Vorschau" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.glade.h:1 #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.glade.h:1 msgid "Search" msgstr "Suche" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/suggestions.glade.h:1 msgid "Did you mean ?" msgstr "Meinten Sie ?" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.glade.h:2 msgid "Advanced search" msgstr "Erweiterte Suche" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/buttons.glade.h:1 msgid "Add page" msgstr "Seite hinzufügen" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/layout_settings.glade.h:1 msgid "Highlight words" msgstr "Wörter hervorheben" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.glade.h:1 msgid "Documents" msgstr "Dokumente" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.glade.h:2 msgid "Selection" msgstr "Auswahl" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/extra_text.glade.h:1 msgid "Additional keywords" msgstr "Zusätzliche Schlüsselwörter" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/docproperties.glade.h:1 msgid "Properties" msgstr "Eigenschaften" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/label.glade.h:1 msgid "Change the label color" msgstr "Ändere die Farbe des Labels" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/label.glade.h:2 msgid "Delete the label from all documents" msgstr "Lösche das Label für alle Dokumente" #: paperwork-gtk/src/paperwork_gtk/settings/update.glade.h:1 msgid "" "Updates\n" "Check periodically for new versions of Paperwork" msgstr "" "Updates\n" "Überprüfe regelmäßig auf neue Versionen von " "Paperwork" #: paperwork-gtk/src/paperwork_gtk/settings/update.glade.h:3 msgid "" "Look about once a week for new versions of " "Paperwork.\n" "You will be notified when a new version is available but it won't be " "installed automatically.\n" "" msgstr "" "Überprüfe ungefähr einmal pro Woche auf eine neue " "Version von Paperwork.\n" "Sie werden benachrichtigt wenn eine neue Version verfügbar ist, aber sie " "wird nicht automatisch installiert.\n" "" #: paperwork-gtk/src/paperwork_gtk/settings/stats.glade.h:1 msgid "" "Send metrics\n" "Give us clues about how you use Paperwork" msgstr "" "Nutzerstatistiken senden\n" "Geben Sie uns einen Einblick, wie Sie Paperwork " "verwenden" #: paperwork-gtk/src/paperwork_gtk/settings/stats.glade.h:3 msgid "" "Those clues will help us to make Paperwork an even " "better piece of software, for you. Statistics also show us that people are " "actually using our work, keeping us motivated to improve it.\n" "\n" "Here are the data we gather:\n" "- Hardware: CPU, RAM, screen resolution.\n" "- Software: Version of Paperwork, Operating system, desktop environment, " "system language.\n" "- Data metrics: number of documents, maximum and average number of pages, " "number of labels.\n" "- Number of times you used each feature.\n" "\n" "We do not collect document content nor any other sensitive or personal " "information. Still we think it's fair to request your authorization ;-).\n" "\n" "Collected statistics are visible on openpaper.work.\n" "" msgstr "" "Dieser Einblick wird uns helfen, Paperwork noch " "besser für Sie zu machen. Die Statistiken zeigen auch, dass die Leute unsere " "Arbeit wirklich benutzen, was uns motviert sie weiter zu verbessern.\n" "\n" "Folgende Daten werden erfasst:\n" "- Hardware: CPU, RAM, Bildschirmauflösung.\n" "- Software: Version von Paperwork, Betriebssystem, Desktop-Umgebung, " "Systemsprache.\n" "- Datenmetriken: Anzahl an Dokumenten, maximale und durchschnittliche " "Seitenzahl, Anzahl von Labels.\n" "- Nutzungshäufigkeit für jedes Feature.\n" "\n" "Wir erfassen keinen Dokumenteninhalt oder sonstige vertrauliche oder " "persönlichen Informationen. Wir denken trotzdem, dass es fair ist, Ihre " "Zustimmung zu erfragen ;-).\n" "\n" "Die erfassten Statistiken sind auf openpaper.work sichtbar.\n" "" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.glade.h:1 msgid "Languages" msgstr "Sprachen" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.glade.h:1 msgid "Flatpak" msgstr "Flatpak" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.glade.h:2 msgid "" "You are using Paperwork from a Flatpak container. Paperwork needs Saned to " "access your scanners.\n" "\n" "Important: the following procedure will only work for local (non-network) " "scanners !\n" "\n" "To enable Saned on the host system, you must copy and paste the following " "commands in a terminal:" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:1 msgid "Maximize" msgstr "Maximieren" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:2 msgid "Automatic" msgstr "Automatisch" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:3 msgid "Scan" msgstr "Scanne" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:4 msgid "Scanner Calibration" msgstr "Scanner Kalibrierung" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:1 msgid "Device" msgstr "Gerät" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:2 msgid "Resolution" msgstr "Auflösung" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:3 msgid "Mode" msgstr "Modus" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:4 msgid "Calibration" msgstr "Kalibrierung" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:5 msgid "Re-calibrate" msgstr "Rekalibrieren" #: paperwork-gtk/src/paperwork_gtk/settings/storage.glade.h:1 msgid "Work directory" msgstr "Arbeitsverzeichnis" #~ msgid "" #~ "You are using Paperwork from a Flatpak container. Paperwork needs Saned " #~ "to access your scanners. To enable Saned on the host system, you must " #~ "copy and paste the following commands in a terminal:" #~ msgstr "" #~ "Sie benutzen Paperwork in einem Flatpak Container. Paperwork benötigt " #~ "Saned, um auf Ihre Scanner zuzugreifen. Um Saned auf dem System zu " #~ "aktivieren, kopieren Sie die folgenden Kommandos in eine " #~ "Eingabeaufforderung und führen Sie sie aus:" #, python-format #~ msgid "Are you sure you want to remove label '%s' from ALL documents ?" #~ msgstr "" #~ "Sind Sie sicher, dass Sie das Label '%s' von allen Dokumenten löschen " #~ "möchten?" #~ msgid "©2020" #~ msgstr "©2020" paperwork-2.1.1/paperwork-gtk/l10n/es.po000066400000000000000000000600331417573700700200600ustar00rootroot00000000000000# Spanish translations for PACKAGE package. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2021-09-09 05:14+0000\n" "Last-Translator: Marcel Mente \n" "Language-Team: Spanish \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.4\n" #: paperwork-gtk/src/paperwork_gtk/cmd/install.py:40 msgid "Install Paperwork icons and shortcuts" msgstr "Instalar iconos y accesos directos de Paperwork" #: paperwork-gtk/src/paperwork_gtk/cmd/install.py:44 msgid "Install everything only for the current user" msgstr "Instalar todo para el usuario actual" #: paperwork-gtk/src/paperwork_gtk/cmd/import.py:35 msgid "Run Paperwork and import files passed as arguments into a new document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/cmd/import.py:39 msgid "URLs or paths of files to import" msgstr "" #: paperwork-gtk/src/paperwork_gtk/main.py:201 msgid "command" msgstr "comando" #: paperwork-gtk/src/paperwork_gtk/docimport.py:86 #, python-format msgid "Don't know how to import '%s'. Sorry." msgstr "" #: paperwork-gtk/src/paperwork_gtk/docimport.py:105 msgid "PDF password" msgstr "" #: paperwork-gtk/src/paperwork_gtk/docimport.py:123 msgid "No new document to import found" msgstr "No se encontró ningún documento nuevo para importar" #: paperwork-gtk/src/paperwork_gtk/docimport.py:163 msgid "Imported file(s) deleted" msgstr "Archivo(s) importado(s) ha(n) sido eliminado(s)" #: paperwork-gtk/src/paperwork_gtk/docimport.py:170 msgid "Imported:\n" msgstr "Importado:\n" #: paperwork-gtk/src/paperwork_gtk/docimport.py:176 msgid "Import successful" msgstr "Importación exitosa" #: paperwork-gtk/src/paperwork_gtk/docimport.py:185 msgid "Delete imported files" msgstr "Eliminando archivos importados" #: paperwork-gtk/src/paperwork_gtk/print.py:42 #, python-brace-format msgid "Loading {doc_id} p{page_idx} for printing" msgstr "Cargando {doc_id} p{page_idx} para imprimir" #: paperwork-gtk/src/paperwork_gtk/print.py:144 #, python-format msgid "Printing %s" msgstr "Inmprimiendo %s" #: paperwork-gtk/src/paperwork_gtk/print.py:179 #, python-brace-format msgid "Printing {doc_id} ({page_idx}/{nb_pages})" msgstr "Imprimiendo {doc_id} ({page_idx}/{nb_pages})" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/copy_text.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/edit.py:30 msgid "Page" msgstr "Pagina" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/copy_text.py:30 msgid "Copy selected text to clipboard" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/edit.py:30 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/actions.glade.h:1 msgid "Edit" msgstr "Edicion" #: paperwork-gtk/src/paperwork_gtk/shortcuts/app/find.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/new.py:30 msgid "Global" msgstr "Global" #: paperwork-gtk/src/paperwork_gtk/shortcuts/app/find.py:30 msgid "Find" msgstr "Buscar" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/properties.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/print.py:30 msgid "Document" msgstr "Documento" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/properties.py:30 msgid "Edit document properties" msgstr "Editar las propiedades del documento" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:35 msgid "Document list" msgstr "Lista de documentos" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:30 msgid "Open next document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:35 msgid "Open previous document" msgstr "Abrir el documento anterior" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/print.py:30 msgid "Print" msgstr "Impresión" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/new.py:30 msgid "Create new document" msgstr "Crear nuvo documento" #: paperwork-gtk/src/paperwork_gtk/menus/page/print.py:49 msgid "Print page" msgstr "Imprimir página" #: paperwork-gtk/src/paperwork_gtk/menus/page/reset.py:49 msgid "Reset page" msgstr "Reiniciar la página" #: paperwork-gtk/src/paperwork_gtk/menus/page/delete.py:49 msgid "Delete page" msgstr "eliminar pagina" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_inside_doc.py:50 msgid "Another position" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_inside_doc.py:53 #: paperwork-gtk/src/paperwork_gtk/menus/page/move_to_doc.py:53 msgid "Move page to" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_to_doc.py:50 msgid "Another document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/copy_text.py:50 msgid "Copy selected text" msgstr "Copiar texto seleccionado" #: paperwork-gtk/src/paperwork_gtk/menus/page/export.py:49 msgid "Export page" msgstr "Exportar pagina" #: paperwork-gtk/src/paperwork_gtk/menus/page/redo_ocr.py:50 #: paperwork-gtk/src/paperwork_gtk/actions/page/redo_ocr.py:62 msgid "Redo OCR on page" msgstr "Rehacer OCR en la página" #: paperwork-gtk/src/paperwork_gtk/menus/docs/properties.py:56 msgid "Change labels" msgstr "Cambiar la etiqueta" #: paperwork-gtk/src/paperwork_gtk/menus/docs/delete.py:51 msgid "Delete" msgstr "Eliminar" #: paperwork-gtk/src/paperwork_gtk/menus/docs/select_all.py:51 msgid "Select all" msgstr "Seleccionar todo" #: paperwork-gtk/src/paperwork_gtk/menus/docs/export.py:51 msgid "Export" msgstr "Exportar" #: paperwork-gtk/src/paperwork_gtk/menus/docs/redo_ocr.py:51 msgid "Redo OCR" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_settings.py:43 #: paperwork-gtk/src/paperwork_gtk/settings/settings.glade.h:1 msgid "Settings" msgstr "Configuración" #: paperwork-gtk/src/paperwork_gtk/menus/app/help.py:64 msgid "Help" msgstr "Ayuda" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_shortcuts.py:45 msgid "Shortcuts" msgstr "Atajos" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_bug_report.py:49 msgid "Report bug" msgstr "Reportar una falla" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_about.py:45 msgid "About" msgstr "Acerca" #: paperwork-gtk/src/paperwork_gtk/menus/doc/properties.py:43 msgid "Document properties" msgstr "Propiedades del documento" #: paperwork-gtk/src/paperwork_gtk/menus/doc/print.py:41 msgid "Print document" msgstr "imprimir documento" #: paperwork-gtk/src/paperwork_gtk/menus/doc/delete.py:40 msgid "Delete document" msgstr "eliminar documento" #: paperwork-gtk/src/paperwork_gtk/menus/doc/add_to_selection.py:41 msgid "Add to selection" msgstr "Agregar a la selección" #: paperwork-gtk/src/paperwork_gtk/menus/doc/export.py:38 msgid "Export document" msgstr "Exportar documento" #: paperwork-gtk/src/paperwork_gtk/menus/doc/open_external.py:36 msgid "Open folder" msgstr "Abrir carpeta" #: paperwork-gtk/src/paperwork_gtk/menus/doc/redo_ocr.py:40 msgid "Redo OCR on document" msgstr "Rehacer el OCR en el documento" #: paperwork-gtk/src/paperwork_gtk/actions/page/delete.py:85 #, python-brace-format msgid "Are you sure you want to delete page {page_idx} of document {doc_id} ?" msgstr "" "¿Está seguro de que desea eliminar la página {page_idx} del documento " "{doc_id}?" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:116 #, python-format msgid "Move the page %d to what position ?" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:135 #, python-format msgid "Invalid page position: %s" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:147 #, python-format msgid "Invalid page position: %d. Out of document bounds (1-%d)." msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:156 msgid "Page position unchanged" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/redo_ocr.py:100 #: paperwork-gtk/src/paperwork_gtk/actions/docs/redo_ocr.py:92 #: paperwork-gtk/src/paperwork_gtk/actions/doc/redo_ocr.py:105 #, python-brace-format msgid "OCR on {doc_id} p{page_idx}" msgstr "OCR en {doc_id} p{page_idx}" #: paperwork-gtk/src/paperwork_gtk/actions/docs/delete.py:72 #, python-format msgid "Are you sure you want to delete %d documents ?" msgstr "¿Está seguro de que quiere eliminar %d documentos?" #: paperwork-gtk/src/paperwork_gtk/actions/docs/redo_ocr.py:83 #: paperwork-gtk/src/paperwork_gtk/actions/doc/redo_ocr.py:97 #, python-format msgid "OCR on %s" msgstr "OCR en %s" #: paperwork-gtk/src/paperwork_gtk/actions/doc/delete.py:78 #, python-format msgid "Are you sure you want to delete document %s ?" msgstr "¿Está seguro de que quiere eliminar documentos %s?" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:405 #, python-format msgid "Estimated file size: %s" msgstr "Tamaño estimado del archivo: %s" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:628 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:108 msgid "Select a file or a directory to import" msgstr "Seleccione un archivo o una carpeta para importar" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:637 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:128 msgid "Any files" msgstr "Cualquier archivo" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:715 msgid "Export has failed" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:81 msgid "Keyword(s)" msgstr "Palabra(s) clave" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:102 msgid "No labels" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:142 msgid "Label" msgstr "Etiqueta" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:154 msgid "From:" msgstr "De:" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:160 msgid "to:" msgstr "a:" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:270 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/name.glade.h:1 msgid "Date" msgstr "Fecha" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:302 msgid "and" msgstr "y" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:303 msgid "or" msgstr "o" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:320 msgid "not" msgstr "no" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:344 msgid "Remove" msgstr "Eliminar" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/scan.py:118 #, python-format msgid "Scan from %s" msgstr "Escaneando de %s" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:77 msgid "Import image or PDF file(s)" msgstr "Importar imagen o archivos PDF" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:78 msgid "Import file(s)" msgstr "Importando archivo(s)" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:122 msgid "All supported file formats" msgstr "Todos los formatos soportados" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/title.py:33 #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/name.py:59 msgid "New document" msgstr "Nuevo documento" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/empty_doc/__init__.py:89 msgid "Empty" msgstr "Vacio" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/boxes/__init__.py:83 msgid "Loading text ..." msgstr "CArgando texto ..." #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/__init__.py:93 msgid "Loading page {}/{} ..." msgstr "Cargando pagina {}/{} ..." #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/__init__.py:622 #, python-format msgid "%d documents" msgstr "%d documentos" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/thumbnailer.py:116 msgid "Loading document thumbnails" msgstr "Carga de miniaturas de documentos" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:247 msgid "Renaming label" msgstr "Cambiar el nombre de la etiqueta" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:294 #, python-format msgid "Are you sure you want to delete label '%s' from ALL documents ?" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:396 #, python-brace-format msgid "Changing label {old_label} into {new_label} on document {doc_id}" msgstr "Cambio de etiqueta {old_label} a {new_label} en el documento {doc_id}" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:462 #, python-brace-format msgid "Deleting label {old_label} from document {doc_id}" msgstr "Eliminando etiqueta {old_label} del documento {doc_id}" #: paperwork-gtk/src/paperwork_gtk/settings/update.py:60 msgid "Updates" msgstr "Actualizaciones" #: paperwork-gtk/src/paperwork_gtk/settings/storage.py:84 msgid "Storage" msgstr "Almacenamiento" #: paperwork-gtk/src/paperwork_gtk/settings/storage.py:90 msgid "Work Directory" msgstr "Directorio de Trabajo" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.py:64 msgid "Optical Character Recognition" msgstr "OCR - Reconocimiento óptico de caracteres" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.py:76 msgid "OCR disabled" msgstr "OCR desabilitado" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.py:168 msgid "Loading ..." msgstr "Cargando ..." #: paperwork-gtk/src/paperwork_gtk/settings/scanner/dev_id_popover.py:67 msgid "No scanner" msgstr "No Scanner" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/resolution_popover.py:123 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:66 msgid "{} dpi" msgstr "{} dpi" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/resolution_popover.py:125 msgid "{} dpi (recommended)" msgstr "{} dpi (recomendado)" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:15 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:1 msgid "Color" msgstr "Color" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:16 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:2 msgid "Grayscale" msgstr "Escala de Grises" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:17 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:3 msgid "Black & White" msgstr "Blamco y Negro" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:62 msgid "No scanner selected" msgstr "No ha seleccionado scanner" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:66 msgid "No resolution selected" msgstr "No ha seleccionado una resolucion" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:70 msgid "No mode selected" msgstr "No ha seleccionado modo" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:113 msgid "Scanner" msgstr "Scanner" #: paperwork-gtk/src/paperwork_gtk/settings/stats.py:60 msgid "Help Improve Paperwork" msgstr "Ayud a a mejorar Paperwork" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:33 msgid "Now with 10% more freedom in it !" msgstr "¡Ahora con un 10% más de libertad!" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:34 #, python-format msgid "Buy it now and get a 100% discount !" msgstr "" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:35 msgid "New features and bugs available !" msgstr "¡Nuevas funciones y correcciones a errores disponibles!" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:36 msgid "New taste !" msgstr "¡Nuevo sabor!" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:37 msgid "We replaced your old bugs with new bugs. Enjoy." msgstr "Hemos sustituido tus viejos bugs con nuevos. Disfrutalo." #: paperwork-gtk/src/paperwork_gtk/update_notification.py:38 msgid "Smarter, Better, Stronger" msgstr "Más inteligente, mejor, más fuerte" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:40 msgid "It's better when it's free." msgstr "Es mejor cuando es libre y aun mejor gratis." #: paperwork-gtk/src/paperwork_gtk/update_notification.py:45 #, python-brace-format msgid "A new version of Paperwork is available: {new_version}" msgstr "Hay una nueva version disponible: {new_version}" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:64 msgid "Introduction" msgstr "Introducción" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:65 msgid "User manual" msgstr "Manual de usuario" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:67 msgid "Documentation" msgstr "Documentación" #: paperwork-gtk/src/paperwork_gtk/about/about.glade.h:1 msgid "©2021" msgstr "" #: paperwork-gtk/src/paperwork_gtk/about/about.glade.h:2 msgid "Sorting documents is a machine's job." msgstr "La clasificación de documentos es un trabajo de máquina." #: paperwork-gtk/src/paperwork_gtk/actions/page/move_to_doc/move_to_doc.glade.h:1 msgid "Select target document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:1 msgid "Export Steps" msgstr "Pasos de exportación" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:2 msgid "Quality" msgstr "Calidad" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:3 msgid "Paper format" msgstr "Formato de papel" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:4 msgid "Export Settings" msgstr "Exportart configuración" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:5 msgid "Send by email" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:6 msgid "Preview" msgstr "Previsualizar" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.glade.h:1 #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.glade.h:1 msgid "Search" msgstr "Buscar" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/suggestions.glade.h:1 msgid "Did you mean ?" msgstr "¿Quieres decir que...?" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.glade.h:2 msgid "Advanced search" msgstr "Busqueda avanzada" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/buttons.glade.h:1 msgid "Add page" msgstr "Agregar pagina" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/layout_settings.glade.h:1 msgid "Highlight words" msgstr "Resaltar palabras" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.glade.h:1 msgid "Documents" msgstr "Documentos" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.glade.h:2 msgid "Selection" msgstr "Selección" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/extra_text.glade.h:1 msgid "Additional keywords" msgstr "Palabras clave adicionales" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/docproperties.glade.h:1 msgid "Properties" msgstr "Propiedades" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/label.glade.h:1 msgid "Change the label color" msgstr "Cambiar el color de la etiqueta" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/label.glade.h:2 msgid "Delete the label from all documents" msgstr "Eliminar la etiqueta de todos los documentos" #: paperwork-gtk/src/paperwork_gtk/settings/update.glade.h:1 msgid "" "Updates\n" "Check periodically for new versions of Paperwork" msgstr "" "Actualizaciones\n" "Comprueba periódicamente si hay nuevas versiones " "de Paperwork" #: paperwork-gtk/src/paperwork_gtk/settings/update.glade.h:3 msgid "" "Look about once a week for new versions of " "Paperwork.\n" "You will be notified when a new version is available but it won't be " "installed automatically.\n" "" msgstr "" "Busque aproximadamente una vez a la semana nuevas " "versiones de Paperwork.\n" "Se le notificará cuando haya una nueva versión disponible, pero no se " "isntala automaticamente.\n" "" #: paperwork-gtk/src/paperwork_gtk/settings/stats.glade.h:1 msgid "" "Send metrics\n" "Give us clues about how you use Paperwork" msgstr "" "Enviar metricas\n" "Danos pistas sobre cómo utilizas Paperwork" #: paperwork-gtk/src/paperwork_gtk/settings/stats.glade.h:3 msgid "" "Those clues will help us to make Paperwork an even " "better piece of software, for you. Statistics also show us that people are " "actually using our work, keeping us motivated to improve it.\n" "\n" "Here are the data we gather:\n" "- Hardware: CPU, RAM, screen resolution.\n" "- Software: Version of Paperwork, Operating system, desktop environment, " "system language.\n" "- Data metrics: number of documents, maximum and average number of pages, " "number of labels.\n" "- Number of times you used each feature.\n" "\n" "We do not collect document content nor any other sensitive or personal " "information. Still we think it's fair to request your authorization ;-).\n" "\n" "Collected statistics are visible on openpaper.work.\n" "" msgstr "" "Esas pistas nos ayudarán a hacer de Paperwork una " "mejor pieza de software, para usted. Las estadísticas también nos muestran " "que la gente está tilizando realmente nuestro trabajo, lo que nos mantiene " "motivados para mejorarlo.\n" "\n" "Estos son los datos que recogemos:\n" "- Hardware: CPU, RAM, resolución de pantalla.\n" "- Software: Version de Paperwork, Sistema operativo, entorno de escritorio, " "leguaje del sistema.\n" "- Métricas de datos: número de documentos, número máximo y medio de páginas, " "numero de etiquetas.\n" "- Número de veces que ha utilizado cada función.\n" "\n" "No recogemos el contenido de los documentos ni ningún otro dato sensible o " "información personal. Aun así, creemos que es justo solicitar su " "autorización ;-).\n" "\n" "Las estadísticas recogidas son visibles en openpaper.work.\n" "" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.glade.h:1 msgid "Languages" msgstr "Lenguajes" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.glade.h:1 msgid "Flatpak" msgstr "Flatpak" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.glade.h:2 msgid "" "You are using Paperwork from a Flatpak container. Paperwork needs Saned to " "access your scanners.\n" "\n" "Important: the following procedure will only work for local (non-network) " "scanners !\n" "\n" "To enable Saned on the host system, you must copy and paste the following " "commands in a terminal:" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:1 msgid "Maximize" msgstr "Maximizar" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:2 msgid "Automatic" msgstr "Automatico" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:3 msgid "Scan" msgstr "Scan" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:4 msgid "Scanner Calibration" msgstr "Calibrar scanner" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:1 msgid "Device" msgstr "Dispositivo" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:2 msgid "Resolution" msgstr "REsolución" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:3 msgid "Mode" msgstr "Modo" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:4 msgid "Calibration" msgstr "Calibrar" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:5 msgid "Re-calibrate" msgstr "Re-calibarar" #: paperwork-gtk/src/paperwork_gtk/settings/storage.glade.h:1 msgid "Work directory" msgstr "Directorio de trabajo" #~ msgid "" #~ "You are using Paperwork from a Flatpak container. Paperwork needs Saned " #~ "to access your scanners. To enable Saned on the host system, you must " #~ "copy and paste the following commands in a terminal:" #~ msgstr "" #~ "Está utilizando Paperwork de un contenedor Flatpak. Paperwork necesita " #~ "Saned paraacceder a sus escáneres. Para habilitar Saned en el sistema " #~ "anfitrión, debe copiar y pegar los siguientes comandos en una terminal:" #~ msgid "©2020" #~ msgstr "©2021" paperwork-2.1.1/paperwork-gtk/l10n/fr.po000066400000000000000000000630061417573700700200630ustar00rootroot00000000000000# French translations for PACKAGE package # Traductions françaises du paquet PACKAGE. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2021-12-01 11:07+0000\n" "Last-Translator: Jerome Flesch \n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 4.9\n" #: paperwork-gtk/src/paperwork_gtk/cmd/install.py:40 msgid "Install Paperwork icons and shortcuts" msgstr "Installer les icônes et raccourcis de Paperwork" #: paperwork-gtk/src/paperwork_gtk/cmd/install.py:44 msgid "Install everything only for the current user" msgstr "Tout installer seulement pour l'utilisateur courant" #: paperwork-gtk/src/paperwork_gtk/cmd/import.py:35 msgid "Run Paperwork and import files passed as arguments into a new document" msgstr "" "Exécute Paperwork et importe les fichiers passés en arguments dans de " "nouveaux documents" #: paperwork-gtk/src/paperwork_gtk/cmd/import.py:39 msgid "URLs or paths of files to import" msgstr "URLs ou chemin des fichiers à importer" #: paperwork-gtk/src/paperwork_gtk/main.py:201 msgid "command" msgstr "commande" #: paperwork-gtk/src/paperwork_gtk/docimport.py:86 #, python-format msgid "Don't know how to import '%s'. Sorry." msgstr "Ne sait pas comment importer '%s'. Désolé." #: paperwork-gtk/src/paperwork_gtk/docimport.py:105 msgid "PDF password" msgstr "Mot de passe du PDF" #: paperwork-gtk/src/paperwork_gtk/docimport.py:123 msgid "No new document to import found" msgstr "Pas de nouveau document à importer trouvé" #: paperwork-gtk/src/paperwork_gtk/docimport.py:163 msgid "Imported file(s) deleted" msgstr "Fichier(s) importé(s) effacé(s)" #: paperwork-gtk/src/paperwork_gtk/docimport.py:170 msgid "Imported:\n" msgstr "Importé(s) :\n" #: paperwork-gtk/src/paperwork_gtk/docimport.py:176 msgid "Import successful" msgstr "Import réussi" #: paperwork-gtk/src/paperwork_gtk/docimport.py:185 msgid "Delete imported files" msgstr "Effacer les fichiers importés" #: paperwork-gtk/src/paperwork_gtk/print.py:42 #, python-brace-format msgid "Loading {doc_id} p{page_idx} for printing" msgstr "Chargement de {doc_id} p{page_idx} pour l'impression" #: paperwork-gtk/src/paperwork_gtk/print.py:144 #, python-format msgid "Printing %s" msgstr "Impression de %s" #: paperwork-gtk/src/paperwork_gtk/print.py:179 #, python-brace-format msgid "Printing {doc_id} ({page_idx}/{nb_pages})" msgstr "Impression de {doc_id} ({page_idx}/{nb_pages})" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/copy_text.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/edit.py:30 msgid "Page" msgstr "Page" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/copy_text.py:30 msgid "Copy selected text to clipboard" msgstr "Copier le texte sélectionner vers le presse-papiers" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/edit.py:30 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/actions.glade.h:1 msgid "Edit" msgstr "Éditer" #: paperwork-gtk/src/paperwork_gtk/shortcuts/app/find.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/new.py:30 msgid "Global" msgstr "Global" #: paperwork-gtk/src/paperwork_gtk/shortcuts/app/find.py:30 msgid "Find" msgstr "Trouver" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/properties.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/print.py:30 msgid "Document" msgstr "Document" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/properties.py:30 msgid "Edit document properties" msgstr "Éditer les propriétés du document" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:35 msgid "Document list" msgstr "Liste de documents" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:30 msgid "Open next document" msgstr "Ouvrir le document suivant" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:35 msgid "Open previous document" msgstr "Ouvrir le document précédent" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/print.py:30 msgid "Print" msgstr "Imprimer" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/new.py:30 msgid "Create new document" msgstr "Créer un nouveau document" #: paperwork-gtk/src/paperwork_gtk/menus/page/print.py:49 msgid "Print page" msgstr "Imprimer la page" #: paperwork-gtk/src/paperwork_gtk/menus/page/reset.py:49 msgid "Reset page" msgstr "Réinitialiser la page" #: paperwork-gtk/src/paperwork_gtk/menus/page/delete.py:49 msgid "Delete page" msgstr "Effacer la page" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_inside_doc.py:50 msgid "Another position" msgstr "Une autre position" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_inside_doc.py:53 #: paperwork-gtk/src/paperwork_gtk/menus/page/move_to_doc.py:53 msgid "Move page to" msgstr "Déplacer la page vers" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_to_doc.py:50 msgid "Another document" msgstr "Un autre document" #: paperwork-gtk/src/paperwork_gtk/menus/page/copy_text.py:50 msgid "Copy selected text" msgstr "Copier le texte sélectionné" #: paperwork-gtk/src/paperwork_gtk/menus/page/export.py:49 msgid "Export page" msgstr "Exporter la page" #: paperwork-gtk/src/paperwork_gtk/menus/page/redo_ocr.py:50 #: paperwork-gtk/src/paperwork_gtk/actions/page/redo_ocr.py:62 msgid "Redo OCR on page" msgstr "Refaire la ROC sur la page" #: paperwork-gtk/src/paperwork_gtk/menus/docs/properties.py:56 msgid "Change labels" msgstr "Changer les étiquettes" #: paperwork-gtk/src/paperwork_gtk/menus/docs/delete.py:51 msgid "Delete" msgstr "Effacer" #: paperwork-gtk/src/paperwork_gtk/menus/docs/select_all.py:51 msgid "Select all" msgstr "Tout sélectionner" #: paperwork-gtk/src/paperwork_gtk/menus/docs/export.py:51 msgid "Export" msgstr "Exporter" #: paperwork-gtk/src/paperwork_gtk/menus/docs/redo_ocr.py:51 msgid "Redo OCR" msgstr "Refaire la ROC" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_settings.py:43 #: paperwork-gtk/src/paperwork_gtk/settings/settings.glade.h:1 msgid "Settings" msgstr "Réglages" #: paperwork-gtk/src/paperwork_gtk/menus/app/help.py:64 msgid "Help" msgstr "Aide" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_shortcuts.py:45 msgid "Shortcuts" msgstr "Raccourcis" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_bug_report.py:49 msgid "Report bug" msgstr "Signaler un bug" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_about.py:45 msgid "About" msgstr "À propos" #: paperwork-gtk/src/paperwork_gtk/menus/doc/properties.py:43 msgid "Document properties" msgstr "Propriétés du document" #: paperwork-gtk/src/paperwork_gtk/menus/doc/print.py:41 msgid "Print document" msgstr "Imprimer le document" #: paperwork-gtk/src/paperwork_gtk/menus/doc/delete.py:40 msgid "Delete document" msgstr "Effacer le document" #: paperwork-gtk/src/paperwork_gtk/menus/doc/add_to_selection.py:41 msgid "Add to selection" msgstr "Ajouter à la sélection" #: paperwork-gtk/src/paperwork_gtk/menus/doc/export.py:38 msgid "Export document" msgstr "Exporter le document" #: paperwork-gtk/src/paperwork_gtk/menus/doc/open_external.py:36 msgid "Open folder" msgstr "Ouvrir le dossier" #: paperwork-gtk/src/paperwork_gtk/menus/doc/redo_ocr.py:40 msgid "Redo OCR on document" msgstr "Refaire la ROC sur le document" #: paperwork-gtk/src/paperwork_gtk/actions/page/delete.py:85 #, python-brace-format msgid "Are you sure you want to delete page {page_idx} of document {doc_id} ?" msgstr "" "Êtes-vous sûr de vouloir effacer la page {page_idx} du document {doc_id} ?" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:116 #, python-format msgid "Move the page %d to what position ?" msgstr "Déplacer la page %d vers quelle position ?" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:135 #, python-format msgid "Invalid page position: %s" msgstr "Position de page invalide : %s" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:147 #, python-format msgid "Invalid page position: %d. Out of document bounds (1-%d)." msgstr "" "Position de page invalide : %d. En dehors des limites du document (1-%d)." #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:156 msgid "Page position unchanged" msgstr "Position de la page inchangée" #: paperwork-gtk/src/paperwork_gtk/actions/page/redo_ocr.py:100 #: paperwork-gtk/src/paperwork_gtk/actions/docs/redo_ocr.py:92 #: paperwork-gtk/src/paperwork_gtk/actions/doc/redo_ocr.py:105 #, python-brace-format msgid "OCR on {doc_id} p{page_idx}" msgstr "ROC sur {doc_id} p{page_idx}" #: paperwork-gtk/src/paperwork_gtk/actions/docs/delete.py:72 #, python-format msgid "Are you sure you want to delete %d documents ?" msgstr "Êtes-vous sûr de vouloir effacer %d documents ?" #: paperwork-gtk/src/paperwork_gtk/actions/docs/redo_ocr.py:83 #: paperwork-gtk/src/paperwork_gtk/actions/doc/redo_ocr.py:97 #, python-format msgid "OCR on %s" msgstr "ROC sur %s" #: paperwork-gtk/src/paperwork_gtk/actions/doc/delete.py:78 #, python-format msgid "Are you sure you want to delete document %s ?" msgstr "Êtes-vous sûr de vouloir effacer le document %s ?" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:405 #, python-format msgid "Estimated file size: %s" msgstr "Taille estimée du fichier : %s" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:628 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:108 msgid "Select a file or a directory to import" msgstr "Sélectionnez un fichier ou un répertoire à importer" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:637 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:128 msgid "Any files" msgstr "N'importe-quels fichiers" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:715 msgid "Export has failed" msgstr "L'export a échoué" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:81 msgid "Keyword(s)" msgstr "Mot(s)-clé(s)" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:102 msgid "No labels" msgstr "Pas d'étiquette" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:142 msgid "Label" msgstr "Étiquette" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:154 msgid "From:" msgstr "Du :" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:160 msgid "to:" msgstr "Au :" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:270 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/name.glade.h:1 msgid "Date" msgstr "Date" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:302 msgid "and" msgstr "et" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:303 msgid "or" msgstr "ou" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:320 msgid "not" msgstr "non" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:344 msgid "Remove" msgstr "Retirer" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/scan.py:118 #, python-format msgid "Scan from %s" msgstr "Scanner depuis %s" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:77 msgid "Import image or PDF file(s)" msgstr "Importer un ou plusieurs PDF(s) ou image(s)" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:78 msgid "Import file(s)" msgstr "Importer" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:122 msgid "All supported file formats" msgstr "Tous les formats de fichiers supportés" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/title.py:33 #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/name.py:59 msgid "New document" msgstr "Nouveau document" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/empty_doc/__init__.py:89 msgid "Empty" msgstr "Vide" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/boxes/__init__.py:83 msgid "Loading text ..." msgstr "Chargement du texte…" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/__init__.py:93 msgid "Loading page {}/{} ..." msgstr "Chargement de la page {}/{}…" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/__init__.py:622 #, python-format msgid "%d documents" msgstr "%d documents" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/thumbnailer.py:116 msgid "Loading document thumbnails" msgstr "Chargement des miniatures des documents" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:247 msgid "Renaming label" msgstr "Renommer l'étiquette" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:294 #, python-format msgid "Are you sure you want to delete label '%s' from ALL documents ?" msgstr "" "Êtes-vous sûr de vouloir supprimer l'étiquette '%s' de tous les documents ?" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:396 #, python-brace-format msgid "Changing label {old_label} into {new_label} on document {doc_id}" msgstr "" "Changement de l'étiquette {old_label} en {new_label} sur le document {doc_id}" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:462 #, python-brace-format msgid "Deleting label {old_label} from document {doc_id}" msgstr "Suppression de l'étiquette {old_label} du document {doc_id}" #: paperwork-gtk/src/paperwork_gtk/settings/update.py:60 msgid "Updates" msgstr "Mises à jour" #: paperwork-gtk/src/paperwork_gtk/settings/storage.py:84 msgid "Storage" msgstr "Stockage" #: paperwork-gtk/src/paperwork_gtk/settings/storage.py:90 msgid "Work Directory" msgstr "Répertoire de travail" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.py:64 msgid "Optical Character Recognition" msgstr "Reconnaissance Optique de Caractères" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.py:76 msgid "OCR disabled" msgstr "ROC désactivée" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.py:168 msgid "Loading ..." msgstr "Chargement…" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/dev_id_popover.py:67 msgid "No scanner" msgstr "Pas de scanner" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/resolution_popover.py:123 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:66 msgid "{} dpi" msgstr "{} ppp" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/resolution_popover.py:125 msgid "{} dpi (recommended)" msgstr "{} ppp (recommandé)" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:15 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:1 msgid "Color" msgstr "Couleur" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:16 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:2 msgid "Grayscale" msgstr "Niveaux de gris" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:17 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:3 msgid "Black & White" msgstr "Noir et blanc" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:62 msgid "No scanner selected" msgstr "Pas de scanner sélectionné" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:66 msgid "No resolution selected" msgstr "Pas de résolution sélectionnée" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:70 msgid "No mode selected" msgstr "Pas de mode sélectionné" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:113 msgid "Scanner" msgstr "Scanner" #: paperwork-gtk/src/paperwork_gtk/settings/stats.py:60 msgid "Help Improve Paperwork" msgstr "Aidez à améliorer Paperwork" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:33 msgid "Now with 10% more freedom in it !" msgstr "Maintenant avec 10% de libertés en plus dedans !" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:34 #, python-format msgid "Buy it now and get a 100% discount !" msgstr "Achetez-le maintenant et recevez une réduction de 100% !" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:35 msgid "New features and bugs available !" msgstr "Nouvelles fonctionnalités et nouveaux bugs disponibles !" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:36 msgid "New taste !" msgstr "Nouveau goût !" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:37 msgid "We replaced your old bugs with new bugs. Enjoy." msgstr "" "Nous avons remplacé vos anciens bugs par de nouveaux. Amusez-vous bien !" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:38 msgid "Smarter, Better, Stronger" msgstr "Smarter, Better, Stronger" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:40 msgid "It's better when it's free." msgstr "C'est meilleur quand c'est gratuit." #: paperwork-gtk/src/paperwork_gtk/update_notification.py:45 #, python-brace-format msgid "A new version of Paperwork is available: {new_version}" msgstr "Une nouvelle version de Paperwork est disponible : {new_version}" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:64 msgid "Introduction" msgstr "Introduction" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:65 msgid "User manual" msgstr "Manuel utilisateur" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:67 msgid "Documentation" msgstr "Documentation" #: paperwork-gtk/src/paperwork_gtk/about/about.glade.h:1 msgid "©2021" msgstr "©2021" #: paperwork-gtk/src/paperwork_gtk/about/about.glade.h:2 msgid "Sorting documents is a machine's job." msgstr "Trier des documents est un travail de machine." #: paperwork-gtk/src/paperwork_gtk/actions/page/move_to_doc/move_to_doc.glade.h:1 msgid "Select target document" msgstr "Sélectionnez le document cible" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:1 msgid "Export Steps" msgstr "Étapes d'export" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:2 msgid "Quality" msgstr "Qualité" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:3 msgid "Paper format" msgstr "Format de papier" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:4 msgid "Export Settings" msgstr "Réglages d'export" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:5 msgid "Send by email" msgstr "Envoyer par email" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:6 msgid "Preview" msgstr "Aperçu" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.glade.h:1 #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.glade.h:1 msgid "Search" msgstr "Recherche" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/suggestions.glade.h:1 msgid "Did you mean ?" msgstr "Vouliez-vous dire … ?" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.glade.h:2 msgid "Advanced search" msgstr "Recherche avancée" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/buttons.glade.h:1 msgid "Add page" msgstr "Ajouter une page" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/layout_settings.glade.h:1 msgid "Highlight words" msgstr "Surligner les mots" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.glade.h:1 msgid "Documents" msgstr "Documents" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.glade.h:2 msgid "Selection" msgstr "Sélection" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/extra_text.glade.h:1 msgid "Additional keywords" msgstr "Mots-clés additionnels" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/docproperties.glade.h:1 msgid "Properties" msgstr "Propriétés" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/label.glade.h:1 msgid "Change the label color" msgstr "Changer la couleur de l'étiquette" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/label.glade.h:2 msgid "Delete the label from all documents" msgstr "Effacer l'étiquette de tout les documents" #: paperwork-gtk/src/paperwork_gtk/settings/update.glade.h:1 msgid "" "Updates\n" "Check periodically for new versions of Paperwork" msgstr "" "Mises à jour\n" "Vérifier périodiquement si il y a de nouvelles " "versions de Paperwork" #: paperwork-gtk/src/paperwork_gtk/settings/update.glade.h:3 msgid "" "Look about once a week for new versions of " "Paperwork.\n" "You will be notified when a new version is available but it won't be " "installed automatically.\n" "" msgstr "" "Cherche de nouvelles versions de Paperwork environ " "1 fois par semaine.\n" "Vous serez notifié quand une nouvelle version sera disponible, mais elle ne " "sera pas installée automatiquement.\n" "" #: paperwork-gtk/src/paperwork_gtk/settings/stats.glade.h:1 msgid "" "Send metrics\n" "Give us clues about how you use Paperwork" msgstr "" "Envoyer des métriques\n" "Aidez-nous à comprendre comment vous utilisez " "Paperwork" #: paperwork-gtk/src/paperwork_gtk/settings/stats.glade.h:3 msgid "" "Those clues will help us to make Paperwork an even " "better piece of software, for you. Statistics also show us that people are " "actually using our work, keeping us motivated to improve it.\n" "\n" "Here are the data we gather:\n" "- Hardware: CPU, RAM, screen resolution.\n" "- Software: Version of Paperwork, Operating system, desktop environment, " "system language.\n" "- Data metrics: number of documents, maximum and average number of pages, " "number of labels.\n" "- Number of times you used each feature.\n" "\n" "We do not collect document content nor any other sensitive or personal " "information. Still we think it's fair to request your authorization ;-).\n" "\n" "Collected statistics are visible on openpaper.work.\n" "" msgstr "" "Ces métriques nous aideront à faire de Paperwork " "un logiciel encore meilleur, pour vous. Ces statistiques nous montre aussi " "que des gens se servent effectivement de notre travail, ce qui nous garde " "motivé pour l'améliorer.\n" "\n" "Voici les données que nous collectons :\n" "- Matériel : Processeur, mémoire vive, résolution d'écran.\n" "- Logiciel : Version de Paperwork, système d'exploitation, environnement " "graphique, langue du système.\n" "- Métriques : nombre de documents, nombre maximum et moyen de pages, nombre " "d'étiquettes.\n" "- Nombre de fois que vous avez utilisé chaque fonctionnalité.\n" "\n" "Nous ne collectons pas le contenu des documents ni d'autre informations " "sensibles, mais nous considérons tout de même normal de vous demander " "avant ;-).\n" "\n" "Les statistiques collectées sont visibles sur openpaper.work.\n" "" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.glade.h:1 msgid "Languages" msgstr "Langues" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.glade.h:1 msgid "Flatpak" msgstr "Flatpak" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.glade.h:2 msgid "" "You are using Paperwork from a Flatpak container. Paperwork needs Saned to " "access your scanners.\n" "\n" "Important: the following procedure will only work for local (non-network) " "scanners !\n" "\n" "To enable Saned on the host system, you must copy and paste the following " "commands in a terminal:" msgstr "" "Vous utilisez Paperwork depuis un conteneur Flatpak. Paperwork a besoin de " "Saned pour accéder à vos scanners.\n" "\n" "Important : la procédure qui suit ne fonctionnera que pour des scanners " "locaux (non-réseau) !\n" "\n" "Pour activer Saned sur le système hôte, vous devez copier et coller les " "commandes suivantes dans un terminal :" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:1 msgid "Maximize" msgstr "Maximiser" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:2 msgid "Automatic" msgstr "Automatique" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:3 msgid "Scan" msgstr "Scanner" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:4 msgid "Scanner Calibration" msgstr "Calibration du scanner" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:1 msgid "Device" msgstr "Périphérique" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:2 msgid "Resolution" msgstr "Résolution" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:3 msgid "Mode" msgstr "Mode" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:4 msgid "Calibration" msgstr "Calibration" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:5 msgid "Re-calibrate" msgstr "Recalibrer" #: paperwork-gtk/src/paperwork_gtk/settings/storage.glade.h:1 msgid "Work directory" msgstr "Répertoire de travail" #~ msgid "" #~ "You are using Paperwork from a Flatpak container. Paperwork needs Saned " #~ "to access your scanners. To enable Saned on the host system, you must " #~ "copy and paste the following commands in a terminal:" #~ msgstr "" #~ "Vous utilisez Paperwork depuis un conteneur Flatpak. Paperwork a besoin " #~ "de Saned pour accéder à votre scanner. Pour activer Saned sur le système, " #~ "vous devez copier et coller les commandes suivantes dans un terminal :" #, python-format #~ msgid "Are you sure you want to remove label '%s' from ALL documents ?" #~ msgstr "" #~ "Êtes-vous sûre de vouloir retirer l'étiquette '%s' de TOUS vos documents ?" #~ msgid "©2020" #~ msgstr "©2020" paperwork-2.1.1/paperwork-gtk/l10n/messages.pot000066400000000000000000000466031417573700700214530ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: paperwork-gtk/src/paperwork_gtk/cmd/install.py:40 msgid "Install Paperwork icons and shortcuts" msgstr "" #: paperwork-gtk/src/paperwork_gtk/cmd/install.py:44 msgid "Install everything only for the current user" msgstr "" #: paperwork-gtk/src/paperwork_gtk/cmd/import.py:35 msgid "Run Paperwork and import files passed as arguments into a new document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/cmd/import.py:39 msgid "URLs or paths of files to import" msgstr "" #: paperwork-gtk/src/paperwork_gtk/main.py:201 msgid "command" msgstr "" #: paperwork-gtk/src/paperwork_gtk/docimport.py:86 #, python-format msgid "Don't know how to import '%s'. Sorry." msgstr "" #: paperwork-gtk/src/paperwork_gtk/docimport.py:105 msgid "PDF password" msgstr "" #: paperwork-gtk/src/paperwork_gtk/docimport.py:123 msgid "No new document to import found" msgstr "" #: paperwork-gtk/src/paperwork_gtk/docimport.py:163 msgid "Imported file(s) deleted" msgstr "" #: paperwork-gtk/src/paperwork_gtk/docimport.py:170 msgid "Imported:\n" msgstr "" #: paperwork-gtk/src/paperwork_gtk/docimport.py:176 msgid "Import successful" msgstr "" #: paperwork-gtk/src/paperwork_gtk/docimport.py:185 msgid "Delete imported files" msgstr "" #: paperwork-gtk/src/paperwork_gtk/print.py:42 #, python-brace-format msgid "Loading {doc_id} p{page_idx} for printing" msgstr "" #: paperwork-gtk/src/paperwork_gtk/print.py:144 #, python-format msgid "Printing %s" msgstr "" #: paperwork-gtk/src/paperwork_gtk/print.py:179 #, python-brace-format msgid "Printing {doc_id} ({page_idx}/{nb_pages})" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/copy_text.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/edit.py:30 msgid "Page" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/copy_text.py:30 msgid "Copy selected text to clipboard" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/edit.py:30 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/actions.glade.h:1 msgid "Edit" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/app/find.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/new.py:30 msgid "Global" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/app/find.py:30 msgid "Find" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/properties.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/print.py:30 msgid "Document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/properties.py:30 msgid "Edit document properties" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:35 msgid "Document list" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:30 msgid "Open next document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:35 msgid "Open previous document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/print.py:30 msgid "Print" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/new.py:30 msgid "Create new document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/print.py:49 msgid "Print page" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/reset.py:49 msgid "Reset page" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/delete.py:49 msgid "Delete page" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_inside_doc.py:50 msgid "Another position" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_inside_doc.py:53 #: paperwork-gtk/src/paperwork_gtk/menus/page/move_to_doc.py:53 msgid "Move page to" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_to_doc.py:50 msgid "Another document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/copy_text.py:50 msgid "Copy selected text" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/export.py:49 msgid "Export page" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/redo_ocr.py:50 #: paperwork-gtk/src/paperwork_gtk/actions/page/redo_ocr.py:62 msgid "Redo OCR on page" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/docs/properties.py:56 msgid "Change labels" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/docs/delete.py:51 msgid "Delete" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/docs/select_all.py:51 msgid "Select all" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/docs/export.py:51 msgid "Export" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/docs/redo_ocr.py:51 msgid "Redo OCR" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_settings.py:43 #: paperwork-gtk/src/paperwork_gtk/settings/settings.glade.h:1 msgid "Settings" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/app/help.py:64 msgid "Help" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_shortcuts.py:45 msgid "Shortcuts" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_bug_report.py:49 msgid "Report bug" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_about.py:45 msgid "About" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/doc/properties.py:43 msgid "Document properties" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/doc/print.py:41 msgid "Print document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/doc/delete.py:40 msgid "Delete document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/doc/add_to_selection.py:41 msgid "Add to selection" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/doc/export.py:38 msgid "Export document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/doc/open_external.py:36 msgid "Open folder" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/doc/redo_ocr.py:40 msgid "Redo OCR on document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/delete.py:85 #, python-brace-format msgid "Are you sure you want to delete page {page_idx} of document {doc_id} ?" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:116 #, python-format msgid "Move the page %d to what position ?" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:135 #, python-format msgid "Invalid page position: %s" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:147 #, python-format msgid "Invalid page position: %d. Out of document bounds (1-%d)." msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:156 msgid "Page position unchanged" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/redo_ocr.py:100 #: paperwork-gtk/src/paperwork_gtk/actions/docs/redo_ocr.py:92 #: paperwork-gtk/src/paperwork_gtk/actions/doc/redo_ocr.py:105 #, python-brace-format msgid "OCR on {doc_id} p{page_idx}" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/docs/delete.py:72 #, python-format msgid "Are you sure you want to delete %d documents ?" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/docs/redo_ocr.py:83 #: paperwork-gtk/src/paperwork_gtk/actions/doc/redo_ocr.py:97 #, python-format msgid "OCR on %s" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/doc/delete.py:78 #, python-format msgid "Are you sure you want to delete document %s ?" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:405 #, python-format msgid "Estimated file size: %s" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:628 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:108 msgid "Select a file or a directory to import" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:637 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:128 msgid "Any files" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:715 msgid "Export has failed" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:81 msgid "Keyword(s)" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:102 msgid "No labels" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:142 msgid "Label" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:154 msgid "From:" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:160 msgid "to:" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:270 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/name.glade.h:1 msgid "Date" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:302 msgid "and" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:303 msgid "or" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:320 msgid "not" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:344 msgid "Remove" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/scan.py:118 #, python-format msgid "Scan from %s" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:77 msgid "Import image or PDF file(s)" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:78 msgid "Import file(s)" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:122 msgid "All supported file formats" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/title.py:33 #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/name.py:59 msgid "New document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/empty_doc/__init__.py:89 msgid "Empty" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/boxes/__init__.py:83 msgid "Loading text ..." msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/__init__.py:93 msgid "Loading page {}/{} ..." msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/__init__.py:622 #, python-format msgid "%d documents" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/thumbnailer.py:116 msgid "Loading document thumbnails" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:247 msgid "Renaming label" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:294 #, python-format msgid "Are you sure you want to delete label '%s' from ALL documents ?" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:396 #, python-brace-format msgid "Changing label {old_label} into {new_label} on document {doc_id}" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:462 #, python-brace-format msgid "Deleting label {old_label} from document {doc_id}" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/update.py:60 msgid "Updates" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/storage.py:84 msgid "Storage" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/storage.py:90 msgid "Work Directory" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.py:64 msgid "Optical Character Recognition" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.py:76 msgid "OCR disabled" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.py:168 msgid "Loading ..." msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/dev_id_popover.py:67 msgid "No scanner" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/resolution_popover.py:123 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:66 msgid "{} dpi" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/resolution_popover.py:125 msgid "{} dpi (recommended)" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:15 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:1 msgid "Color" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:16 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:2 msgid "Grayscale" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:17 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:3 msgid "Black & White" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:62 msgid "No scanner selected" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:66 msgid "No resolution selected" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:70 msgid "No mode selected" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:113 msgid "Scanner" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/stats.py:60 msgid "Help Improve Paperwork" msgstr "" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:33 msgid "Now with 10% more freedom in it !" msgstr "" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:34 #, python-format msgid "Buy it now and get a 100% discount !" msgstr "" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:35 msgid "New features and bugs available !" msgstr "" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:36 msgid "New taste !" msgstr "" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:37 msgid "We replaced your old bugs with new bugs. Enjoy." msgstr "" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:38 msgid "Smarter, Better, Stronger" msgstr "" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:40 msgid "It's better when it's free." msgstr "" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:45 #, python-brace-format msgid "A new version of Paperwork is available: {new_version}" msgstr "" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:64 msgid "Introduction" msgstr "" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:65 msgid "User manual" msgstr "" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:67 msgid "Documentation" msgstr "" #: paperwork-gtk/src/paperwork_gtk/about/about.glade.h:1 msgid "©2021" msgstr "" #: paperwork-gtk/src/paperwork_gtk/about/about.glade.h:2 msgid "Sorting documents is a machine's job." msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_to_doc/move_to_doc.glade.h:1 msgid "Select target document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:1 msgid "Export Steps" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:2 msgid "Quality" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:3 msgid "Paper format" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:4 msgid "Export Settings" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:5 msgid "Send by email" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:6 msgid "Preview" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.glade.h:1 #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.glade.h:1 msgid "Search" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/suggestions.glade.h:1 msgid "Did you mean ?" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.glade.h:2 msgid "Advanced search" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/buttons.glade.h:1 msgid "Add page" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/layout_settings.glade.h:1 msgid "Highlight words" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.glade.h:1 msgid "Documents" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.glade.h:2 msgid "Selection" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/extra_text.glade.h:1 msgid "Additional keywords" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/docproperties.glade.h:1 msgid "Properties" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/label.glade.h:1 msgid "Change the label color" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/label.glade.h:2 msgid "Delete the label from all documents" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/update.glade.h:1 msgid "" "Updates\n" "Check periodically for new versions of Paperwork" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/update.glade.h:3 msgid "" "Look about once a week for new versions of " "Paperwork.\n" "You will be notified when a new version is available but it won't be " "installed automatically.\n" "" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/stats.glade.h:1 msgid "" "Send metrics\n" "Give us clues about how you use Paperwork" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/stats.glade.h:3 msgid "" "Those clues will help us to make Paperwork an even " "better piece of software, for you. Statistics also show us that people are " "actually using our work, keeping us motivated to improve it.\n" "\n" "Here are the data we gather:\n" "- Hardware: CPU, RAM, screen resolution.\n" "- Software: Version of Paperwork, Operating system, desktop environment, " "system language.\n" "- Data metrics: number of documents, maximum and average number of pages, " "number of labels.\n" "- Number of times you used each feature.\n" "\n" "We do not collect document content nor any other sensitive or personal " "information. Still we think it's fair to request your authorization ;-).\n" "\n" "Collected statistics are visible on openpaper.work.\n" "" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.glade.h:1 msgid "Languages" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.glade.h:1 msgid "Flatpak" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.glade.h:2 msgid "" "You are using Paperwork from a Flatpak container. Paperwork needs Saned to " "access your scanners.\n" "\n" "Important: the following procedure will only work for local (non-network) " "scanners !\n" "\n" "To enable Saned on the host system, you must copy and paste the following " "commands in a terminal:" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:1 msgid "Maximize" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:2 msgid "Automatic" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:3 msgid "Scan" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:4 msgid "Scanner Calibration" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:1 msgid "Device" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:2 msgid "Resolution" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:3 msgid "Mode" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:4 msgid "Calibration" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:5 msgid "Re-calibrate" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/storage.glade.h:1 msgid "Work directory" msgstr "" paperwork-2.1.1/paperwork-gtk/l10n/oc.po000066400000000000000000000612301417573700700200520ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2021-10-27 22:23+0000\n" "Last-Translator: Quentin PAGÈS \n" "Language-Team: Occitan \n" "Language: oc\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 4.4\n" #: paperwork-gtk/src/paperwork_gtk/cmd/install.py:40 msgid "Install Paperwork icons and shortcuts" msgstr "Installar las icònas e acorchis de Paperwork" #: paperwork-gtk/src/paperwork_gtk/cmd/install.py:44 msgid "Install everything only for the current user" msgstr "Tot installar sonque per l'utilizaire actual" #: paperwork-gtk/src/paperwork_gtk/cmd/import.py:35 msgid "Run Paperwork and import files passed as arguments into a new document" msgstr "" "Executar Paperwork e importar los fichièrs donats en argument cap a un " "document novèl" #: paperwork-gtk/src/paperwork_gtk/cmd/import.py:39 msgid "URLs or paths of files to import" msgstr "URL o camin dels fichièrs d’importar" #: paperwork-gtk/src/paperwork_gtk/main.py:201 msgid "command" msgstr "comanda" #: paperwork-gtk/src/paperwork_gtk/docimport.py:86 #, python-format msgid "Don't know how to import '%s'. Sorry." msgstr "Sap pas cossí importar « %s ». Desconsolat." #: paperwork-gtk/src/paperwork_gtk/docimport.py:105 msgid "PDF password" msgstr "Senhal del PDF" #: paperwork-gtk/src/paperwork_gtk/docimport.py:123 msgid "No new document to import found" msgstr "Cap de document novèl d'importar pas trobat" #: paperwork-gtk/src/paperwork_gtk/docimport.py:163 msgid "Imported file(s) deleted" msgstr "Fichiè(s) importat(s) suprimit(s)" #: paperwork-gtk/src/paperwork_gtk/docimport.py:170 msgid "Imported:\n" msgstr "Importat(s) :\n" #: paperwork-gtk/src/paperwork_gtk/docimport.py:176 msgid "Import successful" msgstr "Importacion capitada" #: paperwork-gtk/src/paperwork_gtk/docimport.py:185 msgid "Delete imported files" msgstr "Suprimir los fichièrs importats" #: paperwork-gtk/src/paperwork_gtk/print.py:42 #, python-brace-format msgid "Loading {doc_id} p{page_idx} for printing" msgstr "Cargament de {doc_id} p{page_idx} per impression" #: paperwork-gtk/src/paperwork_gtk/print.py:144 #, python-format msgid "Printing %s" msgstr "Impression de « %s »" #: paperwork-gtk/src/paperwork_gtk/print.py:179 #, python-brace-format msgid "Printing {doc_id} ({page_idx}/{nb_pages})" msgstr "Impression de {doc_id} ({page_idx}/{nb_pages})" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/copy_text.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/edit.py:30 msgid "Page" msgstr "Pagina" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/copy_text.py:30 msgid "Copy selected text to clipboard" msgstr "Copiar lo tèxte seleccionat dins lo quichapapièr" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/edit.py:30 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/actions.glade.h:1 msgid "Edit" msgstr "Edicion" #: paperwork-gtk/src/paperwork_gtk/shortcuts/app/find.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/new.py:30 msgid "Global" msgstr "General" #: paperwork-gtk/src/paperwork_gtk/shortcuts/app/find.py:30 msgid "Find" msgstr "Trobar" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/properties.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/print.py:30 msgid "Document" msgstr "Document" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/properties.py:30 msgid "Edit document properties" msgstr "Modificar las proprietats del document" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:35 msgid "Document list" msgstr "Lista de documents" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:30 msgid "Open next document" msgstr "Dobrir lo document seguent" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:35 msgid "Open previous document" msgstr "Dobrir lo document precedent" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/print.py:30 msgid "Print" msgstr "Imprimir" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/new.py:30 msgid "Create new document" msgstr "Crear un document novèl" #: paperwork-gtk/src/paperwork_gtk/menus/page/print.py:49 msgid "Print page" msgstr "Imprimir la pagina" #: paperwork-gtk/src/paperwork_gtk/menus/page/reset.py:49 msgid "Reset page" msgstr "Reïnicializar la pagina" #: paperwork-gtk/src/paperwork_gtk/menus/page/delete.py:49 msgid "Delete page" msgstr "Suprimir la pagina" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_inside_doc.py:50 msgid "Another position" msgstr "Una autra posicion" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_inside_doc.py:53 #: paperwork-gtk/src/paperwork_gtk/menus/page/move_to_doc.py:53 msgid "Move page to" msgstr "Desplaçar la pagina a" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_to_doc.py:50 msgid "Another document" msgstr "Un autre document" #: paperwork-gtk/src/paperwork_gtk/menus/page/copy_text.py:50 msgid "Copy selected text" msgstr "Copiar lo tèxt seleccionat" #: paperwork-gtk/src/paperwork_gtk/menus/page/export.py:49 msgid "Export page" msgstr "Exportar la pagina" #: paperwork-gtk/src/paperwork_gtk/menus/page/redo_ocr.py:50 #: paperwork-gtk/src/paperwork_gtk/actions/page/redo_ocr.py:62 msgid "Redo OCR on page" msgstr "Tornar far la ROC sus la pagina" #: paperwork-gtk/src/paperwork_gtk/menus/docs/properties.py:56 msgid "Change labels" msgstr "Cambiar las etiquetas" #: paperwork-gtk/src/paperwork_gtk/menus/docs/delete.py:51 msgid "Delete" msgstr "Suprimir" #: paperwork-gtk/src/paperwork_gtk/menus/docs/select_all.py:51 msgid "Select all" msgstr "Seleccionar tot" #: paperwork-gtk/src/paperwork_gtk/menus/docs/export.py:51 msgid "Export" msgstr "Exportar" #: paperwork-gtk/src/paperwork_gtk/menus/docs/redo_ocr.py:51 msgid "Redo OCR" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_settings.py:43 #: paperwork-gtk/src/paperwork_gtk/settings/settings.glade.h:1 msgid "Settings" msgstr "Paramètres" #: paperwork-gtk/src/paperwork_gtk/menus/app/help.py:64 msgid "Help" msgstr "Ajuda" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_shortcuts.py:45 msgid "Shortcuts" msgstr "Acorchis" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_bug_report.py:49 msgid "Report bug" msgstr "Senhalar un bug" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_about.py:45 msgid "About" msgstr "A prepaus" #: paperwork-gtk/src/paperwork_gtk/menus/doc/properties.py:43 msgid "Document properties" msgstr "Proprietats del document" #: paperwork-gtk/src/paperwork_gtk/menus/doc/print.py:41 msgid "Print document" msgstr "Imprimir lo document" #: paperwork-gtk/src/paperwork_gtk/menus/doc/delete.py:40 msgid "Delete document" msgstr "Suprimir lo document" #: paperwork-gtk/src/paperwork_gtk/menus/doc/add_to_selection.py:41 msgid "Add to selection" msgstr "Ajustar a la seleccion" #: paperwork-gtk/src/paperwork_gtk/menus/doc/export.py:38 msgid "Export document" msgstr "Exportar lo document" #: paperwork-gtk/src/paperwork_gtk/menus/doc/open_external.py:36 msgid "Open folder" msgstr "Dobrir lo dossièr" #: paperwork-gtk/src/paperwork_gtk/menus/doc/redo_ocr.py:40 msgid "Redo OCR on document" msgstr "Tornar far la ROC sul document" #: paperwork-gtk/src/paperwork_gtk/actions/page/delete.py:85 #, python-brace-format msgid "Are you sure you want to delete page {page_idx} of document {doc_id} ?" msgstr "" "Volètz vertadièrament suprimir la pagina {page_idx} del document {doc_id} ?" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:116 #, python-format msgid "Move the page %d to what position ?" msgstr "Ont desplaçar la pagina %d ?" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:135 #, python-format msgid "Invalid page position: %s" msgstr "Posicion de pagina invalida : %s" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:147 #, python-format msgid "Invalid page position: %d. Out of document bounds (1-%d)." msgstr "Posicion de pagina invalida : %d. Defòra limitas del document (1-%d)." #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:156 msgid "Page position unchanged" msgstr "Posicion de la pagina pas cambiada" #: paperwork-gtk/src/paperwork_gtk/actions/page/redo_ocr.py:100 #: paperwork-gtk/src/paperwork_gtk/actions/docs/redo_ocr.py:92 #: paperwork-gtk/src/paperwork_gtk/actions/doc/redo_ocr.py:105 #, python-brace-format msgid "OCR on {doc_id} p{page_idx}" msgstr "ROC sus {doc_id} p{page_idx}" #: paperwork-gtk/src/paperwork_gtk/actions/docs/delete.py:72 #, python-format msgid "Are you sure you want to delete %d documents ?" msgstr "Volètz vertadièrament suprimir %d documents ?" #: paperwork-gtk/src/paperwork_gtk/actions/docs/redo_ocr.py:83 #: paperwork-gtk/src/paperwork_gtk/actions/doc/redo_ocr.py:97 #, python-format msgid "OCR on %s" msgstr "ROC sus %s" #: paperwork-gtk/src/paperwork_gtk/actions/doc/delete.py:78 #, python-format msgid "Are you sure you want to delete document %s ?" msgstr "Volètz vertadièrament suprimir lo document %s ?" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:405 #, python-format msgid "Estimated file size: %s" msgstr "Talha estimada del fichièr : %s" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:628 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:108 msgid "Select a file or a directory to import" msgstr "Seleccionatz un fichièr o repertòri d'importar" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:637 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:128 msgid "Any files" msgstr "Quin que siá fichièr" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:715 msgid "Export has failed" msgstr "L’export a pas reüssit" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:81 msgid "Keyword(s)" msgstr "Mot(s)-clau" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:102 msgid "No labels" msgstr "Cap d’etiqueta" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:142 msgid "Label" msgstr "Etiqueta" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:154 msgid "From:" msgstr "A partir de :" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:160 msgid "to:" msgstr "Al :" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:270 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/name.glade.h:1 msgid "Date" msgstr "Data" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:302 msgid "and" msgstr "e" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:303 msgid "or" msgstr "o" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:320 msgid "not" msgstr "pas" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:344 msgid "Remove" msgstr "Levar" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/scan.py:118 #, python-format msgid "Scan from %s" msgstr "Numerizar a partir de %s" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:77 msgid "Import image or PDF file(s)" msgstr "Importar un o mantun PDF o imatges" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:78 msgid "Import file(s)" msgstr "Importar" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:122 msgid "All supported file formats" msgstr "Totes los formats de fichièrs compatibles" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/title.py:33 #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/name.py:59 msgid "New document" msgstr "Document novèl" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/empty_doc/__init__.py:89 msgid "Empty" msgstr "Void" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/boxes/__init__.py:83 msgid "Loading text ..." msgstr "Cargament del tèxt…" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/__init__.py:93 msgid "Loading page {}/{} ..." msgstr "Cargament de la pagina {}/{}…" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/__init__.py:622 #, python-format msgid "%d documents" msgstr "%d documents" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/thumbnailer.py:116 msgid "Loading document thumbnails" msgstr "Cargament de las miniaturas dels documents" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:247 msgid "Renaming label" msgstr "Renomenar l'etiqueta" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:294 #, python-format msgid "Are you sure you want to delete label '%s' from ALL documents ?" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:396 #, python-brace-format msgid "Changing label {old_label} into {new_label} on document {doc_id}" msgstr "" "Cambiament de l'etiqueta {old_label} en {new_label} sul document {doc_id}" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:462 #, python-brace-format msgid "Deleting label {old_label} from document {doc_id}" msgstr "Supression de l'etiqueta {old_label} del document {doc_id}" #: paperwork-gtk/src/paperwork_gtk/settings/update.py:60 msgid "Updates" msgstr "Mesas a jorn" #: paperwork-gtk/src/paperwork_gtk/settings/storage.py:84 msgid "Storage" msgstr "Emmagazinatge" #: paperwork-gtk/src/paperwork_gtk/settings/storage.py:90 msgid "Work Directory" msgstr "Repertòri de trabalh" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.py:64 msgid "Optical Character Recognition" msgstr "Reconeissença Optica de Caractèrs" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.py:76 msgid "OCR disabled" msgstr "ROC desactivada" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.py:168 msgid "Loading ..." msgstr "Cargament..." #: paperwork-gtk/src/paperwork_gtk/settings/scanner/dev_id_popover.py:67 msgid "No scanner" msgstr "Cap de numerizador" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/resolution_popover.py:123 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:66 msgid "{} dpi" msgstr "{} ppp" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/resolution_popover.py:125 msgid "{} dpi (recommended)" msgstr "{} ppp (recomandat)" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:15 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:1 msgid "Color" msgstr "Color" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:16 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:2 msgid "Grayscale" msgstr "Nivèls de gris" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:17 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:3 msgid "Black & White" msgstr "Blanc e negre" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:62 msgid "No scanner selected" msgstr "Cap de numerizador pas seleccionat" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:66 msgid "No resolution selected" msgstr "Cap de resolucion pas seleccionada" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:70 msgid "No mode selected" msgstr "Cap de mòde pas seleccionat" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:113 msgid "Scanner" msgstr "Numerizador" #: paperwork-gtk/src/paperwork_gtk/settings/stats.py:60 msgid "Help Improve Paperwork" msgstr "Ajudatz a melhorar Paperwork" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:33 msgid "Now with 10% more freedom in it !" msgstr "Ara amb 10% mai de libertats dedins !" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:34 #, python-format msgid "Buy it now and get a 100% discount !" msgstr "Crompatz-lo ara e recebètz una reduccion de 100% !" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:35 msgid "New features and bugs available !" msgstr "Nòvas foncionalitats e nòus bugs disponibles !" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:36 msgid "New taste !" msgstr "Nòu gost !" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:37 msgid "We replaced your old bugs with new bugs. Enjoy." msgstr "Remplacèrem vòstres ancians bugs per de nòus. Divertissètz-vos ben !" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:38 msgid "Smarter, Better, Stronger" msgstr "Smarter, Better, Stronger" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:40 msgid "It's better when it's free." msgstr "Es melhor quand es gratuit." #: paperwork-gtk/src/paperwork_gtk/update_notification.py:45 #, python-brace-format msgid "A new version of Paperwork is available: {new_version}" msgstr "Una version novèla de Paperwork es disponibla : {new_version}" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:64 msgid "Introduction" msgstr "Introduccion" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:65 msgid "User manual" msgstr "Mòde d'emplec" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:67 msgid "Documentation" msgstr "Documentacion" #: paperwork-gtk/src/paperwork_gtk/about/about.glade.h:1 msgid "©2021" msgstr "©2021" #: paperwork-gtk/src/paperwork_gtk/about/about.glade.h:2 msgid "Sorting documents is a machine's job." msgstr "Triar los documents es un trabalh per las maquinas." #: paperwork-gtk/src/paperwork_gtk/actions/page/move_to_doc/move_to_doc.glade.h:1 msgid "Select target document" msgstr "Seleccionar lo document cibla" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:1 msgid "Export Steps" msgstr "Etapas de l'export" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:2 msgid "Quality" msgstr "Qualitat" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:3 msgid "Paper format" msgstr "Format pagina" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:4 msgid "Export Settings" msgstr "Paramètres de l'export" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:5 msgid "Send by email" msgstr "Enviar per corrièl" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:6 msgid "Preview" msgstr "Apercebut" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.glade.h:1 #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.glade.h:1 msgid "Search" msgstr "Recercar" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/suggestions.glade.h:1 msgid "Did you mean ?" msgstr "Voliatz dire… ?" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.glade.h:2 msgid "Advanced search" msgstr "Recèrca avançada" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/buttons.glade.h:1 msgid "Add page" msgstr "Ajustar una pagina" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/layout_settings.glade.h:1 msgid "Highlight words" msgstr "Suslinhar los mots" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.glade.h:1 msgid "Documents" msgstr "Documents" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.glade.h:2 msgid "Selection" msgstr "Seleccion" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/extra_text.glade.h:1 msgid "Additional keywords" msgstr "Mots-clau addicionals" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/docproperties.glade.h:1 msgid "Properties" msgstr "Proprietats" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/label.glade.h:1 msgid "Change the label color" msgstr "Cambiar la color de l'etiqueta" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/label.glade.h:2 msgid "Delete the label from all documents" msgstr "Suprimir l'etiqueta de totes los documents" #: paperwork-gtk/src/paperwork_gtk/settings/update.glade.h:1 msgid "" "Updates\n" "Check periodically for new versions of Paperwork" msgstr "" "Mesas a jorn\n" "Verificatz de temps en temps se i a de mesas a " "jorn per Paperwork" #: paperwork-gtk/src/paperwork_gtk/settings/update.glade.h:3 msgid "" "Look about once a week for new versions of " "Paperwork.\n" "You will be notified when a new version is available but it won't be " "installed automatically.\n" "" msgstr "" "Cèrca las versions novèlas de Paperwork cada " "setmana.\n" "Seretz avisat quand una version novèla es disponibla, mas serà pas " "installada automaticament.\n" "" #: paperwork-gtk/src/paperwork_gtk/settings/stats.glade.h:1 msgid "" "Send metrics\n" "Give us clues about how you use Paperwork" msgstr "" "Enviar las estatisticas\n" "Ajudatz-nos a comprendre coma utilizatz Paperwork" #: paperwork-gtk/src/paperwork_gtk/settings/stats.glade.h:3 msgid "" "Those clues will help us to make Paperwork an even " "better piece of software, for you. Statistics also show us that people are " "actually using our work, keeping us motivated to improve it.\n" "\n" "Here are the data we gather:\n" "- Hardware: CPU, RAM, screen resolution.\n" "- Software: Version of Paperwork, Operating system, desktop environment, " "system language.\n" "- Data metrics: number of documents, maximum and average number of pages, " "number of labels.\n" "- Number of times you used each feature.\n" "\n" "We do not collect document content nor any other sensitive or personal " "information. Still we think it's fair to request your authorization ;-).\n" "\n" "Collected statistics are visible on openpaper.work.\n" "" msgstr "" "Aquestas estatisticas nos ajudaràn a far que " "Paperwork sia un logicial encara melhor. Nos mostran tanben que monde " "utilizan vertadièrament de nòstre trabalh, nos garda motivat per lo " "melhorar.\n" "\n" "Vaquí las donadas que reculhissèm :\n" "- Material : processor, memòria viva, resolucion d’ecran.\n" "- Logicial : version de Paperwork, sistèma operatiu, environament grafic, " "lenga del sistèma.\n" "- Estatisticas : nombre de documents, nombre maximum e mejana de paginas, " "nombre d’etiquetas.\n" "- Nombre de còp qu’utilizatz cada foncionalitat.\n" "\n" "Collectam pas cap de contengut dels documents nimai informacions sensiblas, " "mas pensam qu’es normal ça que là de vos demandar abans ;-)\n" "\n" "Las estatisticas collectadas son visiblas sus openpaper.work.\n" "" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.glade.h:1 msgid "Languages" msgstr "Lengas" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.glade.h:1 msgid "Flatpak" msgstr "Flatpak" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.glade.h:2 msgid "" "You are using Paperwork from a Flatpak container. Paperwork needs Saned to " "access your scanners.\n" "\n" "Important: the following procedure will only work for local (non-network) " "scanners !\n" "\n" "To enable Saned on the host system, you must copy and paste the following " "commands in a terminal:" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:1 msgid "Maximize" msgstr "Maximizar" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:2 msgid "Automatic" msgstr "Automatic" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:3 msgid "Scan" msgstr "Numerizar" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:4 msgid "Scanner Calibration" msgstr "Calibratge del numerizador" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:1 msgid "Device" msgstr "Periferic" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:2 msgid "Resolution" msgstr "Resolucion" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:3 msgid "Mode" msgstr "Mòde" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:4 msgid "Calibration" msgstr "Calibratge" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:5 msgid "Re-calibrate" msgstr "Recalibrar" #: paperwork-gtk/src/paperwork_gtk/settings/storage.glade.h:1 msgid "Work directory" msgstr "Repertòri de trabalh" #~ msgid "" #~ "You are using Paperwork from a Flatpak container. Paperwork needs Saned " #~ "to access your scanners. To enable Saned on the host system, you must " #~ "copy and paste the following commands in a terminal:" #~ msgstr "" #~ "Utilizatz Paperwork d’un contenedor Flatpak estant. Paperwork requerís " #~ "Saned per accedir al numerizador. Per activar Saned sul sistèma, vos cal " #~ "copiar pegar las comandas seguentas dins un terminal :" #~ msgid "©2020" #~ msgstr "©2020" paperwork-2.1.1/paperwork-gtk/l10n/sv.po000066400000000000000000000524211417573700700201030ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-09-01 22:01+0200\n" "PO-Revision-Date: 2021-01-04 15:31+0000\n" "Last-Translator: Åke Engelbrektson \n" "Language-Team: Swedish \n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.4\n" #: paperwork-gtk/src/paperwork_gtk/docimport.py:82 #, python-format msgid "Don't know how to import '%s'. Sorry." msgstr "Vet inte hur man importerar \"%s\", tyvärr." #: paperwork-gtk/src/paperwork_gtk/docimport.py:99 msgid "No new document to import found" msgstr "Inga nya dokument för import hittades" #: paperwork-gtk/src/paperwork_gtk/docimport.py:139 msgid "Imported file(s) deleted" msgstr "Importerad(e) fil(er) borttagna" #: paperwork-gtk/src/paperwork_gtk/docimport.py:146 msgid "Imported:\n" msgstr "Importerat:\n" #: paperwork-gtk/src/paperwork_gtk/docimport.py:152 msgid "Import successful" msgstr "Import slutförd" #: paperwork-gtk/src/paperwork_gtk/docimport.py:161 msgid "Delete imported files" msgstr "Ta bort importerade filer" #: paperwork-gtk/src/paperwork_gtk/print.py:42 #, python-brace-format msgid "Loading {doc_id} p{page_idx} for printing" msgstr "Läser in {doc_id} s.{page_idx} för utskrift" #: paperwork-gtk/src/paperwork_gtk/print.py:144 #, python-format msgid "Printing %s" msgstr "Skriver ut %s" #: paperwork-gtk/src/paperwork_gtk/print.py:179 #, python-brace-format msgid "Printing {doc_id} ({page_idx}/{nb_pages})" msgstr "Skriver ut {doc_id} ({page_idx}/{nb_pages})" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:9 msgid "Now with 10% more freedom in it !" msgstr "Nu med 10% mer frihet i det!" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:10 #, python-format msgid "Buy it now and get a 100% discount !" msgstr "Köp den nu och få 100% rabatt!" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:11 msgid "New features and bugs available !" msgstr "Nya funktioner och fel tillgängliga!" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:12 msgid "It's better when it's free." msgstr "Det är bättre när det är fritt." #: paperwork-gtk/src/paperwork_gtk/update_notification.py:13 msgid "New taste !" msgstr "Ny smak!" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:14 msgid "We replaced your old bugs with new bugs. Enjoy." msgstr "Vi ersatte dina gamla fel med nya fel. Håll tillgodo." #: paperwork-gtk/src/paperwork_gtk/update_notification.py:15 msgid "Smarter, Better, Stronger" msgstr "Smartare, Bättre, Starkare" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:45 #, python-brace-format msgid "A new version of Paperwork is available: {new_version}" msgstr "En ny version av Paperwork finns tillgänglig: {new_version}" #: paperwork-gtk/src/paperwork_gtk/settings/update.py:60 msgid "Updates" msgstr "Uppdateringar" #: paperwork-gtk/src/paperwork_gtk/settings/storage.py:84 msgid "Storage" msgstr "Lagring" #: paperwork-gtk/src/paperwork_gtk/settings/storage.py:90 msgid "Work Directory" msgstr "Arbetsmapp" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.py:64 msgid "Optical Character Recognition" msgstr "Optisk teckenidentifiering" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.py:76 msgid "OCR disabled" msgstr "OCR inaktiverat" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:15 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:1 msgid "Color" msgstr "Färg" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:16 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:2 msgid "Grayscale" msgstr "Gråskala" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:17 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:3 msgid "Black & White" msgstr "Svartvit" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:27 msgid "No scanner selected" msgstr "Ingen skanner vald" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:31 msgid "No resolution selected" msgstr "Ingen upplösning vald" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:31 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/resolution_popover.py:123 msgid "{} dpi" msgstr "{} dpi" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:35 msgid "No mode selected" msgstr "Inget läge valt" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:104 msgid "Scanner" msgstr "Skanner" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/dev_id_popover.py:67 msgid "No scanner" msgstr "Ingen skanner" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.py:154 msgid "Loading ..." msgstr "Läser in..." #: paperwork-gtk/src/paperwork_gtk/settings/scanner/resolution_popover.py:125 msgid "{} dpi (recommended)" msgstr "{} dpi (rekommenderas)" #: paperwork-gtk/src/paperwork_gtk/settings/stats.py:60 msgid "Help Improve Paperwork" msgstr "Hjälp till att förbättra Paperwork" #: paperwork-gtk/src/paperwork_gtk/cmd/install.py:40 msgid "Install Paperwork icons and shortcuts" msgstr "Installera Paperworks ikoner och genvägar" #: paperwork-gtk/src/paperwork_gtk/cmd/install.py:44 msgid "Install everything only for the current user" msgstr "Installera endast för aktuell användare" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/edit.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/copy_text.py:30 msgid "Page" msgstr "Sida" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/edit.py:30 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/actions.glade.h:1 msgid "Edit" msgstr "Redigera" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/copy_text.py:30 msgid "Copy selected text to clipboard" msgstr "Kopiera markerad text till urklipp" #: paperwork-gtk/src/paperwork_gtk/shortcuts/app/find.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/new.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:35 msgid "Global" msgstr "Övergripande" #: paperwork-gtk/src/paperwork_gtk/shortcuts/app/find.py:30 msgid "Find" msgstr "Sök" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/print.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/properties.py:30 msgid "Document" msgstr "Dokument" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/print.py:30 msgid "Print" msgstr "Skriv ut" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/new.py:30 msgid "Create new document" msgstr "Skapa nytt dokument" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/properties.py:30 msgid "Edit document properties" msgstr "Redigera dokumentegenskaper" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:30 msgid "Document list" msgstr "Dokumentlista" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:30 msgid "Open next document" msgstr "Öppna nästa dokument" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:35 msgid "Open previous document" msgstr "Öppna föregående dokument" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:372 #, python-format msgid "Estimated file size: %s" msgstr "Beräknad filstorlek: %s" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:593 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:104 msgid "Select a file or a directory to import" msgstr "Välj en fil eller mapp att importera" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:602 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:124 msgid "Any files" msgstr "Alla filer" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/__init__.py:613 #, python-format msgid "%d documents" msgstr "%d dokument" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/name.py:59 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/title.py:23 msgid "New document" msgstr "Nytt dokument" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/thumbnailer.py:113 msgid "Loading document thumbnails" msgstr "Läser in dokumentminiatyrer" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:78 msgid "Keyword(s)" msgstr "Nyckelord" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:131 msgid "Label" msgstr "Etikett" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:143 msgid "From:" msgstr "Från:" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:149 msgid "to:" msgstr "till:" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:259 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/name.glade.h:1 msgid "Date" msgstr "Datum" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:291 msgid "and" msgstr "och" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:292 msgid "or" msgstr "eller" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:309 msgid "not" msgstr "inte" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:333 msgid "Remove" msgstr "Ta bort" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:247 msgid "Renaming label" msgstr "Byter namn på etikett" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:378 #, python-brace-format msgid "Changing label {old_label} into {new_label} on document {doc_id}" msgstr "Ändrar etikett {old_label} till {new_label} på dokument {doc_id}" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:444 #, python-brace-format msgid "Deleting label {old_label} from document {doc_id}" msgstr "Tar bort etikett {old_label} från dokument {doc_id}" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/empty_doc/__init__.py:89 msgid "Empty" msgstr "Tom" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/__init__.py:88 msgid "Loading page {}/{} ..." msgstr "Läser in sida {}/{}..." #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/boxes/__init__.py:83 msgid "Loading text ..." msgstr "Läser in text..." #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/scan.py:118 #, python-format msgid "Scan from %s" msgstr "Skanna från %s" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:75 msgid "Import image or PDF file(s)" msgstr "Importera bild- eller PDF-fil(er)" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:76 msgid "Import file(s)" msgstr "Importera fil(er)" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:118 msgid "All supported file formats" msgstr "Alla filformat som stöds" #: paperwork-gtk/src/paperwork_gtk/main.py:197 msgid "command" msgstr "kommando" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:64 msgid "Introduction" msgstr "Introduktion" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:65 msgid "User manual" msgstr "Användarmanual" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:67 msgid "Documentation" msgstr "Dokumentation" #: paperwork-gtk/src/paperwork_gtk/actions/page/delete.py:85 #, python-brace-format msgid "Are you sure you want to delete page {page_idx} of document {doc_id} ?" msgstr "Vill du verkligen ta bort sidan {page_idx} från dokument {doc_id}?" #: paperwork-gtk/src/paperwork_gtk/actions/page/redo_ocr.py:62 #: paperwork-gtk/src/paperwork_gtk/menus/page/redo_ocr.py:50 msgid "Redo OCR on page" msgstr "Gör om OCR på sida" #: paperwork-gtk/src/paperwork_gtk/actions/page/redo_ocr.py:100 #: paperwork-gtk/src/paperwork_gtk/actions/doc/redo_ocr.py:105 #, python-brace-format msgid "OCR on {doc_id} p{page_idx}" msgstr "OCR på {doc_id} s.{page_idx}" #: paperwork-gtk/src/paperwork_gtk/actions/docs/delete.py:72 #, python-format msgid "Are you sure you want to delete %d documents ?" msgstr "Vill du verkligen ta bort %d dokument?" #: paperwork-gtk/src/paperwork_gtk/actions/doc/delete.py:78 #, python-format msgid "Are you sure you want to delete document %s ?" msgstr "Vill du verkligen ta bort dokument %s?" #: paperwork-gtk/src/paperwork_gtk/actions/doc/redo_ocr.py:97 #, python-format msgid "OCR on %s" msgstr "OCR på %s" #: paperwork-gtk/src/paperwork_gtk/menus/page/print.py:49 msgid "Print page" msgstr "Skriv ut sida" #: paperwork-gtk/src/paperwork_gtk/menus/page/export.py:49 msgid "Export page" msgstr "Exportera sida" #: paperwork-gtk/src/paperwork_gtk/menus/page/delete.py:49 msgid "Delete page" msgstr "Ta bort sida" #: paperwork-gtk/src/paperwork_gtk/menus/page/reset.py:49 msgid "Reset page" msgstr "Återställ sida" #: paperwork-gtk/src/paperwork_gtk/menus/page/copy_text.py:50 msgid "Copy selected text" msgstr "Kopiera markerad text" #: paperwork-gtk/src/paperwork_gtk/menus/app/help.py:62 msgid "Help" msgstr "Hjälp" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_bug_report.py:49 msgid "Report bug" msgstr "Rapportera fel" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_shortcuts.py:45 msgid "Shortcuts" msgstr "Genvägar" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_settings.py:43 #: paperwork-gtk/src/paperwork_gtk/settings/settings.glade.h:1 msgid "Settings" msgstr "Inställningar" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_about.py:45 msgid "About" msgstr "Om" #: paperwork-gtk/src/paperwork_gtk/menus/docs/export.py:51 msgid "Export" msgstr "Exportera" #: paperwork-gtk/src/paperwork_gtk/menus/docs/select_all.py:51 msgid "Select all" msgstr "Markera alla" #: paperwork-gtk/src/paperwork_gtk/menus/docs/delete.py:51 msgid "Delete" msgstr "Ta bort" #: paperwork-gtk/src/paperwork_gtk/menus/docs/properties.py:56 msgid "Change labels" msgstr "Ändra etiketter" #: paperwork-gtk/src/paperwork_gtk/menus/doc/print.py:41 msgid "Print document" msgstr "Skriv ut dokument" #: paperwork-gtk/src/paperwork_gtk/menus/doc/export.py:38 msgid "Export document" msgstr "Exportera dokument" #: paperwork-gtk/src/paperwork_gtk/menus/doc/delete.py:40 msgid "Delete document" msgstr "Ta bort dokument" #: paperwork-gtk/src/paperwork_gtk/menus/doc/redo_ocr.py:40 msgid "Redo OCR on document" msgstr "Gör om OCR på dokument" #: paperwork-gtk/src/paperwork_gtk/menus/doc/properties.py:43 msgid "Document properties" msgstr "Dokumentegenskaper" #: paperwork-gtk/src/paperwork_gtk/menus/doc/add_to_selection.py:41 msgid "Add to selection" msgstr "Lägg till samlingen" #: paperwork-gtk/src/paperwork_gtk/menus/doc/open_external.py:36 msgid "Open folder" msgstr "Öppna mapp" #: paperwork-gtk/src/paperwork_gtk/settings/update.glade.h:1 msgid "" "Updates\n" "Check periodically for new versions of Paperwork" msgstr "" "Uppdateringar\n" "Sök med jämna mellanrum efter nya versioner av " "Paperwork" #: paperwork-gtk/src/paperwork_gtk/settings/update.glade.h:3 msgid "" "Look about once a week for new versions of " "Paperwork.\n" "You will be notified when a new version is available but it won't be " "installed automatically.\n" "" msgstr "" "Kolla ungefär en gång i veckan efter nya versioner " "av Paperwork.\n" "Du meddelas när en ny version finns tillgänglig men den installeras inte " "automatiskt.\n" "" #: paperwork-gtk/src/paperwork_gtk/settings/storage.glade.h:1 msgid "Work directory" msgstr "Arbetsmapp" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.glade.h:1 msgid "Languages" msgstr "Språk" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:1 msgid "Maximize" msgstr "Maximera" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:2 msgid "Automatic" msgstr "Automatiskt" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:3 msgid "Scan" msgstr "Skanna" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:4 msgid "Scanner Calibration" msgstr "Skannerkalibrering" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:1 msgid "Device" msgstr "Enhet" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:2 msgid "Resolution" msgstr "Upplösning" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:3 msgid "Mode" msgstr "Läge" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:4 msgid "Calibration" msgstr "Kalibrering" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:5 msgid "Re-calibrate" msgstr "Kalibrera om" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.glade.h:1 msgid "Flatpak" msgstr "Flatpak" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.glade.h:2 msgid "" "You are using Paperwork from a Flatpak container. Paperwork needs Saned to " "access your scanners. To enable Saned on the host system, you must copy and " "paste the following commands in a terminal:" msgstr "" "Du använder Paperwork från en Flatpak-behållare. Paperwork behöver Saned för " "åtkomst till dina skannrar. Du måste kopiera och klistra in följande " "kommandon, i en terminal, för att aktivera Saned på värdsystemet:" #: paperwork-gtk/src/paperwork_gtk/settings/stats.glade.h:1 msgid "" "Send metrics\n" "Give us clues about how you use Paperwork" msgstr "" "Skicka mätvärden\n" "Ge oss ledtrådar om hur du använder " "Paperwork" #: paperwork-gtk/src/paperwork_gtk/settings/stats.glade.h:3 msgid "" "Those clues will help us to make Paperwork an even " "better piece of software, for you. Statistics also show us that people are " "actually using our work, keeping us motivated to improve it.\n" "\n" "Here are the data we gather:\n" "- Hardware: CPU, RAM, screen resolution.\n" "- Software: Version of Paperwork, Operating system, desktop environment, " "system language.\n" "- Data metrics: number of documents, maximum and average number of pages, " "number of labels.\n" "- Number of times you used each feature.\n" "\n" "We do not collect document content nor any other sensitive or personal " "information. Still we think it's fair to request your authorization ;-).\n" "\n" "Collected statistics are visible on openpaper.work.\n" "" msgstr "" "Dessa ledtrådar hjälper oss att göra Paperwork " "till en ännu bättre mjukvara för dig. Statistiken visar oss också att " "människor faktiskt använder vårt arbete och håller oss motiverade att " "förbättra det.\n" "\n" "Detta är data vi samlar in:\n" "- Hårdvara: CPU, RAM, skärmupplösning.\n" "- Mjukvara: Version på Paperwork, Operativsystem, skrivbordsmiljö och " "systemspråk\n" "- Datamätvärden: Antal dokument, max och medel sidantal, antal etiketter.\n" "- Hur många gånger du använder varje funktion.\n" "\n" "Vi samlar inte in dokumentinnehåll eller någon annan känslig eller personlig " "information. Ändå tycker vi att det är rättvist att be om ditt tillstånd. ;-)" "\n" "\n" "Insamlad statistik kan ses på openpaper.work.\n" "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:1 msgid "Export Steps" msgstr "Exportsteg" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:2 msgid "Quality" msgstr "Kvalitet" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:3 msgid "Paper format" msgstr "Pappersformat" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:4 msgid "Export Settings" msgstr "Exportera inställningar" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:5 msgid "Preview" msgstr "Förhandsvisning" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.glade.h:1 msgid "Documents" msgstr "Dokument" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.glade.h:2 msgid "Selection" msgstr "Markering" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/suggestions.glade.h:1 msgid "Did you mean ?" msgstr "Menade du?" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.glade.h:1 #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.glade.h:1 msgid "Search" msgstr "Sök" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.glade.h:2 msgid "Advanced search" msgstr "Avancerat sök" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/docproperties.glade.h:1 msgid "Properties" msgstr "Egenskaper" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/label.glade.h:1 msgid "Change the label color" msgstr "Ändra etikettfärg" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/label.glade.h:2 msgid "Delete the label from all documents" msgstr "Ta bort etiketten från alla dokument" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/extra_text.glade.h:1 msgid "Additional keywords" msgstr "Ytterligare nyckelord" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/layout_settings.glade.h:1 msgid "Highlight words" msgstr "Markera ord" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/buttons.glade.h:1 msgid "Add page" msgstr "Lägg till sida" #: paperwork-gtk/src/paperwork_gtk/about/about.glade.h:1 msgid "©2020" msgstr "©2020" #: paperwork-gtk/src/paperwork_gtk/about/about.glade.h:2 msgid "Sorting documents is a machine's job." msgstr "Sortering av dokument är ett maskinjobb." paperwork-2.1.1/paperwork-gtk/l10n/uk.po000066400000000000000000000467651417573700700201100ustar00rootroot00000000000000# Ukrainian translations for PACKAGE package. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2020-05-03 15:27+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: uk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "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);\n" #: paperwork-gtk/src/paperwork_gtk/cmd/install.py:40 msgid "Install Paperwork icons and shortcuts" msgstr "" #: paperwork-gtk/src/paperwork_gtk/cmd/install.py:44 msgid "Install everything only for the current user" msgstr "" #: paperwork-gtk/src/paperwork_gtk/cmd/import.py:35 msgid "Run Paperwork and import files passed as arguments into a new document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/cmd/import.py:39 msgid "URLs or paths of files to import" msgstr "" #: paperwork-gtk/src/paperwork_gtk/main.py:201 msgid "command" msgstr "" #: paperwork-gtk/src/paperwork_gtk/docimport.py:86 #, python-format msgid "Don't know how to import '%s'. Sorry." msgstr "" #: paperwork-gtk/src/paperwork_gtk/docimport.py:105 msgid "PDF password" msgstr "" #: paperwork-gtk/src/paperwork_gtk/docimport.py:123 msgid "No new document to import found" msgstr "" #: paperwork-gtk/src/paperwork_gtk/docimport.py:163 msgid "Imported file(s) deleted" msgstr "" #: paperwork-gtk/src/paperwork_gtk/docimport.py:170 msgid "Imported:\n" msgstr "" #: paperwork-gtk/src/paperwork_gtk/docimport.py:176 msgid "Import successful" msgstr "" #: paperwork-gtk/src/paperwork_gtk/docimport.py:185 msgid "Delete imported files" msgstr "" #: paperwork-gtk/src/paperwork_gtk/print.py:42 #, python-brace-format msgid "Loading {doc_id} p{page_idx} for printing" msgstr "" #: paperwork-gtk/src/paperwork_gtk/print.py:144 #, python-format msgid "Printing %s" msgstr "" #: paperwork-gtk/src/paperwork_gtk/print.py:179 #, python-brace-format msgid "Printing {doc_id} ({page_idx}/{nb_pages})" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/copy_text.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/edit.py:30 msgid "Page" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/copy_text.py:30 msgid "Copy selected text to clipboard" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/edit.py:30 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/actions.glade.h:1 msgid "Edit" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/app/find.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/new.py:30 msgid "Global" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/app/find.py:30 msgid "Find" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/properties.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/print.py:30 msgid "Document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/properties.py:30 msgid "Edit document properties" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:35 msgid "Document list" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:30 msgid "Open next document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:35 msgid "Open previous document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/print.py:30 msgid "Print" msgstr "" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/new.py:30 msgid "Create new document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/print.py:49 msgid "Print page" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/reset.py:49 msgid "Reset page" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/delete.py:49 msgid "Delete page" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_inside_doc.py:50 msgid "Another position" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_inside_doc.py:53 #: paperwork-gtk/src/paperwork_gtk/menus/page/move_to_doc.py:53 msgid "Move page to" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/move_to_doc.py:50 msgid "Another document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/copy_text.py:50 msgid "Copy selected text" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/export.py:49 msgid "Export page" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/page/redo_ocr.py:50 #: paperwork-gtk/src/paperwork_gtk/actions/page/redo_ocr.py:62 msgid "Redo OCR on page" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/docs/properties.py:56 msgid "Change labels" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/docs/delete.py:51 msgid "Delete" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/docs/select_all.py:51 msgid "Select all" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/docs/export.py:51 msgid "Export" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/docs/redo_ocr.py:51 msgid "Redo OCR" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_settings.py:43 #: paperwork-gtk/src/paperwork_gtk/settings/settings.glade.h:1 msgid "Settings" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/app/help.py:64 msgid "Help" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_shortcuts.py:45 msgid "Shortcuts" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_bug_report.py:49 msgid "Report bug" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_about.py:45 msgid "About" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/doc/properties.py:43 msgid "Document properties" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/doc/print.py:41 msgid "Print document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/doc/delete.py:40 msgid "Delete document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/doc/add_to_selection.py:41 msgid "Add to selection" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/doc/export.py:38 msgid "Export document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/doc/open_external.py:36 msgid "Open folder" msgstr "" #: paperwork-gtk/src/paperwork_gtk/menus/doc/redo_ocr.py:40 msgid "Redo OCR on document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/delete.py:85 #, python-brace-format msgid "Are you sure you want to delete page {page_idx} of document {doc_id} ?" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:116 #, python-format msgid "Move the page %d to what position ?" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:135 #, python-format msgid "Invalid page position: %s" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:147 #, python-format msgid "Invalid page position: %d. Out of document bounds (1-%d)." msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py:156 msgid "Page position unchanged" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/redo_ocr.py:100 #: paperwork-gtk/src/paperwork_gtk/actions/docs/redo_ocr.py:92 #: paperwork-gtk/src/paperwork_gtk/actions/doc/redo_ocr.py:105 #, python-brace-format msgid "OCR on {doc_id} p{page_idx}" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/docs/delete.py:72 #, python-format msgid "Are you sure you want to delete %d documents ?" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/docs/redo_ocr.py:83 #: paperwork-gtk/src/paperwork_gtk/actions/doc/redo_ocr.py:97 #, python-format msgid "OCR on %s" msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/doc/delete.py:78 #, python-format msgid "Are you sure you want to delete document %s ?" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:405 #, python-format msgid "Estimated file size: %s" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:628 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:108 msgid "Select a file or a directory to import" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:637 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:128 msgid "Any files" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:715 msgid "Export has failed" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:81 msgid "Keyword(s)" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:102 msgid "No labels" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:142 msgid "Label" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:154 msgid "From:" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:160 msgid "to:" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:270 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/name.glade.h:1 msgid "Date" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:302 msgid "and" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:303 msgid "or" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:320 msgid "not" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:344 msgid "Remove" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/scan.py:118 #, python-format msgid "Scan from %s" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:77 msgid "Import image or PDF file(s)" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:78 msgid "Import file(s)" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:122 msgid "All supported file formats" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/title.py:33 #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/name.py:59 msgid "New document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/empty_doc/__init__.py:89 msgid "Empty" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/boxes/__init__.py:83 msgid "Loading text ..." msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/__init__.py:93 msgid "Loading page {}/{} ..." msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/__init__.py:622 #, python-format msgid "%d documents" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/thumbnailer.py:116 msgid "Loading document thumbnails" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:247 msgid "Renaming label" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:294 #, python-format msgid "Are you sure you want to delete label '%s' from ALL documents ?" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:396 #, python-brace-format msgid "Changing label {old_label} into {new_label} on document {doc_id}" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:462 #, python-brace-format msgid "Deleting label {old_label} from document {doc_id}" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/update.py:60 msgid "Updates" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/storage.py:84 msgid "Storage" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/storage.py:90 msgid "Work Directory" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.py:64 msgid "Optical Character Recognition" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.py:76 msgid "OCR disabled" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.py:168 msgid "Loading ..." msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/dev_id_popover.py:67 msgid "No scanner" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/resolution_popover.py:123 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:66 msgid "{} dpi" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/resolution_popover.py:125 msgid "{} dpi (recommended)" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:15 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:1 msgid "Color" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:16 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:2 msgid "Grayscale" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:17 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:3 msgid "Black & White" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:62 msgid "No scanner selected" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:66 msgid "No resolution selected" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:70 msgid "No mode selected" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:113 msgid "Scanner" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/stats.py:60 msgid "Help Improve Paperwork" msgstr "" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:33 msgid "Now with 10% more freedom in it !" msgstr "" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:34 #, python-format msgid "Buy it now and get a 100% discount !" msgstr "" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:35 msgid "New features and bugs available !" msgstr "" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:36 msgid "New taste !" msgstr "" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:37 msgid "We replaced your old bugs with new bugs. Enjoy." msgstr "" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:38 msgid "Smarter, Better, Stronger" msgstr "" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:40 msgid "It's better when it's free." msgstr "" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:45 #, python-brace-format msgid "A new version of Paperwork is available: {new_version}" msgstr "" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:64 msgid "Introduction" msgstr "" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:65 msgid "User manual" msgstr "" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:67 msgid "Documentation" msgstr "" #: paperwork-gtk/src/paperwork_gtk/about/about.glade.h:1 msgid "©2021" msgstr "" #: paperwork-gtk/src/paperwork_gtk/about/about.glade.h:2 msgid "Sorting documents is a machine's job." msgstr "" #: paperwork-gtk/src/paperwork_gtk/actions/page/move_to_doc/move_to_doc.glade.h:1 msgid "Select target document" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:1 msgid "Export Steps" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:2 msgid "Quality" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:3 msgid "Paper format" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:4 msgid "Export Settings" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:5 msgid "Send by email" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:6 msgid "Preview" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.glade.h:1 #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.glade.h:1 msgid "Search" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/suggestions.glade.h:1 msgid "Did you mean ?" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.glade.h:2 msgid "Advanced search" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/buttons.glade.h:1 msgid "Add page" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/layout_settings.glade.h:1 msgid "Highlight words" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.glade.h:1 msgid "Documents" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.glade.h:2 msgid "Selection" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/extra_text.glade.h:1 msgid "Additional keywords" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/docproperties.glade.h:1 msgid "Properties" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/label.glade.h:1 msgid "Change the label color" msgstr "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/label.glade.h:2 msgid "Delete the label from all documents" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/update.glade.h:1 msgid "" "Updates\n" "Check periodically for new versions of Paperwork" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/update.glade.h:3 msgid "" "Look about once a week for new versions of " "Paperwork.\n" "You will be notified when a new version is available but it won't be " "installed automatically.\n" "" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/stats.glade.h:1 msgid "" "Send metrics\n" "Give us clues about how you use Paperwork" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/stats.glade.h:3 msgid "" "Those clues will help us to make Paperwork an even " "better piece of software, for you. Statistics also show us that people are " "actually using our work, keeping us motivated to improve it.\n" "\n" "Here are the data we gather:\n" "- Hardware: CPU, RAM, screen resolution.\n" "- Software: Version of Paperwork, Operating system, desktop environment, " "system language.\n" "- Data metrics: number of documents, maximum and average number of pages, " "number of labels.\n" "- Number of times you used each feature.\n" "\n" "We do not collect document content nor any other sensitive or personal " "information. Still we think it's fair to request your authorization ;-).\n" "\n" "Collected statistics are visible on openpaper.work.\n" "" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.glade.h:1 msgid "Languages" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.glade.h:1 msgid "Flatpak" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.glade.h:2 msgid "" "You are using Paperwork from a Flatpak container. Paperwork needs Saned to " "access your scanners.\n" "\n" "Important: the following procedure will only work for local (non-network) " "scanners !\n" "\n" "To enable Saned on the host system, you must copy and paste the following " "commands in a terminal:" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:1 msgid "Maximize" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:2 msgid "Automatic" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:3 msgid "Scan" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:4 msgid "Scanner Calibration" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:1 msgid "Device" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:2 msgid "Resolution" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:3 msgid "Mode" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:4 msgid "Calibration" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:5 msgid "Re-calibrate" msgstr "" #: paperwork-gtk/src/paperwork_gtk/settings/storage.glade.h:1 msgid "Work directory" msgstr "" paperwork-2.1.1/paperwork-gtk/l10n/zh_Hans.po000066400000000000000000000512621417573700700210470ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-09-01 22:01+0200\n" "PO-Revision-Date: 2021-02-06 06:20+0000\n" "Last-Translator: 玉堂白鹤 \n" "Language-Team: Chinese (Simplified) \n" "Language: zh_Hans\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Weblate 4.4\n" #: paperwork-gtk/src/paperwork_gtk/docimport.py:82 #, python-format msgid "Don't know how to import '%s'. Sorry." msgstr "对不起,不知道如何导入“%s”。" #: paperwork-gtk/src/paperwork_gtk/docimport.py:99 msgid "No new document to import found" msgstr "未发现要导入的新文档" #: paperwork-gtk/src/paperwork_gtk/docimport.py:139 msgid "Imported file(s) deleted" msgstr "已删除导入的文件" #: paperwork-gtk/src/paperwork_gtk/docimport.py:146 msgid "Imported:\n" msgstr "导入:\n" #: paperwork-gtk/src/paperwork_gtk/docimport.py:152 msgid "Import successful" msgstr "导入成功" #: paperwork-gtk/src/paperwork_gtk/docimport.py:161 msgid "Delete imported files" msgstr "删除导入文件" #: paperwork-gtk/src/paperwork_gtk/print.py:42 #, python-brace-format msgid "Loading {doc_id} p{page_idx} for printing" msgstr "真正加载{doc_id}页{page_idx}以打印" #: paperwork-gtk/src/paperwork_gtk/print.py:144 #, python-format msgid "Printing %s" msgstr "打印%s" #: paperwork-gtk/src/paperwork_gtk/print.py:179 #, python-brace-format msgid "Printing {doc_id} ({page_idx}/{nb_pages})" msgstr "打印 {doc_id} ({page_idx}/{nb_pages})" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:9 msgid "Now with 10% more freedom in it !" msgstr "现在有了 10% 的自由度!" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:10 #, python-format msgid "Buy it now and get a 100% discount !" msgstr "现在购买,可以享受100% 的折扣!" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:11 msgid "New features and bugs available !" msgstr "新增功能和 bug!" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:12 msgid "It's better when it's free." msgstr "免费的时候更好。" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:13 msgid "New taste !" msgstr "新体验 !" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:14 msgid "We replaced your old bugs with new bugs. Enjoy." msgstr "我们用新 bug 替换了您的旧 bug。好好享受。" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:15 msgid "Smarter, Better, Stronger" msgstr "更聪明,更好,更强" #: paperwork-gtk/src/paperwork_gtk/update_notification.py:45 #, python-brace-format msgid "A new version of Paperwork is available: {new_version}" msgstr "Paperwork 新版本可用: {new_version}" #: paperwork-gtk/src/paperwork_gtk/settings/update.py:60 msgid "Updates" msgstr "更新" #: paperwork-gtk/src/paperwork_gtk/settings/storage.py:84 msgid "Storage" msgstr "存储" #: paperwork-gtk/src/paperwork_gtk/settings/storage.py:90 msgid "Work Directory" msgstr "工作目录" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.py:64 msgid "Optical Character Recognition" msgstr "光学字符识别" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.py:76 msgid "OCR disabled" msgstr "已禁用 OCR" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:15 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:1 msgid "Color" msgstr "彩色" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:16 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:2 msgid "Grayscale" msgstr "灰度" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:17 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade.h:3 msgid "Black & White" msgstr "黑白" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:27 msgid "No scanner selected" msgstr "未选择扫描仪" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:31 msgid "No resolution selected" msgstr "未选择分辨率" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:31 #: paperwork-gtk/src/paperwork_gtk/settings/scanner/resolution_popover.py:123 msgid "{} dpi" msgstr "{} dpi" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:35 msgid "No mode selected" msgstr "未选择模式" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py:104 msgid "Scanner" msgstr "扫描仪" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/dev_id_popover.py:67 msgid "No scanner" msgstr "无扫描仪" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.py:154 msgid "Loading ..." msgstr "加载中 ..." #: paperwork-gtk/src/paperwork_gtk/settings/scanner/resolution_popover.py:125 msgid "{} dpi (recommended)" msgstr "{} dpi (推荐)" #: paperwork-gtk/src/paperwork_gtk/settings/stats.py:60 msgid "Help Improve Paperwork" msgstr "帮助改进 Paperwork" #: paperwork-gtk/src/paperwork_gtk/cmd/install.py:40 msgid "Install Paperwork icons and shortcuts" msgstr "安装 Paperwork 图标和快捷方式" #: paperwork-gtk/src/paperwork_gtk/cmd/install.py:44 msgid "Install everything only for the current user" msgstr "只为当前用户安装所有内容" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/edit.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/copy_text.py:30 msgid "Page" msgstr "页面" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/edit.py:30 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/actions.glade.h:1 msgid "Edit" msgstr "编辑" #: paperwork-gtk/src/paperwork_gtk/shortcuts/page/copy_text.py:30 msgid "Copy selected text to clipboard" msgstr "将所选文本复制到剪贴板" #: paperwork-gtk/src/paperwork_gtk/shortcuts/app/find.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/new.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:35 msgid "Global" msgstr "全局" #: paperwork-gtk/src/paperwork_gtk/shortcuts/app/find.py:30 msgid "Find" msgstr "查找" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/print.py:30 #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/properties.py:30 msgid "Document" msgstr "文档" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/print.py:30 msgid "Print" msgstr "打印" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/new.py:30 msgid "Create new document" msgstr "创建新文档" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/properties.py:30 msgid "Edit document properties" msgstr "编辑文档属性" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:30 msgid "Document list" msgstr "文档列表" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:30 msgid "Open next document" msgstr "打开下一文档" #: paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py:35 msgid "Open previous document" msgstr "打开上一文档" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:372 #, python-format msgid "Estimated file size: %s" msgstr "估计文件大小: %s" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:593 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:104 msgid "Select a file or a directory to import" msgstr "选择要导入的文件或目录" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py:602 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:124 msgid "Any files" msgstr "所有文件" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/__init__.py:613 #, python-format msgid "%d documents" msgstr "%d 个文档" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/name.py:59 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/title.py:23 msgid "New document" msgstr "新建文档" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/thumbnailer.py:113 msgid "Loading document thumbnails" msgstr "加载文档缩略图" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:78 msgid "Keyword(s)" msgstr "关键字" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:131 msgid "Label" msgstr "标签" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:143 msgid "From:" msgstr "从:" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:149 msgid "to:" msgstr "到:" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:259 #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/name.glade.h:1 msgid "Date" msgstr "日期" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:291 msgid "and" msgstr "和" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:292 msgid "or" msgstr "或" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:309 msgid "not" msgstr "不包含" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py:333 msgid "Remove" msgstr "移除" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:247 msgid "Renaming label" msgstr "重命名标签" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:378 #, python-brace-format msgid "Changing label {old_label} into {new_label} on document {doc_id}" msgstr "将文档{doc_id}上的标签{old_label}更改为{new_label}" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py:444 #, python-brace-format msgid "Deleting label {old_label} from document {doc_id}" msgstr "从文档{doc_id}中删除标签{old_label}" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/empty_doc/__init__.py:89 msgid "Empty" msgstr "空白" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/__init__.py:88 msgid "Loading page {}/{} ..." msgstr "正在加载页面 {}/{} ..." #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/boxes/__init__.py:83 msgid "Loading text ..." msgstr "正在加载文本 ..." #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/scan.py:118 #, python-format msgid "Scan from %s" msgstr "扫描自 %s" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:75 msgid "Import image or PDF file(s)" msgstr "导入图像或 PDF 文件" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:76 msgid "Import file(s)" msgstr "导入文件" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py:118 msgid "All supported file formats" msgstr "所有支持的格式" #: paperwork-gtk/src/paperwork_gtk/main.py:197 msgid "command" msgstr "命令" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:64 msgid "Introduction" msgstr "介绍" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:65 msgid "User manual" msgstr "用户手册" #: paperwork-gtk/src/paperwork_gtk/model/help/__init__.py:67 msgid "Documentation" msgstr "文档" #: paperwork-gtk/src/paperwork_gtk/actions/page/delete.py:85 #, python-brace-format msgid "Are you sure you want to delete page {page_idx} of document {doc_id} ?" msgstr "确实要删除文档 {doc_id} 的第 {page_idx} 页吗?" #: paperwork-gtk/src/paperwork_gtk/actions/page/redo_ocr.py:62 #: paperwork-gtk/src/paperwork_gtk/menus/page/redo_ocr.py:50 msgid "Redo OCR on page" msgstr "重做页面 OCR" #: paperwork-gtk/src/paperwork_gtk/actions/page/redo_ocr.py:100 #: paperwork-gtk/src/paperwork_gtk/actions/doc/redo_ocr.py:105 #, python-brace-format msgid "OCR on {doc_id} p{page_idx}" msgstr "OCR {doc_id} 页{page_idx}" #: paperwork-gtk/src/paperwork_gtk/actions/docs/delete.py:72 #, python-format msgid "Are you sure you want to delete %d documents ?" msgstr "您确信要删除 %d 文档 ?" #: paperwork-gtk/src/paperwork_gtk/actions/doc/delete.py:78 #, python-format msgid "Are you sure you want to delete document %s ?" msgstr "你确信要删除文档 %s?" #: paperwork-gtk/src/paperwork_gtk/actions/doc/redo_ocr.py:97 #, python-format msgid "OCR on %s" msgstr "OCR 于 %s" #: paperwork-gtk/src/paperwork_gtk/menus/page/print.py:49 msgid "Print page" msgstr "打印页面" #: paperwork-gtk/src/paperwork_gtk/menus/page/export.py:49 msgid "Export page" msgstr "导出页面" #: paperwork-gtk/src/paperwork_gtk/menus/page/delete.py:49 msgid "Delete page" msgstr "删除页面" #: paperwork-gtk/src/paperwork_gtk/menus/page/reset.py:49 msgid "Reset page" msgstr "重置页面" #: paperwork-gtk/src/paperwork_gtk/menus/page/copy_text.py:50 msgid "Copy selected text" msgstr "复制所选文本" #: paperwork-gtk/src/paperwork_gtk/menus/app/help.py:62 msgid "Help" msgstr "帮助" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_bug_report.py:49 msgid "Report bug" msgstr "报告 bug" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_shortcuts.py:45 msgid "Shortcuts" msgstr "快捷方式" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_settings.py:43 #: paperwork-gtk/src/paperwork_gtk/settings/settings.glade.h:1 msgid "Settings" msgstr "设置" #: paperwork-gtk/src/paperwork_gtk/menus/app/open_about.py:45 msgid "About" msgstr "关于" #: paperwork-gtk/src/paperwork_gtk/menus/docs/export.py:51 msgid "Export" msgstr "导出" #: paperwork-gtk/src/paperwork_gtk/menus/docs/select_all.py:51 msgid "Select all" msgstr "全选" #: paperwork-gtk/src/paperwork_gtk/menus/docs/delete.py:51 msgid "Delete" msgstr "删除" #: paperwork-gtk/src/paperwork_gtk/menus/docs/properties.py:56 msgid "Change labels" msgstr "更改标签" #: paperwork-gtk/src/paperwork_gtk/menus/doc/print.py:41 msgid "Print document" msgstr "打印文档" #: paperwork-gtk/src/paperwork_gtk/menus/doc/export.py:38 msgid "Export document" msgstr "导出文档" #: paperwork-gtk/src/paperwork_gtk/menus/doc/delete.py:40 msgid "Delete document" msgstr "删除文档" #: paperwork-gtk/src/paperwork_gtk/menus/doc/redo_ocr.py:40 msgid "Redo OCR on document" msgstr "重做文档 OCR" #: paperwork-gtk/src/paperwork_gtk/menus/doc/properties.py:43 msgid "Document properties" msgstr "文档属性" #: paperwork-gtk/src/paperwork_gtk/menus/doc/add_to_selection.py:41 msgid "Add to selection" msgstr "添加至所选" #: paperwork-gtk/src/paperwork_gtk/menus/doc/open_external.py:36 msgid "Open folder" msgstr "打开文件夹" #: paperwork-gtk/src/paperwork_gtk/settings/update.glade.h:1 msgid "" "Updates\n" "Check periodically for new versions of Paperwork" msgstr "" "更新\n" "定期检查 Paperwork 新版本" #: paperwork-gtk/src/paperwork_gtk/settings/update.glade.h:3 msgid "" "Look about once a week for new versions of " "Paperwork.\n" "You will be notified when a new version is available but it won't be " "installed automatically.\n" "" msgstr "" "每周查看一次 Paperwork 新版本。\n" "当有新版本可用但不会自动安装时,系统将会通知您。\n" "" #: paperwork-gtk/src/paperwork_gtk/settings/storage.glade.h:1 msgid "Work directory" msgstr "工作目录" #: paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.glade.h:1 msgid "Languages" msgstr "语言" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:1 msgid "Maximize" msgstr "最大化" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:2 msgid "Automatic" msgstr "自动" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:3 msgid "Scan" msgstr "扫描" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade.h:4 msgid "Scanner Calibration" msgstr "扫描仪校准" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:1 msgid "Device" msgstr "设备" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:2 msgid "Resolution" msgstr "分辨率" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:3 msgid "Mode" msgstr "模式" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:4 msgid "Calibration" msgstr "校准" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade.h:5 msgid "Re-calibrate" msgstr "重新校准" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.glade.h:1 msgid "Flatpak" msgstr "Flatpak" #: paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.glade.h:2 msgid "" "You are using Paperwork from a Flatpak container. Paperwork needs Saned to " "access your scanners. To enable Saned on the host system, you must copy and " "paste the following commands in a terminal:" msgstr "" "您使用的是 Flatpak 容器的 Paperwork 。它需要使用 Saned 访问你的扫描仪。要在主机系统上启用 " "Saned,必须在终端中复制并粘贴以下命令:" #: paperwork-gtk/src/paperwork_gtk/settings/stats.glade.h:1 msgid "" "Send metrics\n" "Give us clues about how you use Paperwork" msgstr "" "发送指标\n" "给我们一些关于您如何使用 Paperwork 的线索" #: paperwork-gtk/src/paperwork_gtk/settings/stats.glade.h:3 msgid "" "Those clues will help us to make Paperwork an even " "better piece of software, for you. Statistics also show us that people are " "actually using our work, keeping us motivated to improve it.\n" "\n" "Here are the data we gather:\n" "- Hardware: CPU, RAM, screen resolution.\n" "- Software: Version of Paperwork, Operating system, desktop environment, " "system language.\n" "- Data metrics: number of documents, maximum and average number of pages, " "number of labels.\n" "- Number of times you used each feature.\n" "\n" "We do not collect document content nor any other sensitive or personal " "information. Still we think it's fair to request your authorization ;-).\n" "\n" "Collected statistics are visible on openpaper.work.\n" "" msgstr "" "这些线索将帮助我们使 Paperwork " "成为一个更好的软件,奉献给您。统计数据还将显示,人们实际上在使用我们的作品,使我们保持改进工作的动力。\n" "\n" "以下是我们收集的数据:\n" "-硬件:CPU,RAM,屏幕分辨率。\n" "-软件:文件版本,操作系统,桌面环境,系统语言。\n" "-数据指标:文档数、最大和平均页数、标签数。\n" "-使用每个功能的次数。\n" "\n" "我们不收集文件内容或任何其他敏感或个人信息。我们仍然认为请求您的授权是公平的;-)。\n" "\n" "收集的统计数据位于 openpaper." "work。\n" "" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:1 msgid "Export Steps" msgstr "导出步骤" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:2 msgid "Quality" msgstr "质量" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:3 msgid "Paper format" msgstr "页面格式" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:4 msgid "Export Settings" msgstr "导出设置" #: paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade.h:5 msgid "Preview" msgstr "预览" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.glade.h:1 msgid "Documents" msgstr "文档" #: paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.glade.h:2 msgid "Selection" msgstr "选择" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/suggestions.glade.h:1 msgid "Did you mean ?" msgstr "你的意思是 ?" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.glade.h:1 #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.glade.h:1 msgid "Search" msgstr "搜索" #: paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.glade.h:2 msgid "Advanced search" msgstr "高级搜索" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/docproperties.glade.h:1 msgid "Properties" msgstr "属性" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/label.glade.h:1 msgid "Change the label color" msgstr "更改标签颜色" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/label.glade.h:2 msgid "Delete the label from all documents" msgstr "从所有文档中删除标签" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/extra_text.glade.h:1 msgid "Additional keywords" msgstr "其他关键字" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/layout_settings.glade.h:1 msgid "Highlight words" msgstr "突出显示单词" #: paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/buttons.glade.h:1 msgid "Add page" msgstr "添加页面" #: paperwork-gtk/src/paperwork_gtk/about/about.glade.h:1 msgid "©2020" msgstr "©2020" #: paperwork-gtk/src/paperwork_gtk/about/about.glade.h:2 msgid "Sorting documents is a machine's job." msgstr "整理文件是机器的工作。" paperwork-2.1.1/paperwork-gtk/pylintrc000066400000000000000000000156461417573700700201400ustar00rootroot00000000000000[MASTER] # Specify a configuration file. #rcfile= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Profiled execution. profile=no # Add files or directories to the blacklist. They should be base names, not # paths. ignore=CVS # Pickle collected data for later comparisons. persistent=yes # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= [MESSAGES CONTROL] # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time. #enable= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). disable=W0511,R0903,R0902,E0611,C0111 [REPORTS] # Set the output format. Available formats are text, parseable, colorized, msvs # (visual studio) and html output-format=text # Include message's id in output include-ids=yes # Put messages in a separate file for each module / package specified on the # command line instead of printing them on stdout. Reports (if any) will be # written in a file name "pylint_global.[txt|html]". files-output=no # Tells whether to display a full report or only the messages reports=yes # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Add a comment according to your evaluation note. This is used by the global # evaluation report (RP0004). comment=no [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME,XXX,TODO [VARIABLES] # Tells whether we should check for unused import in __init__ files. init-import=no # A regular expression matching the beginning of the name of dummy variables # (i.e. not used). dummy-variables-rgx=_|dummy # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= [SIMILARITIES] # Minimum lines number of a similarity. min-similarity-lines=4 # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes [BASIC] # Required attributes for module, separated by a comma required-attributes= # List of builtins function names that should not be used, separated by a comma bad-functions=map,filter,apply,input # Regular expression which should only match correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression which should only match correct module level names const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ # Regular expression which should only match correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ # Regular expression which should only match correct function names function-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct method names method-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct instance attribute names attr-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct argument names argument-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct variable names variable-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct list comprehension / # generator expression variable names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ # Good variable names which should always be accepted, separated by a comma good-names=i,j,k,ex,Run,_ # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata # Regular expression which should only match functions or classes name which do # not require a docstring no-docstring-rgx=__.*__ [TYPECHECK] # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # List of classes names for which member attributes should not be checked # (useful for classes with attributes dynamically set). ignored-classes=SQLObject # When zope mode is activated, add a predefined set of Zope acquired attributes # to generated-members. zope=no # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E0201 when accessed. Python regular # expressions are accepted. generated-members=REQUEST,acl_users,aq_parent [FORMAT] # Maximum number of characters on a single line. max-line-length=80 # Maximum number of lines in a module max-module-lines=1000 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' [DESIGN] # Maximum number of arguments for function / method max-args=5 # Argument names that match this expression will be ignored. Default to name # with leading underscore ignored-argument-names=_.* # Maximum number of locals for function / method body max-locals=15 # Maximum number of return / yield for function / method body max-returns=6 # Maximum number of branch for function / method body max-branchs=12 # Maximum number of statements in function / method body max-statements=50 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Minimum number of public methods for a class (see R0903). min-public-methods=2 # Maximum number of public methods for a class (see R0904). max-public-methods=20 [IMPORTS] # Deprecated modules which should not be used, separated by a comma deprecated-modules=regsub,string,TERMIOS,Bastion,rexec # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled) import-graph= # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled) ext-import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled) int-import-graph= [CLASSES] # List of interface methods to ignore, separated by a comma. This is used for # instance to not check methods defines in Zope's Interface base class. ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp paperwork-2.1.1/paperwork-gtk/setup.py000077500000000000000000000167361417573700700200670ustar00rootroot00000000000000#!/usr/bin/env python3 import codecs import glob import os import sys import setuptools quiet = '--quiet' in sys.argv or '-q' in sys.argv freeze = 'build_exe' in sys.argv try: with codecs.open("src/paperwork_gtk/_version.py", "r", encoding="utf-8") \ as file_descriptor: version = file_descriptor.readlines()[1].strip() version = version.split(" ")[2][1:-1] if not quiet: print("Paperwork version: {}".format(version)) if "-" in version: version = version.split("-")[0] except FileNotFoundError: print("ERROR: _version.py file is missing") print("ERROR: Please run 'make version' first") sys.exit(1) kwargs = { "name": "paperwork", "version": version, "description": "Using scanner and OCR to grep dead trees the easy way", "long_description": """Paperwork is a tool to make papers searchable. The basic idea behind Paperwork is "scan & forget" : You should be able to just scan a new document and forget about it until the day you need it again. Let the machine do most of the work. Main features are: - Scan - Automatic orientation detection - OCR - Indexing - Document labels - Automatic guessing of the labels to apply on new documents - Search - Keyword suggestions - Quick edit of scans - PDF support """, "keywords": "scanner ocr gui", "url": "https://gitlab.gnome.org/World/OpenPaperwork/paperwork", "download_url": ( "https://gitlab.gnome.org/World/OpenPaperwork/paperwork/-/" "archive/{}/paperwork-{}.tar.gz".format(version, version) ), "classifiers": [ "Development Status :: 5 - Production/Stable", "Environment :: X11 Applications :: GTK", "Environment :: X11 Applications :: Gnome", "Intended Audience :: End Users/Desktop", ("License :: OSI Approved ::" " GNU General Public License v3 or later (GPLv3+)"), "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", "Topic :: Multimedia :: Graphics :: Capture :: Scanners", "Topic :: Multimedia :: Graphics :: Graphics Conversion", "Topic :: Scientific/Engineering :: Image Recognition", "Topic :: Text Processing :: Filters", "Topic :: Text Processing :: Indexing", ], "license": "GPLv3+", "author": "Jerome Flesch", "author_email": "jflesch@openpaper.work", "packages": setuptools.find_packages('src'), "package_dir": {'': 'src'}, "include_package_data": True, "entry_points": { 'gui_scripts': [ 'paperwork-gtk = paperwork_gtk.main:main', ] }, # zip_safe: pkg_resources.resource_filename() is currently broken in MSYS2 # + setuptools 41.0.1-1 "zip_safe": (os.name != 'nt'), "install_requires": [ "distro", "openpaperwork-core", "openpaperwork-gtk", "pycountry", "pyocr >= 0.3.0", "python-Levenshtein", "pyxdg >= 0.25", "paperwork-backend>={}".format(version), # paperwork-chkdeps take care of all the dependencies that can't be # handled here. For instance: # - Dependencies using gobject introspection # - Dependencies based on language (OCR data files, dictionaries, etc) # - Dependencies on data files (icons, etc) ] } if not freeze: setuptools.setup(**kwargs) else: import cx_Freeze common_include_files = [] required_dll_search_paths = os.getenv("PATH", os.defpath).split(os.pathsep) required_dlls = [ 'libatk-1.0-0.dll', 'libepoxy-0.dll', 'libgdk-3-0.dll', 'libgdk_pixbuf-2.0-0.dll', 'libgtk-3-0.dll', 'libhandy-1-0.dll', 'libinsane.dll', 'libinsane_gobject.dll', 'libnotify-4.dll', 'libpango-1.0-0.dll', 'libpangocairo-1.0-0.dll', 'libpangoft2-1.0-0.dll', 'libpangowin32-1.0-0.dll', 'libpoppler-*.dll', 'libpoppler-glib-8.dll', 'librsvg-2-2.dll', 'libsqlite3-0.dll', 'libxml2-2.dll', ] for dll in required_dlls: dll_path = None for p_dir in required_dll_search_paths: p_glob = os.path.join(p_dir, dll) for p in glob.glob(p_glob): if os.path.isfile(p): dll_path = p break if dll_path is not None: break if dll_path is None: raise Exception( "Unable to locate {} in {}".format( dll, required_dll_search_paths ) ) print(f"Found {dll} = {dll_path}") common_include_files.append((dll_path, os.path.basename(dll_path))) # We need the .typelib files at runtime. # The related .gir files are in $PREFIX/share/gir-1.0/$NS.gir, # but those can be omitted at runtime. required_gi_namespaces = [ "Atk-1.0", "cairo-1.0", "Gdk-3.0", "GdkPixbuf-2.0", "Gio-2.0", "GLib-2.0", "GModule-2.0", "GObject-2.0", "Gtk-3.0", "Handy-1", "HarfBuzz-0.0", "Notify-0.7", "Pango-1.0", "PangoCairo-1.0", "Poppler-0.18", "Libinsane-1.0", ] for ns in required_gi_namespaces: subpath = "lib/girepository-1.0/{}.typelib".format(ns) fullpath = os.path.join(sys.prefix, subpath) assert os.path.isfile(fullpath), ( "Required file {} is missing" .format( fullpath, )) common_include_files.append((fullpath, subpath)) common_packages = [ "gi", # always seems to be needed "cairo", # Only needed (for foreign structs) if no "import cairo"s # XXX(Jflesch): bug ? "pyocr", "pyocr.libtesseract", "setuptools", "openpaperwork_core", "openpaperwork_gtk", "paperwork_backend", "paperwork_gtk", "paperwork_shell", ] kwargs['executables'] = [ cx_Freeze.Executable( script="src/paperwork_gtk/main.py", targetName="paperwork.exe", base=("Console" if os.name != "nt" else "Win32GUI"), ), cx_Freeze.Executable( # UGLY script="../paperwork-shell/src/paperwork_shell/main.py", targetName="paperwork-json.exe", base="Console", ), ] kwargs['options'] = { "build_exe": { 'include_files': common_include_files, 'silent': True, 'packages': common_packages, "excludes": ["tkinter", "tk", "tcl"], }, } cx_Freeze.setup(**kwargs) if quiet: sys.exit(0) print("============================================================") print("============================================================") print("|| IMPORTANT ||") print("|| ||") print("|| Please run ||") print("||--------------------------------------------------------||") print("|| paperwork-gtk chkdeps ||") print("|| paperwork-gtk install --user ||") print("||--------------------------------------------------------||") print("|| to find any missing dependencies ||") print("|| and install Paperwork's icons and shortcuts ||") print("============================================================") print("============================================================") paperwork-2.1.1/paperwork-gtk/src/000077500000000000000000000000001417573700700171245ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/000077500000000000000000000000001417573700700220035ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/__init__.py000066400000000000000000000001131417573700700241070ustar00rootroot00000000000000import gettext def _(s): return gettext.dgettext('paperwork_gtk', s) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/about/000077500000000000000000000000001417573700700231155ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/about/__init__.py000066400000000000000000000061041417573700700252270ustar00rootroot00000000000000import logging import openpaperwork_core LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.windows = [] def get_interfaces(self): return [ 'gtk_about', 'gtk_window_listener', ] def get_deps(self): return [ { 'interface': 'authors', 'defaults': ['paperwork_backend.authors'], }, { 'interface': 'app', 'defaults': ['paperwork_backend.app'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'icon', 'defaults': ['paperwork_gtk.icon'], }, ] def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def _set_authors_str(self, method, authors): out = "" for (email, name, line_count) in authors: if line_count > 0: out += "{} ({})\n".format(name, line_count) else: out += "{}\n".format(name) method(out) def _set_authors_list(self, method, authors): out = [] for (email, name, line_count) in authors: txt = name if line_count > 0: txt += " ({})".format(line_count) out.append(txt) method(out) def _on_close(self, dialog, *args, **kwargs): LOGGER.info("Closing dialog 'about'") dialog.destroy() self.core.call_all("on_gtk_window_closed", dialog) def gtk_open_about(self): LOGGER.info("Opening dialog 'about'") widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.about", "about.glade" ) authors = {} self.core.call_all("authors_get", authors) documentation = authors.pop("Documentation", []) translators = authors.pop("Translators", []) version = self.core.call_success("app_get_version") icon = self.core.call_success("icon_get_pixbuf", "paperwork", 128) about_dialog = widget_tree.get_object("about_dialog") about_dialog.set_logo(icon) about_dialog.set_version(version) for (k, v) in authors.items(): self._set_authors_list( lambda authors: about_dialog.add_credit_section(k, authors), v ) if len(translators) > 0: self._set_authors_str( about_dialog.set_translator_credits, translators ) self._set_authors_list(about_dialog.set_documenters, documentation) about_dialog.set_transient_for(self.windows[-1]) about_dialog.set_visible(True) about_dialog.connect("close", self._on_close) about_dialog.connect("response", self._on_close) self.core.call_all("on_gtk_window_opened", about_dialog) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/about/about.glade000066400000000000000000000031501417573700700252240ustar00rootroot00000000000000 False dialog Paperwork ©2021 Sorting documents is a machine's job. https://openpaper.work/ image-missing gpl-3-0 False vertical 2 False end False False 0 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/000077500000000000000000000000001417573700700234435ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/__init__.py000066400000000000000000000000001417573700700255420ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/app/000077500000000000000000000000001417573700700242235ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/app/__init__.py000066400000000000000000000000001417573700700263220ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/app/find.py000066400000000000000000000024061417573700700255170ustar00rootroot00000000000000import logging try: from gi.repository import Gio GIO_AVAILABLE = True except (ImportError, ValueError): GIO_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'chkdeps', 'action', 'action_app', 'action_app_find', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'gtk_search_field', 'defaults': ['paperwork_gtk.mainwindow.search.field'], }, ] def chkdeps(self, out: dict): if not GIO_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def init(self, core): super().init(core) if not GIO_AVAILABLE: return action = Gio.SimpleAction.new('app_find', None) action.connect("activate", self._focus_on_search_field) self.core.call_all("app_actions_add", action) def _focus_on_search_field(self, action, parameter): self.core.call_all("search_focus") paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/app/help.py000066400000000000000000000027001417573700700255240ustar00rootroot00000000000000import logging try: from gi.repository import Gio GIO_AVAILABLE = True except (ImportError, ValueError): GIO_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'chkdeps', 'action', 'action_app', 'action_app_help', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'help_documents', 'defaults': ['paperwork_gtk.model.help'], }, ] def chkdeps(self, out: dict): if not GIO_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def init(self, core): super().init(core) if not GIO_AVAILABLE: return for (title, file_name) in self.core.call_success("help_get_files"): action = Gio.SimpleAction.new('open_help.' + file_name, None) action.connect("activate", self.doc_open_help, file_name) self.core.call_all("app_actions_add", action) def doc_open_help(self, action, parameter, file_name): doc_url = self.core.call_success("help_get_file", file_name) self.core.call_all("doc_open", "help_" + file_name, doc_url) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/app/open_about.py000066400000000000000000000021351417573700700267310ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'action', 'action_app', 'action_app_open_about', 'chkdeps', ] def get_deps(self): return [ { 'interface': 'gtk_about', 'defaults': ['paperwork_gtk.about'], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new('open_about', None) action.connect("activate", self._open_about) self.core.call_all("app_actions_add", action) def _open_about(self, *args, **kwargs): self.core.call_success("gtk_open_about") paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/app/open_bug_report.py000066400000000000000000000021641417573700700277710ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = None def get_interfaces(self): return [ 'action', 'action_app', 'action_app_open_bug_report', 'chkdeps', 'gtk_window_listener', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new('open_bug_report', None) action.connect("activate", self._open_bug_report) self.core.call_all("app_actions_add", action) def _open_bug_report(self, *args, **kwargs): self.core.call_all("open_bug_report") paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/app/open_settings.py000066400000000000000000000021711417573700700274570ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'action', 'action_app', 'action_app_open_settings', 'chkdeps', ] def get_deps(self): return [ { 'interface': 'gtk_settings_dialog', 'defaults': ['paperwork_gtk.settings'], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new('open_settings', None) action.connect("activate", self._open_settings) self.core.call_all("app_actions_add", action) def _open_settings(self, *args, **kwargs): self.core.call_success("gtk_open_settings") paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/app/open_shortcuts.py000066400000000000000000000022001417573700700276460ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'action', 'action_app', 'action_app_open_shortcuts', 'chkdeps', ] def get_deps(self): return [ { 'interface': 'gtk_shortcut_help', 'defaults': ['paperwork_gtk.shortcutswin'], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new('open_shortcuts', None) action.connect("activate", self._open_shortcuts) self.core.call_all("app_actions_add", action) def _open_shortcuts(self, *args, **kwargs): self.core.call_success("gtk_show_shortcuts") paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/doc/000077500000000000000000000000001417573700700242105ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/doc/__init__.py000066400000000000000000000000001417573700700263070ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/doc/add_to_selection.py000066400000000000000000000032511417573700700300620ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_add_to_selection" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = (None, None) self.active_windows = [] def get_interfaces(self): return [ 'action' 'action_doc', 'action_doc_add_to_selection', 'chkdeps', 'doc_open', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'doc_selection', 'defaults': ['paperwork_gtk.doc_selection'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._add_to_selection) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def doc_close(self): self.active_doc = (None, None) def _add_to_selection(self, action, parameter): self.core.call_all("gtk_switch_to_doc_selection_multiple") self.core.call_all("doc_selection_add", *self.active_doc) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/doc/delete.py000066400000000000000000000063341417573700700260320ustar00rootroot00000000000000import gc import logging import os try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_delete" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = (None, None) def get_interfaces(self): return [ 'action', 'action_doc', 'action_doc_delete', 'chkdeps', 'doc_open', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'gtk_dialog_yes_no', 'defaults': ['openpaperwork_gtk.dialogs.yes_no'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._delete) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def doc_close(self): self.active_doc = (None, None) def _delete(self, action, parameter): assert(self.active_doc is not None) active = self.active_doc LOGGER.info("Asking confirmation before deleting doc %s", active[0]) msg = _('Are you sure you want to delete document %s ?') % active[0] active_doc = self.active_doc if os.name == "nt": # On Windows, we have to be absolutely sure the PDF is actually # closed when we try to delete it because Windows s*cks pony d*cks # in h*ll. self.core.call_all("doc_close") # there is no "close()" method in Poppler gc.collect() self.core.call_all( "gtk_show_dialog_yes_no", self, msg, active_doc ) def on_dialog_yes_no_reply(self, parent, reply, *args, **kwargs): if parent is not self: return if not reply: return (active_doc,) = args (doc_id, doc_url) = active_doc LOGGER.info("Will delete doc %s", doc_id) self.core.call_all("doc_close") if os.name == "nt": # there is no "close()" method in Poppler gc.collect() self.core.call_success( "mainloop_schedule", self._really_delete, doc_id ) def _really_delete(self, doc_id): self.core.call_all("storage_delete_doc_id", doc_id) self.core.call_all("search_update_document_list") self.core.call_success("transaction_simple", (('del', doc_id),)) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/doc/export.py000066400000000000000000000031601417573700700261030ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_export" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = None self.active_windows = [] def get_interfaces(self): return [ 'actions', 'actions_doc', 'actions_doc_export', 'chkdeps', 'doc_open', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'gtk_exporter', 'defaults': ['paperwork_gtk.mainwindow.exporter'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._open_exporter) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def doc_close(self): self.active_doc = None def _open_exporter(self, *args, **kwargs): assert(self.active_doc is not None) self.core.call_all("gtk_open_exporter", *self.active_doc) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/doc/new.py000066400000000000000000000023501417573700700253530ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'action', 'action_doc', 'action_doc_new', 'chkdeps', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'doc_open', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new("doc_new", None) action.connect("activate", self._open_new_doc) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def _open_new_doc(self, *args, **kwargs): self.core.call_all("doc_open_new") paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/doc/open_external.py000066400000000000000000000035221417573700700274270ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_open_external" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = None self.active_windows = [] def get_interfaces(self): return [ 'action', 'action_doc', 'action_doc_open_external', 'chkdeps', 'doc_open', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'external_apps', 'defaults': [ 'openpaperwork_core.external_apps.dbus', 'openpaperwork_core.external_apps.windows', 'openpaperwork_core.external_apps.xdg', ], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._open_external) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def doc_close(self): self.active_doc = None def _open_external(self, action, parameter): assert(self.active_doc is not None) (doc_id, doc_url) = self.active_doc self.core.call_success("external_app_open_folder", doc_url) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/doc/prev_next.py000066400000000000000000000026241417573700700266000ustar00rootroot00000000000000import logging try: from gi.repository import Gio GIO_AVAILABLE = True except (ImportError, ValueError): GIO_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'chkdeps', 'action', 'action_doc', 'action_doc_prev_next', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def chkdeps(self, out: dict): if not GIO_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def init(self, core): super().init(core) if not GIO_AVAILABLE: return action = Gio.SimpleAction.new('doc_prev', None) action.connect("activate", self._goto, -1) self.core.call_all("app_actions_add", action) action = Gio.SimpleAction.new('doc_next', None) action.connect("activate", self._goto, 1) self.core.call_all("app_actions_add", action) def _goto(self, action, parameter, offset): self.core.call_all("open_next_doc", offset) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/doc/print.py000066400000000000000000000031021417573700700257120ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_print" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = None self.active_windows = [] def get_interfaces(self): return [ 'action', 'action_doc', 'action_doc_print', 'chkdeps', 'doc_open', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'doc_print', 'defaults': ['paperwork_gtk.print'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._print) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def doc_close(self): self.active_doc = None def _print(self, *args, **kwargs): assert(self.active_doc is not None) self.core.call_all("doc_print", *self.active_doc) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/doc/properties.py000066400000000000000000000034061417573700700267610ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.promise import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_properties" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = None def get_interfaces(self): return [ 'action', 'action_doc', 'action_doc_properties', 'chkdeps', 'doc_open', 'gtk_window_listener', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'gtk_doc_properties', 'defaults': ['paperwork_gtk.mainwindow.docproperties'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._open_properties) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def doc_close(self): self.active_doc = None def _open_properties(self, *args, **kwargs): assert(self.active_doc is not None) active = self.active_doc LOGGER.info("Opening properties of document %s", active[0]) self.core.call_all("open_doc_properties", *active) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/doc/redo_ocr.py000066400000000000000000000073751417573700700263720ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps import openpaperwork_core.promise from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_redo_ocr" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = (None, None) self.action = None def get_interfaces(self): return [ 'action', 'action_doc', 'action_doc_redo_ocr', 'chkdeps', 'doc_open', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'ocr', 'defaults': ['paperwork_backend.guesswork.ocr.pyocr'], }, { 'interface': 'ocr_settings', 'defaults': ['paperwork_backend.pyocr'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return self.action = Gio.SimpleAction.new(ACTION_NAME, None) self.action.connect("activate", self._redo_ocr) self.core.call_all("app_actions_add", self.action) self._update_sensitivity() self.core.call_all( "ocr_add_observer_on_enabled", self._update_sensitivity ) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def doc_close(self): self.active_doc = (None, None) def _update_sensitivity(self): enabled = self.core.call_success("ocr_is_enabled") self.action.set_enabled(enabled is not None) def _redo_ocr(self, action, parameter): assert(self.active_doc is not None) (doc_id, doc_url) = self.active_doc nb_pages = self.core.call_success("doc_get_nb_pages_by_url", doc_url) if nb_pages is None: LOGGER.warning("No pages in document %s. Nothing to do", doc_id) return LOGGER.info( "Will redo OCR on document %s (nb pages = %d)", doc_id, nb_pages ) promise = openpaperwork_core.promise.Promise( self.core, self.core.call_all, args=("on_progress", "redo_ocr", 0.0, _("OCR on %s") % doc_id) ) promise = promise.then(lambda *args, **kwargs: None) for page_idx in range(0, nb_pages): promise = promise.then( self.core.call_all, "on_progress", "redo_ocr", page_idx / nb_pages, _("OCR on {doc_id} p{page_idx}").format( doc_id=doc_id, page_idx=(page_idx + 1) ) ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(openpaperwork_core.promise.ThreadedPromise( self.core, self.core.call_all, args=("ocr_page_by_url", doc_url, page_idx,) )) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then( self.core.call_all, "on_progress", "redo_ocr", 1.0 ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(self.core.call_success( "transaction_simple_promise", (('upd', doc_id),) )) self.core.call_success("transaction_schedule", promise) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/docs/000077500000000000000000000000001417573700700243735ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/docs/__init__.py000066400000000000000000000000001417573700700264720ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/docs/delete.py000066400000000000000000000056521417573700700262170ustar00rootroot00000000000000import gc import logging import os try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_delete_many" class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'action', 'action_docs', 'action_docs_delete', 'chkdeps', 'doc_action', 'doc_open', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'doc_selection', 'defaults': ['paperwork_gtk.doc_selection'], }, { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'gtk_dialog_yes_no', 'defaults': ['openpaperwork_gtk.dialogs.yes_no'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._delete) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def _delete(self, action, parameter): docs = set() self.core.call_all("doc_selection_get", docs) LOGGER.info("Asking confirmation before deleting %d doc", len(docs)) msg = _('Are you sure you want to delete %d documents ?') % len(docs) if os.name == "nt": # On Windows, we have to be absolutely sure the PDF is actually # closed when we try to delete it because Windows s*cks pony d*cks # in h*ll. self.core.call_all("doc_close") # there is no "close()" method in Poppler gc.collect() self.core.call_all("gtk_show_dialog_yes_no", self, msg, docs) def on_dialog_yes_no_reply(self, parent, reply, *args, **kwargs): if parent is not self: return if not reply: return (docs,) = args self.core.call_all("doc_close") for (doc_id, doc_url) in docs: LOGGER.info("Deleting document %s", doc_id) self.core.call_all("storage_delete_doc_id", doc_id) self.core.call_all("search_update_document_list") self.core.call_success( "transaction_simple", [ ('del', doc_id) for (doc_id, doc_url) in docs ] ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/docs/export.py000066400000000000000000000034231417573700700262700ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.promise LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_export_many" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = (None, None) self.active_page_idx = 0 def get_interfaces(self): return [ 'action', 'action_docs', 'action_docs_export', 'chkdeps', 'doc_open', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'doc_selection', 'defaults': ['paperwork_gtk.doc_selection'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._export) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def on_page_shown(self, active_page_idx): self.active_page_idx = active_page_idx def _export(self, action, parameter): docs = set() self.core.call_all("doc_selection_get", docs) self.core.call_all( "gtk_open_exporter_multiple_docs", docs, self.active_doc[0], self.active_doc[1], self.active_page_idx ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/docs/properties.py000066400000000000000000000037751417573700700271550ustar00rootroot00000000000000import itertools import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_change_labels" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.menu_add = None self.menu_remove = None self.idx_generator = itertools.count() def get_interfaces(self): return [ 'action', 'action_docs', 'action_docs_properties', 'chkdeps', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'doc_labels', 'defaults': ['paperwork_backend.model.labels'], }, { 'interface': 'doc_selection', 'defaults': ['paperwork_gtk.doc_selection'], }, { 'interface': 'gtk_doc_properties', 'defaults': ['paperwork_gtk.mainwindow.docproperties'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._open_editor) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def _open_editor(self, action, parameter): selection = set() self.core.call_all("doc_selection_get", selection) if len(selection) <= 0: LOGGER.info("No document selected") return LOGGER.info("Opening properties for %d documents", len(selection)) self.core.call_all("open_docs_properties", selection) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/docs/redo_ocr.py000066400000000000000000000104311417573700700265400ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.promise from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_redo_ocr_many" class Plugin(openpaperwork_core.PluginBase): PRIORITY = -40 def get_interfaces(self): return [ 'action', 'action_docs', 'action_docs_redo_ocr', 'chkdeps', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'doc_selection', 'defaults': ['paperwork_gtk.doc_selection'], }, { 'interface': 'ocr', 'defaults': ['paperwork_backend.guesswork.ocr.pyocr'], }, { 'interface': 'ocr_settings', 'defaults': ['paperwork_backend.pyocr'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._redo_ocr_many) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def _redo_ocr_single( self, doc_id, doc_url, progression_start_page_idx, progression_total_pages): nb_pages = self.core.call_success("doc_get_nb_pages_by_url", doc_url) if nb_pages is None: LOGGER.warning("No pages in document %s. Nothing to do", doc_id) return LOGGER.info( "Will redo OCR on document %s (nb pages = %d)", doc_id, nb_pages ) promise = openpaperwork_core.promise.Promise( self.core, self.core.call_all, args=( "on_progress", "redo_ocr", progression_start_page_idx / progression_total_pages, _("OCR on %s") % doc_id) ) promise = promise.then(lambda *args, **kwargs: None) for page_idx in range(0, nb_pages): promise = promise.then( self.core.call_all, "on_progress", "redo_ocr", (progression_start_page_idx + page_idx) / progression_total_pages, _("OCR on {doc_id} p{page_idx}").format( doc_id=doc_id, page_idx=(page_idx + 1) ) ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(openpaperwork_core.promise.ThreadedPromise( self.core, self.core.call_all, args=("ocr_page_by_url", doc_url, page_idx,) )) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then( self.core.call_all, "on_progress", "redo_ocr", (progression_start_page_idx + nb_pages) / progression_total_pages, ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(self.core.call_success( "transaction_simple_promise", (('upd', doc_id),) )) return (promise, nb_pages) def _redo_ocr_many(self, action, parameter): docs = set() self.core.call_all("doc_selection_get", docs) total_pages = 0 for (doc_id, doc_url) in docs: nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", doc_url ) if nb_pages is None: nb_pages = 0 total_pages += nb_pages promise = openpaperwork_core.promise.Promise(self.core) nb_all_pages = 0 for (doc_id, doc_url) in docs: (p, nb_doc_pages) = self._redo_ocr_single( doc_id, doc_url, nb_all_pages, total_pages ) nb_all_pages += nb_doc_pages promise = promise.then(p) self.core.call_success("transaction_schedule", promise) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/docs/select_all.py000066400000000000000000000030061417573700700270530ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_select_all" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.visible_docs = [] def get_interfaces(self): return [ 'action', 'action_docs', 'action_docs_select_all', 'chkdeps', 'search_listener', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'doc_selection', 'defaults': ['paperwork_gtk.doc_selection'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._select_all) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_search_results(self, query, docs): self.visible_docs = docs def _select_all(self, action, parameter): for doc in self.visible_docs: self.core.call_all("doc_selection_add", *doc) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/page/000077500000000000000000000000001417573700700243575ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/page/__init__.py000066400000000000000000000000001417573700700264560ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/page/copy_text.py000066400000000000000000000041141417573700700267470ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps import openpaperwork_gtk.deps LOGGER = logging.getLogger(__name__) ACTION_NAME = "page_copy_text" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.selected_text = "" self.windows = [] def get_interfaces(self): return [ 'action', 'action_page', 'action_page_copy_text', 'chkdeps', 'selected_boxes_listener', 'gtk_window_listener', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._copy_text) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def on_page_boxes_selected(self, doc_id, doc_url, page_id, boxes): self.selected_text = " ".join([b.content for b in boxes]) def _copy_text(self, *args, **kwargs): LOGGER.info( "Copying %d characters to clipboard", len(self.selected_text) ) clipboard = Gtk.Clipboard.get_default(self.windows[-1].get_display()) clipboard.set_text(self.selected_text, -1) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/page/delete.py000066400000000000000000000060431417573700700261760ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "page_delete" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = None self.active_page_idx = -1 def get_interfaces(self): return [ 'action', 'action_page', 'action_page_delete', 'chkdeps', 'doc_open', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'gtk_dialog_yes_no', 'defaults': ['openpaperwork_gtk.dialogs.yes_no'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._delete) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def doc_close(self): self.active_doc = None def on_page_shown(self, page_idx): self.active_page_idx = page_idx def _delete(self, *args, **kwargs): assert(self.active_doc is not None) LOGGER.info( "Asking confirmation before deleting page %d of document %s", self.active_page_idx, self.active_doc[0] ) msg = ( _( "Are you sure you want to delete page" " {page_idx} of document {doc_id} ?" ).format( page_idx=(self.active_page_idx + 1), doc_id=self.active_doc[0] ) ) self.core.call_success( "gtk_show_dialog_yes_no", self, msg, self.active_doc ) def on_dialog_yes_no_reply( self, parent, reply, *args, **kwargs): if parent is not self: return if not reply: return (active_doc,) = args (doc_id, doc_url) = active_doc page_idx = self.active_page_idx LOGGER.info("Will delete page %s p%d", doc_id, page_idx) self.core.call_all("page_delete_by_url", doc_url, page_idx) self.core.call_all("search_update_document_list") self.core.call_all("doc_reload", doc_id, doc_url) self.core.call_success("transaction_simple", (('upd', doc_id),)) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/page/edit.py000066400000000000000000000031361417573700700256610ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = None self.active_page_idx = -1 def get_interfaces(self): return [ 'action', 'action_page', 'action_page_edit', 'chkdeps', 'doc_open', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'gtk_page_editor', 'defaults': ['paperwork_gtk.mainwindow.pageeditor'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new("page_edit", None) action.connect("activate", self._on_edit) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def on_page_shown(self, page_idx): self.active_page = page_idx def _on_edit(self, *args, **kwargs): self.core.call_all( "gtk_open_page_editor", *self.active_doc, self.active_page ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/page/export.py000066400000000000000000000034161417573700700262560ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) ACTION_NAME = "page_export" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = None self.active_page_idx = -1 self.active_windows = [] def get_interfaces(self): return [ 'action', 'action_page', 'action_page_export', 'chkdeps', 'doc_open', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'gtk_exporter', 'defaults': ['paperwork_gtk.mainwindow.exporter'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._open_exporter) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def doc_close(self): self.active_doc = None def on_page_shown(self, page_idx): self.active_page_idx = page_idx def _open_exporter(self, *args, **kwargs): assert(self.active_doc is not None) self.core.call_all( "gtk_open_exporter", *self.active_doc, self.active_page_idx ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/page/move_inside_doc.py000066400000000000000000000122621417573700700300620ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False try: from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps import openpaperwork_gtk import openpaperwork_gtk.deps from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "page_move_inside_doc" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = None self.active_page_idx = -1 self.windows = [] def get_interfaces(self): return [ 'action', 'action_page', 'action_page_move_inside_doc', 'chkdeps', 'doc_open', 'gtk_window_listener', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'pages', 'defaults': [ 'paperwork_backend.model.img', 'paperwork_backend.model.img_overlay', 'paperwork_backend.model.hocr', 'paperwork_backend.model.pdf', ], }, { 'interface': 'gtk_dialog_single_entry', 'defaults': ['openpaperwork_gtk.dialogs.single_entry'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._start_move_inside_doc) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def doc_close(self): self.active_doc = None def on_page_shown(self, page_idx): self.active_page_idx = page_idx def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def _show_error(self, msg): flags = ( Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT ) dialog = Gtk.MessageDialog( transient_for=self.windows[-1], flags=flags, message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, text=msg ) dialog.connect("response", lambda dialog, response: dialog.destroy()) dialog.show_all() def _start_move_inside_doc(self, *args, **kwargs): assert(self.active_doc is not None) assert(self.active_page_idx is not None) msg = _("Move the page %d to what position ?") % ( self.active_page_idx + 1 ) self.core.call_success( "gtk_show_dialog_single_entry", self, msg, str(self.active_page_idx + 1), active_doc=self.active_doc, active_page_idx=self.active_page_idx ) def on_dialog_single_entry_reply( self, origin, r, new_position, *args, **kwargs): if origin is not self: return None if not r: return None try: new_position = int(new_position) - 1 except ValueError: self._show_error(_("Invalid page position: %s") % (new_position)) return True active_doc = kwargs['active_doc'] active_page_idx = kwargs['active_page_idx'] nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", active_doc[1] ) if new_position >= nb_pages or new_position < 0: self._show_error( _( "Invalid page position: %d." " Out of document bounds (1-%d)." ) % ( new_position, nb_pages ) ) return True if new_position == active_page_idx: self._show_error(_("Page position unchanged")) return True promise = openpaperwork_core.promise.Promise(self.core) promise = promise.then( self.core.call_all, "page_move_by_url", active_doc[1], active_page_idx, active_doc[1], new_position ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then( self.core.call_all, "doc_reload", *active_doc ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(self.core.call_success( "transaction_simple_promise", [('upd', active_doc[0])] )) self.core.call_success("transaction_schedule", promise) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/page/move_to_doc/000077500000000000000000000000001417573700700266545ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/page/move_to_doc/__init__.py000066400000000000000000000130621417573700700307670ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) ACTION_NAME = "page_move_to_doc" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = None self.active_page_idx = -1 self.waiting_for_target_doc = False def get_interfaces(self): return [ 'action', 'action_page', 'action_page_move_to_doc', 'chkdeps', 'doc_open', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, { 'interface': 'gtk_mainwindow', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'gtk_search_field', 'defaults': ['paperwork_gtk.mainwindow.search.field'], }, { 'interface': 'pages', 'defaults': [ 'paperwork_backend.model.img', 'paperwork_backend.model.img_overlay', 'paperwork_backend.model.hocr', 'paperwork_backend.model.pdf', ], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.actions.page.move_to_doc", "move_to_doc.glade" ) if widget_tree is None: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to load widget tree") return widget_tree.get_object("move_to_doc_cancel").connect( "clicked", self._cancel_move_to_doc ) self.core.call_all( "mainwindow_add", "right", "move_to_doc", -9999999999, widget_tree.get_object("move_to_doc_header"), widget_tree.get_object("move_to_doc"), ) action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._start_move_to_doc) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def doc_open(self, doc_id, doc_url): new_active_doc = (doc_id, doc_url) if self.waiting_for_target_doc: self.waiting_for_target_doc = False self._move_to_doc(new_active_doc) self.active_doc = new_active_doc def doc_close(self): self.active_doc = None def on_page_shown(self, page_idx): self.active_page_idx = page_idx def _start_move_to_doc(self, *args, **kwargs): assert(self.active_doc is not None) if self.active_page_idx < 0: return LOGGER.info("Starting 'move to doc' process") self.core.call_all("mainwindow_show", "right", "move_to_doc") self.core.call_all("mainwindow_show", "left", "doclist") self.waiting_for_target_doc = True def _run_transaction(self, original_doc, target_doc): nb_remaining_pages = self.core.call_success( "doc_get_nb_pages_by_url", original_doc[1] ) if nb_remaining_pages is None: nb_remaining_pages = 0 self.core.call_success( "transaction_simple_promise", [ ('upd', target_doc[0]), ( 'upd' if nb_remaining_pages > 0 else 'del', original_doc[0] ), ] ) def _move_to_doc(self, target_doc): promise = openpaperwork_core.promise.Promise(self.core) promise = promise.then( self.core.call_all, "page_move_by_url", self.active_doc[1], self.active_page_idx, target_doc[1], 0 ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then( self.core.call_one, "mainloop_schedule", self.core.call_all, "doc_reload", *self.active_doc ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then( self.core.call_one, "mainloop_schedule", self.core.call_all, "doc_reload", *target_doc ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then( self.core.call_all, "search_update_document_list" ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then( self._run_transaction, self.active_doc, target_doc ) self.core.call_success("transaction_schedule", promise) def _cancel_move_to_doc(self, *args, **kwargs): LOGGER.info("Canceling 'move to doc' process") self.core.call_all("mainwindow_back", "right") self.waiting_for_target_doc = False paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/page/move_to_doc/move_to_doc.glade000066400000000000000000000063011417573700700321470ustar00rootroot00000000000000 True False True :close True False center center vertical 10 True False end 50 True False gtk-go-back 5 False True 0 True False Select target document False True 1 True True 0 True False end gtk-cancel True True True True False True 1 False True 1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/page/print.py000066400000000000000000000033451417573700700260720ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) ACTION_NAME = "page_print" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = None self.active_page_idx = -1 self.active_windows = [] def get_interfaces(self): return [ 'action', 'action_page', 'action_page_print', 'chkdeps', 'doc_open', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'doc_print', 'defaults': ['paperwork_gtk.print'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._print) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def doc_close(self): self.active_doc = None def on_page_shown(self, page_idx): self.active_page_idx = page_idx def _print(self, *args, **kwargs): assert(self.active_doc is not None) self.core.call_all( "doc_print", *self.active_doc, [self.active_page_idx] ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/page/redo_ocr.py000066400000000000000000000066061417573700700265350ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps import openpaperwork_core.promise from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "page_redo_ocr" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = (None, None) self.active_page_idx = -1 self.action = None def get_interfaces(self): return [ 'action', 'action_page', 'action_page_redo_ocr', 'chkdeps', 'doc_open', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'ocr', 'defaults': ['paperwork_backend.guesswork.ocr.pyocr'], }, { 'interface': 'ocr_settings', 'defaults': ['paperwork_backend.pyocr'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return self.item = Gio.MenuItem.new( _("Redo OCR on page"), "win." + ACTION_NAME ) self.action = Gio.SimpleAction.new(ACTION_NAME, None) self.action.connect("activate", self._redo_ocr) self.core.call_all("app_actions_add", self.action) self._update_sensitivity() self.core.call_all( "ocr_add_observer_on_enabled", self._update_sensitivity ) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def doc_close(self): self.active_doc = (None, None) def _update_sensitivity(self): enabled = self.core.call_success("ocr_is_enabled") self.action.set_enabled(enabled is not None) def on_page_shown(self, page_idx): self.active_page_idx = page_idx def _redo_ocr(self, *args, **kwargs): (doc_id, doc_url) = self.active_doc page_idx = self.active_page_idx promise = openpaperwork_core.promise.Promise( self.core, self.core.call_all, args=( "on_progress", "redo_ocr", 0.0, _("OCR on {doc_id} p{page_idx}").format( doc_id=doc_id, page_idx=page_idx ) ) ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(openpaperwork_core.promise.ThreadedPromise( self.core, self.core.call_all, args=("ocr_page_by_url", doc_url, page_idx,) )) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then( self.core.call_all, "on_progress", "redo_ocr", 1.0 ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(self.core.call_success( "transaction_simple_promise", (('upd', doc_id),) )) self.core.call_success("transaction_schedule", promise) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/actions/page/reset.py000066400000000000000000000045331417573700700260600ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) ACTION_NAME = "page_reset" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = None self.active_page_idx = -1 def get_interfaces(self): return [ 'action', 'action_page', 'action_page_reset', 'chkdeps', 'doc_open', ] def get_deps(self): return [ { 'interface': 'app_actions', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'page_reset', 'defaults': ['paperwork_backend.model.img_overlay'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def init(self, core): super().init(core) if not GLIB_AVAILABLE: return action = Gio.SimpleAction.new(ACTION_NAME, None) action.connect("activate", self._reset) self.core.call_all("app_actions_add", action) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def doc_close(self): self.active_doc = None def on_page_shown(self, page_idx): self.active_page_idx = page_idx def _reset(self, *args, **kwargs): assert(self.active_doc is not None) (doc_id, doc_url) = self.active_doc page_idx = self.active_page_idx LOGGER.info("Will reset page %s p%d", doc_id, page_idx) self.core.call_all("page_reset_by_url", doc_url, page_idx) self.core.call_all("doc_reload", doc_id, doc_url) self.core.call_success( "mainloop_schedule", self.core.call_all, "doc_goto_page", page_idx ) self.core.call_success("transaction_simple", (("upd", doc_id),)) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/cmd/000077500000000000000000000000001417573700700225465ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/cmd/__init__.py000066400000000000000000000000001417573700700246450ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/cmd/import.py000066400000000000000000000035561417573700700244430ustar00rootroot00000000000000import logging import openpaperwork_core from .. import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() def get_interfaces(self): return ['shell'] def get_deps(self): return [ { 'interface': 'gtk_doc_import', 'defaults': ['paperwork_gtk.docimport'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, ] def cmd_complete_argparse(self, parser): p = parser.add_parser('import', help=_( "Run Paperwork and import files passed as arguments into a new " "document" )) p.add_argument("files", metavar="URLS", type=str, nargs="*", help=_( "URLs or paths of files to import" )) def cmd_run(self, args): if args.command != 'import': return None self.core.call_all("on_initialized") # when no file is specified, we still start the GUI, because the # desktop file always calls paperwork-gtk import (see install.py for # the rationale). if args.files: files = [self.core.call_success("fs_safe", f) for f in args.files] LOGGER.info("Scheduling import for files %s", " ".join(files)) self.core.call_one( "mainloop_schedule", self.core.call_all, "gtk_doc_import", files ) LOGGER.info("Ready") self.core.call_one("mainloop", halt_on_uncaught_exception=False) LOGGER.info("Quitting") self.core.call_all("config_save") self.core.call_all("on_quit") return True paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/cmd/install.py000066400000000000000000000121711417573700700245700ustar00rootroot00000000000000import os try: import pkg_resources PKG_RESOURCES_AVAILABLE = True except Exception: PKG_RESOURCES_AVAILABLE = False import shutil import xdg.BaseDirectory import xdg.DesktopEntry import xdg.IconTheme import openpaperwork_core from .. import _ ICON_SIZES = [ 16, 22, 24, 30, 32, 36, 42, 48, 50, 64, 72, 96, 100, 128, 150, 160, 192, 256, 512 ] class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.interactive = False def get_interfaces(self): return ['shell'] def get_deps(self): return [] def cmd_set_interface(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): p = parser.add_parser('install', help=_( "Install Paperwork icons and shortcuts" )) p.add_argument( "--user", "-u", help=_("Install everything only for the current user"), action="store_true" ) p.add_argument("--icon_base_dir", default="/usr/share/icons") p.add_argument("--data_base_dir", default="/usr/share") def _install(self, icondir, datadir): assert(PKG_RESOURCES_AVAILABLE) png_src_icon_pattern = "paperwork_{}.png" png_dst_icon_pattern = os.path.join( icondir, "hicolor", "{size}x{size}", "apps", "work.openpaper.Paperwork.png" ) desktop_path = os.path.join( datadir, 'applications', 'work.openpaper.Paperwork.desktop' ) appdata_path = os.path.join( datadir, "metainfo", "work.openpaper.Paperwork.appdata.xml" ) os.makedirs(os.path.dirname(desktop_path), exist_ok=True) to_copy = [ ( pkg_resources.resource_filename( 'paperwork_gtk.data', png_src_icon_pattern.format(size) ), png_dst_icon_pattern.format(size=size), ) for size in ICON_SIZES ] to_copy.append( ( pkg_resources.resource_filename( 'paperwork_gtk.data', 'work.openpaper.Paperwork.appdata.xml', ), appdata_path ) ) for icon in ['paperwork.svg', 'paperwork_halo.svg']: src_icon = icon dst_icon = icon if icon == 'paperwork.svg': dst_icon = 'work.openpaper.Paperwork.svg' to_copy.append( ( pkg_resources.resource_filename( 'paperwork_gtk.data', src_icon ), os.path.join( icondir, "hicolor", "scalable", "apps", dst_icon ) ) ) for (src, dst) in to_copy: print("Installing {} ...".format(dst)) os.makedirs(os.path.dirname(dst), exist_ok=True) shutil.copyfile(src, dst) print("Generating {} ...".format(desktop_path)) entry = xdg.DesktopEntry.DesktopEntry(desktop_path) entry.set("GenericName", "Personal Document Manager") entry.set("Type", "Application") entry.set("Categories", "Office;Scanning;OCR;Archiving;GNOME;") entry.set("Terminal", "false") entry.set("Comment", "Grepping dead trees the easy way") # It's possible to add several actions to a single desktop file. # Ideally, we want a main "Open paperwork" and a secondary "Import # these files to paperwork". Unfortunately, the specification does # not allow specifying different MimeType= for the main and secondary # actions. So we always import (possibly 0) files. # https://specifications.freedesktop.org/desktop-entry-spec/1.5/ar01s11.html entry.set("Exec", "paperwork-gtk import %U") entry.set("Name", "Paperwork") entry.set("Icon", "work.openpaper.Paperwork") entry.set("Keywords", "document;scanner;ocr;") # PDF and all image formats supported by pillow entry.set( "MimeType", "application/pdf;" "image/bmp;" "image/gif;" "image/ico;" "image/icon;" "image/jp2;" "image/jpeg2000;" "image/jpeg;" "image/jpg;" "image/jpx;" "image/pjpeg;" "image/png;" "image/tiff;" "image/webp;" "image/x-bmp;" "image/x-MS-bmp;" "image/x-png;" "image/x-portable-bitmap;" "image/x-portable-graymap;" "image/x-portable-pixmap;" "image/x-tga;" ) entry.validate() entry.write() print("Done") def cmd_run(self, args): if args.command != 'install': return None icon_base_dir = args.icon_base_dir data_base_dir = args.data_base_dir if args.user: icon_base_dir = xdg.IconTheme.icondirs[0] data_base_dir = xdg.BaseDirectory.xdg_data_dirs[0] self._install(icon_base_dir, data_base_dir) return True paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/000077500000000000000000000000001417573700700227145ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/__init__.py000066400000000000000000000000001417573700700250130ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/make_icons.sh000077500000000000000000000006301417573700700253620ustar00rootroot00000000000000#!/bin/sh for size in 16 22 24 30 32 36 42 48 50 64 72 96 100 128 150 160 192 256 512 ; do echo "Generating icon ${size}x${size} ..." source=paperwork_halo.svg if [ ${size} -ge 96 ]; then source=paperwork.svg fi inkscape -w ${size} -h ${size} -e paperwork_${size}.png ${source} if [ ${size} -lt 256 ]; then # max size for .ico files convert paperwork_${size}.png paperwork_${size}.ico fi done paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork.svg000066400000000000000000000056651417573700700254630ustar00rootroot00000000000000 image/svg+xml paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_100.png000066400000000000000000000066251417573700700260250ustar00rootroot00000000000000PNG  IHDRddpTsBIT|d pHYs'b6tEXtSoftwarewww.inkscape.org< IDATx8TWmFjw/mZԵm`>f<(>.%?nvY2zr֖%2cɘ{ha̡c{u;=9H$D"H$D"H$iS#> jjj"B0D"D"#B e.uud2H$HKK8螽aԭTwߝ0?0BB2ѣx<ىܹ/33_&N"oKtpp>!!ٳg߻}vqii 99达 )\T 8÷~tq{{ @( n:G͛70gggdL8rHBh4330wIq&ݺukѝٱcO>Ň' %%۳gO_ZZt߼KBo=ioܹˬX,VP()KKKKe#5BccDeZ344|HGBSSӈgɼNx- bs?jre[OTT˗&z\LSSS_OOϒjXH$p!^DD(..dR4,уN ypF *uvv222𬬬W~6::"zSt:= M#uwwNMMW~ɓ'2ss=)mժU3ϝ;.t 1*!""/G;DyJڴivW; y)J###{}( jټy?||| &g...Xee%x*<?OR#~1'N`aaa|HMM~׮]{jhh NKK#c111 㹬F }7nHl6{_D;wXJOO3q!%%m"H0. ǎ ZZZB_ޖ`śTuuuÇNh.&:.6믿QYY)D=H$+WbX,p; !.RA,oii{Yd21,W;-[%:HJ`E4Thhhadeexxx`8ŋ!77W܊\xUv:w…Vy."J!,, KNN~&J̙3PUU%w{ڵk3000:;;_z*oOE]]]:@ (xd2`0DMfP(ݻw=; e2X,V}/HJJ-#:nccwСǏOpŋ}6$&&N]@&:6{lm*z,$$![Q|ͭ\ 999 i;44 !At%D0c'''Gp ::&Tp,n122%:neewNiHjjj ߿'OA8ɼFtc~>ebރk0V<: tttVh,ihQ ;wJJJ~>M":#(xeU2HxMMM|666rBPj>>>o޼9bEٵkW?˅H,6KGG< iffw򚚚1 q:uצJW=܁ &:s?looWD=]]] F@)PB:aY;w2vrU}TZZ*H455uBb2%$W4{U;y$>xs)H !!~w;++rJc&bӦM}||:QXQQǃHPÑb/TV3fuuW&޾{h}!w<544);oH$a[lA .p[ZZ~'stuu;[|G͸t܁m p8O5 A^^^P(PFzBHgŊ?ÿ.]tO?tBH$;vl@R%@!X,FP:Zw=55Oe>è;99uutt nJp1QqGR[[+ڸqJTjtHHH[ f. <У``V\\\ 4*yB566Njy]"@PP/%%UɆ`X?!//[fV,5 +aooo:;;c /{m'K*/yJF'&&6w-c+333V Nj-ZBt@BKK#GG y,K0|ٳgBQEOOX[['G[~S@@o|>ݹs5߮M璞1/O\|ё?| bbb'C566J?ҭ[n3 i N> * z={iV#7vzxl6ddd#>޺y%0Lm()) ͜9bCz{zzz͵7x޽{?)@cPiB^mjj_%P@@@EvvCKKK_VZEYxk-5kBgIIB^Ńk"J̇6lꫯ~d*^(a*H puu-D|!|gcckssK7o(R]]-x?b-{LHn1N+ oĖHsFq1333Fo(i4Vi||)m}GGǴ?GKݻw&&&8@Nb=8y']p}x $+.Tjqm4<1l6k``1711q'zzzӬCd?H$D"H$D"H$D"H$4U\IENDB`paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_128.png000066400000000000000000000107071417573700700260330ustar00rootroot00000000000000PNG  IHDR>asBIT|d pHYs,,gtEXtSoftwarewww.inkscape.org<DIDATxyXS׶-E | B$PƄjl `WEE+ ꫵT-(JAȤ @B l3Yg䜳A         c<f%;FCCQAA!/5kV rۗ-[-ޛ9syyyzr##jNoٳ (AUUNljj*o<2qHHWYXXRϟfee V⎕ Yvttt3H+++ÇMMMnݺ$!nooWJJJH$>---`0x,;vT qNL,-[ܺqz*Դ˫W(?^\\,R'&N(յB4bL&'66wvvvE!yAJ 3Z[[:;;a˖-m۶^tI?H97Mdll|ݽlLvbg{yyq~gqqqK,1Ɲ7"䤗WUCCDO>͛7s҄G1/6lw.(tz_|dD t:h\ A[[ioo999mvÉc;;;Ƶ|uuu{;/ӝ~q;==}q#`=~xϝ;w`0ҵWaXBsp'hoddthΝ%%%p}TTT  ""z{</IMLL]YWW7dm2?εg HLL Ooo/0|D|$GEE$#88ٳg;|ػw/|>!''gԦAǝiASS};;ߓ^56[PP ѣ ǃ7fjkk{]x-K_ZZNs87tvvN=CBBNjAcY~~~FFF:R311v޽"o pi}d6Ν\HHH]_׌4]rommiiiPJ佋ztuuq@PltGiРܸb8tPΝ;9ϧeyyyAee"Hctuuw^xKZvuvv9sFOA))) wyT*5lŹR;NQQNskjj ;;>WN؜077;Sѻ}Е޴ioɓ'!!!A뉈h|wVNLHHHӧO%Mώx~ _Wss3_4O jjj֖4e4TVVt:ݻ|2ƾ}\*۽{wBmܹINGGaH;pL&ﬢ<==E*wpڵqY4m?źu!вbz6;<<|P1mL܅5յkٹ|vhY3Lk?㥺ތ U\pCfV8R;]tgaa'!66~W(qD&ڡcbbFl>>>G|}}X?rH* wmINKKkXۡeA544Xs&.Cgeea=N^^Ғܝ# >>>#qJҔMLLZ.../U955bbb@Vc >544#NrrrX,x8{LcaX!Eu455t:}вVZZ* `yLc?k֬]ɘAP.OZ֒܁q>~<<3 pAz*^ 7n3.]ʒbg˃lEE9 ) hB0xPSSoBfBVxAjᄏ@__ŋp[_{-wVaaV=[zM/6Xh2sss [[[Kh ꫯܹ+ӕNGdshtZT*  hh-tM?wݻw9p3*:wڑ#G&}Wqqq)CM3p/]?qD F [.v֭wsrr$7n~*Lwggg$6j*;;SNMY,MzM;{@pav_b׬Y#qE􌈈<8q]CC!Օ"vp喲AIIIō%j:E ~UӖ[Ç~~~l>k0j``h3Vbspss+صkWGII΂ B;Bcc#[.w(# ܼI2TTT)]<ܱ@FR0M ,@)c455cBC.SAAAAAAAAAAAAyd(oltfIENDB`paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_150.png000066400000000000000000000123561417573700700260300ustar00rootroot00000000000000PNG  IHDR<qsBIT|d pHYs<<*ƀtEXtSoftwarewww.inkscape.org<kIDATx{\Ca  ! # "ke/UCGmkVD2QxX,r"r'J7P@K.'Oy } DAAAAAAAAAAA;b±522Z+QL "bjjh֬Y j&E3gbX'ޞ{D. ؘLב Cdddȸ\.EEE&.."0\ݵkWK{{sJ.ۻhH=uMMM F9sl E'Nx{rΝpԩQ|TTTԩSp###W>bF|TjTEE]n޼9Tq8b,,,ܮ4Prl"+++A$XuU!sCYf:R  ŵk`9|! #k}>>>eXΦ=<{'޶=:YTWW'?o޼+/""!d{'>m{2#qE9ǣ{Μ777CDDܾ}[++,,T:::&f*P;X]RTgi@@ݳ+**իWCcc֎!d{"&*a ZZZ 00-W><m^:sn;ɘL,Xb=v-‡&x<ޏɸ'eH]zj7ϧՙ ;wmWK@ɰEeG.]Zyƍq LEEJ Νw!!!WhHRrvW޶=_R|>za{Ϟ=)gMfff p'F=msIm{t kժU ,, q6Fpvv[%Ю*amԩS|Bxxۮ"jBq'megg\]]銊gȀ[jY]?3I8::]lYkhJX髯J S蠏BFc,z xK,:4MÖ-[ 33GhJOOoŝ ҷmO}}=1Ԋ<@ y"""CdÓd 8)Ym:tZh5m9w܁իW q'pѥǏCpp0gϞgN%|7ojY]zwmpO_~p3NfXufSS,Y>~K 'O2,I$pss n__k8ҥK|>j,D6lN5-[4P}c`mmm%EEErҥҡTUUAXXTUUiZWUU' $3aaa52ƍn:,ȚbŊ*f:;;g5*??_uKMMhl׻?=a㊩{7F]RooojS=ȟ}ړRPX0rgSl~HT;{)pjǎ^@x lڴ ._щֵ$u  =˽v P]^^yT;#RȦߚbkk1 _򤥥QB_ρn߾ ׌111M&&&c1uqqb]]]o?sӧOCtt4csѪ{&,KKGG/rs_v D6.//CM@϶kYYYw*((Pwu+wA)J_ӧ#SSS4c dmmlll xS>۾}qǡqXIII>i_)B]vMSo``clll9c _~yzPPT33kyU[[)B˄7WaKK !˯d Bhڀ3a2sMYp͵kKKK{&hG6֕kϞ=7op+4FL&ԀGvJ}XXXMue`buuuʕ+=aoo9oiӦys8wH$Cը~gkz; eee=sE :dCCCy}zBf~Zb'Fobuwwlqqq,+FK!988|[,fwQZJJII=;* 6o`ggQL233[ۻw#Mm@6ơob-\6L!!!fcb0k)ʢ̙G]O_ٳݻwK~^/[EZk/HTU^^>|\EBz"I,__߮B[[V;Ꭽ3GGǣF`|I ܁k֬lhhPXF`r흿|򎬬,3㡨 `0qBceVVVy}_+--UTVV8οq›1cOgϾ;5Xc+WȦmB                _7IENDB`paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_16.png000066400000000000000000000016461417573700700257510ustar00rootroot00000000000000PNG  IHDRasBIT|d pHYsttEXtSoftwarewww.inkscape.org<#IDAT8MN\e5k30æHvZ hx 1>< ̃zb1*i"  ߬׃97:i6a<f 2* P+\AAV`Ǯ4>;ւR'i hE +ZHYDb@ 6G>/OCfU3 Ȳ#8 !RaDNH>TeZtv5 ]V et "Jd]ԗ֑)"@ "@f!ٍSo;= $AB`dl<,Ηٕ[d9 n=jGV) $* ǃVkkSU)l}e).'GfAYQx.'c|sV<@?AJP*F:zY ~T*Nĥp\?w n( [&BR`hG|ǯ_xqp27V_Sk_K+ :&_|e|xZ?%Y ?xw[/.,1?ݓMW~Da[mضy^՟~1t dN?ػ%tWVюZH8;g^ݓܸ;]sYduD* l\ 萋ٸE!=kHCid6-H~.p, L$"~ ^XyN.ճ{'$Ij(P|g"6Rf`mro)"O<7xIENDB`paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_160.png000066400000000000000000000131561417573700700260300ustar00rootroot00000000000000PNG  IHDRg-sBIT|d pHYs%tEXtSoftwarewww.inkscape.org<IDATx{\TH >^-!33S;eʔqHObӧOW6i4 $ #Gh;;OpI?|''AAA%W^e@*jM׃Zgg)`~%Dr7޸[QQRSS5|>_pBVM6Y[[3a̓dbcck[ a裏;vhP;p¸ r+{yhj4iΤ1Wr89 P(HJJRCbX mqa|xbxPPPɕ+W  ..N訞3gSWWnO B;CAMũ:;???׷)""i qpp>ouD"?^ʸΝ|~;655T*-Bqpa:88ĢQ1wVI$*44RSS;i޾}{";H\.w\.W$%% i( -Z>}^sA, 2hA,+~r=RRRtLL6((|o]=dȐ)w0Egg%K*--n@bb"H}gb xtii)g!bBTTݚFw}WM]>nϯ !Ž>ߔWoިRzTxE_^kѣGӧO7x/qP(ZwO,:H$trr2Õ+W:a` PA^,0mڴҬ,K??~<*ܹsۯo3-܃Aȝ+n޼ۺ:xW`ŠT*zR WBdG?gbii9dTTT{Txϟ:;;))) ֭{DGG߳{p1r+%I?=vJ||ݝKBbbbΝ; 2p|'&%%Q=hKTܹsSn݂@8|+$$!=Pn ٳg{}bёՕ>p677hy<^܃EqsttL (,,eiLF߼y~'Xp!1b4ߢ޽Ò{H@dd200P0 lݺyPo>|Xrcp{fVVVb:]XUZZ 'Nk EQtR4MD"GdQ򢄄IG52χwBpp0}~ 6YYYHgzN_5bbb>>>Z˗/C@@5{@ dLLMMe:(UWW7a5@JJ um333)q%`Ĉd2oV[t'ҺgϞxxtZ~~># =D;6T&]%SyzzRh`ժUϘ:uj B  inGQ}zt Чsie2}ɖaΜ9p∊knn;aŸ޴#c_|rssZ \xBBBzԝA NkݎTCC˖-Z?~Сw'Ѕ[!ÇH$?#c[AANLLl0LKw.^N1s;2ٳJT[Qo۷KL&L(FN11 N0O2X u݇j,y<޾Si}*--zΝ-H?! p4 NNNWBNAp8E;2%%%NNNTAAc'''ŋԝ֭[W;jԨ5T:oGƦ&xTTۋ[lUVlPoTUUP(<;ɆH/TUUSL7nX4 ˖-{ ł nIp'ې#;x^ɓ'5|_^^# pCmnGvi:xxoÆ T !((nܸ'6^z饛qmnGܿf͚EJ?~ ۙp1SFؖJtJJJm pBрL&+DOKSkku yAkݝn{;vYG=w1i([F;26T* ϟOVSS!!!#NւH$: Ҏ?UȵkWWWz߾}.?1ҥK+9΋ CߤB}ôL&;:?`zNWEEE .iݎ,Ȏصk޺Œ3JB/]knGvvdlj^uvӛ`72224ƍیXtiرc?dl#c[ZZF*җ.]j^{5!jv!]40J,9uԛGW'm^IQNt !//Cthkkwvdlj^wx@wxW5jhhX'2VЎmZ̙3n?x 1L_j] pQO#[vmɆ8;QGya`ݺuvZ \YY㸋' `tdddMWT*DDD#G+Bv;|Ҏm{K***`Μ9)Z wa=çTF4nܸ;&&&&MMM93IQZx1mbb.^hs~'?{b߿_UUU%2Q!GFGGW) ܿbuu5̛7XN7*++A$ %˫0-- ۷aҤIYnҥKXBXXm333'I7D\R !!A_U#H]\_tz;Srss>w ٘1cvwwv Nc֬Y>}zY"##U~~~TWO+QzKCe'No߾<==۷ojBg&XqppwR:cǎ]@vSNr;SZ-%ٙ3gv={+ ;A_ر~ыq'BWmj37~嗱m7h4N<B 6 YZZ" #[[[rjrik #7?? !c/Ig:Č?d&!AuuuUUUd&--~РA,-- 4iԩSDlGYMOujcTƍ;,BS$ɩ3f_r̈́qLNT* )III뀀2J8k,ug}z<&R* (9zh3g4988|ӑ$zt֬Y`nn.|O>r;o&30 /!4DGAA V^~]Җ.]zm= 'O.:x ϟo1c~=XÜ9s;ioe!A6662,g:ARGG+ǮK+%K}fYqLLLIFFZM.] uŶm42hPdMM3$QaZWWWٳgPXX/++BI05{!j1c {RGDD:n߾ C .w,^w޿8YD+ʻ111=l~Itvvl":u ٳgz0}'kaV(O>XoM[n:99?3l߾~Aii^رc ̓ild<ވo(5͐aܸq@F .nmW\Y=C{#0HT >4i?~ PҀeKFWkkw ŽvqBbb?C.]B>|x!dB8p)ē899Cxxx0{ienܸ- ~m~ԨQښp+Q!..f͚ՒQ]] NNN9;ڃER߿ޞ={Z.4YشixhΆطo@puu%0 ŵȂ_~Zՙ3gtJR8p~-WR앭[V1 @7uIIBEXjUn| )UJYY,A=ŁK&dر|ݵ{;M,XfAGkŁWʉmC]V7Sa̙YO<a~!t=q"\j7o֪T*B=z4ݻReEDDn8۷ov+++c򁁁|Ik<@8<|(˿M7++d2\jWWWaf}O>PYDQBTafqY˗/4nڵW(Bjjj׶o&L/^PqwﮖdK):RŁK&W[^^k<Gc;REqR{9 4HXxq...ѣG֭[i֬O>䥝| !Վ}wP=*(JԩS^{ٳg)TּLBhJGF8;"0@к܋/B@@"sswh7LGa踜Vߟn$&!!ƍƼnܸ,^48p?^q_{&mMg>?q/_0kP¨;;;7olo yyyW6 ˟B,h7sniTYY 'O5l`J'3f(HY8mj!=x<<<+W6UUU-y:[{ݽ{NU˲gsj1:tHPT?YYY %FQHh8pC=OԊ'O?I\]|n/4 o};v Ǐ'#zB$Ç5;;ۃ"Ү=+**B!UjW "1fffl߾}]w>ܻufT*-í{2уޖH@`ҥK֭[ Oӑ+VrQbnn.q`Xbͯqx{{gYUM,++ |}}&/ĉa˖-R&T`Y3V=&TCNNN+2)u%''8|ǏaĈp*w!ĕv/sڵ妲(bŊj77z٪{DI'/h7)ֳgJ1cLaKQQ'Nlt"[]vAXX4[QTUUJ&LS.2dHٳgvիWEggg/h111F^k֬yioo1H8L=gϞ=L HHH]\\nCTTIW^^0 ұ.g \=&#G}V]ǎ:Kn?|||]&ܣlk׮mv ە+Wnܸ!Eiݺu XMRP3fL'O >IIIZZrA \e, !i7Ecnn?O%eeezj}}}}~͂iӦAgJ;v?iBgf:qegϞpf9=AGeypqq!8nDq ޵ǧO'O6l w7V:88=M,^_hQikbEQ+WjNݼyڵkm!U\\ r9!Ēwqq>}t`ذaBc٪$W^Sh4j\.?UTŋEZ-4w'>>Lbl#d?NqbzQ,>/ׯyΖft:>WC 0̶gϞAYYDDDaaaږ3BDD$&&Jb+%%EdY8DmdaaDzc͛7۷.\`2j:<==s !Jg[Gv6۶md=xcbb};L:[{B$$5:Vh5QIhh(sqqq!&M"ZniiIlmmI޽L&#jpG8#*I ++//#TЮEjҥ+xΝpB׺UWWRRRBrrrȑ#G˗w(*++l|}}M> 94ɯ@$%$$T0 sRûB!9qܝd:u$L0W:::]8ƈs 6_f͚RRy}jjсPTT'LСC+L>D./ ?,ѣǻDAKKK0i#z½{@.. I+k׮} B()ݽM $ݻ;d?Ю$КB!B!B!B!B!B!B!B!B!B!B!B!B!?bf aIENDB`paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_22.png000066400000000000000000000026741417573700700257500ustar00rootroot00000000000000PNG  IHDRĴl;sBIT|d pHYsgtEXtSoftwarewww.inkscape.org<9IDAT8eMO]{=^p1HhSrՓ꼝$Jt`ꠒ6Mj\0+s:ݫ:k3ك%x\[ƇʢHD] )+֗zP :IP*o!Qgvt劔 pu5t|fDDdT҂ (AӹoW/~$=,W(IzDW+:audddpR%-"ޫ8X֓úYL*)B"  "jK1T鉱Nڀ;W]ü:,X&jTĐ!PUh a& Ѕj')k??]yb˺@kT`HEPP%o&3Wg?{/5vំ8E&Υ$j ZјLT4"@e==9$ .,9ig_ۗ{~wm/\Xi00 `D](U b#=οR\0?$"?nnmPEƧ6 H)*fA& 2H%a?[K~7+߾*v[[y|=JUP< a@*b !Kk gT\nl;ɻ\v?tOgEBxOPU4 P |dnU? E^%z Xiuw~<7^pqi/ŕ[w9?99) u޺-Ýٳn|lvk㖇"#MO6W.O>M~Y|][Oefz%nmj>} ׿UvGjёI|ys4 kԩ{> h.DNrQɽvIcr{sk%֛LO@!옡%zi79S {;{962RhTgg'ߣw?&c_-@e<)!rR58PH ,667O1{?|nIkř7_ >ce&QsÝЎBKn?&,RE xTg]ui 5Ӗ3=?sѧVuI0P'&߂eh/]A;e|zMkq+n+5UCX!:Pg0AXw}נ]I\K/qp+W}*OLc8JWlL qz^SOElS%IENDB`paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_24.png000066400000000000000000000032311417573700700257400ustar00rootroot00000000000000PNG  IHDRw=sBIT|d pHYs((r^tEXtSoftwarewww.inkscape.org<IDATHuo\Wƿo}ΜcNL\Ԥ TPRRw TyK^(4mp8r$vƱǗ33g^Ǩ%a~{Zk˸|l*Cb+%X"- " > XPZȗS}8!i@ J:/AT޺rTk''GN?ÅLOl)w>}/bJgV,@IS}PQ$3@jI fW~>(QTo ¹q~) v ,R=J`N4657w`hd59t8,.=]ْϽaD` 'QX]~r} `.ݸh93S UhkSWj=q,,B;+n2BhCiI#z(ܞeҠ;;$Y"u=)*t%d43Z{2RW d Ac#'t# 8-@4=p:?>7kh`G!$ZD*t0:G V @:\/r͏NCҸ fO^m%V"1z( EEtۢU|Z\;[[8we {hIENDB`paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_256.png000066400000000000000000000236111417573700700260330ustar00rootroot00000000000000PNG  IHDR\rfsBIT|d pHYs X X`rtEXtSoftwarewww.inkscape.org< IDATxytն0M$5vwL0Q/H)ps]cXu{ {3QE! * B@I IOj`s2vtgjJɩ!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!Biڳ{Iz8tKXC&)?((H0AB'EYQNCA8m۶d!;Bt:DINuٳl6kϣ  u%Stup՚$IسgO,..X+d|EQ\Ǝk;x \.vKGFÁ .TeY:XBHH5L<;O^}DDm4gQQQqq?8zn;t`t>(BȭL-6gwyyM~ee%5-f4߯uHBȵĠ<W^UMfŊ ح[7޽;:u>EB" z:tP~{_t)Sܒ$i={aÆaEEMߐ8ı>`B@`xyԩS}ӧOcnݴXwRRΘ1ɾ5ʮ_d}Єv銢lT6gWYYY>"uP$ӧ^l۶ z%Y$v>uy܊|ҍF^iii>"bEE>\ SyW_}AqY<{"##-Z& |D={`dd֣G7İ0/>Ǐw:DH fq.]/_nqҥ(999 ~{"qU`b} 搐qW9dȐ[6ˠG2Y7~xUQmڵ8rHLOO7g Ey8qk |+yΝ;^zc=ՍzO?EQ,+B)d2+b{'.;wY>"b~~&IN0A۾};FGG/Ƙ+ g}GAe2vZ,EWi. g͚Z ?C47}_z%UO"!&TMeSN˗/o{ũEEE8w\l׮Mz߲2O'x 2oma^4Zfff¾}vJ %$AɄVjwߡ DҒxaС%oNf""bnn.j7D+dR\mK۶mCӉ<U=x74I&okuU .]w݅cƌ;OSUVV`O!L-FϪN2ŭ(ylˆ\|W~'<%۳R0L$j;˗/{eǩSk׮Zbb#ɾܲe~#`t)Sܲ,kW ׯ0mnEQJ(oz<՟ŚCQQjڀɓ'7;OS RϚ_eYƑ#G5رpܹCɓ';%IzE&&ϊ[]sv_}߾5k֠lƵk2C^@8 Naivؾtyׯiخ];ܳg0`5$$)3qP>dȐhM&\p8pاO`i(˫}I4\`* ׼v9;pرҝ\.&$$X`4 5Ϟ=zL4IEE1B S`999-W^yEe'I@Z 2;<]smb6{hG7t/l6|($IuB!DMT}mٲe 8k֬^Řl j9sf,˫Y' |Wa[}5ǛN0AUEꫯytĉGxiFjvǎkcݛ=IIIZ_Zii)0"Lq:AH`y;l_[v&I>78pp̙^T6mBKJ|f2L&\5|՛Yq\8k,UEmʕ7#""pٲe صkWN{uAC4wbLMMU۵kքSs;}]M@%I1mk6mBɤeffjw\8c ޽;o"[UUL&;N⿢$Ii}yUݻwWsպիl6?.rQ#lyaV|(2{Zmӝ'11< ¦0aCŬc222-/,Ͻ|ZZ8zh۷/>y۷9@$$AQyM>k1** _}UE֊+PccYLED+Vh ivɒ%ׯAdv46`=Z+vحfsVE۰a-LJJִxs+ A]3vXkӧOcnݴ"UUU8j(ׯ?G vz0&״-ͺuP$(uݻ7>cX]]Z;DQ|h-Ze;l_r\Wsun}vׯ588x&?o}m˖-h4AL={|a駟(EpiFA^֡CoV54-ZTz!ӧvijpփ%$ wxM:ZaREE>\ WSzٳxq;OSK,;X@(FO;l{HW^j}:۷pܹXYYw Ϩ6CK.DQgy^'h2pʕ^vJWbPPQR;l߳8~x`0h~mXܵkk 144veFFuesLLLԺuVN'L={DbРA6f=vؓ&Mrz1*??_e'MT[x]w1c;OS}w(T h4nL앖~<$Iڧ~Z}8[1n7&&&Z~%Lv،9s{ǫnW_aDD._7|S$i?PZQ;F#9U}yyy[lrt  `=Zkaoݺj4My橂 hyyyrӱ{xIf͚uYYvؿ+kDj[m5**Jo2BdQԐQh_hQ_ӉSLq7x\At. 0u‘)U_z]u&'\QQh jC^G}{Ay+7U>Ս펳~zeǍ5;{ii)8 + x"JX'EQ###m yvO>5Ƒ#Gcǎԝ͜9ZY'? 5^$1==]R 믿ƈ\ti#Ӝjv'^E(j<ߣd 5;R^^Z,ܼys\+33qN"ReY>> v 6lpXB(uNHp? ZT]]cƌqȲIt\(_ P(-##/߿ 4 {e *p%eYۿfm!-EXQ}W﷊^WFIN^xW}nS V嘖f$nbqwɴ8[^.j 8t8hXl0WzPe͛oyӤt:hXo^I|9LNN<4,AT1o<ɓ'iϻE猐xbK.|?qFGG[9ЃNQ8gϞz-9Ǥߝ}pV7ۍ -[V߳gF{pp0ɫ0`08~Zlԛ&NE1#٭nk֬pw] l޼EQt䠠),?ZVV1lۇY #̭niiiͻߺu4m0"`08>j(?TmOiE޺!) 8Vߩ/_J8(%]tXdI~u֡ pe2 v4hcʔ)Λ%… ](Noa`_r{u˗166 XJvؿ?iǎ!i5M'x~\(oB?7W_}Uyu%ܵk*btj //O$ξ0 o&g͚uAKnh4Џu\e0 :NAtqzz),kYK5 C> ЃuL~s#8}ɓqT7"A8 A({]ѣz%ցr>c 0A,EǕC`$NЅu,:B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!y$\IENDB`paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_30.png000066400000000000000000000045321417573700700257420ustar00rootroot00000000000000PNG  IHDR;0sBIT|d pHYsrr3tEXtSoftwarewww.inkscape.org<IDATH]o_UvƟg}ޜ?lj_@@  %R5btF'ԋ~^zQQ5tT3 !L^H!889{=0)t]mi콈ol&OhY F DH:ÔΩ[ &ں˵W^y%g A6쑉7}kǏlݻ1 %tU2U4Kd\89N#/=7]X;wGb8S1:BG9c.WT@TW K@@fdd6}닓O PNm#/p=:~x`l:Dee2HƜBd GE"TuSGLd޻?3O?;_B?|~)D!eB, pϝ) A}g5ma1?<}p CP [GgߝwdzjM@~ۻ;LLDont]o7DKÏ zs[ W].~fצ'O/rt9 *1gW77>t_O)}AKKK>fggڴ->98-% 뒺d:՗m1A[>=/<=[ӓ[C}kT>gW'6nXku)_7Oj1C?n⍍i/N,?׺T/XK6qR`M4AR)@ lz]dksSӋ׮ann[pjrjIړ Pd$ɥGݾ}[c~󷧦v힝ڹk玱#w҅=4u8F CdpDwd2Cp @rv7566-ϯ@Wvŭ;5=M̞4: ه[DZ:%CL脒ɓ'}GCw&\[szKvsgJ٠aZ5h}M5zvi*E*TB%^Z|hlDgBH(>{Ω5AC-` $ >0K^~p%^4Ң\; ;vDBiSտHۦI [2l` u>[!6?!dVt)`CL`R$`D3L-+9\I ! Mc9hIYpG*_*įthEۮUe$:B0'"P(2$Rdf gRcgS#$<;R&O 匝n!Znny,_@Qh/.3Uנk{^4G%iZIENDB`paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_32.ico000066400000000000000000000102761417573700700257340ustar00rootroot00000000000000  ( @  ̦̭̪ҭ"Ю&ͮ)Ѯ,ɨ/Ϫ0Ь1̨2̨2ˬ1ʪ0ͬ.Ь+̬(Ϭ%ɪ!ѭӱƪ̙ϯΧέϬ%Ϊ*ͬ.ͪ3˪6Ϊ9ϭ;ͫ=έ>Ϊ?ʪ?έ>ͫ=˩;ͭ8ʨ5ˬ1̪-̬(̨#ʰӱ蹢 ž̪ͪ$ʬ+Ϫ0˪6ϫ:ʪ?ʫC̫FͪHϬJϭKͫLͫL̪K˩JͬG˪ḘAͫ=Ϊ9ά4ͬ.̬(˭"ЪȶŮɪ!̬(Ψ/ϭ5̪<̭AЫF̪K˫OͫR̫UͪWΫXάYˬYΫXͬVΪTͪQ˪NΫIέDΪ?Ϊ9ͪ3Ѯ,Ϭ%̪۶׮̨#Ϊ*̭2Ϊ9ϫ@ͩGͩMͫRͪWͫ[̬_άb̫dͪfͭfͪfͬeέcͫa˫^̪Z̫U̬PϬJέDΩ>̬7ή/̬(ǧ ̳ ̨#Ь+ά4̪<ΫCϬJͪQ˫Xͫgͬk̪oͬq̫sΫsΫsάrεñͪfͫaͫ[ϫU˫OͪHϫ@Ϊ9ˬ1̬(έЬ+ά4Ъ<˪EͩMέTͩ\άbƶ̪xͬ{̪~ͫͫҸʿ̪rͬlͭfͪ`άYͫRˬJͪBΪ9ˬ1̬(ͪ3̪<˪EͬMͩV˫^Ϭe̫mλ̫ȨîĻƳ̪xͬq̫j˪cͫ[άSϬJͪBɪ9ή/ʫ:ΫCͫLϫUΫ^ͫg̪o̫w¯Ϋͬ{̫tͪl̫dͫ[ͫRΫI˫@Ъ6˫@˩JάSΪ]ͪf̪o̪xε⺷ȶͫΫ}ͪuͪl˪c̪Z̬PЫFЮ<ϮE̩P̪ZέcΫm|ĸʨ̫̫}Ϋs̫jͪ`ͬVͫLͪBϬJ̫U̬_Ҷwøɷ̬ͪͫzͫpͪfͩ\ͪQͩGz¼ɧͪͫͪuͩkͪ`ϫU̪KʨRľʺ̫ͫͪyάnέc˩YΪNάSΫ^άiͼģ̪Ϋ|ͬqͪfͫ[̬PέTͪ`ͬkͫvΪðŸέΫ}άrͫgͬ\ͪQ̫Uͪ`ͬkͫvΪΫ̫ѻܾ̪~̫sͫgͬ\ͪQ˪T̬_̫jͪu̪̫ͫͫͪһżǻ̫}̪r˫gϫ[ϬPͫRΪ]̩hάrΫ}̫ͪͫͪͪͫϹ׺̬oϫd̪Z˫O˫O̪Zϫd̪oͩzͪͫͫͬͪΪ̪Ϋ̴ĻȾͪlͫaͪWͫL̪KͩVͪ`ϫjͪuͫέ̫ΫͫͫͫͪΫΪȱ¹ÿϴzέ]άSͪHͩGͪQͫ[ͬe̪oάx̫̫ͬΫ̫̫ͫͫͫͫͬһ¹ΫXΪNΩḒA̪K̫UΫ^̬hͬqͫzΫ̫ͬͪͪͫͪΫͪΫ̫ͫѹoͮHʪ?˭;˪EΪNͭWͭ`̪iͬq̫y̪άͫͬͫΫά̪̫̫ͫͫͫͶ½ͪBɪ9ά4ʩ>ͩGϫOΫXͪ`άhϬoͫv̫}ΫάͫͫͬάΫͬͫͬΫΪͬ{Žą2̪-˪6έ>ͬGϫOͪWΫ^ͬe˪lͬq̫wͬ{̫ͫΫ̫ͬͬΪ̪~ͫzͬuͫp̫jέcɳv½ʬ+Ϭ%ͬ.˪6έ>̫FͬMΪTϭZϭ`ͪfͬk̪oάrͪuͫvΫwΫwͫv̫tͬq̬n̫jͩe̬_ˬYͫR̪Ka쟗XΧϬ%̪-ά4̪<ΫCΫIϫO̫U̪Z̩_άbͪf̬hάi̫j̫j̪iͫgͬe˩b˫^άY˪TΪNͪH̭Aϫ:ͪ3Kvtt̙Ƴ̯#ʬ+̨2ͭ8έ>έDΫIΪNͫRͬVˬYͫ[ͬ\Ϊ]Ϊ]ͬ\ϭZΫX̫UͭQͬMͪHΫCͫ=̬7Ϫ0ͨ)Ѫ!Ѣ<<ͪB̫FΫIͫLΪNϫO̬P̬PϫO˪NϭK˫IϪḘAͫ=ͨ8̭2˨,ɨ&έΪ̙ζѭҭ"̬(̪-̨2˪6ʫ:ͫ=Ϊ?̭AͪBΫCΫCͪB̩Aʪ?̪<Ϊ9ϭ5ˬ1˨,˪'Ѫ!ЪҴ̻ȦѭѪ!ɨ&Ȫ*̪-ʪ0̭2ά4ϭ5˪6˪6Ϩ5ά4̨2ή/Ѯ,ͨ)Ϭ%ϯ ЪѮ꿪 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_32.png000066400000000000000000000051171417573700700257440ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYsҼtEXtSoftwarewww.inkscape.org< IDATX}ۏWuǿߵ05 6ݸ1M8[HU G?UC"5iR;;0`nqǀmo,bt#}^k/;|Xa./e MU,"2d0L]K)Q禄S)wY2]}mK?;v3y͉l^\M;,sXfb ҂Ir%^$)cλj E5PӘ%84j).`),)DH#A{ ђw IS]Li <5Y넸3$=91,KuHCπ`%@DIM¤B\fAYdُ![[ϓCaҩs2gM0HJ xRJu h*B$<&?~s{a}̘S`IpZAyr 3ApP)В.Im$e2 /ō~?DO (` 9 wf$FER%$ˑHu2 bǿSWS3m}D3+RDzS3RP CAX!1A ZdApΡ֤f꙱+?޽w?+/Z5d8~WO"2yA0g LR`)g)4c.@.#!$OK#k3]CCqnn޺{GGG#ȓKE,Xн4ԃ4U d"Ha @OK3׆Vq/>uP+_~x}{>?]n+bG )+,,^B(a,) 9!2Z6tCvܝ[}oz-fYF8G8sEvQ223 y//Q(HdkV_{ᩑ7U};5 lEҵȠg3c< Jf. sJ% Wn7^}f6k?8i}O/]q]"a 'G̡X8vGj(6mPSo\{Aq% @@D"္k о-`X*#/vov͚}~\=0vpmk.ŮL;n?Bx$oӟK{vn׬Y=syT9d%(`LH]3-̎m}eٟE/.\&oᱍk͛7c``{}Otv-T+jj//w1|䅯]t~Rf횑~𣉓 }9e({+_Q+Z@ UAZ ߶u{?~;Sӷkpw8;>dܽ{7G 󉳓atb l!S `!Ab_5Cĕ/G3߼8r%<@J %P B Y xA&`5ɊTE<~˕~'#?M܅}>hGO~rܰ%8,SVѼ"YɬF5 `R}yt?'Wo29~u}:+FC}ys6ܡsB>ס:VAmdt$PX04 L&( 2*ÑE&Ƒ]1o}rС7?'?f)V"m;DQޑ DʌBdA 7 ̠g@ `P{UScq޽~dx8sRb'[+Rhev@W@d鿰! gOX|wm6I}nٲ[tsW}[1{.#R.Y+3VVXQX TJ %) 泐KLO]zQgݣݻz=䩟]ڲi;(` Mݻ @(x%K V@,^!P _R_kI9A8@/y{bjX6-v?O=ܹ~`kVsƑjxju,EFQP0QU\P?>  Y}ŝv5tnosމo ,]$|+_;>rA l 1ѳ  Rb z%CЇc}ñEUo7k5^ݳZ=iw˖6919h4:9S1B 0OfF 0yҌ:4T>>qӲ![*& /͛9:&,]P`rrk7 { `aT==fTvfN||djXж-7tGWW޼d&&&0::0NػcdCq dL!(Z"jYb÷Ǒig~yMw zaŊ {ԑ%aVZ_zVB3,ZPȂ$qtņ7^c'_z=7xreީ^h%4&3Pj@421hɹ ]8qc[sN ]^}?Au@{px"s$$բG !4!:e7Ckr'i0s)y\x׬Y~pϖUgC6oX75` [.C+_4#&K,`py`57m!Y]eyOir UOUeG],Z6oްnzlhyzЪZMEt!reVSmօ ˎl&H%=ŽWo,]xh4+o;~Evv]j<| X"\sf:˂5@eY-,Κ]V@J2*ʜsX\9v3бNg0 `2dAY$ܙV],+yTmkf5升PX\'?:snrblQI`=''K#- =Y',̂'eM'ՙ|~rt 0a~aѓ 2܍=$Qp78\T ɳAm| ԟ0^zg̟gv e$,10r(b;͑ fQr0)PISZCnQM嶌\%on( O=ZɈEXdYab"T3"0SV[2CA2͏cR!{p'1|Ags6`FFXH`S$XVȮȮփRNU*?%//+2*NJvLY2d daFXBuݽ<F2_}k-cGI)ơ8TP9ѢѼH4}J+Y{]a""{VKoSs=LIsbJT'C M& e"B ђKpHd<0I\^ܦ[ SuME楶^01u7/!$cs5*'PXDcQJ`0  UoQ5UVUg,*n^<{ H9 N2._m /k<6499B( 1kEÜ ߱BB `0U8*C1=gx5O.~u˂ $>wb$0K-KI#HyHH0t&` :h XQP6L(Wx6xb~lw~t,4քd)09<1Ĩ≁U#c0@IfG%**ZQ A2B=6'?GG_<"k,4,ЛRkcd`ODު| C BъP=~`oz^ y?ĜK M)j!fՓ`AHh;&r̬T `BT,֥#h1[G ` h k٣[d\C 9o>> /`aɾK7qU}ɇ.<6mkfW_ ###֭[G>pD?ŘP؀% DXzcCy#0q`] W@f$`δC*AīO7cؖ_===0s||lox.Kmae[~6.؀lj`HK, IpYr+h7 M;WoL8qdo?<}nU+ӥKʡc羻b Kd"<{^zzF& "JdإS@t]v\'ٳgΜ9L+j41:IQU dP] `@r1Z!E&::܉7#kWݼz}ͻxlǏD~==n=B0RBbUQDc< L R՘YryCX!cY8ܺx#5&'zxⶱUn 3vĒEtFFY,YT|шwhκ=M竈ɷf[9bG»SON[;>_i|۔na4]]" )vV =L>6k[鏎^d }W5Vشi?ۓ'SiB9LF<i23tG< p%"rĜ0t˝7.Y۵=O5p3/B* 4#,0y53v 0T1)8AP;2w~sedm ( ؏W.[\8իWpޯ^ܼAPJ0{5YdGy1d݁` M.=4:7=/Z}˖{}\=>ÿ]qBk遼$DW9 "I8Į0V}Յ w|rѹso}B[7osŢEs+].3Ww. j%@3'_;pM#u_a`߁pV._.\{GVm4R. |]Œ2$:7?uߺ̙;erx(8FFFG?x o{I@Q7k"WItlV^ Rp+_|#6ww0tތ. WGs)́y.f?`;}p)d@D+, ;2҅>dfCm;co]o|nPAJ+q#;֎-n^kp@RIe5KJmZ3*ζI֜IXp)Vzl[gph[:k-nI)sșţ4@*nUYTk-m oؖv2sS~c:ПAFӝ4ꪱON'al/XϞ6|Cb lPfWeTh!o4[>e\CXj@@$늾ퟷbͺ 4>;$Hj'/֜P6HD'U1$[v۟=گqx>-4`i"X&U1TH:^͓ 3H~ WKCQUIrW5Rψ-Y)RmCi g~Hhs'&ϖ `1kD @WϑB&Qip8 VU*Ŷlgz%O/7۶\:=0fv(5vҸbtb f0dbDlIтR;iBZ W ]ѩbT HM=_=wC'fĥ##=Nu*$k-1rD`(1 "".IvJ-%у[׫͆uEFC,N^ "풉%mc?z/}VNA!"2rD n`4΢$JȀ"}}CơT2Y5%5x)9Or܉~= F:;DhFBʠ!dh6hr}C,(brnT,^TNFZ 5͘w3?c\VIENDB`paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_48.ico000066400000000000000000000226761417573700700257520ustar00rootroot0000000000000000 %(0` $QQȤɮѮΧʧǧ ҭ"Ȭ%˪'ͨ)ʬ+ˮ,̪-ͬ.Ψ/ή/ʪ0ή/ή/ɨ/ͬ.̪-Ь+Ȫ*̬(ɨ&̯#ɪ!̪Ъʪ̳ҥ ߟɮȦƪ̪˭"Ϭ%̬(Ϊ*̪-ή/Ь1ͪ3ʨ5˪6̬7ͨ8ͭ8ͭ8ͭ8ͨ8̬7Ъ6ϭ5ά4̭2Ϫ0ͬ.˨,ͨ)Ш&̨#ǧ ѭ̣Ϊ꿪 Ѯα̪ҭ"ɨ&ͮ)̪-ʪ0ͪ3ϭ5ͨ8ʫ:̪<ͫ=ʪ?˫@ϫ@̩A̭A̭A̩A˫@Ϊ?Ω>̪<˩;ɪ9Ъ6ά4ˬ1ͬ.ʬ+Ѫ'ͪ$ϧ ѭʪɡ̪̭ʰ˥"ɨ&Ϊ*Ұ-ˬ1ʨ5ͨ8˩;ͫ=˫@ͪBέD̫FͬGͪHΫI˩J˩J˩JΫI˫IͪHЫF˪EΫC̭Aʪ?̪<Ϊ9˪6ͪ3ή/Ь+Ѫ'̯#έƪѮժϯƪέͪ$ͨ)̪-ˬ1ϭ5Ϊ9̪<˫@ΫC̫FͪHϬJͩMΪN̩PͪQͫRͫR˩SͫRͫRͭQϬP˫OͬMϭKΫIͩGέḒAΩ>ϫ:̬7ͪ3Ψ/Ϊ*Ю&˥"ʧʪժ̪ѭѪ!Ю&Ь+ʪ0ά4ͭ8Ъ<ϫ@έDͬG̪K˪NϬPάS̫UͪWΫX̪ZϭZͫ[ͫ[ͫ[ͫ[̪ZˬY˫XͬV˪TͭQ˫OͫLΫI̫FͪBέ>ϫ:˪6̨2̪-̬(̯#̪ʪ̙Ѣ ʧҭ"̬(̪-̨2Ъ6˭;ή?˩DͪHͫLϫO˩SͩV˩Yͫ[έ]Ϭ_ͫaάbΪc̫d̫d̫dέc˪c˩bͪ`Ϋ^ͬ\̪ZͪWΪTͪQͬMΫI̫FЭAͫ=ͭ8ά4Ψ/Ϊ*Ϭ%έʟ۶̨#̬(ͬ.ͪ3ͭ8ͫ=ͪBЫF̪KϫOάSͪWk˫^ͫaέcͪf̬hάiͬkͪlͬl̫m̫mͬlͬk̫j̪iīy iάb̬_ͬ\ΫX̫UͪQͩMͮHέDΪ?ϫ:ϭ5Ϫ0ʬ+Ϭ%ϧ ɮ̨#ͨ)ͬ.ά4Ϊ9έ>˩DͮHͬMͫRͬVϭZΫ^WURầ̪iͬk̬nͫpΪrΫsͪuͬu˫vͬuͪu̫tٺz𬡕̫jͫg̫dͪ`ͬ\ΫX˪TϫO̪K̫F̩A̪<Ъ6ˬ1Ь+ɨ&ϧ ̬(ͬ.ά4ʫ:Ϊ?˪E˩J˫O˪T˩Yέ]˩bͪf渶̫tͫv̫y˪{Ϋ|Ϋ}̪~̬~̬~޽ټy̪oͬk̩h̫dϬ_ͫ[ͬVͪQͫLͬGͪB̪<̬7ˬ1Ь+Ϭ%̪-ͪ3Ϊ9Ϊ?˪EϬJ̬PϫUϭZϬ_̫d̪iΫmþ侾ά~Ϊ̫ͫΫ̪ٻ̫w̫s̪oͬkͭf˩bΪ]˫XͫRͬMͬGͪB̪<Ъ6Ϫ0Ϊ*̭2ͭ8έ>έDϬJ̬PͬVͫ[ͫaͪfͬkͫp̫t¾ͫͫ˩ҵͫzͫv̪rΫmάhΪc˫^ΫXάSͬMͬGЭAϭ;ϭ5Ψ/̬7ͫ=ΫC˩J̩PͩVͫ[ͫa˫gͪlͬqͫvͪ{¿ֽ̫Ϋ}̫y̫t̪oάi̫dΫ^ΫXͫRͫLЫFϫ@ϫ:ɬ4ϭ;ͪBͪHέN̫Uͫ[ͫa˫gͬlΪrΫw̫}γγͬͫͫzͪu̪oάi̫d˫^˫XͭQ̪K˪Eέ>ͨ8Ϊ?̫FͫLΩSάYϬ_ͪfͪl̪r̪xΫ}̫ͫͫͫzͪu̪o̪i˪cͬ\ͬVϫOΫIͮB̪<ΫC˩JϬPͪWέ]̫d̫jͬqΫwƭȭ̫ͫͫͫz̫tΫmͫgͫa̪Z˪TͬM̫Fή?̫FͬM˪Tͫ[ͫaάh̪o޽{ΫͫͫͫάxΪrͬkͬeΫ^ͭWϬP˩JΫCΫI̬PͪWΫ^ͬeŭ|Ʈ̫̩ͪͪ}ͫv̬oάh˩bͫ[˪TͩM̫Fwmͫͫάͫϫy̫s˪lͩe˫^ͪWϫOͮHiΫͬΫΫ|ͬuάnͫgͪ`ˬYͫR̪K̬PͪWwͫάΫ̪ͫxͬqάiάbͫ[˪TͫLͪQ˩Yͪ`ͫg̫ͫΪͫzάrͬk̫dͬ\̫UΪNͫR̪ZͫaάhͫpΫwʹΫͪΫͪ{̫tͬlͬe˫^ͬV˫O˩S̪Zͫa̪iͫp̪xͫΫϴ̫̫ͬ|̫tΫmϬeΫ^ͪWϫOάS̪Z˩b̪iͫp̪xַ̪̫̫ͫͫͫΫ|ϫtΫmͪfΫ^ͪWϫOͫR̪Zͫa̪iͫpΫwͫΫάͫάֹ̫ͬͬ{̫t̫mͬe˫^ͪWϫOͫRάYͭ`̬h̪oͫv̪~ͫΫά̫ͬͫΫŦͫzΫsͪlϫdΪ]ͩVέNϬP˫X̬_ͭfΫmͪuΫ|̫̫̫̪ͬͪͫͪάƶ̫y̪r̫jΪcͬ\έTͬM˫OͬVΪ]ϫd˪l̫sͫzΪ̫̪̫ͫͫΫͫͫͫͫ£̾¾࿢|ͫpάhͫa̪ZάSͫLͩM˪Tͫ[άb̪iͫpΫw̪~̫ͫΫ̫̫̪ͬͬͬͪͫͪã;Ϋmͪf̬_ΫXͪQ˩JϬJͪQΫXϬ_ͪfΫm̫tͪ{ά̫ͫͫΫΫͫͫͪͫͫͪͫͪäɻٻqέcͬ\ϫUΪNͬGͬGΪN̫Uͬ\˪cάiͫp̫wΫ}ͪͪͬά̪ΫͫͪΫ̫ͫͪͪͫΫƤͪ`άYͫRϭKέDέD̪KͫRΫX̬_ͪfͪlάr̫yͫͫͫͬάΫΫ̪̫̪ͫͫͫͫͪΫͫǥ¾ȮkͩV˫OͪH̭Aϫ@ͬGΪNέTͫ[ͫaͫg̬n̫tͫz̫ͫͫͪͫͬͫΫάͬΫͪͫͫͫͫΫͬƤɾͫR̪KέDΩ>ͫ=ΫC˩J̬PͬV˪]˪c̪iάn̫tͫzͫͬͫͫΫά̫̫̪ͫͫͬάάͫͫΫ̪ͪȦ𾧏bͬG̩Aϫ:ͭ8ʪ?ϪEϭKͭQͭWέ]έc̪iάn̫t̫y̪~Ϋ̪̫ͫͫͫάͬΫ̬άά̪ͬͪά̫̬ͫɨ쪩ΫC̪<˪6ά4ϫ:ϫ@ЫFͫLͫRΫX˫^ΪcάhΫmάrΫwͬ{ͫΫ̪̪ͬͫΫΫ̫̫̫̫̪ͬͫͫͬͬͫͫ~ɨ{򮚉[ͨ8Ь1ή/ϭ5ϭ;̭AͬGͩMͫR˫XΪ]άbͫgͬkͫp̫tΪx̫|̫̪ͫͬΫ̫̫ͪͫͫͬͫΫͪΪ̪~ͫz̫w̫sάnȧkͪ3̪-Ϊ*Ϫ0˪6̪<̭AͩGͫLͭQͬVͫ[ͪ`ϫd̪i̫mͫp̫tΫwͫzΫ|̬~ͫά̫ΫΫ̫̪ͫΫ}ͬ{̫yͫv̫s̪oͬkͫg˪cΫ^Ǩ[~Y̬(Ϭ%ʬ+Ϫ0˪6ϭ;̩A̫F̪K̬P̫UάYέ]ͫaͬe̪iͪl̪o̪r̫tͫvΫwάx̫yϫyϫy̫y̪x̫wͪu̫sͬq̬nͬkͫg̫dͪ`ͬ\ͭWάSΪN˩J̨#έϬ%ʬ+Ϫ0ϭ5ϫ:˫@έDΫI˪NͫRͬV̪Z˫^ͫa̫dͫgάiͬkΫm̪oͫpͫpͬqͬqͫpϬoάn̫mͬkάhͪf˪cͪ`ͬ\˩Y̫UϬPͫLͬGΫCΩ>Ϊ9uƗ{Q̦έȬ%Ϊ*ή/ά4Ϊ9Ω>ͪBͩG̪K˫OͫRͬVάYͬ\̩_ͫaΪcͩeͪfͫg̬hάhάhͫgͫgͪf̫dάbͪ`˫^ͫ[ΫX̫UͪQͬMΫI˪E̩A̪<̬7̭2̪-̬(}jccb___ӱ̪ͪ$ͨ)Ҫ-̭2̬7ϭ;Ϊ?ΫCͬG̪KΪNͪQ˪TͬV˩YϭZͬ\έ]Ϋ^̬_Ϭ_̬_̬_Ϋ^Ϊ]ͩ\̪ZΫXϫUάS̬PͩMΫI̫FͪBΩ>ʫ:ϭ5ˬ1˨,˪'˭"ʧر 666Ȧʰ˭"˪'Ь+ʪ0ά4ͨ8̪<ή?ΫC̫FΫIͫLΪN̬PͫRάS̫UͩVͬVЬVͬVͬVϫUέTάSͭQϫOͬM̪KͪH˪EͪBέ>ϫ:̬7ѭ2ͬ.Ϊ*Ϭ%ϯ Ъݻ̙ѮЪϧ ͪ$Ҭ(̪-Ϫ0ά4ͨ8˭;έ>̩AΫC̫FͪHΫI̪KͫLͬMͬM˪N˪NͬMͩMͫLϬJΫIͬG˪EͮB˫@ͫ=ʫ:Я6ͪ3ή/Ь+˪'̨#̪Χϯɮ̭ʧѪ!Ϭ%ͨ)Ѯ,ʪ0ͪ3˪6Ϊ9˭;ͫ=Ϊ?̩AͪBΫCέD˪E˪E˪E˪EέDΫCͪBϫ@ʪ?ͫ=ϫ:ͨ8ϭ5̨2Ψ/Ь+̬(ͪ$ϧ ȭӱȶϯѮΧ̪Ѫ!Ȭ%̬(Ь+ͬ.Ϫ0ͪ3ʨ5̬7ͭ8ʫ:˩;ϭ;̪<̪<̪<̪<ϭ;ϫ:Ϊ9ͨ8˪6ά4̭2ʪ0̪-Ϊ*˪'ͪ$ϯ ѭ̣̳̳  ժѮΧʧϧ ̨#ɨ&̬(Ϊ*Ѯ,ͬ.ʪ0ˬ1̭2ͪ3ͪ3ͯ3ͯ3ͪ3ͪ3̨2ˬ1ή/ͬ.˨,Ȫ*Ѫ'Ϭ%˭"έȭժΪ߯ժ??paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_48.png000066400000000000000000000114751417573700700257570ustar00rootroot00000000000000PNG  IHDR00WsBIT|d pHYsQQ dtEXtSoftwarewww.inkscape.org<IDAThZY]u]kwoЬ'4"  % rvbJpRI%6vBR*NRq%Np., OALI h@qe쮺{sڽ{q&A}Xԉb+j1͍K64#H rݕ%Yprj>,:]QM3KcǶ^ݽVSH+OOuZb,x7"Z@d@epҢe#s1FZ~!D9 reG ëa0UT0dX3_0ؾ]tNC;Ds9b;N pBDF$hkB!hreAzdڐ J:S9viI?T, [ݒLE5&g@`(`F Hy5$@ ½̤ тKUTpwy4#PUc+\U#UܼxNZ-}cyÌ3@C??:{:ʐ*r[1C#Bi氆)B37 2ADt C8CaVU8U1xlS}h9%68rXC$$U#!`=z#@:@:N$IRpj1xb(/bV锂 5Uf߿鋼w١Aus[!EXr09j,QBMTC@xU $~Ii#_> {U\"7̼`[>;ۤj7?g?=pկo}k?GFWIBe00 L@L.O4F'd@ϥ PF  ;0pN%wRTVwYt[/=|ҡiΪѱ/}itowbD6LLj&w J"1" h A8JZq w}bEƋ/ue!;rzf|S<W_=s& ?M):n< b59Y)%y9`KD(Q%`-Sw%O<ߺSnƟgO:F8ois6op{|s3M\|.|';OyndjY`@`9Z "H!ʑ&!#0Ij'εW-l`hh]uOL?~VNYx1.\xQϼs|T!zPhN4cLA2[ P36ҟO6eC8>|:Z9rJ ]{ox B4&di$CTaDBARGGNOٻ+y%g<_:_nq 6 >ѣsV h$3AJb/]l_1-$HXH<}@9oƵW.Z{m \ ^<;Z5ov[(o:a$) '#Ir'HSohO+Xc} FFӟxܷol +YhB7}mnxKOqp;HpIT{<󯘑>[֥KgGogakXh͛w8Y=s U**9f.kZsW5[~Ç Sf͚_aXs ?uPHf E"z1gИ#B7wm'~ sF+u_/h;~3r q{yѷs\}PbǁQ;֮;rud [eLN#e8}Poh#ܵkqG;44Kptqum۶ah/ya>L[zB "+Zh UQB6juT* 3Ǧm/3_y߆u+/O;v-[pRkwBBȀ2,,V*kOዋ *a/B8{ɋ۷p4;J_=wvZ /M6r_юg.9]`WP,C!FtIE*1PREܫ4i}xwg:|0=7JW_orѤ kƮ3`YbURQPqDU/h, 4C^Hf/Xzl\k0ϟ|cg;sf \/-"?kﳳWr\Tg|0օ3H$F MQ 8ze|#K-_溣ھtጸ+,~ϟS> Bc60;)ˀu@2`9'ϱ"fhVlТ5$d0 t JczWO=7_y!:,ҵ_姏oܰjѬ߿#{ ZuېbT,n>t(ʈj<_"k(NDB6^;.7۞7v?r۔)7ǿ`uVJ.Xc蟵ѸGuT:-O :q"-7eh]7 6a5d1xfefL>/NwVf r9EFTB0:mٺ,Xn1VJMPH4˩3寽y;[sʏg/^ Z׈A]x24b2#n\3ձEgtrX *4Q$_xfhF`FZSʊH 3]|zʔ8<缼dEPIYTP&CI&{F nWL4 GM[1s֔a ^qEځ>T AƺGn*HFl(1 f FYDs,U G]Wζ#[/_41l;:Ņ6w IENDB`paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_50.png000066400000000000000000000121501417573700700257370ustar00rootroot00000000000000PNG  IHDR22?sBIT|d pHYsii]tEXtSoftwarewww.inkscape.org<IDAThz[]řv$I$q6X؀͌+SyH*䍷Srq&ql p1@E $GGG{Gwpj{^G}gr\&LG#}%E;ژ$ʆ@~Ϙ$sDH(5^t=W*SQC#eP~n~|1}xr|h2,y:M0!a h6FfV$hIX$(Y0W0 TU*tPU/{X 4de$Xv65dEՍ0M#=̠ZsH( 򠕈P Pa^QU=ȼkT*IM4Q T{%iyS@-,)(@MnapV+в0Y `*$@SRP^AU*8r3LSn\1/O_[׬F<_:)Tr4 @`pDLa CVA0**=D 9UCjˈAE(Ւ5QUD7QT Eݷ;{۷wI>`9:C9ɎNkLg9`"##"ArL0 FV۰B0!#PeBȭ2TC 3+`+p1Ze 9]&|zt}Ӳ9+.,Ʋ0'Z0GDE"L.3`L$J̎*6cEPjD UUd%nrKfp{.c}u%o}0Xzw+P2hev3fzJ)&"hCrc2$tT$!bUJJFFFf;s1~MW\˗K.d3[\|iJˀe3`1\lgȒIbJ"#:!#MHP+ZeA¤ zQ T+꯽e)Ӛ/xٗ._lxuXjUw_#DNe"gܲVv@ AeF \i"+-+$e !ZE$ @GӋgj/yѹXbv|m wֿoO9|«oݾfe&(: I Z5:)#0ː`LL* 0P.*Ɓl  YBQ)-Q_WziMSoٲn̛7yl&Pbb"je@ǘN3 G6CVD6cR KJF8H!8)#Ip(MA2(ȊL扳+riDco7vdwoM6޽ޚذz`L5b2,Jqe'%eC0Ђp&3RDZ 9/]>::NPJ?mlƴΜ믿9{M/*&-~i0#Pd?ƌQvP0KFsr,) b!śnY49g:h}ml?׏&Vܜkx}ɷ&&$բ Ψ F`31HO s@YblfL-r$]/.=o袅/j߿g_n}?s$Z :0HtZ]H&IT2C EO:N3)ShEJhޗ=K^ sG-?`Oٳ{=\p~G޵9D$cQJ2cT2- nppRIB2"DCC62 lO!;`sIs?'P;+W=ȓ[ZQP@dՆN^%wsCrT$.D2D!d`C" < յ̞z>~|伳Wp͚5osƏN;p3*p͂4jA3*I#ȁ3ֶh.(H2 '˓ ??n՝K.\7mΜ9XtGݸG8(ni`d d8 Nzb֬ށ)#Q)dbz f7]qYf}"x[뺵 -Z-Ⱦz`F܌8Z ko(D< #h o̢ީgO)O~oz5X;R|pR+^yj<_\ŅCC 퍽pǯ7mߴiO޽W=o5_Cq+؇؇c  ˢgA1֩`UdE/s?m'`|x̅ #<wf٠`Q(46P/6]Z`F:O^ݾxVT{snM3<͛7jEQ_޵}h%!j4&6`P!C˽gڕ+TRkH5 4EW?-7蓮Æ 0<P TIoD#R%h Ā:p6 dT`ǎ[<ђ9ËםztOԬ?uF Th"V51`UF%J 4sB(m~FmL*[~7޻d헟&aup{_n{9ZL0/wGQV? j cCnM Me^oOb8SvdH0UAc{^:L:eɩ~{ګ'kAYf]nw}6=fx=w?I[&ƠFq\fM@Mܯ(&rG!ZZ 6¬d*r QK;^?='HNco3T!dt "2cr*dT=t$ XimAVԴ<;RbCqtv'u& Ơ7G;=գ|bñ8UF+ըHm*#N,0yQ[ X _MZJṯ[Wr. =oJC3סdkK8zm-J kd-U9UPP[ Z P6 B&D ,#Y*ҫr SZ:;=C'V } ;™t&{Gm|*M,6ASzr%BQme^S7TZ-ՇJir{b>|73hB%Do봺0XTZ)%iʊ~_|ª֩Hhv[&KלIz?2)?洡.fSg<;VТHA>oPG#/mCWw+U8zHgϜ=n?H͌8IENDB`paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_512.png000066400000000000000000000566141417573700700260370ustar00rootroot00000000000000PNG  IHDRxsBIT|d pHYs)itEXtSoftwarewww.inkscape.org< IDATxy|T?WB6Bʚ@XJP55ZcW~?V}~>u\j;.1@] M@Y ̽Yfr&jf93sy{QQ @& 4J$x @#?n"l8_*kO}???UN"""r w W믿^Wȑ#M]s;"< &Iz- уm&VUzZMDDD]' 𙙙bZIx≳M&HZZߟPr"""p&+gggKQQ8)++K/Taaad),,!>uT8ʖ-[D_Jtt,۶m'NHTT9 ?]D <[ b0ȑ#zw8RXX>_&L eeek mI"""2p&d2ImmWUU%]wY͝;WN>fϞ= \Sy%W W0OOO,r2x೒FvU}0"""q,qľs91`Yp^|r5M)DDD;ob_qq^Κ򏉉u6M222י}L"""0&K+((}dٱcG7N>|׿*DDD}Y8} qqqb2_DjJhhYkϚ5KZ>5Qgb_ \?--Mf455]4777O`0(ٯ7k9@DDD= Kp&}/~/"w AAA;pk ""CX 3zb߆ :5| 0)hYfE{M7ii}|)--Tlb4߯_?}=f-9 ADDDn .pX1LRUUթ/"RQQ!t:9yE&YYY[!""Qp&USRRl6ogYFZr̙oj!""98'/++KVkΥm]W/rihhDm𠺯wdXv}+WRTWWˬY/_ީz)=}SDDDط$ݻ[_Dd۶mOk?55嗗+#""꽴ľTT 9zhbѧ?-.oZ_k#""b+'''lF)w}27mڤa#""eRL; W`8qXVln ""%%%2vX}am>???1L]*plu_!Q1 >\8''G5-0=`ԨQ.K.rO9ADDDgbJ[[???_vƐ"A>| :TСC믿r2bǕ}DDD^,ľ=pH1 R^^Ɛ++++B1cDFF +*ׂ8%c* #F-U˗/h w뭷vh 9q>pȻ$(p ?~x'nd҃%//d?z>-Q y h׳EEEn RYY)^{~ƌrUW wy-ܹS[6  |%//Onꖠ{1֭kWVUy/<<\>s1cʟju{2h mp>\[͛`;Y~*Ѳ}v=zYvG~Gz7!O ` \6<<\ [:kOW+dر{R-砈gh}G b2#.]* ЧWX!o˩SpW$Q]]-_>UWn{O/$''ǣ[.`'~~@""Έ3*fYz$v֭[{81Lz`AA9sGs! *:( K YYYbZ=)E􍄶l"_BٯG4o<-$""]b߆ z4pvF}}z Y=*^z> XlYkƍڬ#DDD%m+K~~~$uGIIddd876lٳgOpɓAYOKDDcP W(ǎр .0 !!!_|o3e9qℒZDDD™WWOII==E >埞.@ϟd¦&>|>H gbiI:\G/\E9(dRւ)")`I[r ˖- ?PDDdڴiD6?~\"""@ߜ(-o=\WAAA qed2SCGȷ~+#F2dٶmҶ}Z_'"HK W JcWTVVJNN~?77WDD_DFF ~P;vh6DDԧDx QeRRfillTjڵ/cXf}F //+g̘ T^WwQ_ gb_#\ĉbZf]p8l6K``AIiil6կ~'F}F@>H  DD&g+HQQX-uuu2g}?;;[Z[[EDFj *nSkkkmC|pNbݻUn۾}埑! - p 7H]]?"".KP\qbZ{lo>7ncg-k0"ٯ{jlU ~.$"q8[}'}osk\eCzp&9H``ɖ-[T8lb4+$~K+i_~PvQ%+FDD`2#eʔ)z?Y_~ 9r۷OQK/nرc#z8 b2VulիWK||5ñhѢ[oIPP뮻NN:ki$""I p?n8X,J)C e{СCgd2^a}}>p_&}g}窪3g}ƌge744M7$$ @^y'xB<;q,g%KnnlڴIu [n$}#|+**dҤI@(_~v%88X(< `3\Wb0êWX,(ٱcY/.. w^E-[nE KbDDML;W$555VySYYY?ox%Nz#lÆ 'Z+;҈+ÙWWKKK,MMMcVڻw3F_駟>Z+kV5lkk.LKfK[x-W,X aaa@BCCe͚5gYnv=PMCւ heG)sľ7QJ477^ѣIѣGp)**RڮijjaÆiu4-@,RZZ:>)sa=5;wÇ :tl߾]AK뿴_ PQHDD=& @}pX1LR]]:.)UTT$7裏~?PBCC\yrq-힊 YE"" qp&USSSl6ӧU$l6L&}UCʱc~:٬n ZRHDDgb_=fZnEʝ8qBf̘ǖ뮻&׮v}vmc0NQIDD3b8;z=ocꫯ$11Q7~j>}0YdϴiӴk M""r3-oI+))Q{,@bbbΛk.}֭[}/^z87q""^.]pftAh4ѣGUrԩKJvvy$**JȄ LAkݧUҴ#"1Zrr2~_-)))-^xἯX, dܹ>]jF""RL; W4iXVs^%ժ!7okl6<#zh+++ sDDUY'rJ1k555ɽޫOgddHcc^WSS#\s7`|P);zSľp ϗ={-^Tƍ_?}w}'Gs'֭[-={h3.SwQGi}{ b0Bu\zK,ѧCBB>;V\7n89|pԳfΜ][DDQp$r)l61מf 3TDD&™gCg{ZJ;sKnnyz=_ւ@`I۵kѫ8),,7IHH1cիWpk{ɓ'%&&F;̉HgbnDD )//W7z*3fT .+q;S$DDH$}p1LrIWڲe1BW^׾"k ;䉈 +gffj=1E 0@pdg |{馛gb[,iii9kog^znm[fݴHSuESLs'[lQz={%\ok+**$++KϱX|yT63s`+I;rV R۷oC III3%[Zs1)""8 b2Vu< RPPpF>?ŋl9qDVILLԾDD>.3j Lb6Yu,}?^Gמ+=Zz)- "U,>Odּ+V׶ȝwީL&S5 f;=|Y}+6mRm#??? #F4cd@eҥ=Z0|-Px lj?<<\ d|TQQ!_pfo2d|=Zi&mdl!"p&+ʼndZf$$$,X_bDFF +~衖z!SNL(;czx8j iiib6Iu_ﳴ~OQa۶m?g@BBBw顖z^ ;z-`aM|d|}dȑ@'Nܬt""~!+&IT}FCCCG$++ '}zǏ"rkeg3"fYN>SJKKeر}b r466@KΝ;6Y,""/ gb_]mZV&);Hxx˪U.z&>>S`0ZݮZ{yKED䅲,`Gľ׫1 z 5jE7I3fov}'w QYFD%ľ 8'o޽>L }{{8 \rAڵk{Ù3gDHO;݈ 3%A=Ӗ/_.@?{Kllc"/*;눈`p ,fIbv1L*}C 7ސ@ 3gΔSN@k{Z4h6]ݩGDF } 'N*6MuUVVOPٳg_4qnhSPP=\K\tMnKff6xAݩIDIp&5'L>/fZ%44THxxlڴq8b2O< gΜ^E ' hrQQQ577߄F222C6ׯ@k{zILLԾDDn42H``4 IDATˮ]Ttק7MwQKDDDɍ~;]GKW eeeZK񆄄ȧ~ڡرC /ħ7:LZu-Qi}p1LrI,]fѨ߷OJJ'Nt|ĦLȼy_%"D8 W"---Wꀊ ~p8RXX/<|inn7j.1N`"gb0Zz$$$9-ZZZ$??_0Lmq82emeVwu\ټy>:Azׯ~zoUUL6MHXX|nYhk Rw:]X <[ GQݗR'UWW_OϘ1Cv{JRR߶mp>5CD5L쫅+𧧧lfW/fX$((HȀdǎ~loӧ=RuqDDf$L rmSYYYl6yd?(mmmna&/JJJ$##CO=T[SS#W_}`Yp[vء=2`88 7ue&o>}%E$,,LHhhYݿ5JHbb"puu=ep&+ʼndj}$AKK }ԨQf/З̔Ç{}Ò%KߣųzL}5p41Ԥ$79r\~۩[, 2k,PKVIOOWQ%Mhطx?Mòe$::ZHPP|G~nѨ &jkPUS,`Gľ 6vL&}MÇ˱c:z?<>FUa@D>NKו\ppKII꾐<}r̙3SW1c4hPy)৪c " .pX1eڵotׯ8 #GnVRRS8d™W WOII̕|٬';U[o xuɓ'=ھk֬Y"e=T8NnV}>N̙OO>]Μ9;1L fy}ӗ_~}MF('ȇdXsV\}vIMM:馛/졖mv]ƍ ^NK[U[PP޽{UuCV_Hddd᭨I&_bZJTsQ%+GEE`G㨇477}ݧOgffJccc(.. 8zP}}k=z"bONN鎟z}S~~~egN*h)i|I-s袒qNbĉj2I%**JHeժUzBr}IkkZK""`m+!b}6rrrHuF lMMMN,wq>@aaZK{:cWpNbݻUcHyyL2EwqG8v\ve@O>@K\6lf[ZTֳ 3o\|dd )//W݇BW֓eŝ.oÇ :tl߾-s.{!"o 1pD1L\k$$$r># rWX:~[ 52DU8W?~XNFJfΜO_]ڂl6[oUۥr>3} &p۵ ;q!"I p?n8X,ܬ_"/pB}844T֭[ץr,3g>}-6mvWe=L8΀}---b0) t96My=h4vir>@M "%9͕͛7ȋ9rD&O|K5\#$88XV[J*iiiIyH <[z  V+**K?w.ߧ7$00P_ԩSnn-uŃ>pnEDX:4}Ek׮D ov~VAv[K]w^ ˔XDmM+..VP/p8l6Wqqqr.U__/g_Zv{6X[DEfVpp˾}T/-ܢ_O><:tH222DGGիZ+Vhs:0"pYʼndj} B_v 6H\\wRZZƖRwl6}p#Έ3>*%۷orYkɓnl)+ a:3"48 YYYbZPE]${>埙)]*pFV |<ɓpnED^j*pnΡ'_^u?B\ii7N'rY_BH~u<뷿ÙCDD^DKۈsJJJT,Y/*VrYG,}e˖N`m0]]GD sJ#FQ; `z&?55U\ގ;dذa@RRRdϞ=nl-7߬(,p&U1l֨6eeerW;Vy| M8qM-%OXf۷™SDD wNyҤIbZf/ȇZJ,/((H>.p8P{V7ܭMM<">???͕+W'ǜȑ#].EN=qd21o+D&-o%\WAAAUUUruSfF>ZyӦMӷZ򔆆}O) -o/\qdd P7ڲe1B,btoV/oȐ!m67<駟ւ^zB>$ c۠GY, 2p@ٵkW׿%@.rRr=QLu!Qߐ gb_#\„ L#<}N3zyt/sk e="Q0>\pvvػw\r%F>?|kmm[O3p8Z 6m???38C]HXs;JQVU nW]]-?ȢER)CN]$=Ap&9}}#`0OCCCܿ9RHbblٲM{iI1:J"_ gb_9\nbbL&nyJ=reS=Pe`;:͒ ~$ #92~xZz'|"7ZbE˴X,(nz7Txǐ 'E=fd$%%vyG[Z'Np gbJ^^2zRu{Z__/7x>o߯uDO <[{W_}% @d.c -%v)m.Uוp&d2Imms0m#W.rСn~z2vXI]{uDCgǍ'EUUWW 7ܠߛkܲo/Xi0XQJ2X4}䅾kINN7yW]nѨ( 48s挌5JmVֳy%77W6mڤ%Y, %;wv \!xWRfY BuDl ٿH ?k#w܊*//';~nh-yZ4hvܡ%"p&C$555Yرc|y[\\,@Ҥ-hf8&!L쫁+𧥥l6y *k֬qK_ȴiӤ-N8oqI^9}/ݮ<%63f[2Y\G͞=[;~W)tľ7>7~RYY\q~>[mnno]z-Yz[낉zW SppKii肖/_.@䣏>rKGw "KާM&M ^P0cccd2IuusvL&2|pRΝ;e@en)믿pnQNL쫆+𧦦lӧO>.22w\Jhh+R?r;Kbbv,=S&T88j2zuEx^um6nO@ޙC,`Gľ׫>:pl@}-e]wݥ'L&KޭLp>ȭľ 8'o޽;NsSӧOwۣxUUU2}t aaadK[oՎ"e=5 @ \eLLF9zv!iii~?o%))Ie֭n+[qq u]6Q08WONN,5.Zmddl۶me%d„ RVV液ɻ9֮mI34\ҤIbZ5)ZSfOO--ҎZTuD]5 $''P~qm6n{|z&1b6xLaN)ZbJ$??_٣"궥Kʀ߭[W_'.\meSj e9Qط)A***TODfh4HeedԨQzRuV6ǏHm0[]NtqQpNQU-y Yrwp[+Wgƍ'v[Իs=qZYNtIp&5)N0}s֬Y# ŋ|Ţ/4k,sk{رC[ *"2>\?;;}s߻|.FQU0 Lrrr e=<9Xsvڥ!rj5kgֽ͚(%77W?lꝖ,Yo UuD-o7\aDD .HB>k۶m,_~kʘ1cDGG˚5kZ>> ~'r5rBBL&9ysc, 0`|7n-Æ '$==]JKKZ>N2z~<$ڭnfZݶ 7jhhnMfwk'._ܭSIwE"L+Ӌ$555svz!ױi&}TٻwmժUZo0BYd gb_-\?==]f477>zԲe􄫠 YtXx_ϥq~v7NAYt 5@3G}nɤ-*ÇǏm@mOo_p>vMM+..V})QYY~UsrwL&['Q__pPAm#7]b_AA۷ONڵk%11QH@@nرc2ydmO>wn. 30\?..NL&TWW>Ήq8b6]noÇ :t|n|ǡC$88X(ԫWWOKK,MMMq"dΜ95\㑅wVX!@ ;wv\~*xP%8/^֝ʈz۷Kjjߟ'c6[oo7j F #L@%mذA1M5V]TTl߾uw}Vq8reiW0P/%mj?88XQ;rS&MFS]]-W]uyw^&ժ5U~P W+&ITD^e߾}_FԳk.INN([lH={DQVZț™W WOII,OV} y?X  j*gITTs v;A q| yÆ ïk\}cׯ` Cpp[MΜ9'|/2`(..ƀ^믿Gyvs z7=zFӧFR$ @ܰD`` 1p@DGG#66111E||<$ :C A` c޼yشiPPPonnǫ 0 x; ')/`X `搗g:8op igyia=|p.1d=cƌ1cp%`̘14hP>};Q]] 3g9y$j*_]wzȷر^z)Gvny43 ΅P?A+ 66=z4222p饗bҤI :ˉC@y^ն9 E``ƌ=ztϷ|Zqq1͛ _x7|?0l6zH]Μ9 8pGq% pn5\ף?IIIRPP /:OP/gX$((HȠAdϞ=nhԏce[?j!"sDIDAT%~pm?` $$DfΜ)fY߯|^)rUWyd#={U𫯾z館׌0WILLh7 HKK(;vP}~oeȑF>?*//' "BԷWɋ,\P{t7J||>8Hw^ |?/[&MҪ}Xss<11~xihhX}c`NNz.[fΜVE}-WjXo+˴idܬ<rA}Oqp8d2i<{VX&8&  p @[4yPQQ 8PzŊQ~_ׯz.{vdddhgTuDU{lBYJ}]iiiQ}l6y'+X}GK/THDD,[cuQꫯjg/ND4k O@BCCL2EN8oopîDcYӥHTtC^xׯx=Z_aa ?>K#<(;FyXI6p RSSl6ӧUt:[Gf̘o1L&Et7k:D(εs=}֭[%))IϼW'KAs='>Q}/#׿$22R~h}DߵcUT8,@ڷo7Nuf=///y q 2x`m+]Uh7#0h y祱Qus{= *k׮h}r= ȧAG|ui6oެ_a0VaEDgp fϞ-wVx5.' 6L;z- SjWQm;08^\7# bZ}v 苝DDDHqqt8b2ن.DJٳG@Ku?D &_{]Uyq!rK bʥ4B!*S4b-xAjԥ8fev2鄊,o JqJإrZpD Qh 3wr圳OV"w}~ozGFj̘1T@}}g?s#GFem:+6h &8߅?xrĒ8I3$U*`͜9K߱c5]o֬YQ9nEE1yQRR'j*/l Xn6-rJKKK3I֡C[fMT~z0IֿۺukT Hcc 4I0q#$Wرc2M.v7,ʱ_xKII1IgQ9.p2EEEN_Bap\N@ ^Dz㪪.}Fe=gt|,{n޽\aLt$OԱاoZ=Ld_q8`ӧO7I@S͚5i?](V)d[[QQ3233/ʱKwaNlʕQ9.T۷owנt@$io%Yjjy͞={lʔ)'FmG}d{6IvYgه~1ydwaUˋhM6Y>}W .ڱ/_.*tyYeeeԎ 4_?@LS!"؊-99$YnݢpQQQǛ$ꫭ>jZ2H8uIzJA w}7"Aή^^^T3fpg̝;Mᩧr'Uz y)@u%O>IJ g7|ccƌ1IֱcG[bEԎ 4W]]eff: @A@A 9Zff˭hK.N: ;047o}$ٳQ;6sq-=ڥI|U?i$hv khh3ghw@<7xúvjlذaV^^c-Q^^P4=W> 7߼F馛":T\\-0mڴ UW]4yIR/I(t_իW[nL%''kK;pB9crԍ{#Kd)))6w\;xܹsiv}w/d[xqԎ \_t84I N2dmܸ̪m„ n~~~T?38pIt_ctR绳[Rgp8K$)_AA$Ybb=Q k׮4d999G@k׻bJtX +++j|'-))K k_iKJ+ s:r9s|JҥS/vE'\%)33SIIIQ9`]]O_]~o=*{վ}$-I<.9Ĵ2MSI*?pE114hl5tP}w)##C}Q޽ݟ}$@8(++K4] p1aٲe:tz#K.JHH޽{efڳg$i߾}ڳgjkkU[[{Qyyvڥ]viÆ ,G.]4p@hQvv222"~ py<.p~$i]8444_~Ԓ%Kt׆sRڱc~jƍR[cv?׽{aÆcǎ++ڶO>DC QccIʕ2@kH <8l-\$Y^q0lڴijl ~r~]$tH ?6b;lɒ%}me]^8%5|eG gH]]]|N`tA II)II%w3<Ӯ 7o۶bϟgNK/ x% /hsV[[|+% kH4R,I11'!!Fivڨo|>q@k$ٳydzc;s]ޒTC IlԩOXiiiDx'KR3 }Jdce;묳^Q^$MR`> Əmnzu .ث#GZQQUVVڡe{=!IxWm rJ;$Y~1 ͛7/tԽO6@8?wmwׯyzebHS0HMM뎀 ;K1pJH 2Yclƌ_} : _IĦ.nBfwy[}ڳ믿޹k=5-J,Y6@} ---;Mqbk׮6sL+++kh>CgO$ډ%.|_|q͛gϞN7EӥHWIcO~+))1ߪ^3Ʃ^Lf'k֬q׬Y⠻n:ڵYo)-7B^s9ؼy"5s[|rS`Fh;$ـZj*KIIq ̇Gx ={6^4qlСM 6mr~ŋKKDT"C6i${w[tN5ϲzWl'ntgpȐ_*o}Z;3ybx xgܧK0nڴӝ('KtjѣmՑh{q ~vm$>|QM:toZΝ["gH(i6H^z饘|Ԓ9ˋ ʕ+ݧG}uŊRII^&YRL2ũ/{y 75Iֹs&pХ}W푓T(آEbbu9 c&6lpcy湿#i'p avv-Z|>_4>|=\>X3A嗛$KHH#wlO4I3$mW04h'@ I<&sJmֿ+v7R#%)|1CXtT?񏝿VD/ 6I>Uߩ[%%zxH***NswZ^^PwK:R-JtBlaعs~N}˓XuDXXveCuWxY`yWؒ%K–\wuN}}Xv$9rݻ׺vj_~̾;TROKSI|It &1,>66r)))ΟWHa <+ Ϊ?˫@̩AЭAͪBͪBͪBͪBͪBЭA̭Aϫ@Ϊ?έ>ͫ=̪<ϫ:Ϊ9̬7ϭ5ͪ3ˬ1ɨ/Ѯ,Ȫ*˪'ͪ$Ѫ!ժЪժΪȶѢ ѮΧʧϯ ̯#Ю&ͮ)ˮ,Ψ/Ь1ά4Ъ6ɪ9˩;ͫ=ʪ?ϫ@ͪBΫCέD̫FͩGͬGͪHͮH˫I˫I˫IͮHͪHͬGͩG̫F˪E˩DͪB̩AΪ?ͫ=ϭ;Ϊ9̬7ʨ5̭2ή/̪-Ϊ*˪'ͪ$ɪ!̪αȦ߯Ѣ ʪЪέ˭"Ϭ%ͨ)˨,Ψ/̨2ʨ5̬7ʫ:̪<ʪ?̩AΫC˪EͩGͪH˩J̪KͫLͬMΪNέN˫OϫOϫOϫO˫O˫OΪNͬMͫL̪KˬJͮHͬGϪEΫC̭AΪ?ͫ=ϫ:ͨ8ϭ5ѭ2ʪ0̪-ͮ)Ю&̨#ǧ ѭ̣ժ̳ ̭ʧϯ ͪ$Ѫ'ʬ+ͬ.Ь1ά4̬7ϫ:ͫ=˫@ͪB˪EͩGΫI̪KͩMέN̬PͪQͫRάSέT̫UϫUͩVͬVͬVͩV̫UέT˪T˩SͫRϬP˫OͬMϭKΫIͬGϪEΫCϫ@Ω>˩;ͭ8ϭ5̭2Ψ/˨,̬(Ȭ%Ѫ!ӰΧժ߿Χ̪Ѫ!Ϭ%ͨ)Ѯ,ʪ0ͪ3̬7ʫ:ͫ=˫@ΫCϮEͪHϬJͬMϫOͪQάS̫UͬV˫XˬY̪Zͫ[ϫ[ͬ\ͬ\ͬ\ͬ\ͬ\ͩ\ͫ[̪ZάYΫXͪW̫UάSͫR̩PͬM̪K˫I̫FΫC̩AΩ>˩;̬7ά4ˬ1̪-Ϊ*Ю&ҭ"έƪå̭̪ҭ"Ю&Ϊ*ͬ.Ь1Ϩ5ͭ8̪<Ϊ?ͪBϪEͪH̪KΪNϬPάS̫UͪWάYͫ[˪]Ϋ^Ϭ_ͭ`ͫaάbάb˪cΪc˪c˪cάbͫaͫaͪ`Ϋ^Ϊ]ͫ[̪Z˫XͩVάSͪQέNͫLΫI̫FΫC˫@ͫ=Ϊ9˪6̭2Ψ/Ь+˪'̯#έЪժ Ȧέ̨#˪'ʬ+Ψ/ͪ3Ъ6ϫ:ʩ>̭AέDͪH̪KΪNͪQ˪TͬV˩Yͫ[έ]Ϭ_ͫa˪cϫdͪfͫg̬hάh̪iάiάiάiάi̪iάhͫgͪfͬeέc˩bͪ`˫^ͩ\άYͪWΪTͫR˫OͫLͮHϪEͪBέ>˭;̬7ɬ4ʪ0˨,̬(ͪ$ϧ ȭέ̨#Ѫ'Ь+ή/ͪ3̬7ϭ;Ϊ?ʫC̫F˩JͬM̬PάSͬVάYuh\sͫaέcϬeͫg̪iͬkͪlΫmάn̪oͫpͫpͫpͫpͫp̬oάn̩nͬlٺrzͪf̫d˩bϬ_˪]̪ZͪWΪTͪQΪNϬJͬG˩D˫@̪<ͭ8ά4ϯ0Ѯ,̬(ͪ$ϧ ħ̨#Ѫ'Ь+ʪ0ά4ͨ8̪<˫@˩DͬG̪K˫OͫRϫU˩Yͬ\̬_{sba^zάiͬkΫm̬oͬqάr̫tͪu˫vͫvͫv̫w̩wͫvͫvͪuǦvxͪl̫j̩hͬeάbͪ`Ϊ]̪ZͬVάS̩PͫLͮH˪ḘAͫ=Ϊ9ϭ5ˬ1̪-̬(ͪ$ǧ ˪'Ь+ʪ0ά4ͭ8̪<ϫ@έDͮHͫL̬P˪TͭWͫ[Ϋ^ͫaϫdͬq̫t˫vΫw̫yͫzͬ{Ϋ|̫}Ϋ}Ϋ}Ϋ}̫}̩}}ͫpΫmͬkάhͬeάb̬_ͩ\ΫX̫UͪQͬMΫI̫FͪBͫ=Ϊ9ϭ5ˬ1̪-̬(ͪ$ʬ+ή/ά4ͭ8Ъ<̩A˪EΫIͬMͪQ̫U˩Yͬ\ͪ`έc˫g̫jūͫz̫|̪~̫ͫͫέ̫ͪͪƦ{ͫvΫsͩq̬nͬk̩hϫdͫaέ]̪ZͬVͫRΪNϬJ̫FͪBΩ>Ϊ9Ϩ5ϯ0ˮ,Ѫ'ͬ.ͪ3ͨ8̪<ϫ@˪EΫIͬMͭQͩVάYέ]ͫaͬeάhͪl̬oǮ̫̪̫ͪͫͫͪã̫yͫvΫsͫpΫmάiͪfάb̩_ͫ[ͪW˩S˫OϬJ̫FͪBͫ=Ϊ9ά4ʪ0Ь+̭2̬7ϭ;˫@έDΫIͬMͫRͬV̪ZΫ^άbͪf̫jΫmͬqϫtŬ̫ͪά˩ӹͫΫ|̫yͬuΪr̪oͬkͫgέcϬ_ͫ[ͪWάS˫OϬJ̫F̭Aͫ=ͭ8ͪ3Ψ/ϭ5ϫ:Ϊ?˩DͪHͩMͪQͩV̪ZΫ^άb˫gͩk̪oάrͫvͫzràɨܵά̬~ͪ{ΫwΫsͫpͪl̬h̫dͪ`ͫ[ͪW˩SΪN˩J˪Eϫ@̪<̬7̭2Ϊ9ʩ>ͪBͬGͫLϬP̫U̪ZΫ^άbͫgͬk̪oΫsΫwͪ{̪̫ͫͫΫ|άxϫtͫpͬlάh̫dϬ_ͫ[ͬVͫRͬM˫IΩDΪ?ϫ:ϭ5̪<̩A̫FϬJϫO˪T˩Yέ]άbͭfͬk̬oΫs̪x̫|ͫ옗ΫͫΪ̫}̫yͪuͫpͬl̬hέc̬_̪ZϫUͪQͫLͬGͪBͫ=ͭ8ʪ?˩D˫I˪NͫRͭWͬ\ͫaϬe̫j̪oΫs̪xΫ|~˩ͫͭάΫ}̫yͪuͫp˪lͫgάb˫^ˬYΪTϫOϬJϮEϫ@ϭ;ͪBͩGͫLͪQͩVϭZϬ_ϫd̪i̬nάrΫwͬ{̫ͫͪά̫}άx̫t̬oͩkͪfͫaͬ\ͪWͫRͬMͪHΫCέ>έDΫIέNάSΫXέ]άbͫgͬlͬqͫvϳŤͫͪͫΪΫ|Ϋw̫s̬n̪i̫d̬_̪Z̫U̬P̪K̫F̩AЫFͫLͪQͬVͫ[ͭ`ͬe̫j̬oַ|¿Ϋͫͫͬͫͪ{ͫvͬqͪlͫgάbΪ]ΫX˩SͬMͪHΫC˫IΪNάSΫX˫^˪cάhź¢ΫΫ̫̫̪~̫y̫t̪o̫jϫdϬ_̪Z̫U̩PϬJϪEĦV¦\dpy~̫̪̫̫̪̫|ͫvͬqͪlͫg˩bͬ\ͪWͫRͫLͬGg^VvtqٺͪάΫ̫̪~̫yΫsάn̪i̫dΫ^άY˪TΪNΫIΪNģV~̫ͫͪͫͫͪ{ͬuͫpͬkϬeͪ`ͫ[ϫU̬PϬJϫO̫U̪Zʧ`г̫̪ά̫̫}Ϋw̪rͬlͫgͫaͬ\ͪWͪQͫL̬PͩVͫ[ͫaͪfǦm~ͫͫͫΫ̪~̫yΫs̬nάh˪cΪ]˫XͫRͩMͪQЬVͬ\˩bͫg̫mΪrȦy~ɭ̪ͬͫͫͫz̫t̪o̪i̫dΫ^˩YάS˪NͫRͪW˪]άb̬hΫm̫sάx̪~˨ͬͫͫͫͫzͪu̬o̫jϫd̬_άY˪TΪNͫRͭWΪ]άbάh̩nΫs̫y̬~ͪͫͫͫΫͫͪ{ͬuͫp̫jͩe̬_̪ZΪTέNͫRͭWΪ]˪cάh̬nΫs̫yά~̫ͪͫͫͬͫΫͫͪ{ͬuͫp̫jͬeϬ_̪ZΪT˫OͫRͪWΪ]άb̬hΫm̫sάx̬~̪ͪͫͫͫͫͬŤΫͫͪ{ͬuͫp̫jͩe̬_άY˪TέNͪQͪWͬ\άbͫgΫmάr̪xΫ}̫έͫάͬͫͫͬͪɦͫͫͫzͪu̬o̫j̫d̩_άYάSΪNͪQͬVͩ\ͫa˫gͪl̪rΫw̫}Ϋά̬ΫͫͫͫͬͫͫͪШͫϫy̫tάn̪iέc˫^ΫXάSͬM̩P̫Uͫ[ͪ`ͪfͬkͫpͫvͬ{̪Ϋ̫̫ά̪̫ΫΫͪͫͪͫש̪~άx̫sΫm̬hάbΪ]ͭWͫRͫLέN˪TάY̬_ϫd̫j̪oϫtͫz̫ͫͬͪͫͫͪͫͫͫͫͪͫͫͫƸ٨̫wͬqͪlͭfͫaͩ\ͬVͪQϭKͬMͫRΫXέ]˪cάhΫm̫s̪xΫ}Ϋ̫̪̫ΫάΫΫΫ̫̪̪ͫͫͫͪª٨˩vͫp̫jͬeͪ`̪Z̫UϫOˬJϭKͪQͬVϫ[ͫaͪfͬk˩qͫvͪ{ͫͫͬͫͬͬͫͫͬάͫά̫ͫͫͪͫάêا̩nάhΪc˫^ΫXάSΪNͮH˩J˫OΪTάY̩_̫d̪iάnΫsάx̪~̫̫ΫΫάΫ̫̪ͫͬͫͫͬͫΫͫάͫͫêձɨlͭfͫaͬ\ЬVͪQͫLͩGͪHͩMͫRͪWͬ\ͫa˫gͪlͬqͫvͪ{ͫͬͫͫͪΫ̪Ϋ̫ͫͬͪͫͪͫͫͬͫά̫ͫЧ̫d̩_άYέTϫOˬJ˪EϪEϬJ̩P̫U̪Z̬_̫d̪i̬n̫s̪xΫ|άΫ̪̫̫̫̫ͬͫͬͫͫͫͫͫͫͪͪͫͫͫάȤeͬ\ͪWͫRͩMͪHͮBΫCͪHͬMͫRͪWͬ\ͫaͪfͬkϬo̫t̫yΫ}ΫΫͫͫͬΫΫͪͬά̫ΫͫΫ̪ͪͫάͫά̪ͫͫɭ~|άYέTϫOϬJϪE˫@ϫ@ϪEϬJϫOΪTˬY˫^˪cͫgͪlͬqͬuͫz̬~Ϋ̪̫ͫΫά̫ͫΫ̫ͫͬΫ̪ͪͫͫͪͪͫͪά̪ͫǭ񬬬ٺ]ͭQͫLͬGͮBͫ=ͫ=ͪBͬGͫLͪQͩVϭZ̬_̫dάh̫mͬqͫvͫz̬~ΫΫͪάΫͫά̫ͬͫͪͫά̫̫ͫͪͬͬͬͬͫͫͫͫŪ𤤣ΪNΫI˪E˫@˩;ϫ:Ϊ?έD˫IͬMͫRͪWͫ[ͪ`ϫd̪iΫmͬqͬuͩzΫ}άͫΫ̫̫ͫͫΫ̫̪άͬΫ̫ͪͬͪΫ̫̫̫ͫͫΫάͬ©󐐐ǯV̫FЭAͫ=ͨ8̬7̪<̩AϪEˬJ˫OάS˫Xͬ\ͭ`ͩe̪iΫmͬqͪu̫yΫ|̫ͫΫͫΫͫΫͫͫΫ̫ͪͫΫΫΫ̫ͫͬͫά̫Ϋ̫ͬͫͫɭΫCέ>Ϊ9ʨ5ά4ͭ8ͫ=ͪB̫F̪KϫO˪TΫXͬ\ͭ`ϫdάhͬlͫp̫tΫwͪ{̪~Ϊ̪ͪͫͫά̫ͫΫ̫̫ͬͫͫͫͫͫͪͬͫΫ̫ͬͫΫͫΫ|Ȯwwv𳳳겚S˩;˪6Ь1Ϫ0ʨ5Ϊ9έ>ͮBͩG̪KϫO˪TΫXͬ\ͪ`̫dͫgͬk̪oΪrͬuάxͬ{̬~̪̫ͫά̫̫̪ͫͫͫͫͫͫάΫ̫ͫͪΫ̫̫ͬͫ}ͫzΫw̫tͫpū|||z𗗗̬7ѭ2ͬ.̪-ˬ1˪6ϫ:έ>ΫCͬG̪KϫOάSͭWͫ[̬_άbͪfάi̫mͫp̫sͫvάxͪ{Ϋ}ͫά̫ͬͫΫάά̫̫ά̪ΫͫΫ̫̬ͫ~Ϋ|ͫzΫw̫tͬqάnͬk̬hϫdƫp풒{OΨ/Ϊ*ͨ)̪-̨2˪6ϫ:έ>ΫCͩG̪K˫O˩SͬV̪Zέ]ͫaϫdͫg̫jΫmͫpάrͪuΫw̫yͪ{Ϋ|̪~ͫͫͫΪάά̪ͫͫά~Ϋ}̫|ͫzάxͫv̫tͬq̪oͪl̪iͪf˪c̬_ͩ\ΫXb{{zЬ+Ю&Ϭ%ͮ)Ұ-̨2˪6ϫ:έ>ͪB̫FˬJ˪NͭQ̫UΫXͩ\̬_άbͬe̩h̫j̫m̪oͬq̫sͪuͫvΫwάxϫyͫzͫzͪ{ͫzͫzͫz̫y̪x̫wͬu̫tΪrͫp̬nͬk̪iͭfέcͭ`Ϊ]̪ZͪWάS̩PͫLʱRqqp{mM̨#ɪ!Ϭ%ͮ)Ҫ-̨2˪6ʫ:ʩ>ЭAϪE˫IͫL̩PάSͬVάYͬ\̬_άbϫd˫g̪iͬk̫mάnͫpͬqΪr̫sΫs̫t̫t̫t̫tΫsάr̪rͫp̪o̩nͪl̫j̬hϬeΪcϭ`˫^ͫ[ΫX̫UͭQΪN̪KͬGΫCή?æH~ylkkέʧɪ!Ϭ%ͮ)̪-ˬ1Ϩ5Ϊ9Ю<ϫ@˩DͬGϬJ˪NͪQ˪TͪWάYͬ\Ϋ^ͫa˪cͩeͭfάhάiͩk˪lͬlΫmΫmΫmΫmΫm̫mͪlͬk̫j̪iͫgͪf̫dάbͪ`Ϊ]ͫ[ΫXϫUͫRϫOͫLΫIϮEͪBέ>˩;̬7ͪ3:wvvib[I۶ѭϯ ȥ%Ҭ(Ѯ,Ϫ0ά4Ѭ7˭;έ>ͪBϪEͪH̪KΪNͪQ˪TͬVΫXͫ[˪]Ϋ^ͪ`˩bΪc̫dͬeͪfͭfͫgͫgͫg˫gͪfϬeͩe̫dάbͫaϬ_˫^ͩ\̪ZͭW̫UͫR̩PͩM˩JͩG˩Dϫ@ͫ=Ϊ9˪6̭2ͬ.Ϊ*˪'-}yWVVSSSɮȭϧ ͪ$̬(Ь+Ψ/ѭ2˪6Ϊ9ͫ=˫@ΫC̫FͮH̪K˪N̬PͫRέTͬVΫX̪Zͫ[˪]˫^̩_Ϭ_ͪ`ͪ`ͭ`ͭ`ͪ`ͪ`̬_Ϋ^Ϊ]ͬ\ͫ[άYͭWͩVάSͭQ˫OͫLˬJͬGέḒAέ>˭;ͨ8ά4ˬ1̪-Ȫ*ɨ&˭"̪ΧiZK222ΪЪέ̨#Ю&Ϊ*̪-ˬ1ά4̬7ϫ:ͫ=˫@ΫCϪEͪHϬJͫLέN̬PͫR˪T̫UͬVͪWΫXˬYάY̪Z̪Z̪ZάYάYΫX˫XͪWͩVέTάSͪQϫOͬMϪKΫIͩGέDЭAʪ?̪<Ϊ9˪6ѭ2ή/˨,̬(Ȭ%ɪ!ʰ̭ƪ ٳΧ̪Ѫ!Ϭ%̬(Ь+ɨ/̨2ʨ5ͨ8ϫ:ͫ=ή?ͪBέD̫FͪHˬJͫLͬM˫O̩PͪQͫRͫRΩSάSάSάSάS˩SͫRͪQϬPϫOΪNͩM̪KΫIͬGϪEΫC̩Aέ>̪<Ϊ9Ъ6ͪ3Ϫ0̪-Ϊ*˪'̯#ǧ ѭժ̳ ̦̭ѭǧ ̨#Ш&ͮ)ˮ,ή/̨2ʨ5̬7ʫ:̪<έ>ϫ@ͪB˩DϮEͬGͪHΫIϬJ̪KͫLͫLͩMͩMͩMͫLͫLϭK̪KˬJΫIͪH̫F˪EΫC̭AΪ?ͫ=˩;ͭ8˪6ͪ3ˬ1ͬ.ʬ+̬(Ȭ%Ѫ!̪ƪӦѹ ժȦαӰɪ!ͪ$˪'ͮ)ˮ,Ψ/Ь1ɬ4˪6ͭ8ϫ:̪<ʩ>Ϊ?ϫ@ͪBΫC˩D˪EϪE̫F̫F̫F̫F̫FϮE˪EέDΫCͮB̭A˫@έ>ͫ=˭;Ϊ9̬7ʨ5ͪ3Ϫ0ͬ.ʬ+̬(Ϭ%ҭ"έѭ̭Ϊ̳ ￯ΪʪЪ̪ɪ!ͪ$Ю&ͨ)Ь+ͬ.ʪ0̨2ά4˪6̬7Ϊ9ϫ:ϭ;Ъ<ͫ=έ>ʪ?Ϊ?ή?˫@ή?Ϊ?Ϊ?έ>Ω>ͫ=̪<˩;ʫ:ͭ8̬7ʨ5ͪ3ˬ1Ψ/̪-Ϊ*̬(Ϭ%ҭ"ǧ ʧΧȦ׮۶ ɡŢժЪ̪ɪ!̨#Ϭ%̬(Ϊ*˨,ͬ.ή/ˬ1ѭ2ά4Ϩ5˪6̬7ͨ8ͭ8ɪ9Ϊ9Ϊ9Ϊ9Ϊ9ͭ8ͨ8̬7Я6ϭ5ά4ͪ3̨2Ϫ0ɨ/̪-ʬ+ͨ)˪'ͪ$˭"έʧΧӱ̳ϯ̙۶ϯɮŢʪƪʧέ˥"ͪ$ɨ&Ѫ'ͮ)ʬ+ˮ,̪-ͬ.ή/Ϫ0ˬ1̨2̭2̭2ѭ2ѭ2̭2̨2Ь1ˬ1Ϫ0ή/ͬ.̪-Ь+Ϊ*̬(˪'Ϭ%̨#ɪ!̪ѭΧȦ̳ƪժ ??paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_64.png000066400000000000000000000172271417573700700257560ustar00rootroot00000000000000PNG  IHDR@@iqsBIT|d pHYs1tEXtSoftwarewww.inkscape.org<IDATx{}]Uu>}DP b("Bu:cLO[;v:c=R$@JI)JCYveAZ )᜘_2v J,޽f Gs]5_s7L\4eQg1I+NCFˠ bDCAʝGUԣ 4"@ 2W͖:aTÚ16Q=cqw\ .–ҡ1qTs9ЋO0^| hiՌӪcdTɨ PDE$I$RH2"Fr6]mJxJ 2Mrbq ֙n`чۇeHzIC>tz1fhi0MIcIPI# 8Aw׀VJ@#S%2\iYcJ\AW*3&뜜vq# p *ߺh:v+(惠#{ӇIGV/֙ LISºf% F 3woCPa y#)螖0$DDԈ`FjFTfu>}=+.|:,tÂtzހn!;iUt',&,d0q@@O?DLd mB<)dڜb 9cWgz`n(uΎ>l$ٚOxzsXe n<]c"$Nap31L a4$IFȌ]`. 6>BOJ)iHRz1Ĉ Je((,u&:`԰'\w4#۶.aսsRW dV`9dfYG*$ ZTH+JuJB =ҝt#="p:Hw9)3ҍ4a!2 p3M4H L&d2H+ VF3[.*W߃;&(9wI7MtGGtn -I{bK+,y_;Uvvsvv3Cd;R0R84΁HN 5:rqt91slspWD v5r`.s}{ׯ_]`E_~ߗnq /p{sw bKkgZLV$(, mIсHA&m|dQl!I p@ (H2ZJ!@BH$-,FH3x {7ʣ;7?;ɇ?r݋^w!<^<>;;?Ї w^^u87媽j XP]36_,D=9"Ni5}-e~Q)*DJS{֦-z_WS[=X?m;yguV/|qs]P]Q9@4@gͶb"; MyhQpP&FLM&"aeg#G@l[M7'Gy9:,j|[w\veSN:ӟaqt7y/m1: 5A&z=kXq <ViҚJlg@S4킑DB ޽߼O8.x?59Sy^ /7"p‘ @>Ddizh=Y,4TPXdh6>/,Rm Kl6I f23!Rߝ}laVqkـ;7Oz =X,_bW^ueZlNV Ȍ#a4p] # !,6W|\nQ$ijv@ ?>pѧ,+EEFΣ;NB*};u7%5h7ևI#~0g[nmx˭գZ1oժUX`nͥ?t g_,)d Pydxn nMS!s7NPeLm(Qh( a%s5vm5'$>|7mdIZU?'yqŕktt3<]233{6nCZL،(XaEnFacd@F\Q (4@4)mw͟[{qG.>Г_pI?||Q+ظq̞_~ҹ_62(\nh+8#L0 ۶0yBN7&9#GlPP@~kO?׭\p!' uzs7#SOǶraޢS!p|?w['xs9{Ϛˮ׾6 eiƌBfi,NE6w'3 ciI8! S|[׽(p˭_ߺN&lقN;yټysn[dv;wfʬ8KhDfMCFS28FK(m[h綸_k}NZlK^4pEkǼD?teW}O|wiR4(Ba,Q)F^ L4~`fBF%L0{۷5rwqբ/ۏCɯ+<Ê+^н7n9_]BZ$!fcQ2fAAGH1C@&i`r7B4_vt.{۷=|O?e~9O?4#_/|{/8#t|p:~@D&E9>aQH!IZiIѡLɤ:FU|!|cɏ s>}[.2Z { ݺi.^LB|}H]!7c43fR"64sFA,ۯ?xm_+瞹ג%"pWnvW3ܹ=[]6`[>Dvᆌ&m;7obhwo}pԯ-^'_z,z/wyi=׸gcKF/]ߘJP 8l^<2Ľ_}oZhŸ Z+Vo9k5c ֮]m_|㺛vǝ}!hVRp!2*…"!@E uG9'g-[p qHw~6oތo\p~njyԑ8@@jD F@fh RҠ͗ly';9`ܹ?﮻'^s?ׯǣ>>~nƽ-hUJ`%\hM$h^o9/3gO x|M[>;ctI?n?vRj*K,4KJJI /}捇~WyF~/oV\=kwݿ޼UPRJ "boz)͓!=;m}Ozͫ7?񘝝3G2~_ҥKqGDu7lqܹF +J"R(J"rV </~9e{-t=pq.=d~`C@2lF-e)!OB5_}bF^";'듯=87,~w|~/o\$\uUXj~yj#/3p`/' jnCB}zzw&A`^/Yuɲ#6+/:cA^;i&KpK.ozӛ055_w7=!JZ*J{B@ R޾ v4:!;NPy߰sA$:8|zzk׮y睇R7Ek.~,=ɹ*aJou҄\}z LLitlDA=+\M3wC_O8}Q\}xS?33{s'xLT #۹̣.4y0̢uMƲcɍw\{O9Cw7nxR |A_zOx|!.2zCcs*앪4I TB}zLXoPPo ;dQG9<$Laμ3_ֵܵ/׻c? ֭͛১7neY.n2 **R)DS Wd˻[Q5ZThw*SUJcc/_qwe_{{W_+{녏Kǟ~R=@Oc~ Dޙ RaPE"b Z8ZX#`D5ZV*(2z 5G+6 4<11+\ܵ{ /c%\,Y'7o·mGHDBAXXHUUՌ& 1 DA(L1(#$c"8AQDJ$R2K9\}:g~7>/>_yFQ.܁K3xݷoSukqQJk\TT^U ׀դ&̞F29Q%& .@ZH,999W\uͦ u)1f;u Xi/[ܼXJǍ501UeT*&#9Ϊ2ӝ *s݆ D@Fj\9|`qK_zxӭ.Z!I$2, uC7hj;.t| '''h{|ӗ}&6R})T+jlo XUE`IUMQZYa"x dZs\PHFO(ujMN}|C|RJ`t#F%r$9*ج&S.J"g]Ö-:s^:1Jsp\78,qTT^WE VbNUY{*lTD#$ԝI cQ"GqpqW?o}}WYh1 d@. zC>yt:rԧ.7Xy)T UVi=b5:fpZYŨNY'Ucht tXV\%$T{,xK#P+z8w\c `2R-%iL_ՄQMO= _〈}!"ኬHP#G*gjCP5dՠTIհ:nyt`OS_LVjb>pk`CѪJƕ`n-ޚF:LkIfqaJ HHNf G3=3ZG6+oM #YjR0kaYYW_Xoޏ?QeOtb,{\@fEQP)<ᣒyA\jw 4 F!h=tk͈Jm#`nRAhMX *KE@Z)X[D*w%eEUGV2+8QcSG!Ԓ]2,52(;[eLh%ҺEjCZ #}f`-fo63;{hH*;9jA"qˌw S,+"fmϿ'ݲWƹCt ie3 f.3m322BR ]980ZZpUrXQ4>@`޽m'3w[Owl7yfcOvJ34f[뜍4: [ӔIj$ZIҤfPIaDvV;Q\m8sqࢧ?g>SrzMS[F 50͋hYF 3Y3hr ⇃*{q9jY&r0;@ tUL OXuk,¬9&wvދOYeAFhg5됵"KIq#5-EİͲ (Mn9}T^|>|Q|>0ZqZ L6dݳ}ߣ"HA^v͎ȭ sV7Mr%dxݖ&g"&xDċd7gj6-4}͢uhYf6AZxncW"nԗde9YQfd1)5sn٪s " 7vNX*IENDB`paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_72.png000066400000000000000000000221651417573700700257520ustar00rootroot00000000000000PNG  IHDRHHUGsBIT|d pHYsyy ,tEXtSoftwarewww.inkscape.org< IDATx|yUuZ{{{5UCX(S  F@HmN݉v;I[1;&11DàR Z( Të={|( 9uwoݵ{},ν+'0p6m>V̢ZpZaǙu`1f 4F 97-C T @)^́Re?3eJ+a(DW%DƶorI{3$^p&llu:Qh=bH+N KZ:4#QkJ#^VzO8)KO\ylLAVEi&C9xL׭3@Mj8߳֓Of'$7>h8#`c )F7bhdi44$ 5d4&ܥZ$AtOvK,id"&oT{?AD e"6. \yYp[gΠ n4̍K)s3jD! &0p9#rUTL Jt@RC4+I!sRIEͩE2͓1fu|1sϕ7/-V|tAAW <3fb0euMFkD2fQ`tI̹1!`& 3QDҕbHAJ a}k\aƶM%N>dmO йtMQ{8QJ0뙼gp=Y0%PIc "Ɍ#3x\ElbHIy))O3dT<,kFH1V(` x"l 2,]q{pq}y?=sTiXt8}@`t7XM%|f@"dI2,H;`\,ejg^hH'MB3Tk9Db0LPKb䓢gu+|}h-^\éaAЫjiVA̽А zfH՚.4HRX3$m6s2Ad g")d6'B d1Q%z(0:A%8" 3)_)dlѳd(@J"1˜&) 1! d!RDBL#2RB`F/f2L䵗^?Ó9蕿K.-ЇDx-Nz)a;4t )%TB#,@ .ΚӘn@HiAs5&I Dz*SLibHҤT aK`C$Qa[d_wy[??[UKA9=wz<ʒBq ,0z t .h4F)Hj#%   $6+Ec"!}EG5H=@_.9_NZ06oެ?5.zLeW}_Ө6[YHYXY)z;%3 +Yв)nnPzvDZAJGsB#)hfRB6ׄY4oo- K_-1GW0_g1꣏I'̀sE_^98]1Ġ@(CYpG rNcRRF@љ͹2-(dBl9Lo*[U7?"}H8B"A- S@2IYK8Udt frR43IeFDeO-Yꒇy<5HH)CJHAiq+}~ՇxO?0V_~E />{>n[/;GOr¨TY[T16CAtC̪*YHtQfa)'aLB-MPH?m:OHm~cl|W z!dˡk^rjժUO}yq؛·[+[IEVhꂿxBҌH,l`)K.Xs,aѤRA %)f" JidKw_㍯9|痽'V7~p?kz5.<#|'355[ۯP<*gqeSMF7)=͛2:9L e Le#$Z9 LԂC1.~>S^{؎{^K[n{lI'|}kN:nR $VhRp-Nin:LNˤO0r֮И"k5D ɜ DtMWZlժ]&0sΝޒE {z*J)O͛7k}_0Fdp fCdFSM#&o,X4jlZvJDA@?e-?7f鮻bOyOO&_>_V`y/^+DZf:e!^D2L1Ed E5:2 @X2՜v@!aZC7pŲzⱋwyg lܸQmʼ;zzU;{$*L0EX! N[."Lһ4 \j3cTb4c&N2cJÛܮ'ۿM-ig\|r=-o'xY kXtNk*928D!M a.viE M$]X ̀'hr.Y1>o<~w|NE{Ͻ:c'''qGȰ*8S0RPM0CX[rJ|P)`n00&Nnoiqa;olBlY7?o{g>]v^Yvn>|YY}/?[E!-ӌh/l4XHFZ1*|4qhU@F *~[&P{1(,nr .]G>7|3k/^Y};'2&*a@Xa7L+>#c"i C+6D14Ј 4hIka\ro='a%K.@O.k׮N;?E߼凝ٳ$Ha nM$sD&V l9iF#$j&АCwY667Oyӂŋ?o`n̋}#؟>lx.[;|ȴ4@[d[?@ $т5R5͂E)SSN׬mio-/cc?o~xކ'U3q\+6yة3mD2 #' hA9lnBT*G'q07\͗oZ4o޼I̙gŞ/]mGK3Y[Q3zzl{߽gO~}+8#.[{!׶gРV` .B!0fPu@ȠIj=Vݾw/FS֫k>֭;}y33qܱao YheY pulRD}ctQ{h_ܟU99-[u[b.}~/ش㾇_ЎhUAh%OPZ "Zۺ+\?_^}ϲ+` t~#v39眃: +V;qˆ֯9)%DJ"[,I@v0)?/ܸ'5x+p؟`0gO> .}y?A +[B)B% $5PHO@k@i/zkʗ}p/8gױ~ooݺ_W.P<155 ?~}\<0Uljp˶0IJrdjD ,kT_ɟ?U]_lbӍZ+cg ЃGi=."r)O[;~x~K{Q0RI0hP`Gy'T'dpW?slzeWmlɮzի_\Ï< %Kpw㪫iBym~(DBJuR{LTV{D!ccQdo_={~psΫ|m݆ { }'K^/C %UDU8TmT%lJ!aZbڨHTB8xw/_<`͚cys[ƷqN?k7G[أB1@$چCH"Rl&aH?ow:[ a/Xlr?o/~?>M˯\~A/hCx9 @ BҪ0Z)^:ab#DH5_3@E*~ *v\G};o|'~=  _!kp.b,_p }ߟaM+ !Rٮ[+~@D*ydքy5f ft#el0jul#H~7pWrqC蟝vi'WUYY׎Y޳zWv̩@QIV@pVA"jB";¦ guc}:[߸`c͛?>"gy&֮]]w ?n%J* "&5MƦzgwk>ޚ5ƒh SB%Pb,͛Ʈ>{ſù瞋7x!O7/>E+2mg Jf2UUT{U% UЊi-3*BUU("~U@UD8`є[]|8lܸR .2[w}wK_q-N Ѧg vJԹrff8G1k1َuzVꚬ- mHe]~%S㋯{K/YxVx!k<9߅\~K?r Ac]UնU=E5U8 B( Td*Zfi>B eBfSk g?_9dm*&^?}vYh#=pw͔-[}E$*00Ev`Y 9jd j*V( F Y2^dҭ [w-R΂ R)kp{uqGߝy/vXswd0rqni^%$+wE9`v?G/|67.u[E˹HsH:|UKa<@Őb eD;<6Y1vcMTH mi725H~c9gŠ_2IFZ |a@y=!\e'w3˾}Ŗ;MÄҡ,BTSc Fj@(T3TQըZJ͡?Α^[,#Pغ߭ dOb+(Q].% |w\ Ջc;+V0aLB4flKp 2/Z2d n/} .%/yҪg>=|$Ì5 YaTsUEOg01$EU s ?tXhmdsA*ffmw\h\Eanު-xl|>;c3id$I؀ 9n{Vڵ_hݣSw?kMJ hU)9 +Dg-hc.8^J',v81R-`m]ςff"El+tmw^oc׽3vm&S!V2m!P&̢96Vj1ƀ n7Eknwr%_X4b%W~ﻫ_ͬ AC Ѫ"JYG9 w#1gPe7O:AdmjQ7$v|֚oe=Z[b4IL>n*)(5ABo|e/w]KwirBmkyUe.H QC S$U^MvBg@޴le{U-}+Zfv=ۜFY o]颛&4m ﲸ+[G&dTd; gmcJ9; =B-mwZMy(ʘfU֩`>ƤM{. 0Db%0wmO5v(=T =6##SL@=@ CٙUS 9#2fV ՈW'*Jz!" ZjF*-¢[/7תK>ljlAtj=|AI$:p,2:!e-9kf-vd&G+&S[k$h^mmBJIDAT#Z}E"֎"^(@Z TW} z6n?n,v\r HS]h@UO&ZBhf6&MxRThp˺?IT̈2ICV^-Hsف XjS_Y30( o9qOn].쳗j|[cH0G4P¼L Q}ivjnZ"Ɯބ h]T~GmGN#DwDԄMJZY&QY#(d)';x٦ ;;(,pk8lJa5PrIDpn-|;zG殨sH+IN" 0ىX1Ri@kr-[JƘm ;ULx.\xl3ǐޔoM"̈*4#l2G73ZVDcӓ_*ޚtFr8MuJs`UY X KdX1c bk҂m'í5,wشpm.Y`#ӦէҚԬǨVJADmK38$u^ϨPIPK6& gTjBer{1 $˽!D @ @ 1x}`WR!&NXbٳ=رcoݺ5F>rvvf҂+v޽ wi~ H$pM6aBn;܊F<}taawՃa,[w;9===3\PRgϰM6IcbbB1j~(A?yyyR777YNNNOk/ F ƭ_>f|* 3̞*EJ<<DFFBgg琏/Eݥb&~ܹVB1xJPT'ixP*C>~3y L^b.8aW\K. xgNNNY,555Xչ{nז-[DRݻ.>\w84qÆ G4ph<oe`7oN; 022o`8.صk 77W. <<Fv%m HxgUVVVh4YӬNmmmP @UU kO$5kBKkֱX˹\xQK ##C[L VXA/0aAAA4ŋX2>|('x Ӂ{OټysL\\R"؍7ڼJB!X,ָܼ<#Y5NOp8M6袓FT* ?.x!DGG6}$ 3۱_]nnnbD  mڱchl]bq[0mm$l6sII $''뤮[n٭;wGGG;wQRR2%' rػw ++o::;;_T*xyy]3qk׮ zm;xӧO9y$|Bvl}l߱coT Ң:e2Px;PwB>?sLe "##A EMKs]:ڪ6445N:ncKKrt= F`۱@d2d~t o333ӧ#Є  NjD"NjD&?i#^(**JD" &Ђ_.//m߾}[YYWc-,,V}׿۷/̙3MbN.|NgBPuq*++a577{w.]|Μ9W^}N'$$BCC%_'Nx|$oI;!ٺuuvvΝ;/`_LLLĉuجɾáFz'zh`>"HnݺCxgGGGzhhh@JMM~^wwwѣGѣGy@{칏w0aƍrss_0 ~'aFFKgH @XXmЗ.2wpZp¥ŭJ>h'O:8~8h{px{{ tC r8(( :}8ׯΝtF .@QQQ(e˖Trۣ4/=|1044#C a|>_4m4ɬY&MۉȤ{uZ,Sgg'?~`1xYx?ٓ%?B1T---mP(S֔)SLTj#G#ujhh!;tQp8z@^^`ʕ=bkrw4ZHUTNfgggm_8|6JYQQQ*mV`07oJLL]3c sOOϔu'BR;[Lpd2ۣmkkXmݺ5Oi211\:K,nkkdcccz{Oy*JJQ$S:"@ @ @ ?΍AB0IENDB`paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/paperwork_halo.svg000066400000000000000000000100101417573700700264420ustar00rootroot00000000000000 image/svg+xml paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/data/work.openpaper.Paperwork.appdata.xml000066400000000000000000000154151417573700700320000ustar00rootroot00000000000000 work.openpaper.Paperwork.desktop CC0-1.0 Paperwork Personal document manager Gestionnaire de documents personnels

Sorting documents is a machine's job. Paperwork is an easy-to-use application which allows you to find in all your documents when you need them. Type a few keywords in the search bar and the list of papers will shrink to only the relevant content.

Trier les documents est un travail de machine. Paperwork est une application qui vous permet de trouver tout vos documents, quand vous en avez besoin. Tapez quelques mots-clés et la liste des papiers se réduira au contenu pertinent.

This is where the magic happens: Paperwork uses automatically optical character recognition (OCR) to convert your papers into searchable documents.

Là où la magie se produit : Paperwork utilise automatiquement la reconnaissance optique de caractères (ROC) pour convertir vos papiers en documents cherchables.

Main features are:

Les principales fonctionnalités sont :

  • Scanner support
  • Support des scanners
  • PDF support
  • Support des PDFs
  • Automatic detection of page orientation
  • Détection automatique de l'orientation des pages
  • OCR
  • ROC
  • Document labels
  • Labels pour trier les documents
  • Automatic guessing of the labels to apply on new documents
  • Labelisation semi-automatique des documents
  • Search
  • Recherche
  • Keyword suggestions
  • Suggestions de mots-clés
  • Quick edit of scans
  • Édition rapides des pages
Office Scanning OCR Archiving GNOME https://openpaper.work/ https://gitlab.gnome.org/World/OpenPaperwork/paperwork/issues https://gitlab.gnome.org/World/OpenPaperwork/paperwork/#readme https://www.patreon.com/openpaper work.openpaper.Paperwork.desktop paperwork paperwork application/pdf image/jpeg image/png image/x-ms-bmp GPL-3.0+ Openpaper.work Home screen https://gitlab.gnome.org/World/OpenPaperwork/paperwork-screenshots/-/raw/master/2.0/paperwork_home.png Paperwork document view https://gitlab.gnome.org/World/OpenPaperwork/paperwork-screenshots/-/raw/master/2.0/paperwork_doc.png Paperwork page view https://gitlab.gnome.org/World/OpenPaperwork/paperwork-screenshots/-/raw/master/2.0/paperwork_page.png Paperwork import https://gitlab.gnome.org/World/OpenPaperwork/paperwork-screenshots/-/raw/master/2.0/paperwork_import.png Paperwork export https://gitlab.gnome.org/World/OpenPaperwork/paperwork-screenshots/-/raw/master/2.0/paperwork_export.png Paperwork document list https://gitlab.gnome.org/World/OpenPaperwork/paperwork-screenshots/-/raw/master/2.0/doclist.png Paperwork main windows https://gitlab.gnome.org/World/OpenPaperwork/paperwork-screenshots/-/raw/master/2.0/main_window.png Paperwork label properties https://gitlab.gnome.org/World/OpenPaperwork/paperwork-screenshots/-/raw/master/2.0/multiple_labels.png Paperwork settings window https://gitlab.gnome.org/World/OpenPaperwork/paperwork-screenshots/-/raw/master/2.0/settings.png Paperwork suggestions https://gitlab.gnome.org/World/OpenPaperwork/paperwork-screenshots/-/raw/master/2.0/suggestions.png paperwork paperwork_backend paperwork_gtk openpaperwork_core openpaperwork_gtk paperwork_shell jflesch@openpaper.work
paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/doc_selection.py000066400000000000000000000020141417573700700251640ustar00rootroot00000000000000import logging import openpaperwork_core LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): # so we can report an accurate document count PRIORITY = 1000 def __init__(self): super().__init__() self.selection = set() def get_interfaces(self): return ['doc_selection'] def doc_selection_reset(self): self.selection = set() def doc_selection_add(self, doc_id, doc_url): self.selection.add((doc_id, doc_url)) def doc_selection_remove(self, doc_id, doc_url): try: self.selection.remove((doc_id, doc_url)) except KeyError: pass def doc_selection_get(self, out: set): out.update(self.selection) def doc_selection_len(self): nb_docs = len(self.selection) if nb_docs <= 0: return None return nb_docs def doc_selection_in(self, doc_id, doc_url): if (doc_id, doc_url) in self.selection: return True return None paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/docimport.py000066400000000000000000000211131417573700700243530ustar00rootroot00000000000000import logging try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_gtk.deps import paperwork_backend.docimport from . import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc_id = None self.windows = [] def get_interfaces(self): return [ 'chkdeps', 'doc_open', 'gtk_doc_import', 'gtk_window_listener', ] def get_deps(self): return [ { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'gtk_dialog_single_entry', 'defaults': ['openpaperwork_gtk.dialogs.single_entry'], }, { 'interface': 'import', 'defaults': [ 'paperwork_backend.docimport.img', 'paperwork_backend.docimport.pdf', ], }, { 'interface': 'notifications', 'defaults': ['paperwork_gtk.notifications.dialog'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def doc_open(self, doc_id, doc_url): if self.core.call_success("is_doc", doc_url) is None: # New document --> no need to track this doc id. # Importer will create a new document in time self.active_doc_id = None return self.active_doc_id = doc_id def doc_close(self): self.active_doc_id = None def _show_no_importer(self, file_uris): msg = ( _("Don't know how to import '%s'. Sorry.") % (file_uris) ) flags = ( Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT ) dialog = Gtk.MessageDialog( transient_for=self.windows[-1], flags=flags, message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, text=msg ) dialog.connect("response", lambda dialog, response: dialog.destroy()) dialog.show_all() def _request_password(self, importer, file_import): self.core.call_success( "gtk_show_dialog_single_entry", self, _("PDF password"), "", importer=importer, file_import=file_import ) def on_dialog_single_entry_reply( self, origin, reply, password, *args, **kwargs): if origin != self: # Not ours return if not reply: # User cancelled return importer = kwargs['importer'] file_import = kwargs['importer'] self._do_import(importer, file_import, data={'password': password}) return True def _show_result_no_doc(self): msg = _("No new document to import found") flags = ( Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT ) dialog = Gtk.MessageDialog( transient_for=self.windows[-1], flags=flags, message_type=Gtk.MessageType.WARNING, buttons=Gtk.ButtonsType.OK, text=msg ) dialog.connect( "response", lambda dialog, response: dialog.destroy() ) dialog.show_all() def _show_result_doc(self, doc_id): doc_url = self.core.call_success("doc_id_to_url", doc_id) assert(doc_url is not None) if self.active_doc_id != doc_id: self.core.call_all("doc_open", doc_id, doc_url) nb_pages = self.core.call_success("doc_get_nb_pages_by_url", doc_url) if nb_pages <= 0: # empty PDF ? LOGGER.warning("Document import %s, but no page in it ?!", doc_id) return self.core.call_success( "mainloop_schedule", self.core.call_all, "doc_goto_page", nb_pages - 1 ) def _delete_files(self, file_uris): LOGGER.info("Moving imported file(s) to trash ...") for file_uri in file_uris: LOGGER.info("Moving %s to trash ...", file_uri) self.core.call_success("fs_unlink", file_uri) notification = self.core.call_success( "get_notification_builder", _("Imported file(s) deleted"), ) if notification is None: return notification.set_icon("edit-delete").show() def _show_result_notification(self, file_import): msg = _("Imported:\n") for (k, v) in file_import.stats.items(): msg += ("- {}: {}\n".format(k, v)) msg = msg.strip() notification = self.core.call_success( "get_notification_builder", _("Import successful"), need_actions=True ) if notification is not None: notification.set_message( msg ).set_icon( "document-new" ).add_action( "delete", _("Delete imported files"), self._delete_files, file_import.imported_files ).show() def _show_result(self, file_import): doc_id = None if len(file_import.upd_doc_ids) > 0: doc_id = list(file_import.upd_doc_ids)[0] if len(file_import.new_doc_ids) > 0: doc_id = list(file_import.new_doc_ids)[0] if doc_id is None: self._show_result_no_doc() return self._show_result_doc(doc_id) self._show_result_notification(file_import) def _reload_docs(self, file_import): for doc_id in file_import.upd_doc_ids: doc_url = self.core.call_success("doc_id_to_url", doc_id) self.core.call_all("doc_reload", doc_id, doc_url) def _add_to_recent(self, file_uris): for file_uri in file_uris: if self.core.call_success("fs_isdir", file_uri) is None: # If the user imported a file, assume they won't import it # twice but they may import again other files from the same # directory file_uri = self.core.call_success("fs_dirname", file_uri) LOGGER.info("Adding %s to recently used files", file_uri) Gtk.RecentManager().add_item(file_uri) def _log_result(self, file_import): LOGGER.info("Import result:") LOGGER.info("- Imported files: %s", file_import.imported_files) LOGGER.info("- Non-imported files: %s", file_import.ignored_files) LOGGER.info("- New documents: %s", file_import.new_doc_ids) LOGGER.info("- Updated documents: %s", file_import.upd_doc_ids) for (k, v) in file_import.stats.items(): LOGGER.info("- %s: %s", k, v) def _do_import(self, importer, file_import, data=None): promise = importer.get_import_promise(data) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(self._log_result, file_import) promise = promise.then(self._show_result, file_import) promise = promise.then(self._reload_docs, file_import) self.core.call_success("transaction_schedule", promise) def gtk_doc_import(self, file_urls): LOGGER.info("Importing: %s", file_urls) file_import = paperwork_backend.docimport.FileImport( file_urls, self.active_doc_id ) importers = [] self.core.call_all("get_importer", importers, file_import) if len(importers) <= 0: self._show_no_importer(file_urls) return # TODO(Jflesch): Should ask the user what must be done if many # importers are possible. importer = importers[0] self._add_to_recent(file_urls) required = set() for (k, v) in importer.get_required_data().items(): required.update(v) LOGGER.info( "Required data to import %s: %s", file_urls, required ) if "password" in required: self._request_password(importer, file_import) else: self._do_import(importer, file_import) return True paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/drawer/000077500000000000000000000000001417573700700232675ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/drawer/__init__.py000066400000000000000000000000001417573700700253660ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/drawer/calibration.py000066400000000000000000000101541417573700700261310ustar00rootroot00000000000000""" Ensure calibration is displayed when a scan is running. """ import logging import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) class Drawer(object): def __init__(self, core, drawing_area, read_only=False): self.core = core self.drawing_area = drawing_area self.content_full_size = None self.scan_id = None self.active = False self.drawer = None self.read_only = read_only def _get_frame(self): return self.core.call_success("config_get", "scanner_calibration") def _set_frame(self, frame): self.core.call_all("config_put", "scanner_calibration", frame) def set_content_full_size(self, size): self.content_full_size = size def start(self): if self.active: self.stop() self.core.call_all( "config_add_observer", "scanner_calibration", self.request_redraw ) self.drawer = self.core.call_success( "draw_frame_start", self.drawing_area, self.content_full_size, self._get_frame, self._set_frame if not self.read_only else None ) self.active = True def request_redraw(self, *args, **kwargs): if self.drawer is None: return self.drawer.request_redraw() def stop(self): if not self.active: return self.core.call_all("draw_frame_stop", self.drawing_area) self.core.call_all( "config_remove_observer", "scanner_calibration", self.request_redraw ) self.active = False class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.drawers = {} # drawing_area --> drawer self.scan_id_to_drawers = {} # int --> drawer def get_interfaces(self): return [ 'gtk_drawer_calibration', 'gtk_drawer_scan', ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'gtk_drawer_frame', 'defaults': ['paperwork_gtk.drawer.frame'], }, { 'interface': 'scan', 'defaults': ['paperwork_backend.docscan.libinsane'], }, { 'interface': 'scanner_calibration', 'defaults': [ 'paperwork_backend.guesswork.cropping.calibration' ] }, ] def draw_calibration_start( self, drawing_area, content_full_size, read_only=False): drawer = Drawer(self.core, drawing_area, read_only) drawer.set_content_full_size(content_full_size) drawer.start() self.drawers[drawing_area] = drawer def draw_calibration_stop(self, drawing_area): if drawing_area not in self.drawers: return self.drawers.pop(drawing_area).stop() def draw_scan_start(self, drawing_area, scan_id=None): if drawing_area in self.drawers: drawer = self.drawers[drawing_area] elif scan_id is not None and scan_id in self.scan_id_to_drawers: drawer = self.scan_id_to_drawers[scan_id] self.drawers.pop(drawer.drawing_area, None) drawer.stop() drawer.drawing_area = drawing_area drawer.start() else: drawer = Drawer(self.core, drawing_area, read_only=True) drawer.scan_id = scan_id self.scan_id_to_drawers[scan_id] = drawer self.drawers[drawing_area] = drawer def draw_scan_stop(self, drawing_area): if drawing_area in self.drawers: self.drawers.pop(drawing_area).stop() def on_scan_page_start(self, scan_id, page_nb, scan_params): for drawer in self.drawers.values(): if drawer.scan_id is None or scan_id == drawer.scan_id: drawer.set_content_full_size( (scan_params.get_width(), scan_params.get_height()) ) drawer.start() paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/drawer/frame.py000066400000000000000000000243251417573700700247410ustar00rootroot00000000000000""" Draws a frame on a GtkDrawingArea. This frame may or may not be resizable. This is usually used for the scanner calibration dialog or the page image cropping. """ import logging import math try: import gi gi.require_version('Gdk', '3.0') from gi.repository import Gdk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_gtk.deps LOGGER = logging.getLogger(__name__) class Drawer(object): HANDLE_RADIUS = 10 HANDLE_DEFAULT_COLOR = (0.0, 0.25, 1.0, 0.5) HANDLE_HOVER_COLOR = (0.1, 0.75, 0.1, 1.0) HANDLE_SELECTED_COLOR = (0.75, 0.1, 0.1, 1.0) HANDLE_MIN_DIST = 50 RECTANGLE_COLOR = (0.0, 0.25, 1.0, 1.0) OUTTER_COLOR = (0.0, 0.25, 1.0, 0.25) FRAME_CORNERS = [ (0, 1), (0, 3), (2, 1), (2, 3), ] def __init__( self, core, drawing_area, content_full_size, get_frame_cb, set_frame_cb=None): """ Arguments: - drawing_area: the drawing area on which the frame must be represented - content_full_size: the drawing area has its own size, smaller than the actual scan or image. We need to know the actual size of the content, so we can figure out at which scale the content and the frame are to represented. - get_frame_cb : callback returning the frame to display relative to content_full_size. A callback is provided so we can always have an up-to-date value (for instance coming from the configuration). """ self.core = core if drawing_area.get_window() is None: drawing_area.connect("realize", self.on_realize) else: self.on_realize(drawing_area) self.drawing_area = drawing_area self.get_frame_cb = get_frame_cb self.set_frame_cb = set_frame_cb self.content_full_size = content_full_size self.active_handle = None self.selected_handle = None self.draw_connect_id = drawing_area.connect("draw", self.on_draw) self.motion_connect_id = None self.press_connect_id = None self.release_connect_id = None if self.set_frame_cb: self.motion_connect_id = drawing_area.connect( "motion-notify-event", self.on_motion ) self.press_connect_id = drawing_area.connect( "button-press-event", self.on_pressed ) self.release_connect_id = drawing_area.connect( "button-release-event", self.on_released ) def stop(self): if self.draw_connect_id is not None: self.drawing_area.disconnect(self.draw_connect_id) if self.motion_connect_id is not None: self.drawing_area.disconnect(self.motion_connect_id) if self.press_connect_id is not None: self.drawing_area.disconnect(self.press_connect_id) if self.release_connect_id is not None: self.drawing_area.disconnect(self.release_connect_id) self.draw_connect_id = None self.motion_connect_id = None self.press_connect_id = None self.release_connect_id = None def request_redraw(self): self.drawing_area.queue_draw() def _get_factor(self): widget_height = self.drawing_area.get_allocated_height() widget_width = self.drawing_area.get_allocated_width() factor_w = self.content_full_size[0] / widget_width factor_h = self.content_full_size[1] / widget_height factor = max(factor_w, factor_h) return factor def _get_frame(self): factor = self._get_factor() frame = self.get_frame_cb() if frame is None: frame = ( 0, 0, self.content_full_size[0], self.content_full_size[1] ) return ( frame[0] / factor, frame[1] / factor, frame[2] / factor, frame[3] / factor, ) def _get_closest_handle(self, x, y): frame = self._get_frame() handle_dists = [ ( math.hypot(frame[corner_x] - x, frame[corner_y] - y), (corner_x, corner_y) ) for (corner_x, corner_y) in self.FRAME_CORNERS ] handle = min(handle_dists) if handle[0] > self.HANDLE_MIN_DIST: return None return handle[1] def on_realize(self, drawing_area, *args, **kwargs): mask = ( Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.POINTER_MOTION_MASK ) drawing_area.add_events(mask) drawing_area.get_window().set_events( drawing_area.get_window().get_events() | mask ) def on_motion(self, drawing_area, event): if self.selected_handle is None: active_handle = self._get_closest_handle(event.x, event.y) if active_handle != self.active_handle: self.request_redraw() self.active_handle = active_handle else: assert(self.set_frame_cb is not None) factor = self._get_factor() frame = self.get_frame_cb() if frame is not None: frame = list(frame) else: frame = [ 0, 0, self.content_full_size[0], self.content_full_size[1] ] x = event.x * factor y = event.y * factor frame[self.selected_handle[0]] = x frame[self.selected_handle[1]] = y frame = ( max(0, min(frame[0], frame[2])), max(0, min(frame[1], frame[3])), min(self.content_full_size[0], max(frame[0], frame[2])), min(self.content_full_size[1], max(frame[1], frame[3])), ) self.set_frame_cb(frame) self.request_redraw() def on_pressed(self, drawing_area, event): self.selected_handle = self._get_closest_handle(event.x, event.y) self.request_redraw() def on_released(self, drawing_area, event): self.selected_handle = None self.request_redraw() def on_draw(self, drawing_area, cairo_ctx): frame = self._get_frame() widget_height = self.drawing_area.get_allocated_height() widget_width = self.drawing_area.get_allocated_width() # outter cairo_ctx.save() try: color = self.OUTTER_COLOR cairo_ctx.set_source_rgba(color[0], color[1], color[2], color[3]) outters = [ (0, 0, widget_width, frame[1]), (0, frame[3], widget_width, widget_height - frame[3]), (0, frame[1], frame[0], frame[3] - frame[1]), ( frame[2], frame[1], widget_width - frame[2], frame[3] - frame[1] ), ] for (x, y, w, h) in outters: cairo_ctx.rectangle(x, y, w, h) cairo_ctx.fill() finally: cairo_ctx.restore() (x, y, w, h) = ( frame[0], frame[1], frame[2] - frame[0], frame[3] - frame[1] ) # rectangle cairo_ctx.save() try: color = self.RECTANGLE_COLOR cairo_ctx.set_source_rgba(color[0], color[1], color[2], color[3]) cairo_ctx.set_line_width(1.0) cairo_ctx.rectangle(x, y, w, h) cairo_ctx.stroke() finally: cairo_ctx.restore() if self.set_frame_cb is None: return # handles cairo_ctx.save() try: for corner in self.FRAME_CORNERS: if self.selected_handle == corner: color = self.HANDLE_SELECTED_COLOR elif self.active_handle == corner: color = self.HANDLE_HOVER_COLOR else: color = self.HANDLE_DEFAULT_COLOR cairo_ctx.set_source_rgba( color[0], color[1], color[2], color[3] ) cairo_ctx.arc( frame[corner[0]], frame[corner[1]], self.HANDLE_RADIUS, 0., 2 * math.pi ) cairo_ctx.fill() finally: cairo_ctx.restore() class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() # drawing area --> Drawer self.active_drawers = {} def get_interfaces(self): return [ 'chkdeps', 'gtk_drawer_frame', ] def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def draw_frame_start( self, drawing_area, content_full_size, get_frame_cb, set_frame_cb=None): drawer = Drawer( self.core, drawing_area, content_full_size, get_frame_cb, set_frame_cb ) self.active_drawers[drawing_area] = drawer return drawer def draw_frame_stop(self, drawing_area): if drawing_area not in self.active_drawers: return drawer = self.active_drawers.pop(drawing_area) drawer.stop() return drawer if __name__ == "__main__": import sys import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk core = openpaperwork_core.Core() core._load_module("test", sys.modules[__name__]) core.init() window = Gtk.Window() window.set_size_request(600, 600) box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 20) window.add(box) drawing_area_a = Gtk.DrawingArea() box.pack_start(drawing_area_a, expand=True, fill=True, padding=0) drawing_area_b = Gtk.DrawingArea() box.pack_start(drawing_area_b, expand=True, fill=True, padding=0) frame = (30, 50, 100, 110) def get_frame(): return frame def set_frame(f): global frame frame = f drawing_area_a.queue_draw() drawing_area_b.queue_draw() core.call_success( "draw_frame_start", drawing_area_a, (200, 300), get_frame ) core.call_success( "draw_frame_start", drawing_area_b, (200, 300), get_frame, set_frame ) window.show_all() Gtk.main() paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/gesture/000077500000000000000000000000001417573700700234615ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/gesture/__init__.py000066400000000000000000000000001417573700700255600ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/gesture/drag_and_drop.py000066400000000000000000000123371417573700700266240ustar00rootroot00000000000000import logging try: import gi gi.require_version('Gdk', '3.0') gi.require_version('Gtk', '3.0') from gi.repository import Gdk from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_core.promise import openpaperwork_gtk.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = (None, None) self.updated = set() self.promise = None def get_interfaces(self): return [ 'doc_open', 'gtk_drag_and_drop' ] def get_deps(self): return [ { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def init(self, core): super().init(core) self.promise = openpaperwork_core.promise.Promise(self.core) def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def _parse_paperwork_uri(self, uri): infos = uri.split("#", 1)[1] infos = infos.split("&") infos = (i.split("=", 1) for i in infos) infos = {k: v for (k, v) in infos} return (infos['doc_id'], int(infos['page'])) def doc_close(self): self.active_doc = (None, None) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def drag_and_drop_page_enable(self, widget): """ Arguments: widget - GTK widget on which page drag'n'drop must be enabled. When a drop is received, it will call the method 'drag_and_drop_get_destination(widget, x, y)'. The plugin to whom the widget belongs should reply with (doc_id, page_idx). """ widget.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.MOVE) targets = Gtk.TargetList.new([]) targets.add_uri_targets(0) widget.drag_dest_set_target_list(targets) widget.connect("drag-data-received", self._on_drag_data_received) def _on_drag_data_received( self, widget, drag_context, x, y, selection_data, info, time): uris = selection_data.get_uris() LOGGER.info( "drag_data_received(%s, %d, %d, %s, %s)", widget, x, y, uris, info ) dst = self.core.call_success( "drag_and_drop_get_destination", widget, x, y ) if dst is None: LOGGER.error("Nobody accepted a drop on %s (%d, %d)", widget, x, y) return (dst_doc_id, dst_doc_url, dst_page_idx) = dst for uri in reversed(uris): LOGGER.info("Drop: URI: %s", uri) self.core.call_all( "drag_and_drop_page_add", uri, dst_doc_id, dst_doc_url, dst_page_idx ) self.core.call_all("drag_and_drop_apply") def drag_and_drop_page_add( self, src_uri, dst_doc_id, dst_doc_url, dst_page_idx): LOGGER.info("Drop: %s --> %s p%d", src_uri, dst_doc_id, dst_page_idx) if "doc_id=" not in src_uri or "page=" not in src_uri: LOGGER.info("Drop: Import of %s", src_uri) # TODO(Jflesch): Should import to the target document (drop) # instead of the currently-opened document self.core.call_all("gtk_doc_import", [src_uri]) return else: # moving page inside the current document (src_doc_id, src_page_idx) = self._parse_paperwork_uri(src_uri) src_doc_url = self.core.call_success("doc_id_to_url", src_doc_id) assert(src_doc_url is not None) if src_doc_id == dst_doc_id and src_page_idx < dst_page_idx: dst_page_idx -= 1 dst_page_idx = max(dst_page_idx, 0) LOGGER.info( "Drop: %s p%d --> %s p%d", src_doc_id, src_page_idx, dst_doc_id, dst_page_idx ) if src_doc_id == dst_doc_id and src_page_idx == dst_page_idx: return self.promise = self.promise.then( self.core.call_all, "page_move_by_url", src_doc_url, src_page_idx, dst_doc_url, dst_page_idx ) self.promise = self.promise.then(lambda *args, **kwargs: None) for doc in ((src_doc_id, src_doc_url), (dst_doc_id, dst_doc_url)): self.promise = self.promise.then( self.core.call_all, "doc_reload", *doc ) self.promise = self.promise.then(lambda *args, **kwargs: None) self.updated.add(src_doc_id) self.updated.add(dst_doc_id) def drag_and_drop_apply(self): if len(self.updated) <= 0: return self.promise = self.promise.then(self.core.call_success( "transaction_simple_promise", [('upd', doc_id) for doc_id in self.updated] )) self.core.call_success("transaction_schedule", self.promise) self.updated = set() self.promise = openpaperwork_core.promise.Promise(self.core) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/gesture/zoom.py000066400000000000000000000046051417573700700250240ustar00rootroot00000000000000import logging try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_gtk.deps LOGGER = logging.getLogger(__name__) class Zoomer(object): def __init__(self, zoomable, adjustment): self.adjustment = adjustment self.gesture = Gtk.GestureZoom.new(zoomable) self.ref_adj_value = 1.0 self.ref_gesture_value = 1.0 self.gesture.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) self.gesture.connect("update", self._on_gesture, adjustment) self.gesture.connect("end", self._update_refs) self.adjustment.connect("value-changed", self._update_refs) def _on_gesture(self, gesture, sequence, adjustment): scale = gesture.get_scale_delta() adj_scale = (self.ref_adj_value * scale / self.ref_gesture_value) LOGGER.debug("Zoom gesture: %f --> %f", scale, adj_scale) adjustment.set_value(adj_scale) def _update_refs(self, *args, **kwargs): self.ref_adj_value = self.adjustment.get_value() self.ref_gesture_value = self.gesture.get_scale_delta() LOGGER.debug("Ref: %f, %f", self.ref_adj_value, self.ref_gesture_value) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() # XXX(Jflesch): Looks like a GObject Introspection bug: if the # GtkGesture objects get garbage-collected, the gesture isn't detected # anymore. --> We need to keep a reference to those objects as long # as we need them. self.refs = {} def get_interfaces(self): return [ 'chkdeps', 'gtk_zoomable', ] def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def on_zoomable_widget_new(self, zoomable, adjustment): """ zoomable is the widget on which user can zoom. zoom gestures are not applied directly on the widget. They are applied on a GtkAdjustment. """ zoomer = Zoomer(zoomable, adjustment) self.core.call_all("on_objref_track", zoomer) self.refs[zoomable] = zoomer def on_zoomable_widget_destroy(self, zoomable, adjustment): if zoomable in self.refs: self.refs.pop(zoomable) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/icon/000077500000000000000000000000001417573700700227335ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/icon/Makefile000066400000000000000000000004601417573700700243730ustar00rootroot00000000000000all: data out/paperwork_%.png: data/paperwork.svg convert -background None $(CURDIR)/$< -resize $*x$* $(CURDIR)/$@ data: \ out/paperwork_16.png \ out/paperwork_32.png \ out/paperwork_48.png \ out/paperwork_64.png \ out/paperwork_128.png clean: rm -f $(CURDIR)/out/*.png .PHONY: all data clean paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/icon/__init__.py000066400000000000000000000010361417573700700250440ustar00rootroot00000000000000import openpaperwork_core class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['icon'] def get_deps(self): return [ { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, ] def icon_get_pixbuf(self, icon_name, size_px): file_name = "{}_{}.png".format(icon_name, size_px) return self.core.call_success( "gtk_load_pixbuf", "paperwork_gtk.icon.out", file_name ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/icon/data/000077500000000000000000000000001417573700700236445ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/icon/data/paperwork.svg000066400000000000000000000056651417573700700264130ustar00rootroot00000000000000 image/svg+xml paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/icon/data/paperwork_halo.svg000066400000000000000000000100101417573700700273720ustar00rootroot00000000000000 image/svg+xml paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/icon/out/000077500000000000000000000000001417573700700235425ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/icon/out/__init__.py000066400000000000000000000000001417573700700256410ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/keyboard_shortcut/000077500000000000000000000000001417573700700255365ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/keyboard_shortcut/__init__.py000066400000000000000000000000001417573700700276350ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/keyboard_shortcut/zoom.py000066400000000000000000000051151417573700700270760ustar00rootroot00000000000000import logging try: import gi gi.require_version('Gdk', '3.0') from gi.repository import Gdk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_gtk.deps LOGGER = logging.getLogger(__name__) class Zoomer(object): ZOOM_INCREMENT = 0.02 def __init__(self, zoomable, adjustment): self.adjustment = adjustment if zoomable.get_window() is not None: self._enable_events(zoomable) else: zoomable.connect("realize", self._enable_events) zoomable.connect("scroll-event", self._on_scroll) def _enable_events(self, zoomable): zoomable.add_events(Gdk.EventMask.SCROLL_MASK) zoomable.get_window().set_events( zoomable.get_window().get_events() | Gdk.EventMask.SCROLL_MASK ) def _on_scroll(self, widget, event): if (event.state & Gdk.ModifierType.CONTROL_MASK): original = self.adjustment.get_value() delta = event.get_scroll_deltas()[2] if delta < 0: zoom = original + self.ZOOM_INCREMENT elif delta > 0: zoom = original - self.ZOOM_INCREMENT else: return False LOGGER.debug("Ctrl + Scrolling: zoom %f --> %f", original, zoom) self.adjustment.set_value(zoom) return True # don't know what to do, don't care. Let someone else take care of it return False class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() # XXX(Jflesch): Looks like a GObject Introspection bug: if the # Zoomer objects get garbage-collected, the signal isn't handled # anymore. --> We need to keep a reference to those objects as long # as we need them. self.refs = {} def get_interfaces(self): return [ 'chkdeps', 'gtk_zoomable', ] def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def on_zoomable_widget_new(self, zoomable, adjustment): """ zoomable is the widget on which user can zoom. zoom gestures are not applied directly on the widget. They are applied on a GtkAdjustment. """ zoomer = Zoomer(zoomable, adjustment) self.core.call_all("on_objref_track", zoomer) self.refs[zoomable] = zoomer def on_zoomable_widget_destroy(self, zoomable, adjustment): if zoomable in self.refs: self.refs.pop(zoomable) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/l10n/000077500000000000000000000000001417573700700225555ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/l10n/__init__.py000066400000000000000000000007251417573700700246720ustar00rootroot00000000000000import openpaperwork_core class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['l10n_init'] def get_deps(self): return [ { 'interface': 'l10n', 'defaults': ['openpaperwork_core.l10n.python'], }, ] def init(self, core): super().init(core) self.core.call_all( "l10n_load", "paperwork_gtk.l10n", "paperwork_gtk" ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/main.py000066400000000000000000000213711417573700700233050ustar00rootroot00000000000000import argparse import logging import sys import openpaperwork_core import openpaperwork_gtk import paperwork_backend # this import must be non-relative due to cx_freeze running this .py # as an independant Python script from paperwork_gtk import _ LOGGER = logging.getLogger(__name__) DEFAULT_GUI_PLUGINS = ( paperwork_backend.DEFAULT_PLUGINS + openpaperwork_gtk.GUI_PLUGINS + [ 'openpaperwork_core.spatial.rtree', 'openpaperwork_gtk.drawer.pillow', 'openpaperwork_gtk.drawer.scan', 'openpaperwork_gtk.gesture.autoscrolling', 'paperwork_backend.docscan.autoselect_scanner', 'paperwork_backend.guesswork.cropping.calibration', 'paperwork_gtk.about', 'paperwork_gtk.actions.app.find', 'paperwork_gtk.actions.app.help', 'paperwork_gtk.actions.app.open_about', 'paperwork_gtk.actions.app.open_bug_report', 'paperwork_gtk.actions.app.open_settings', 'paperwork_gtk.actions.app.open_shortcuts', 'paperwork_gtk.actions.doc.add_to_selection', 'paperwork_gtk.actions.doc.delete', 'paperwork_gtk.actions.doc.export', 'paperwork_gtk.actions.doc.new', 'paperwork_gtk.actions.doc.open_external', 'paperwork_gtk.actions.doc.prev_next', 'paperwork_gtk.actions.doc.print', 'paperwork_gtk.actions.doc.properties', 'paperwork_gtk.actions.doc.redo_ocr', 'paperwork_gtk.actions.docs.delete', 'paperwork_gtk.actions.docs.export', 'paperwork_gtk.actions.docs.properties', 'paperwork_gtk.actions.docs.redo_ocr', 'paperwork_gtk.actions.docs.select_all', 'paperwork_gtk.actions.page.copy_text', 'paperwork_gtk.actions.page.delete', 'paperwork_gtk.actions.page.edit', 'paperwork_gtk.actions.page.export', 'paperwork_gtk.actions.page.move_inside_doc', 'paperwork_gtk.actions.page.move_to_doc', 'paperwork_gtk.actions.page.print', 'paperwork_gtk.actions.page.redo_ocr', 'paperwork_gtk.actions.page.reset', 'paperwork_gtk.cmd.import', 'paperwork_gtk.cmd.install', 'paperwork_gtk.docimport', 'paperwork_gtk.doc_selection', 'paperwork_gtk.drawer.calibration', 'paperwork_gtk.drawer.frame', 'paperwork_gtk.gesture.drag_and_drop', 'paperwork_gtk.gesture.zoom', 'paperwork_gtk.icon', 'paperwork_gtk.keyboard_shortcut.zoom', 'paperwork_gtk.l10n', 'paperwork_gtk.mainwindow.doclist', 'paperwork_gtk.mainwindow.doclist.labeler', 'paperwork_gtk.mainwindow.doclist.name', 'paperwork_gtk.mainwindow.doclist.thumbnailer', 'paperwork_gtk.mainwindow.docproperties', 'paperwork_gtk.mainwindow.docproperties.extra_text', 'paperwork_gtk.mainwindow.docproperties.labels', 'paperwork_gtk.mainwindow.docproperties.name', 'paperwork_gtk.mainwindow.docview', 'paperwork_gtk.mainwindow.docview.controllers.autoscrolling', 'paperwork_gtk.mainwindow.docview.controllers.click', 'paperwork_gtk.mainwindow.docview.controllers.drop', 'paperwork_gtk.mainwindow.docview.controllers.empty_doc', 'paperwork_gtk.mainwindow.docview.controllers.layout', 'paperwork_gtk.mainwindow.docview.controllers.page_number', 'paperwork_gtk.mainwindow.docview.controllers.scroll', 'paperwork_gtk.mainwindow.docview.controllers.title', 'paperwork_gtk.mainwindow.docview.controllers.zoom', 'paperwork_gtk.mainwindow.docview.drag', 'paperwork_gtk.mainwindow.docview.pageadd.buttons', 'paperwork_gtk.mainwindow.docview.pageadd.import', 'paperwork_gtk.mainwindow.docview.pageadd.scan', 'paperwork_gtk.mainwindow.docview.pageadd.source_popover', 'paperwork_gtk.mainwindow.docview.pageinfo', 'paperwork_gtk.mainwindow.docview.pageinfo.actions', 'paperwork_gtk.mainwindow.docview.pageinfo.layout_settings', 'paperwork_gtk.mainwindow.docview.pageprocessing', 'paperwork_gtk.mainwindow.docview.pageview', 'paperwork_gtk.mainwindow.docview.pageview.boxes', 'paperwork_gtk.mainwindow.docview.pageview.boxes.all', 'paperwork_gtk.mainwindow.docview.pageview.boxes.hover', 'paperwork_gtk.mainwindow.docview.pageview.boxes.search', 'paperwork_gtk.mainwindow.docview.pageview.boxes.selection', 'paperwork_gtk.mainwindow.docview.progress', 'paperwork_gtk.mainwindow.docview.scanview', 'paperwork_gtk.mainwindow.exporter', 'paperwork_gtk.mainwindow.home', 'paperwork_gtk.mainwindow.pageeditor', 'paperwork_gtk.mainwindow.search.advanced', 'paperwork_gtk.mainwindow.search.field', 'paperwork_gtk.mainwindow.search.suggestions', 'paperwork_gtk.mainwindow.window', 'paperwork_gtk.menus.app.help', 'paperwork_gtk.menus.app.open_about', 'paperwork_gtk.menus.app.open_bug_report', 'paperwork_gtk.menus.app.open_settings', 'paperwork_gtk.menus.app.open_shortcuts', 'paperwork_gtk.menus.doc.add_to_selection', 'paperwork_gtk.menus.doc.delete', 'paperwork_gtk.menus.doc.export', 'paperwork_gtk.menus.doc.open_external', 'paperwork_gtk.menus.doc.print', 'paperwork_gtk.menus.doc.properties', 'paperwork_gtk.menus.doc.redo_ocr', 'paperwork_gtk.menus.docs.delete', 'paperwork_gtk.menus.docs.export', 'paperwork_gtk.menus.docs.properties', 'paperwork_gtk.menus.docs.redo_ocr', 'paperwork_gtk.menus.docs.select_all', 'paperwork_gtk.menus.page.copy_text', 'paperwork_gtk.menus.page.delete', 'paperwork_gtk.menus.page.export', 'paperwork_gtk.menus.page.move_inside_doc', 'paperwork_gtk.menus.page.move_to_doc', 'paperwork_gtk.menus.page.print', 'paperwork_gtk.menus.page.redo_ocr', 'paperwork_gtk.menus.page.reset', 'paperwork_gtk.model.help', 'paperwork_gtk.model.help.intro', 'paperwork_gtk.new_doc', 'paperwork_gtk.notifications.dialog', 'paperwork_gtk.notifications.notify', 'paperwork_gtk.print', 'paperwork_gtk.settings', 'paperwork_gtk.settings.ocr.selector_popover', 'paperwork_gtk.settings.ocr.settings', 'paperwork_gtk.settings.scanner.calibration', 'paperwork_gtk.settings.scanner.dev_id_popover', 'paperwork_gtk.settings.scanner.flatpak', 'paperwork_gtk.settings.scanner.mode_popover', 'paperwork_gtk.settings.scanner.resolution_popover', 'paperwork_gtk.settings.scanner.settings', 'paperwork_gtk.settings.stats', 'paperwork_gtk.settings.storage', 'paperwork_gtk.settings.update', 'paperwork_gtk.shortcuts.app.find', 'paperwork_gtk.shortcuts.doc.new', 'paperwork_gtk.shortcuts.doc.prev_next', 'paperwork_gtk.shortcuts.doc.print', 'paperwork_gtk.shortcuts.doc.properties', 'paperwork_gtk.shortcuts.page.copy_text', 'paperwork_gtk.shortcuts.page.edit', 'paperwork_gtk.shortcutswin', 'paperwork_gtk.sync_on_start', 'paperwork_gtk.update_notification', 'paperwork_gtk.widget.flowlayout', 'paperwork_gtk.widget.label', ] ) def main_main(in_args): # To load the plugins, we need first to load the configuration plugin # to get the list of plugins to load. # The configuration plugin may write traces using logging, so we better # enable and configure the plugin logs.print first. core = openpaperwork_core.Core() # plugin 'uncaught_exceptions' requires a mainloop plugin core.load('openpaperwork_gtk.mainloop.glib') for module_name in paperwork_backend.DEFAULT_CONFIG_PLUGINS: core.load(module_name) core.init() core.call_all( "init_logs", "paperwork-gtk", "info" if len(in_args) <= 0 else "warning" ) core.call_all("config_load") core.call_all("config_load_plugins", "paperwork-gtk", DEFAULT_GUI_PLUGINS) if len(in_args) <= 0: core.call_all("on_initialized") LOGGER.info("Ready") core.call_one("mainloop", halt_on_uncaught_exception=False) LOGGER.info("Quitting") core.call_all("config_save") core.call_all("on_quit") else: parser = argparse.ArgumentParser() cmd_parser = parser.add_subparsers( help=_('command'), dest='command', required=True ) core.call_all("cmd_complete_argparse", cmd_parser) args = parser.parse_args(in_args) core.call_all("cmd_set_interactive", True) core.call_all("cmd_run", args) core.call_all("on_quit") def main(): main_main(sys.argv[1:]) if __name__ == "__main__": # Do not remove. Cx_freeze goes throught here main() paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/000077500000000000000000000000001417573700700241575ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/__init__.py000066400000000000000000000000001417573700700262560ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/000077500000000000000000000000001417573700700256205ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/__init__.py000066400000000000000000000525271417573700700277440ustar00rootroot00000000000000import datetime import logging import time try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps import openpaperwork_gtk.deps from ... import _ LOGGER = logging.getLogger(__name__) # GtkListBox doesn't scale well with too many elements # --> by default we only display 50 documents, and only extend the list # as needed NB_DOCS_PER_PAGE = 50 class Plugin(openpaperwork_core.PluginBase): PRIORITY = -100 def __init__(self): super().__init__() self.widget_tree = None self.doclist = None self.scrollbar = None self._scrollbar_last_value = -1 self.docs = [] self.doc_visibles = 0 self.row_to_doc = {} self.docid_to_row = {} self.docid_to_widget_tree = {} self.last_date = datetime.datetime(year=1, month=1, day=1) self.active_doc = (None, None) self.previous_doc = (None, None) self.main_actions = [] self.doc_actions = None self.selection_multiple = False # the following boolean is a dirty hack to ignore the signal 'toggle' # when we toggle the checkbox ourselves (if another plugin # calls doc_selection_add() for instance) self._toggling = False def get_interfaces(self): return [ 'chkdeps', 'doc_actions', 'doc_open', 'doc_selection', 'docs_actions', 'drag_and_drop_destination', 'gtk_app_menu', 'gtk_doclist', 'screenshot_provider', 'search_listener', ] def get_deps(self): return [ { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'gtk_drag_and_drop', 'defaults': ['paperwork_gtk.gesture.drag_and_drop'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'gtk_mainwindow', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'gtk_widget_flowlayout', 'defaults': ['paprwork_gtk.widget.flowlayout'], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'screenshot', 'defaults': ['openpaperwork_gtk.screenshots'], }, ] def init(self, core): super().init(core) self.core.call_success( "gtk_load_css", "paperwork_gtk.mainwindow.doclist", "doclist.css" ) self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.doclist", "doclist.glade" ) if self.widget_tree is None: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to load widget tree") return self.doc_actions = Gio.Menu.new() self.doclist = self.widget_tree.get_object("doclist_listbox") self.core.call_all( "mainwindow_add", side="left", name="doclist", prio=10000, header=self.widget_tree.get_object("doclist_header"), body=self.widget_tree.get_object("doclist_body"), ) self.core.call_all( "mainwindow_add", side="left", name="doclist_selection_multiple", prio=-100000, header=self.widget_tree.get_object( "doclist_header_selection_multiple" ), body=None ) self.vadj = self.widget_tree.get_object( "doclist_scroll" ).get_vadjustment() self.vadj.connect("value-changed", self._on_scrollbar_value_changed) self.doclist.connect("row-activated", self._on_row_activated) self.doclist.connect("drag-motion", self._on_drag_motion) self.doclist.connect("drag-leave", self._on_drag_leave) self.widget_tree.get_object("doclist_new_doc").connect( "clicked", self._on_new_doc ) self.widget_tree.get_object( "doclist_back_to_selection_single" ).connect("clicked", lambda button: self.core.call_all( "gtk_switch_to_doc_selection_single" )) self.menu_model = self.widget_tree.get_object("doclist_menu_model") self.selection_multiple_menu_model = self.widget_tree.get_object( "doclist_selection_multiple_menu_model" ) self.core.call_all("drag_and_drop_page_enable", self.doclist) self.core.call_one( "mainloop_schedule", self.core.call_all, "on_doclist_initialized" ) self.on_mainwindow_fold_change() def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def on_initialized(self): self.core.call_all( "mainwindow_show", side="left", name="doclist" ) def doclist_add(self, widget, vposition): body = self.widget_tree.get_object("doclist_body") body.add(widget) body.reorder_child(widget, vposition) def _on_new_doc(self, button): self.doc_open_new() def doc_open_new(self): new_doc = self.core.call_success("get_new_doc") self.core.call_all("doc_open", *new_doc) self.doclist_show(self.docs, show_new=True) def _doclist_clear(self): start = time.time() for child in self.doclist.get_children(): self.doclist.remove(child) stop = time.time() LOGGER.info( "%d documents cleared in %dms", self.doc_visibles, (stop - start) * 1000 ) def doclist_clear(self): self._doclist_clear() self.last_date = datetime.datetime(year=1, month=1, day=1) self.doc_visibles = 0 self.row_to_doc = {} self.docid_to_row = {} self.docid_to_widget_tree = {} def _add_date_box(self, name, txt): widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.doclist", name ) widget_tree.get_object("date_label").set_text(txt) row = widget_tree.get_object("date_box") self.doclist.insert(row, -1) def _toggle_doc(self, button, doc_id, doc_url): if button.get_active(): self.core.call_all("doc_selection_add", doc_id, doc_url) else: self.core.call_all("doc_selection_remove", doc_id, doc_url) def _add_doc_box(self, doc_id, doc_url, box="doc_box.glade", new=False): widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.doclist", box ) doc_box = widget_tree.get_object("doc_box") flowlayout = self.core.call_success( "gtk_widget_flowlayout_new", spacing=(3, 3) ) flowlayout.set_visible(True) doc_box.pack_start(flowlayout, expand=True, fill=True, padding=0) doc_box.reorder_child(flowlayout, 1) self.core.call_all( "on_doc_box_creation", doc_id, widget_tree, flowlayout ) doc_actions = widget_tree.get_object("doc_actions") if new: doc_actions.set_visible(False) else: widget_tree.get_object("doc_actions_menu").set_menu_model( self.doc_actions ) for action in self.main_actions: button = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.doclist", "main_action.glade" ) button.get_object("doc_main_action_image").set_from_icon_name( action['icon_name'], Gtk.IconSize.MENU ) button.get_object("doc_main_action").set_tooltip_text( action['txt'] ) button.get_object("doc_main_action").connect( "clicked", lambda _: action['callback']() ) widget_tree.get_object("doc_actions").pack_start( button.get_object("doc_main_action"), expand=True, fill=True, padding=0 ) widget_tree.get_object("doc_box_selector").set_visible( self.selection_multiple ) widget_tree.get_object("doc_box_selector").set_active( self.core.call_success("doc_selection_in", doc_id, doc_url) is not None ) widget_tree.get_object("doc_box_selector").connect( "toggled", self._toggle_doc, doc_id, doc_url ) row = widget_tree.get_object("doc_listbox") self.row_to_doc[row] = (doc_id, doc_url) self.docid_to_row[doc_id] = row self.docid_to_widget_tree[doc_id] = widget_tree self.doclist.insert(row, -1) def doclist_extend(self, nb_docs): start = time.time() # FIXME: show month only when there are too many documents. How many? show_month = True docs = self.docs[ self.doc_visibles:self.doc_visibles + nb_docs ] LOGGER.info( "Adding %d documents to the document list (%d-%d)", len(docs), self.doc_visibles, self.doc_visibles + nb_docs ) for (doc_id, doc_url) in docs: doc_date = self.core.call_success("doc_get_date_by_id", doc_id) if doc_date is not None: if not show_month: if doc_date.year != self.last_date.year: doc_year = self.core.call_success( "i18n_date_long_year", doc_date ) self._add_date_box("year_box.glade", doc_year) else: if (doc_date.year != self.last_date.year or doc_date.month != self.last_date.month): doc_year = self.core.call_success( "i18n_date_long_year", doc_date ) + " / " + self.core.call_success( "i18n_date_long_month", doc_date ) self._add_date_box("year_box.glade", doc_year) self.last_date = doc_date self._add_doc_box(doc_id, doc_url) self.doc_visibles = min(len(self.docs), self.doc_visibles + nb_docs) stop = time.time() LOGGER.info( "%d documents shown in %dms (%d displayable)", len(docs), (stop - start) * 1000, len(self.docs) ) return len(docs) def doclist_show(self, docs, show_new=True): self.doclist_clear() scroll = (self.docs != docs) self.docs = docs if show_new: new_doc = self.core.call_success("get_new_doc") self._add_doc_box(*new_doc, new=True) self.doclist_extend(NB_DOCS_PER_PAGE) self._reselect_current_doc(scroll=scroll) def on_search_results(self, query, docs): self.doclist_show(docs, show_new=(query == "")) def doc_close(self): self.active_doc = (None, None) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) self._reselect_current_doc(scroll=False) def _show_doc_id(self, doc_id): row = self.docid_to_row.get(doc_id) while row is None: if self.doclist_extend(NB_DOCS_PER_PAGE) <= 0: break row = self.docid_to_row.get(doc_id) return row def _reselect_current_doc(self, scroll=True): (doc_id, doc_url) = self.active_doc if doc_id not in {doc[0] for doc in self.docs}: LOGGER.info( "Document %s not found in the document list", doc_id ) self.vadj.set_value(self.vadj.get_lower()) return row = self._show_doc_id(doc_id) assert(row is not None) self.doclist.select_row(row) if (self.previous_doc[0] is not None and self.previous_doc[0] in self.docid_to_widget_tree): widget_tree = self.docid_to_widget_tree[self.previous_doc[0]] widget_tree.get_object("doc_actions").set_visible(False) if ((not self.selection_multiple) and self.core.call_success("is_doc", doc_url) is not None and doc_id in self.docid_to_widget_tree): widget_tree = self.docid_to_widget_tree[doc_id] widget_tree.get_object("doc_actions").set_visible(True) self.previous_doc = self.active_doc if not scroll: return handler_id = None def scroll_to_row(row, allocation): adj = allocation.y adj -= self.vadj.get_page_size() / 2 adj += allocation.height / 2 min_val = self.vadj.get_lower() if adj < min_val: adj = min_val self.vadj.set_value(adj) row.disconnect(handler_id) handler_id = row.connect("size-allocate", scroll_to_row) def _on_scrollbar_value_changed(self, vadj): lower = vadj.get_lower() upper = vadj.get_upper() - lower value = ( vadj.get_value() + vadj.get_page_size() - lower ) / upper if value < 0.95: return if self._scrollbar_last_value == vadj.get_value(): # Previous extend call hasn't been taken into account yet return self.doclist_extend(NB_DOCS_PER_PAGE) self._scrollbar_last_value = vadj.get_value() def _doc_open(self, doc_id, doc_url): if self.active_doc[0] == doc_id: return LOGGER.info("Opening document %s (%s)", doc_id, doc_url) self.core.call_all("doc_open", doc_id, doc_url) def _on_row_activated(self, list_box, row): (doc_id, doc_url) = self.row_to_doc[row] self._doc_open(doc_id, doc_url) def menu_app_append_item(self, item): # they are actually the same menu self.doclist_menu_append_item(item) def menu_app_append_submenu(self, label, menu): # they are actually the same menu self.doclist_menu_append_submenu(label, menu) def doclist_menu_append_item(self, item): self.menu_model.append_item(item) def doclist_menu_append_submenu(self, label, menu): self.menu_model.append_submenu(label, menu) def docs_menu_append_item(self, item): self.selection_multiple_menu_model.append_item(item) def docs_menu_append_submenu(self, label, menu): self.selection_multiple_menu_model.append_submenu(label, menu) def add_doc_action(self, action_label, action_name): self.doc_actions.append(action_label, action_name) def add_doc_main_action(self, icon_name, txt, callback): self.main_actions.append({ "icon_name": icon_name, "txt": txt, "callback": callback, }) def _on_drag_motion(self, widget, drag_context, x, y, time): widget.drag_unhighlight_row() row = widget.get_row_at_y(y) if row is not None: widget.drag_highlight_row(row) def _on_drag_leave(self, widget, drag_context, time): widget.drag_unhighlight_row() def drag_and_drop_get_destination(self, widget, x, y): if self.doclist != widget: return None row = self.doclist.get_row_at_y(y) if row is None: LOGGER.warning("No row at %d. Can't get drop destination", y) return None (doc_id, doc_url) = self.row_to_doc[row] nb_pages = self.core.call_success("doc_get_nb_pages_by_url", doc_url) if nb_pages is None: nb_pages = 0 return (doc_id, doc_url, nb_pages) def gtk_open_app_menu(self): self.widget_tree.get_object("doclist_menu").clicked() def screenshot_snap_app_menu(self, out_file): widget = self.widget_tree.get_object("doclist_menu") if widget.get_popover() is not None: popover = widget.get_popover() if popover.get_visible() and popover.is_drawable(): widget = popover self.core.call_success( "screenshot_snap_widget", widget, out_file, margins=(50, 30, 50, 30) ) def screenshot_snap_all_doc_widgets(self, out_dir): self.core.call_success( "screenshot_snap_widget", self.widget_tree.get_object("doclist_new_doc"), self.core.call_success("fs_join", out_dir, "doc_new_button.png"), margins=(30, 30, 30, 30) ) self.screenshot_snap_app_menu( self.core.call_success("fs_join", out_dir, "app_menu.png") ) if self.active_doc[0] is None: return widget_tree = self.docid_to_widget_tree[self.active_doc[0]] self.core.call_success( "screenshot_snap_widget", widget_tree.get_object("doc_actions"), self.core.call_success( "fs_join", out_dir, "doc_properties_button.png" ), margins=(50, 50, 50, 50) ) def _set_all_selector_visibility(self, visible): for widget_tree in self.docid_to_widget_tree.values(): checkbox = widget_tree.get_object("doc_box_selector") checkbox.set_visible(visible) def gtk_switch_to_doc_selection_single(self): self.core.call_all( "mainwindow_show", side="left", name="doclist" ) self._set_all_selector_visibility(False) self.selection_multiple = False self._reselect_current_doc(scroll=False) def gtk_switch_to_doc_selection_multiple(self): self.core.call_all( "mainwindow_show", side="left", name="doclist_selection_multiple" ) if not self.selection_multiple: self.core.call_all("doc_selection_reset") self.selection_multiple = True self._set_all_selector_visibility(True) self._reselect_current_doc(scroll=False) def doc_selection_reset(self): self._update_count() self._toggling = True try: for widget_tree in self.docid_to_widget_tree.values(): checkbox = widget_tree.get_object("doc_box_selector") checkbox.set_active(False) finally: self._toggling = False def doc_selection_add(self, doc_id, doc_url): self._update_count() if self._toggling: return self._toggling = True try: widget_tree = self.docid_to_widget_tree.get(doc_id, None) if widget_tree is None: return checkbox = widget_tree.get_object("doc_box_selector") if not checkbox.get_active(): checkbox.set_active(True) finally: self._toggling = False def doc_selection_remove(self, doc_id, doc_url): self._update_count() if self._toggling: return self._toggling = True try: widget_tree = self.docid_to_widget_tree.get(doc_id, None) if widget_tree is None: return checkbox = widget_tree.get_object("doc_box_selector") if checkbox.get_active(): checkbox.set_active(False) finally: self._toggling = False def _update_count(self): count = self.core.call_success("doc_selection_len") if count is None: count = 0 self.widget_tree.get_object( "doclist_selection_multiple_nb_docs" ).set_text(_("%d documents") % count) def open_next_doc(self, offset=1): try: idx = self.docs.index(self.active_doc) idx += offset except ValueError: idx = 0 idx = max(idx, 0) idx = min(idx, len(self.docs)) if idx >= len(self.docs): # may happen with an empty doc list return (doc_id, doc_url) = self.docs[idx] self.core.call_all("doc_open", doc_id, doc_url) self._reselect_current_doc(scroll=True) def doc_menu_open(self): if self.active_doc[0] is None: return doc_id = self.active_doc[0] widget_tree = self.docid_to_widget_tree[doc_id] button = widget_tree.get_object("doc_actions_menu") button.clicked() def screenshot_snap_doc_action_menu(self, out_file): if self.active_doc[0] is None: return doc_id = self.active_doc[0] widget_tree = self.docid_to_widget_tree[doc_id] button = widget_tree.get_object("doc_actions_menu") self.core.call_success( "screenshot_snap_widget", button, out_file, margins=(100, 50, 100, 200) ) def on_mainwindow_fold_change(self): folded = self.core.call_success("mainwindow_get_folded") self.widget_tree.get_object("doclist_header").set_show_close_button( folded ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doc_box.glade000066400000000000000000000072071417573700700302410ustar00rootroot00000000000000 True True False horizontal True False False False none end 2 2 2 2 True False x-office-document-symbolic 3 start False False vertical False True False False none True False view-more-symbolic 1 end True True start False True paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.css000066400000000000000000000036601417573700700300000ustar00rootroot00000000000000#doclist_body spinner { background-color: rgba(0, 0, 0, 0.25); } #doclist_scroll button image { border: 0px; } .search_field { padding: 12px 12px 10px 12px; } .search-left, .search-right { border: 1px solid shade(@theme_bg_color, 0.88); background-color: shade(@theme_bg_color, 0.88); } .suggestions_box { padding: 0px 12px 4px 12px; } .suggestions_label { margin: 5px; } .suggestions_list { border: 1px solid shade(@theme_bg_color, 0.88); background-color: shade(@theme_bg_color, 0.88); } .date_label { color: mix(@theme_bg_color, @theme_fg_color, 0.70); font-size: 11pt; font-weight: 800; padding-bottom: 2px; margin: 12px 6px -2px 6px; } .border-bottom-light { padding-bottom: 5px; border-bottom: 1px solid shade(@theme_bg_color, 0.88); } .doclist { padding: 2px 6px 0px 6px; background-color: @theme_bg_color; } .doclist_item { padding: 4px 6px 0px 6px; border: 1px solid @theme_bg_color; border-radius: 6px; color: @theme_fg_color; } .doclist_item:hover { margin-top: -2px; padding: 6px 6px 0px 6px; border: 1px solid shade(@theme_bg_color, 0.88); background-color: shade(@theme_bg_color, 0.88); color: @theme_fg_color; } .doclist_item:selected { margin-top: -2px; padding: 6px 6px 0px 6px; border: 1px solid shade(@theme_bg_color, 0.75); background-color: mix(@theme_bg_color, @theme_base_color, 0.6); color: @theme_fg_color; } .doclist_item:selected image { color: mix(@theme_base_color, @theme_fg_color, 0.85); } .doclist_item:hover .border-bottom-light, .doclist_item:selected .border-bottom-light { border-bottom: 1px solid transparent; } .doclist_thumbnail { border: 1px solid shade(@theme_bg_color, 0.75); background-color: mix(@theme_bg_color, @theme_base_color, 0.9); color: mix(@theme_base_color, @theme_fg_color, 0.85); } .doclist_item label { margin-left: 2px; color: mix(@theme_base_color, @theme_fg_color, 0.85); font-size: 11pt; font-weight: normal; } paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/doclist.glade000066400000000000000000000174141417573700700302660ustar00rootroot00000000000000
doclist_body True False False True vertical True False doclist_scroll True False in False True True False -1 True True 0 True False False Documents False True False False True True False list-add-symbolic 1 True True True doclist_menu_model True False open-menu-symbolic end 1 True False False False True False False True none True False go-previous-symbolic 1 True True True none doclist_selection_multiple_menu_model True False True False vertical True False Selection False True 0 True False 3 documents False True 1 False True 0 True False go-down-symbolic False True 1
paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/labeler.py000066400000000000000000000110321417573700700275750ustar00rootroot00000000000000import logging try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_gtk.deps LOGGER = logging.getLogger(__name__) class LabelingTask(object): def __init__(self, plugin, doc_id, doc_url, flowlayout): self.plugin = plugin self.core = plugin.core self.doc_id = doc_id self.doc_url = doc_url self.flowlayout = flowlayout def show_labels(self, labels): for widget in list(self.flowlayout.get_children()): if hasattr(widget, 'txt'): self.flowlayout.remove(widget) labels = [ ( self.core.call_success("i18n_strip_accents", label[0].lower()), label[0], label[1] ) for label in labels ] labels.sort() for label in labels: try: color = self.core.call_success("label_color_to_rgb", label[2]) except Exception as exc: LOGGER.warning( "Invalid label %s on document %s", label, self.doc_id, exc_info=exc ) continue widget = self.core.call_success( "gtk_widget_label_new", label[1], color ) widget.set_visible(True) self.flowlayout.add_child(widget, Gtk.Align.END) def get_promise(self): promise = openpaperwork_core.promise.Promise( self.core, LOGGER.debug, args=("Loading labels of document %s", self.doc_id,) ) promise = promise.then(lambda *args: None) # drop logger return value promises = [] self.core.call_all( "doc_get_labels_by_url_promise", promises, self.doc_url ) for p in promises: promise = promise.then(p) return promise.then(self.show_labels) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 100 def __init__(self): super().__init__() self.default_thumbnail = None self.running = False self.tasks = {} def get_interfaces(self): return [ 'gtk_doclist_listener', ] def get_deps(self): return [ { 'interface': 'doc_labels', 'defaults': ['paperwork_backend.model.labels'], }, { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.maindow.doclist'], }, { 'interface': 'gtk_widget_label', 'defaults': ['paperwork_gtk.widget.label'], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, { 'interface': 'work_queue', 'defaults': ['openpaperwork_core.work_queue.default'], }, ] def init(self, core): super().init(core) self.core.call_all("work_queue_create", "labeler", stop_on_quit=True) def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def doclist_show(self, docs): self.core.call_all("work_queue_cancel_all", "labeler") self.task = {} def on_doc_box_creation(self, doc_id, gtk_row, gtk_custom_flowlayout): doc_url = self.core.call_success("doc_id_to_url", doc_id) if doc_url is None: return task = LabelingTask(self, doc_id, doc_url, gtk_custom_flowlayout) self.tasks[doc_url] = task self.core.call_success( "work_queue_add_promise", "labeler", task.get_promise() ) def _refresh_doc(self, doc_url): if doc_url not in self.tasks: LOGGER.debug( "Labels on '%s' have changed, but it is not displayed at" " the moment", doc_url ) return LOGGER.info("Reloading labels of '%s'", doc_url) self.core.call_success( "work_queue_add_promise", "labeler", self.tasks[doc_url].get_promise() ) def doc_add_label_by_url(self, doc_url, label, color=None): self._refresh_doc(doc_url) def doc_remove_label_by_url(self, doc_url, label): self._refresh_doc(doc_url) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/main_action.glade000066400000000000000000000013361417573700700311020ustar00rootroot00000000000000 False True False False none True True False paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/month_box.glade000066400000000000000000000013461417573700700306170ustar00rootroot00000000000000 True False False True False December 0 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/name.py000066400000000000000000000034721417573700700271200ustar00rootroot00000000000000import logging try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_gtk.deps from ... import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 1000 def __init__(self): super().__init__() def get_interfaces(self): return [ 'chkdeps', 'gtk_doclist_listener', 'gtk_doclist_name', ] def get_deps(self): return [ { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.maindow.doclist'], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, ] def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def on_doc_box_creation(self, doc_id, gtk_row, custom_flowlayout): doc_url = self.core.call_success("doc_id_to_url", doc_id) is_new = doc_url is None or self.core.call_success( "is_doc", doc_url ) is None if is_new: doc_txt = _("New document") else: doc_date = self.core.call_success("doc_get_date_by_id", doc_id) if doc_date is not None: doc_txt = self.core.call_success("i18n_date_short", doc_date) else: doc_txt = doc_id label = Gtk.Label.new(doc_txt) label.set_visible(True) custom_flowlayout.add_child(label, Gtk.Align.START) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/thumbnailer.py000066400000000000000000000106741417573700700305140ustar00rootroot00000000000000import logging import PIL import openpaperwork_core from paperwork_backend.model.thumbnail import ( THUMBNAIL_HEIGHT, THUMBNAIL_WIDTH ) from ... import _ LOGGER = logging.getLogger(__name__) DELAY = 0.01 # placeholders size must include borders PLACEHOLDER_HEIGHT = THUMBNAIL_HEIGHT + 2 PLACEHOLDER_WIDTH = THUMBNAIL_WIDTH + 2 class ThumbnailTask(object): def __init__(self, plugin, doc_id, gtk_image): self.plugin = plugin self.core = plugin.core self.doc_id = doc_id self.gtk_image = gtk_image def set_thumbnail(self, img=None): if img is None: LOGGER.warning( "Failed to get thumbnail for document %s", self.doc_id ) return pixbuf = self.core.call_success("pillow_to_pixbuf", img) self.gtk_image.set_from_pixbuf(pixbuf) def get_promise(self): doc_url = self.core.call_success("doc_id_to_url", self.doc_id) if doc_url is None: return openpaperwork_core.promise.Promise(self.core) promise = openpaperwork_core.promise.Promise( self.core, LOGGER.debug, args=("Thumbnailing of document %s", self.doc_id,) ) promise = promise.then(lambda *args: None) # drop logger return value promise = promise.then( self.core.call_success("thumbnail_get_doc_promise", doc_url) ) promise = promise.then(self.set_thumbnail) return promise class Plugin(openpaperwork_core.PluginBase): PRIORITY = 100 def __init__(self): super().__init__() self.default_thumbnail = None self.running = False self.nb_loaded = 0 self.nb_to_load = 0 self._progress_str = None def get_interfaces(self): return [ 'gtk_doclist_listener', 'gtk_thumbnailer', ] def get_deps(self): return [ { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.maindow.doclist'], }, { 'interface': 'pillow_util', 'defaults': ['openpaperwork_core.pillow.util'], }, { 'interface': 'pixbuf_pillow', 'defaults': ['openpaperwork_gtk.pixbuf.pillow'], }, { 'interface': 'thumbnail', 'defaults': ['paperwork_backend.model.thumbnail'], }, { 'interface': 'work_queue', 'defaults': ['openpaperwork_core.work_queue.default'], }, ] def init(self, core): super().init(core) img = PIL.Image.new( "RGB", (THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT), color="#EEEEEE" ) self.default_thumbnail = self.core.call_success( "pillow_to_pixbuf", img ) self.core.call_all( "work_queue_create", "thumbnailer", stop_on_quit=True ) self._progress_str = _("Loading document thumbnails") def doclist_show(self, docs): self.core.call_all("work_queue_cancel_all", "thumbnailer") def on_doc_box_creation(self, doc_id, gtk_row, gtk_custom_flowlayout): gtk_img = gtk_row.get_object("doc_thumbnail") gtk_img.set_size_request(PLACEHOLDER_WIDTH, PLACEHOLDER_HEIGHT) gtk_img.set_visible(True) self.nb_to_load += 1 task = ThumbnailTask(self, doc_id, gtk_img) promise = task.get_promise() def _when_loaded(): self.nb_loaded += 1 self._update_progress() promise = promise.then(_when_loaded) # Gives back a bit of CPU time to GTK so the GUI remains # usable promise = promise.then(openpaperwork_core.promise.DelayPromise( self.core, DELAY )) self.core.call_success( "work_queue_add_promise", "thumbnailer", promise ) def _update_progress(self): assert(self.nb_to_load > 0) if self.nb_loaded > self.nb_to_load: self.nb_loaded = 0 self.nb_to_load = 0 self.core.call_all("on_progress", "thumbnailing", 1.0) return self.core.call_all( "on_progress", "thumbnailing", self.nb_loaded / self.nb_to_load, self._progress_str ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/doclist/year_box.glade000066400000000000000000000013721417573700700304310ustar00rootroot00000000000000 True False False True False 1985 0 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/000077500000000000000000000000001417573700700270415ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/__init__.py000066400000000000000000000151211417573700700311520ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.promise LOGGER = logging.getLogger(__name__) class DocPropertiesUpdate(object): def __init__(self, doc_id, multiple_docs=False): self.doc_id = doc_id self.multiple_docs = multiple_docs self.new_docs = set() self.upd_docs = set() self.del_docs = set() class DocPropertiesEditor(object): def __init__(self, name, plugin, multiple_docs=False): self.name = name self.core = plugin.core self.plugin = plugin self.multiple_docs = multiple_docs self.active_docs = set() self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.docproperties", "docproperties.glade" ) if self.widget_tree is None: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to load widget tree") return self.widget_tree.get_object("docproperties_back").connect( "clicked", self._apply ) self.widget_tree.get_object("docproperties_cancel").connect( "clicked", self._cancel ) self.core.call_all( "mainwindow_add", side="left", name="docproperties_" + self.name, prio=0, header=self.widget_tree.get_object("docproperties_header"), body=self.widget_tree.get_object("docproperties_body"), ) self.core.call_one( "mainloop_schedule", self._build_doc_properties, multiple_docs ) def show(self): self.core.call_all( "mainwindow_show", side="left", name="docproperties_" + self.name ) def _build_doc_properties(self, multiple_docs): components = [] self.core.call_all( "doc_properties_components_get", components, multiple_docs=multiple_docs ) for component in components: self.widget_tree.get_object("docproperties_box").pack_start( component, expand=False, fill=True, padding=0 ) def doc_open(self, doc_id, doc_url): self.active_docs = {(doc_id, doc_url)} self.core.call_all( "doc_properties_components_set_active_doc", doc_id, doc_url ) def docs_open(self, docs): self.active_docs = docs self.core.call_all( "doc_properties_components_set_active_docs", docs ) def _open_doc(self, upd): active_docs = {doc[0] for doc in self.active_docs} if upd.doc_id is not None and upd.doc_id not in active_docs: doc_url = self.core.call_success("doc_id_to_url", upd.doc_id) if doc_url is None: return self.core.call_all("doc_open", upd.doc_id, doc_url) def _apply(self, *args, **kwargs): LOGGER.info( "Changes validated by the user (multiple_docs=%s)", self.multiple_docs ) upd = DocPropertiesUpdate( list(self.active_docs)[0][0], self.multiple_docs ) promise = openpaperwork_core.promise.ThreadedPromise( self.core, self.core.call_all, args=("doc_properties_components_apply_changes", upd) ) # drop call_all return value promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(self._open_doc, upd) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then( self.core.call_all, "search_update_document_list" ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(self._upd_index, upd) promise.schedule() self.core.call_all("mainwindow_back", side="left") def _cancel(self, *args, **kwargs): LOGGER.info("Changes cancelled by the user") self.core.call_all("doc_properties_components_cancel_changes") self.core.call_all("mainwindow_back", side="left") def _upd_index(self, upd): total = len(upd.new_docs) + len(upd.upd_docs) + len(upd.del_docs) if total <= 0: LOGGER.info("Document %s not modified. Nothing to do", upd.doc_id) return LOGGER.info( "Document %s modified. %d documents impacted", upd.doc_id, total ) changes = [] for doc_id in upd.new_docs: changes.append(('add', doc_id)) for doc_id in upd.upd_docs: changes.append(('upd', doc_id)) for doc_id in upd.del_docs: changes.append(('del', doc_id)) self.core.call_success("transaction_simple", changes) def docproperties_scroll_to_last(self): scroll = self.widget_tree.get_object("docproperties_body") vadj = scroll.get_vadjustment() vadj.set_value(vadj.get_upper()) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.editors = {} def get_interfaces(self): return ['gtk_doc_properties'] def get_deps(self): return [ { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'gtk_mainwindow', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def init(self, core): super().init(core) self.editors = { 'single_doc': DocPropertiesEditor( "single", self, multiple_docs=False ), 'multiple_docs': DocPropertiesEditor( "multiple", self, multiple_docs=True ), } def doc_open(self, doc_id, doc_url): for e in self.editors.values(): e.doc_open(doc_id, doc_url) def open_doc_properties(self, doc_id, doc_url): for e in self.editors.values(): e.doc_open(doc_id, doc_url) self.editors['single_doc'].show() def open_docs_properties(self, docs): for e in self.editors.values(): e.docs_open(docs) self.editors['multiple_docs'].show() def docproperties_scroll_to_last(self): for e in self.editors.values(): e.docproperties_scroll_to_last() paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/docproperties.glade000066400000000000000000000035451417573700700327300ustar00rootroot00000000000000 True Properties False True False True True gtk-cancel start True False False True True gtk-apply end True False True False vertical docproperties_body paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/extra_text.glade000066400000000000000000000040211417573700700322230ustar00rootroot00000000000000 True False vertical 10 True False Additional keywords start False True 0 True True automatic automatic 100 True True True False True GTK_WRAP_WORD False True 1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/extra_text.py000066400000000000000000000063021417573700700316030ustar00rootroot00000000000000import logging import openpaperwork_core LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 750 def __init__(self): super().__init__() self.widget_tree = None self.active_doc = None def get_interfaces(self): return [ 'gtk_doc_property', 'screenshot_provider', ] def get_deps(self): return [ { 'interface': 'extra_text', 'defaults': ['paperwork_backend.model.extra_text'], }, { 'interface': 'gtk_doc_properties', 'defaults': ['paperwork_gtk.docproperties'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'screenshot', 'defaults': ['openpaperwork_gtk.screenshots'], }, ] def _get_widget_text(self): text_buffer = self.widget_tree.get_object("doctext_text").get_buffer() start = text_buffer.get_iter_at_offset(0) end = text_buffer.get_iter_at_offset(-1) return text_buffer.get_text(start, end, False) def doc_properties_components_get(self, out: list, multiple_docs=False): if multiple_docs: return self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.docproperties", "extra_text.glade" ) out.append(self.widget_tree.get_object("doctext")) def doc_properties_components_set_active_doc(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) txt = [] self.core.call_all("doc_get_extra_text_by_url", txt, doc_url) txt = "\n".join(txt) self.widget_tree.get_object("doctext_text").get_buffer().set_text(txt) def doc_properties_components_apply_changes(self, out): if out.multiple_docs: return # The document may have been renamed: use out.doc_id instead of # self.active_doc doc_id = out.doc_id doc_url = self.core.call_success("doc_id_to_url", doc_id) self.active_doc = (doc_id, doc_url) orig_txt = [] self.core.call_all( "doc_get_extra_text_by_url", orig_txt, doc_url ) orig_txt = "\n".join(orig_txt).strip() new_txt = self._get_widget_text() if new_txt == orig_txt: return LOGGER.info("Extra keywords have been changed in document %s", doc_id) self.core.call_all( "doc_set_extra_text_by_url", doc_url, new_txt ) out.upd_docs.add(doc_id) def doc_properties_components_cancel_changes(self): self.doc_properties_components_set_active_doc(*self.active_doc) def screenshot_snap_all_doc_widgets(self, out_dir): if self.widget_tree is None: return self.core.call_success( "screenshot_snap_widget", self.widget_tree.get_object("doctext_text"), self.core.call_success( "fs_join", out_dir, "doc_extra_text.png" ), margins=(50, 50, 50, 50) ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/label.glade000066400000000000000000000070261417573700700311230ustar00rootroot00000000000000 30 True False True False True False False half True False False True 0 True False False True Change the label color none False True 1 True False False none False False 2 True False False True Delete the label from all documents none True False edit-delete-symbolic 1 False True end 3 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.glade000066400000000000000000000076761417573700700313210ustar00rootroot00000000000000 True False vertical 10 True False Labels start False True 0 True True automatic never True True False True 1 True False 30 True False horizontal True False none False True 0 docproperties_new_label_entry True True True True True True 1 True False True False 1 list-add-symbolic False True 2 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/labels.py000066400000000000000000000547111417573700700306650ustar00rootroot00000000000000import enum import logging import re try: import gi gi.require_version('Gdk', '3.0') gi.require_version('Gtk', '3.0') from gi.repository import Gdk from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_gtk.deps import paperwork_backend.sync from ... import _ LOGGER = logging.getLogger(__name__) # forbid: # - empty strings # - strings that contain a comma RE_FORBIDDEN_LABELS = re.compile("(^$|.*,.*)") class LabelChange(enum.Enum): UNCHANGED = 0 ADDED = 1 REMOVED = 2 class LabelAction(object): def __init__(self, current_state): # current_state == None for multiple document selection self.current_state = current_state self.change = LabelChange.UNCHANGED def on_change(self): if self.current_state is None: # multi-docs mode : rotate over the 3 possible change values if self.change == LabelChange.UNCHANGED: self.change = LabelChange.ADDED elif self.change == LabelChange.ADDED: self.change = LabelChange.REMOVED elif self.change == LabelChange.REMOVED: self.change = LabelChange.UNCHANGED else: # single-doc mode if not self.current_state: if self.change == LabelChange.UNCHANGED: self.change = LabelChange.ADDED else: self.change = LabelChange.UNCHANGED else: if self.change == LabelChange.UNCHANGED: self.change = LabelChange.REMOVED else: self.change = LabelChange.UNCHANGED def get_image(self): if self.change == LabelChange.ADDED: return "list-add-symbolic" elif self.change == LabelChange.REMOVED: return "list-remove-symbolic" else: if self.current_state: return "object-select-symbolic" return None def __str__(self): return str((self.change, self.current_state)) class LabelEditor(object): def __init__(self, plugin): super().__init__() self.core = plugin.core self.plugin = plugin self.active_docs = [] # self.changed_labels contains the label that have been modified # (color or text) self.changed_labels = {} # self.toggled_labels indicates the labels that have been selected or # unselected (value = LabelAction, depending on whether they are # now selected or not) self.toggled_labels = {} # self.new_labels contains the labels that user has just created # (but are not yet applied on any document) self.new_labels = set() # self.deleted_labels contains the labels that the user wants to # remove from *all* the documents. self.deleted_labels = set() self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.docproperties", "labels.glade" ) color_widget = self.widget_tree.get_object("new_label_color") color_widget.set_rgba(self.core.call_success( "gtk_theme_get_color", "theme_bg_color" )) self.widget_tree.get_object("new_label_button").connect( "clicked", self._on_new_label ) new_label_entry = self.widget_tree.get_object("new_label_entry") new_label_entry.connect("activate", self._on_new_label) new_label_entry.connect("changed", self._on_label_txt_changed) def _update_button_img(self, toggle, label_action): img = label_action.get_image() if img is not None: image = Gtk.Image.new_from_icon_name(img, Gtk.IconSize.MENU) else: image = Gtk.Image() toggle.set_image(image) def _refresh_list(self): listbox = self.widget_tree.get_object("listbox_labels") for widget in list(listbox.get_children()): listbox.remove(widget) doc_labels = None if len(self.active_docs) == 1: doc_labels = set() active_doc = list(self.active_docs)[0] self.core.call_all( "doc_get_labels_by_url", doc_labels, active_doc[1] ) labels = set() self.core.call_all("labels_get_all", labels) if doc_labels is not None: labels.update(doc_labels) labels = [ ( self.core.call_success("i18n_strip_accents", label[0].lower()), label[0], label[1] ) for label in labels ] labels.sort() labels = [(label[1], label[2]) for label in labels] if doc_labels is not None: doc_labels = {doc_label[0] for doc_label in doc_labels} for old_label in labels: if old_label in self.changed_labels: new_label = self.changed_labels[old_label] else: new_label = old_label if old_label in self.deleted_labels: continue if old_label in self.toggled_labels: label_action = self.toggled_labels[old_label] else: if doc_labels is None: active = None else: active = old_label[0] in doc_labels label_action = LabelAction(active) widget_tree_label = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.docproperties", "label.glade" ) button = widget_tree_label.get_object("label_label") button.set_label(new_label[0]) button.connect("clicked", self._on_label_button_clicked, old_label) toggle = widget_tree_label.get_object("toggle_button") toggle.set_sensitive(True) self._update_button_img(toggle, label_action) toggle.connect("clicked", self._on_toggle, old_label, label_action) color = self.core.call_success("label_color_to_rgb", new_label[1]) color_button = widget_tree_label.get_object("color_button") color_button.set_sensitive(old_label not in self.new_labels) color_button.set_rgba(Gdk.RGBA(color[0], color[1], color[2], 1.0)) color_button.connect( "color-set", self._on_color_changed, old_label ) delete = widget_tree_label.get_object("delete_button") delete.connect("clicked", self._on_delete, old_label) listbox.add(widget_tree_label.get_object("label_row")) new_labels = list(self.new_labels) new_labels.sort() for label in new_labels: widget_tree_label = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.docproperties", "label.glade" ) button = widget_tree_label.get_object("label_label") button.set_label(label[0]) button.set_sensitive(False) toggle = widget_tree_label.get_object("toggle_button") toggle.set_sensitive(False) image = Gtk.Image.new_from_icon_name( "list-add-symbolic", Gtk.IconSize.MENU ) toggle.set_image(image) color = self.core.call_success("label_color_to_rgb", label[1]) color_button = widget_tree_label.get_object("color_button") color_button.set_sensitive(True) color_button.set_rgba(Gdk.RGBA(color[0], color[1], color[2], 1.0)) color_button.connect( "color-set", self._on_new_color_changed, label ) delete = widget_tree_label.get_object("delete_button") delete.connect("clicked", self._on_new_delete, label) listbox.add(widget_tree_label.get_object("label_row")) listbox.add(self.widget_tree.get_object("row_add_label")) def _on_label_button_clicked(self, label_button, label): original_label = label if label in self.changed_labels: label = self.changed_labels[label] self.core.call_success( "gtk_show_dialog_single_entry", self.plugin, _("Renaming label"), label[0], self, label_button, original_label, label ) def on_label_rename_response( self, new_label_txt, label_button, original_label, label): new_label_txt = new_label_txt.strip() if not self._check_label_name(new_label_txt): return LOGGER.info("Renaming label %s --> %s", label, new_label_txt) self.changed_labels[original_label] = (new_label_txt, label[1]) label_button.set_label(new_label_txt) def _on_toggle(self, button, label, label_action): label_action.on_change() self._update_button_img(button, label_action) self.toggled_labels[label] = label_action def _on_color_changed(self, button, original_label): color = button.get_rgba() color = (color.red, color.green, color.blue) color = self.core.call_success("label_color_from_rgb", color) label = original_label if original_label in self.changed_labels: label = self.changed_labels[original_label] new_label = (label[0], color) self.changed_labels[original_label] = new_label def _on_new_color_changed(self, button, original_label): self.new_labels.remove(original_label) color = button.get_rgba() color = (color.red, color.green, color.blue) color = self.core.call_success("label_color_from_rgb", color) new_label = (original_label[0], color) self.new_labels.add(new_label) self._refresh_list() def _on_delete(self, button, original_label): self.core.call_all( "gtk_show_dialog_yes_no", self, _( "Are you sure you want to delete label '%s'" " from ALL documents ?" ) % (original_label,), original_label, ) def on_dialog_yes_no_reply(self, origin, response, *args, **kwargs): if origin is not self: return if not response: LOGGER.info("Label delete canceled") return (original_label,) = args LOGGER.info("Will delete label %s on all documents", original_label) self.deleted_labels.add(original_label) self._refresh_list() def _on_new_delete(self, button, label): self.new_labels.remove(label) self._refresh_list() def _check_label_name(self, label_name): all_labels = set() self.core.call_all("labels_get_all", all_labels) for label in set(all_labels): if label in self.changed_labels: all_labels.add(self.changed_labels[label]) all_labels.update(self.new_labels) all_labels = {label[0] for label in all_labels} if label_name in all_labels: return False if RE_FORBIDDEN_LABELS.match(label_name): return False return True def _on_label_txt_changed(self, *args, **kwargs): entry = self.widget_tree.get_object("new_label_entry") button = self.widget_tree.get_object("new_label_button") txt = entry.get_text().strip() valid = self._check_label_name(txt) button.set_sensitive(valid) if valid or txt == "": self.core.call_all("gtk_entry_reset_colors", entry) else: self.core.call_all("gtk_entry_set_colors", entry) def _on_new_label(self, *args, **kwargs): text = self.widget_tree.get_object("new_label_entry").get_text() text = text.strip() if text == "": LOGGER.info("New label requested, but no text provided") return color_widget = self.widget_tree.get_object("new_label_color") color = color_widget.get_rgba() color = (color.red, color.green, color.blue) color = self.core.call_success("label_color_from_rgb", color) self.new_labels.add((text, color)) self._refresh_list() # reset fields self.widget_tree.get_object("new_label_entry").set_text("") color_widget.set_rgba(self.core.call_success( "gtk_theme_get_color", "theme_bg_color" )) def _make_new_labels(self, out, doc_id, doc_url): if len(self.new_labels) <= 0: return for label in self.new_labels: self.core.call_success( "doc_add_label_by_url", doc_url, label[0], label[1] ) out.upd_docs.add(doc_id) def _make_label_updates(self, out): if len(self.changed_labels) <= 0: return all_docs = [] self.core.call_all("storage_get_all_docs", all_docs) all_docs = set(all_docs) current = 0 total = len(self.changed_labels) * len(all_docs) for (doc_id, doc_url) in all_docs: for (old_label, new_label) in self.changed_labels.items(): LOGGER.info( "Changing label %s into %s on document %s", old_label, new_label, doc_id ) self.core.call_all( "on_progress", "label_upd", current / total, _( "Changing label {old_label} into {new_label}" " on document {doc_id}" ).format( old_label=old_label, new_label=new_label, doc_id=doc_id ) ) current += 1 doc_labels = set() self.core.call_all( "doc_get_labels_by_url", doc_labels, doc_url ) if old_label not in doc_labels: continue out.upd_docs.add(doc_id) self.core.call_all( "doc_remove_label_by_url", doc_url, old_label[0] ) self.core.call_success( "doc_add_label_by_url", doc_url, new_label[0], new_label[1] ) self.core.call_all("on_progress", "label_upd", 1.0) def _make_label_toggling(self, out, doc_id, doc_url): if len(self.toggled_labels) <= 0: return for (label, action) in self.toggled_labels.items(): doc_labels = set() self.core.call_all("doc_get_labels_by_url", doc_labels, doc_url) if action.change == LabelChange.ADDED: if label in doc_labels: continue self.core.call_success( "doc_add_label_by_url", doc_url, label[0], label[1] ) out.upd_docs.add(doc_id) elif action.change == LabelChange.REMOVED: if label not in doc_labels: continue self.core.call_all( "doc_remove_label_by_url", doc_url, label[0] ) out.upd_docs.add(doc_id) def _make_label_deletions(self, out): if len(self.deleted_labels) <= 0: return all_docs = [] self.core.call_all("storage_get_all_docs", all_docs) all_docs = set(all_docs) current = 0 total = len(self.deleted_labels) * len(all_docs) for (doc_id, doc_url) in all_docs: for old_label in self.deleted_labels: LOGGER.info( "Deleting label %s from document %s", old_label, doc_id ) self.core.call_all( "on_progress", "label_del", current / total, _( "Deleting label {old_label} from document {doc_id}" ).format(old_label=old_label, doc_id=doc_id) ) current += 1 doc_labels = set() self.core.call_all( "doc_get_labels_by_url", doc_labels, doc_url ) if old_label not in doc_labels: continue out.upd_docs.add(doc_id) self.core.call_all( "doc_remove_label_by_url", doc_url, old_label[0] ) self.core.call_all("on_progress", "label_del", 1.0) def doc_properties_components_set_active_doc(self, doc_id, doc_url): self.doc_properties_components_set_active_docs({(doc_id, doc_url)}) def doc_properties_components_set_active_docs(self, docs: set): self.active_docs = docs self.changed_labels = {} self.toggled_labels = {} self.new_labels = set() self.deleted_labels = set() new_label_entry = self.widget_tree.get_object("new_label_entry") new_label_entry.set_text("") self._refresh_list() def doc_properties_components_apply_changes(self, out): LOGGER.info("Selected/Unselected labels: %s", self.toggled_labels) LOGGER.info("Modified labels: %s", self.changed_labels) LOGGER.info("New labels: %s", self.new_labels) LOGGER.info("Deleted labels: %s", self.deleted_labels) if len(self.active_docs) == 1: # The document may have been renamed: use out.doc_id instead of # self.active_docs doc_id = out.doc_id doc_url = self.core.call_success("doc_id_to_url", doc_id) self.active_docs = {(doc_id, doc_url)} for (doc_id, doc_url) in self.active_docs: LOGGER.info("Updating document %s", doc_id) self._make_label_toggling(out, doc_id, doc_url) self._make_new_labels(out, doc_id, doc_url) if len(self.changed_labels) <= 0 and len(self.deleted_labels) <= 0: return self._make_label_deletions(out) self._make_label_updates(out) def doc_properties_components_cancel_changes(self): self.doc_properties_components_set_active_docs(self.active_docs) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 1000 def __init__(self): super().__init__() self.windows = [] self.editors = [] def get_interfaces(self): return [ 'chkdeps', 'gtk_doc_property', 'gtk_window_listener', 'screenshot_provider', 'syncable', ] def get_deps(self): return [ { 'interface': 'doc_labels', 'defaults': ['paperwork_backend.model.labels'], }, { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'gtk_colors', 'defaults': ['openpaperwork_gtk.colors'], }, { 'interface': 'gtk_dialog_yes_no', 'defaults': ['openpaperwork_gtk.dialogs.yes_no'], }, { 'interface': 'gtk_doc_properties', 'defaults': ['paperwork_gtk.docproperties'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, { 'interface': 'screenshot', 'defaults': ['openpaperwork_gtk.screenshots'], }, ] def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def init(self, core): super().init(core) if not GTK_AVAILABLE: # chkdeps() must still be callable return def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def doc_properties_components_get(self, out: list, multiple_docs=False): editor = LabelEditor(self) self.editors.append(editor) out.append(editor.widget_tree.get_object("listbox_global")) def on_dialog_yes_no_reply(self, origin, response, *args, **kwargs): for editor in self.editors: editor.on_dialog_yes_no_reply(origin, response, *args, **kwargs) def doc_properties_components_set_active_doc(self, doc_id, doc_url): for editor in self.editors: editor.doc_properties_components_set_active_doc(doc_id, doc_url) def doc_properties_components_set_active_docs(self, docs: set): for editor in self.editors: editor.doc_properties_components_set_active_docs(docs) def doc_properties_components_apply_changes(self, out): for editor in self.editors: editor.doc_properties_components_apply_changes(out) def doc_properties_components_cancel_changes(self): for editor in self.editors: editor.doc_properties_components_cancel_changes() def on_dialog_single_entry_reply( self, parent, r, new_value, *args, **kwargs): if parent is not self: return if not r: return editor = args[0] args = args[1:] editor.on_label_rename_response(new_value, *args, **kwargs) def doc_transaction_start(self, out: list, total_expected=-1): class RefreshTransaction(paperwork_backend.sync.BaseTransaction): priority = -100000 def commit(s): for editor in self.editors: self.core.call_one( "mainloop_schedule", editor._refresh_list ) out.append(RefreshTransaction(self.core, total_expected)) def sync(self, promises: list): pass def on_label_loading_end(self): for editor in self.editors: editor._refresh_list() def screenshot_snap_all_doc_widgets(self, out_dir): for editor in self.editors: self.core.call_success( "screenshot_snap_widget", editor.widget_tree.get_object("listbox_global"), self.core.call_success( "fs_join", out_dir, "doc_labels.png" ), margins=(10, 10, 10, -400) ) self.core.call_success( "screenshot_snap_widget", editor.widget_tree.get_object("new_label_button"), self.core.call_success( "fs_join", out_dir, "doc_new_label.png" ), margins=(400, 50, 50, 50) ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/name.glade000066400000000000000000000031641417573700700307630ustar00rootroot00000000000000 True False vertical 10 True False Date start False True 0 True True True True x-office-calendar-symbolic True False True 1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docproperties/name.py000066400000000000000000000117261417573700700303420ustar00rootroot00000000000000import logging import openpaperwork_core LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 10000 def __init__(self): super().__init__() self.widget_tree = None self.active_doc = None def get_interfaces(self): return [ 'gtk_doc_property', ] def get_deps(self): return [ { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'gtk_calendar_popover', 'defaults': ['openpaperwork_gtk.widgets.calendar'], }, { 'interface': 'gtk_doc_properties', 'defaults': ['paperwork_gtk.docproperties'], }, { 'interface': 'gtk_colors', 'defaults': ['openpaperwork_gtk.colors'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, ] def doc_properties_components_get(self, out: list, multiple_docs=False): if multiple_docs: return self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.docproperties", "name.glade" ) self.core.call_all( "gtk_calendar_add_popover", self.widget_tree.get_object("docname_entry") ) self.widget_tree.get_object("docname_entry").connect( "changed", self._on_doc_date_changed ) out.append(self.widget_tree.get_object("docname")) def doc_properties_components_set_active_doc(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) doc_date = self.core.call_success("doc_get_date_by_id", doc_id) if doc_date is not None: doc_txt = self.core.call_success("i18n_date_short", doc_date) else: doc_txt = doc_id self.widget_tree.get_object("docname_entry").set_text(doc_txt) def _check_doc_id(self, doc_id): if doc_id == "": return False for forbidden in ["/", "\\", "?", "*"]: if forbidden in doc_id: return False return True def _on_doc_date_changed(self, gtk_entry): txt = gtk_entry.get_text() txt = txt.strip() r = self.core.call_success("i18n_parse_date_short", txt) if r is not None: self.core.call_all("gtk_entry_reset_colors", gtk_entry) elif not self._check_doc_id(txt): self.core.call_all("gtk_entry_set_colors", gtk_entry, bg="#ff0000") else: self.core.call_all("gtk_entry_set_colors", gtk_entry, bg="#ee9000") def doc_properties_components_apply_changes(self, out): if out.multiple_docs: return doc_id = self.widget_tree.get_object("docname_entry").get_text() doc_id = doc_id.strip() doc_date = self.core.call_success("i18n_parse_date_short", doc_id) if doc_date is not None: doc_id = self.core.call_success("doc_get_id_by_date", doc_date) elif not self._check_doc_id(doc_id): LOGGER.error( "Invalid document id specified. Won't rename the document" ) return else: LOGGER.warning( "Failed to parse document date: %s. Using as is", doc_id ) orig_id = out.doc_id orig_date = self.core.call_success("doc_get_date_by_id", orig_id) if orig_date is not None: orig_date = orig_date.date() else: orig_date = orig_id dest_id = doc_id dest_date = self.core.call_success("doc_get_date_by_id", dest_id) if dest_date is not None: dest_date = dest_date.date() else: dest_date = dest_id if orig_date == dest_date: return LOGGER.info("Previous document id: %s (%s)", orig_id, orig_date) LOGGER.info("New document id: %s (%s)", dest_id, dest_date) orig_url = self.core.call_success("doc_id_to_url", orig_id) dest_url = self.core.call_success( "doc_id_to_url", dest_id, existing=False ) LOGGER.info("Renaming document %s into %s", orig_id, dest_id) dest_url = self.core.call_success( "doc_rename_by_url", orig_url, dest_url ) if not dest_url: LOGGER.warning("Renaming %s into %s failed", orig_id, dest_id) return dest_id = self.core.call_success("doc_url_to_id", dest_url) out.del_docs.add(out.doc_id) out.new_docs.add(dest_id) out.doc_id = dest_id def doc_properties_components_cancel_changes(self): self.doc_properties_components_set_active_doc(*self.active_doc) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/000077500000000000000000000000001417573700700256175ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/__init__.py000066400000000000000000000267421417573700700277430ustar00rootroot00000000000000import logging try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_gtk.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): LAYOUTS = { # name: pages per line (columns) 'paged': 1, 'grid': 3, } MAX_PAGES = max(LAYOUTS.values()) def __init__(self): super().__init__() self.controllers = {} self.widget_tree = None self.scroll = None self.page_layout = None self.pages = [] self.widget_to_page = {} self.page_to_widget = {} self.nb_columns = self.MAX_PAGES self.zoom = 0.0 self.layout_name = None self.requested_page_idx = 0 self.active_page_idx = 0 self.active_doc = (None, None) self.active_doc_mtime = None def get_interfaces(self): return [ 'chkdeps', 'doc_open', 'drag_and_drop_destination', 'gtk_docview', ] def get_deps(self): return [ { 'interface': 'gtk_mainwindow', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'gtk_zoomable', 'defaults': [ 'paperwork_gtk.gesture.zoom', 'paperwork_gtk.keyboard_shortcut.zoom', ], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, ] def init(self, core): super().init(core) self.core.call_success( "gtk_load_css", "paperwork_gtk.mainwindow.docview", "docview.css" ) self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.docview", "docview.glade" ) if self.widget_tree is None: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to load widget tree") return self.scroll = self.widget_tree.get_object("docview_scroll") self.scroll.get_vadjustment().connect( "value-changed", self._on_vscroll_value_changed ) self.scroll.get_vadjustment().connect( "changed", self._on_vscroll_changed ) self.overlay = self.widget_tree.get_object("docview_drawingarea") self.overlay.connect("draw", self._on_overlay_draw) self.page_layout = self.widget_tree.get_object("docview_page_layout") self.page_layout.connect( "size-allocate", self._on_layout_size_allocate ) self.page_layout.connect("child-activated", self._on_child_activated) self.page_layout.connect("drag-motion", self._on_drag_motion) self.page_layout.connect("drag-leave", self._on_drag_leave) self.page_layout.connect("draw", self._on_draw) self.core.call_all( "gtk_fix_headerbar_buttons", self.widget_tree.get_object("docview_header") ) self.on_mainwindow_fold_change() self.widget_tree.get_object("docview_back").connect( "clicked", lambda *args, **kwargs: ( self.core.call_all("mainwindow_show", "left") ) ) self.core.call_all( "mainwindow_add", side="right", name="docview", prio=10000, header=self.widget_tree.get_object("docview_header"), body=self.widget_tree.get_object("docview_body"), ) self.core.call_success( "mainloop_schedule", self.core.call_all, "on_gtk_docview_init", self ) def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def _on_child_activated(self, page_layout, child): page = self.widget_to_page[child] for controller in self.controllers.values(): controller.on_page_activated(page) def docview_set_bottom_margin(self, height): self.widget_tree.get_object("docview_padding").set_size_request( 1, height + 10 ) def docview_get_headerbar(self): return self.widget_tree.get_object("docview_header") def docview_get_body(self): return self.widget_tree.get_object("docview_overlay") def docview_get_scrollwindow(self): return self.widget_tree.get_object("docview_scroll") def docview_switch_controller(self, name, new_controller_ctor): self.controllers[name].exit() new_controller = new_controller_ctor(self) LOGGER.info( "%s: %s --> %s", name, str(self.controllers[name]), str(new_controller) ) self.controllers[name] = new_controller new_controller.enter() def _on_layout_size_allocate(self, layout, allocation): for controller in self.controllers.values(): controller.on_layout_size_allocate(layout) def _on_vscroll_value_changed(self, vadj): for controller in self.controllers.values(): controller.on_vscroll_value_changed(vadj) def _on_vscroll_changed(self, vadj): for controller in self.controllers.values(): controller.on_vscroll_changed(vadj) def _on_drag_motion(self, layout, drag_context, x, y, time): for controller in self.controllers.values(): controller.on_drag_motion(drag_context, x, y, time) def _on_drag_leave(self, layout, drag_context, time): for controller in self.controllers.values(): controller.on_drag_leave(drag_context, time) def drag_and_drop_get_destination(self, widget, x, y): if widget != self.page_layout: return None for controller in self.controllers.values(): r = controller.drag_and_drop_get_destination(x, y) if r is not None: return r return None def _on_draw(self, layout, cairo_context): for controller in self.controllers.values(): controller.on_draw(cairo_context) def _on_overlay_draw(self, widget, cairo_context): for controller in self.controllers.values(): controller.on_overlay_draw(widget, cairo_context) def doc_close(self): for page in self.pages: page.set_visible(False) for controller in self.controllers.values(): controller.on_close() def _build_flow_box_child(self, child): widget = Gtk.FlowBoxChild.new() widget.set_visible(True) widget.set_property('name', 'docview_page') widget.set_property('halign', Gtk.Align.CENTER) widget.add(child) return widget def doc_open(self, doc_id, doc_url): self.doc_close() changed = self.core.call_success( "mainwindow_show", side="right", name="docview" ) self.zoom = 0.0 self.layout_name = 'grid' self.active_doc = (doc_id, doc_url) self.active_doc_mtime = self.core.call_success( "doc_get_mtime_by_url", doc_url ) self.active_page_idx = 0 self.pages = [] self.widget_to_page = {} self.page_to_widget = {} if changed: # WORKAROUND(Jflesch): # This delay is used to work around what looks like a GTK bug. # Delay here must be slightly longer than the GtkStack animation # from the main window. # To reproduce: # - disable this delay (you can leave the mainloop_schedule()) # - start Paperwork # - it should start by showing the welcome page # - open a *one-page* document # Expected: # - the one-page document is visible # Result: # - the document doesn't appear # - resizing the main window makes the document appear self.core.call_success( "mainloop_schedule", self._doc_open, doc_id, doc_url, delay_s=0.350 ) else: self._doc_open(doc_id, doc_url) def _doc_open(self, doc_id, doc_url): self.controllers = {} self.core.call_all( "gtk_docview_get_controllers", self.controllers, self ) for controller in self.controllers.values(): controller.enter() def doc_reload(self, doc_id, doc_url): if self.active_doc != (doc_id, doc_url): LOGGER.info( "Reload requested for document %s, but active doc is %s", (doc_id, doc_url), self.active_doc ) return mtime = self.core.call_success("doc_get_mtime_by_url", doc_url) if mtime == self.active_doc_mtime: LOGGER.info( "Reload for document %s requested, but mtime hasn't changed" " (%s)", doc_url, mtime ) return self.active_doc_mtime = mtime LOGGER.info("Reloading document %s", self.active_doc) for controller in self.controllers.values(): controller.doc_reload() def on_page_size_obtained(self, page): # page may been wrapped page = self.pages[page.page_idx] for controller in self.controllers.values(): controller.on_page_size_obtained(page) def on_page_shown(self, page_idx): self.active_page_idx = page_idx def doc_goto_previous_page(self): self.doc_goto_page(self.active_page_idx - 1) def doc_goto_next_page(self): self.doc_goto_page(self.active_page_idx + 1) def doc_goto_page(self, page_idx): LOGGER.info("Going to page %d", page_idx) if page_idx < 0: page_idx = 0 self.requested_page_idx = page_idx for controller in self.controllers.values(): controller.doc_goto_page(page_idx) def docview_set_layout(self, name): self.layout_name = name for controller in self.controllers.values(): controller.docview_set_layout(name) def docview_set_zoom(self, zoom): self.zoom = zoom for controller in self.controllers.values(): controller.docview_set_zoom(zoom) def docview_add_page_viewer(self, page, visible=True): widget = self._build_flow_box_child(page.widget) self.widget_to_page[widget] = page self.page_to_widget[page] = widget self.pages.append(page) if visible: self.page_layout.add(widget) def docview_hide_page_viewer(self, page): # Jflesch> Hiding is not enough. We have to remove the widget # otherwise it's still taken into account when computing # the layout (keep in mind that homogenous is set to True) self.page_layout.remove(self.page_to_widget[page]) def docview_show_page_viewer(self, page): # XXX(Jflesch): We mess up the order of the widgets here. But since # only the scan viewer uses this method at the moment, it's fine. self.page_layout.add(self.page_to_widget[page]) def on_mainwindow_fold_change(self): folded = self.core.call_success("mainwindow_get_folded") self.widget_tree.get_object("docview_header").set_show_close_button( not folded ) self.widget_tree.get_object("docview_back").set_visible(folded) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/000077500000000000000000000000001417573700700301655ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/__init__.py000066400000000000000000000037551417573700700323100ustar00rootroot00000000000000import logging LOGGER = logging.getLogger(__name__) class BaseDocViewController(object): def __init__(self, plugin): self.plugin = plugin def __str__(self): return str(type(self)) def enter(self): LOGGER.debug("%s", self.enter) def exit(self): LOGGER.debug("%s", self.exit) def on_layout_size_allocate(self, layout): LOGGER.debug("%s(%s)", self.on_layout_size_allocate, layout) def on_page_size_obtained(self, page): LOGGER.debug("%s(%d)", self.on_page_size_obtained, page.page_idx) def on_vscroll(self, vadj): LOGGER.debug( "%s(%d, %d)", self.on_vscroll, vadj.get_value(), vadj.get_upper() ) def on_vscroll_changed(self, vadj): self.on_vscroll(vadj) def on_vscroll_value_changed(self, vadj): self.on_vscroll(vadj) def doc_goto_page(self, page_idx): LOGGER.debug("%s(%d)", self.doc_goto_page, page_idx) def docview_set_layout(self, name): LOGGER.debug("%s(%s)", self.docview_set_layout, name) def docview_set_zoom(self, zoom): LOGGER.debug("%s(%f)", self.docview_set_zoom, zoom) def doc_reload(self): LOGGER.debug("%s()", self.doc_reload) def on_page_activated(self, page): LOGGER.debug("%s(%d)", self.on_page_activated, page.page_idx) def on_drag_motion(self, drag_context, x, y, time): LOGGER.debug("%s()", self.on_drag_motion) def on_drag_leave(self, drag_context, time): LOGGER.debug("%s()", self.on_drag_leave) def drag_and_drop_get_destination(self, x, y): LOGGER.debug("%s(%d, %d)", self.drag_and_drop_get_destination, x, y) def on_draw(self, cairo_context): # no LOGGER.debug() here for performance reasons pass def on_overlay_draw(self, overlay_drawing_area, cairo_context): # no LOGGER.debug() here for performance reasons pass def on_close(self): LOGGER.debug("%s()", self.on_close) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/autoscrolling.py000066400000000000000000000023651417573700700334320ustar00rootroot00000000000000import logging import openpaperwork_core from . import BaseDocViewController LOGGER = logging.getLogger(__name__) class AutoScrollingController(BaseDocViewController): def __init__(self, plugin): super().__init__(plugin) self.handler = None self.enter() def enter(self): if self.handler is not None: return self.handler = self.plugin.core.call_success( "gesture_enable_autoscrolling", self.plugin.scroll ) def exit(self): if self.handler is None: return self.handler.disable() self.handler = None def on_close(self): self.exit() class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['gtk_docview_controller'] def get_deps(self): return [ { 'interface': 'gtk_docview', 'defaults': ['paperwork_gtk.mainwindow.docview'], }, { 'interface': 'gtk_gesture_autoscrolling', 'defaults': ['openpaperwork_gtk.gesturee.autoscrolling'], }, ] def gtk_docview_get_controllers(self, out: dict, docview): out['autoscrolling'] = AutoScrollingController(docview) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/click.py000066400000000000000000000023141417573700700316240ustar00rootroot00000000000000import logging import openpaperwork_core from . import BaseDocViewController LOGGER = logging.getLogger(__name__) class ClickController(BaseDocViewController): def on_page_activated(self, page): super().on_page_activated(page) LOGGER.info("User activated page %d", page.page_idx) self.plugin.core.call_all("docview_set_layout", "paged") self.plugin.core.call_all("doc_goto_page", page.page_idx) def docview_set_layout(self, name): if name == 'grid': return self.plugin.docview_switch_controller('click', NoClickController) class NoClickController(BaseDocViewController): def docview_set_layout(self, name): if name != 'grid': return self.plugin.docview_switch_controller('click', ClickController) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['gtk_docview_controller'] def get_deps(self): return [ { 'interface': 'gtk_docview', 'defaults': ['paperwork_gtk.mainwindow.docview'], }, ] def gtk_docview_get_controllers(self, out: dict, docview): out['click'] = ClickController(docview) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/drop.py000066400000000000000000000111451417573700700315050ustar00rootroot00000000000000""" When drag'n'dropping, enables the dropping-in-a-document part. """ import logging import openpaperwork_core import openpaperwork_core.promise from . import BaseDocViewController LOGGER = logging.getLogger(__name__) class DropController(BaseDocViewController): LINE_BORDERS = 10 LINE_WIDTH = 3 LINE_COLOR = (0.0, 0.8, 1.0, 1.0) def __init__(self, core, plugin): super().__init__(plugin) self.core = core self.closest = None self.beforeafter = -1 # 0 for before the widget, 1 for after @staticmethod def _compute_squared_distance(rect, x, y): pts = ( (rect.x, rect.y), (rect.x + rect.width, rect.y), (rect.x, rect.y + rect.height), (rect.x + rect.width, rect.y + rect.height), ) dists = { ((abs(pt[0] - x) ** 2) + (abs(pt[1] - y) ** 2)) for pt in pts } return min(dists) def _compute_closest(self, x, y): dists = { ( self._compute_squared_distance(widget.get_allocation(), x, y), widget ) for widget in self.plugin.page_layout.get_children() } try: return min(dists)[1] except ValueError: # empty list of widgets return None @staticmethod def _compute_beforeafter(rect, x, y): center = rect.x + (rect.width / 2) return 0 if x < center else 1 def on_drag_motion(self, drag_context, x, y, time): super().on_drag_motion(drag_context, x, y, time) self.closest = self._compute_closest(x, y) if self.closest is None: self.beforeafter = -1 return self.beforeafter = self._compute_beforeafter( self.closest.get_allocation(), x, y ) self.plugin.page_layout.queue_draw() def on_drag_leave(self, drag_context, time): super().on_drag_leave(drag_context, time) self.closest = None self.plugin.page_layout.queue_draw() def on_draw(self, cairo_ctx): super().on_draw(cairo_ctx) if self.closest is None: return allocation = self.closest.get_allocation() height = allocation.height x = allocation.x x -= 2 * self.LINE_BORDERS x += (self.beforeafter * (allocation.width + 2 * self.LINE_BORDERS)) x = max(0, x) y = allocation.y cairo_ctx.save() try: cairo_ctx.set_source_rgba(*self.LINE_COLOR) cairo_ctx.set_line_width(self.LINE_WIDTH) cairo_ctx.move_to(x + self.LINE_BORDERS, y) cairo_ctx.line_to(x + self.LINE_BORDERS, y + height) cairo_ctx.stroke() cairo_ctx.move_to(x, y + self.LINE_BORDERS) cairo_ctx.line_to( x + (2 * self.LINE_BORDERS), y + self.LINE_BORDERS ) cairo_ctx.stroke() cairo_ctx.move_to(x, y + height - self.LINE_BORDERS) cairo_ctx.line_to( x + (2 * self.LINE_BORDERS), y + height - self.LINE_BORDERS ) cairo_ctx.stroke() finally: cairo_ctx.restore() def drag_and_drop_get_destination(self, x, y): super().drag_and_drop_get_destination(x, y) closest = self._compute_closest(x, y) if closest is None: dst_page_idx = 0 else: page = self.plugin.widget_to_page[closest] beforeafter = self._compute_beforeafter( closest.get_allocation(), x, y ) dst_page_idx = page.page_idx + beforeafter return (*self.plugin.active_doc, dst_page_idx) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() def get_interfaces(self): return [ 'chkdeps', 'gtk_docview_controller', ] def get_deps(self): return [ { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'gtk_docview', 'defaults': ['paperwork_gtk.mainwindow.docview'], }, { 'interface': 'gtk_drag_and_drop', 'defaults': ['paperwork_gtk.gesture.drag_and_drop'], }, ] def on_gtk_docview_init(self, docview): self.core.call_all("drag_and_drop_page_enable", docview.page_layout) def gtk_docview_get_controllers(self, out: dict, docview): out['drop'] = DropController(self.core, docview) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/empty_doc/000077500000000000000000000000001417573700700321505ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/empty_doc/__init__.py000066400000000000000000000133071417573700700342650ustar00rootroot00000000000000import logging try: import cairo CAIRO_AVAILABLE = True except (ImportError, ValueError): CAIRO_AVAILABLE = False try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False try: import gi gi.require_version('Pango', '1.0') gi.require_version('PangoCairo', '1.0') from gi.repository import Pango from gi.repository import PangoCairo PANGO_AVAILABLE = True except (ImportError, ValueError): PANGO_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps import openpaperwork_gtk.deps from .. import BaseDocViewController from ..... import _ LOGGER = logging.getLogger(__name__) class BaseDisplayNewDocController(BaseDocViewController): def __init__(self, docview, empty_doc_plugin): super().__init__(docview) self.empty_doc_plugin = empty_doc_plugin self.core = empty_doc_plugin.core def _switch_controller(self): nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", self.plugin.active_doc[1] ) if nb_pages is None: nb_pages = 0 if nb_pages > 0 or self.empty_doc_plugin.scanning: self.plugin.docview_switch_controller( "new_doc", lambda docview: NoDisplayController( docview, self.empty_doc_plugin ) ) else: self.plugin.docview_switch_controller( "new_doc", lambda docview: DisplayNewDocController( docview, self.empty_doc_plugin ) ) def doc_reload(self): self._switch_controller() class DisplayNewDocController(BaseDisplayNewDocController): def __init__(self, docview, plugin): super().__init__(docview, plugin) self.core = docview.core def enter(self): self.plugin.overlay.set_visible(True) def on_overlay_draw(self, overlay_drawing_area, cairo_ctx): if self.empty_doc_plugin.scanning: self._switch_controller() return img = self.empty_doc_plugin.img alloc = overlay_drawing_area.get_allocation() style = overlay_drawing_area.get_style_context() color = style.get_color(Gtk.StateFlags.ACTIVE) layout = PangoCairo.create_layout(cairo_ctx) layout.set_text(_("Empty"), -1) # text must be short txt_size = layout.get_size() if 0 in txt_size: return zoom = img.get_height() / txt_size[1] scaled_txt_width = txt_size[0] * zoom cairo_ctx.save() try: cairo_ctx.set_source_rgba(color.red, color.green, color.blue, 0.33) cairo_ctx.mask_surface( self.empty_doc_plugin.img, ((alloc.width - img.get_width() - scaled_txt_width) / 2), ((alloc.height - img.get_height()) / 2), ) cairo_ctx.fill() finally: cairo_ctx.restore() cairo_ctx.save() try: cairo_ctx.set_source_rgba(color.red, color.green, color.blue, 0.33) cairo_ctx.translate( ((alloc.width - img.get_width() - scaled_txt_width) / 2) + img.get_width(), ((alloc.height - img.get_height()) / 2), ) cairo_ctx.scale(zoom * Pango.SCALE, zoom * Pango.SCALE) PangoCairo.update_layout(cairo_ctx, layout) PangoCairo.show_layout(cairo_ctx, layout) finally: cairo_ctx.restore() class NoDisplayController(BaseDisplayNewDocController): def enter(self): self.plugin.overlay.set_visible(False) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.scanning = False def get_interfaces(self): return [ 'chkdeps', 'gtk_docview_controller', ] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, { 'interface': 'gtk_docview', 'defaults': ['paperwork_gtk.mainwindow.docview'], }, { 'interface': 'resources', 'defaults': ['openpaperwork_core.resources.setuptools'], }, ] def init(self, core): super().init(core) if not CAIRO_AVAILABLE: # chkdeps() must still be callable return file_path = self.core.call_success( "resources_get_file", "paperwork_gtk.mainwindow.docview.controllers.empty_doc", "empty_doc.png" ) file_path = self.core.call_success("fs_unsafe", file_path) self.img = cairo.ImageSurface.create_from_png(file_path) def chkdeps(self, out: dict): if not CAIRO_AVAILABLE: out['cairo'].update(openpaperwork_core.deps.CAIRO) if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) if not PANGO_AVAILABLE: out['pango'].update(openpaperwork_core.deps.PANGO) def on_scan2doc_start(self, *args, **kwargs): self.scanning = True def on_scan2doc_end(self, *args, **kwargs): self.scanning = False def gtk_docview_get_controllers(self, out: dict, docview): nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", docview.active_doc[1] ) if nb_pages is None: nb_pages = 0 out['new_doc'] = ( DisplayNewDocController(docview, self) if nb_pages <= 0 else NoDisplayController(docview, self) ) empty_doc.png000066400000000000000000000025151417573700700345650ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/empty_docPNG  IHDR\`WzTXtRaw profile type exifxMr b$q0tTbm6B"1$5%+T7ŴҶGI3ۿ®xwPffFe$|in:WWd+ >$3\wŝ$q8iٓsjuQ4v-ޏLn)>k7q^)CަjpIVbu[8@b=(QJn0'̍e\ 7,4ؤH@M`Xh-+_#GNdB0BO%<3Ic.]['yi cOx.}W M|V@P̎ x\!ڒY1xm @"V b&QوOY @) ɀ݀c|Y2h T+%XCUESPլEk-3X2lfnŪ'Wn^.#LK.RjEҊ+ʨ7j5@kjMbiٝfo!h':'qy2N)$7ʠF:t@0:NKnAoq_ ݿ &M3'z](]852p^oxzzzzzz OlؒzbKGD pHYs.#.#x?vtIME(/A.IDATxAN@c` *&v "+VxqQؘ L}>iΈDD4HIsIkId$$Fҗ#S[+Õ]qJz >/gwI?7stF^4z6{w-gpݡ p7%Hv#-< "8'=х?rIN5pppGuW m WLq5b=.4Megp"3TIENDB`paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/layout.py000066400000000000000000000101121417573700700320470ustar00rootroot00000000000000import logging import openpaperwork_core from . import BaseDocViewController LOGGER = logging.getLogger(__name__) class BaseLayoutController(BaseDocViewController): def __init__(self, core, plugin): super().__init__(plugin) self.core = core self.real_nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", self.plugin.active_doc[1] ) if self.real_nb_pages is None: self.real_nb_pages = 0 def on_close(self): for page in self.plugin.page_layout.get_children(): # weird but has to be done. Otherwise it seems the children # are destroyed with 'page' for c in page.get_children(): page.remove(c) self.plugin.page_layout.remove(page) self.plugin.pages = [] self.plugin.widget_to_page = {} self.plugin.page_to_widget = {} def _update_visibility(self): vadj = self.plugin.scroll.get_vadjustment() lower = vadj.get_lower() p_min = vadj.get_value() - lower p_max = vadj.get_value() + vadj.get_page_size() - lower for widget in self.plugin.page_layout.get_children(): alloc = widget.get_allocation() p_lower = alloc.y p_upper = alloc.y + alloc.height visible = (p_min <= p_upper and p_lower <= p_max) page = self.plugin.widget_to_page[widget] page.set_visible(visible) def on_vscroll_value_changed(self, vadj): super().on_vscroll_value_changed(vadj) self._update_visibility() def on_vscroll_changed(self, vadj): super().on_vscroll_changed(vadj) self._update_visibility() def on_page_size_obtained(self, page): super().on_page_size_obtained(page) self._update_visibility() def on_layout_size_allocate(self, layout): super().on_layout_size_allocate(layout) self._update_visibility() def _add_pages(self): pages = [] self.core.call_all( "doc_open_components", pages, *self.plugin.active_doc ) for (visible, page) in pages: self.plugin.docview_add_page_viewer(page, visible) def doc_reload(self): super().doc_reload() LOGGER.info("Reloading document %s", self.plugin.active_doc) self.on_close() self._add_pages() for page in self.plugin.pages: page.load() class LayoutControllerLoading(BaseLayoutController): def __init__(self, core, plugin): super().__init__(core, plugin) self.nb_loaded = 0 # page instantiation must be done before the calls to enter() # because other controllers may need the pages. So we cheat a little # bit and make it in the constructor. self._add_pages() def enter(self): super().enter() if len(self.plugin.pages) <= 0: self.plugin.docview_switch_controller( 'layout', LayoutControllerLoaded ) return self.nb_loaded = 0 for page in self.plugin.pages: page.load() def on_page_size_obtained(self, page): super().on_page_size_obtained(page) self.nb_loaded += 1 if self.nb_loaded >= len(self.plugin.pages): self.plugin.docview_switch_controller( 'layout', lambda plugin: LayoutControllerLoaded(self.core, plugin) ) def exit(self): LOGGER.info( "Size of all pages of doc %s loaded", self.plugin.active_doc[0] ) class LayoutControllerLoaded(BaseLayoutController): def enter(self): super().enter() self._update_visibility() class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['gtk_docview_controller'] def get_deps(self): return [ { 'interface': 'gtk_docview', 'defaults': ['paperwork_gtk.mainwindow.docview'], }, ] def gtk_docview_get_controllers(self, out: dict, docview): out['layout'] = LayoutControllerLoading(self.core, docview) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/page_number.py000066400000000000000000000055741417573700700330360ustar00rootroot00000000000000import logging import openpaperwork_core from . import BaseDocViewController LOGGER = logging.getLogger(__name__) class PageNumberController(BaseDocViewController): def _get_closest_widget(self): vadj = self.plugin.scroll.get_vadjustment() view_width = self.plugin.scroll.get_allocated_width() view_height = self.plugin.scroll.get_allocated_height() center = ( view_width / 2, vadj.get_value() + (view_height / 2) ) # look if the center is precisely on in a widget for widget in self.plugin.page_layout.get_children(): if not widget.get_visible(): continue alloc = widget.get_allocation() if alloc.width < 10 or alloc.height < 10: continue if (alloc.x <= center[0] and alloc.y <= center[1] and alloc.x + alloc.width >= center[0] and alloc.y + alloc.height >= center[1]): return widget # else look for the closest widget min_dist = (99999999999999999, None) for widget in self.plugin.page_layout.get_children(): if not widget.get_visible(): continue alloc = widget.get_allocation() if alloc.width < 10 or alloc.height < 10: continue widget_center = ( (alloc.x + (alloc.width / 2)), (alloc.y + (alloc.height / 2)), ) dist_w = (center[0] - widget_center[0]) dist_h = (center[1] - widget_center[1]) dist = (dist_w * dist_w) + (dist_h * dist_h) if dist < min_dist[0]: min_dist = (dist, widget) return min_dist[1] def _update_current_page(self): widget = self._get_closest_widget() if widget is None: return page = self.plugin.widget_to_page[widget] self.plugin.core.call_all("on_page_shown", page.page_idx) def on_layout_size_allocate(self, layout): super().on_layout_size_allocate(layout) self._update_current_page() def on_page_size_obtained(self, layout): super().on_page_size_obtained(layout) self._update_current_page() def on_vscroll_changed(self, vadj): super().on_vscroll_changed(vadj) self._update_current_page() def on_vscroll_value_changed(self, vadj): super().on_vscroll_value_changed(vadj) self._update_current_page() class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['gtk_docview_controller'] def get_deps(self): return [ { 'interface': 'gtk_docview', 'defaults': ['paperwork_gtk.mainwindow.docview'], }, ] def gtk_docview_get_controllers(self, out: dict, docview): out['page_number'] = PageNumberController(docview) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/scroll.py000066400000000000000000000105651417573700700320440ustar00rootroot00000000000000import logging import openpaperwork_core from . import BaseDocViewController LOGGER = logging.getLogger(__name__) class Scroller(object): def __init__(self, controller, widget): self.controller = controller self.widget = widget self.allocate_handler_id = None def goto(self): alloc = self.widget.get_allocation() if alloc.y < 0: self.allocate_handler_id = self.widget.connect( "size-allocate", self._goto_on_allocate ) else: self._goto_on_allocate() def _goto_on_allocate(self, *args, **kwargs): alloc = self.widget.get_allocation() assert(alloc.y >= 0) self.controller.last_value = alloc.y self.controller.plugin.scroll.get_vadjustment().set_value(alloc.y) if self.allocate_handler_id is not None: self.widget.disconnect(self.allocate_handler_id) self.allocate_handler_id = None class ScrollPageLockedController(BaseDocViewController): def __init__(self, plugin): super().__init__(plugin) self.last_upper = -1 self.last_value = -1 def enter(self): self.last_value = self.plugin.scroll.get_vadjustment().get_value() self.last_upper = self.plugin.scroll.get_vadjustment().get_upper() self.doc_goto_page(self.plugin.requested_page_idx) def doc_goto_page(self, page_idx): super().doc_goto_page(page_idx) for widget in self.plugin.page_layout.get_children(): page = self.plugin.widget_to_page[widget] if page.page_idx != page_idx: continue Scroller(self, widget).goto() def on_page_size_obtained(self, page): super().on_page_size_obtained(page) self.doc_goto_page(self.plugin.requested_page_idx) def on_layout_size_allocate(self, layout): super().on_layout_size_allocate(layout) self.doc_goto_page(self.plugin.requested_page_idx) def docview_set_layout(self, name): super().docview_set_layout(name) self.doc_goto_page(self.plugin.requested_page_idx) def docview_set_zoom(self, zoom): super().docview_set_zoom(zoom) self.doc_goto_page(self.plugin.requested_page_idx) def docview_reload_page(self, page_idx): super().docview_reload_page(page_idx) self.doc_goto_page(self.plugin.requested_page_idx) def on_vscroll_changed(self, adj): super().on_vscroll_changed(adj) if adj.get_upper() == self.last_upper: return self.last_upper = adj.get_upper() self.doc_goto_page(self.plugin.requested_page_idx) def on_vscroll_value_changed(self, adj): super().on_vscroll_value_changed(adj) if adj.get_value() == self.last_value: return self.last_value = adj.get_value() self.plugin.docview_switch_controller('scroll', ScrollFreeController) class ScrollFreeController(BaseDocViewController): def __init__(self, plugin): super().__init__(plugin) self.scroll_proportion = None def _upd_proportion(self): vadj = self.plugin.scroll.get_vadjustment() self.scroll_proportion = vadj.get_value() / vadj.get_upper() def enter(self): self._upd_proportion() def doc_goto_page(self, page_idx): self.plugin.docview_switch_controller( 'scroll', ScrollPageLockedController ) def _on_vscroll(self, adj): if self.scroll_proportion is None: return pos = self.scroll_proportion * adj.get_upper() self.scroll_proportion = None adj.set_value(pos) def docview_set_zoom(self, zoom): self._upd_proportion() def docview_set_layout(self, name): self._upd_proportion() def on_vscroll_changed(self, adj): super().on_vscroll_changed(adj) self._on_vscroll(adj) def on_vscroll_value_changed(self, adj): super().on_vscroll_value_changed(adj) self._on_vscroll(adj) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['gtk_docview_controller'] def get_deps(self): return [ { 'interface': 'gtk_docview', 'defaults': ['paperwork_gtk.mainwindow.docview'], }, ] def gtk_docview_get_controllers(self, out: dict, docview): out['scroll'] = ScrollPageLockedController(docview) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/title.py000066400000000000000000000041171417573700700316630ustar00rootroot00000000000000import openpaperwork_core from .... import _ from . import BaseDocViewController class TitleController(BaseDocViewController): def enter(self): folded = self.plugin.core.call_success("mainwindow_get_folded") if folded: self.plugin.widget_tree.get_object( "docview_header" ).set_title("") return (doc_id, doc_url) = self.plugin.active_doc if self.plugin.core.call_success("is_doc", doc_url) is not None: doc_date = self.plugin.core.call_success( "doc_get_date_by_id", doc_id ) if doc_date is not None: doc_date = self.plugin.core.call_success( "i18n_date_short", doc_date ) else: doc_date = doc_id self.plugin.widget_tree.get_object( "docview_header" ).set_title(doc_date) else: self.plugin.widget_tree.get_object( "docview_header" ).set_title(_("New document")) def on_close(self): self.plugin.widget_tree.get_object("docview_header").set_title("") class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.controller = None def get_interfaces(self): return ['gtk_docview_controller'] def get_deps(self): return [ { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'gtk_docview', 'defaults': ['paperwork_gtk.mainwindow.docview'], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, ] def gtk_docview_get_controllers(self, out: dict, docview): self.controller = TitleController(docview) out['title'] = self.controller def on_mainwindow_fold_change(self): if self.controller is None: return # update the title bar self.controller.enter() paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/controllers/zoom.py000066400000000000000000000055551417573700700315350ustar00rootroot00000000000000import logging import openpaperwork_core from . import BaseDocViewController LOGGER = logging.getLogger(__name__) class ZoomLayoutController(BaseDocViewController): def __init__(self, plugin): super().__init__(plugin) self.last_zoom = -1 def _recompute_zoom(self): layout_name = self.plugin.layout_name spacing = self.plugin.page_layout.get_column_spacing() nb_columns = self.plugin.LAYOUTS[layout_name] max_columns = 0 view_width = self.plugin.scroll.get_allocated_width() zoom = 1.0 for page_idx in range(0, len(self.plugin.pages), nb_columns): pages = self.plugin.pages[page_idx:page_idx + nb_columns] pages_width = sum([p.get_full_size()[0] for p in pages]) pages_width += (len(pages) * 30 * spacing) + 1 zoom = min(zoom, view_width / pages_width) max_columns = max(max_columns, len(pages)) if zoom == self.last_zoom: return self.last_zoom = zoom self.plugin.core.call_all("docview_set_zoom", zoom) for page in self.plugin.pages: page.set_zoom(zoom) if nb_columns > 1: layout = 'grid' else: layout = 'paged' self.plugin.core.call_all("on_layout_change", layout) def enter(self): super().enter() self._recompute_zoom() def docview_set_zoom(self, zoom): super().docview_set_zoom(zoom) if zoom == self.last_zoom: return self.plugin.docview_switch_controller('zoom', ZoomCustomController) def docview_set_layout(self, name): super().docview_set_layout(name) self._recompute_zoom() def on_page_size_obtained(self, page): super().on_page_size_obtained(page) self._recompute_zoom() def on_layout_size_allocate(self, layout): self._recompute_zoom() def doc_reload(self): self._recompute_zoom() class ZoomCustomController(BaseDocViewController): def _reapply_zoom(self): zoom = self.plugin.zoom for page in self.plugin.pages: page.set_zoom(zoom) def enter(self): super().enter() self._reapply_zoom() def docview_set_zoom(self, zoom): super().docview_set_zoom(zoom) self._reapply_zoom() def docview_set_layout(self, name): super().docview_set_layout(name) self.plugin.docview_switch_controller('zoom', ZoomLayoutController) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['gtk_docview_controller'] def get_deps(self): return [ { 'interface': 'gtk_docview', 'defaults': ['paperwork_gtk.mainwindow.docview'], }, ] def gtk_docview_get_controllers(self, out: dict, docview): out['zoom'] = ZoomLayoutController(docview) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/docview.css000066400000000000000000000001541417573700700277710ustar00rootroot00000000000000#docview_scroll { background-color: shade(@theme_bg_color, 0.95); } #docview_box { margin-top: 10px; } paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/docview.glade000066400000000000000000000105171417573700700302610ustar00rootroot00000000000000 True False False False True True False docview_scroll True True in True False True False vertical docview_box True True 10 10 1 5 horizontal center True False True 0 True False True True 1 -1 True False True True :minimize,maximize,close False True False False True none True False go-previous-symbolic 2 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/drag.py000066400000000000000000000070701417573700700271120ustar00rootroot00000000000000""" When drag'n'dropping, enables the dragging-from-pages part. """ import logging try: import gi gi.require_version('Gdk', '3.0') gi.require_version('Gtk', '3.0') from gi.repository import Gdk from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_gtk.deps LOGGER = logging.getLogger(__name__) TARGET_ENTRY_URI = 0 class Plugin(openpaperwork_core.PluginBase): PRIORITY = -10000 def __init__(self): super().__init__() self.pages = [] self.active_doc = None self.enabled = False def get_interfaces(self): return [ 'chkdeps', 'gtk_pageview', ] def get_deps(self): return [ { 'interface': 'page_img', 'defaults': [ 'paperwork_backend.model.img', 'paperwork_backend.model.pdf', ], }, ] def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def _enable_drag(self, widget, doc_id, doc_url, page_idx): widget.drag_source_set( Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.MOVE ) targets = Gtk.TargetList.new([]) targets.add_uri_targets(TARGET_ENTRY_URI) widget.drag_source_set_target_list(targets) widget.drag_source_set_icon_name("document-send-symbolic") widget.connect( "drag-data-get", self._on_drag_data_get, doc_id, doc_url, page_idx ) def _disable_drag(self, widget): widget.drag_source_unset() def _on_drag_data_get( self, widget, drag_context, data, info, time, doc_id, doc_url, page_idx): LOGGER.info("drag_data_get(%s, p%d, type=%d)", doc_id, page_idx, info) if info == TARGET_ENTRY_URI: # drag'n'drop API allows us to provide many URIs. But if we # do, applications like Firefox will try to display them all, # even if they don't understand the URI scheme. # --> we cheat and use the URI target for the extra info we may # need in Paperwork img_url = self.core.call_success( "page_get_img_url", doc_url, page_idx ) if img_url is None: return img_url = img_url.split("#", 1)[0] img_url += "#doc_id={}&page={}".format(doc_id, page_idx) LOGGER.info("Img URL: {}".format(img_url)) data.set_uris([img_url]) return assert() def doc_open_components(self, out: list, doc_id, doc_url): self.active_doc = (doc_id, doc_url) self.pages = [p[1] for p in out] if self.enabled: for (visible, page) in out: if visible: self._enable_drag( page.widget, doc_id, doc_url, page.page_idx ) def on_layout_change(self, layout_name): enabled = (layout_name == 'grid') if enabled == self.enabled: return if enabled: LOGGER.info("Layout %s --> Enabling drag(-and-drop)", layout_name) for page in self.pages: self._enable_drag(page.widget, *self.active_doc, page.page_idx) else: LOGGER.info("Layout %s --> Disabling drag(-and-drop)", layout_name) for page in self.pages: self._disable_drag(page.widget) self.enabled = enabled paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/000077500000000000000000000000001417573700700272045ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/__init__.py000066400000000000000000000000001417573700700313030ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/buttons.glade000066400000000000000000000023121417573700700316760ustar00rootroot00000000000000 True False False True False False Add page 0 False True False 1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/buttons.py000066400000000000000000000071131417573700700312560ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.promise LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.widget_tree = None self.default_action = None self.default_action_args = None self.active_doc_id = None self.active_doc_url = None self.busy = 0 def get_interfaces(self): return [ 'doc_open', 'gtk_scan_buttons', ] def get_deps(self): return [ { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'gtk_docview', 'defaults': ['paperwork_gtk.mainwindow.docview'], }, { 'interface': 'new_doc', 'defaults': ['paperwork_gtk.new_doc'], }, { 'interface': 'scan2doc', 'defaults': ['paperwork_backend.docscan.scan2doc'], }, { 'interface': 'screenshot', 'defaults': ['openpaperwork_gtk.screenshots'], }, ] def init(self, core): super().init(core) self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.docview.pageadd", "buttons.glade" ) if self.widget_tree is None: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to load widget tree") return self.widget_tree.get_object("pageadd_button").connect( "clicked", self._on_clicked ) headerbar = self.core.call_success("docview_get_headerbar") headerbar.pack_start(self.widget_tree.get_object("pageadd_buttons")) def pageadd_buttons_set_source_popover(self, selector): self.widget_tree.get_object("pageadd_switch").set_popover(selector) def _update_sensitivity(self): sensitive = ( self.default_action is not None and self.busy <= 0 ) button = self.widget_tree.get_object("pageadd_button") button.set_sensitive(sensitive) self.core.call_all("on_widget_busyness_changed", button, sensitive) def pageadd_set_default_action(self, txt, callback, *args): self.default_action = callback self.default_action_args = args self.widget_tree.get_object("pageadd_button").set_label(txt) self._update_sensitivity() def pageadd_busy_add(self): self.busy += 1 self._update_sensitivity() def pageadd_busy_remove(self): self.busy -= 1 self._update_sensitivity() def _on_clicked(self, widget): self.default_action( self.active_doc_id, self.active_doc_url, *self.default_action_args ) def doc_open(self, doc_id, doc_url): self.active_doc_id = doc_id self.active_doc_url = doc_url self._update_sensitivity() def doc_close(self): self.active_doc_id = None self.active_doc_url = None self._update_sensitivity() def on_scan_feed_start(self, scan_id): self.pageadd_busy_add() def on_scan_feed_end(self, scan_id): self.pageadd_busy_remove() def screenshot_snap_all_doc_widgets(self, out_dir): self.core.call_success( "screenshot_snap_widget", self.widget_tree.get_object("pageadd_buttons"), self.core.call_success("fs_join", out_dir, "page_add.png") ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/import.py000066400000000000000000000110601417573700700310660ustar00rootroot00000000000000import logging try: import gi GI_AVAILABLE = True except (ImportError, ValueError): GI_AVAILABLE = False try: GTK_AVAILABLE = False if GI_AVAILABLE: gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): pass import openpaperwork_core import openpaperwork_core.promise import openpaperwork_gtk.deps from .... import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.widget_tree = None self.windows = [] def get_interfaces(self): return [ 'chkdeps', 'gtk_scan_buttons_popover_sources', 'gtk_window_listener', ] def get_deps(self): return [ { 'interface': 'gtk_doc_import', 'defaults': ['paperwork_gtk.docimport'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'gtk_scan_buttons_popover', 'defaults': [ 'paperwork_gtk.mainwindow.docview.pageadd.source_popover' ], }, ] def init(self, core): super().init(core) if not GTK_AVAILABLE: return self.core.call_all( "mainloop_schedule", self.core.call_all, "pageadd_sources_refresh" ) def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def pageadd_get_sources(self, out: list): widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.docview.pageadd", "source_box.glade" ) source_long_txt = _("Import image or PDF file(s)") source_short_txt = _("Import file(s)") img = "insert-image-symbolic" widget_tree.get_object("source_image").set_from_icon_name( img, Gtk.IconSize.SMALL_TOOLBAR ) widget_tree.get_object("source_name").set_text(source_long_txt) out.append( ( widget_tree.get_object("source_selector"), source_short_txt, "import", self._on_import ) ) def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def _on_import(self, doc_id, doc_url, source_id): LOGGER.info("Opening file chooser dialog") mimes = set() self.core.call_all("get_import_mime_types", mimes) mimes = list(mimes) mimes.sort() dialog = Gtk.FileChooserDialog( _("Select a file or a directory to import"), self.windows[-1], Gtk.FileChooserAction.OPEN, ( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, # WORKAROUND(Jflesch): Use response ID 0 so the user # can select a folder. Gtk.STOCK_OPEN, 0 ) ) dialog.set_select_multiple(True) dialog.set_local_only(False) filter_all = Gtk.FileFilter() filter_all.set_name(_("All supported file formats")) for (txt, mime) in mimes: filter_all.add_mime_type(mime) dialog.add_filter(filter_all) file_filter = Gtk.FileFilter() file_filter.set_name(_("Any files")) file_filter.add_pattern("*.*") dialog.add_filter(file_filter) for (txt, mime) in mimes: file_filter = Gtk.FileFilter() file_filter.add_mime_type(mime) file_filter.set_name(txt) dialog.add_filter(file_filter) dialog.set_filter(filter_all) dialog.connect("response", self._on_dialog_response) self.core.call_all("pageadd_busy_add") dialog.show_all() def _on_dialog_response(self, dialog, response_id): self.core.call_all("pageadd_busy_remove") if (response_id != 0 and response_id != Gtk.ResponseType.ACCEPT and response_id != Gtk.ResponseType.OK and response_id != Gtk.ResponseType.YES and response_id != Gtk.ResponseType.APPLY): LOGGER.info("User canceled (response_id=%d)", response_id) dialog.destroy() return selected = dialog.get_uris() dialog.destroy() self.core.call_all("gtk_doc_import", selected) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/scan.py000066400000000000000000000122501417573700700305020ustar00rootroot00000000000000import logging try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_core.promise import openpaperwork_gtk.deps from .... import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.widget_tree = None self.busy = False def get_interfaces(self): return [ 'chkdeps', 'gtk_scan_buttons_popover_sources', ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'gtk_scan_buttons_popover', 'defaults': [ 'paperwork_gtk.mainwindow.docview.pageadd.source_popover' ], }, { 'interface': 'i18n_scanner', 'defaults': ['paperwork_backend.i18n.scanner'], }, { 'interface': 'scan', 'defaults': ['paperwork_backend.docscan.libinsane'], }, { 'interface': 'scan2doc', 'defaults': ['paperwork_backend.docscan.scan2doc'], }, ] def init(self, core): super().init(core) opt = self.core.call_success( "config_build_simple", "pageadd", "scanner_sources", lambda: [] ) self.core.call_all("config_register", "pageadd_sources", opt) self.core.call_all( "config_add_observer", "scanner_dev_id", self._update_sources ) self.core.call_all( "mainloop_schedule", self.core.call_all, "pageadd_sources_refresh" ) def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def _update_sources(self): def get_sources(dev=None): if dev is None: source_names = [] else: sources = dev.dev.get_children() source_names = [] for src in sources: source_names.append(src.get_name()) sources = None LOGGER.info("Scanner sources: %s", source_names) return source_names def store_sources(sources): self.core.call_all("config_put", "pageadd_sources", list(sources)) promise = self.core.call_success("scan_get_scanner_promise") promise = promise.then(openpaperwork_core.promise.ThreadedPromise( self.core, get_sources )) promise = promise.then(store_sources) promise = promise.then(self.core.call_all, "pageadd_sources_refresh") self.core.call_success("scan_schedule", promise) def pageadd_get_sources(self, out: list): sources = self.core.call_success("config_get", "pageadd_sources") for source in list(sources): widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.docview.pageadd", "source_box.glade" ) source_txt = self.core.call_success( "i18n_scanner_source", source ) source_txt = _("Scan from %s") % source_txt img = "view-paged-symbolic" if "flatbed" in source: img = "document-new-symbolic" widget_tree.get_object("source_image").set_from_icon_name( img, Gtk.IconSize.SMALL_TOOLBAR ) widget_tree.get_object("source_name").set_text(source_txt) out.append( ( widget_tree.get_object("source_selector"), source_txt, source, self._on_scan ) ) def _on_scan(self, doc_id, doc_url, source_id): LOGGER.info("Scanning from %s", source_id) if doc_id is None or doc_url is None: (doc_id, doc_url) = self.core.call_success("get_new_doc") self.core.call_all("doc_open", doc_id, doc_url) self.core.call_all("on_busy") self.core.call_all("pageadd_busy_add") self.busy = True promise = self.core.call_success( "scan2doc_promise", doc_id=doc_id, doc_url=doc_url, source_id=source_id ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then( self.core.call_all, "pageadd_busy_remove" ) self.core.call_success("scan_schedule", promise) def on_scan2doc_page_scanned(self, scan_id, doc_id, doc_url, nb_pages): LOGGER.info( "Page scanned: %s p%d (scan id = %d)", doc_id, nb_pages, scan_id ) self.core.call_all("doc_reload", doc_id, doc_url) def on_scan_started(self, scan_id): if not self.busy: return self.core.call_all("on_idle") self.busy = False paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/source_box.glade000066400000000000000000000034751417573700700323630ustar00rootroot00000000000000 True True False True True True False 12 12 12 12 12 True False gtk-missing-image False True 0 True False put a source name here False True 1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/source_popover.glade000066400000000000000000000011021417573700700332460ustar00rootroot00000000000000 False True False vertical 20 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageadd/source_popover.py000066400000000000000000000067541417573700700326440ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.promise LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.widget_tree = None def get_interfaces(self): return [ 'gtk_scan_buttons_popover', ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'gtk_scan_buttons', 'defaults': [ 'paperwork_gtk.mainwindow.docview.pageadd.buttons' ], }, ] def init(self, core): super().init(core) opt = self.core.call_success( "config_build_simple", "pageadd", "active_source", lambda: None ) self.core.call_all("config_register", "pageadd_active_source", opt) self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.docview.pageadd", "source_popover.glade" ) if self.widget_tree is None: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to load widget tree") return def pageadd_sources_refresh(self): active = self.core.call_success("config_get", "pageadd_active_source") parent = self.widget_tree.get_object("page_sources_box") for child in parent.get_children(): parent.remove(child) selectors = [] self.core.call_all("pageadd_get_sources", selectors) for (selector, source_name, source_id, callback) in selectors[1:]: selector.join_group(selectors[0][0]) for (selector, source_name, source_id, callback) in selectors: selector.connect( "toggled", self._on_toggle, source_name, source_id, callback ) parent.pack_start(selector, expand=False, fill=True, padding=0) for (selector, source_name, source_id, callback) in selectors: if active == source_id: break else: active = None if active is None: if len(selectors) > 0: (selector, source_name, source_id, callback) = selectors[0] selector.set_active(True) self._on_toggle(selector, source_name, source_id, callback) else: for (selector, source_name, source_id, callback) in selectors: if active == source_id: selector.set_active(True) self._on_toggle(selector, source_name, source_id, callback) break # if there is a single choice, there is no point in letting # the user choose. This is just confusing. self.core.call_all( "pageadd_buttons_set_source_popover", self.widget_tree.get_object("page_sources_popover") if len(selectors) > 1 else None ) def _on_toggle(self, widget, source_name, source_id, callback): if not widget.get_active(): return self.core.call_all( "pageadd_set_default_action", source_name, callback, source_id ) self.core.call_all( "config_put", "pageadd_active_source", source_id ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/000077500000000000000000000000001417573700700274075ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/__init__.py000066400000000000000000000116041417573700700315220ustar00rootroot00000000000000import logging import openpaperwork_core import paperwork_backend.sync LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.widget_tree = None self.page_info = None self.active_doc = (None, None) self.nb_pages = None self.current_page = None self.nb_pages = None def get_interfaces(self): return [ 'doc_open', 'gtk_docview_pageinfo', 'syncable', ] def get_deps(self): return [ { 'interface': 'gtk_docview', 'defaults': ['paperwork_gtk.mainwindow.docview'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, ] def init(self, core): super().init(core) self.core.call_success( "gtk_load_css", "paperwork_gtk.mainwindow.docview.pageinfo", "pageinfo.css" ) self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.docview.pageinfo", "pageinfo.glade" ) if self.widget_tree is None: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to load widget tree") return self.page_info = self.widget_tree.get_object("page_info") self.current_page = self.widget_tree.get_object( "page_current_nb" ) self.current_page.connect("activate", self._change_page) self.nb_pages = self.widget_tree.get_object("page_total") button = self.widget_tree.get_object("page_prev") button.connect( "clicked", lambda *args, **kwargs: self.core.call_all( "doc_goto_previous_page" ) ) button = self.widget_tree.get_object("page_next") button.connect( "clicked", lambda *args, **kwargs: self.core.call_all("doc_goto_next_page") ) self.core.call_success("docview_get_body").add_overlay(self.page_info) def _set_docview_margin(page_info, size_allocation): self.core.call_all( "docview_set_bottom_margin", page_info.get_allocated_height() ) self.page_info.connect("size-allocate", _set_docview_margin) def _change_page(self, *args, **kwargs): txt = self.current_page.get_text() if txt == "": return self.core.call_all("doc_goto_page", int(txt) - 1) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) nb_pages = self.core.call_success("doc_get_nb_pages_by_url", doc_url) if nb_pages is None: nb_pages = 0 self.current_page.set_text("") self.nb_pages.set_text(f"/ {nb_pages}") self.page_info.set_visible(True) self.page_info.set_sensitive(nb_pages > 0) def doc_reload(self, doc_id, doc_url): if (doc_id, doc_url) != self.active_doc: return self.doc_open(*self.active_doc) def on_page_shown(self, page_idx): txt = str(page_idx + 1) if txt != self.current_page.get_text(): self.current_page.set_text(txt) def page_info_add_left(self, widget): self.widget_tree.get_object("page_prevnext").pack_end( widget, expand=False, fill=True, padding=0 ) return True def page_info_add_right(self, widget): self.widget_tree.get_object("page_info").pack_end( widget, expand=False, fill=True, padding=0 ) return True def doc_transaction_start(self, out: list, total_expected=-1): class RefreshTransaction(paperwork_backend.sync.BaseTransaction): priority = -100000 def __init__(s, core, total_expected=-1): super().__init__(core, total_expected) s.active_doc = False def _refresh(s): self.core.call_success( "mainloop_schedule", self.doc_open, *self.active_doc ) s.active_doc = True def add_doc(s, doc_id): if self.active_doc[0] == doc_id: s._refresh() def upd_doc(s, doc_id): if self.active_doc[0] == doc_id: s._refresh() def del_doc(s, doc_id): if self.active_doc[0] == doc_id: s._refresh() def cancel(s): if s.active_doc: s._refresh() def commit(s): if s.active_doc: s._refresh() out.append(RefreshTransaction(self.core, total_expected)) def sync(self, promises: list): # sync don't change document content --> no need to refresh pass paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/actions.glade000066400000000000000000000037671417573700700320620ustar00rootroot00000000000000
True False 6 Edit False True False False True none False True 0 False True False False True page_menu_model none True False view-more-symbolic 2 False True 1
paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/actions.py000066400000000000000000000070251417573700700314250ustar00rootroot00000000000000import logging try: from gi.repository import Gio GIO_AVAILABLE = True except (ImportError, ValueError): GIO_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = None self.active_page = None self.button_edit = None def get_interfaces(self): return [ 'chkdeps', 'doc_open', 'page_actions', ] def get_deps(self): return [ { 'interface': 'gtk_docview_pageinfo', 'defaults': ['paperwork_gtk.mainwindow.docview.pageinfo'], }, { 'interface': 'gtk_page_editor', 'defaults': ['paperwork_gtk.mainwindow.pageeditor'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'screenshot', 'defaults': ['openpaperwork_gtk.screenshots'], }, ] def init(self, core): super().init(core) self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.docview.pageinfo", "actions.glade" ) if self.widget_tree is None: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to load widget tree") return self.menu_model = self.widget_tree.get_object("page_menu_model") self.submenus = {} self.button_edit = self.widget_tree.get_object("page_action_edit") self.button_edit.connect( "clicked", self._on_edit ) self.core.call_success( "page_info_add_right", self.widget_tree.get_object("page_actions") ) self.core.call_success( "mainloop_schedule", self.core.call_all, "on_page_menu_ready" ) def chkdeps(self, out: dict): if not GIO_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def on_page_shown(self, page_idx): self.active_page = page_idx def _on_edit(self, button): self.core.call_all( "gtk_open_page_editor", *self.active_doc, self.active_page ) def page_menu_open(self): self.widget_tree.get_object("page_actions_other").clicked() def page_menu_append_item(self, item, submenu_name=None): menu = self.menu_model if submenu_name is not None: menu = self.submenus.get(submenu_name, None) if menu is None: menu = Gio.Menu() self.submenus[submenu_name] = menu self.menu_model.append_submenu(submenu_name, menu) menu.append_item(item) def screenshot_snap_all_doc_widgets(self, out_dir): self.core.call_success( "screenshot_snap_widget", self.widget_tree.get_object("page_actions"), self.core.call_success("fs_join", out_dir, "page_actions.png"), margins=(50, 50, 50, 50) ) def screenshot_snap_page_action_menu(self, out_file): self.core.call_success( "screenshot_snap_widget", self.widget_tree.get_object("page_actions"), out_file, margins=(50, 200, 50, 50) ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/layout_settings.css000066400000000000000000000004231417573700700333550ustar00rootroot00000000000000#layout_settings { background-color: rgba(16, 16, 16, 0.90); color: white; } #layout_settings button { color: white; background-image: none; background-color: #373737; border: 1px solid #202121; box-shadow: none; } #layout_settings button:disabled { color: gray; } paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/layout_settings.glade000066400000000000000000000152131417573700700336440ustar00rootroot00000000000000 False True False False True none True False 2 0.05 1.0 0.5 0.05 0.05 layout_settings 10 True vertical 7 True 30 True True view-paged-symbolic 1 True True 0 True True view-grid-symbolic 1 True True 1 False True 1 True True edit-find-symbolic 1 False True 1 True True horizontal adjustment_zoom 200 3 False True True True 2 False True 2 True horizontal 400 False True 3 True True Highlight words True True 0 True False False 1 False True 4 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/layout_settings.py000066400000000000000000000141701417573700700332210ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_gtk.deps try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): LAYOUTS = { 'paged': { 'icon': 'view-paged-symbolic', }, 'grid': { 'icon': 'view-grid-symbolic', }, } def __init__(self): super().__init__() self.widget_tree = None self.layout_icon = None self.layout_button = None self.notify = True def get_interfaces(self): return [ 'chkdeps', 'gtk_layout_settings', 'screenshot_provider', ] def get_deps(self): return [ { 'interface': 'gtk_docview', 'defaults': ['paperwork_gtk.mainwindow.docview'], }, { 'interface': 'gtk_docview_pageinfo', 'defaults': ['paperwork_gtk.mainwindow.docview.pageinfo'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'screenshot', 'defaults': ['openpaperwork_gtk.screenshots'], }, ] def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def init(self, core): super().init(core) self.core.call_success( "gtk_load_css", "paperwork_gtk.mainwindow.docview.pageinfo", "layout_settings.css" ) self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.docview.pageinfo", "layout_settings.glade" ) if self.widget_tree is None: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to load widget tree") return self.layout_icon = self.widget_tree.get_object("page_layout_icon") self.layout_button = self.widget_tree.get_object("page_layout") self.layout_button.connect("clicked", self.gtk_open_layout_settings) self.zoom = self.widget_tree.get_object("adjustment_zoom") self.zoom.connect("value-changed", self._on_zoom_changed) self.all_boxes = self.widget_tree.get_object("show_all_boxes") self.all_boxes.connect("notify::active", self._notify_all_boxes) self.layout_buttons = { self.widget_tree.get_object("layout_grid"): { "handler": None, "layout": "grid", }, self.widget_tree.get_object("layout_paged"): { "handler": None, "layout": "paged", }, } for button in self.layout_buttons.keys(): self.layout_buttons[button]['handler'] = button.connect( "clicked", self._on_layout_change ) self.core.call_success("page_info_add_left", self.layout_button) scroll = self.core.call_success("docview_get_scrollwindow") self.core.call_all("on_zoomable_widget_new", scroll, self.zoom) def on_layout_change(self, layout_name): if self.layout_icon is None: return icon = self.LAYOUTS[layout_name]['icon'] # smallest icon size available self.layout_icon.set_from_icon_name(icon, Gtk.IconSize.SMALL_TOOLBAR) def _on_layout_change(self, widget): assert(widget in self.layout_buttons) layout = self.layout_buttons[widget]['layout'] self.core.call_all("docview_set_layout", layout) def gtk_open_layout_settings(self, *args, **kwargs): menu = self.widget_tree.get_object("layout_settings") menu.set_relative_to(self.layout_button) menu.set_visible(True) def gtk_close_layout_settings(self, *args, **kwargs): menu = self.widget_tree.get_object("layout_settings") menu.set_visible(False) def _on_zoom_changed(self, _=None): new_value = self.zoom.get_value() if self.notify: self.core.call_all("docview_set_zoom", new_value) def _notify_all_boxes(self, *args, **kwargs): active = self.all_boxes.get_active() self.core.call_all("set_all_boxes_visible", active) def docview_set_zoom(self, zoom): current = self.zoom.get_value() if current != zoom: self.notify = False try: self.zoom.set_value(zoom) finally: self.notify = True def screenshot_snap_all_doc_widgets(self, out_dir): if self.widget_tree is None: return self.core.call_success( "screenshot_snap_widget", self.widget_tree.get_object("layout_settings"), self.core.call_success("fs_join", out_dir, "docview_layout.png"), ) self.core.call_success( "screenshot_snap_widget", self.widget_tree.get_object("layout_paged"), self.core.call_success( "fs_join", out_dir, "docview_layout_paged.png" ), margins=(50, 50, 50, 50) ) self.core.call_success( "screenshot_snap_widget", self.widget_tree.get_object("layout_grid"), self.core.call_success( "fs_join", out_dir, "docview_layout_grid.png" ), margins=(50, 50, 50, 50) ) self.core.call_success( "screenshot_snap_widget", self.widget_tree.get_object("scale_zoom"), self.core.call_success( "fs_join", out_dir, "docview_layout_scale.png" ), margins=(50, 50, 50, 50) ) self.core.call_success( "screenshot_snap_widget", self.widget_tree.get_object("show_all_boxes"), self.core.call_success( "fs_join", out_dir, "docview_layout_show_all_boxes.png" ), margins=(275, 50, 50, 50) ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/pageinfo.css000066400000000000000000000006541417573700700317160ustar00rootroot00000000000000#page_info { background-color: rgba(16, 16, 16, 0.70); color: white; padding: 5px; padding-left: 10px; padding-right: 10px; } #page_info .button-left { border-right-width: 0px; } #page_info > box > label { color: #919693; } #page_info { color: gray; } #page_info button { color: white; background-image: none; background-color: #373737; border: 1px solid #202121; } #page_info button:disabled { color: gray; } paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageinfo/pageinfo.glade000066400000000000000000000123161417573700700322000ustar00rootroot00000000000000 page_info False False end True False False True False False True none True False go-previous-symbolic 2 False True 0 False True False False 6 True none True False go-next-symbolic 2 False True start 1 False True 0 True False True True 1 True False True True False 0 3 1 True True False True 0 True False False / 42 False True 1 False True 2 True False True True 3 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageprocessing/000077500000000000000000000000001417573700700306305ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageprocessing/__init__.py000066400000000000000000000122061417573700700327420ustar00rootroot00000000000000""" Wraps a pageview in a GtkOverlay + GtkSpinner. Displays the spinner on top of the pageview when another plugin is working on the page. """ import logging import openpaperwork_core try: from gi.repository import GObject GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False # workaround so chkdeps can still be called class GObject(object): TYPE_BOOLEAN = 0 class SignalFlags(object): RUN_LAST = 0 class GObject(object): pass LOGGER = logging.getLogger(__name__) class PageWrapper(GObject.GObject): __gsignals__ = { 'getting_size': (GObject.SignalFlags.RUN_LAST, None, ()), 'size_obtained': (GObject.SignalFlags.RUN_LAST, None, ()), 'img_obtained': (GObject.SignalFlags.RUN_LAST, None, ()), 'visibility_changed': (GObject.SignalFlags.RUN_LAST, None, ( GObject.TYPE_BOOLEAN, )), } def __init__(self, plugin, page): super().__init__() self.plugin = plugin self.page = page self.page_idx = page.page_idx self.busy = False self.widget_tree = self.plugin.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.docview.pageprocessing", "pageprocessing.glade", ) self.widget = self.widget_tree.get_object("pageprocessing_overlay") self.widget.add(page.widget) page.connect("getting_size", lambda p: self.emit('getting_size')) page.connect("size_obtained", lambda p: self.emit("size_obtained")) page.connect("img_obtained", lambda p: self.emit("img_obtained")) page.connect( "visibility_changed", lambda p, v: self.emit("visibility_changed", v) ) page.widget.connect("draw", self._on_draw) def load(self): self.page.load() def close(self): self.page.close() def get_zoom(self): return self.page.get_zoom() def set_zoom(self, zoom): return self.page.set_zoom(zoom) def get_full_size(self): return self.page.get_full_size() def get_size(self): return self.page.get_size() def resize(self): return self.page.resize() def refresh(self, reload=False): return self.page.refresh(reload=reload) def set_visible(self, visible): return self.page.set_visible(visible) def get_visible(self): return self.page.get_visible() def _on_draw(self, overlay, cairo_ctx): if not self.busy: return w = overlay.get_allocated_width() h = overlay.get_allocated_height() cairo_ctx.set_source_rgba(0.5, 0.5, 0.5, 0.5) cairo_ctx.rectangle(0, 0, w, h) cairo_ctx.fill() def on_page_modification_start(self): self.busy = True spinner = self.widget_tree.get_object("pageprocessing_spinner") spinner.set_visible(True) spinner.start() def on_page_modification_end(self): self.busy = False spinner = self.widget_tree.get_object("pageprocessing_spinner") spinner.set_visible(False) spinner.stop() self.page.refresh(reload=True) if GLIB_AVAILABLE: GObject.type_register(PageWrapper) class Plugin(openpaperwork_core.PluginBase): PRIORITY = -1000 def __init__(self): super().__init__() self.wrappers = [] self.active_doc_id = None self.active_pages = set() def get_interfaces(self): return [ 'chkdeps', 'gtk_pageview', ] def get_deps(self): return [ { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def doc_close(self): self.wrappers = [] def doc_open_components(self, out: list, doc_id, doc_url): if doc_id != self.active_doc_id: self.active_doc_id = doc_id self.active_pages = set() self.doc_close() # instantiate PageWrapper objects, and replace the pages in the # list 'out' by those wrappers. self.wrappers = [ PageWrapper(self, page) for (visible, page) in out ] for page_idx in range(0, len(out)): out[page_idx] = (out[page_idx][0], self.wrappers[page_idx]) if page_idx in self.active_pages: out[page_idx][1].on_page_modification_start() def on_page_modification_start(self, doc_id, page_idx): if doc_id != self.active_doc_id: return self.active_pages.add(page_idx) if page_idx >= len(self.wrappers): return self.wrappers[page_idx].on_page_modification_start() def on_page_modification_end(self, doc_id, page_idx): if doc_id != self.active_doc_id: return if page_idx in self.active_pages: self.active_pages.remove(page_idx) if page_idx >= len(self.wrappers): return self.wrappers[page_idx].on_page_modification_end() pageprocessing.glade000066400000000000000000000011261417573700700345600ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageprocessing True False False False True paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/000077500000000000000000000000001417573700700274265ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/__init__.py000066400000000000000000000231361417573700700315440ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.deps import openpaperwork_core.promise try: from gi.repository import GObject GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False # workaround so chkdeps can still be called class GObject(object): TYPE_BOOLEAN = 0 class SignalFlags(object): RUN_LAST = 0 class GObject(object): pass from .... import _ LOGGER = logging.getLogger(__name__) class Page(GObject.GObject): __gsignals__ = { 'getting_size': (GObject.SignalFlags.RUN_LAST, None, ()), 'size_obtained': (GObject.SignalFlags.RUN_LAST, None, ()), 'img_obtained': (GObject.SignalFlags.RUN_LAST, None, ()), 'visibility_changed': (GObject.SignalFlags.RUN_LAST, None, ( GObject.TYPE_BOOLEAN, )), } def __init__(self, core, doc_id, doc_url, page_idx, nb_pages): super().__init__() self.core = core self.doc_id = doc_id self.doc_url = doc_url self.page_idx = page_idx self.nb_pages = nb_pages self.visible = False self.mtime = 0 self.zoom = 1.0 self.widget_tree = None self.widget = None self.renderer = None self._load_renderer() self.rebuild_widget() def _load_renderer(self): page_img_url = self.core.call_success( "page_get_img_url", self.doc_url, self.page_idx ) LOGGER.info( "URL for %s p%d: %s", self.doc_url, self.page_idx, page_img_url ) assert(page_img_url is not None) self.renderer = self.core.call_success( "cairo_renderer_by_url", "page_loader", page_img_url ) self.renderer.connect("getting_size", self._on_renderer_getting_size) self.renderer.connect("size_obtained", self._on_renderer_size) self.renderer.connect("img_obtained", self._on_renderer_img) def rebuild_widget(self): self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.docview.pageview", "pageview.glade" ) self.widget = self.widget_tree.get_object("pageview_area") self.widget.set_visible(False) # visible in the GTK sense self.widget.connect("draw", self._on_draw) self.resize() def __str__(self): return "{} p{}".format(self.doc_id, self.page_idx) def _on_renderer_getting_size(self, renderer): self.core.call_all( "on_progress", "loading_page_sizes", self.page_idx / self.nb_pages, _("Loading page {}/{} ...").format(self.page_idx, self.nb_pages) ) self.emit('getting_size') def _on_renderer_size(self, renderer): LOGGER.info( "Page %d: size %d x %d", self.page_idx, self.renderer.size[0], self.renderer.size[1] ) self.widget.set_visible(True) self.resize() self.core.call_all("on_page_size_obtained", self) self.emit('size_obtained') def _on_renderer_img(self, renderer): assert(self.visible) self.refresh() self.core.call_all("on_page_img_obtained", self) self.emit('img_obtained') def load(self): if not self.refresh(reload=True): # renderer has already loaded the page --> reemit the page size if self.renderer.size[0] != 0: self.core.call_success( "mainloop_schedule", self._on_renderer_size, self.renderer ) def close(self): self.renderer.close() def get_zoom(self): return self.zoom def set_zoom(self, zoom): self.zoom = zoom self.resize() def get_full_size(self): return self.renderer.size def get_size(self): return ( int(self.renderer.size[0] * self.zoom), int(self.renderer.size[1] * self.zoom) ) def resize(self): if self.renderer.size[0] == 0: return self.renderer.zoom = self.zoom size = self.get_size() self.widget.set_size_request(size[0], size[1]) def refresh(self, reload=False): if reload: # only if the mtime has changed. Otherwise there is no point. mtime = self.core.call_success( "page_get_mtime_by_url", self.doc_url, self.page_idx ) if self.mtime == mtime: reload = False else: self.mtime = mtime if reload: self.set_visible(False) self.close() self._load_renderer() self.renderer.start() return True elif self.widget is not None: self.widget.queue_draw() return False def hide(self): self.visible = False self.renderer.hide() self.core.call_all("on_page_visibility_changed", self, False) self.emit('visibility_changed', False) def show(self): self.visible = True self.renderer.render() self.core.call_all("on_page_visibility_changed", self, True) self.emit('visibility_changed', True) def set_visible(self, visible): if self.visible == visible: return if visible: self.show() else: self.hide() def get_visible(self): return self.visible def _on_draw(self, widget, cairo_ctx): self.renderer.draw(cairo_ctx) self.core.call_all("on_page_draw", cairo_ctx, self) def blur(self): self.renderer.blur() self.widget.queue_draw() def unblur(self): self.renderer.unblur() self.widget.queue_draw() def detach_from_parent(self): parent = self.widget.get_parent() if parent is None: return parent.remove(self.widget) if GLIB_AVAILABLE: GObject.type_register(Page) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 10000 def __init__(self): super().__init__() self.pages = [] self.nb_to_load = 0 self.active_doc = (None, None) def get_interfaces(self): return [ 'chkdeps', 'gtk_pageview', ] def get_deps(self): return [ { 'interface': 'cairo_url', 'defaults': [ 'paperwork_backend.cairo.pillow', 'paperwork_backend.cairo.poppler', ], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'page_img', 'defaults': [ 'paperwork_backend.model.img', 'paperwork_backend.model.pdf', ], }, { 'interface': 'work_queue', 'defaults': ['openpaperwork_core.work_queue.default'], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def init(self, core): super().init(core) self.core.call_all( "work_queue_create", "page_loader", stop_on_quit=True ) def doc_close(self): self.core.call_success("work_queue_cancel_all", "page_loader") for page in self.pages: page.close() self.pages = [] def doc_open_components(self, out: list, doc_id, doc_url): active_doc = (doc_id, doc_url) if self.active_doc != active_doc: self.doc_close() self.active_doc = active_doc nb_pages = self.core.call_success("doc_get_nb_pages_by_url", doc_url) LOGGER.info("Number of pages displayed: %s", nb_pages) if nb_pages is None: LOGGER.warning("Failed to get the number of pages in %s", doc_id) nb_pages = 0 self.core.call_all("on_objref_graph") self.core.call_all( "on_perfcheck_start", "pageview->doc_open_components({})".format(doc_id) ) # drop the extra pages we have if any for page in self.pages[nb_pages:]: page.close() self.pages = self.pages[:nb_pages] # reuse the pages we already have to avoid useless refreshes for page in self.pages: page.detach_from_parent() # add any new page for page_idx in range(len(self.pages), nb_pages): self.pages.append(Page( self.core, doc_id, doc_url, page_idx, nb_pages )) LOGGER.info( "%d pages in the documents (%d components)", nb_pages, len(self.pages) ) self.nb_to_load = len(self.pages) for page in self.pages: page.connect("size_obtained", self._on_page_img_size_obtained) out.append((True, page)) self.core.call_all( "on_perfcheck_stop", "pageview->doc_open_components({})".format(doc_id), nb_pages=nb_pages ) def _on_page_img_size_obtained(self, page): self.nb_to_load -= 1 if self.nb_to_load > 0: return self.nb_to_load = 0 self.core.call_all("on_progress", "loading_page_sizes", 1.0) def pageview_refresh_all(self): for page in self.pages: page.refresh() def on_screenshot_before(self): LOGGER.info("Blurring pages") for page in self.pages: page.blur() def on_screenshot_after(self): LOGGER.info("Unblurring pages") for page in self.pages: page.unblur() paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/boxes/000077500000000000000000000000001417573700700305465ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/boxes/__init__.py000066400000000000000000000215131417573700700326610ustar00rootroot00000000000000import logging try: import gi gi.require_version('Pango', '1.0') gi.require_version('PangoCairo', '1.0') from gi.repository import Pango from gi.repository import PangoCairo PANGO_AVAILABLE = True except (ImportError, ValueError): PANGO_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps import openpaperwork_core.promise from ..... import _ LOGGER = logging.getLogger(__name__) DELAY = 0.1 class NBox(object): """ Chained boxes. Useful for some plugins like boxes.selection. """ def __init__(self, box, index): self.box = box self.next = None self.index = index class Plugin(openpaperwork_core.PluginBase): """ Load the boxes on the pages and notify them to other plugins. Do nothing else. See other plugins in paperwork_gtk.mainwindow.docview.pageview.boxes to have something actually happening. """ def __init__(self): super().__init__() self.cache = {} self.running_promises = {} self.nb_to_load = 0 self.nb_loaded = 0 def get_interfaces(self): return [ 'chkdeps', 'gtk_pageview_boxes', ] def get_deps(self): return [ { "interface": "gtk_pageview", "defaults": ["paperwork_gtk.mainwindow.docview.pageview"], }, { 'interface': 'spatial_index', 'defaults': ['openpaperwork_core.spatial.rtree'], }, { 'interface': 'work_queue', 'defaults': ['openpaperwork_core.work_queue.default'], }, ] def chkdeps(self, out: dict): if not PANGO_AVAILABLE: out['pango'].update(openpaperwork_core.deps.PANGO) def _upd_progress(self): if self.nb_to_load <= self.nb_loaded or self.nb_to_load == 0: self.core.call_all("on_progress", "boxes", 1.0) self.nb_to_load = 0 self.nb_loaded = 0 return self.core.call_all( "on_progress", "boxes", self.nb_loaded / self.nb_to_load, _("Loading text ...") ) def doc_close(self): self.cache = {} self.nb_to_load = 0 self.nb_loaded = 0 self._upd_progress() def doc_open(self, *args, **kwargs): self.doc_close() def _index_boxes(self, boxes): # Tesseract seems (seemed ?) to have a bug: boxes taking the whole # page. --> we remove them. # Also we strip empty boxes. boxes = [ line_box for line_box in boxes if line_box.position[0][0] > 0 or line_box.position[0][1] > 0 ] for line_box in boxes: line_box.word_boxes = [ word_box for word_box in line_box.word_boxes if ((word_box.position[0][0] > 0 or word_box.position[0][1] > 0) and word_box.content.strip() != "") ] # chain the boxes chained_boxes = [] pbox = None index = 0 for line in boxes: for word in line.word_boxes: new_box = NBox(word, index) index += 1 if pbox is not None: pbox.next = new_box chained_boxes.append(pbox) pbox = new_box if pbox is not None: chained_boxes.append(pbox) # and then index them spatial_index = self.core.call_success( "spatial_indexer_get", [ (b.box.position, b) for b in chained_boxes ] ) return (boxes, spatial_index) def _set_boxes(self, boxes, page): (boxes, spatial_index) = boxes ref = (page.doc_id, page.page_idx) self.cache[ref] = (boxes, spatial_index) return (boxes, spatial_index) def pageview_get_boxes_by_id(self, doc_id, page_idx): ref = (doc_id, page_idx) if ref not in self.cache: return None return self.cache[ref][0] def pageview_get_indexed_boxes_by_id(self, doc_id, page_idx): ref = (doc_id, page_idx) if ref not in self.cache: return None return self.cache[ref][1] def on_page_visibility_changed(self, page, visible): ref = (page.doc_id, page.page_idx) if not visible: promise = self.running_promises.pop(ref, None) if promise is not None: self.core.call_all("work_queue_cancel", "page_loader", promise) self.nb_to_load -= 1 self._upd_progress() if ref in self.cache: self.cache.pop(ref) return if ref in self.cache: return # even they are not yet loaded, they will soon be --> we can mark them # as loaded self.cache[ref] = (None, None) # Gives back a bit of CPU time to GTK so the GUI remains # usable promise = openpaperwork_core.promise.DelayPromise(self.core, DELAY) promise = promise.then( LOGGER.debug, "Loading boxes of %s p%d", page.doc_id, page.page_idx ) # drop the returned value promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(openpaperwork_core.promise.ThreadedPromise( self.core, self.core.call_success, args=("page_get_boxes_by_url", page.doc_url, page.page_idx,) )) promise = promise.then(openpaperwork_core.promise.ThreadedPromise( self.core, lambda boxes=[]: self._index_boxes(boxes) )) promise = promise.then(lambda boxes: self._set_boxes(boxes, page)) promise = promise.then(lambda boxes: self.core.call_all( # boxes => (boxes, spatial_index) "on_page_boxes_loaded", page, boxes[0], boxes[1] )) def stop_promise_tracking(*args, **kwargs): if ref in self.running_promises: self.nb_loaded += 1 self._upd_progress() self.running_promises.pop(ref) promise = promise.then(stop_promise_tracking) self.nb_to_load += 1 self._upd_progress() self.running_promises[ref] = promise # piggy back page loader work queue, but with a low priority self.core.call_success( "work_queue_add_promise", "page_loader", promise, priority=-10 ) def on_page_boxes_loaded(self, page, boxes, spatial_index): LOGGER.info( "Page %s %d: %d line boxes loaded", page.doc_id, page.page_idx, len(boxes) ) def _paint_txt(self, cairo_ctx, txt, x, y, w, h): cairo_ctx.set_source_rgb(1.0, 1.0, 1.0) cairo_ctx.rectangle(x, y, w, h) cairo_ctx.fill() layout = PangoCairo.create_layout(cairo_ctx) layout.set_text(txt, -1) txt_size = layout.get_size() if 0 in txt_size: return cairo_ctx.save() try: txt_factor = min( float(w) * Pango.SCALE / txt_size[0], float(h) * Pango.SCALE / txt_size[1], ) cairo_ctx.set_source_rgb(0, 0, 0) cairo_ctx.translate(x, y) # make the text use the whole box space cairo_ctx.scale(txt_factor, txt_factor) PangoCairo.update_layout(cairo_ctx, layout) PangoCairo.show_layout(cairo_ctx, layout) finally: cairo_ctx.restore() def _page_draw_box( self, cairo_ctx, page, box_position, border_color, border_width=2, box_content=None): zoom = page.zoom ((tl_x, tl_y), (br_x, br_y)) = box_position tl_x *= zoom tl_y *= zoom br_x *= zoom br_y *= zoom w = br_x - tl_x h = br_y - tl_y if box_content is not None: self._paint_txt(cairo_ctx, box_content, tl_x, tl_y, w, h) cairo_ctx.save() try: cairo_ctx.set_source_rgb( border_color[0], border_color[1], border_color[2] ) cairo_ctx.set_line_width(border_width) cairo_ctx.rectangle( tl_x - (border_width / 2), tl_y - (border_width / 2), w + border_width, h + border_width ) cairo_ctx.stroke() finally: cairo_ctx.restore() def page_draw_box(self, *args, **kwargs): # WORKAROUND(Jflesch): with some malformed PDF file, we get unexpected # exceptions. See: # https://gitlab.gnome.org/World/OpenPaperwork/paperwork/-/issues/913 # https://gitlab.freedesktop.org/poppler/poppler/-/issues/1020 try: self._page_draw_box(*args, **kwargs) except Exception as exc: LOGGER.error("page_draw_box() failed", exc_info=exc) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/boxes/all.py000066400000000000000000000027621417573700700316770ustar00rootroot00000000000000import logging import openpaperwork_core LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 1000 def __init__(self): super().__init__() self.visible = False def get_interfaces(self): return [ 'gtk_pageview_boxes_all', 'gtk_pageview_boxes_listener', ] def get_deps(self): return [ { 'interface': 'gtk_pageview', 'defaults': ['paperwork_gtk.mainwindow.docview.pageview'], }, { 'interface': 'gtk_pageview_boxes', 'defaults': [ 'paperwork_gtk.mainwindow.docview.pageview.boxes' ], }, ] def set_all_boxes_visible(self, visible): self.visible = visible self.core.call_all("pageview_refresh_all") def on_page_boxes_loaded(self, page, boxes, spatial_index): page.refresh() def on_page_draw(self, cairo_ctx, page): if not self.visible: return boxes = self.core.call_success( "pageview_get_boxes_by_id", page.doc_id, page.page_idx ) if boxes is None: return for line_box in boxes: for word_box in line_box.word_boxes: self.core.call_all( "page_draw_box", cairo_ctx, page, word_box.position, (0.0, 0.0, 1.0), border_width=1 ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/boxes/hover.py000066400000000000000000000102651417573700700322470ustar00rootroot00000000000000import logging try: import gi gi.require_version('Gdk', '3.0') from gi.repository import Gdk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_gtk.deps LOGGER = logging.getLogger(__name__) class PageHoverHandler(object): def __init__(self, core, page): self.core = core self.page = page self.boxes = None self.actives = [] self.realize_handler_id = None self.motion_handler_id = None def set_boxes(self, boxes, spatial_index): self.boxes = spatial_index def connect(self): assert(self.realize_handler_id is None) assert(self.motion_handler_id is None) self.realize_handler_id = self.page.widget.connect( "realize", self.on_realize ) self.motion_handler_id = self.page.widget.connect( "motion-notify-event", self.on_motion ) self.on_realize() def disconnect(self): assert(self.realize_handler_id is not None) assert(self.motion_handler_id is not None) self.page.widget.disconnect(self.realize_handler_id) self.page.widget.disconnect(self.motion_handler_id) self.realize_handler_id = None self.motion_handler_id = None def on_realize(self, widget=None): mask = Gdk.EventMask.POINTER_MOTION_MASK self.page.widget.add_events(mask) if self.page.widget.get_window() is not None: self.page.widget.get_window().set_events( self.page.widget.get_window().get_events() | mask ) def on_motion(self, widget, event): if self.boxes is None: self.actives = [] return zoom = self.page.get_zoom() x = int(event.x / zoom) y = int(event.y / zoom) actives = [(a[0], a[1].box) for a in self.boxes.get_boxes(x, y)] if len(actives) > 1: # will sort smaller areas last actives = [ (abs((p[1][0] - p[0][0]) * (p[1][1] - p[0][1])), b) for (p, b) in actives ] actives.sort(reverse=True) actives = [ a[1] for a in actives ] if actives != self.actives: self.page.widget.queue_draw() self.actives = actives def draw(self, cairo_ctx): for box in self.actives: self.core.call_all( "page_draw_box", cairo_ctx, self.page, box.position, (0.0, 0.0, 1.0), border_width=2, box_content=box.content ) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 10 def __init__(self): super().__init__() self.handlers = {} def get_interfaces(self): return [ 'chkdeps', 'gtk_pageview_boxes_listener', ] def get_deps(self): return [ { 'interface': 'gtk_pageview', 'defaults': ['paperwork_gtk.mainwindow.docview.pageview'], }, { 'interface': 'gtk_pageview_boxes', 'defaults': [ 'paperwork_gtk.mainwindow.docview.pageview.boxes' ], }, ] def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def on_docview_closed_page(self, page): self.handlers.pop(page.widget).disconnect() def on_page_boxes_loaded(self, page, boxes, spatial_index): h = PageHoverHandler(self.core, page) self.handlers[page.widget] = h assert(page.widget in self.handlers) h = self.handlers[page.widget] h.set_boxes(boxes, spatial_index) if page.get_visible(): h.connect() def on_page_visibility_changed(self, page, visible): if page.widget not in self.handlers: return h = self.handlers[page.widget] if visible: h.connect() else: h.disconnect() def on_page_draw(self, cairo_ctx, page): if page.widget not in self.handlers: return self.handlers[page.widget].draw(cairo_ctx) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/boxes/search.py000066400000000000000000000046631417573700700323760ustar00rootroot00000000000000import logging import re import openpaperwork_core LOGGER = logging.getLogger(__name__) MIN_WORD_LENGTH = 3 SPLIT = r"\W+" class Plugin(openpaperwork_core.PluginBase): PRIORITY = 100 # those are keywords used in python-whoosh query syntax. There is no # point in highlighting them IGNORE_LIST = {"and", "or"} def __init__(self): super().__init__() self.re_split = re.compile(SPLIT) self.keywords = set() def get_interfaces(self): return [ 'gtk_pageview_boxes_listener', 'search_listener', ] def get_deps(self): return [ { 'interface': 'gtk_pageview', 'defaults': ['paperwork_gtk.mainwindow.docview.pageview'], }, { 'interface': 'gtk_pageview_boxes', 'defaults': [ 'paperwork_gtk.mainwindow.docview.pageview.boxes' ], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, ] def on_search_start(self, query): query = self.re_split.split( self.core.call_success("i18n_strip_accents", query) ) self.keywords = { keyword.lower() for keyword in query if len(keyword) >= MIN_WORD_LENGTH } self.core.call_all("pageview_refresh_all") def on_search_results(self, query, docs): pass def on_page_draw(self, cairo_ctx, page): if len(self.keywords) <= 0: return boxes = self.core.call_success( "pageview_get_boxes_by_id", page.doc_id, page.page_idx ) if boxes is None: return for line_box in boxes: for word_box in line_box.word_boxes: word = self.core.call_success( "i18n_strip_accents", word_box.content ) word = word.strip() if word.lower() in self.IGNORE_LIST: continue for w in self.re_split.split(word): w = w.lower() if w not in self.keywords: continue self.core.call_all( "page_draw_box", cairo_ctx, page, word_box.position, (0.0, 1.0, 0.0), border_width=2 ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/boxes/selection.py000066400000000000000000000130261417573700700331070ustar00rootroot00000000000000import itertools import logging try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_gtk.deps LOGGER = logging.getLogger(__name__) IDX_GENERATOR = itertools.count() class PageSelectionHandler(object): def __init__(self, core, plugin, page): self.core = core self.plugin = plugin self.page = page self.actives = [] self.boxes = None self.orig = (-1, -1) self.first = None self.last = None self.gesture = Gtk.GestureDrag.new(page.widget) self.gesture.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) self.signal_handlers = [] def set_boxes(self, line_boxes, spatial_index): self.boxes = spatial_index def connect(self): self.disconnect() handlers = ( ('drag-begin', self.on_drag_begin), ('drag-update', self.on_drag_update), ('drag-end', self.on_drag_end), ) for (signal, cb) in handlers: self.signal_handlers.append( (signal, self.gesture.connect(signal, cb)) ) def disconnect(self): for (signal, handler_id) in self.signal_handlers: self.gesture.disconnect(handler_id) self.signal_handlers = [] def _find_box(self, x, y): if self.boxes is None: return None zoom = self.page.get_zoom() x /= zoom y /= zoom nboxes = self.boxes.get_boxes(x, y) # take the box with the smallest area try: nbox = min({ ( abs( (n[1].box.position[1][0] - n[1].box.position[0][0]) * (n[1].box.position[1][1] - n[1].box.position[0][1]) ), n[1] ) for n in nboxes })[1] except ValueError: return None return nbox def _get_selected(self): if self.first is None: return [] if self.first.index <= self.last.index: first = self.first last = self.last else: first = self.last last = self.first current = first while current is not None and current != last: yield current.box current = current.next yield last.box def on_drag_begin(self, gesture, x, y): if not self.plugin.enabled: self.first = None self.last = None return self.actives = [] self.orig = (x, y) self.first = self._find_box(x, y) self.last = self.first def on_drag_update(self, gesture, x, y): if self.first is None: return (x, y) = (self.orig[0] + x, self.orig[1] + y) last = self._find_box(x, y) if last is None or last == self.last: return self.last = last self.page.widget.queue_draw() LOGGER.info( "Text selection: first: %d ; last: %d", self.first.index, self.last.index ) def on_drag_end(self, gesture, x, y): if self.first is None: return self.on_drag_update(gesture, x, y) self.core.call_all( "on_page_boxes_selected", self.page.doc_id, self.page.doc_url, self.page.page_idx, list(self._get_selected()) ) def draw(self, cairo_ctx): if not self.plugin.enabled: return for box in self._get_selected(): self.core.call_all( "page_draw_box", cairo_ctx, self.page, box.position, (0.0, 1.0, 0.0), border_width=2 ) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 10 def __init__(self): super().__init__() self.handlers = {} self.enabled = False def get_interfaces(self): return [ 'chkdeps', 'gtk_pageview_boxes_listener', ] def get_deps(self): return [ { 'interface': 'gtk_pageview', 'defaults': ['paperwork_gtk.mainwindow.docview.pageview'], }, { 'interface': 'gtk_pageview_boxes', 'defaults': [ 'paperwork_gtk.mainwindow.docview.pageview.boxes' ], }, ] def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def on_docview_closed_page(self, page): self.handlers.pop(page.widget).disconnect() def on_page_boxes_loaded(self, page, boxes, spatial_index): h = PageSelectionHandler(self.core, self, page) self.handlers[page.widget] = h assert(page.widget in self.handlers) h = self.handlers[page.widget] h.set_boxes(boxes, spatial_index) if page.get_visible(): h.connect() def on_page_visibility_changed(self, page, visible): if page.widget not in self.handlers: return h = self.handlers[page.widget] if visible: h.connect() else: h.disconnect() def on_page_draw(self, cairo_ctx, page): if page.widget not in self.handlers: return self.handlers[page.widget].draw(cairo_ctx) def on_layout_change(self, layout_name): self.enabled = (layout_name == 'paged') paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/pageview/pageview.glade000066400000000000000000000005021417573700700322300ustar00rootroot00000000000000 True False paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/progress.py000066400000000000000000000014011417573700700300310ustar00rootroot00000000000000import openpaperwork_core class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [] def get_deps(self): return [ { 'interface': 'gtk_progress_widget', 'defaults': ['openpaperwork_gtk.widgets.progress'], }, { 'interface': 'gtk_docview', 'defaults': ['paperwork_gtk.mainwindow.docview'], }, ] def init(self, core): super().init(core) widget = self.core.call_success("gtk_progress_make_widget") if widget is None: # GTK is not available return header_bar = self.core.call_success("docview_get_headerbar") header_bar.pack_end(widget) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/scanview/000077500000000000000000000000001417573700700274365ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/scanview/__init__.py000066400000000000000000000166241417573700700315600ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.deps try: from gi.repository import GObject GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False # workaround so chkdeps can still be called class GObject(object): TYPE_BOOLEAN = 0 class SignalFlags(object): RUN_LAST = 0 class GObject(object): pass LOGGER = logging.getLogger(__name__) class Scan(GObject.GObject): """ Implements the same interface than the mainwindow.docview.pageview.Page objects, but display any scan relative to the current document. """ __gsignals__ = { # never emitted 'getting_size': (GObject.SignalFlags.RUN_LAST, None, ()), # emitted immediately and when a scan is started 'size_obtained': (GObject.SignalFlags.RUN_LAST, None, ()), # never emitted 'img_obtained': (GObject.SignalFlags.RUN_LAST, None, ()), 'visibility_changed': (GObject.SignalFlags.RUN_LAST, None, ( # TODO(Jflesch): not implemented here (never emitted), but not # used anywhere anyway. GObject.TYPE_BOOLEAN, )), } def __init__(self, core, plugin, doc_id, page_idx, scan_id=None): super().__init__() self.core = core self.plugin = plugin self.doc_id = doc_id self.zoom = 1.0 self.page_idx = page_idx self.scan_id = scan_id if scan_id is not None: scan_id = self.core.call_success( "draw_scan_get_max_size", self.scan_id ) self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.docview.scanview", "scanview.glade" ) self.widget = self.widget_tree.get_object("scanview_area") self.visible = False def __str__(self): return "Scan renderer ({})".format(self.doc_id) def set_visible(self, visible): if visible == self.visible: return LOGGER.info("Scan renderer: Visible: {}".format(visible)) self.visible = visible def start(self): LOGGER.info("Scan renderer: start") size = self.get_size() self.widget.set_size_request(size[0], size[1]) self.widget.queue_resize() self.core.call_all("draw_scan_start", self.widget, self.scan_id) self.widget.set_visible(True) def stop(self): LOGGER.info("Scan renderer: stop") self.core.call_all("draw_scan_stop", self.widget) self.widget.set_visible(False) def get_visible(self): return self.visible def load(self): # the docview rely on this signal to know when pagss have been loaded # (in other words, when their size have been defined). # It remains in state 'loading' as long as not all the pages have # reported having be loaded --> we report immediately ourselves # as loaded self.core.call_all("on_page_size_obtained", self) self.emit("size_obtained") def close(self): pass def get_zoom(self): return self.zoom def set_zoom(self, zoom): self.zoom = zoom self.resize() def get_full_size(self): if self.scan_id is None: return (0, 0) return self.plugin.scan_sizes.get(self.scan_id, (1, 1)) def get_size(self): size = self.get_full_size() return ( int(size[0] * self.zoom), int(size[1] * self.zoom) ) def resize(self): if self.scan_id is None: return size = self.get_size() self.widget.set_size_request(size[0], size[1]) self.core.call_all("on_page_size_obtained", self) self.emit("size_obtained") self.widget.queue_resize() def refresh(self, reload=False): self.widget.queue_draw() def hide(self): pass def show(self): pass if GLIB_AVAILABLE: GObject.type_register(Scan) class Plugin(openpaperwork_core.PluginBase): PRIORITY = -10000 def __init__(self): super().__init__() self.scan = None self.doc_id = None self.doc_url = None self.scan_sizes = {} def get_interfaces(self): return [ 'gtk_pageview', ] def get_deps(self): return [ { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'gtk_drawer_scan', 'defaults': [ 'openpaperwork_gtk.drawer.scan', 'paperwork_gtk.drawer.calibration', ], }, ] def doc_open_components(self, out: list, doc_id, doc_url): self.doc_id = doc_id self.doc_url = doc_url nb_pages = self.core.call_success("doc_get_nb_pages_by_url", doc_url) if nb_pages is None: nb_pages = 0 scan_id = self.core.call_success("scan2doc_doc_id_to_scan_id", doc_id) self.scan = Scan(self.core, self, doc_id, nb_pages, scan_id) if scan_id is None: out.append((False, self.scan)) else: out.append((True, self.scan)) self.scan.start() def on_scan2doc_start(self, scan_id, doc_id, doc_url): if self.scan is None: return if doc_id != self.doc_id: return self.scan.scan_id = scan_id def on_scan_feed_start(self, scan_id): if self.scan is None: return if scan_id != self.scan.scan_id: return self.scan.start() self.core.call_all("docview_show_page_viewer", self.scan) def on_scan_page_start(self, scan_id, page_nb, scan_params): self.scan_sizes[scan_id] = ( scan_params.get_width(), scan_params.get_height() ) if self.scan is None: return if scan_id != self.scan.scan_id: return LOGGER.info("Scan size: %dx%d", *(self.scan_sizes[scan_id])) self.scan.resize() nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", self.doc_url ) if nb_pages is None: nb_pages = 0 handle_id = None def goto_page(*args, **kwargs): self.scan.widget.disconnect(handle_id) self.core.call_all("doc_goto_page", nb_pages) self.core.call_all("doc_goto_page", nb_pages) handle_id = self.scan.widget.connect("size-allocate", goto_page) def on_scan2doc_page_scanned(self, scan_id, doc_id, doc_url, page_idx): if self.scan is None: return if scan_id != self.scan.scan_id: return nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", self.doc_url ) assert(nb_pages is not None) LOGGER.info("Displaying new page %d", nb_pages - 1) self.core.call_all("doc_goto_page", nb_pages - 1) def on_scan_feed_end(self, scan_id): self.scan_sizes.pop(scan_id, None) if self.scan is None: return if scan_id != self.scan.scan_id: return self.core.call_all("docview_hide_page_viewer", self.scan) self.scan.stop() def on_scan2doc_end(self, scan_id, doc_id, doc_url): if self.scan is None: return if doc_id != self.doc_id: return self.scan.scan_id = None paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/docview/scanview/scanview.glade000066400000000000000000000004361417573700700322560ustar00rootroot00000000000000 False False paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/000077500000000000000000000000001417573700700260275ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/__init__.py000066400000000000000000000621531417573700700301470ustar00rootroot00000000000000import logging try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_core.promise import openpaperwork_gtk.deps import paperwork_backend.docexport from ... import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.ui = None self.active_doc = None self.active_page_idx = None self.windows = [] self.button_validate = None self.button_send_email = None self.preciew = None self.zoom = None self.quality = None self.combobox_page_format = None self.model_page_format = None # The reference ('ref_') is what is displayed as example to the user. # It's always only one page. The exported document size is estimated # by multiplying this reference page after post-processing by the # number of pages in the document. # Inputs (export_input_*) refer to export pipe inputs # (see paperwork_backend.docexport) self.ref_input_page = None self.ref_input_doc = None self.export_input_type = None self.export_input = None self.export_input_doc_urls = None self.need_zoom_auto = True self.pipeline = [] self.renderer = None self.tmp_file_url = None def get_interfaces(self): return [ 'chkdeps', 'doc_open', 'gtk_exporter', 'gtk_window_listener', ] def get_deps(self): return [ { 'interface': 'cairo_url', 'defaults': [ 'paperwork_backend.cairo.pillow', 'paperwork_backend.cairo.poppler', ], }, { 'interface': 'export_pipes', 'defaults': [ 'paperwork_backend.docexport.img', 'paperwork_backend.docexport.pdf', 'paperwork_backend.docexport.pillowfight', ], }, { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, { 'interface': 'gtk_mainwindow', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'gtk_zoomable', 'defaults': ['paperwork_gtk.gesture.zoom'], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, { 'interface': 'notifications', 'defaults': [ 'paperwork_gtk.notifications.dialog', 'paperwork_gtk.notifications.notify', ], }, { 'interface': 'work_queue', 'defaults': ['openpaperwork_core.work_queue.default'], }, { 'interface': 'external_apps', 'defaults': [ 'openpaperwork_core.external_apps.dbus', 'openpaperwork_core.external_apps.windows', 'openpaperwork_core.external_apps.xdg', ], }, ] def init(self, core): super().init(core) self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.exporter", "exporter.glade" ) if self.widget_tree is None: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to load widget tree") return self.widget_tree.get_object("exporter_cancel").connect( "clicked", self._on_cancel ) self.button_validate = self.widget_tree.get_object("exporter_validate") self.button_validate.connect( "clicked", self._on_apply ) self.button_send_email = self.widget_tree.get_object( "exporter_send_email" ) self.button_send_email.connect( "clicked", self._on_send_email ) can_send_as_attachment = core.call_success( "external_app_can_send_as_attachment" ) if not can_send_as_attachment: self.button_send_email.set_visible(False) self.zoom = self.widget_tree.get_object("exporter_zoom_adjustment") self.zoom.connect("value-changed", self._on_zoom_changed) self.quality = self.widget_tree.get_object( "exporter_quality_adjustment" ) self.quality.connect("value-changed", self._on_quality_changed) self.preview = self.widget_tree.get_object("exporter_img") self.preview.connect("draw", self._on_draw) self.core.call_all( "on_zoomable_widget_new", self.widget_tree.get_object("exporter_scroll"), self.zoom ) self.core.call_all( "mainwindow_add", "right", "exporter", prio=0, header=self.widget_tree.get_object("exporter_header"), body=self.widget_tree.get_object("exporter_body") ) self.core.call_success("work_queue_create", "exporter") self.combobox_page_format = self.widget_tree.get_object( "exporter_page_format" ) self.model_page_format = self.widget_tree.get_object( "exporter_page_format_model" ) self._add_page_formats() self.combobox_page_format.connect( "changed", self._on_page_format_changed ) def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def on_page_shown(self, page_idx): self.active_page_idx = page_idx def _add_page_formats(self): default_idx = -1 for (idx, paper_size) in enumerate( Gtk.PaperSize.get_paper_sizes(True)): store_data = ( paper_size.get_display_name(), paper_size.get_width(Gtk.Unit.POINTS), paper_size.get_height(Gtk.Unit.POINTS) ) self.model_page_format.append(store_data) if paper_size.get_name() == paper_size.get_default(): default_idx = idx if default_idx >= 0: self.combobox_page_format.set_active(default_idx) def _get_possible_pipes(self, input_type, active_pipe=""): pipes = [] if input_type == paperwork_backend.docexport.ExportDataType.DOCUMENT: self.core.call_all( "export_get_pipes_by_doc_urls", pipes, self.export_input_doc_urls ) else: self.core.call_all("export_get_pipes_by_input", pipes, input_type) pipeline = {p.name for p in self.pipeline} if active_pipe in pipeline: pipeline.remove(active_pipe) pipes = [p for p in pipes if p.name not in pipeline] return pipes def _expand_pipeline(self): """ When there is only once choice possible, this isn't really a choice. --> we expand automatically the pipeline to include the only choice possible. """ while True: if len(self.pipeline) > 0: last_output_type = self.pipeline[-1].output_type else: last_output_type = self.export_input_type if last_output_type == 'file_url': # pipeline is complete return pipes = self._get_possible_pipes(last_output_type) if len(pipes) != 1: # there is a choice (or none at all), nothing to do return pipe = pipes[0] assert(pipe.name not in (p.name for p in self.pipeline)) self.pipeline.append(pipe) LOGGER.info("Pipeline expanded: %s", [ p.name for p in self.pipeline ]) def _add_combobox(self, steps, previous_input_type, active_pipe): possible_alternative_pipes = self._get_possible_pipes( previous_input_type, active_pipe ) if len(possible_alternative_pipes) <= 1: # no really a choice return possible_alternative_pipes.sort(key=lambda p: str(p)) widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.exporter", "pipe.glade" ) combobox = widget_tree.get_object("exporter_pipe") choices = widget_tree.get_object("exporter_pipe_model") choices.clear() choices.append(("", "")) active_idx = 0 for (alternative_idx, alternative_pipe) in enumerate( possible_alternative_pipes): choices.append( (str(alternative_pipe), alternative_pipe.name) ) if alternative_pipe.name == active_pipe: active_idx = alternative_idx + 1 combobox.set_active(active_idx) combobox.connect('changed', self._on_pipeline_changed) steps.add(combobox) def _refresh_pipeline_ui(self): LOGGER.info("Displaying pipeline: %s", [p.name for p in self.pipeline]) steps = self.widget_tree.get_object("exporter_steps") for widget in steps: steps.remove(widget) for (pipe_idx, pipe) in enumerate(self.pipeline): if pipe_idx <= 0: previous_input_type = self.export_input_type else: previous_input_type = self.pipeline[pipe_idx - 1].output_type self._add_combobox(steps, previous_input_type, pipe.name) if len(self.pipeline) > 0: last_output_type = self.pipeline[-1].output_type LOGGER.info( "Last pipe: %s ; Last output type: %s", self.pipeline[-1], last_output_type ) else: last_output_type = self.export_input_type LOGGER.info("Input type: %s", last_output_type) t = paperwork_backend.docexport.ExportDataType.OUTPUT_URL_FILE if last_output_type == t: self.button_validate.set_sensitive(True) self.button_send_email.set_sensitive(True) else: self._add_combobox(steps, last_output_type, "") self.button_validate.set_sensitive(False) self.button_send_email.set_sensitive(False) def _rebuild_pipeline_from_ui(self): LOGGER.info("Rebuilding pipeline from UI") self.pipeline = [] steps = self.widget_tree.get_object("exporter_steps") for combobox in steps: self._expand_pipeline() pipe = combobox.get_active() pipe = combobox.get_model()[pipe][1] if pipe == "": return pipe = self.core.call_success("export_get_pipe_by_name", pipe) self.pipeline.append(pipe) LOGGER.info("Pipeline from UI: %s", [p.name for p in self.pipeline]) def _hide_preview(self): if self.tmp_file_url is not None: self.core.call_success("fs_unlink", self.tmp_file_url, trash=False) self.tmp_file_url = None if self.renderer is not None: self.renderer.close() self.renderer = None label = self.widget_tree.get_object("exporter_estimated_size") label.set_text("") label.set_visible(False) def _resize_preview(self): self.preview.set_size_request( int(self.renderer.size[0] * self.renderer.zoom), int(self.renderer.size[1] * self.renderer.zoom) ) def _on_size_obtained(self, *args, **kwargs): if self.need_zoom_auto: allocation = self.widget_tree.get_object( "exporter_scroll" ).get_allocation() allocation = (allocation.width - 20, allocation.height - 20) zoom = min( allocation[0] / self.renderer.size[0], allocation[1] / self.renderer.size[1], ) self.zoom.set_value(zoom) self.renderer.zoom = zoom self.need_zoom_auto = False self._resize_preview() def _redraw_preview(self, renderer): self.preview.queue_draw() def _show_preview(self, tmp_file_urls): tmp_file_url = list(tmp_file_urls)[0] LOGGER.info("Preview: %s", tmp_file_url) self.tmp_file_url = tmp_file_url self.renderer = self.core.call_success( "cairo_renderer_by_url", "exporter", tmp_file_url ) self.renderer.zoom = self.zoom.get_value() self.renderer.connect("size_obtained", self._on_size_obtained) self.renderer.connect("img_obtained", self._redraw_preview) self.renderer.start() self.renderer.render() return tmp_file_url def _show_estimated_size(self, tmp_file_url): preview_size = self.core.call_success("fs_getsize", tmp_file_url) factors = ( p.get_estimated_size_factor(self.export_input) for p in self.pipeline ) final_size = preview_size for f in factors: final_size *= f LOGGER.info( "Preview size: %d ; Estimated final size: %d", preview_size, final_size ) final_size = self.core.call_success("i18n_file_size", final_size) label_txt = _("Estimated file size: %s") % (final_size) label = self.widget_tree.get_object("exporter_estimated_size") label.set_text(label_txt) label.set_visible(True) return tmp_file_url def _get_pipe_plug(self): if self.pipeline[-1].output_type == 'pages': return self.core.call_success( "export_get_pipe_by_name", "png" ) return None def _set_quality(self): can_change_quality = False for pipe in self.pipeline: if not pipe.can_change_quality: continue can_change_quality = True pipe.set_quality(self.quality.get_value() / 100) self.widget_tree.get_object("exporter_quality").set_sensitive( can_change_quality ) self.widget_tree.get_object("exporter_quality_label").set_sensitive( can_change_quality ) def _set_page_format(self): page_format = self.combobox_page_format.get_active() page_format = self.model_page_format[page_format] can_change_page_format = False for pipe in self.pipeline: if not pipe.can_change_page_format: continue can_change_page_format = True pipe.set_page_format((page_format[1], page_format[2])) self.widget_tree.get_object("exporter_page_format").set_sensitive( can_change_page_format ) self.widget_tree.get_object( "exporter_page_format_label" ).set_sensitive( can_change_page_format ) def _reload_preview(self): self.core.call_all("work_queue_cancel_all", "exporter") promise = openpaperwork_core.promise.Promise( self.core, self._hide_preview ) if len(self.pipeline) <= 0: self.core.call_success( "work_queue_add_promise", "exporter", promise ) return # some pipeline only accepts ExportDataType.DOCUMENT as input, # other accept ExportDataType.PAGE, but not immediately. # For the preview, we prefer ExportDataType.PAGE, # if not available we fall back to ExportDataType.DOCUMENT for (idx, pipe) in enumerate(self.pipeline): if (pipe.input_type == paperwork_backend.docexport.ExportDataType.PAGE): pipeline = self.pipeline[idx:] ref_input = self.ref_input_page break else: for (idx, pipe) in enumerate(self.pipeline): if (pipe.input_type == paperwork_backend.docexport.ExportDataType.DOCUMENT): pipeline = self.pipeline[idx:] ref_input = self.ref_input_doc break else: LOGGER.warning( "Can't display export preview:" " No matching input pipe found in %s", [str(p) for p in self.pipeline] ) return ref_input = ref_input.clone() pipe_plug = self._get_pipe_plug() if pipe_plug is not None: if pipe_plug.can_change_quality: pipe_plug.set_quality(self.quality.get_value() / 100) if pipe_plug.can_change_page_format: page_format = self.combobox_page_format.get_active() page_format = self.model_page_format[page_format] pipe_plug.set_page_format((page_format[1], page_format[2])) t = paperwork_backend.docexport.ExportDataType.OUTPUT_URL_FILE if pipe_plug is None and pipeline[-1].output_type != t: LOGGER.warning( "Can't display export preview: unexpected pipe end: %s", pipeline[-1].output_type ) self.core.call_success( "work_queue_add_promise", "exporter", promise ) return promise = promise.then(self.core.call_all, "on_busy") promise = promise.then(lambda *args, **kwargs: ref_input) for pipe in pipeline: promise = promise.then(pipe.get_promise(result='preview')) if pipe_plug is not None: promise = promise.then(pipe_plug.get_promise(result='preview')) promise = promise.then(self._show_preview) if pipe_plug is None: promise = promise.then(self._show_estimated_size) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(self.core.call_all, "on_idle") self.core.call_success("work_queue_add_promise", "exporter", promise) def _gtk_open_exporter(self): self.pipeline = [] self.need_zoom_auto = True self._expand_pipeline() self._refresh_pipeline_ui() self._set_quality() self._set_page_format() self._reload_preview() self.core.call_all("mainwindow_show", "right", "exporter") def gtk_open_exporter(self, doc_id, doc_url, page_idx=None): ref_page_idx = 0 if page_idx is not None: ref_page_idx = page_idx elif doc_url == self.active_doc[1]: ref_page_idx = self.active_page_idx self.ref_input_page = ( paperwork_backend.docexport.ExportData.build_page( doc_id, doc_url, ref_page_idx ) ) self.ref_input_doc = ( paperwork_backend.docexport.ExportData.build_doc(doc_id, doc_url) ) self.export_input_type = ( paperwork_backend.docexport.ExportDataType.DOCUMENT if page_idx is None else paperwork_backend.docexport.ExportDataType.PAGE ) self.export_input = ( paperwork_backend.docexport.ExportData.build_doc(doc_id, doc_url) if page_idx is None else paperwork_backend.docexport.ExportData.build_page( doc_id, doc_url, page_idx ) ) self.export_input_doc_urls = [doc_url] self._gtk_open_exporter() def gtk_open_exporter_multiple_docs( self, docs, ref_doc_id, ref_doc_url, ref_page_idx): self.ref_input_page = ( paperwork_backend.docexport.ExportData.build_page( ref_doc_id, ref_doc_url, ref_page_idx ) ) self.ref_input_doc = ( paperwork_backend.docexport.ExportData.build_doc( ref_doc_id, ref_doc_url ) ) self.export_input_type = ( paperwork_backend.docexport.ExportDataType.DOCUMENT_SET ) self.export_input = ( paperwork_backend.docexport.ExportData.build_doc_set(docs) ) self.export_input_doc_urls = [doc[1] for doc in docs] self._gtk_open_exporter() def _on_draw(self, drawing_area, cairo_context): if self.renderer is None: return self.renderer.draw(cairo_context) def _on_zoom_changed(self, adj): if self.renderer is None: return self.renderer.zoom = adj.get_value() self._resize_preview() self.preview.queue_draw() def _on_quality_changed(self, adj): self._set_quality() self._reload_preview() def _on_page_format_changed(self, combobox): self._set_page_format() self._reload_preview() def _on_pipeline_changed(self, *args, **kwargs): self._rebuild_pipeline_from_ui() self._expand_pipeline() # WORKAROUND(JFlesch): call this method using mainloop_schedule. # Otherwise, in Flatpak, with Gnome 40, it crashes. self.core.call_one("mainloop_schedule", self._refresh_pipeline_ui) self._set_quality() self._set_page_format() self._reload_preview() def _on_cancel(self, button): LOGGER.info("Export canceled") self.core.call_all("mainwindow_back", side="right") def _on_apply(self, button): LOGGER.info("Export settings defined. Opening file chooser dialog") dialog = Gtk.FileChooserNative.new( _("Select a file or a directory to import"), self.windows[-1], Gtk.FileChooserAction.SAVE, None, None ) dialog.set_modal(True) dialog.set_local_only(False) file_filter = Gtk.FileFilter() file_filter.set_name(_("Any files")) file_filter.add_pattern("*.*") dialog.add_filter(file_filter) (mime, file_extensions) = self.pipeline[-1].get_output_mime() file_filter = Gtk.FileFilter() file_filter.add_mime_type(mime) file_filter.set_name(file_extensions[0]) # TODO(Jflesch): better name dialog.add_filter(file_filter) dialog.set_filter(file_filter) dialog.connect("response", self._on_dialog_response) dialog.run() dialog.hide() dialog.destroy() def _on_dialog_response(self, dialog, response_id): if (response_id != Gtk.ResponseType.ACCEPT and response_id != Gtk.ResponseType.OK and response_id != Gtk.ResponseType.YES and response_id != Gtk.ResponseType.APPLY): LOGGER.info("User canceled (response_id=%d)", response_id) return selected = dialog.get_uris()[0] self.core.call_all("mainwindow_back", side="right") # make sure the file extension is set (mime, file_extensions) = self.pipeline[-1].get_output_mime() for file_extension in file_extensions: if selected.lower().endswith(file_extension): break else: selected += "." + file_extensions[0] promise = openpaperwork_core.promise.Promise( self.core, lambda: self.export_input ) for pipe in self.pipeline: promise = promise.then(pipe.get_promise( result='final', target_file_url=selected )) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(Gtk.RecentManager().add_item, selected) promise = promise.then(lambda *args, **kwargs: None) promise = promise.catch(self._on_error) # do not use the work queue ; must never be canceled promise.schedule() def _on_send_email(self, button): (_, file_extensions) = self.pipeline[-1].get_output_mime() (target_file_url, fd) = self.core.call_success( "fs_mktemp", suffix="." + file_extensions[0], on_disk=True ) fd.close() # we only need the name self.core.call_all("mainwindow_back", side="right") promise = openpaperwork_core.promise.Promise( self.core, lambda: self.export_input ) for pipe in self.pipeline: promise = promise.then(pipe.get_promise( result='final', target_file_url=target_file_url )) promise = promise.then(lambda *args, **kwargs: self.core.call_success( "external_app_send_as_attachment", target_file_url )) promise = promise.then(lambda *args, **kwargs: None) promise = promise.catch(self._on_error) # do not use the work queue ; must never be canceled promise.schedule() def _on_error(self, exc): LOGGER.error("Export failed", exc_info=exc) notif = self.core.call_success( "get_notification_builder", _("Export has failed"), ) notif.set_message(f"Export has failed: {type(exc)} {exc}") notif.set_icon("dialog-error") notif.show() paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/exporter.glade000066400000000000000000000447631417573700700307130ustar00rootroot00000000000000 True False mail-message-new True False True :close 100 75 1 10 0.01 1.5 0.01 0.01 0.10 True False True False 10 10 10 10 vertical 20 True False 0 none True False 12 True False vertical True False 10 10 Export Steps False True 0 True False 0 none True False 12 True False 10 True True True True exporter_quality_adjustment False 100 0 0 left 1 0 True False Quality 0 0 True False Paper format 0 1 True False True exporter_page_format_model 0 1 1 True False 0 2 2 True False Export Settings False True 1 True False 10 True False True True 0 gtk-cancel True True True True False True 1 Send by email True False True True exporter_email_icon False True 2 gtk-save-as True False True True True False True 3 False True 2 True False 20 0 none True False 12 True False 10 5 True False vertical True False edit-find-symbolic 3 False True 0 True True vertical exporter_zoom_adjustment True False True True 1 False True 0 True True always always in True False True False True False False False 1 True True 1 True False Preview True True 3 -1 True False True paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/exporter/pipe.glade000066400000000000000000000014631417573700700277660ustar00rootroot00000000000000 True False exporter_pipe_model 0 1 0 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/global.css000066400000000000000000000011371417573700700261330ustar00rootroot00000000000000.button-left { border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-right-width: 0px; margin-right: 0px; } .button-center { border-radius: 0px; border-right-width: 0px; margin-left: 0px; margin-right: 0px; } .button-right { margin-left: 0px; border-top-left-radius: 0px; border-bottom-left-radius: 0px; } .borderless { border: 0px; } .txt-hint { color: mix (@theme_fg_color, @theme_bg_color, 0.3); } .headerbar-selection-multiple, .headerbar-selextion-multiple > * { background-color: #38a1d6; background-image: none; } .button-tight { padding: 0px; } paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/home/000077500000000000000000000000001417573700700251075ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/home/__init__.py000066400000000000000000000027611417573700700272260ustar00rootroot00000000000000import openpaperwork_core class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['home'] def get_deps(self): return [ { 'interface': 'app', 'defaults': ['paperwork_backend.app'], }, { 'interface': 'gtk_mainwindow', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'icon', 'defaults': ['paperwork_gtk.icon'], }, ] def init(self, core): super().init(core) widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.home", "home.glade" ) if widget_tree is None: return logo_pixbuf = self.core.call_success( "icon_get_pixbuf", "paperwork", 128 ) logo_widget = widget_tree.get_object("paperwork_logo") logo_widget.set_from_pixbuf(logo_pixbuf) logo_text = widget_tree.get_object("main_label") logo_text.set_text("Paperwork {}".format( self.core.call_success("app_get_version") )) self.core.call_all( "mainwindow_add", side="right", name="home", prio=100000, header=None, body=widget_tree.get_object("home") ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/home/home.glade000066400000000000000000000025001417573700700270320ustar00rootroot00000000000000 True False center center vertical False True 0 True False Paperwork kick-ass edition False True 1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/pageeditor/000077500000000000000000000000001417573700700263025ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/pageeditor/__init__.py000066400000000000000000000311461417573700700304200ustar00rootroot00000000000000import collections import logging try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_gtk.deps import paperwork_backend.pageedit LOGGER = logging.getLogger(__name__) MODIFIER_ICONS = collections.defaultdict( lambda: "document-properties", { "color_equalization": ( "paperwork_gtk.mainwindow.pageeditor", "magic_colors.png" ), "crop": "edit-cut-symbolic", "rotate_clockwise": "object-rotate-left-symbolic", "rotate_counterclockwise": "object-rotate-right-symbolic", } ) class GtkPageEditorUI(paperwork_backend.pageedit.AbstractPageEditorUI): CAPABILITIES = ( paperwork_backend.pageedit.AbstractPageEditorUI.CAPABILITY_SHOW_FRAME ) def __init__(self, plugin): super().__init__() self.plugin = plugin self.core = plugin.core self.editor = None self.modifiers_to_toggles = {} self.buttons_to_modifiers = {} self.pil_img = None self.surface_img = None self.size_allocated = None self.allocate_handler_id = None self.draw_handler_id = self.plugin.widget_tree.get_object( "pageeditor_img" ).connect("draw", self.draw) self.zoom = self.plugin.widget_tree.get_object( "pageeditor_zoom_adjustment" ) self.zoom.connect("value-changed", self._on_zoom_changed) def add_modifier_toggles(self): assert(self.editor is not None) toolbox = self.plugin.widget_tree.get_object("pageeditor_tools") zoom_box = self.plugin.widget_tree.get_object("pageeditor_zoom_box") for widget in list(toolbox.get_children()): if widget == zoom_box: continue toolbox.remove(widget) for modifier in self.editor.get_modifiers(): glade = ( "modifier_toggle.glade" if modifier['togglable'] else "modifier.glade" ) modifier_widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.pageeditor", glade ) button = modifier_widget_tree.get_object("pageeditor_modifier") button.set_tooltip_text(modifier['name']) if modifier['togglable']: button.set_active(modifier['enabled']) button.connect("toggled", self._on_modifier_changed) self.modifiers_to_toggles[modifier['id']] = button else: button.connect("clicked", self._on_modifier_changed) img = modifier_widget_tree.get_object("pageeditor_modifier_img") icon = MODIFIER_ICONS[modifier['id']] if isinstance(icon, tuple): icon = self.core.call_success("gtk_load_pixbuf", *icon) img.set_from_pixbuf(icon) else: img.set_from_icon_name(icon, Gtk.IconSize.LARGE_TOOLBAR) toolbox.pack_start(button, expand=False, fill=True, padding=0) self.buttons_to_modifiers[button] = modifier['id'] if self.size_allocated is None: self.allocate_handler_id = self.plugin.widget_tree.get_object( "pageeditor_scroll" ).connect("size-allocate", self._on_size_allocate) else: self.set_default_zoom() self.plugin.widget_tree.get_object( "pageeditor_img" ).connect("realize", self.refresh) def _on_modifier_changed(self, button): modifier = self.buttons_to_modifiers[button] promise = openpaperwork_core.promise.Promise( self.core, self.core.call_all, args=("on_busy",) ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(self.editor.on_modifier_selected(modifier)) promise.then(self.refresh) promise.then(self.core.call_all, "on_idle") promise.schedule() def _on_size_allocate(self, *args, **kwargs): self.plugin.widget_tree.get_object( "pageeditor_scroll" ).disconnect(self.allocate_handler_id) self.allocate_handler_id = None self.set_default_zoom() def set_default_zoom(self, *args, **kwargs): allocation = self.plugin.widget_tree.get_object( "pageeditor_scroll" ).get_allocation() img_size = self.pil_img.size zoom = min( (allocation.width - 20) / img_size[0], (allocation.height - 20) / img_size[1] ) LOGGER.info( "Allocation: %dx%d ; Image: %dx%d ==> Setting zoom at %f", allocation.width, allocation.height, img_size[0], img_size[1], zoom ) self.zoom.set_value(zoom) self.refresh() def _get_scaled_image_size(self): img_size = self.pil_img.size zoom = self.zoom.get_value() return (img_size[0] * zoom, img_size[1] * zoom) def refresh(self, *args, **kwargs): widget_size = self._get_scaled_image_size() img = self.plugin.widget_tree.get_object("pageeditor_img") img.set_size_request(widget_size[0], widget_size[1]) # WORKAROUND(JFlesch): I shouldn't have to use 'mainloop_schedule' # here. But somehow, I do have to use it :/ self.core.call_all("mainloop_schedule", img.queue_resize) def _on_zoom_changed(self, adj=None): self.refresh() def set_modifier_state(self, modifier_id, enabled): super().set_modifier_state(modifier_id, enabled) LOGGER.info("Modifier %s: %s", modifier_id, enabled) def show_preview(self, img): super().show_preview(img) need_rezoom = (self.pil_img is None) self.pil_img = img self.surface_img = self.core.call_success( "pillow_to_surface", self.pil_img ) if need_rezoom: self.set_default_zoom() self.refresh() LOGGER.info("Preview refreshed (%s)", img.size) def show_frame_selector(self): super().show_frame_selector() if self.pil_img is None: return img = self.plugin.widget_tree.get_object("pageeditor_img") self.core.call_all("draw_frame_stop", img) LOGGER.info( "show_frame_selector() (img size: %s ; frame: %s)", self.pil_img.size, self.editor.frame.get() ) self.core.call_all( "draw_frame_start", img, self.pil_img.size, self.editor.frame.get, self.editor.frame.set ) def hide_frame_selector(self): super().hide_frame_selector() LOGGER.info("hide_frame_selector()") img = self.plugin.widget_tree.get_object("pageeditor_img") self.core.call_all("draw_frame_stop", img) def on_edit_end(self, doc_url, page_idx): super().on_edit_end(doc_url, page_idx) self.plugin._on_edit_end(doc_url, page_idx) self._disconnect_draw() self.surface_img = None def _disconnect_draw(self): if self.draw_handler_id is None: return self.plugin.widget_tree.get_object( "pageeditor_img" ).disconnect(self.draw_handler_id) self.draw_handler_id = None def cancel(self): self.editor.on_cancel() self._disconnect_draw() def save(self): promise = openpaperwork_core.promise.Promise( self.core, self.core.call_all, args=("on_busy",) ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(self.editor.on_save()) promise = promise.then(self.core.call_all, "on_idle") promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(self.core.call_success( "transaction_simple_promise", {("upd", self.plugin.active_doc[0])} )) self.core.call_success("transaction_schedule", promise) def draw(self, widget, cairo_ctx): if self.surface_img is None: return cairo_ctx.save() try: zoom = self.zoom.get_value() cairo_ctx.scale(zoom, zoom) cairo_ctx.set_source_surface(self.surface_img.surface) cairo_ctx.paint() finally: cairo_ctx.restore() class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.ui = None self.active_doc = None def get_interfaces(self): return [ 'chkdeps', 'gtk_page_editor', ] def get_deps(self): return [ { 'interface': 'gtk_drawer_frame', 'defaults': ['paperwork_gtk.drawer.frame'], }, { 'interface': 'gtk_mainwindow', 'defaults': ['paperwork_gtk.mainwindow.window'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'gtk_zoomable', 'defaults': ['paperwork_gtk.gesture.zoom'], }, { 'interface': 'page_editor', 'defaults': ['paperwork_backend.pageedit.pageeditor'], }, { 'interface': 'pillow_to_surface', 'defaults': ['paperwork_backend.cairo.pillow'], }, { 'interface': 'resources', 'defaults': ['openpaperwork_core.resources.setuptools'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def init(self, core): super().init(core) self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.pageeditor", "pageeditor.glade" ) if self.widget_tree is None: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to load widget tree") return self.widget_tree.get_object("pageeditor_cancel").connect( "clicked", self._on_cancel ) self.widget_tree.get_object("pageeditor_back").connect( "clicked", self._on_apply ) self.core.call_all( "on_zoomable_widget_new", self.widget_tree.get_object("pageeditor_scroll"), self.widget_tree.get_object("pageeditor_zoom_adjustment"), ) self.core.call_all( "mainwindow_add", "right", "pageeditor", prio=0, header=self.widget_tree.get_object("pageeditor_header"), body=self.widget_tree.get_object("pageeditor_body") ) def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def gtk_open_page_editor(self, doc_id, doc_url, page_idx): self.active_doc = (doc_id, doc_url) page_url = self.core.call_success( "page_get_img_url", doc_url, page_idx ) if page_url is None: LOGGER.error( "Can't open page editor: Failed to get page url (%s, p%d)", doc_id, page_idx ) return self.ui = GtkPageEditorUI(self) promise = openpaperwork_core.promise.Promise( self.core, self.core.call_all, args=("on_busy",) ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then( self.core.call_success, "page_editor_get", doc_url, page_idx, self.ui ) promise = promise.then( self._show_page_editor, self.ui, doc_id, doc_url, page_idx ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(self.core.call_all, "on_idle") promise.schedule() def _on_cancel(self, button): if self.ui is None: return self.ui.cancel() self.ui = None def _on_apply(self, button): if self.ui is None: return self.ui.save() def _on_edit_end(self, doc_url, page_idx): self.core.call_all("mainwindow_back", side="right") self.core.call_all("doc_reload", *self.active_doc) self.core.call_success( "mainloop_schedule", self.core.call_all, "doc_goto_page", page_idx ) self.ui = None def _show_page_editor(self, page_editor, ui, doc_id, doc_url, page_idx): self.ui.editor = page_editor self.ui.add_modifier_toggles() self.core.call_all("mainwindow_show", "right", "pageeditor") paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/pageeditor/magic_colors.png000066400000000000000000000017421417573700700314550ustar00rootroot00000000000000PNG  IHDRŜsBIT|d pHYsccΖstEXtSoftwarewww.inkscape.org<_IDATHKhUmZ+I444H&"BԪB1Հ(†Q+ݹBP%Vō(JіV'64Ms7c&rag}&6KUttt www}W,a=LFՖ,Ѫ6O,hUϲUߺPؘ?Q8n#R?hkksjjʮq`] 6,KF3lhhx&4o'UJ̲]D%^` h]hT_&bu 0 ܽ\-NX- 5K #@M8) o ~QCħ;[hM倫Sx8O/;gUo] XaB`En_ 3I kp sk AR xNܓoM˓O0 uȮ/8)=ȠS`W9\»dS(E|#.?z3hu$JPO&d^ur9/ٯ{ w4g s?y/ypu9蔡Mٻ#zd5! Ipmm2|ׯh].tx&r18^\ݮ~9_e'=.< %)fodE*\yѯU+e>8wRGL)Ŏf0'wOP*e$]]{d/ў-Y|_BrCQpc>jITc8t\e)6d(Z*sE. True True False True True False 1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/pageeditor/modifier_toggle.glade000066400000000000000000000012711417573700700324400ustar00rootroot00000000000000 True True False True True False 1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/pageeditor/pageeditor.glade000066400000000000000000000141211417573700700314220ustar00rootroot00000000000000 0.01 1.5 0.01 0.01 0.1 True False True False True True False True False always always False True False True True False False 0 start True True True False vertical True False vertical 10 10 True edit-find-symbolic 1 False True start True True vertical pageeditor_zoom_adjustment True False True True start True True end end False True -1 True False True :close True False True True gtk-cancel start True False False True True gtk-apply end paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/search/000077500000000000000000000000001417573700700254245ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/search/__init__.py000066400000000000000000000000001417573700700275230ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.glade000066400000000000000000000175121417573700700301750ustar00rootroot00000000000000 False True False False Search 800 300 system-search dialog False vertical 2 False end gtk-cancel True True True True True True 0 gtk-apply True True True True True True 1 False False 0 True True in True False True False vertical True False False True 0 True False True False True True 0 gtk-add True True True True False True 1 False True 1 True False 0.02 True True 2 True True 1 button_cancel button_apply paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/search/advanced.py000066400000000000000000000455651417573700700275620ustar00rootroot00000000000000import datetime import logging import re try: from gi.repository import GLib from gi.repository import GObject GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_gtk.deps from ... import _ LOGGER = logging.getLogger(__name__) def strip_quotes(txt): if txt[0] == u'"' and txt[-1] == u'"': return txt[1:-1] if txt[0] == u'\'' and txt[-1] == u'\'': return txt[1:-1] return txt class SearchElement(object): def __init__(self, dialog, widget): self.dialog = dialog self.widget = widget widget.set_hexpand(True) widget.show_all() def get_widget(self): return self.widget def get_search_string(self): assert() @staticmethod def get_from_search(dialog, text): assert() @staticmethod def get_name(): assert() def on_model_updated(self): pass class SearchElementText(SearchElement): """This is the keyword search term text field""" def __init__(self, dialog): super(SearchElementText, self).__init__(dialog, Gtk.Entry()) def get_search_string(self): txt = self.widget.get_text() txt = txt.replace('"', '\\"') return '"%s"' % txt @staticmethod def get_from_search(dialog, text): text = strip_quotes(text) element = SearchElementText(dialog) element.widget.set_text(text) return element @staticmethod def get_name(): return _("Keyword(s)") def __str__(self): return ("Text: [%s]" % self.widget.get_text()) class SearchElementLabel(SearchElement): def __init__(self, dialog): super().__init__(dialog, Gtk.ComboBoxText()) self.on_model_updated() self.widget.set_active(0) def on_model_updated(self): labels = set() self.dialog.core.call_all("labels_get_all", labels) labels = [label[0] for label in labels] labels = self.dialog.core.call_success("i18n_sort", labels) store = Gtk.ListStore.new([GObject.TYPE_STRING]) if len(labels) <= 0: # happens temporarily when Paperwork starts store.append([_("No labels")]) self.widget.set_sensitive(False) else: for label in labels: store.append([label]) self.widget.set_sensitive(True) self.widget.set_model(store) def get_search_string(self): active_idx = self.get_widget().get_active() if active_idx < 0: return "" model = self.get_widget().get_model() txt = model[active_idx][0] txt = txt.replace('"', '\\"') return 'label:"%s"' % txt @staticmethod def get_from_search(dialog, text): if not text.startswith(u"label:"): return None text = text[len(u"label:"):] text = strip_quotes(text) element = SearchElementLabel(dialog) active_idx = -1 idx = 0 for line in element.get_widget().get_model(): value = line[0] if value == text: active_idx = idx idx += 1 element.get_widget().set_active(active_idx) return element @staticmethod def get_name(): return _("Label") def __str__(self): return ("Label: [%d]" % self.get_widget().get_active()) class SearchElementDate(SearchElement): """Search entry using a time span""" def __init__(self, dialog): box = Gtk.Box() box.set_spacing(10) label = Gtk.Label.new(_("From:")) box.add(label) self.start_date = self._make_date_widget() box.add(self.start_date) label = Gtk.Label.new(_("to:")) box.add(label) self.end_date = self._make_date_widget() box.add(self.end_date) super(SearchElementDate, self).__init__(dialog, box) self.calendar_popover = dialog.widget_tree.get_object( "calendar_popover" ) self.calendar = dialog.widget_tree.get_object("calendar_calendar") self.current_entry = None self.calendar.connect( "day-selected-double-click", lambda _: GLib.idle_add(self._close_calendar) ) def _make_date_widget(self): entry = Gtk.Entry() entry.set_text("") entry.set_property("secondary_icon_sensitive", True) entry.set_property("secondary_icon_name", "x-office-calendar-symbolic") entry.connect( "icon-release", lambda entry, icon, event: GLib.idle_add(self._open_calendar, entry) ) return entry @staticmethod def _parse_date(txt): txt = txt.strip() if txt == u"": dt = datetime.datetime.today() else: try: dt = datetime.datetime.strptime(txt, "%Y%m%d") except ValueError: LOGGER.warning( "Failed to parse [%s]. Will use today date", txt ) dt = datetime.datetime.today() return (dt.year, dt.month, dt.day) @staticmethod def _format_date(date): return "%04d%02d%02d" % (date[0], date[1], date[2]) def _open_calendar(self, entry): self.calendar_popover.set_relative_to(entry) date = self._parse_date(entry.get_text()) self.calendar.select_month(date[1] - 1, date[0]) self.calendar.select_day(date[2]) self.calendar_popover.show_all() self.current_entry = entry def _close_calendar(self): date = self.calendar.get_date() date = datetime.datetime(year=date[0], month=date[1] + 1, day=date[2]) date = self._format_date((date.year, date.month, date.day)) self.current_entry.set_text(date) self.calendar_popover.set_visible(False) def get_search_string(self): start_date = self._parse_date(self.start_date.get_text()) end_date = self._parse_date(self.end_date.get_text()) if end_date < start_date: tmp_date = start_date start_date = end_date end_date = tmp_date if start_date == end_date: return ( "date:%04d%02d%02d" % (start_date[0], start_date[1], start_date[2]) ) return ( 'date:[%04d%02d%02d to %04d%02d%02d]' % ( start_date[0], start_date[1], start_date[2], end_date[0], end_date[1], end_date[2] ) ) @staticmethod def get_from_search(dialog, txt): if not txt.startswith(u"date:"): return None txt = txt[len(u"date:"):] txt = strip_quotes(txt) if txt[0] == "[" and txt[-1] == "]": txt = txt[1:-1] if " to " in txt: txt = txt.split(" to ", 1) else: txt = [txt, txt] dates = [ SearchElementDate._parse_date(date) for date in txt ] se = SearchElementDate(dialog) se.start_date.set_text(se._format_date(dates[0])) se.end_date.set_text(se._format_date(dates[1])) return se @staticmethod def get_name(): return _("Date") def __str__(self): return ( "Date: [%s] - [%s]" % (self.start_date.get_text(), self.end_date.get_text()) ) class SearchLine(object): SELECT_ORDER = [ SearchElementText, SearchElementLabel, SearchElementDate, ] TXT_EVAL_ORDER = [ SearchElementDate, SearchElementLabel, SearchElementText, ] def __init__(self, dialog, has_operator, has_remove_button): LOGGER.info("Search line instantiated") self.dialog = dialog self.line = [] if has_operator: model = Gtk.ListStore.new([ GObject.TYPE_STRING, GObject.TYPE_STRING, ]) model.append([_("and"), "AND"]) model.append([_("or"), "OR"]) self.combobox_operator = Gtk.ComboBoxText.new() self.combobox_operator.set_model(model) self.combobox_operator.set_size_request(75, -1) self.combobox_operator.set_active(0) self.line.append(self.combobox_operator) else: self.combobox_operator = None placeholder = Gtk.Label.new("") placeholder.set_size_request(75, -1) self.line.append(placeholder) model = Gtk.ListStore.new([ GObject.TYPE_STRING, GObject.TYPE_STRING, ]) model.append(["", ""]) model.append([_("not"), "NOT"]) self.combobox_not = Gtk.ComboBoxText.new() self.combobox_not.set_model(model) self.combobox_not.set_size_request(75, -1) self.combobox_not.set_active(0) self.line.append(self.combobox_not) model = Gtk.ListStore.new([ GObject.TYPE_STRING, GObject.TYPE_PYOBJECT, ]) for element in self.SELECT_ORDER: model.append([ element.get_name(), element ]) self.combobox_type = Gtk.ComboBoxText.new() self.combobox_type.set_model(model) self.placeholder = Gtk.Label.new("") self.placeholder.set_hexpand(True) self.element = None self.remove_button = Gtk.Button.new_with_label(_("Remove")) self.line.append(self.combobox_type) self.line.append(self.placeholder) if has_remove_button: self.line.append(self.remove_button) else: self.line.append(Gtk.Label.new("")) self.combobox_type.set_active(0) self.change_element() def connect_signals(self): self.combobox_type.connect( "changed", lambda w: GLib.idle_add(self.change_element) ) self.remove_button.connect( "clicked", lambda x: GLib.idle_add( self.dialog.remove_element, self ) ) @staticmethod def _select_value(combobox, value): if not combobox: return active_idx = 0 model = combobox.get_model() for line in model: if line[1] == value: LOGGER.info("Element %d selected", active_idx) combobox.set_active(active_idx) return active_idx += 1 assert() def select_operator(self, operator): self._select_value(self.combobox_operator, operator.upper()) def select_not(self, not_value): self._select_value(self.combobox_not, not_value) def select_element_type(self, et): self._select_value(self.combobox_type, et) def change_element(self): LOGGER.info("Element changed") active_idx = self.combobox_type.get_active() if (active_idx < 0): return element_class = self.combobox_type.get_model()[active_idx][1] element = element_class(self.dialog) self.set_element(element) def set_element(self, element): LOGGER.info("Set element: %s", str(element)) if self.placeholder: self.line.remove(self.placeholder) self.placeholder = None if self.element: self.line.remove(self.element.get_widget()) self.element = None self.line.insert(3, element.get_widget()) self.element = element self.dialog.rebuild() def get_widgets(self): return self.line @staticmethod def _get_combobox_value(combobox): active_idx = combobox.get_active() if (active_idx < 0): return "" value = combobox.get_model()[active_idx][1] return value def get_operator(self): if not self.combobox_operator: return u"" return self._get_combobox_value(self.combobox_operator) def get_not(self): return self._get_combobox_value(self.combobox_not) def get_search_string(self): if self.element is None: return "" return self.element.get_search_string() @staticmethod def get_from_search(dialog, next_operator, not_value, search_txt): for se_class in SearchLine.TXT_EVAL_ORDER: se = se_class.get_from_search(dialog, search_txt) if not se: continue sl = SearchLine( dialog, next_operator is not None, next_operator is not None ) if next_operator: sl.select_operator(next_operator) sl.select_element_type(se_class) sl.select_not(not_value) sl.set_element(se) sl.connect_signals() LOGGER.info( "Loaded from search: %s --> %s", search_txt, str(se) ) return sl assert() def on_model_updated(self): if self.element: self.element.on_model_updated() class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.widget_tree = None self.windows = [] self.search_element_box = None self.search_elements = [] self.dialog = None def get_interfaces(self): return [ 'chkdeps', 'gtk_advanced_search_dialog', 'gtk_window_listener', 'screenshot_provider', ] def get_deps(self): return [ { 'interface': 'doc_labels', 'defaults': ['paperwork_backend.model.labels'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, { 'interface': 'screenshot', 'defaults': ['openpaperwork_gtk.screenshots'], }, ] def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def gtk_open_advanced_search_dialog(self): if self.dialog is not None: LOGGER.warning("Advanced search dialog already opened") return self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.search", "advanced.glade" ) if self.widget_tree is None: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to load widget tree") return self.dialog = self.widget_tree.get_object("search_dialog") self.dialog.set_transient_for(self.windows[-1]) keywords = self.core.call_success("search_get") keywords = keywords.strip() keywords = re.findall( r'(?:\[.*\]|(?:[^\s"]|"(?:\\.|[^"])*"))+', keywords ) self.search_element_box = self.widget_tree.get_object( "box_search_elements" ) self.search_elements = [] add_button = self.widget_tree.get_object("button_add") add_button.connect( "clicked", lambda w: GLib.idle_add(self.add_element) ) if keywords == []: LOGGER.info("Starting from an empty search") self.add_element() else: LOGGER.info("Current search: %s", keywords) next_operator = None not_value = u"" for keyword in keywords: if keyword.upper() == u"AND": next_operator = u"AND" continue elif keyword.upper() == u"OR": next_operator = u"OR" continue elif keyword.upper() == u"NOT": not_value = u"NOT" continue LOGGER.info("Instantiating line for [%s]", keyword) sl = SearchLine.get_from_search( self, next_operator, not_value, keyword ) self.add_element(sl) next_operator = u"AND" not_value = u"" self.dialog.connect("response", self._on_response) self.dialog.show_all() def _on_response(self, dialog, response_id): if (response_id == Gtk.ResponseType.ACCEPT or response_id == Gtk.ResponseType.OK or response_id == Gtk.ResponseType.YES or response_id == Gtk.ResponseType.APPLY): search = self._get_search_string() self.core.call_all("search_set", search) self.gtk_close_advanced_search_dialog() def gtk_close_advanced_search_dialog(self): self.dialog.destroy() self.widget_tree = None self.dialog = None def add_element(self, sl=None): if sl is None: sl = SearchLine( self, len(self.search_elements) > 0, len(self.search_elements) > 0 ) sl.connect_signals() self.search_elements.append(sl) self.rebuild() def rebuild(self): # purge for child in self.search_element_box.get_children(): self.search_element_box.remove(child) # rebuild for (line, sl) in enumerate(self.search_elements): for (pos, widget) in enumerate(sl.get_widgets()): self.search_element_box.attach( widget, pos, line, 1, 1 ) self.search_element_box.show_all() def remove_element(self, sl): self.search_elements.remove(sl) self.rebuild() def _get_search_string(self): """concat all our search terms into a single string""" out = "" for element in self.search_elements: # Add AND/OR oper = element.get_operator() out += " %s " % oper not_value = element.get_not() if not_value: out += "%s " % not_value out += element.get_search_string() out = out.strip() LOGGER.info("Search: [%s]", out) return out def screenshot_snap_all_doc_widgets(self, out_dir): if self.dialog is None: return self.core.call_success( "screenshot_snap_widget", self.dialog, self.core.call_success( "fs_join", out_dir, "advanced_search.png" ), ) def on_label_loading_end(self): for element in self.search_elements: element.on_model_updated() paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.glade000066400000000000000000000053411417573700700275100ustar00rootroot00000000000000 True False vertical True False True True Search GTK_INPUT_HINT_SPELLCHECK | GTK_INPUT_HINT_WORD_COMPLETION | GTK_INPUT_HINT_NO_EMOJI | GTK_INPUT_HINT_NONE True True 0 True False False True Advanced search none True False text-editor-symbolic 1 False True 1 False True 1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/search/field.py000066400000000000000000000136471417573700700270740ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.promise import paperwork_backend.sync LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = -1000000 def __init__(self): super().__init__() self.search_entry = None self.widget_tree = None def get_interfaces(self): return [ 'gtk_search_field', 'screenshot_provider', 'syncable', ] def get_deps(self): return [ { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'index', 'defaults': ['paperwork_backend.index.whoosh'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_gtk.mainloop.glib'], }, { 'interface': 'work_queue', 'defaults': ['openpaperwork_core.work_queue.default'], }, { 'interface': 'screenshot', 'defaults': ['openpaperwork_gtk.screenshots'], }, { 'interface': 'thread', 'defaults': ['openpaperwork_core.thread.simple'], }, ] def init(self, core): super().init(core) self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.search", "field.glade" ) if self.widget_tree is None: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to load widget tree") return self.search_entry = self.widget_tree.get_object("search_entry") search_field = self.widget_tree.get_object("search_field") self.core.call_all("doclist_add", search_field, vposition=0) self.search_entry.connect( "search-changed", self.search_update_document_list ) self.search_entry.connect("stop-search", lambda w: self.search_stop()) self.widget_tree.get_object("search_dialog_button").connect( "clicked", lambda button: self.core.call_all( "gtk_open_advanced_search_dialog" ) ) self.core.call_all("work_queue_create", "doc_search") self.core.call_one( "mainloop_schedule", self.search_update_document_list ) def search_update_document_list(self, _=None): query = self.search_entry.get_text() LOGGER.info("Looking for [%s]", query) self.core.call_all("search_by_keywords", query) def search_get(self): return self.search_entry.get_text() def search_set(self, text): self.search_entry.set_text(text) def search_by_keywords(self, query): self.core.call_all("work_queue_cancel_all", "doc_search") self.core.call_all("on_search_start", query) if query == "": out = [] promise = openpaperwork_core.promise.ThreadedPromise( self.core, lambda: self.core.call_all( "storage_get_all_docs", out ) ) else: out = [] promise = openpaperwork_core.promise.ThreadedPromise( self.core, lambda: self.core.call_all( "index_search", out, query ) ) promise = promise.then(lambda *args, **kwargs: out) promise = promise.then(lambda docs: sorted(docs, reverse=True)) def show_if_query_still_valid(docs): # While we were looking for the documents, the query may have # changed (user tying). No point in displaying obsolete results. if query != self.search_entry.get_text(): return self.core.call_all("on_search_results", query, docs) promise = promise.then(show_if_query_still_valid) self.core.call_all("work_queue_add_promise", "doc_search", promise) def search_stop(self): self.core.call_all("work_queue_cancel_all", "doc_search") def doc_transaction_start(self, out: list, total_expected=-1): class RefreshTransaction(paperwork_backend.sync.BaseTransaction): priority = -100000 def commit(s): self.core.call_one( "mainloop_schedule", self.search_update_document_list ) out.append(RefreshTransaction(self.core, total_expected)) def sync(self, promises: list): # If someone requested a sync, assume something has changed # --> try to keep the view as up-to-date as possible. self.search_update_document_list() promises.append(openpaperwork_core.promise.Promise( self.core, self.search_update_document_list )) def screenshot_snap_all_doc_widgets(self, out_dir): self.core.call_success( "screenshot_snap_widget", self.search_entry, self.core.call_success("fs_join", out_dir, "search.png"), margins=(50, 50, 50, 140) ) self.core.call_success( "screenshot_snap_widget", self.widget_tree.get_object("search_dialog_button"), self.core.call_success( "fs_join", out_dir, "advanced_search_button.png" ), margins=(200, 30, 30, 30) ) def search_focus(self): LOGGER.info("Focusing on search field") self.widget_tree.get_object("search_entry").grab_focus() def search_field_add(self, widget): search_field = self.widget_tree.get_object("search_field") search_field.add(widget) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/search/suggestions.glade000066400000000000000000000111361417573700700307760ustar00rootroot00000000000000 True False gtk-close 2 test1 test2 True False 75 True False vertical True False True False Did you mean ? 0 True True 0 True True True end image_close none True False False 1 False True 0 True True in True True liststore_suggestions False 0 True 0 True True 1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/search/suggestions.py000066400000000000000000000065201417573700700303530ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.promise LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = -2000000 def __init__(self): super().__init__() self.widget_tree = None def get_interfaces(self): return [ 'doc_open', 'gtk_search_field_completion', ] def get_deps(self): return [ { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'gtk_search_field', 'defaults': ['paperwork_gtk.mainwindow.search.field'], }, { 'interface': 'suggestions', 'defaults': ['paperwork_backend.index.whoosh'], }, ] def init(self, core): super().init(core) self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.search", "suggestions.glade" ) if self.widget_tree is None: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to load widget tree") return suggestion_revealer = self.widget_tree.get_object( "suggestions_revealer" ) self.core.call_all("search_field_add", suggestion_revealer) self.widget_tree.get_object("button_close").connect( "clicked", self._close ) self.widget_tree.get_object("treeview_suggestions").connect( "row-activated", self._on_row_activated ) def _get_suggestions(self, query): out = set() self.core.call_all("suggestion_get", out, query) out = list(out) out.sort() return out def _show_suggestions(self, suggestions, query): if query != self.core.call_success("search_get"): # Text has changed while we were looking for suggestions # No point in displaying them now. return LOGGER.info("%d suggestions found", len(suggestions)) model = self.widget_tree.get_object("liststore_suggestions") model.clear() for suggestion in suggestions: model.append((suggestion,)) self.widget_tree.get_object( "suggestions_revealer" ).set_reveal_child(len(suggestions) > 0) def search_by_keywords(self, query): self.widget_tree.get_object("liststore_suggestions").clear() self.widget_tree.get_object( "suggestions_revealer" ).set_reveal_child(False) if query == "": return promise = openpaperwork_core.promise.ThreadedPromise( self.core, self._get_suggestions, args=(query,) ) promise = promise.then(self._show_suggestions, query) self.core.call_success( "work_queue_add_promise", "doc_search", promise ) def _close(self, *args, **kwargs): self.widget_tree.get_object( "suggestions_revealer" ).set_reveal_child(False) def doc_open(self, *args, **kwargs): self._close() def _on_row_activated(self, treeview, path, column): model = treeview.get_model() suggestion = model[path][0] self.core.call_all("search_set", suggestion) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/window/000077500000000000000000000000001417573700700254665ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/window/__init__.py000066400000000000000000000303521417573700700276020ustar00rootroot00000000000000import collections import logging try: from gi.repository import Gio from gi.repository import GLib GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import PIL import PIL.Image import PIL.ImageDraw import openpaperwork_core import openpaperwork_core.deps import openpaperwork_gtk.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.widget_tree = None self.stacks = None self.components = collections.defaultdict(dict) self.navigation_stacks = {'left': [], 'right': []} self.defaults = collections.defaultdict(list) # keep track of the prio self.mainwindow = None self._mainwindow_size = None def get_interfaces(self): return [ 'app_actions', 'app_shortcuts', 'chkdeps', 'gtk_mainwindow', 'screenshot_provider', ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'] }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'icon', 'defaults': ['paperwork_gtk.icon'], }, { 'interface': 'l10n_init', 'defaults': ['paperwork_gtk.l10n'], }, { 'interface': 'screenshot', 'defaults': ['openpaperwork_gtk.screenshots'], }, ] def init(self, core): super().init(core) self.core.call_success( "gtk_load_css", "paperwork_gtk.mainwindow", "global.css" ) self.core.call_success( "gtk_load_css", "paperwork_gtk.mainwindow.window", "mainwindow.css" ) self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.mainwindow.window", "mainwindow.glade" ) if self.widget_tree is None: # init must still work so 'chkdeps' is still available LOGGER.error("Failed to load widget tree") return opt = self.core.call_success( "config_build_simple", "GUI", "main_window_size", lambda: (1024, 600) ) self.core.call_all("config_register", "main_window_size", opt) main_win_size = self.core.call_success( "config_get", "main_window_size" ) self.mainwindow = self.widget_tree.get_object("mainwindow") self.mainwindow.set_default_size(main_win_size[0], main_win_size[1]) self.mainwindow.connect("destroy", self._on_mainwindow_destroy) self.mainwindow.connect( "size-allocate", self._on_mainwindow_size_allocate ) try: self.mainwindow.set_icon_list([ self.core.call_success("icon_get_pixbuf", "paperwork", 16), self.core.call_success("icon_get_pixbuf", "paperwork", 32), self.core.call_success("icon_get_pixbuf", "paperwork", 48), self.core.call_success("icon_get_pixbuf", "paperwork", 64), self.core.call_success("icon_get_pixbuf", "paperwork", 128), ]) except KeyError as exc: # Will fail when generating data for the first time. LOGGER.warning("Failed to load main window icon", exc_info=exc) if hasattr(GLib, 'set_application_name'): GLib.set_application_name("Paperwork") GLib.set_prgname("work.openpaper.Paperwork") app = Gtk.Application( application_id=None, flags=Gio.ApplicationFlags.FLAGS_NONE ) app.register(None) Gtk.Application.set_default(app) self.mainwindow.set_application(app) self.stacks = { "left": { "header": self.widget_tree.get_object( "mainwindow_stack_header_left" ), "body": self.widget_tree.get_object( "mainwindow_stack_body_left" ), }, "right": { "header": self.widget_tree.get_object( "mainwindow_stack_header_right" ), "body": self.widget_tree.get_object( "mainwindow_stack_body_right" ), }, } self.widget_tree.get_object("mainwindow_box_titlebar").connect( "notify::folded", self._on_titlebar_fold_change ) self._on_titlebar_fold_change() def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def on_initialized(self): for side in self.defaults.keys(): self.mainwindow_show_default(side) self.mainwindow.set_visible(True) self.core.call_all("on_gtk_window_opened", self.mainwindow) def on_quit(self): # needed to save window size # TODO(JFlesch): not really config --> should not be stored in config ? if self.mainwindow is not None: self.core.call_all("config_save") self.mainwindow.set_visible(False) def _on_titlebar_fold_change(self, *args, **kwargs): LOGGER.info("Main window fold has changed") self.core.call_all("on_mainwindow_fold_change") def mainwindow_get_folded(self): return self.widget_tree.get_object( "mainwindow_box_titlebar" ).get_folded() def _on_mainwindow_destroy(self, main_window): LOGGER.info("Main window destroy. Quitting") self.core.call_all("on_gtk_window_closed", self.mainwindow) self.core.call_all("mainloop_quit_graceful") def _on_mainwindow_size_allocate(self, main_win, rectangle): (w, h) = main_win.get_size() if self._mainwindow_size == (w, h): return self._mainwindow_size = (w, h) self.core.call_all("config_put", "main_window_size", (w, h)) def mainwindow_get_main_container(self): return self.widget_tree.get_object("main_box") def mainwindow_add(self, side: str, name: str, prio: int, header, body): self.components[side][name] = { "header": header, "body": body, } components = self.components[side][name] stacks = self.stacks[side] for (position, widget) in components.items(): if widget is None: continue stacks[position].add_named(widget, name) self.defaults[side].append((prio, name)) self.defaults[side].sort(reverse=True) return True def mainwindow_show(self, side: str, name: str = None): self.widget_tree.get_object( "mainwindow_box_titlebar" ).set_visible_child( self.stacks[side]['header'] ) self.widget_tree.get_object( "mainwindow_box_body" ).set_visible_child( self.stacks[side]['body'] ) if name is None: return False if name in self.navigation_stacks[side]: self.navigation_stacks[side].remove(name) self.navigation_stacks[side].append(name) return self._mainwindow_show(side) def _mainwindow_show(self, side: str): stacks = self.stacks[side] LOGGER.info("Navigation: %s", self.navigation_stacks[side]) # some plugin give us None as component, which means to use # the previous component in the navigation stack (see doclist # in multiple selection mode) component_names = {h: None for h in stacks.keys()} for name in reversed(self.navigation_stacks[side]): LOGGER.info("Showing %s on %s", name, side) for (k, v) in self.components[side][name].items(): if component_names[k] is None and v is not None: component_names[k] = name if None not in component_names.values(): break has_changed = False for (h, stack) in stacks.items(): if h not in component_names or component_names[h] is None: continue if stack.get_visible_child_name() == component_names[h]: continue has_changed = True stack.set_visible_child_name(component_names[h]) return has_changed def mainwindow_show_default(self, side: str): # the default component may be incomplete (for instance, it may # provide a header, but not a body). So we update the navigation # stack to make it the behaviour consistent: # We ensure it contains at least one element with all components # We respect priorities for (prio, name) in self.defaults[side]: self.navigation_stacks[side].append(name) if None not in self.components[side][name].values(): break first = self.navigation_stacks[side].pop(0) self.navigation_stacks[side].reverse() self.core.call_all("mainwindow_show", side, first) def mainwindow_back(self, side: str): navigation_stack = self.navigation_stacks[side][:-1] self.navigation_stacks[side] = navigation_stack self._mainwindow_show(side) def app_actions_add(self, action): if self.mainwindow is not None: self.mainwindow.add_action(action) def mainwindow_focus(self): self.mainwindow.grab_focus() def _draw_overlay(self, img, area, color): overlay = PIL.Image.new('RGBA', img.size, color + (0,)) draw = PIL.ImageDraw.Draw(overlay) draw.rectangle(area, fill=color + (0x5F,)) return PIL.Image.alpha_composite(img, overlay) def screenshot_snap_all_doc_widgets(self, out_dir): out = self.core.call_success("fs_join", out_dir, "main_window.png") self.core.call_success( "screenshot_snap_widget", self.mainwindow, out ) left = self.stacks['left']['body'] left_alloc = left.get_allocation() left_width = left_alloc.width (left_x, left_y) = left.translate_coordinates(self.mainwindow, 0, 0) left = left_x + left_width with self.core.call_success("fs_open", out, 'rb') as fd: img = PIL.Image.open(fd) img.load() img = img.convert("RGBA") img = self._draw_overlay( img, (0, 0, left, img.size[1]), (0, 0xFF, 0) ) img = self._draw_overlay( img, (left, 0, img.size[0], img.size[1]), (0, 0, 0xFF) ) # drop some black border in the scn img = img.crop( (left_x, left_x, img.size[0] - left_x, img.size[1] - left_x) ) # keep only the top 150px img = img.crop((0, 0, img.size[0], min(300, img.size[1]))) out = self.core.call_success( "fs_join", out_dir, "main_window_split.png" ) with self.core.call_success("fs_open", out, 'wb') as fd: img.save(fd, format="PNG") def app_shortcut_add( self, shortcut_group, shortcut_desc, shortcut_keys, action_name): """ Arguments: - shortcut_group: Group name in which this shortcut belongs (human readable, translated ; actually used in the shortcut window if enabled) - shortcut_desc: Describe the shortcut (human readable, translated ; actually used in the shortcut window if enabled) - shortcut_keys: see gtk_accelerator_parse() - action_name: GAction that must be triggered """ if self.mainwindow is None: return LOGGER.info("Keyboard shortcut: %s --> %s", shortcut_keys, action_name) self.mainwindow.get_application().set_accels_for_action( action_name, (shortcut_keys, None) ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/window/mainwindow.css000066400000000000000000000006251417573700700303570ustar00rootroot00000000000000#mainwindow_stack_header_left headerbar { border-right-width: 1px; border-top-right-radius: 0px; border-bottom-right-radius: 0px; } #mainwindow_stack_header_right headerbar { border-left-width: 0px; border-top-left-radius: 0px; border-bottom-left-radius: 0px; } #mainwindow_stack_body_left { border-right: 1px solid @borders; } #mainwindow_stack_bodyr_right { border-left-width: 0px; } paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/mainwindow/window/mainwindow.glade000066400000000000000000000074251417573700700306500ustar00rootroot00000000000000 mainwindow False Paperwork False True vertical True False True mainwindow_stack_header_left 300 slide-right 333 True mainwindow_stack_header_right slide-left 333 True False True horizontal mainwindow_stack_body_left True slide-right 333 mainwindow_stack_body_right True slide-left 333 horizontal vertical paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/000077500000000000000000000000001417573700700231325ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/__init__.py000066400000000000000000000000001417573700700252310ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/app/000077500000000000000000000000001417573700700237125ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/app/__init__.py000066400000000000000000000000001417573700700260110ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/app/help.py000066400000000000000000000032121417573700700252120ustar00rootroot00000000000000import logging try: from gi.repository import Gio GIO_AVAILABLE = True except (ImportError, ValueError): GIO_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = -1000 def __init__(self): super().__init__() self.menu = None def get_interfaces(self): return [ 'chkdeps', 'menu', 'menu_app', 'menu_app_help', ] def get_deps(self): return [ { 'interface': 'action_app_help', 'defaults': ['paperwork_gtk.actions.app.help'], }, { 'interface': 'help_documents', 'defaults': ['paperwork_gtk.model.help'], }, { 'interface': 'gtk_app_menu', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def chkdeps(self, out: dict): if not GIO_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def init(self, core): super().init(core) if not GIO_AVAILABLE: return self.menu = Gio.Menu() def on_doclist_initialized(self): for (title, file_name) in self.core.call_success("help_get_files"): item = Gio.MenuItem.new(title, "win.open_help." + file_name) self.menu.append_item(item) self.core.call_all("menu_app_append_submenu", _("Help"), self.menu) def help_add_menu_item(self, menu_item): self.menu.append_item(menu_item) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/app/open_about.py000066400000000000000000000021011417573700700264110ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = -10000 def get_interfaces(self): return [ 'menu', 'menu_app', 'menu_app_about', ] def get_deps(self): return [ { 'interface': 'action_app_open_about', 'defaults': ['paperwork_gtk.actions.app.open_about'], }, { 'interface': 'gtk_app_menu', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_doclist_initialized(self): item = Gio.MenuItem.new(_("About"), "win.open_about") self.core.call_all("menu_app_append_item", item) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/app/open_bug_report.py000066400000000000000000000023501417573700700274550ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = -300 def get_interfaces(self): return [ 'menu', 'menu_app', 'menu_app_open_bug_report', ] def get_deps(self): return [ { 'interface': 'action_app_open_bug_report', 'defaults': ['paperwork_gtk.actions.app.open_bug_report'], }, { 'interface': 'gtk_app_menu', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_doclist_initialized(self): item = Gio.MenuItem.new(_("Report bug"), "win.open_bug_report") self.core.call_all("menu_app_append_item", item) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/app/open_settings.py000066400000000000000000000020711417573700700271450ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'menu', 'menu_app', 'menu_app_settings', ] def get_deps(self): return [ { 'interface': 'action_app_open_settings', 'defaults': ['paperwork_gtk.actions.app.open_settings'], }, { 'interface': 'gtk_app_menu', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_doclist_initialized(self): item = Gio.MenuItem.new(_("Settings"), "win.open_settings") self.core.call_all("menu_app_append_item", item) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/app/open_shortcuts.py000066400000000000000000000021221417573700700273400ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = -1500 def get_interfaces(self): return [ 'menu', 'menu_app', 'menu_app_shortcuts', ] def get_deps(self): return [ { 'interface': 'action_app_open_shortcuts', 'defaults': ['paperwork_gtk.actions.app.open_shortcuts'], }, { 'interface': 'gtk_app_menu', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_doclist_initialized(self): item = Gio.MenuItem.new(_("Shortcuts"), "win.open_shortcuts") self.core.call_all("help_add_menu_item", item) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/doc/000077500000000000000000000000001417573700700236775ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/doc/__init__.py000066400000000000000000000000001417573700700257760ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/doc/add_to_selection.py000066400000000000000000000020011417573700700275410ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_add_to_selection" class Plugin(openpaperwork_core.PluginBase): PRIORITY = 10000 def get_interfaces(self): return [ 'menu', 'menu_doc', 'menu_doc_add_to_selection', ] def get_deps(self): return [ { 'interface': 'action_doc_add_to_selection', 'defaults': ['paperwork_gtk.actions.doc.add_to_selection'], }, { 'interface': 'doc_actions', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def on_doclist_initialized(self): self.core.call_all( "add_doc_action", _("Add to selection"), "win." + ACTION_NAME ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/doc/delete.py000066400000000000000000000016701417573700700255170ustar00rootroot00000000000000import logging import openpaperwork_core from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_delete" class Plugin(openpaperwork_core.PluginBase): PRIORITY = -100 def get_interfaces(self): return [ 'menu', 'menu_doc', 'menu_doc_delete', ] def get_deps(self): return [ { 'interface': 'action_doc_delete', 'defaults': ['paperwork_gtk.actions.doc.delete'], }, { 'interface': 'doc_actions', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def on_doclist_initialized(self): self.core.call_all( "add_doc_action", _("Delete document"), "win." + ACTION_NAME ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/doc/export.py000066400000000000000000000016441417573700700255770ustar00rootroot00000000000000import logging import openpaperwork_core from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_export" class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'menu', 'menu_doc', 'menu_doc_export', ] def get_deps(self): return [ { 'interface': 'actions_doc_export', 'defaults': ['paperwork_gtk.actions.doc.export'], }, { 'interface': 'doc_actions', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def on_doclist_initialized(self): self.core.call_all( "add_doc_action", _("Export document"), "win." + ACTION_NAME ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/doc/open_external.py000066400000000000000000000014601417573700700271150ustar00rootroot00000000000000import logging import openpaperwork_core from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_open_external" class Plugin(openpaperwork_core.PluginBase): PRIORITY = -50 def get_interfaces(self): return [ 'menu', 'menu_doc', 'menu_doc_open_external', ] def get_deps(self): return [ { 'interface': 'doc_actions', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def on_doclist_initialized(self): self.core.call_all( "add_doc_action", _("Open folder"), "win." + ACTION_NAME ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/doc/print.py000066400000000000000000000016271417573700700254130ustar00rootroot00000000000000import logging import openpaperwork_core from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_print" class Plugin(openpaperwork_core.PluginBase): PRIORITY = -10 def __init__(self): super().__init__() self.active_doc = None self.active_windows = [] def get_interfaces(self): return [ 'menu', 'menu_doc', 'menu_doc_print', ] def get_deps(self): return [ { 'interface': 'doc_actions', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def on_doclist_initialized(self): self.core.call_all( "add_doc_action", _("Print document"), "win." + ACTION_NAME ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/doc/properties.py000066400000000000000000000025541417573700700264530ustar00rootroot00000000000000import logging import openpaperwork_core from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_properties" class Plugin(openpaperwork_core.PluginBase): PRIORITY = 100 def __init__(self): super().__init__() self.active_doc = None def get_interfaces(self): return [ 'menu', 'menu_doc', 'menu_doc_properties', ] def get_deps(self): return [ { 'interface': 'doc_actions', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def init(self, core): super().init(core) self.core.call_all( "add_doc_main_action", "document-properties-symbolic", _("Document properties"), self._open_properties ) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def doc_close(self): self.active_doc = None def _open_properties(self, *args, **kwargs): assert(self.active_doc is not None) active = self.active_doc LOGGER.info("Opening properties of document %s", active[0]) self.core.call_all("open_doc_properties", *active) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/doc/redo_ocr.py000066400000000000000000000017041417573700700260470ustar00rootroot00000000000000import logging import openpaperwork_core from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_redo_ocr" class Plugin(openpaperwork_core.PluginBase): PRIORITY = -90 def get_interfaces(self): return [ 'menu', 'menu_doc', 'menu_doc_redo_ocr', ] def get_deps(self): return [ { 'interface': 'action_doc_redo_ocr', 'defaults': ['paperwork_gtk.actions.doc.redo_ocr'], }, { 'interface': 'doc_actions', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def on_doclist_initialized(self): self.core.call_all( "add_doc_action", _("Redo OCR on document"), "win." + ACTION_NAME ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/docs/000077500000000000000000000000001417573700700240625ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/docs/__init__.py000066400000000000000000000000001417573700700261610ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/docs/delete.py000066400000000000000000000024061417573700700257000ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.promise from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_delete_many" class Plugin(openpaperwork_core.PluginBase): PRIORITY = -100 def get_interfaces(self): return [ 'chkdeps', 'menu', 'menu_docs', 'menu_docs_delete', ] def get_deps(self): return [ { 'interface': 'action_docs_delete', 'defaults': ['paperwork_gtk.actions.docs.delete'], }, { 'interface': 'doc_actions', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_doclist_initialized(self): item = Gio.MenuItem.new(_("Delete"), "win." + ACTION_NAME) self.core.call_all("docs_menu_append_item", item) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/docs/export.py000066400000000000000000000024051417573700700257560ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.promise from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_export_many" class Plugin(openpaperwork_core.PluginBase): PRIORITY = -50 def get_interfaces(self): return [ 'chkdeps', 'menu', 'menu_docs', 'menu_docs_export', ] def get_deps(self): return [ { 'interface': 'action_docs_export', 'defaults': ['paperwork_gtk.actions.docs.export'], }, { 'interface': 'doc_actions', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_doclist_initialized(self): item = Gio.MenuItem.new(_("Export"), "win." + ACTION_NAME) self.core.call_all("docs_menu_append_item", item) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/docs/properties.py000066400000000000000000000026651417573700700266410ustar00rootroot00000000000000import itertools import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_change_labels" class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.menu_add = None self.menu_remove = None self.idx_generator = itertools.count() def get_interfaces(self): return [ 'chkdeps', 'menu', 'menu_docs', 'menu_docs_properties', ] def get_deps(self): return [ { 'interface': 'action_docs_properties', 'defaults': ['paperwork_gtk.actions.docs.properties'], }, { 'interface': 'docs_actions', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_doclist_initialized(self): item = Gio.MenuItem.new(_("Change labels"), "win." + ACTION_NAME) self.core.call_all("docs_menu_append_item", item) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/docs/redo_ocr.py000066400000000000000000000024171417573700700262340ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.promise from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_redo_ocr_many" class Plugin(openpaperwork_core.PluginBase): PRIORITY = -50 def get_interfaces(self): return [ 'chkdeps', 'menu', 'menu_docs', 'menu_docs_redo_ocr', ] def get_deps(self): return [ { 'interface': 'action_docs_redo_ocr', 'defaults': ['paperwork_gtk.actions.docs.redo_ocr'], }, { 'interface': 'doc_actions', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_doclist_initialized(self): item = Gio.MenuItem.new(_("Redo OCR"), "win." + ACTION_NAME) self.core.call_all("docs_menu_append_item", item) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/docs/select_all.py000066400000000000000000000024211417573700700265420ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "doc_select_all" class Plugin(openpaperwork_core.PluginBase): PRIORITY = 1000 def get_interfaces(self): return [ 'chkdeps', 'menu', 'menu_docs', 'menu_docs_select_all', ] def get_deps(self): return [ { 'interface': 'action_docs_select_all', 'defaults': ['paperwork_gtk.menus.docs.select_all'], }, { 'interface': 'docs_actions', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, { 'interface': 'gtk_doclist', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_doclist_initialized(self): item = Gio.MenuItem.new(_("Select all"), "win." + ACTION_NAME) self.core.call_all("docs_menu_append_item", item) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/page/000077500000000000000000000000001417573700700240465ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/page/__init__.py000066400000000000000000000000001417573700700261450ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/page/copy_text.py000066400000000000000000000023261417573700700264410ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "page_copy_text" class Plugin(openpaperwork_core.PluginBase): PRIORITY = 1000 def get_interfaces(self): return [ 'chkdeps', 'menu', 'menu_page', 'menu_page_copy_text', ] def get_deps(self): return [ { 'interface': 'action_page_copy_text', 'defaults': ['paperwork_gtk.actions.page.copy_text'], }, { 'interface': 'page_actions', 'defaults': [ 'paperwork_gtk.mainwindow.docview.pageinfo.actions' ], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_page_menu_ready(self): item = Gio.MenuItem.new( _("Copy selected text"), "win." + ACTION_NAME ) self.core.call_all("page_menu_append_item", item) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/page/delete.py000066400000000000000000000022451417573700700256650ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "page_delete" class Plugin(openpaperwork_core.PluginBase): PRIORITY = -100 def get_interfaces(self): return [ 'chkdeps', 'menu', 'menu_page', 'menu_page_delete', ] def get_deps(self): return [ { 'interface': 'action_page_delete', 'defaults': ['paperwork_gtk.page.delete'], }, { 'interface': 'page_actions', 'defaults': [ 'paperwork_gtk.mainwindow.docview.pageinfo.actions' ], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_page_menu_ready(self): item = Gio.MenuItem.new(_("Delete page"), "win." + ACTION_NAME) self.core.call_all("page_menu_append_item", item) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/page/export.py000066400000000000000000000022521417573700700257420ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "page_export" class Plugin(openpaperwork_core.PluginBase): PRIORITY = 0 def get_interfaces(self): return [ 'chkdeps', 'menu', 'menu_page', 'menu_page_export', ] def get_deps(self): return [ { 'interface': 'action_page_export', 'defaults': ['paperwork_gtk.actions.page.export'], }, { 'interface': 'page_actions', 'defaults': [ 'paperwork_gtk.mainwindow.docview.pageinfo.actions' ], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_page_menu_ready(self): item = Gio.MenuItem.new(_("Export page"), "win." + ACTION_NAME) self.core.call_all("page_menu_append_item", item) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/page/move_inside_doc.py000066400000000000000000000024421417573700700275500ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "page_move_inside_doc" class Plugin(openpaperwork_core.PluginBase): PRIORITY = 910 def get_interfaces(self): return [ 'chkdeps', 'menu', 'menu_page', 'menu_page_move_inside_text', ] def get_deps(self): return [ { 'interface': 'action_page_move_inside_doc', 'defaults': ['paperwork_gtk.actions.page.move_inside_doc'], }, { 'interface': 'page_actions', 'defaults': [ 'paperwork_gtk.mainwindow.docview.pageinfo.actions' ], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_page_menu_ready(self): item = Gio.MenuItem.new( _("Another position"), "win." + ACTION_NAME ) self.core.call_all( "page_menu_append_item", item, submenu_name=_("Move page to") ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/page/move_to_doc.py000066400000000000000000000024221417573700700267150ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "page_move_to_doc" class Plugin(openpaperwork_core.PluginBase): PRIORITY = 900 def get_interfaces(self): return [ 'chkdeps', 'menu', 'menu_page', 'menu_page_move_to_text', ] def get_deps(self): return [ { 'interface': 'action_page_move_to_doc', 'defaults': ['paperwork_gtk.actions.page.move_to_doc'], }, { 'interface': 'page_actions', 'defaults': [ 'paperwork_gtk.mainwindow.docview.pageinfo.actions' ], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_page_menu_ready(self): item = Gio.MenuItem.new( _("Another document"), "win." + ACTION_NAME ) self.core.call_all( "page_menu_append_item", item, submenu_name=_("Move page to") ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/page/print.py000066400000000000000000000022471417573700700255610ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "page_print" class Plugin(openpaperwork_core.PluginBase): PRIORITY = -10 def get_interfaces(self): return [ 'chkdeps', 'menu', 'menu_page', 'menu_page_print', ] def get_deps(self): return [ { 'interface': 'action_page_print', 'defaults': ['paperwork_gtk.actions.page.print'], }, { 'interface': 'page_actions', 'defaults': [ 'paperwork_gtk.mainwindow.docview.pageinfo.actions' ], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_page_menu_ready(self): item = Gio.MenuItem.new(_("Print page"), "win." + ACTION_NAME) self.core.call_all("page_menu_append_item", item) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/page/redo_ocr.py000066400000000000000000000023171417573700700262170ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "page_redo_ocr" class Plugin(openpaperwork_core.PluginBase): PRIORITY = -70 def get_interfaces(self): return [ 'chkdeps', 'menu', 'menu_page', 'menu_page_redo_ocr', ] def get_deps(self): return [ { 'interface': 'action_page_redo_ocr', 'defaults': ['paperwork_gtk.actions.page.redo_ocr'], }, { 'interface': 'page_actions', 'defaults': [ 'paperwork_gtk.mainwindow.docview.pageinfo.actions' ], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_page_menu_ready(self): item = Gio.MenuItem.new( _("Redo OCR on page"), "win." + ACTION_NAME ) self.core.call_all("page_menu_append_item", item) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/menus/page/reset.py000066400000000000000000000022471417573700700255470ustar00rootroot00000000000000import logging try: from gi.repository import Gio GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) ACTION_NAME = "page_reset" class Plugin(openpaperwork_core.PluginBase): PRIORITY = -80 def get_interfaces(self): return [ 'chkdeps', 'menu', 'menu_page', 'menu_page_reset', ] def get_deps(self): return [ { 'interface': 'action_page_reset', 'defaults': ['paperwork_gtk.actions.page.reset'], }, { 'interface': 'page_actions', 'defaults': [ 'paperwork_gtk.mainwindow.docview.pageinfo.actions' ], }, ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) def on_page_menu_ready(self): item = Gio.MenuItem.new(_("Reset page"), "win." + ACTION_NAME) self.core.call_all("page_menu_append_item", item) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/000077500000000000000000000000001417573700700231035ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/__init__.py000066400000000000000000000000001417573700700252020ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/000077500000000000000000000000001417573700700240335ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/Makefile000066400000000000000000000021451417573700700254750ustar00rootroot00000000000000all: data out/paperwork_going_up.png: data/paperwork_going_up.svg convert $< $@ screenshots: [ -f $(CURDIR)/${OUT_INTRO_PDF} ] || LANG=C WAYLAND_DISPLAY= xvfb-run sh ./screenshot.sh l10n_extract: cd $(CURDIR) && po4a -M UTF-8 -k 0 po4a.conf l10n_compile: l10n_extract out/translated_%.pdf: out/translated_%.tex l10n_compile screenshots out/paperwork_going_up.png # run pdflatex twice because of the TOC pdflatex --output-directory=out ../$< pdflatex --output-directory=out ../$< out/%.pdf: data/%.tex l10n_compile screenshots out/paperwork_going_up.png # run pdflatex twice because of the TOC pdflatex --output-directory=out ../$< pdflatex --output-directory=out ../$< clean: rm -f $(CURDIR)/out/*.png rm -f $(CURDIR)/out/*.pdf rm -f $(CURDIR)/out/*.toc rm -f $(CURDIR)/data/*.aux rm -f $(CURDIR)/data/*.log data: rm -f $(CURDIR)/out/*.pdf rm -f $(CURDIR)/out/*.toc $(MAKE) \ out/intro.pdf \ out/translated_intro_de.pdf \ out/translated_intro_fr.pdf \ out/usage.pdf \ out/translated_usage_de.pdf \ out/translated_usage_fr.pdf .PHONY: all clean screenshots data l10n_extract l10n_compile paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/__init__.py000066400000000000000000000132771417573700700261560ustar00rootroot00000000000000import datetime import locale import logging import openpaperwork_core import openpaperwork_core.promise from ... import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 10000 def __init__(self): super().__init__() self.doc_urls_to_names_to_urls = {} self.doc_urls_to_names = {} self.thumbnails = {} # At this point, translations are not yet available self.help_files = () self.label = () def get_interfaces(self): return [ 'doc_labels', 'document_storage', 'help_documents', 'thumbnail', ] def get_deps(self): return [ { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, { 'interface': 'page_img', 'defaults': ['paperwork_backend.model.pdf'], }, { 'interface': 'pillow', 'defaults': [ 'openpaperwork_core.pillow.img', 'paperwork_backend.pillow.pdf', ], }, { 'interface': 'resources', 'defaults': ['openpaperwork_core.resources.setuptools'], }, { 'interface': 'thumbnailer', 'defaults': ['paperwork_backend.model.thumbnail'], }, ] def init(self, core): super().init(core) self.help_files = ( (_("Introduction"), "intro"), (_("User manual"), "usage"), ) self.label = (_("Documentation"), "#ffffffffffff") def help_get_files(self): return self.help_files def help_get_file(self, name): lang = "en" try: lang = locale.getdefaultlocale()[0] if lang is None: lang = "en" LOGGER.warning("Failed to get default locale !") else: lang = lang[:2] LOGGER.info("User language: %s", lang) except Exception as exc: LOGGER.error( "get_documentation(): Failed to figure out locale." " Will default to English", exc_info=exc ) if lang == "en": docs = [name + '.pdf'] else: docs = ['translated_{}_{}.pdf'.format(name, lang), name + ".pdf"] for doc in docs: url = self.core.call_success( "resources_get_file", "paperwork_gtk.model.help.out", doc ) if url is None: LOGGER.warning("No documentation '%s' found", doc) else: LOGGER.info("Documentation '%s': %s", doc, url) self.doc_urls_to_names_to_urls[name] = url self.doc_urls_to_names[url] = name return url LOGGER.error("Failed to find documentation '%s'", name) return None def doc_id_to_url(self, doc_id, existing=True): if not doc_id.startswith("help_"): return None name = doc_id[len("help_"):] return self.help_get_file(name) def doc_get_date_by_id(self, doc_id): if not doc_id.startswith("help_"): return None return datetime.datetime.now() def thumbnail_get_doc_promise(self, doc_url): if doc_url not in self.doc_urls_to_names: return None return openpaperwork_core.promise.ThreadedPromise( self.core, self.thumbnail_get_doc, args=(doc_url,) ) def thumbnail_get_doc(self, doc_url): return self.thumbnail_get_page(doc_url, page_idx=0) def thumbnail_get_page(self, doc_url, page_idx): if doc_url not in self.doc_urls_to_names: return None if page_idx != 0: return None if doc_url in self.thumbnails: url = self.thumbnais[doc_url] return self.core.call_success("url_to_pillow", url) page_url = self.core.call_success( "page_get_img_url", doc_url, page_idx ) assert(page_url is not None) img = self.core.call_success("url_to_pillow", page_url) img = self.core.call_success("thumbnail_from_img", img) (self.thumbnail_url, fd) = self.core.call_success( "fs_mktemp", prefix="thumbnail_help_intro", suffix=".jpeg", mode="wb" ) fd.close() self.core.call_success( "pillow_to_url", img, self.thumbnail_url, format='JPEG', quality=0.85 ) return img def doc_get_mtime_by_url(self, doc_url): if doc_url not in self.doc_urls_to_names: return return datetime.datetime(year=1971, month=1, day=1).timestamp() def doc_has_labels_by_url(self, doc_url): if doc_url not in self.doc_urls_to_names: return None return True def doc_get_labels_by_url(self, out: set, doc_url): if doc_url not in self.doc_urls_to_names: return out.add(self.label) def doc_get_labels_by_url_promise(self, out: list, doc_url): if doc_url not in self.doc_urls_to_names: return def get_labels(labels=None): if labels is None: labels = set() labels.add(self.label) return labels promise = openpaperwork_core.promise.Promise( self.core, get_labels ) out.append(promise) def doc_add_label_by_url(self, doc_url, label, color=None): if doc_url not in self.doc_urls_to_names: return None return True def help_labels_get_all(self, out: set): out.add(self.label) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/data/000077500000000000000000000000001417573700700247445ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.png000066400000000000000000006671331417573700700266250ustar00rootroot00000000000000PNG  IHDRHxsBIT|d pHYsetEXtSoftwarewww.inkscape.org< IDATxy\u .V3f'ƲL,ǜ35:s&5rɝD3qQQd_? \_ u}>|Yf TXXHDD{ԩS9sk׮q 233qpp WWW4h@Æ iѢ;w}8:: """"""""RA*m۶|rnJVVcggG;v,G˫L|kϟ˗-_wuu%((VZ١餥qE.^ѣG9piii1ܹ3&M駟F~""""""""R~v_W ԩS ɠT͆ x׸tPR>L4Ν;cccs_fN>Yf G|VF &M/LV=DDDDDDDDb޽޽{k.C /5kpwwg̙K899s.^իYl.\|=88?vڕ۳DDDDDDDD߿۷[feeEXX}5$ Ry(Gaܸqaccëܹs+tdb۶m,Y-[`2b̝;-ZTسEDDDDDDD9rٳgy[>2e _}J yf&L@NN|wtܹR38q9svZf3vvv̘1YfUǎcf7n999lڴ OOON:aUYr%< ۗիWSvmDEE; @FXd 4,ԩS||տ#G>ɉ-[RPPʕ+y' ͪT͛5jEEE5+WRZ5cqF^~ebڴi,\;;;0a>ݸq3fX޽;Vd""""""""˗/3oKZnݺү_Tf;6:6,^;vȚ5k~({K.ܸquVc&%''3c 7n̢E'00-[~r`ѢEL<ʔRmۖl7xH-;;dzuV k׮FM~: .dѢEdggжm[>C~σ\B)..ѣk׮2bf=$;;ݻ3}t<ggg֭[G߾}!88'OKDDDDDDD۾};5bdggӲeKBBBdĈe:~ݺuPQPA*c6l؀ _|ֿo֯_OǎIMMeذaKDDDDDDDJ{ȠYfرc7Lh-GYQ1o g69s&?}:={$77Sjȯ|טfO֭zmQQ{k3339}4ׯ_gϞ9rbhFFFTni NS7on"STTdp"mٲ$\\\5j;}4VZpQZjwyyy/~ [^7oNn݌Sz-8<˗/7:BBB0a|n69x jՊ Ο?OϞ=oYꊷ7> [N4iI*GZ,Ν;WHEDDDDDDf2 `ȑ|~5݋#>>>۷jժѿo;f߾},V%V檸_ '& .аaC#Ul|||HKKcݺuw:."""""""(;p]tёׯ4422;;;lmm"))]Ouu1fjժEBBիW(3 ㏘L&rJ6 ~gXdiDDDDDDDDsN,hNNШQ#.]D^^g9 0|p|}}q[ Rɏ?A NR^|Eؾ};F1DTT:u|-??777\\\8|0[[2ikk믿~Hrrr@*Hl6[ ӽ{wf3qDDDDDDDD  @۶m-_suuի$&&ҵkW{W_}͛oQny R"))jժѫW/bر]$""""""""вe˛>kժAAAٕy|o|UfCb@^prr281ƌ"!!8""""""""*==boN:5Vll,۷o'++>}{ogϞLTѣG֭IK۶m1͖ EDDDDDDD@ɞjz1233 '++@j֬ygfРA̡C:PA*X ݻ$""""""""d2`mm}ݛCdd$.]LJիeYb_ښ:wLFFȑ#K˹s=zSEDDDDDDD*Sт|_\\899Q^="##GԮ]kԨAhh(:t 55޽{iӦr{ae6͆www'==MҲe2/WҫW/6mڄ=˗/gΜ9lToJPAZdT.OY̼^z@;v.]+۶m駟~C`2vTiAZ""""""""8y7&11?rgϞӠAӧOT aeeEݺuS%p T>'''bccCHHg~1+ 00[6 RJj NS5v#bA~;w.$57=9]vYVMXfIΚ6 N""""""" #̙3-GϟOpp0ׯ_/ӧOg͚5899UHT,%`i)(`kk @aaIDDDDDDQoBΝӟĹs猎$r[saѢEٱeZlɷ~{RSS0`+WƆ?> JH]v*HEmhPf;ЬY3Znٳ6:M^}UvEFv֭?m[nٳ'''֬YkVɩF *5[DDDDDD-3f̠zԬYkkkN8~H@@7歷"""dpZ֭ǏޞKǎꫯ,}ڵ+111xxxk.Fip;SA**HoCT4www?%\|˗=.\`…t OOO&Oƍɉakkˑ#Gxgo߾ТE O``ѱJX ғUIDDDDDDQ[oΥK III,_LJJ +V`Ĉxxx0ydBBB61ՠAٳW$77>}N zO*H"? $%%ipyU^wyO?2;Օɓ'wߑի8q"5j --+V0a֭˓O>w}GVV")///̙C\\!!! :iӦm6\\\W&*HGGGrss NRu$&&P^=ȣnԩqU6lpիWg„ ZokN: 0?k׮&8cܸql޼*HŲ!~W^$""""""stt駟/ߟ?+Wg^{5#,,_///zaNDL }K~ٳ4i$""""""8:u*VVVرo{ 8qٳgGqq1ұcG̙CLLLol6BN=F1lf͚deeM6m$"""""">}k.f̘… jh֮]ڵk9qMk׎1c0zhZlPy  ՕT.!!H""""""׿O?MF-qϝ;g)K:į ???ƌØ1cСVVV\ Bll,M4Ύn֭ :BT)..̙33ٺu+7n$44Bg>>> 2` m?_*+ܸq4;t:t08_H*H___SNq55TDDDDDD*]۶mqpp //gx ĉkGGGFm)Km /999]#Fpҥr}QT P h;ҥ Ǎ?~8;;-%,,r]oooϠAΕ+Wؽ{7͚5M6sDTJ ~DDDDDDDWZɓ$66f͚ѵkWN:u#!!Gl.\666ٓz 7"FRA* y}~~>?788;;S\\Lbb"vԩSlذ/%}XÇ`׮]dee=x"F5:T ͚5 ,,LܹqDDDDDD1ոqc1OJJ sezETT:unݺ\vk׮QZ5Ӊ̙34jdվ}{իիWپ};G~w T P 5ׯ_ʕ+xyy@Olll N#""""""+WWWxx{{c2&55urQ prre˖accCbb"9sLRRVVVtؑ,W^|VVV 6e˖i&򛧂TQ5DGG?viqqe#GFDDDDDDg... _FԨQu뒐s爊d2၇ 334W{d2O޽2l2lp/+R mry:tq*Վ;HJJf͚TDDDDDD U:ƍL{nnniӆ ]ŋ uG`` xxxٳg9rգM6Ԯ]bozFPP$%%q5jT>//R9}4xzz燝WA*YhT+V0~x N#""""""ҙ&2/}5kRfM6mJPP8qgϒ˹shٲ%4mڔDZhAZظq#^^^0p[m޼9'O*Hp,]3vx z)NJ˖-oW؋E@@QQQ'\٬_~4""""""+((CkooO6l#F^zpy]ơCߟ:u0i$iҤm+=Q~L&,YBFx-娧' 6ׯxbڴiɓINNi bQZ?7n^,ׯOϞ=#""""""\lll,ayxxеkWFI6mpvvիӰa;UA*F;{,AAA$%%Qn].\ȕ+WHLL… dddfdbŊaGXx{{S~}L&{5:N/x{_VZVp6661ydG޽o.]I~~~e ))W^yVZgULll,3f̠^zk3f ڵƍȐ!C,Mz O?dp#Gcʔ)F1 GGG6l]oٲ%sJJ) f͚E&M/(,,gϞ=zw}gz͑#Gܹ37n` OAZ:{t̘17tADDDDDD(W\._UX'58rmff&{%//zzz /%gӨ 4lؐf͚Q\\Lhhq*… ),,K.8"""""""o -=I͛73<åKx'LLL < 666@x~g|}}VZ0`GUA*:t([n58IŹ~:_5iDDDDDDDTfpIQqCpp0Ǐɉ~?Rq9Ο?OQQ-[ܹse>0 (T-J m۶Q\\lp`iӦ}EDDDDDDSNXʴ^˩S=z4]ta׮]1uTΟ?qqq\֭[Ƞf͚\zBv튃j IDATC rTnѫW/jԨٽ{qիWYx1fD"""""""% 000e y ⋴mۖcee9y$K,nݺ9s ]vƍҼys5kmǿgЬY3r+F`9QC;vqDDDDDDD,8,)6m"<<Tnz@l<\ϗ߮TfΜIfXt)EEE׏wѴiS˵EEE={(<<<4k֌mvlܸ?>}PA*w1qD֮]KQQiϹs,{~ǚ=*""""""UJiAڥK2ߓG Xn!!!WZY 6^Errr?>7O?%77JXX;v\k2t۷oz4jԈǏSV-zmk)))\zVZѵkԩ RABJJ aaaF)73f̠>}0p@㈈$<<()m ///Ǝ[.}WرcOA Uϒ߶"-[FfxwIOOLJgy??[&##&MC ~X[Zk͍70`&[KLLСC ۳g,]4c֭lܸGDDDDDD&۷̓z k5bĉL8;;;V^ի-{.'gggCoD~l6fZn /+Wn<>>ӧӨQ#zYYYӇubccs3ٲe 111k׎urYbbb,{ںu0ʹjՊFaerQQQkbcc_ёXAAmڴ!&&W^yrHHU q+::gg2xELL 666ٳ{KjԩS4o޼\ƔGիWyg,k֬ɴi%++|._JV\ɨQ9l&%%PڶmK&M8tiii䄫-oߞHΝ{gm0:ӟDLL nnn̙38"""""""ذa8s{  āؽ{7vvvr2L%"99-[0~Xn?<ׯ_ޞ^z{ڵk۷Os5z)i߾OLL$** ///r ;wI&PVwa"##eʔ)Zb/wK/_fʟ>}y`;F1JAA 6nƲӵkWƏٿ?!!!A͹;oO?D@@>,_~q3vX_N-8p}];7%͜9w^"""AM c9 JrذaԫW@K h֬.]O?74:}1Lݛ{Ν;urT9k׮eر=INN̙3ի\2Kz޽=III[P\\G}ܹs)..`ժUexY`=_qtt=oc֬YdddXwӖGԫWڵkspqqSN}|ff&^^^dffyf h܃=o6~onEػw/NNNQJg=Se*G][777?~#7nlp"[e&O\n9ԯ_Lcgffx;wM6ѣtUǑ#Gx7xyLDD}aDFFRzu.\ȡCԩ.]JE>|t3f uԹg\fϞp R'f͚y󈋋36P͌37nТE .^ȩSprrAx{{)צMhժ~!yyyуW_}kTJL2.]ôiӌsO3g<|MEDDDDDD*l__eŋ4lذR -}"IIIߟwyb&Lcܹ3K.:"""عs'|aN>SO=Evظq#L8'Olٲ \|hժݻwយ9r$Ç… ԩSݻwvӵjL/eӦM|FGUVh"-[HKDDDDDD >}GGG?_~Lˊƅ T2mРALv}ܹ>3V^M͚5}FƬR;FAfnԩSwUVZ İa8rVz(YR}ޱcGˁ`wG}D-ذaL:}۞OTʬm۶<\pD:u?<oe_I&Qv2FPPPt2yff>cGbb"M6%""mvmڴ1*jcFeɓ'y'iݺ5+Wd2ѿٴiw/66gI㏴nݚYfK`` `ɒ%>r_ΝKHOOgܸqƍ;,z'#QLL XYYkٌ]tYvsY5jTq*\vv6'N1LL8ÇӮ];v>>>,~\si:uę3g8q"mڴa՘L&޽{پ}=nMOO@KxƏϠA8wnnn|߿L{ RZ5{܈_l6"&L3gdvHyO1ۗVZ\ !22²L{^ω':pݺu#$$>SVZeYRk۶m[1/RTT| [d21h Ghh(ݻw/xd2yϭ Y`͛7ʊ)SË/X3iltȯ4hЀ+V0|pPV-iTVkR^=C󈈈˗YbNsrrwPRLZ<<< *5[QQQRzuF '븹rJxcbbSեDrx۷/{~W_}SNЮ];ѵk~ Ry C?gڴi|gT^>x.\@ll, $%%Drr2)))dggGzze橭-5jwwwpwwyжm[<==/bADDDDDD2}'ХK@c4lвGcjj*۶m#//777z{a&LP:tPzUW_}K/Daa!_??Sqƕ7tZ(YpسgD|M[jժܹsyx ܹ3࣏>O>)**"-- (t?_kԨ >^zY_|9o&P?jY1RBB?5kVȑ#tPvmzm*b˴#Gi&w} 0zhW~{KVA}%((#F0mڴ2Nnn.{kEEE,^ٳg&MbwXWV檰f\zKzjΜ9sиqc|}}[.SN\\\pppKKFF\zD?NTTqqq7=ӓW^y3ex饗/*@DDDDDD䡼+|tؑCURRR)..ח.]ܼܵy3ݻw/ 6$..ƍW+L^^'O&$$3f`2̀ŋ?ܿPOSDDDDDD";we˖0gΜ ^:u,˗ l6ACdee!..W![JJ Fb߾}ذh"^z2UL&er۷0`IMM>`ŘL&y7kT*%%9stR _dԨQ<͍'2qDYr%s̱qq,"""""""wߥ^z1lذJ}qqqlذڷoOf3fLڶm:uյ2˗/3p@bbbpvvfʕ >Ƹvk~.J ~70c ~V!ZiӦYO NVbܹ̟?\]]Ylcǎ5:m @||ޒWfg:SwĘT7n0~xz)~ٺu+U9B@@iii7_|⭷l63v;PR={Y:tĉ\fddk.\ѣGٳ'4lؐ={++ 5jWDp=SN;Y7T,oN=8w^^^O|'TVhwg}ƍSǏsٳh"""""""2sLMFfxÇqwwɓԩS޽{3`~ʊoz +++r͎;4h7n 00={PcZP8q"lٲkVY3*H+WLVV={ѣXeذa:t6mڐB믍%"""""" ;fͺ~~~899a69}4ׯ'99@z͕+WHJJb߾}۷DL&S% $''sN ȑ#%((;v! ҊҧO6nȐ!C**Hŋ3i$ 1bԯ_pFEAAx)**2:?>˗/~Çmt4yLlݺuammŋ~zѻwo<<<8tG!,, [[[{_8qz8{,U =v#--@mvיfpի{,Ƴ5:TǏBqq17I&ѹsgVɓ'[ర0 ͛﹤ADDDDDDa+K/ѵkrF@\\YYY8p˗/Kɡe˖?~-[0h hذ!rtɒ%@a:nnn>!>>!CJ@@wwlf߾}ԬY:u`6˴lt,j2fCHŹvf믿AAA̜9>23Fe > :t@hhhl,""""""r;o&[.OЉ:999?Һukhժ͚5#//Ν;IKKcǎq_{iѴiSؿhu z'hܸ1ԩSAƍVGk׮Yу:uꐜ\!SA*,,>㣏>"##`{*Xe;x'JZbΝwCSDDDDDDADGGӱcGXz5&Lgs ]ٽ{7AAAԬYٴiԫW2-K.DDDT›[AACeǎԮ]}Ѵi;^_TTqtt' >>&MQ]roood7Ǐ3e> @Yp!C58N:ŀz*m۶e׮]e@DDDDDD,L&=z ""Am6ò\rϓ#F ==|HNN&22lmm۷mǹ~:';;3rJ~[fyVX#aaato>|}})**"11-[RɄ ۷ܶM鐦GHAAs̡C>|Zjd?rעE BCCquu%::qƑot,yD|DDD_|ah///zMرPT `Ȑ!xyyѰa;g?Ç7sb Yb]\ߏ#֜={֭[{t ̢wQUY߇?* EfEĆ,11he4cy$Mb,јIvco b`C@9,qr}5+ gW<}̝;w΍7ꫯ*9EըQ#|}}100ɓ 8|V!B!m=yYfbkk[U4i҄:uݻaÆ HFC#G0w\/^L_lvv6'O}}}BCCi۶-FFFoջ`ը[/MZ7ŋTZ답uyjѢ۷oG[[{2a„I!B!{nŊ$%%ٳrߴh֬ Օ _~$lmm`׋dȐ!(J|ȟ_|Qz +HSF" IDATwޥUVm̘1gJ8ZLC a@V'NDB!B!W-Z\xqallL||<nnn4jԈŋSޣɒ%KHNNё8L4Ε]vڵ+q8 P(1b_T(=taLbb"իWg˖-tڵzoT* Ķm011!00S !B!EŁذaG!77MMMvʰaӧzzz<ё46mT'|B`Wm={FJJJόMHHvڤ* }̟?qqq!((HR(Ygggׯiii=B!B.~) 66+WJ^^Gˋ5k2tPN8o%-- ggg{€3@@dddW3m4MFttCDNN#Gd֬Y(J ƹs]vyAgϞ=p5F]# !B!_͛7̌d6nH׮]aܻw\g F.\_(";;(._=.?}t5jDbb"{&55U-sDYn,YSRRNmۆ&[neÆ =B!BPÆ Y`?~!COdd$ .6mڰj*?^M2\zZU(j[ei֬vvvܸq"۩Vٓ&ʞΩST[la=K.L> &Q !B!PijjҥK|||fʕR8wcƌF 0W3닆?c+#Zn6ׯ_'**MV ؽ{7={-[r'ʇ\|2{&66sssߏ[yͥM6\t www,ﱄB!BTl۶ 6[ZZ2x` WRƕ+W9r$k֬Q{dNJ~ؽ{[ٷonݚt"""055QFU͠ <<S&L_m۶ˋΝ;coo_[VVQQQ|ȣG YP,< HA?3(JtΝ;ZjyU;mmm\Sy$B!JIIa޽lܸ'On5եk׮ :~V333W>d֬YĶoqssz7++˗/Nǎ!00bݿ?-Z쥟c޼y>}רQ'''ԩCթ^:Fvv6*xy<}(?~\utt 333LLLؾ};pa:w\C';DR1uT.] ȑ#?.*&JE^ݝg -B!BGeV^ݻw ?^fM<==>|x\hޘUח={Rn]BCC^ZZǎRJjՊ[n]T*N<1...o|>44M6q^R,r Wvmw&O/[nejC";"77ѣG~z͛WA\p`Ǝ*ߧQFzjFU# !B!\ªUغu 77hЀC2bjԨۓK{bt-[ؘ"?:uBPFRRnnnXXXѡjժѺu"իWuQQQ<{ M000(<SSSLLL^:&&&ԪU kkk,--n…Qn>>l۶;991l0ػw/[& {Ύ{DRqYJ%ѣ_NڵINNΝ;o߾ثk/\+/uz8n޼YIŹs֭׏ MMͷ_QѣGmۖ 8~xJ8ZD۷>CR]!B!EҪU+VXǏپ};={DSS7n0uT݋B`t8 P^=nܸg U8z]"##144b7* bu!!!4nܸܺu޽{NٲeE i9 M6RfMuoI>}@bߟC8B!B!DUTp!iԨ}YoiӦ牾yׯ_ڵkqi]vjܿ,,,|rgHII!$$uLjj߉gϞ$$$Ğ={TR{WD:uDdd$666Tfoܸ?Pf>$|WL6k!B!}bnnΔ)SqǧG*ggg^Zj=044Ņ+W`ll\앣AAAhkkcaaADDDa[.\ ORAjӮ]-GȨX}+2 HPpp0ڵ#&& pyʬxx8}ٳzJł زeZ}7TR6mڤB!B!DyS;ykJll'''sbx9iiihjjEݺu+iiiѰaCnܸVkIMMW^RF =J͚55wE'ix";wٳg4i??2I׮]yqw*QQQ̘1QFYzL6 s璓SB!B!xZjJɓjs._LӦM '''X4+J߿۷똚bmm]mxxx0`._Lʕ޾=E> H3g֭III4oޜSNajjZf߿Ovx!ԪU 2+++ʕ+j_cbbƒغuZj !B!ͺv j&$$ n"##-[FDD<---\\\000(VÇSNΟ?/wR*1___tuuٳgOW|2???ziӦ Ǐ/3 """h߾=>N:8qhb0>R(?^-5 0a?JR-uB!Bz{`޽^nn.iii萞NZZڵCC葙J"$$:upM0.++K.G^3GL͛`ƍtܹ=ŋ$ -EGҾ}{>L*Uʬ;wС=Ɔ'ORjU7+ 8q"+W&44{B!B!^K.XYYݻK\J1lQ=z3gGfffE~?''+W`mm͛7Wo|履~bٲe,]OOb-^$i)ٹs'#33={r*W\fCCCر#8::@:ux ظ)OڵWycccK %!B!B> >suRɡCST\ط'$$3gΜuT^py,,, 7vZf͚ٳ4iRKReO>aϞ=TTҮ];bbb_>~~~XZZ5AY= ЪU+VJRR/^T[ɓ'KPPOV[]!B!Bڄ  >~ᷪQp!+y!W\\\\?EGGcllLLL 5jZ[!B!/7PJ]ƒ%Kn:uٳgԫWX*%''s9̤yX\2 T*͛7ݻ7tЁM6Y$ U? RɨQظqc &;;~qiLLL繂-  &&0$VMҢE rssYr !B!,--?>}]Գs/N^%99Zn666TVX}?~LNNYYYT*lmmÇ޽;4i҄{QjdƎRd̘1\ eL<Jٱc+C[ܹSǏʕ+Rkm!B!Bرcڵ+YYY|'^[g իWpf͚T*i޼}KժUW%4|N _ڳbŊ2 G޽; .DPw߱nݺ׮\[ _055%..{B!B!^NCC۷Ǐi۶-sssBCCiԨo]ޞf͚P(^\zz:{&,, SSS>\신DH@Zsa֬YL>%K7ɓ'T>>>̙3dgg?tRJZue@%]B!B!(FFF>|ڵkZ/hOi R PpUTѣ8::zߊJҷR:u*ۛ~:t777BCC" GT*~f`%*;va8v\$B!B!{{{Μ9Cy)=z`ر<{L^)JαceϞ=4mڴVdewBT2~x,XwGTҴiSWZYYU Vڵk6m7lWWW\֭[Z[!B!WV-._̸qظq#۶m㫯bXYYLDGGӧOy1O>%++DIKK#//MMM%**={ɦMԩS^H@Z yyy5ףP(Xt)_u ˋW0zhֲG& 6mp BCC9wj1tP\% B!B!ʘ!>>>|gL20,Xŋqwwo߾mۖ&MHOOÇDGGMdd$QQQDEEC=zDJJJzo|%!F"fРAڵ MMMV^͈#ʤǏ'55Ub ]'::f^ 555o߾lݺ5k֨5 4hSN\rWWWB!B!DՋnݺc~W9s gΜ@OOGGG^:ժUjժTR.\ᙔDll,qqq<{'O\Y455155FXXXPF V룩I^^deeN֭ O"ȠƍJ{8q"W?nڴ/Xz1@&)XAj``_|֭[ٱc˖-J*jQzuw΁رcB!B!D9??'44ݻws1IOOڵk\v=j֬5VVVXYYammMZ nff\ RSSϏJ*c<<$ B!B!xI@iFFFzjQT,X_۫7*J*2d|3gΐB!B!B$ }ccՇo{)cǎ`ԩez)fffe]Qfdd̙.ΝW- 33S+SB!B!zI@ZZZHKTkʚ5k1^={ SS2^֮]ѣG0k֬ن6 E6{uB!B!B$ -{{{ܹS:&&&̚5 ϟx*HW^f=% H!?544ի\R-= ÇB!B!P? H(y@ ;;;bccYhQU||<VD+W %%奟777g̘۱j IDAT1R֭ZZZܻwB!B!'i888^Z:::|,YGfQTם#ggg>}z{Ѽys@nB!B!xWI@Z\A 0p@SK7[썌ZZZ( 6nȩSJܷ{B!B!* HnݺRB? zZNE_AZ&$$6m0|pT*ƍ#--D} ӧOUZB!B!B$ -{{{ )))j9]vxxxT*9sZjJ*\9YQW|+i_gѢEĉKccc ,Q-!B!B~!uPkȵpB8r'OT[Hnn.PqWZXXgW]-[u_ 8s[B!B >>+VO?qbcc{$!D i\ΝS[3b~Goutt osh)S3fL.j۶-o]C!B!>Tyyy2`,--7n3gΤGceesחYQT*Uy>Xjcƌuj IݻGݺujJm \|-Z`nn#7qqqԨQ"999H-8<ŋiժUT!!!MMbB!BMDD֭cÆ DGG~nݺ888t&NNNmۖ6mбcGLMMrt!F"}6 6DGGDVˋm۶'k.-pIt那#aaaj>PT듙ɭ[hР ӓ[nCll,ժU+vHKKʕ+͗ B!エ4vڵk9{,Dʕdȑ;8!իW &((sɓjjhhJ޽;-Z)Bb-ET~}LMMVe;( ˝;wZ 55~_( lmm]777nݺEٷo[ڴlsHB!ӹs5j5kd7j۶-֭#&&5kּҭ[7OΝ;y1waݺu9{{{J%|cffѣ9qyyy !3BuրϒlҤ ;wFTfֆAeSYݺu^B1rH FZZmڴիt޽D۵kٳgKTG!B!?f…ԫW6mڰf?%3f ,,3g0|b}Çf ~W^ϟI׮]`W*xI@Z !Ç^/`Æ 7Ϋ Wꘁ@\\\Xn 'r)JܻࢦB"B!lكjb鄅ç~ʡCx!A-=0a$>>}1h y)˗/777֬YSH! H@Z | .}ȈX=+HCCC_x^^?ܹssss>̯Zzl*9B!B͛7}~z7===\]]9T!BaHJJbŊ4o'''~022b3i$W^ѿ133#11yacc7e>"i1yzz_8xZԪ+H7nj*4i¹scٲeݻSSSNOO/ e^_!B!ʂR$ 1c`eeŸq DCCwwwV\ITT{"򖝝MTTÆ lذzs-Z .$;;GB HO?EPpEZYfXXXl+Hutthذ!3Tݹv&M* t҅K.KKB!B0w\h۶-V"-- [[[͛ /D__@Rc_9ϟ?GR1tPn޼ɖ-[ppp 11ӧʅ {l!D9ԩCfP*l߾] }Z!222 |] 8soQQQmۖ .PJ|}}  !B!`̙wרQ;ˑ#Ggri#;;VSS///n޼ɢER 7oޤ]v̙3G' !m!C|rZ <;rjlQ[́hذ!W^Ņ7o2a44J?ݹ}6fffѱcR%B!͛GOf͛ǘ1cСիWf͚tޝ2ݺMLL wޥz$''cbbBݺu_hHGGiӦF>}e޼ymۖ2[ɡ[ntJU|0*,ϟcmmMrr2w_~j)yyyDDD`ggW⚟9[l᧟~bjĉ9q5kdѢE FFFŮ5zhZñh۶-m۶jUHHHVpT}d[8q"ZZZV]ֺA||<ӧO˗CNz*/.ptժUۗ4ڷoB!B Ԕ;͡CHHHL2عs':tٙ7t~Q(JBBBuƤabbB֭*-аaC.^H-x9/o 8p`9Oa-ծ]>}tR.X" }s֖ Nf8r'O,2~xƌCnn.9G)#B!BԤUVܻw3x`tuu~:CI&8pصECC(ڴiZ6334oޜwݻwR[ªUR\fiP(l߾]M*H:T{1i$j׮ͼyHII~ر˗/tڕ˗P(;w.7nRJ[!B! VZqF>|ȬY044ƍݛ?"333#>> Kʕɉx @VVZ{QTӧO'77O>D25Zh'JSn:uP($&&駟ҢE Wcǎe߾}އTRO~ppp_899i&n޼eΝ; ~~~uVfϞ]&B!}eff?@DDǏGKKCѨQ#֭[W::tԴ466fϞ=TZ`MVjxVYz[K޽{4hЀ,[bbbX~=\tǏgڴioaoylڴ?\fd^^K,o!''vڅSB!Bɕ+W9r$ׯ_/K-ڵO? |iЧOܻNlmm0abx"Ԯ]Yf@NNժUАl޼/cƌ ̞=ggg^ۣ 4|ۃӳg[nԪUӧ>Æ & 2 G޽K&''}(B!B%WWWFPzj:vXx!MYxQa@O޽Q*L4fի/p R5HLLxfϞܹsΓ'O>}:>>>[ߛ7oڵ+{YYYl۶ӧlmmp5jxǏgx{{`"н{8pՕ!C0dt.R7h"Ǝ+[B!BMvIMMq?~UOr-,,,Uzzz/}."" ۷/(зo_WsW:$ U 60|pE|رcxyyBˋɓ'Zqqqθq>7}t.\g͛79x o޽{3գ_~ :qir &Lŋ@6lnݺ2B!B|Ȯ\G}D||< 4̙3Ǔ'OHII!33jժEݺu_gȐ!lڴsN#uNNN ,--DCC6ɰa۷/ >>tRzIBBNNN9s͛7;s',YÇ_\ʕHII)vcf̘=NNN̘17rܹpCܹsϟ_.hLL Fy\xCCC/^̹s$B!BRʩSQoߦmT*x1dddP(hݺC؂#WEŴp.$-%F+WF?_hSL!//s%ڴiS@YbddP>|ȸq㰵eܽ{vʼy@L2rɓ'ޞ5k֠T*8p L:MMrK!B!*ƍsAgܸq%APPԮ]H*UDfͰ.R-Z`ggGnn.G-{,Zb 233qssc<}cǎҥK eNqL˖-###f͚q)n݊e&B!>>>( ֬YÎ;]#%%>$%%ѫW/W^{!j/bkkKfqj֧O&LJbȑrƏɓ?н{W>SEGGoQ]v͛MFF[ѣ\|AQjg%J۷/vvv,]t\\\ؿ?/_cǎe:B!B|/9~رEmff&nT\233qvviӦo}xvŅ( Ϟ=cڵ/|lrAt)/t҅ <<<۷/YYYՋŋODD/xs@~}(m6mu\z*Ul2Ξ=Kn^Ç]v#ƌݺuc߾}ѦMKPPB!B иqc6mZy pqqťĻ96l;wv.~#-- cc3Gy@KK;vHll,O>ё͛70ݱcǒChҤ+@OORIxxJVV_}C !==.]pm&Mگ?** Zjl ZcccêUHNNFWWѣGsUΞ=K>}$B!Bw.˗/GPm6.\wɡ^z8::RR"R* rrre|rlllP*ԯ_Fd6 HK{wv9q|w}VSS[nmIII{\B38rH}6uQLiiil߾>}PfMƌÙ3gP*s}gZAB!B'*3f:uL*U޽{\pW>UX3))ȵ(+Wٳ4hP9Oᓀݹs͛쬖oߞ_p#Gf/Vwnܸ9^&66:燁{aE:ZT@֭K>> 0333Φ^z̙30,YKS!B!Dߟ@})ϟ'//ի499Ym3Sxzz/N$ErYWYϟ?ۛ+WT*֦AdffFxx8@ӡC:t耛6m߿ijLdd$;w&"":D-7HHH@__͛jj*~~~>$066&11###݋~zz:-dggO?0ydv LeERTp^:ɓYv-X333OOO6??P455177]]] //Q{;w`ҥ())[o襽G! ƆDqsskбgΜaڴi$&&ɓoXZZ.];;-Zwެ,CXXVVV\t {{sܾ}AǨөS'\]]quuG8::63f`Ν̟?u=   _3|pݻw"//,,,hݺ5 :wll,vvv(++STTJJJhݺ5IIIY-[/zyAڄ:uDbb"NR8@… $ oΨQssZ433cر;*>}u\¹s "&& {gd=6gQQ#G$,, sssΟ?Tџ~Im޼9TVV cbll9NNN899Ѷm[^hu9Z   m|O"22\GEE޽{T玍>7;033c֬Y,9#2Hɓ'/ IDATDSS???S9skN< 6<PVVFpp0#!!+++lmmkYtx"/^$,,Zlْ}ҿz… ŋtܹL(vJ^^/;LFhh(YYY˗$,:vȔ)S*@ p)F3!!!O5   chhHAAW\_ulTTӾ}V[~=gРA={4eӦM׏;" MOHبQ8u:::|L6kXxü;wnܸA^^akkŤ*gii(+++挌_M]]3gп*PZZ믿NPP\|;~?#G96mbڴi ^Khh(:tYf7(,    ܹslܸs:eeFk4a:ΪUeN5yd~Gڶm˝;wXl_5E/!7Ç3bΟ?ό3Xr%+Wؘ;wp!y֢ӧOgʕNÇsy$IbȐ!hhh`nnL&8BBBh޼9;Ą1c0fٔ?LJrOTQ >K!f͚ő#GPVVFVVVfҤItڕ),,d޼y ZKVPRRtqwFAA3Νݻuiн{FWx~6l؀|8JJJ8pN?&LL,XƍQRROl9iii\z^{$   <_wfԩՋ˗/?sFDD;ާchh\+d2z͛73f GƍuuuRSS100x|e &)--Օ_k׮ Ce񘚚>qlVVFFF:igg-]v%66###())AWW͛ӦMi֬W\WQVV~S`` &M",, .ȠwO>'ND$f͚UkpT$.\ix5F,Y"~zٷoӦMgϞ5iժiiiƊ   -[Y]sk׮"8*<իWsMXn`С"8iJJJbСdffҾ}{N>hii)׮]wuwuuEWWޠjՏ:8h d2YYY$$$ٴnSSS{.iEEW櫯###m/±cm8ULFAvv6]vܹsͪU 11}}}.]x%%%nJ@@,^={(VZq5|]   W]---zaÞ97o|r֮]K˖-d߯ڒɄ#M޽{98ZhBhⰲK.Oqq17n}aggJs*++cbb eeeTTTÇL~>X{=O̞={biӦAGAAF"** NTBCC{+889{,VVV gll $''{rr2&&&q KKzWWWG]]OLL`̚5ݫY[[+1rHۻ޵^rwwwyfg۶m|#F4zc1~"@pUsWUUe߾}qPZZ2 eeǎCOOb`` ꒔Dvv6NNNhтRVVFhh(/^;;;,--JEEN> Tej߿U_Edggɟ6'O|"8[hڵk7pYȬGeݺu 7oAAA[u9*~g&N~gٷo&&&@cE:d2_78 [[[SOOO~/{ee%***OAAjjj!խu]v߽{wINN&,,ݻ#ɰsΔ@FFA֭(///fΜIff&̟?+V8Ą233}"""$<<͛sIjJDNN|$IߧO:t@HHcɒ%SkAAA$Wfl*ׯ_'<<uuuy&=tQ/^ e4h?ttuuE}DHk={عs'111=עE ^uz聋 ۷g޽ }}}֬Yd2QWW'''(--dddP\\L=(**¢)航*YYYRYYɭ[Рm۶PYYL tuu0`Snn.s@<;kkkunnn;~8>KΝz+++W,Ȋn2e | ^^^ H6>zRAAAxy˿o-O|6L=,AQ~~~d2ƍ_|x:MGDŗ_~Ν;)--@KK2rH<<<y]<<< `Ȑ!ڵ <| ՕLLLٳ'C`` FFFcllomdd$sѣaaaqttt000ϡ&?ɓ'saյ6@6vXزe [ދYx1[lA$t邗-[T砪>999ckk+NX]>o~)AAAckk[M][AAAhR 444zI(--dggXOJJ ?~-Zx2fJKKx(@ii)Ǐ/OLݡ-[(--]vl޼N:OLƦMڵ+cN>]k"---LMM޽;FBCC 444ĉѺuk 믿TLJ뼖vѼys~m022B$ O?A gϞeӦMG&M9QQQݛ~BVjΝټy3gի V !!@^ 5**J[hAN$SN;z֭[7x   󗜜 OirIMM'%%Jlmmi&JKKqqqavհw^FMII xyyתQ7K.ڵk5jTcbb|$Ig<<<<ښy}QRRP#u̚5qܸq   /U 2ʢpqqeeey!:::ݒر>k ,^oO?˨^?f̘Z{M t;wҥKhiii&BBBj JΝ;qqq---V^dѪU+F5;vDMM$¸z*NNNӻwo͛\xι+++oٳ'wАÇ?MII/`ʕ5׹sg"""177,$I[n?SpTeQVVFPPBsT… ufΛ7D5ki݂     '((K.ZB]]xեG=VJnǎТE Ǝۤ#3ѷo_ypO>ȑ#5-,,_W6СC#Gرcc2e ϟgϞMmhnn9HDff&In0446m:OTT&M" g,--7|? ,`Сkbb¦Mشi׏dV\3_Ϟ=ٲe W^vޝsq5y bjjJzz:.\x"h[YY $=AAA warrss;;;"77;w%WVVƦM h+E&k.,X@~~>ZZZlٲ>cN8Aaa!u6+Ai&}]͛GO8*TСCL0-[ʷ_;vs璘;w|tԉ[n5AAA"OIMM}QөSGOO{{Z{w&//֭[u?~]r +V }̛7ތ㜜)--% @&L~xee%3f`,\5kPTT/_&22>|HLL| 4Hs))) fffXYYann%-ZGGG:9s&tR>'aJāYz5{sVVV͛7?fϜ={ÇӧO444HOOOtСCQUUYooo._ =^^^tؑtV\۷|2{nAAAWu 6mž_~tݺuddd`eeG}<=z͛7_U;X'NW_}BoҪU+zєKJH?~<'N@IIիWӡC~m)--Zk֬$P|&;;ZgggtB.]h۶S5?f@Э['FGG3{l:vȥKPyyy| j`rcH ?,ӬY3^u.\BRLLLHIIahjj2o<,Y"Ϙ555eر8p-[   K… ߿ijj*6l`ժU/|?L&c„ (++₳3:tˠ/p PUUe̘1,]iyv M>''QFЫW/Ӊ133coߞ-ZuV._̈#?>b``PgZ~NNCZZiii$&&Nbb"111DFFU]KK:гgOC޽k/gf͚5OK}ڵSe$**'3rǞKLL*bhaaa$IЪU+֬Yŋ߿a}:w۷QSSO>a֬Y5ѹq=z@UU7iAAAWL&ښx7u>m۶ѡCnݺ%jC6-[?񸞞δoZn-_E[aa!nի\|̚5 kkϟܾ}־8&''3tPBBBxNGGq1ezX~ɒ%^S?4$I"!!"""}6AAAPVVx'''pww###JJJ׿ƍ Sc) O+VhPϏe˖?ҦM'wڵCUUҮ];kR]Ķmۘ9s|k: L]n .DII, jwUCee%Æ {   ]z^zEFFF%""ggg0g3=JBBBxA(++cccC˖-+++XVW瓙)Of!**;w.oF]ȈQF1~x LYfѶm[žzc0d{Ą>?CC~ÇM6%%%lmm]xyy9re._Ltt4ڵ uFn_ٺuk߬]777)Xv-7nuBu&gEEcǎ?)vvvtؑvaooƆ |||9s&;vʊ$Ν;Н™3gh"$IbǎZn}2 zAAAGUe38 0w\ׯ63gO׮]9r<ݻw !<g߿? K.b7xZ~ܸq^|#3Ho߾Ȑ?ȧ~{gf޽Sү_?y]%99?/'^T%%%,_m۶Z3'kȴiׯ .M ((]*~Q FFFXZZbiitRRR߱$//555MƮ]2eV}ZlI\\...ܹsG$IxyyUAJ*** ip AAA锕ammMFFjӉ'=z4hs~'&N*7oޤSNSYYI\\QQQőDbb")))ǓFVV:t׾077G]]]>Xù>())ѦMڶm -ZLvɭ[޽;ͫ GGG,YBqqqO:źuطo _wff&~)hkk+t\LAAtjJJ B`` 9~t҅J8w={dРAhjjNPPݺuw ,`TVV兡!#Fذax{{رc|W    B8qbXv-_6ڼs璙#_|Eϯ>Zj*886mpeڴiSk]? GnV!!!Maap]HNN&11|tuuQQQurr +++{%VZEǎ/L666&++7nн{wwٳݻW'#GX~=˗/gȐ! WёH|||PLFll,!!!iuiEE[h!/JMM*M-**sҺukΝ;-,,$99YceeeܻwPBCC%88*++QQQa={ ѣ$%%ЬY3yVqwwח_UHAA@ff& 6LyIgYf oFG>c ޽;'Ony_e̘13fPUp̘1׹eHD^Dp%#2HkPRR"ԠښfA6k sssY'''"""eذa >>77+W̆ h׮3gܸq-鍝69Pf2~xbbbGYY$u?ӧPմ֭[8d888x#  B2e {֖UV1aP/[nѭ[7d2ׯ_յQ=vcƌAEEvh Oolݺ+++޽ދ^R),,$ :PVVced2b̙3VkQMMMy:5eѢE <޽{3uTlŋ yyy|r|Mpwwg̙ܹ  kqqqd߿www:t3g-,,dɏ"7$TQfffUK;;;ڴiL& &/ܯ_ZGF]]Hnݺl   BEĉquu… /xUBcYx12 OOF 0gm$׮]c۶m|wsѸZ$Iz,&RSp̙3DEE5Z#АzxzzlٲmO/++#""@Cpp0ݛ}ңG: N׶ž. 0OOO\R?ںdgg[oGחիWؾ};>>>k׎vSk##k/ׯ_?6|pQxD.]~:{ @ll,_^Аɓ'.L.]Ď;O2d-Z@SS3vX/_Α#G |l uMJݻ7qecڴi())!I;vs'rf   I0aSڵkX~=fff [PP?ȅ luL&wlvv6V,sM6!ӧ... Z|-P4ՕK.Uw"""_ʋann L< k }{Gͷ~u 2]v\XOOOyt /  gmm TmMKK7Mpww'00ݻw|rRSSYx1۶m/CUU|<~{QVVƠA;Q*ĤQ}mܸ7o-oФRRR[pTTUUܺu;ԺZII C%<<::nÆ Ӯ];FК@jh" fΜR>H7Ү]^{M?%IK. ҰaäZ $@=z$R)44T:r䈴b iܸq!!!$IRZZ,RDDBsxbc$%%% bbbt   <_ ]v1Қ5k$###iRaas\PSJddd$%%%5ʜHn9Ittt$@ڸqcN8!I$IW\|}}߼yS߿_D~~C)++Kڿ[3gHYYYґ#GHzzz  Ǐg Z:۷筷bҥ:t;wлwoN> )ݺuW۶m+ou:ZZZn5  |=Z6,Z/_̜9kkk-Zăӊڵ]vĮ]~[lƍ4k֌۷7ʜ:I:u*уٳg7xppp300*)))X[[+=GZZ˷פ%%%GMMqׯ'77{{v / ECjM~ٲeTTTp.]ʃիK. wwwyoРA\ŋk:t(gΜ?V; "wŋ '_AAz"M^tuuYl111XKKKohݺ5o&'N-<" @d[hGny~Aa>Ctttذa]t]ۺu+gϞֶAs~gTTT0i$i/_L~~>Kէ`tFFu7&   Ϗ chh(O9x ={~ѣGcnnΤI8tPww}FEii) bժU@&ـ"##9}4&Lx  tH1~xƏ͛78vYYY8p3ݻw{oߞ֭[cnny򈊊"**xzk$%%1tP233СkwǓDbb" $%%@bb"IIIdddǫ?LF2{liӦ /nб\x;;;:w 7o$''r~ݣm۶ ܹsL4IwܡUV8qssskGQQyyyJbccٵk_ )@ꊫk橨hnb/I;wdɊ+Ok(}}}v܉{SPJJJ <}q<==QRRbذa޽jՊhP?b6l'DTAA^R9994Out֭;wooo&88`v-JEE\233zbWϴuIII 0`ccӧQSS޽{$%%D\\꠨%TTT`tԩo兲2{ASSAWVVbii5d2ƎKAAA7\ߠ2 ḸвeKtttɐ!C_yk駟RVV#FPx‹#S^^^k4Epw}jj*ӧOԩSСC1c۷o?_~4s۷ﱚÇg"IB)#Gdƍ;ӓ 6Kii)OvAAANcSSScذaR_\v7nHDD:::XZZABBBUrr2'22JrvvV8ȭXYYaeeMU#;{ʀNHHQYYi۶-=zq|tt4 yiDDD`bbBQQu6KHHTk =}4ǏGYYoV5 /x1JJJHĈ#ضmvvv/zYrȑ#̜9ꫯL'Zk׮ۛDw6dTTTHHH4 RRRu]twO>7RQQ'jݻ7FFFdeeqE TkAATi?ŖKLL <XXX`nnNff&&&&#/ٸq<8 7RWWg`ee666?Ga.\Hrr2 ۷/DFFb``Pـ}]揋A$QQQ q{毿bii)`ڴiOHk͚5k׿ٳgiӦ SLaѢE/EY|;v ۳o>v TKLLŅ0:ut7oΆ xټy3'Ns WWW8s m۶EGG777Ν;BR[[[eΝuHUTT>|8GHAA%Ty6{5FFFSVVFZZ+ 3QKKKJJJHJJBUU@JKKyoÆ ٳg#IS3tP*uZu7G_ի ̙3 /  SAZWꗅujj ^͋Ӷm[oΚ5k3gt }3}t$Ibĉ 9888v튱q~MڱQQQnZ5Ӯ];233Ш$I`eeݻwkl1^<%i#T___pc͚5Ã!C0h lmmzn޼+dee)tLvv6g@U1}Y/̙3ؐS }Ttt4۷Ç3vQ}dffCtt4[FYYdNqqqu >|)DEE5   MoӦM̛7#Grzu֍\$ӢEo066&,,LWԭ[ջuIHHͭ>RXX2$!I2 555)))A$JJJnpuHHHıGIDHHXiŠ ^{5/!cD<<<|2waΜ9QXXȑ#G:u*-ZM6̙3'N(|Z b燋 DIIӧTo1~CV %%ٻ﨨ah")"(# Ƃ{-MAML3&Q)jƖ" KcCAW("ޙH FY˵;{ީw'''z-.\XΝ;ceeEII uP;sȑbݫVu]tD   qqq8;; _hܼyիWk<_KKU9f@```c1{AAAxv 3ԿIYwTݎ4( ^z%:t('Nx舮. \~aÆall[VKyy97֭[`oo_\skϟWx IDAToiR"@ZOr>}7ݻwٲe ӧOIeÆ ktԉ-Z)((x+KTcҐ<<<شi$1uT"##Ve{bb"]veϞ=>+rrr4^C믿TהC]}@r 5YAAAh8H/_Lddd>4$e`(//Cj_1k׮ݻwU4Z!BlllL82|}}={vOwi(,,>>,_+WhܬHA 6j*,,d޼y)SLAPh@ߨQ#]F6m:3gdeek<hiiqM_/H&Mɓj3o<)>kMv특9igAAAes)))d2LMMINNʕ+<e\~%Kl2,ʕ+ѯ_?E[[[[Ge׮]j*(( !!]]]JKKaRW_}7obmm/"ze͌1KKKF뉏G&Ѯ];x ~WbbbTSNښR;… ԩL6[UuHϞ=Kǎ$I&-;w&$$=<<(,,Ξ=[utttXr,,, 1{sss,,,_j+߿{  lP~%vvv$''6DFFjohܸq} ³D$fΜIQQʎLNN&""  wӣG֊ё:ZQQ prr蠯:+V`rOH5P(طo&LƆ^z}QXX%/2۶m#55(֬YÌ3pss֖SeRSS|2K.O>꒖֭[6mtԉ>ƀhaa!%%%|x{{su,,,عs'۶mm۶STTW^w&4jԈr]X_3f4<~յ!ݻ7.\P"@*  e&fmR===lmmM6JYY͛7zƧ4_櫯۬Y =WY433'N닕:uŋ8pzuz쩪=zjUбcG._5kAAQv-&tL ILLVI9}4iii( dϋcbbL&ݻdggs-x*TM{O9s搝4_RRBJJ 4Rۇ*4bbbx())Waqqqj ;;;k߿ÇMǎ"@ٳsΩw֍ٳg3a„*?Gqqqu.={6gFPpYUvŋyfB`O4k֬rsDFFbll\뜚4n777rssqww'##u;X`s㭷Pʚ;P2d/^djHݹtZG˗ RAAAxFܻw W5jD۶m)++B93F5w5\XDžo>r96mYMHHHm۶X[[sq:wҥK144WA ^x8s2dׯ_Ɔ?S؅aO$vM;v,ΝC[[I&q%Ξ=/`Ѥ$?ν{ŋՎۛ%Kp9yW000@&d>&MI6m4OwEGGlJJJ6/++cܸq?cccXgŖSN3( ?>l۶N:UeΜ9C~~>={VuC]-ZՕ .вeKJJJ ґ:L0~ 裏4:ojjJǎח+Wyߚ888`ddDyy9fffDGGkL7EiT|РAhiiquܹ3666p1֙3g2 I駟j{Z,  BW}{ʻde ϊwy4Zl pssudggsuLMM8pFkIDTT...:uJUq(&$$}VP0e<>{}$N${׏CNF?>Xj繺ٳg꘨.\\\ؿ?nnniojy-(//ҥKhтzPvww';;---233IOOWkރظq#}yݻ|2---Сw*Tn'Ϳ[cUM  䤦AG++n޼I-.j ϒÇyfd26l t۷/>>>QTTDnhڴFkbbbBAA2A]"Ije2av܉\.~O>\Hsssر#:uѣhkk3gYbE)3<]Ƌ/H֭ٿ.pvv_~dddзo_bbbv="""r 111DDDY85kFvv6-Zʕ+L&GS7oñe֬YRQQjF{}PP$Ny]CBB4ګ    ֭[@zeffC+--WJrrr 8ڣOjB̙T6Rx I $77WU##jԦzɝ;wh=TJIHH:0avލ\._eя\H'Ñ$:ڵk5:79rӸ nԩSoߞ .pMMұcGڶmKqvvҥKՎoٲ%EEEdgg#IYYY\.ťJKK bʔ)X[[3yd)--ɉ>Ts42@Bqq1P%55֙?>P@̙3U!=~(.  Oٍ7hݺGRRR' kSXXXm6}$&&bcc7|S5055 ;;;.^HZZwzؘWbdd u۷oOv3)+??#Fw^ttt`ԩ\HGjlTKKK|||>VZ={* R^{55jcǎ:;v`!ILLTפDii)C{DWWj_{DDD`ffÇ1bD_x6=RKKK?I;wꉉz쉓fffH'dffjZ[nKppv:7oNbbbUTT8̌6m uϤ$/_+ڵcܼy}}}͛s 6 mmjQ6֮qLm86tPΟ?v(e{ήu򘽨C*  Oׁ'AJKKA*<=K.%::&Mzz9ׯ_Eܺu7nTc &EEEbmmMZZZf% =zp-6mJHH={/<]eqƪ/-W^A$XjUִI&޽HhhF똛Ӯ]; ׏:u̙3U D~~>4nܸƱwaժUx{{ӲeK.\H\\r///֯_OZZL>]w@p]Mj2h d2W^Uiݺ5:tHu^yU}2ғ'OVɀAASXX:b߮]jHDYYY!Iu뒟a+ꈍeҥ|wW*))AKK gggrssʊI&i֙3g.] 'M( {=&NHAA=z… UI )%lڴ7|IxYbEִSNDDD+yyyu6'333zӧ)))ޞ0Ue] ֭DFFIFhٲes-,,֭[沲TY̟?SN!I;wfʕܹs'O2k,5zuuÇU ^GG͛jӱcG(,,ٳٶ   )22rkz}bbb8s )))7vZikSPPPcɃLfVQQY())W^kZٳ\vT,--DP`kkFka``@vv6ϯ$÷~ Y iP6@ldL&c!I_|E577u֜8qf͚Fzzztޝk׮T[?vvvjѩ򄦨@Ə 3f ((rOŋ̛7ٟw|#R~b[8---^|EN8~AAAx|W\*TהqEvv6ϩS&!!S{W^Uݼyj_SH@ <~ԩS4jԈM6;S___FIDDZ899H'N+++Ν;)--???._jP]ܹ?`Ց^z͟>}Zu~m< qc.AAAFxx8@ YfP($ ccc y*"Ij~}'''Qm޼9:::Ceܹ4iDffݣe˖\r{O5INNfذa̘1vL&cŊ,Z> j\.gĈx{{situu56P /t޽[uCɓ̛7[[[ϖ-[???8s ӧO"#""j|ҔJnnnkxyyѤI TK]]]Iu u2@z3|AAAh8.\jB9sss5kFU5KKK"55+++JJJHHHM&J5vV755ڂP_-B__8ڷoۨQ#5gcc ;w~b +V]v!Yp!OW s mӦ PyL⟾ >/_zА1VVVDDDVeccСC9|0͚5#22s5HTڪU+zOFF&&&L6}̊+dر URHjՊݻ׺6>>>@ t Zu rssR{}AAA_^^Cӹsgڷo'r",,,=z4۷oܹs we8 <쥗^tؑf̘A޽,3ҤI_788Ν;;O9}4K.m Z῟V Xx1˖-*{C&1|p,,,kkk5J/͍гgO9))˗JvXx17oD__CcT !''PtRΝ;!С#R\\Llllf1 <LFll, jIYܽ{q:::tf/  ¿GyҒ֭[?Z:::4mwwGZXX0i$ؾ};ǎwmUXA7iӆ3gΰpBttt8~8:u⣏>ӯ_?J^t/ޞ yn#9995,Xo3gN'ݑd$''caaA&MvFARGGGUՅQ)))Z oooYp!qqqr͛IOO'00qơKff&o9?3|A( N8AZZZcBCChݺ5&&&nllLv#:h d2ܺu | L}86ƍC.gC*  Owpooz7D۶m4iڵ? ědggD- BmXt)׏KY|9EEEO{r) 'GE[[Yfo/JmI&!?|7[_^O {MVVw_Ȉ/  BRH5=^?~<&L??]vk. q~JJ PYڶ TٙÇ<`…899tz7}v&((LƘ1cdZ'=|||$@Zrec׮]+d2 ^{5\JRllT\\,ҙ3gb)**233}Pڱc4tPIWWWTܤO?TJHHvnIIi&SZ|TRRѾbbbKJ} $}Rbbbs***0B8v uԩν =Zuٳ Iyyyj H$k;ɑrHIIIj-  )((%@pNYYY҃j_߿H[w&+))VZ%Y[[ 믿.?io|Per\2e'x .i„ diRFFںu$--- &O,i|ϜiӦMRTT+I7n|Q\\,۷O6mdddT%( -X@zj+VHݺu[ɓ'_f̘!ڵK… %^ǫeeeպѣG%@266JKKUU~ܷoZ޽{:C[   篿¢^I,Ow}'ҠAVZڵk%gg*q6mH_~)}W{K~aP:@(RӦMyMPHR=e˖<7nܨ N8^ARIm۶IRzzt9ѣ5>YNyy&IU!4mTj\SPH!!!/,yyyI6lP+D fΜ)yzzJϗBBB$BQ[nI*RG___;vtĉ:pttiϞ=uq Ϙ1Cٳgy/%333 _7xC9s樽   7o< L򴷢YfIo?B˥}IÆ ttt|wrr{JiiiOlҖ-[^{Mj֬Y=rG_'Luăjlanʚ5kС۶mc锗3n8~7eddp1\\\%##,FY뼘l–-[4211aذa7A]e˖7SLk?]|͛7sy|}}9r$۷s^vv6vb֭8qJ3ydFE&M\ `̙lڴ7|~ֱ#G?d…,];w2~x5kƭ[*\ؘm۶)SNddZGAAsuu%..M^zq ֯_ϬYvA-ر3gP^^^VZѹsg\]]iӦ [KKJܽ{Tv111\|*cbԨQ?^cFBe:&''3sLƎˬYߙ6m Cѽ322%>>KKKݫ ^zoUׯӧOgĈyTrssqqqslII ۶mՕ3fUgPuV(..V֦M^Zg&L8ZkAeS9sо}{‚2 }???>crys]oF||Qe;wNOOON>޽{ykkooX=t/2C !$$ uss#""q]ta\pAHAA ڵkcƌu\yyykm.]y"8*ϰeь=ݻGzz:HDvv6:::1VVVXYY=s^s ݻ72hѢEs{=֭[ǴigϞ=;2bKFԺQQAAAs!T1n8f̘ʔy&+Vȑ#ѷo_f̘AΝrrrTk|Wd׮]WnӣW o`ԨQ>}={ ի8pJܹsܿ :9n8"""HJJr]vUHAAAx2ܹJg0bcc044BSHtw"O!NNN899=BѴiS:u}T׋ٹsg^u^|E^yݻZ;@OǏ'((2Zh Tň?3lmmپ}; ֖-[L׮]p$͛73p@\\\ cGعs'Æ ֖ٳgs)bʕȸqj 6DTԩSܻwֱڡGAP[jEyy9Rs^j+  vލ$ItСΠK~~> LFtt4"117n/*eq]AHFǹsꫯjӺuk>c =q ö'O2o<5kٲe 4m???HLLdٲeüy۷/o_~a׮]L8sszpe4i#Ar 亹駟r R'!SQQA```c_|EV=aWM4u LFrr2iiij-  fG2)K.QQQ唖E`` 7n !!BQTߓ*ݻʾ}?~FѲ2իyyyT *<~`ȑڵq8;;#IGQ]2d~R U's ڂ  pN8Abb"5R%4FD˖-;v,ݺuT"##e DTY" HMM\«#+V{{{Xx*:Ь.HP( |X˗ۛ3uTN>6l##Go`ooO?nܸ\.wެZ ???\\\4nUQQΝ;yILL,XkVS=WM(!ڿ?ٵkC QuTɓ{#%:u\.ܼySuAAAƍd Ozzz4k֌^z1x`5kkFGG6}i]  rJؐ5wIըiǎ0hР*MVWt۳sGazڵk0agΜaҥtڵ'޽ӧcmm̀{. 6~{&$H^t>{='יEZ!U۷ѓ̙3dHĺuchhֺ   .##?_)~ =zx^AA}u֭[KH6P(?'99ӴiSUٙ-[_|رc9y#kgΜ9Uݻw Cii)6m˫Ν;UA1cưe000`Сl޼4+R%01`zͩSطo˖-S|CHέ[:wU6]c'5S^#""ZWAAmذ!,:x 7AHʘ8q"3|2@ǬYq+Wښf̘A޽eϞ=,]˗W9rGw;w0o<&O7NbܹfbdffqFǏg˖-cmmͬY8pYYY2}tk] Re`400;vl2LMMkԩSd?~c?7nh4&NNNxzz"I{cOmEEEuw} |4GHAAq-7n̕+Wx뭷XdޖFKii)毂 ³ zj~G\]]UJ|gDFF2p@ ˗/___~Gmmmmʆ>+//g͚5ɱc0`@---e݌=sssƏOPPnݚ>pXj1Y2G>eff˶m駟X~=}1On݈*flE߿qʧ/^$==<U*?V;F MNN   [b `۶m@ݞE]Ǐ׸ę d=wdU9s樂kH9x ;vԔuƱc=zjPya( N81cjOLL .ޞ1cưgϏ0Xd* W_5H4i?ڵk5(I{NNNO8X{V8q"\z+W8'''***8r}PPMv @hhh`mm$I   pǏGKKxU.3ݿG_Ags -))a̘1333Uvh]͚>f닎o&::Z{K@e'ʹjxxx☽   <˗бc*[?-ilݔ$ <˥KTuG5jz>R͛s>Sشi$''nqqq̛7f͚GTTZZZ׏͛7իѣ9QST]ҹsg&NHTTL66oL6mW9|p>f}vkpTI&PԩSUb׮][eVom͜AAAYbb"{U7٭>;wɱ<^?iҤ zΝ;Yf ׯW/ ʎ}8q>}вeK._\P +=8::l2ܹÑ#G>}z`A$I޽wwwNxx8{$''O֭U;H}6l&&&TO>s}.]oɓ;vXUȓ*/6a„'z/A~i||<3g7dɏQvPejjԨQ9rSSS\*$:vt!ӧGAKKAH||< ,Ʀ^e6&ܹsݛ1c!~~~ܸq`nn$I7U\k;fohhHϞ=8p2@z233պavZOҨZZAAAxTJJ{dlذAz}ܾ}###iܸ1:tK.͟v؁BM6k׮AA|ǓCΝ}]b=8~8ܸq\BP5brcâ*}SD@EA}߷rST2ʹ̷-,jx IDATl4\PQ1sA=ETT@TAYaߜ׉E@,s]s]pΙsIxx8GaĈr WܹØ1c֭Νyq]~ k˚C [fo>VVRggg)..رc:Y,e˖T*@   T_M~~]fȐ!deePv*١C5T*bbbh֬fffOBB!!!?u므X^/<H?\9wаj*@ ЦM>9xIΝK9~8ܺu[Ҳe˧:gyT*U333,KAA˗/M6߿B;ԫW4 :qD۷/M6%77{СCtV&pZeh(2AArٴi}l2- lllh֬܏$%%acc$Iy!V>Ipp0/_FWWW_}Atѣ\(i ViXX b޽Uz3ҥ rMϥKzjy cWʕYt)FGf@wHpvv?$??=zp%SgE$''pB+=Fy 'N*^fߪU+(..`,tgϞ_RAAAKxyyK&,Ć0x`̈%<<}}}5j$gp̙J߰aP]I&52_AcL>Ixˬ;ܹ( /ϏwV4ussbT*5j9z(...վ6JիWO>aرՋ)S@vظq#|Ǖ()Z,f͚СC‚:uTk^/}}}yw}Gfpvv>ծ d&%%{ܐ!Ce ̌4.^XijfddSj&@U AA89{`tQ] } Ҋ4lؐ={2~x:vHAAqqqԯ_tZl.6da׮]7 'TS뛟#j!Cp)7oNhh(jժ9Ȕ<.==K.q%ܹCBB*J^&ndd$gc֪U Iػw/L8###ԩ,Y/**SSSLLL.jTRRRHNN&99 122EnZ.]nݧzN8!C#663uTbccQ(̘1e˖갰 ##ӧ[oŋ jeii1bF҇:p,XP1`4hЀ9wȑ:t>$ ?>[2m4z+}  o5g֬Yq??4k/[xx8JIܹsǮ_YfakkKLLAAx#K.O?А@ڷo(JLLLHHHPEQ*deeYHZC222000@ibb!4lkkkիUT111888Cff&_}| 4jԈ͛7YǑ\y~'y{jj*G???}t 777FSXf sΎ2?beeJ"44:n:|Mڶm[ǵnݚp5k۷]xnݺaiiUTAA222puu%..'''._,OLMKrrr ]:uDhh(-?# T O>rJ~J=dIsKKKԩ!!!;u=u*,^c9<b߾}rc' F*%7nLVV;v(9}4K.Kܿ&M I=>s/^ZS$U,  v*Çs)ׯϥKJ팍BAAAAϋP:uꄮ.111 /T \^}U6lsέskHTrelƌukظq#[/_.i&FFF 4~xnܸeٳ' wrJLÆ GEdff;'|R,P;v`9}4ݺu|$ILJիWLѣG4uF5uX+>RRRחGCȈ^z1b<<<ذa^ԸCe…&V>|8aaa>|7|󳲲nݺ<|͛7ˍ4ZlIBB7oޤO>nAAA7PL6Ϟ={իW&''|qơRhР۷oRp4""өS'/ϧ,Sh׮;vR ^zxzzInn.Ǐ___RRR8y$'Oёd֬Y=zX[I&qiƏH+N:E^^Vͣ9-[rin޼YAAIx뭷عs':::lݺ77rd>_~|\\\0`=AtO$IL>cVk}}}T*6###KkhhXf଼ĄL}tUthժ$i+2rH+3+VZ͓ wŮ]hܸ1#GdԨQCCJԔ1c0fj5.]СC:tׯ-;n8<<q.sLJ\_N6m}%Ν  ?]qq1fbӦM( 6lI*|KG[(J2 G4iIll,߿ӧO&&& 8777ttt1c 6)Sc̙#zCCC x7lf$lll(..4k֬%  JI&q VbΜ9O|Ѫ K,a֬Ye&]oԩS">>dAH݋; 'O>B㵶T*J[rssedd΍7&..N^jGF044[nUST(((5e޽JƬYˬ3ZM'Gׯcgg>>>hG>}iӦM+}?s7n$!!A[ Jŋҥ xxxФIݻWtԉPlmmקO}-  `ԨQ;w===֭[kV|8@IنzQTTč7pqqy~- .DPU`ժU;tA+  61k׮abbÆ {A`` ={ڵk;CյRܞFZZaaaqU^JXXܽ{ߗA  ={6k׮ޞ7nTvfM*(( $$ Pppp={oG'?gѢEzٻwoh2&NСC9Zj޽9x Zf5jtp,L0˗/cnnNFFO|r.\ırssU$rJ~myoݺEͫ=gAAAx?wwwСCٳJcN>M^ؿ? gϞJ{кukiժUzBBB<֭[ܺu(z6c 6lPA^/lŋZ/ÇKGpp0۷o'$$GT*U+ ڨQKQQzz볥 mڴRYΟ?ϒ%Kؿ^s%]' ZYfİvZf͚%?/%%E^ &&& 0777F]n2 PkooӧgϞX7&!!]o߾;w>AAժUx(,,ɉWyI|QQFFFe&ܿ~PfY2===,,,077\YV ,/,,$99RWWWڷo+8;;?UAfb:w̕+W3f {&)(( 00?INN.uݺuݺu̬STTDXX'Nrj̟?7nЦMtuuINNLUdڴilݺCCCϼ۴oߞ˗/Wjn  "S*̞=[۫X7>>&MP(-7q#77 .p '22TbKeӨQ#iٲ%-ZElْWkLABH'}]ֶ̈Fƕ$7np N<ɹsJ5\R(4oޜ{Rغu+,ONNt#<<`quue 0Z*4 (,,$22wnY|8 ʿZ*>|-ӫW ֭[ZqsscĈٳ’DFFȝ;w@7'N|5^zp&V/.  /+W0yd"##Q(|g|gήpzںQ#222$==Ltuu133CPPNtuu_> 4QF ¿  MHHUVdeeb ؼy3nnnO5tܙK.lA Θ1M6Ѷm[BCCΝ;ٻw/v}}}vʠAKKKRRR}{_>IIIZ֬YÜ9spuuիպ6AAAx^ݻwW_}U?x`lR#=>3,YRf"  <*@A۶m)..ĉ 4JW*x{{/"o722 ܞtG(JR&}Zݻooo7ސKzȠW5NN ,ˬ,8pGTO>}9r$~)ۗ?(y}֭K^^t[~d}$''Ӹqc^AAGQlܸlXh ,E'Nd,Z?FAާ~Jqq1C Rp4&&{ƍ.nnnxyy\M>]/h_>}(U^@AA^DtܙYfM >n^wAHoܸQ(|וzݻwyqrr'== 駟2eJizzz5`|ݻ~aff?}T4Y### ӧK7yd,--׷F端ϲeh׮j'_бcGS^^VÇe|||Jwww/w   (233?>]v˘sE\\\j|oDTAy/LoA$^~e:tPѼk899믿RXXHٵkcɒ%V&&45N%IҪYIII;@;ƬY京42ϝ;Gqqq&&&xzz30k_~%cԩ9AAA#F@PpU*uMP5< BHصk~aeffVZyfիǏ' W^yAdR_VgUݻ7|4i҄QF'kkkK֭Kmȼy~Xf/  |}}iӦ s!55Mw^:TymMArR  Ԕ"@yf СXz$Ilٲ'''~ ݻ7'Oߟ|4222>7o$44T~DEEq]亘5D*RIX|9vɓѣJ,aW ʪCڬY3$I[J穬>_T^~e=ulU7oޜZj9^AAx0`ܸy&,[Hƍuڶm% ڇ&I+3f(ի\x(\bǏfslk׮۷w)))( tuu133Ԕڵkcll\*XNvv6!I  5jD4iRPPP@ffѾ"iiixzzһwov!;B=^ۿΜ9Ù3gI{뭷8y$7ofɒ%5kذaիYh/rMYlNgò IDAT"??Rsn8q;sru  ŋ|ro===^{5/^Ly\v RAx/_ݻ!o/**bٲe,^BXp!|V䳠dw}Ç$6mХK^yjCDJJ $$$p}T*ɉ˗9F #))VZUxW2{lkW9oݺ͛7[COaa!Z9r$vvvܻw]vWQ(̟?iӦf>,--ܹ3AAA=zT\v+++=z!gV_ĉ7n,WԩSٳ>FMŋٽ{74o\AI.]_|#G:&L?[nk2H5AFIϹŋK4x`y[DDԹsg ݽ{/ʕ+%@6lXݻ'e)ˣGrwE;vT8}J՚Gff4yd ^yI$IRI %KHԮ]jIT*dee%$I_oN$CsVjbIWWW>kJԺu맿A^wޕ/^,999i]F߿4sLo/EFFJ*A_1bP(#FH/_.,)>>^*((xfs***LLL$y xkjS:+WҡC155e͚5;v {{lN254i_6XZZVݱA@I7Oxyyqlll<:v(/ҥ ӧO9ƬY044իZk\_ғ'O#[::::4k ?n8q㆜U+?Ozz:֭w8::g>nnn,\cҦM|9s ׯg=VZabb#/seʕMQQ| ‹;vЮ];z/:::;Wr!ڷo_sq  Tsƍ8991qDv ,y޲eKAgaǎ\_yݻw&::ÇuhYP\\Q(ٳ%Ibʕ,\ի͛x".]BVߟǏs>RcY[[3fvŚ5k֭[R&Mbݺu߿~JzzzYgGfܾ}"yluҿN8={XhQ_ ;yyy>ggg<==>}:+tÉݻ#""R)~qckkpvv;;;tuuu /Gi&V^M\\L:{RK ؘ'N`kkKݺukt/_JV  Ts ȐϘ1DtuuYhqwO#;;9s%osuu-؜:DӦMqpp充JRRIIIr jj׮l[[R4o[nݝ!C[oUy~)))L6M?p@hذ!.]J:.\([oŮ]سg}]ѻwolmmOÇ%]v… >|Rw}˗VxWs v-ʶm߿lmm^Ej"""077GP``` kaaA^իWJNÉPghM5AT{{j5A^\QQQYM6 hkժY`AVT*ȠaÆdE!o׮]k|lAA<@RfΝ 4/GHHܾ}B32?S(sbbbHNNFWWZXXXrrss)..&++4K$IаaCq 03f`ccS頱n͛7KKǎ2qI<==ILLDOOO>>L Ҏ;Rvmt})5n޽i۶-׮]c|U[Ettt6l̙֭3)..3.H'ݠA,--IKKcƍcǎeΜ9ܼy7n4۴ioߖשS777<==8p7PVKnn.bffF J5kԨqkM5X }'Oz!ZA ?C~~>cÆ -Z0gMFڵ<)yyyFff&ڵdMd  Դ:@xFKٳgOjaVWyKŽԴ癚}&?JOP`llr Fj(J%U _dj5M6eΝt]8͇Ǘѻwo>ӧ ̞=ٳgf=zɺu8z(XXXZf߾}%ҏ?ϓA:u8v^8x|qVVVr Dzz:>>>l۶Ə!wwwo055%55SSSj5ajj 999dffr1֭-ԪU4 ~cbbPTDDDY ¢Tٙ6m`nn^ 𬅇ŦM::: 03g2vاlݧO֭Kƍqrrhaa!W\D (C ď?Xk Ⳗ« b۶m4lؐ#Gr!~}ݿlN5IVcffR$,,mVkX<<< J__~_<<<رc}ŊӇgϖylllСC1Z-OLL ( $If۶mdff/%6mʽ{ؽ{7&Lx~~~rR6noS <9t^^^QXXٳ'SLa„ XXXTi܌ QTЪU+BBBPTXZZbjjJVV$V)**"//sssZhArr2*?۷ǭ[+|niѢ͛7y8::ʵAWzz:;wdӦMrpJvO:3g%VVVAAAMjF+Ohh(:uؘr x3H/^$%%T<''33|n5EGG͛s5n޼Y3g$##Zjw1s :AAA]gffԩSYfMHiРIIIL>cǎ `С]ÇW*@K/Oaa!VbZǍǜ9sڵkV Pj5xyyw^233}NNNL<)S`oo_suCs呒Bbb"&&&ܼyMbllLF000 99L$##MbnnNݺu>ennNNԩS}>,4w}qAq"@JIP.]&I7ѣG,X;;;hҤ  ⱒ$?G l"֬C2{M"g̙3lܸE:>s877RcccѣG9|psO?E$nʬY;ٳgs-puub۶mرc@w;SL (ra5kZ333~w$IN:XYYaiiΝ;ڵ+͚5{ahh(gwAޮR044͛T*tuu133UV#IzzeGQQxxx`mm-g'5lY^ ¿VNNgΝ8qBP(޽;L4MN>5ZOC3ޭ[W^=;w3]! "@ ԪUKn#Iek``@qq1PZӭڷoOfͰrS)M0m4QѠAAU:_y 88,j׮]cƌq|8G׷Rurͩ_>xyy ֩SAq|||DTky{{4$S}„ xzzҨQ#011`ȑ瓟OFF;ѽ{w>| 6ߟ$ &JkצN:ԪU_}nm$Y]J%ܹGʁUĉS:͢Asm.gMu -<<nJllʊq1uT9;%??`4iBXXyyyRPP-XZZo JR.q}HOOG$j׮>FFFj߿AAAXXXдiS$IiӦuP(ԯ__kA]vi4i҄ѣG3mڴ2' >  66˗ãG666L8ɓ'Ӯ]j]V-֭F5VF]CUk^eA vZ޽Kݺu=ztcT*< ::7npMnݺ%Osrryyy@U( @=hӦ ]tAPp}bbb~:iii撗Gvv6?]RJnݚK.qU/EEE|,YZ;vgϞ~+ZbP(۷/{̙3.ݟ5k_}.]"88Ν;W{N###K/1~x<<!!!F=Yf͕+W DPy SSN_.XXXpM&&&qY5jѣG\)ݻ7W^̌B233ȠVZӽ{wܹB 55(iѢ=ܜ4lК۶mӪlii:u*={|oVu Pk&`ܸqL<>} $$$*T(_mmm&Eݺu̥Uվ}{.]DXX޽{xxx#Ǎdž zYKEPRT HÆ 5j{_~yfy'YYY]oOOOٳgz Õ vCCCT*?3[l_NÇRA&m۶Y7SS%mڴur}}}rrrHMMޞDׯO֭144$55RItt4EEES^=J%ȵU*T*x"={v$&&T*Q$%%B 88l,--155@ll,w_֭[vcccFQnи""@*Ps8{,۷og5zƆ۷o5gϞ%99j7uuu177G__"_gĉܿ###c BERyѪ!MB ::{{_$}6aaa\~w@rr2YYYrI4T 6l ҵkW7o^˿ o}ٳx{{o)VbUVRIBB~w}777I aÆevܲe ӧOUVDDDT%;wԪ쌻;ӦM%XZZbddăAT#FFFQPP۷<kkk/(JRSSɉHejy^^=xݻwcǎR_~e<<<9r$z=RRṚl^),[>N:\'&&C Uǎ̌N:U;ghРAAA7ڽrrr5jOF__m۶1qj% %2HӮ];zA@@[neѢE5:BQF4jԨe"J{˗޽{RJJRDUА:u`mm--[ŅN:Ѷm[ \ի̛7+W%5AwE֭t/^d\~kHA$7`gȑ垧nݚ7ne{*Sc˖-jv튳5xmiӦoVYv-1={ʊGq9 yxxDbb"Gdd$nݢEպ.A,ߺu+IIIF1~x^}4\\\J ''dܹ!HDzhٲ%JJZԯ_|xcMqD yJSTJ Bocz]~A~G]vZ%wwwRaaV)""BJIIѿ:Ӓ$̔bbb$ISNIvΟ?/]|YϗbccR``HAAAŋ7oJO1 ! ,5-[/uf͚]@AAcǎm!çD\\={LGd򑗗LMM᯿=fϞ 9=&>FFFpB}H`ڵ ܼyso>oƋ/ ''O?vvvcccG` Μ9χ;]9䠫Lom1f… OE^͆Ȟ122,Y" ڥ!;;HKK2ej0Y1ի7^EE#w/Dbb"ꐐ2"444wޭԄ`K@^OO+WĊ+0l0) ݻwGee2`0D={999p}.DJJ ,Y555#11ųB[[0`߭011D_'v'`0DH_CEEK.+[}ddd0ftu QQQ(((@=2FDD믿ٳqƍv|:*cASSͱXr%O?~\)_iɒ%PQQ¢m۶̙3Ghh(N 7oObȐ!;g:u ?nhѢEرcݻB2"MMM sp~Kk׶o߾ܿԄ,Ƞ Ϟ=TUUQPPUUU@QQcuuu(**lx".]$^@KK ~)VX'vx 3 CxXjU""">}7oDee%R̄tuu1b(((tMEE83Fhh,\999͛7K=`0^H[apuuXoɓ1~x3Fl1b@oї/_Ȁ{C||<.^( @sS;w`ҥߺu+T*++Dpp0233%K?zhˣ' q}\vM5N:NlU1tP}gϞ(,,DYY աLj#P^^|tIvĉ˃,Y"'֭[j#00m`0 m3f@II eee0LLL`bbZܿDzz:гgO:::޽;wHJJ[n\駴44~Um o!PUUŖ-[pmH>} KKKlڴ+G~i}G g̘gggt >|8LPLoM"jާCZZZ`{v`09\]] իlr ?(??T__teݻwݝ'O޽{DDCϟ?os ƍ#$%%Evvv]`0 qѵZXFLL JJJbnnnعsX~@Б#GۻH"¹sh"]gϞm)*li>}>meXJng?x'''Mܹs^<'oχ/rJs8vX뒒͛7` 4@sUVAOO@sK`mmI&AUU'O_ dffMBSSaii mmm uuuw}<|~-W.JJJ"x"Bpp0,Yয়~BAA455sNx>>>ݻ%0 mۙ5k ,++# fu… dffFrrrxb6m_ñc7o^e4rA@˖-p }}}&"Zx1 69OhBq=RJ#.-kkkRB$"M]ȈӧHBVVV4l0CZZ FVVVBմMjj*|'sssDRx<ӷ~KӧOm۶Q@@P Ɔ8_eeeܜ'`/oC5lذv%&&3QNN=zHׯӣG(77^JQZZEDD˗/;ISNG,Y"T.`0iXi;97@UU;wD@@BCCfhn`ee޽{cժUĆ ~m?o&&&1bHKK 1_:99;f&De˖AIIkoaa!&MȿZί3f@sfO뚚裏Bd0W0|pYHHH@ii@'Obڵ1babb}eee9991ea׮]x0k,X`P%#|022'̞=}С8tлwo"33mV 0 Cty!))^z$@(UUUMKJJё:'QQQ9881mٲÅΞ;wrٳgwv >cڲe ͙3y~:ݸq***(../ߩABCCի@tY? ***h˖-a(..Ns0 !nXS4NѣGׯSIII*))iU]K;w.=~Xh{ dcc#Ğh֭['|\ig>h x}QZr{JJJ)))\P!;;[({#Ko FYYݸqGs!uuSԽ{w?>? ^u@?SFFDvZZp!PUUP666 H_} oΟ?O| g@FFF펋"'''"LTPP@DwJ >K }`0,@nG"{!͘1 ={V^z ݻ|}}ESXX>hڵBIo߾BYZZJE>Ё455 ]rEh;w\g9sF({W^>/^e '.&(11ʊF=zHIIȑ#iÆ tYJLLlW766͛7iݺu)0`}wFSSHJJJ4`lQn`06iΝܳ ?sΜ9ͽj_Ύxԯ_?+++)""KT;w#gggڻw/ KKKlq婭?o=fdd˗ ihh%V‡/2j(@wߊTĵꫯZh"@{O] Fyy9T󫇚͙3nJ+V={ \Ң[ҽ{ވ0TSSy{{)[oIHH7r/[l$O?_z^DhңGڹs'U|o4JNNCHJHHZܾ}N8A=iΜ%愍\J[#  ss,`0 qPZZ}ѷ/.++ѣ~7ҪUhÆ sN:x 8q\B T__O 4d6lX?BܹC$ ڴiSK‚mwss#@AD-!Z~ж-D CE뇲2YXX`˗/ёf̘A"Ug͚e{{{alݻwsXi$x'?~m``@T]]➄'26l t_\]]x<ŋEzQVSSCǏ-*%'',,,X(`0 hHHHP^^ W^Çѻwo= %%%ˣ(++Cii)p $''[ʕDر ̙3{(,,龦&|w8t`ʔ)|2Цzrk!66!!!X~P=&Mqip z=8;;hV^hv؁Xw׬2 tR$$$ 55uuu->###@tt4***, ƻ̵kЯ_?9{РA4hGTUU!** ػw/?ވ8}4fܼySh/_̙38vRRR011m0|HJJOBB?/ޘ1c|||8dx(--ɓ'qQdggdddrJX[[cԨQm;l0ܻw˖-CDD/_0瑩{˜8iĈע\˗&9]nJߟbcc[} .*IL yYYYX۹Ma&ԫW/!"ruuʦ.]ĕVWW mԸ:{e˖[2:]''Vn޼) ƻB[oߦm۶[ܨ-yL^^*(x< ڵk)**J={F۷o'%%%O999YA8((tttÇ"`0o544hݔ#:ںu+gW^DMMM]k_CCO.]"???G IDATѥK(11Q9Ņny=zq9r$ 2=2 㽠ewko"%%gΜgggcԩ  q58q?6l؀ӧCGG#0m4|ؾ};N>'OIEEPYY)87ӧ"ө@{a>}:O }߲eˠB.uժUǹs֭իWQVVewcܹ7n?>W(=z􀦦&fΔ)SB`L>uۇ*lݺsCDEEELryyymhh+'O `Ȑ!ppp@ee% ;;;dggܹs1bH>͜9aaaׯt.`0(غu+`ooJ?@FF~'3eddpQ\|ͅ%ƍ˗/򫛖/_quu5 %%%B?󑔔D}}=PZZ L6jtX3f@\\TTT`oox`0Jv̳gIKKqI&UWWӓ'OO>iUJgҺuhĉb xbM@spRQQ!Իwo~cׯ_OhϞ="eرN8!}vԙPTT%&& 5 ܹs 6yd  .M\PP5'|BHAAG%4}t}f0DAZ[[KqF222"l"~:th@]ç\\\hԩdgg'Rϗ/_ґ#GH___@~̙tUd85gcnݺz2 Frr2_dddp̘1)}Y,~ ===:rHZQ\\Lݻؒp999׉˗)**JjRDDQFFyzzRtt4'dkkKrrrܺMMMEzf0 ]H!((P߾}\mF{&---ϟ?_ ;vL@Ш5x<qI&uXY;v\OD/!4sLq-^^^\M' G@,??oqQݹ ;ijjr{$޽{Bh; K˗/ [}P\\LdaaAdccCB/q}}}'??lmmȈ\\\NhjjׯӜ9sO?3taˣ#G4Sg0oڼy3u֍ۏL骫WyEFF ÇiРA{ci˖-VU 4rpp ""*,,Dz]CÆ Էo_p <3 avؼy3͛7#EGGرc[ '#tמ={=0IKKv,YBɟ !Xqxx8gnn.ݼyHAQF_Ғ嗾Õ 5?Vf?{K3.P[[Kϟ)SД$###?(--Mh{%%%t 222"[[6H666Rhh(‚<<< ݹsK"rrpp |.FFFtʕ7h޼yY 1 ƛv%P>ydԶ(--HruuT{/ }rС/|r/iE7nܠT""x"=z荼 3fp+**XD` u1cBsGII Kx<"P?TTTGŋx"=z{ܡ)))/(--t]༿?V\2hjjҥK066ngDfcuB#NNNSSS;w,]Uѭx!?~ iii\0k,899!00ضmbbbmmmAvv6dɒZ|9 (++ \:u*|||PDVVVªUCCC!$$!!!غu+FSSS,XǏdZشi6mڄ0l޼رc"%%)SpBhO(++M@OAA@`Fg߄spvvFyy9@^^V¶m0jԨN<o߆+\ Æ ի1~x̚5 RRR- MPSS#G3QpvvSS AD8q"ܡ휜TWWsLuuu!CWyك+WЭ[7XYY~a0  AEEWAu4ݻ7z hhh{߿?444 %%EEE.H(''yyyMMMFUUXhjj4tuuѯ_?ưa8ER\\ R"?={a̘1BE ###ܿ!!!ؾ}{kkkq%%XfgϞ)Κ5 HJJ4 ϟ͛HHH`ܹ8}4 nܸ;wɩ:M Ccc@Іx1b>DDD@FF;x1~'t055TUU[ȑ#(**= bСC1tPܹ555-κ 툰0888ʕ+:r^`ee[vyox YYYymmmXW ..@s@`0$D/b׮]|A&OO> "o߾HLLċ/`ll,tpUdeed,Ypuuř3g777\xYzP6srr0x` 2ɐ`0o/yߍ7'___лf(55ܹCδ{nZx11YYYTRxbSX@˗/j999-J? :uJRSS͍ד KRgϞ=m߾Њ+VQݻ75Pb0m̝;ۇlllHJJ M4[dhhHvvvԮ킂*//_)))Zԑ+**ё.&rqqYXrrrȑ#\挊X[Sxꫯmݺ;WYYI[n~8ٓ(>>P@(lnnN...B*NJܾzEBeBcc#Eۑ#G?LYYYB 0@~2mjjj$yyy.O?儉JNNеk(>>_;99Qll*J{q/;e233;%̽rrr xaM [իH"Dojhn8FEE4 %#>>Quuu---ȑ#允;pp0lll~̙31~zZܹ|7msvvFSSƍ5OziӦAVVxM7n&L͟?7oބlmm;򨩩1uׁ rQQwNQQGaiiϟ 8qlll`ccܸq׮]? OOOHKKc„ X`-Z) }}}DGG#99ƍZxxxÈ`XbXE q{sss_=^Q`0]% ۶mǏ=&OeUUUx)222aÆAVV>>>ѧO.* HJJԩS[?cСEEE Ƿ~0󹹹8z(222`|ᇝ`0 mGtuu/TUUf HJJxHMM jkk4|b}cc#***k|a&YȨVVV"@ss2.5.] ggv`kk/3Չ,1c ݦH@8?hLÀZ}@WPPIpmbΜ9\t߾}8SSS|嗈F^^z í[Z\;v,f &]BMM ^144DLL vGGGxzzݻ8u͛---Yk֬AMM ޽ ___\rYYY{.޽]vAOOX`f̘!٠AoT8::ԩS())м믿ĉEd&;;.\91%PRRŋzj̜9RRR~bQ ,X[[;;;|gz@BB.bbbPTT &=8s`ܸq-QQQ;)))سg<<<@DUУGܼy=BLL bbb`kk~a033Ҏ`0w#++E/wPRR"uuuRWW'---? &OLƍ#$##jWIHHiӦP&瓯յY·/$''G%%%@ݺutuĉ~6߿ٳӧ\T~~!C:{Ps;::rSAA5Gjjj~ Nh }r3XG) n ~>{I666 $E#Kh͚5b`0hll_k!))Iy:M\|zz:yyyэ7"*BW^|ʢwRII 7f…/^,ֹ[#&&V^MݺuSrr4rppYfqc*-_.^(4 ` Cs WKSRRyyyL<{-//999$TUU Jiii(++sה%555ξ1(!!eXL)))tj 1b$%%|{OD8uq eٳgajj*o2224i"P%%%Xr%`ذaPWWǹs4 sCBBWnq^^^r (( q~YO>k׮_|GGG|5  88QQQ->}UQY;vԩSDXX`ÇwX/%%'Zou*`.]/")2z[nܹsBee%wmذa\l^<z!v '..ׯGdd$`̘18~8&M$Dqq1&M &LXЀ2ոsƎx|}}!!!{u~>Doí~öm۰m6޸q^| www[n>}:̰pB`0 Ν;۽~)XYYa .;***PYYz{ZjsQUUU(**BIII  Z' `ee>BojjX###.@k.|vv6LLL}@zgggO===|ZZNH,sĞH (8x c֭B6iF466"::EEEԄ1={x7ÇJK~~>^ ###XZZ"22III:t(̍ۿ?b󙜜/… x)$ SSS|ט>}Pv԰b Xs닌 !((۶mÇ~} `0 ƻ5dٳg~z̘1!!!2ܾ}" PSSCnݠYYY(((@IIݠ#?J }iee%PYY2TVVhhh[n8~x Y7n?Ç,KWcPPP@II dee#!##@\x{{cҥ@NNP=OݻwGii)allǏ# aaa\q=`iiG͛شiϟ @SS\@ZZZ(++CppP_[l!--Z ˗ann#Fd1®]l2\t @s?Okkk8qȗ/_bΝ8y$ 3g`̙BMD/]UF/**ѣt{M6aƍ"o xw^^^ܹs` -- Ĕ)S0x` >ƍC޽ ƻExx8֯_͙'OĀb?77MMMʂ<aٲeo_{CC"##؈CdaL IDAT6 QF!<<ǏYYYr Νڵkamm-*>Ν;~ `#w'''@ӦM8LAwޥ 9zTTTp򈈨>t/L.E@BCCݝ幾#""333@[lھo\MF.\Z*((ARPP%)`O>!_ )vT@RRRTYY)w~˹s$//OKz͛7wz"GGG255׏ &Ѕ SsJYY9sON4oN8g@s}wRRqrB^f }҈#>%KÇh_~f͚E'`T2dо<{̙3Bc0%vMh:Ľ bZjNOOn߾%LJ-[F4~x;v,9r\G|֖.\H&&&}vIOJJ 'Hw殮K^ۙ?ǏiԨQ~`ff&VXruuhrww4#z9EFFwcp{ѯR[[K҆ 裏>j!FETq-iӦׯ9 xi'p'mW|2MlllM: >lllH[[[`_ե۷ÇM}v255 Rmmm}=<#FVTTD7nܠ{҄ 4h߿SYD*jTUUU,֭[ETPP@׮] Ƌ,|2]rJJJ())}t766RZZݽ{\B;mٲfϞM hw---Zr%9995FǏO~~~ot^`06,@ .^=,t"zRTTwDGGSFFX~ԊJ;̼۷Ӛ5kZ-)zGr%FW[oi39}B%ٲA5[,C-c3cb1R%IiJҪ}?y;ΩN)d~}}o<ٳgswEBuFԩSDD4rHP#tB@jǏs;zzzF ѣ ɤŋq&&&˗gpœhn}h"ԩS6Xrss‚LՋ|||QcyҀ^ܜϧŋĉ‚dܪNDD@k׮S7oɉfϞ-ݺukZv-EEEKG­ФI())ѳg(44?N~~~t}zg~ll,۷/p.%%%7nYZZrszS NRR3F, 32 bD sDI9ꢠ CTTPVVyyy]vhժX&H>Lddd%ҥ   2z7JJJ:_[7nĢEd*aȑ#ѪU+رcח}v~C޽xb,^vo>|Brr2`DEE!$$rrrXlvttKd w^ %%%ƍaaahii!%%o{>fBPP<<z--FmÇc„  ARRƌ>}ի2Ϳ&:v---Gѯ_?Sbڔz {쁽=|>o߾8pߛ `|p>9r%@ÇZ^PP@W\u֑>͘1vI...$Bƍw^277'CCC?~L0_EDDzzz4TuaE}DDm6@SLY;bedbbϵ"PȕR۶m 2֬Y#.@AALzay^rެwޕy ǎ@ %<~xgddU>}޽{襁ܜ>dkk[GdaaAFFFpB#??z'+**"777ZnM2,,,\">YeYYYI7nܠI&ik{1mI555%Uͅtr RII ݸqݻ'Sϴ4hΝtaruuJNNnd~AbbD%eee@K.1 apU.@,X~'OҚ fϞMƍ# p[ވ#_t)ܹ5Ţz1矜#999t 3f xo<.{Ď۟>}ZfWT'99 Fe˖ V~=RSS#*QTVVܹ;"UHTee%ի2UHp$)ӦMo`4wbbbPҲ RTT[Jҽ{ԩSufzwrr^[hAVVV֤iԮ];1cA޽iΝFߢ"#[[[;w.rp$::/211!ڿ;e{.ۋkZ4|p :~`0cccbr.:{ Օ"""(++)((AꏍDHAڴiS8`0;@jLT1xBz4uT:u*-S{Ɔx BθoǏ۷E]QѱcNJ>|'XYYI^DD$tA^@@8}f7nLk֬2W7Xj5kȤ׋/8YgeeѼy9s],m޼BCC?R),,$OOO:~8Srr2"#GS_2v۷)--MLN||Z///~4imܸc)))њ}ѥKhΜ9b^8<ѣݰˈ(:e$??&lJHH @o&*oU'''t=rqq8YXXH_gB~n۷o`4ܼ(m}RVVFNNN4sLnU}ϧL3fp^ڵHhEDܑO@U'YU]]].)))G  MĶmHAAbbbGjj*mٲLMM˗Rz 7վ}{ڰaC^ŋ/Ӷmd.TTTiѢEԪU+13)%%\\\(11)!+44\\\n߾MwޭEߟ˞,e0www@***2{|lll$b8ӤIh$FDDеk(11rrrۛ\,ѪU ƍ#//zɩLڊ`0O'OF=bbbPVVR_%%%(//Gqq1|>PQQBBUUEEE(++e˖PRR>F߷~#kC]]GŒ%KP\\ 555TVV"11=z>11SLA||=z[bϞ=(,,իqe)SpihiiI^v-N8իWĉ8rrrHMMEǎĞ={0eܾ}[1@#Gyyy_ׯ ++ /_ĸqOll,6oތQFa۶mPPP+'"۷۶m̛7+V3~k#??nnntZnkbʔ)xMDDD̙3ppp@zz:w_KK K.ŪUп|$&&";;ݺuCjj*F 999Myy9"DGG'OFV###BSS4h"##l iO?_~W ?nnnHHH+OIHwޕL tM QϟsaNٙ`T@HII,ٳg{n5jZ?Uٳ'ߟ\Bs466V\)U__nݺ yOj S_~!dɒZݽ{cVVVI:k=Ne|ٳ'w…DD%А8æ82p@@㴳b~הIG1]v0""vw^KKxQ [nȕ=xf0͛͛7s'j[++)))@+VnݺILy<ߟ-[F&/_Ҋ+$ "L4k, ZK`0 Ƨ36`8::ru [nMvvv)--`V$ruu<2uuuPO>lִp]p!`KKKr te(l5jwSUU\"ŋ9Ԇz} U1K͛Gh˖-2?޺0dff***dmm4w\ڲeg>4//?NFFFdeeE CRSSNlK4_;UTTpKJJΝ;Jw!@ &/##)##RSSɓ,V|||˔J>>> ͥaÆPs:::Z\١CFx" CRXXyݘ:v-\[})**ѣo!GGGD"&&/_.f?~| deeE&&&Tg}ѩO6?`0@?~L@U2OQraÆQ 81bxBjXkΝ;ڴiýx=z;?qDڞ2x( &pa F<3x`ׯ_===iĉtܹo4$$,,,А?ޤJKKՓɑ~ ޼yC^^^TXXHG={(//|}}zǩɩFt~~>5;.Q'44`0͟@nn0`YXXӧ)&&krww'kkk211mJ5*((P޽Ąٙ%6x 1M4c jo߾<==/`0 y0ꍜd!>>xB!աeee@MM PQQᮦ@YYP\\ |͛g(**JKNNƲepݻw&Mlllk.yp9jՊkёχ3`ǎ3g,Y-[4>www~ 0{l9sbܐݻcʕ2Q\\ KKK>};yX!::۷orq=ܸq۶mN Ƨɓ%%%TVV"==&&&;v,Ξ=ӧcʔ)ؽ{7,YGM6G~~>.^''' 05eJee%>|3g… (((ʴaff+WGR|` D`o{7`Ĉbu "44K.KgFpp0)W-K`|a׮]ؾ};_BJJ `Ȑ!000غҥ tٳgB!"##G!((ׯ_ڷnAѫW/ ={DΝMϘ޽NNN &'N">|w7|zܹ3bccV h0ihѢpYxJHH~GNNNti۶TdTUQQTjMmDRRRdddԩ0m4zy .ѣGrOOOĉn:KKK߿34HLLKjW^Evv6ڷocccdgg͛sss?Ν;H˸*c]0l0ƌ@xyyĈ#7obƍuʞ5kC]v2`|q@~l2:t666Ŝ9sp! 2QĚ5k sssܺuKRc .̙3wfff033A2 #FիWӓb 0nnnPRR„ FFFͅf̘!1o<ܿJJJ~:tuu'32+++֭[333UӧO#G000 hC !Czj@EEbbbp߸8"44ZnLNvΝ;CSSN/{n?{8q"lll$^Y(..Ɖ'c֭xөS'@zzz2 ao &\1gիWuz(++s+ ??EEEܕ+J֭༒aoo_"5!!˖-1l0@\\BCC x<ܹ섄@Ϟ=/_EEE899ѣecAA񐖖M6LLL_bƌPPP@\\`С2( 55rrrغu+"""T#""ļf͚xxxd AϞ=ooo,ZHfO,`iiWtۇ())i{ntҥdV'77...ptt?wUV={6͹9K|||0|pӧO… /9?4w,32 36b̌3ڎk׮ݻԫcǎ2_`FjMm8(--5<b8y$ ?w*aoo/q۷G׮]qeb刌DII ݻNNNuzY֗6֭[8s \]]QQQ0vX,_K,3nBhh(&OmԼ}4TRDCCqqq())XPE:t(߿sc!"[hѢΞ=3g9vHO MMM`ƍ8p;-ZpJ @ZZBBB;v@NN#G.Ə/5GcѪU+ >Ç(+((@bb"g0~qM(((/jgҤIcEEΟ?{{{,Ynj/|555U' O𡃠6GwϤϧp & SQQASJKKǏK}ю z yyyу$"nG;& FgR7ߕ8rpp 222"SSS%??&j()??7l@vvvTZZNzВ%Kh3 ˗|`0 @6oLC$H3feeѦMhرdkkKRّ<N:QRRݿƌC_Ϙ0еkW2Ǯ"eeeP(nݺ:}㲶&4n8?t̙ޚN(}eddPDDңG$+++Ν;*--WRxxDDqedd'z>gnL{ F?$4}&'//ښ̙CFFFdeeEݤ}ˊP(^z:q{ϏϟO˗/8 ˗.]uc0 C zIC$H3fddk4ѪUYfQ^^ݹs&L ~uļ2܅ 7^PP Ә*++O>^LՏί_*++)77.\:d3f {ܹsܜS|γɓ'2`0ˈwN&&&dggG/_~xx89jl,iJJ رFE LGָPLJ˗-QN:EϞ=X}v^vvvvYo:Dhƌn`0>~nܸAh?4o<222ϿW?255}/ Brww)S%Xw`0f '"oƖ-[R^^އV#77vIO>}gYϞ=bBRRg xdmmMNSNН;wg88w*" wW#5q)HjffFݻw'$''G3gΤH-y~WADgϞ}L<ݻe)>>{޵y2V^M~b&KKK0aYXX#Ծ}{1cY欆H˗sB/yyy4hmݺݻGEEE,D䐏%$$ŔNrr2ݻwݻ%a-))!WWWz呛Hw n| j`0>nDŹ_| G>|HuT\\d?d-Zb0 cB2}$Vϛ4kf}QWW>o>J/ 2۷1zhBEE...͍˂Yls2+)).]Baa!v튩SW^,_\19::LMMѶm[ʕ+w^ ^zΝ;cԩs^xuuu9rƴiвeK~ǎìYsANNLg͚I^zaРA "xzzk< Ƨƛ7oPRR8___XYY!>> .18d233O?AQQDhhh` "1oJ>}yD<xnܸA[l)S!YlllѣJt rvv`#{ &999,?=Jsέ1fzz:҃{%ψh„ gӧz@QZZJmڴ!tuڼÇ9ϯ=z6ɓ)))I~AA`0>=Hk]v033B!=z8v  ** o޼U۶mC`` BBB9KD֭1zhƍT=ڶm3f`ƌ"Btt4DFF";;g1f;zzz4h#B[[[X-[k[b{..\ F}d!==N3JgYq:F+YVhh(:<bk"w+`0 B\%PRR:'%%޽{i̙Ot)z%%%QNNQQQeffRdd$ݼy1'/QLm۶Gqe~QFQXXDŋ 9:wGJJ hтHQӧO'yfH#$$455 >\jB ۱6mTXO|> ݹs޺0 MfNնm[211{Dz+((;wݻĄ:t Uuuu6mܹ<<}5j˗>}paaak׮3g@GGD8rTXv-n IDAT?/&WKAAcǎŕ+W8+W  1rH ;!Wg4i 4qD*CKK^kR`0;̃ ƢSNprrtA`9rlf̘///@޽qi.#@ 5k0l0|RezwU ϛ7jjj Btt4xb@c63g΄<<.RRRݺuky> bÆ 8t;<|0Zn-)QPP@ǎ{Ǐ+Wݻ_`` vڅ> nnnR;ˑjP? `47_}<==/k׮bu  зo_hkkW^ٳ'z:7322]iiiHOOǛ7o%m۶EN`̘1lݺ|> oacc%%%U e˖a̙5. _u.*#G޽@DXd @Ub"Y%%%2y̘1rrrGll,w :f/2۷NY:uˆ#7ooiJg\CCIÓ ˗c`РAՅ!zd:Bѽ{w,Z@  Edd$'''U/C>gt ###{yf~J2ëWTUW_aܸq8|0222R/o޼w}K. JJJ8bj ?c=x ?gϞ!<<aaaϯ8se|>8r /..Ƃ zjXE9<.\WWWnݺN=Tϻ|2ZZZ>}:|>.^~qDgS 1!!!y&6nȕ!22RfYf!447n`Rl/FZ.]ХKn CCCakkx())q/ԆhӦ{Mс,,,iii\@ vڕc:tPlڴ ҂o8ObF BA.]ªUp)nܸGP(D֭|r߿ڵCQQ>}P\piiiPVVưaàѣGcǎ]-ZCjj*vڅL۷MLL |Ú`0'0gܶO>QPP]54Iee% ",,|vڡO>ݻ7бcGtbI4tdff8qVǶmj<~1ܺu hٲes%>| {{{ڵk!''yڵìYT-߿F;̜9!!![vÇǏ 6)?3|||PRRy2DwMt={IHNaa!={P899!##ZZZՅ AwsܹsT͗ϟ?G@@=z@|_˗qeSVd x)h6ě"$ bbbгgOCWWuTZZƎ$())_pM]\\&1WoOS3AyyyFQQ>wbǎbc `0;@!!!]]]Ѷm[#Gg}~_~ӧL(++CYYY"C/!33HKKCrr2~'λ@BcС2d NJ^?<*%!EQCR1[ZLe2̒Lče a EJ mJRE{ڗ[j1q8}n:~P%K*RPΝ; WWWv?+V@ Aç~ |ğcǸt={Dvv6|}},TCa۶m@YYaaaAj``uuudff͛lMENN9H`aacۻػt½P[ .`ӦM \h~4k ##===q.i@@?hѣ[e %%%C[[֭ٳkUCOO%%%066Jڜ 6:kC$zhͳJ?~Looo9DRmmf`0 sA1`366OJIIA]]PSS:YYY(++CZZeee(//P"^ZZ#==ήS /?!`hhmmmB!)) O>E@@/_㡲D"{t._II[Q7v܉d8qRRR ?x KB1\ԩSN:m8|0lll`bb9eEdffbԨQ(++͛m۶fٵkWL2SLixP9R"⢲-Z??&D `+0hiiq*}}}a޽ӧBBB<|>\ǖsPWWG޽1`={Drr2sΡ222۷/Wj\%555NSD 6@^^ o;wbƍkkk <\> ₊ {044h\vcϞ=@ hV066- }] .f̘'O8x`k`t(DE[,h…x֬Y_~?S XW7"22݃ѯ_?.4m>4 qvtM3`th=z6lX\|9:ݻׯP9%rزeK!)Ь(]g5bĈ&`0 sփ(T]II VVVKdgg#33YYY())AII |> N:AQQ]v9f= kpyZZZ¼y0bĈ+7999Ɂsk!`kk | '"|wؽ{7`͚5ؿ?6mZa9s_xҥK%Pm6x<6n܈Ν;sx1vXimpyxyya޽y)))9H ++DEE1?CK(HE^/mۆɓ'c͞1(++LHHpIO8sL뢼nP?J/^@aa!x<S FGt9rd cŊPRR¢E9st(ddd@%@ujbt Fj9 x`z9HkIKKc*Tq)!11PRR61f,Z] )))!//A)ǧ~ &ήy֭Á\1'|Ν;#>>xXhV(*[wrx<ըXoaa9HEڵ+ƍ;wÃ9HZ"899!((666nv>2x` </nV-B |sڵkqu XjMnEa9HKKK1w\XXXHpx<޽s ן8q0e\ vi z̿i)deeǏK4Hٲ2vv³gϸ͜2oL8QI|xӍe02@7-[@EE.]ׯ_ot%umٺ|>EEE5߿0nܸ6`0@y:ԧ m +qe!iqq1͛˗cҥ WQQ+++8;;CZZG_-G3TUUFFF \A,kkkmKɉ333s!sssyeee?@$޻wyyy-h%q_[ӧ[m-ܹs0i$\XX;V8t T]FJA`***6lXgBdffb„ 022jB^߭[6y'HII͛abb"iG hmv @r/4MAYfa;wnscr ddd?F?glcc@QQ v]BM&V@GG@ D"͛7QQQ!&rx 4.455!ײ2P9H[M~g-:ۄP(ăT+HE{㪪pyXXXp@w8|0z-6FXb/ ƻɽ{|A՛ LMMrJ899!00PM> 6ñc%"33jkݻw'FFF1`0{0i=4VAΝkMZ={6mۆӧ78O~~>LLLqYYYdeeA( CBBdddp}QqsJ(//P]MD6w0qAEE%%% kP7H*Sx˃@ r!"ƌc m!..Ő+aXYYOqwEff&.\ȝ+**‚ !C%RSS 9r]c͛hkk#,, _~%O333['ODtt4 "<<>f_`ood"!!0`G1_;w 3gά5?M xaEA }=?f},]͛aeeը|mB^EEEEҤ$,^NNNUɁ) MMM/+o%Y255JJJpe ߵkjֵb255ѣG#Q᣶@FFƸx"all̵`HHHDEZf̘Ç BCb0ZQ YYVb ,, ->@ @ff&RRR|iiiķ~+vOhkD &VJZZOFQQV\ eeeXYYqƴiTuV=:AAA R`?&Ŧ #ӧOSNӧC~0l0 :0 ,ݻ^C^PPPh0!** Zf\\<,Zׯ_((‰`0AZݺu TZZ?ϟׯc֬YMZ7997o䎴4A8HTb8qD Omdffbx1TTT 899aӦM ȑ#O?ΎS܊3]t ԩS%'HKKCtttnff/ wk377ǁ! FHH [l9Hw*3g΄4ݛ -))GX!>>5TN222ի=hÅ `ll kkktޝs>x#F BCCau׶`0~ݻ(++s V#˗/h\p^P(aÆ5X`ҥرcN>]#r]cڴi={v9EfѣGѽ{w_^"Q"⢬`0ERiΜ9ѣ.\(iFFΝ;ӧOs={bɘ:u*N*s%9HKJJUVٳjplrr2SLJ{8[n,--֭[ظq#>у )[[[Kݾ};B!x<WM1vXۻ8Hg̘xB,7ֆ&Moooxxx0)F ѣG߳gO3غu㒒pMܾ}!!!;wƐ!C``` BCCuuvw:thJJJɓG|bccCAAuVfNNNz`0:.燗mӵkSVVV">>p~7O>Аo`رpvvA())L* Ҍ 8;;pvvnRǏ#-- 2eJla0 㭆uIHQQQ/^Lzj_QQRTT$ƌCDDgcm ]o_~s#F tԥK@wܑh} ƻ ##V[㫯"u"___Zr% 8u֍i˖-tJJJjzSx/55 @Cn:Ȩwϧq6l GСCmJ'm߾>oC w8ݻ7v; IDAT'O\ٳgQвeܜOaaabvIhɍ`0w |>rrr3003g^zhONN_ŋT%"ihh(%څ11`aРA—_~08;;=y$ƌ#Nz||<^| XfM}MMMyf 򂭭-w^^^SN<<<*Iׅ%/_BCC5Mg0څDl۶ *cƌ#jܓ?Ά"`A  99>Dpp0x<zcС[,_uKRUU${Wo>}ƍ8q"LLL󡬬\jTCQQ.\@Ν[|AuHR=۷/| bÆ pvvƘ1cj)S n݂Nk "={gΜQШNprr6mĝwssÁT?+3 _9HA ۷//^ .оfL8C !z@3449`Ȑ!C~_^^˗ٳs޾},Y"",zAMM YYYsf̘!:9vx Acǎ8p ttt .++++.ľܖ-3%%ĥK~ιgaa+++̘1^'P(DFFܸqIII:tuu1l07HkJ@{&M!''سgСCxo4pA(bРAun t}\\\{9K1}t|ܼySbCHHZuq1 PPPh >777={zll,+.j xazPRR /TMM ڹuVDll,ddd0|lsVRRR066 |||:tڵ+ ɓ'smxBRRDJ3f 66Ax8|08P)Z6{RRRp5_4hv؁+VHMJJˇ8P}h"==ݻw'pK :gÇ HNN_JJ /_" [f511igKҥKqy\v Χbʔ)χo6DEꌌjmOOO޽{cmƱc9s˜"226l?H/?s2 N1=zzc ]|N>Mz")))Z|9xB!ݹsVZEcǎK3=~dP(gϞݻw4uN8Ah„ }6SOOA'@HNNvZ@f͒Vwww.W_}%ggg@C x`mF"$\Me0ڜ*~iܹljVVV4w>|8@ h5E撧'988̙3ivZp~~~455=5rۉeyGw>}rJ{lۗ,--sCF' [nڧ 5o||<ّ rB<<<ؘ.@JJJѨ5 xa PQQAnnn*ًM7nD||< ĉ|qqq}68'O MMM;wFN ++ >*z x%TԄA+K 2L0ׯ_G׮]'}\>8w4JM믿rDcLMMgϞAKKKZsss\zعsX=z^&N)RݻǪ2j^z`gg{rJ.]2228p g|gm3{077+Wzzz066[Ta* 4ru lo%%% ;̓'Oشi._#GbԨQ5jz6))),^!--S"%%j#!!~}ddd,|wޅ#a1PVVӧOc޽ܳ2ǃ)|}}Q\\ hDyH $ڵ _5쯭 mmms999HNN朳@.] ##O>իZ!k`ee L<:(ۇ 6@(bر8wF\rrrн{wXZZJd'r7y |TWWǨQ7n4GFF"%%E,~WA,M˗/ӓ9Ho-O>G}Xȑ#Xt)$~yl*PVVnwgǃtttj* &ƦI7w׹v8x ;`㨨@]]ALL N:ŵ#Gs4gfٲeعs'ၤ$xyy?l\> 6EC+**py8qطoq!%K૯LLL닳g϶j>o`0 [љ6mGJ<&77>3 oEZwիW6!4c *--sJZz53|*--'OrWVVќ9s^Zb;7n޽{:Dc7nHh 6-v^ ׯK4?ChСa* ;;K!RDDJii)RZZZںu+ŋeD/^ݻZre`0:>UUUH@%!''GC%kkkrtt@*..nCݣ@~~~Mgݺulll=6##> GGGqQ(RSS#/@^`0w m(HwcǎIN] ңGbժU ?>N>]gQW^a޼yy&x<;l߾<ǏP^FFyyyиG.bffC]OرcQYY`q w^JJ 8u<<<`aa\3fCLL WTx[8z(֬Y*9nnn\i=#:u qqq< mmm߇j-PPPq%Tw6J|'ñwf`0---̜9;}%t[?~L.\_~/^L4c ꫯŅ-]s )))ѓ'Oox')++`믿hڵ4qDYwdiiI?#Qjjjr'z yђ%K(**Jltuuυ㑙ݸqC'ڻwo`0 m(HvD R"Bii)qFʕ+q:CݻwsAvv6TUUqe8qBƍvE KJKUTĈ<G`` Nuss`ooe˖a՘2e I&5Sj&N%%% ++<~X\Q '֬YӚ3&77í[8oV5rCWWb竪(a(..2tuuCCC-RSx"OÇf1wN:a̘13f B!n޼g 5~:wN]]ׇrIhHh"9 [eggرc8pWKNN ,w}W;>&N@.'*`0YCٶmPTTBoذ~[N?CrrrtuubB;%v?{Lb%RΟ?X߂矩O>bC%'m̙3 }5&MDhu}.OֻxcDFF uԉN>]oPȩʚRPP۷>S>}:YYYѮ]( ͶO?n ԵkFf0]  ԩ#P~~>#Y[[СCH7?~<ّ3EEEIZ޽q=}sٓ훬uss#դ `+0i Ғdgg###999())AAAjՕ@ !//Ν;G۷/ )))Bٳ`oo;vԺP(ƍV-={Fe<{ P(ɓ'c][TT?~>># UYY[lM1110lܸ|M5hnnk׮Xn߾ N[PSSCVV$]`5Xp! ѧO\zcƌwLqq1B!֯b_]tq0n8\~~>}ɓ1eL7nN:%ԊIMTђ1w2 yEEE̙3VVV066ڜ0a&L;//aaa Ehh(|ܻwL1`VV<Ǟ={ݗ 000<{ ܻ`09C9u>M=zD}v/ܜ}]8zAZZZu\uQUUU6>S"Ωԯ_?@Ǐڿk.g):u*zdee =z9bccΎddd8ʊ$5x9r}utt4 iiiɑh>kkkbtHB!otܸq.'99NtP Aߗx̮]͝;Ik.[4f]wQ=4ɓ'[zWϏvE ,ש4޽;M>6nHIڵku WWW244sޢ*XQ`0 Q3\zsAnݰ}v15ԑ(gdd$␔ĩCUU={DΝѭ[7j+iiiTVV3++ 騪j_5v]k[aa!zRcԩ}UUU߿?믿bӦMCNNtiӦ͛矱e˖+,,Ĺso>NQ uU?[XlX,\Ν; xE+Y`4rd=zYvFFF;v,`ddi<{ @FF}cƌb +AZZZKJJ+W֭[Ɂ!/r +ÇB`0bcc}vpϜz-V\ V]S|inݺL0h sFnn.^zհĉؽ{7RSST5kV8IIIôiZ|~`0 A9}tfG 9s͛ɉ^JݣgϞQiiiϧTu۷lllH__:uTSN4rHiÆ o>rwwJOO|]TUUQJJ 9;;6xhkk:{l=z& ""ZdT<<<*\^˗/Ѿ}7꺅B!T]]ŋ|W,XPm͚5/^,\yyy5^h/8Eܹ1IIItEz%U荍~__? BDM%111tҥ/^HEy/_)CK,'NPׇkr4p["օ 5x7y1-X@FFFthW ֭[ТEHGGG:u*}7tYoS&v>!!"vJvvvҗ,رc :uUa0 # P\\L F***w҅(>j('}}}O!!!\Q\kOȑ#xϟORRR@9ݺu7n qE;rssʔ)dffF?h  55lmmi&ޑ)**QkxBPuË( fΜ)֦ÇY3fڻwo`0 FG9H%`ȑNRXXXPK.+ݽ{=zDCmp5-ϯZk8xyy9YYYԩS9Eccc@sEЂ#ADD?# jBBBV,偲2ڶZB{n-[j͜9/\dddիW-m*Q/BvcƏO'OOOz QPP]rIFEGT@鐑C-j xCTTTP~h„ b322ܜЌ3())|Xo-[8hf֭-Zbv1ϧ?z_GMmZP\\Ls IDATݣҥKiuQRR[EY[[SXXX_UUE"##`0:A* CC6QP^^ؑD: ]@彨,,,̙3\xH7Al~G'O&}v"vf^[A;vBEcccruum<'4f̘m&4bULuuum1(..sr/yNμ<|2ݽ{,vjj*yzzr|%ݻ2۵kW266&rww& ѵkHMM:uDhqGnii)4f\|{=ѿ:ydI VPPPgш#j(Miƍ<}4- `'DFFbȑEQQUJII ^|,dff"==YYY())AQQ PZZTVVB(BZZ5Ԩ)h/ gφy01133n:TUUJJJHOOoߎ~cƌAppD6$$$`Ȑ! "aȐ!\ۮ]`oo?oASS)))8w,X---ő#G`hhK@ '򩖖lmmaccU(m*Ϟ=àA %%/_B]]kKMMEADHJJf}8r.]'N46C0k,BZZ+Ga@(++cܸqk^µk`jjB=Dijgp]޽{P(OKK Ǐ>&LѣGCJJIIJII  === <qʚ(**BXXF]k/^@__XhΜ9õ]xVVV=z4ZF1ᅦ:uֵ/߰}vTVVBFFFFF񁢢b{XZZbРAHHHhos h?99u̒%KZ,/Pee%:t7oM8&NHg?@t󣐐dDD9D>cN5&Bh߾}! 3Ĕ**[{3ɓν={V%;;;./賰jvo_gĈ:$\nnnzSr0ڞ ի-TTTҥK WRTTTmǏ͛7ߟ222$z9++:L+))q9\]]LTD!<֯_J˫2#4|DׯUd0GFFXSGʒ4yPJHH/^hdN{p}6l}R\\\{%NҲMa0 ]aR 9DǷ~"VVVҥKwD&;;;4B!iii:zh}Dϟ'"".&km}4{v3Z&LW`0  mHxݻ\۴(!'wnԨQbœ|>xs/_Ђ #Rq  /WTTDG!E%.ޞnҒ|}}Tr/!X[UU7{}M&6I[[[SCH)==(<<޽K7nhVTrqqtz9Qttt瓄$Rt%ohĉԹsZ4w\Ν;ot{? v\tM]( xmJ}ۅo(Q6??7(TJJ>S܂"!!!dkk+62֪tۻw/ ##m...XRD*Ǐ72 19¥|x%CEEƍG9ٳ|||PUUE^^^t}*((`hvEO<P999A?#{ѣ͘1nJ>>>j&;v 4o<"{NCr:r3+++@K.rpp }}}N=):TUUʊMJKK)++bbbLJ>|HaaaHD7nܠ*++aIbW^m]|޾U ޹s `azUNTMLMMٺ֡)22|||ԩSg:lj;v***gϞ\]]8|}9=cǎM6Mi"%у{𕗗'kk Ƞ;vpj]]]9eVll,#//+8%iNj50"Dz#???BBBٙҒj<~(++>YYYRTTT 2@z)QHHH^kii)PTTQHHH'&&3 7---&GGG l|G%4m4""$|>gss IHQQQls(##ʊ]4~xڱcG>ϧDz!]p^zE OL׮]_ʅxP۷ocϞ=z*'dv .ӡ$UرcP&iqqq>}:<==ˍO h֬cȐ!uooo5 ]v%hDFFrFEE!11U}MFFFS===N8&xڷoP[y#!!>ģGCSQQlmmakk-[֋>Dvv6ADAF:T9pͅ,X-[@EE>`߾}ٳ1y/xmmm ڵ K."d0 k U`llظq#2ATUU )߿GJJ RRR#''EEEA~~> !P\\ r⥪*Ը&6'6k֬^PSSC^^n߾Ç#,, z";y<Ю];lv /^ݻ,'''۷SNş ٳ'>}c…5ܹ3bbbp1̞=c"""pA\lB ?1_Y~E===@gΜѣG uҥ>2Dwl׮9_~~>'^dddqڨQ#ٳ'M26oL/^j+JJJϟݻw);;\$Brttr-Y|8Y[[sZZZ/OOK ^\Q\\LL۷p_ѡիW@Lii)mݺ+٨Q#ڼysЦMhСt%Δ`0 &V-3fkӳ<*޽>QǏmslmm%򥰰 QXX/NGqBÃtR3bbbBR<ЦM*-XдiIJ;NNHHo FPP6m4NN5kѣC$++[(H=zI&ѯJ|JKKZsqUF}^|YN ; )EFFViÇܶ#doo988HHDt%nKgQQ׶~@{.\Hhbprr"4vX/"&&HII7k֌VZE)))ۋE낂 5~zS 05!JЮHDII ő7m۶ON{gH__lllhժUA_|P(y<۷&DI GzEhϞ=z@@B{ޤI?~9::wEršllljGDhh( :=Z/Ξ=u;uD{ҥK -ZH>2 o >Oڵ#ʵ}³tR tB$+Vpjʕbڷo@]I ?k4iDA9uZQ)m߾}yJE+?KLLˏiӦZpaxI-Rlْ,,,ɉNZohqΝ['O$4`/hH : B M6Q~PKCC&L@Gɓ'suZ رc)55UW<|Ki`hhHgϞ%@P+[?(`02L ;wZjGV i„ ,YB qÓO?~ٳgDDmhЯZ'DšչVQX(ICCVP!66ʉ(Dp,Z@]]͛7TTTx+))QF555 PQQ!++ Ws械yyyAaa!󑝝B ++ EEEυ(,,Dff&sVV PXX a͑ضm֬Y޽{#44TAtt4vډ52JJJдiS"((FQRRpٳZFFF^ kkZS8q~w(\5 ˗/G~c-[`͚5Y۶m>5͛#22FFFҽ07Cjj*Zj(((TfeeݻGNNrrr\?AVVM6ttt]]]BQQQ*H PDFF"**{EGGڱ<ǂ @`` н{wܾ}wVU_7o̙3d0 жm[$%%5;URRR;wOǃ7llljm3<<K.Ŗ-[`bb"Eo ĉ̙3lسg)`0nځ0CVV .׮] >ljsEll,BBBŽbccqq?:u 0c {xx("e⢹9._ BCCy&kHaccOOO\x^R ,YǕ+WsNO>ѣ???Qx@[[>c0_ ]]݆vR`aa{,--$_8q.\MJMݻwcժUhݺ5-6ϗM`00"FGȑ#^zM6=zƍS8p eFYVVD"rrr MZrss!jjjPRRjաhhh@II Ԅ%%%5͛!!!x0qD 8}4D갲˗燭[(߼yW\K6˗/Ð??Ǐǽ{w^\xa``cU.xhҤ KKimm{իmD{TUDFFK.⇶6wU+--E||/bYYY >gΜիWH׮]{U\ q4h"޽{u֡Yf_=e>v :u*מgϞӧضm ++N:w߿?:t >}ҥKx9gzz:PPP;wm۶g)m1h DFF:ujfffŋEnn.x<tttбcG>ݮ]r-"-- j/n߾)))8s dddJKK1c xyyaٲe߹\D$K6`0Fyt())A^`jjʵoذжm[|eddV{ϟÇx!^z,ʩM6hܸ1j߾=7n\$H߯*Dѝ"D˱ur`RPPիWcPWWŋfP6P T\Dǎ+5!M6֭={k׮:::066Ɠ'OEUkQF¹sp…/*ر#<_ߏo~r)P6?s |}}w:::HJJ͛7˥J`0ӇQx<p-̚5 3f %\  pm|>111 ݻ+O>0`zYtΝ;H 1j(ݻwh\|666֭ND\>e&2&&&pss㶯e[JJJZ@l˖-233SYYY]t>ڵkWkÇ "Ύ$.]&M`߾}:M̛7^^^غu+V^]/19 W`塾 I&<==`#G*Dy#׏̙CE+.ӇXݻ쏎&:s =zP6mѥKiii]=77TUU ݼyS1zj@G\\\YXXeˋvT&rrr"Y3NrrrIJHh̙2AFFЫWL{%kkkM6Miˣ@ںu+Ӱahܹtܹ/rH@ TZZZodggG222t /^5k֐͛7_^d:tnnn`0Pzz:IMFcƌZ̓ )""/ڲe ͜9 Fo߮yĉ%~z+{.=zV/֭[Wp `Çs"b^^U9ɓWӓO˗//*~N]vJKKiŴp*=<<ԴabbB觟~ ͟?_l??Nu֜*-DB ;'$//O5%%%%@Rdgg<gϞUn~7]|[D`ƿ@ѵO7mڴhذaL'N[֫(X[j.)$++KUC!!!.]Jh޽\ۇh׮]dnnNϧ{U{ ƷCVk-_ B 5DӧO'?K4?˗WٟI{Eǎ_~x<M6\iAA͝;ˉȱq館(8d@k׮{|jܸ1]OOpѰ5aggG'} }[l!4dlQFHU7J}msZl 7ӦMP(O?qQ\dooO;v전-ZDDe;Ɔ.^(޽{Yd bccChǎ|q믿RXX ( &___2eXaaaߓ rtt*dffR۶msb B>}J;w$;;;7o"ڵk܎9Q>}hÆ ڠA `4߬@OHQQ޿ϵ*/XpVĊ }|>;F R@V}t̵߿PĞTyۖ[ۨ0ae˖qm-"4i$l8qW3afff"""ϱ8QD;ݰa]e|#hjj ]PHdhhG88Љ'<&;;Ο?O6lqƑ>;e*FdnnNϞ=cǒ=}FΟ?OoѼy-^v`0ZЌ3jMU_WWٳgŲJモo-Z[* OOO@>>>Robĉ~ƽy֬YC C)))srrCԲeKrrrcNJ6$$FAVғI]]… %`ݫ#ش4x"-]FAÆ ӧӶmLJ⤲H- j;v3gА!C}\\\eJKKhKVWTTD/^ & ?~\`G]? ߟRƸ #LjԨYXX+%%%I `|m|˗/mFW.YsD[l/?~@:x -Xi4zhrvv={7={LbQOy_^etlNNM:*MnN::pD& iii\(@II iiiqXv Fh$Bn}ҥ .q8Wm!$u%%%K +V 4wZ % ֭[h(!ccc%%%Ѽyh̙ݻwK4i1\T4x=ݼyKҒ,--ƆΝK6l#G/EDDpš~ bŊ:?~<_^%%%t9.UQQڵkGC={6 6v-hII ѣ Y[[W8F5kV!ؘ֯_O=wv0 7)*v6\(z߾}k!ڶuVϛIyf5j']N>MO<8G|/^VZƍWnr4{lڵ$">'VSZݛ6QN^:u/7%f̙U!ԬY3';vq.Qܾla׮]Rq* c:|pFψr{16q[fMrssŅlllɓ'ܸH;r-,,4񍐞snW\\Lt}:<Ҋ+ bΝY+djjZ2pBRWW 6ϧ,--ɉbbbӂ I&5BLk׮ݻWK[hAgϦ .p `t.7/}߻wF;Ǐ'm۶*y yxxЬYĄŅ|}}ÇR b+Ã\ig^^RVVKCp]"*d #Ǐ?P=vX/9k;s ֭[e\騨tWlm۶B_AA)++zX M0Aڮ2DՋopݺub_Dii)={;Fϧ>}p:tP:D{KKKҢwޑ.uԩڼۥF;(o333ٳg\`0DϯcTT74jS=JvvvbEDnn.ѐ!CE*6Eҙ3gW^DM555=z4I_͛7t1bwҒ'Va@`0&9t4E5TGGG,;ӦMͽ{O&MCI,QZZQDD=zEnnn{nںu+Zё&LP*hl^Q^Q޽PϞ=SU+߿_"IMMF v…z;!Ȥ$ڵk :yzzRttW5ɓ'\􏤤ĉɉrssBn666Lf"GGJ>yѨQ#pP(QFqҌb0_'#G$wކvJN:uj\K"X,܄O __~9\dWMD\Rڮ2|w\$#ڵk 0;ڵ+9;;ądaSRruu%+++z׾|rO;mF#G,WSN>Eڶm۶Z:##tttXJѰ?]|gC#(RWWW̙3΋׮]#333ڹsTBR^VZѢE̙3@)))JGWoyCSRRdccdeeI[[-3 HE>X*xIRRIhkk[JCCC@>>>֭[jٲ%WEҒ;ӓm wk4 ڵRRR߿[g[efCml+z8rXDz]v8&&&B$3YXXиqhdaaQ5rH?~<1~{{{' ҢݻWxx89::r9ڿ!(((0߾}[aaadaaAa\"iРAQinf>_./V .[3/˗/'|~hҥtƍjs)~W@~wѤIh唙)U>v"58#qǏ)//ӓ<<<ݻ*4u آ`0&޽{DƆkd%ȑ# VJ!C:zcBBBiӦk"*@jѢ ///x 7mDI&_HOO_|ɵl֭X_J@ ~7~\r%ѣGe۷h3$At9vXŅdddjܮH666Myyy_}#ʝ}s233ɉ&M$VQX5j999UIΥ%@HhԨQ ϖ-[M4\P(pڲe Y[[%]Hω $///uRP($:t(=~X?~HܜMQQVXQm=Q wћ7oƍ'PZf uԩ(*`0 Fm7+`رڵk;;;(**m|=-Zu|`` G޽{000 -- }7`ڴib.eHƍqrXvƎ xRRR`}?eȑhUVѣ_2JJJ>۷om}addlll;v@CCq<&&&5kp%";;}AVpiܺu ϟ@ 'P0lذ;vD>}uV@FF&&&puu055{`0 kh|2ٙk_r%|>x<'̈́@۶m˵ Bdgg#77󑕕" ?CFF222Ѐ*7n ---4n[& @ &}?4iDͩSBII pIݺu˧O">>@K`eeK.5 6l;w --jmtݺuq,\K^-#Fɓ'+4MLLиqcddd ((Scȑx)0pDAA]h߿?qmۗ?`cX~=ի0xyyŋ011-:j2_=>>022µkj\Ν;j*X[[Wy/Nj .\@%[FF066Fdd$-[ÇKl`|݈=EEE'//#22Ϟ=K(**BWWڵ+N >} 99/^?EFv$/H$,XW^ bhӦ Ƭ,ddd "":::ѣwϟcРA֖@ @PP.\˗/ݻw\ {{{=[;rX`0oB ݷo|>Snvq@߾}ѪU+lvʵcʕPQQ ԠYYYYhiiq?ɁP(Dff&t"//DMMMȈ{}*h'k9ufΜ >333\r\QZZ;k֬r`boϒ?3UUU]vHHHk0yxW!>HHH@LL :ubիWHyfܺu l Q4a} 8p ޽˵`ٲe8pLMMq1o߾F[000  s<<={y*_~UVc=y$ }}}l|.]eG?~<[rmƏOtq%$$T($)֭#YYYˣ>Ѐ-[JKKz)/^M6dٮ-=z tEz% 2N߼yڨׯ_cш~WZJװsNܿO>-޽[cz@%y222hٲP2 M6O9s .\L(((য়~”)SBJJJ P(Dpp0=z@NNڔ3xzzӈ5j+++L<666RMDGG#99rrr:tD0 QR"/&jAAAhqFlfffb޽''')z+}:t,_jqq1?b׮]WWJǏ琓Ô)SM|3f}W\K  Vqo߾h֬U9r$'.[F{#FFGGטאMREEEN~W^իWwG@f͠~H###:t|>8~8!++;BWWm۶rqEEE\RNNJJJiӦ5.ؚ+_zV^]}|֭@ 1.]===1СV^ 6pwwG)`0FCD"a~T2220m4L>]r ̞=o߾ܹs{J\E"!C ''߿}ڵ+ QTT*ĉ8wrrrc?~tdLHSzЫW/իȾ$4k wƌ|[n޽;vZV KKKXZZx~wǏ{!''G,jժvڨQѦMYfe,ijj*-Z,`Gnn.lmmgI}Rmٲeľ}`hh===ܽ{=-[~>Ô)SDq*כl8UqܿZ/ݻw_|}G,Z֭Ij ;w+W 66V-2аAJJ v؁["22R7i666:u gϞdddddd^:Nry&p>rHQ\HPhŋ9l0QXhǎ%׾}5b^^I299Z ,[LV*J4hhKOOVVǏ_Yjݻ :t(v˖- h@Tذa}FEE gݻw(^7nGgDD4ܹs5kZhyHLL>0**~[[[ԩS5/$$XZ5\~-p֬Yڔ"ÃNNN%hggG7772''Gj׮-.Ao.ڞ={YfVgٸqc=<<*2vWN\z5Ittts\l_222%3k,Z˗ieeŋɓjӧ&""B~ǎѣ; /^,OLLLxAKrss{nGڱcGY:?ga |IGFFFFF+S쓒_Z … QPP===QhIy~\\ZexRL> BINB^^Z[FFHKKSKTTHLLD~~>J%J%D_iΗ ###;v  *r5k5k111,!g ѻ$ v!33Ć 0w\%ݻwO y5{ooo;vL^>4ix:u ǏlKp"vԨQزe R,*ӣG4o1118}4>4_ GbP(^%Eiڴ)6ms(QRšt333=rrrYn}\|/_ƍ7dkE}Ǐ?,,,4c˖-;w.J%:uꄣGs_xrJ,\-Bѷo_899lقcǎΝ;X|9VX!Cm=WFgϞ@)>}AAA8StQ>}CC ?k_󃞞2220nܸ LLLڵkuV$%%x^piԨQ3g>R "222222%C[.]EJZj{8?%KOի^gnn.'L-[+V{%"##E${4*mڴ!nݺU/"ݻkh)`111#޿_qKɓ+TXt)pԨQEeffАxƛ9s*'Y|9POO=z#}}}3yHLLӧ/Æ W_qӦMFϞ=ˏ?UV}iddĆ W1 IDATm۶j[NسgOڵ+;wL333vڕ:ub۶m٢E 6oޜ_>k׮-&F٪UzYfر#áCrܸqN@/z?=zT[R}7%KP, ___Z*U~~HV@W9rhjj6ٳ'pڵs)`:uJѭ,.^HQ췲"(>ȼȍ7صk"ϩڵksرܸqƋ+{.9"w]<-,,Ļؘ~!yyfee˖ݻEƲo߾‰Ȃr٠K L&$$I&aё&&&joffftrrbBBK4k֌xڵk?S9_K;vF޲e mllt^z略%K.ѣGRx->| kx9rgΜݻw^D/4223fP033Ν;_w>||޽;-ZEΝ GѸϥK{v%jQW<&&&ɓ'|)E!;w044W^uxe>|bPPKyIoΝe3779rիW#TʵEP6 bR`5Ϟ>}gϮbRQJ7//Oz{{|srr`=z^^^twwRdhh(mll[l~R۶mK={͛RICݻyQ^rnnnCt'//IIIx p=ܻw>D^^Ѽys[n֭7n o"Avv6~'(((@ݺu|r̛7 yyy bzz:~W#Gŋ*W?~011'O^$꒱cmllDۃDE'}̙3aaa+ׯ_9`4'O/pΝd`` cbb^B$W^eEDرجY3%"AO3QFܾ}KZj;wxӧO9dq/mmmT*E?CCCyu9s?f^^CBBP('OPT255|1CCCɋ/2..۷gzzzݝvӧk.^xsYӧOʕ+}6ݻLjQ=r!vvvZ#/// _~9Ly駟T|AA>|H///Z~>?>wɰbS%Zjӕbߑ#GijxwiNY(Jnܸ5ՋΝfΜ9Edddddd^G54ڇNy1666( 0((ǏΝ;믿r2e rРAĉ9o/ܹsa?Љ=HLLA?!!IIIHOOI022Bݺuadd5knݺիVZjRI"55iiiGbb"ӡggg?VXw}AAAk$qt 0l0oFI`ذahذ!9t󷱱ݻh޼9>^z!<<aaaHLL QF deeaȐ!HKKCpp0Zn5kEHMMEzhР@TZj/mmmuVx#Νuֽ Sedԩ"##q!?U',, W^EժU1}tdeeM6HJJ͛_V<|xeJ(ᡶԩS"*==]k *!3F>}%ٳG>D*UX6##"}BCCYjU6jԈ+V`rr2w%>^=>ĉibb*-z0R!4GGG]*#RkݝViܸ1?SZ0a{)p ٶm[ZZZ֖NNNtwwg`` mѵkW?HIWR쮲P`ڸq*8<4h nܸ7*MF~K2eʫ6Ecl,l{{{iӦb8577EwעE|r\zuXRHkkkk B#Gp$O8Kseddddd*7AzAΒ*ڥȑ6mڔ9ݻ LS_ ΝKܹ$nҤIv֑ Be`` <ͣ5vڵkk@522b9fcI>͛7gBBBV݅L$EPFbJF^z(8는MөS'p±ٺukQخ<ܾ}jbppp. :99qذa-3g իW3>>WV)/o'Ο?_>}ZL,4H|1"HTx9qp!ϫuj'YF!Δ`)b{S(tssS(*MM/Ǐ { ;$~'СC5/66dd^/.#m۶gϞjw])))׮]oqܹf.]Ɯ6[ ةS'ߟGi@gggn߾ǎ xm&%%?ǼاOժRgϞ1,,O>Έ^zwٳg|r:;;ȑ#LNNMg1??\z5N777:unbpp0׮]{ٳjSRRتU+ᤖÙ&EyhNᬔwjs4ѣGbUwanijF-@"pݢ{6m_DPՕSRRoqРAtpp(N͛ -u&#####F;HSdLJ [F[lYn[FM###{311arrpiF|+J6nܘxǖRgϞM$Μ9bSIǎ @MXC%-((#xLDյȾ`GM6T[H 5#?~\s'&&իtwwUǬԤI!gRMOO&&&ܹ39vXN> .Tӣb4YzB`zz:CCC3 @Dر'tqqk۷Ϗ...?pΝvA200P-䀽q>|Ȁ:uJ .粁]\\D:;;ǎUFFHX;v|զhJ!֭[_ cccߕbӂ syyytss]]]K,zWV_|A222222/x?cƌAv>ĭ[h###@zzzmٰa{=|ᇸzud…ػw/"##w!$$0}t?ԭ[Fhd8qgzRĭ[o[n֭[qjB&Mиqc4nM6EFD)4iFz݈9r$"##q aӦMѳgO/>FѣG# s7pE3ݻw7|?VZԖ#F͛8y$lmmh֬bccq?F7n fͪ,edJYfRٳ'vލ_7nĖ-[UVa͚58q"{i|nW^{{{L:C~~>k888 ##IIIx)>}d$'''%%KJ%H")) III%ڠ"X}~~~~:t(nݺa޽۷/mۢe˖Qj֬~> >> 4@TTT===DDDG]6"""аaCkFHH2331j( //O>=:4n`nn{{{1}4o\㟫L9y$?3:w 333o{AOO[Ɲ;w9&//_56lݻ7ѪUJt"bڵ=z4N:{{{cÆ hР͛wy>UFFFFF=%66VDaÆ(• K#22RD7OPӓoJTIJgtt/͚5K1׮]+J!!!477)$-̆ TإְaCv҅C 8`D{bll".Ey;vAAA cDŽJrr2CCCtP(°0* ʕ+$k׮ɓ|H߫J IDATm}}}#GLJ-MR\:twS€088?fjj*SRRHEBpȐ!kjh)S_^FFF >عYjhffƏ?K._Őj.СCEpzlmm5(/ҜXWK}||hiiI'''d$T*;FKKK.] w)222222/=k%tR֭nܸ===dff^z(((ĉk4VBBLMM](x1>} RZj!++ Z* h[nrO>|)))hڴ) .^k4{g /ի WWWo^:u*?`(((@bb"Dɓ'ERSSKuxF+ 4lPѣT4h@$i&ųg) &Mh=֭[Zls/cX|9૯D7V6iii[.ESAARRR`bbt+++4{lOFFdRRR[nGTTJeWRmڴz-[033CΝQNjo7nѣ;vh{.5j`ǎ4iRӼys;gӆ˗/cʕڵ+-ZziORX~=k>|2###<{쥿eddddd+vЖlQ)}׮]VI52b '''[ǎc```>}'|E>|("5w-7l@l߾ƑYn/jݑdhh(}:99[Y[;wבh^t?ZƒE-^X]ܻwIdddŅ-ZU^666];n8c} k4oyTWzVAА666 z<'''1ɓ)gBBȓ'OҥK={dر$mxI#yU={qqqpoݺŴ4Th#-|"S|DFFF7(J޻wtrr-,vgllLsssŅ>>>Z뵥sǏ+Woƍ˕vJ*_*,,'O-4RxAZXXpժULOO/I͍ 4ȼaRWWWQhCJ)((yKXW^}ZZzeϛ7or۶m9s&w޽{kѣ"pM߾} ?v,\@jJNNN2dJ/O2cݻ3gΔX,i˖-"G]6=iӦM~[} &h5^qHR11ɱlmm͋/Vx*v޽4{%𼰗̫W^&QӹvZo^`҈#xIR7n(ƍQ!p ܰa7nȠ ĉdJJ L| CBB]JWddd.i=U%B˖- gΜѕ222:$66>>>tqqS###NNN{*TSRf͚ׯ|^RZ0ҥ Y8i$>|8U&MX222222/7Aڽ{wE!&PZ#>}I85%66Gw}jX^=ZYYq%U/3I===|+6o޼H$jYQyyy6mpĈŎ`O:??> >\8HCBB4Ә.x֮][YV077֕EK뒐c{x":SFF&L@\rN-(([oqkݻUV%ZZZߟLHH߷Bx}ЩS2#%Ѽys(J\r{]-,,=)SNaUF{{{ :qϜ9KKKӰ@rr2y>}G~~>qm4jsQ]'wApppǦAAA1p@,Yk?͛Wt5 YZFFFFU{heĈ+n8qBj }YLFFnn.W^M<~>qD^ZU,L\\w{hff&T*}vǔ(?s΍7򔘘ȩS iurڵjQUIIIRx>{LwD;H'O$kNk($0~xv*r[4eP߿O;;;ӾZlY q;uDܹsF\8dȐ %#S^$WrJ6mTmVZ'N,3Jח.4iի olڴ)vJ ~/8c Gkժ3f|2׭[?fffB@9dر={HyI; ##p=X|9g̘m۪=?J̊Z|9pȑB^G%FS&&&Ã9r$?000BW&=c233he˖ZH TgCFFFFFF׼Q҈T.9իõv@ooo*:tH9k,dddI&q֭Jǹ`5K)~Ji5j(E҄3O7JE777 jT*Ϟ=K???FEEߟ111j~.\`F 4طc5#\UPvwww`ӦM&l֬ 8}t뼅ILLeS:uhggWl,M$WdY &VVM]-^ {ֻwoݻW#t޿W\C~-'N1cҒ={۴iCcccɫwmۖm۶o͞={‚0aNʹsQ.ܹs'yi3::jיÇsjС׮][]zz:O:E{{{]ر:k֬b=>3 Ν,##Ɛ& _iӦ )ZjE>رcG`&Ml-s:t(Th(fϞ]d_^^]]]9h kU*tuuK)^׬Y3KFFFFFFWQYfP/%&FVㅄMiYW+999^:-ZT &L 9b֬YbEeFggg4HDrСtrrݻpu˩Sx^uYSmFl޼yz!!!ׯyo^TNHNNSx5FFF)RDd۶m⺺vZā,E6N8Q(Rdݺu =##V{P+MK7771q666yVc) P܄̙3Z1\*JL5]%RP*U45/^ Ăxw}@ SRRJ222^-\j[j%N//rdu/^\dѣG9#222Ԟr&7YYYクJ7n`޽3_~233f_Q℄ -K?h+((; DWWW*JAu"zq,#####:8HSRRXV-hӨ?~<p̘1|wtywKL? 4ʕ+E4̙3YV-ktl?~gf֭D[noy)fgggϞ]$}'++KD$,,YR1=5jeˊDZݼy| x)1YYY; 3g^x>ߪK?K#\d$'K%$1}QsĚ=ѣ /.///ʩ Ϙ1CcddtRɈWfGtt4.\񭯯Okkr;DBCC9sL5:upܹ:ӓY>L, mNedd, x޼^i@Q=pڜj֭/b H>wn2NNNZ qhggW6;;Ν?%}xʕ %#####xcH͓>6Ӆ?Vㅇt/r]v/55W\]rJ:::mŊܺu+=<۶mKggg*IT$pȑFFFܼy3U*]\\ijŋf̫@<ʊı7oIĿ+KZh!Ǎ˗k0!/~)>|Xqtvv#hmm͟~Z;b R%. K`ȼRR)& ֭j^i5駟??bٹȱ{~qĈouVz{{ʕ+ kx ޽V̙3n$ ˷zK8m'Nݝ7]\\_ʊٳg[`%Mc }֭ " 2Dk`g zzzݻwTVZ6'h 'pPcǎa̘1l[`` {(lmm_m2p9é,:@K+T nܸF^ztӊwp.͚5ӊYYYzl̘1C|y//Ǝm/IIInZZ6ܯVL6D&nAA+=eGTQs֬YwܻwW2_4i3gΔi%aƍ"š5kD]SYY:uR~yׯ/*Cƍ, :RxxxT: 22ӦMc۫=NL>]`h-G,BwwIMMŅ e̘12dfϞ L0dkxyyֶĢ<<<sLMM驵P( ѣ5yG|77ARMO8NE|V&&&bÆ ŋ}7)) :0]br\mP˗ppp;*ͅ76 VZpppmuM9)j F&M@Dؿ?.4144($--u޷oc_u#@dd$Ξ={-Ə'''بbȶ8qD .9Jlĉ??3gɓR^dY>|| Dkrrr P@wZ 8qB111>G_4cǎЪU+Q<ԯ_DJh a!VB@P8r,Ya̙?^,Fm lذ5O+^haauU([H*ѣ]=p*OǎAD8{leddT:ÇP*(((СCQF ?sѽ{w遈cǎuDTn1eʔJ* e7n 77M  JѫW/v-§Yf3f +8O *JP PQW۬,?(#GvJ|<.]޽{rJ`0`0uT,_իWB*&&&%tׇLMMe0A\6ƴiT"66.\={ Jꊁm۶%D|E߿Կ\.G۶maaaQ[laСrwDFFb߿{1rHڵi4GEEa*M6ҥKK8kAcyȑpss5P(8qYYY޽ 95qDjҥp‚oi O>ĉakk5kTZ3##裏.LDErVVVXfڅgBCCC=zT清;w.Rir{AϞ=U8|hXl۶ CEڵUl믿7p'2@"Bݺuh2 <| kׂжm28: 6|(:)>>tRbVԪU fff„ 0evlm۶]j͚5lXQrDGG#882 lYXwwwСC%߿D=zҸ~:nfff7ʵ&;lR mӦ ]xI MΟ~ G(cǎ.?MjժU"S6??SNСCѮ];CBBd( HU( @* ?d!YYYj*SQFwѠAa4''i+} d*ԕq8ѣ"{9!!>>>7olmm1n8رCEX… Xx1Ty6,, SLСC/*萘]vaɰߏO d,'r|~~>F-Zp8՗nݺH{q2Mh޼9 }BOO3g\SXoܸPXn'*jtRT@<UKhѢp8vCOOzzzdۅySSS -Z(7[F4h@ *J#++ '_" ڹsg̜99RP0=]v;˗/>ӧ0aѶm[k׎粳QfM,\>}ŋlǏATySEpuuh%wMu֩ XW xzzttt,3;D_~EN:gQ~| " >BT7nNefVerr2> 777X[[cڴiؿ)ԩS,KHWz@Q6 <DT`Ç=ƏTfffb۶mՖZXXH}\t$TsTܱc*m+""prr+|||Qj׮`V BsO>2_5އ֮]Jχ'kҩDEE]Kp8M]T@^\QNDjˮK#??fee1LMK+CVV1c }}}b޽`PNDA_2*# ]2gdaʔ)J֭#// … 1deeP[YA}*$0::\;,x+GhA&.}JdDlܸD}7|ƌ#j|!{^zͭp8bHJJ]XrѣEx9vލ+V۷>Nqr9ޞ:!J1`xzz 6 3gDPPFM?E{->#@7mڤv\.Wia~p8C޽AD8|V>prrȑ#6#T.[X~7iG?_PLJU~ Q-s~^Tڵk"8é U׬l-iىP2ߤI+&&& "c-[4F||<\\\FӳDŖ [nʸT*@|rVJ. weiw b2N֭ƍhxO- nϟgϝ;DzJ" zDJ/2k׮?K-oʶ{`C??J+0`֭[}ʕ "XZZS^xm#dx-ׯ_nnnx![8p@sYp,(DWᔎ|E-[n͛75փtZjzUYKTT֯_!C`ҤIZLd2ݽJ,[  † xB.\gϞit9s@D*,Ɂ#ZhA9!˫JΆ?\\\Я_?HR <K, &SQQQ2dիWZ((('tK5kT&@*Y3%Lƶ[ZZMV?lziR믿V(Y^=Pmݺ5+oѢLLLУG pvvW_}%K`Ϟ=:x,hcvQŶP*?qDѶBuՊF7ݸq#rss* 8::ڵk!4O*"Z DENs})2*tlڴeI&.i\/DEDmɻ yRSS www 2Ƃ paQ*֭j׮Յ1|Q8#G?;lmm!ru&M/AAA?r9vލAaݺuצO^9ο>sssDC" 44T{n… Y5Z͚57)ϳJ"§~˗/yΆ @DprrҚj bA#A,--'O,ږRd%wwwP(z*+ Ó'OX)}rr2+/++gϞ "9rD^b%:''eSPP&]bu("77uD{/##u)'di3lY\O3{k'77e nUIaa!=ZBaÆjw?e?99}9m`kk "Ž;/,,DDD H0l08;;C*%y 5jhGEx9֭[;; D4irP(QFaرU5j\]]+wLӳrk֬aرp'wa(O, lA jMߏ֭[뷶FDDDe.AD0zCTÿ}t{p#&@*8l۔)SX&]?"Bڵ[=?Ԋ8y$/jW=zľ:Bf_-Dw3޾};xY.\eEFFbߏ?F&M0urksZTM6 "?~\ehԨQl ׯclVW^Ǐ+聉ep*|rdff"44?~<OOOAP ##}EڵqE^a5QJOOL&5RiYHJҥK5zV dffbհE?aZpꫯDT7ˈHR>>>jJj!22\* ڶmUB9TADpttD\\hAAA/q8AՀ/͛7f͚4{l""*,,CрyYf̘AZҾ"hРeffzRz~)99(99233IT_>}ԬY3jٲ%}dff)ݻښLLLvooo""ruu57""jժ2wݛGWׯ͛vSSS u_PP@o޼-[mDDٳJ,GC ݻwSPP9svvM6Qpp0Qڵ˴@'O@Zj|Ԕ>}Б#G֖Ξ=KO^zӼys(,,o)aڵkHוCxF~Z9קЀh޼yDDS~:y{{ӊ+ѣ@ ggg nݺt15hЀ233ÇԻwRݹs'Spppω3gիO>t1jԨF~) ΝKK,!===lEÆ (==]k69#776nHEDDC85kgUڵ+uڕחFIM6%rpp \.'"mۖiˋϟOo߾5jЂ zi7o_M7o$">nJvvvi֬h/p#GhZ#=R)YmqiV> M1V\Y1H$X|9d2peDEEUXTi۷mgewe+%%ej+qe_xL&ߔ{{{8qBe[X969z(͚5ԩSydE^WjKR2а&Ҵt+˳gЪU+tԩƿAvEU !!!főd077L&ә?fff:pR}ݠ[n= 3*7odRGe3l0v]vō7ӛ7o HXVnݺu!J5 T*{ƍ9y_gXllԩFݔaƌ Ju "!;;m_v-={mK"MY/x Μ9?K.ĉKim6pssرcEhFGGpB}lYHOO? IDAT!wQ⢑sNWQ"""X7oޔ jժr%GXpUԬYNNNZiXWL>DWFHH`N< {nXXX`Z+/ AҥM6:˗/O>lN׼ysرCeaE֭[m۶ >  bQlܸ{E`` ݻWEm˂`E5j?P}Q(bӋifh 6 ˗/g+p8潗oP(ܜCDD$cʕm]tBCC;+1%௿""ǫ۷r""޽{Ioooڳg瓡!k׎ؘؘڴiSÇ?fiŊrJ[HHM}>qF$ȈH~)Lt ھ}{Æ :u͘1C+U]R(66Ξ=KcƌQ߫W/jٲ%%%%ٳgٹL{U8]P~}""Vv GWŋӖ-[H"l,m#H`M;w.5i҄9B7`Zv-1BBBаB~n߾MDK9#Ϟ=%KБ#Gծ][HN:љ3g(''rrr4ȈڷoO;vvڑ uܙ:wL:/%kNe\.'wwwڼy3.]w2)pu3gQ.]h֭)((7i֭ԱcGZz5QJJ hB+>s8Sgt6## GF>`oo_T}rOu0m cΉl %Doo4.IOOg67z)rssEۑd6l2\7_k=KT8pLԮ]D6ti\th+2_?uTf̘!ޱcp8`޽eDh RCnݺxIܹsغu+Ν ڲϢE4) A~]v / Ht+WuUiܢWnS@S=HOOw}zzz7n={V9>pE>>>صk֯__~%Я_?9[Y5kSNptt;ߏ{ilݽDۣGгgOǴim'TH$V'Wt.z%XZZKeoֆ ݸq#:t`v<`e˖u-AOOוˢY~=#G17m"G}JEתUKkp>d B"MmifJJ X0006.AY5[ ѤI "lRȑ#<333 Nxx.\pB@G:ŋ022eeYYYؽ{7Q^=kРw>} @͚5}~ƒ*l x|WpqqљB@@@ً~K.hݺ5t2>ؾ};7ozSRR2nRRnܸGbӦM7o`jj5j8544ħ~ɓ'_Epp&L4 DT hU~}KzA\R>re3gUߣAj%8éB~"A& "|pWcl"O?DkײmhѢ>>>( M=mj|nFFeѩS'n&mӱcG:]&#.BkiM nlVXmw9AD0552KXp!7ngϞ|R9)))db3wիWӧV*bڵkӔ|_yNP*8~8wݻ76n܈05~p8ڵ+ܾ}{8pgϞ!88 Y\.Ç답k=z`s!uf͚~-/ܾ}gΝX>} &&F+~1??CQMEq)̞=8{l A^a9y߼V~}Vv"փyz!Ӕ͛70lذ P(ٳؿ?<==tRL6 NNN%YI|58q@lUT>|D^zit^LL qCBBШQ#Znr={6SLQٞ~ʂMLLtfXZZfADXr({6l_~t)sXб*P(իڶm[555]6舐LNNf߅h333xxxKCqEXXXSg'O֭[f­[t2=˜1czA*yO}^;w 888vBCCq6,Rcƌ/Q)$`۷oUԩTUqv}}}tҥP!q޽ p8NU "|7l++|h[ǏaȐ!pBo߾}ؽ{7V\yaĉ |=sŲe˰m6ˈBzz͹s Ȏ=D3g]Dmڴs R˩&Y/^D~vJPw/^]ADJn6l(Nrr2vUjk|U̙3 "wǏǏ?///\zUgͻ(RĮ]иqccԩ .O>իZ__رcGӦMaZA 11SL+j[ **IU&nnnzk̞=`ƌ~۶mÝ;w퍤$$''#%%wő#G@=z=P^Y婩plق3fwev*ضm[}K8::jTy./AW3p^,Z|"4H'OťKt ]|96l؀{"((oF|||r95k"ѣGT^|YL9w\رf͂,,,XPwo~O "L:U/RdC5YYYYlnU4D7S͸{.vPSX)Ӷfee0nnnptt#ƌwwwd2 &&\Q[[[nݺD&M\\tYb58Y&^Ǻu*U.J۶mΪ70|f``Yf*Ip7'ND:uVGJJ Ξ=… 8z(,||2J%pY}aŰU-i޼9BCC+=۷ѿfWXX*&O,}H9oH]\\@D>|8۶|r פ_~qP*Ic(m46Yt) ^m}FTT?NKu*$ Ϟ=c `P( 0x`]ߋ/Rzpϳ{:NSSSL>|Fvv6>|8z(ك+WӧHNN۷oq;w7;x/P*F˖-w9d<~Rv0h i\唚|)Mp8FH2nݺؑ#GCTDE]B)ʂ t:N||<[-y&b;`>~J_&N;ήԮwEADhРAojR,X"_|QYǏו?mVmZ(enذ싼U#G;ݻ?x(J4hDTfv.*A 6o|400… 8N̤P^~ W^j&&&pqqL&˗/@[׮]C@@T*ϟӧp p%LOYDe?ȴ2n8YT_[e={ }ŕ+WԞ###VW}w/^QQGV1B26tN7J.ŋEtrr׶q߳lK=6>>QQQZda/?3r9pnnnDHH |M5oeY= bc=tkpBˮ]+@"0usso޼Gp1=zΝC``JAFFΝ; ۷DPP^x^ՑT*j֬ DJ,XXXׯ3/p-Ԡ*ۛ^zE|M0޽K?&" ~zͥ]N ^""-ZmCqqqKqqqp7VZGFF˗ˋOhO:EDD &dggQzT|͛G ccܹk*,,}>qpp͛7SPPMˋbbbHPų҅K.ԥKS~Ђ {4eĄq zݺu9B A:t]uڕur0110z왨hܹdjjJd``uKөv$JiѢETFNQ8DDj͛71cИ1ch}%|2]rӧsNڹs'lmmܜlllM6ݤIjҤ uܙ=zDJ^JRBB999Qaa!=z6mJHM6ZjN͛7W4w\zYZZ֭[k׮Ctڷo7NiŊYڴiJp8NQݻ JٶPV]K͛7,+";;۶mc+3 ,YTݻwÇҲG)))3gjԨ 2ڵkntIB̙]|957IJJՖr R6O}&MhT "[v2336oxxͪU@DV?L/^lXmɓ'pppPi矃0m4vpu_pwwLj#`oo''',Z{;w$s(>>!!!􄛛`ii1c@*.Utww=;w:x-^UIFD~i|nbb"|||33٥T,Gܺu OƮ]vZ\~W\a^Caa!˗/׷RM4!::f۪U+ dgg *>!*p8}QeҐ@__D)S%蘚{$%%᧟~BfT&kbK9ڵkk׮Ο?6m@__...4iFR9/f]ÅN 4}D>_.Ą}SLщ]eEݺuAD%;"B-D5lllhJkO<eO>A&i]@ڵFܹs[L2D޽{ؿ?-Zaغu+\R&GPd2H$888Z%p&ZEj*۷cԩ:k̴ Ht"pvҲҶbcc兩Scǎ%O1o]qrdy nfoڵkٰaCYmW*hժ~~~j@Dغu(K,YrG9'W%ZhR1aA{My9 J1rHaܸq瑕qՑgǜ9s0daرXjU JB:i_WʜH$ҡC\pA'cq8֭[ATa]dz ScccS333aѶ߿:tW^ťKp]cΜ9} 2 ʪtֿ?:tB>|e+*:d2sgܹ "H$p8.itt4r9[ d%y "kNtI8p[n?ᅢ`nn۷oUVhԨΟ?F^˲V۷OXnOjo~~>bccqe>|7o.źu "\tT{yyy*ɓ's "_+ޟ}Y}€D=bW^U2f͚v=tPQ.]ߑS}d_>uԁgN{{{q?d[4Zb;;;̛7~/M޼ySfzA7qƌ#:GLeL܋E N~~~\˗z*vڅ={ 88ΝuR2^^^:6m>J2 ÇW[&MW9)U 3gN˗Xl4oD]Wf˖-ڷo:j\DI&UVZ8t>CCe DN\!Hh" +feggÇ8{,zj̝;Çk^ IDAT1h XYYK,͛_jZ˻\.WY:?zeź"66eBǫ۹s'kuaȑqfjj "x_:t-Fڵk,cZL\aa!4i"BppEz[1ϟK.\EP ""2 &M $ uV r-Xj VQ ٸ5Ҫnw $97ǎо}{^zA"G۷oq;w'O/Ο?ׯ3ܸ҄qCt&iNNR)@ׇZ~ml2.]*gϞaС(0mp8y۷,x(ܹh[6mbU%^F,K4%++ ׯ{ߵk3ɶdzRwKJ#!!MW\ɶ/ppp/`׮]8u߿QYRqauwmlt v[l??Tٞb  wޅD"QQCMjj*= ( hB q@D7oqUu^F /m6١f͚%:O'OVi&1C r?jjT4&Ce˖,,peэ9οT*Zرc ԙ/ڵ+7`*&@[DFF?.*{VYD6ǎ&O,*3U+P(XWȶp8߀k׮eϩl◜̶ggg,QRX&[LSDWMgΜȨPRfaa{a̘1%& D°SDmۊ $''ͨp" ӧC ѪFeNN0uT4mT[Z0tPܹIIIZ,J%_Jxd t ###nFׯq9lݺfBϞ=Y5$oaܹ+i:+// RlٲFRchhয়~ٳg+Tٖe˖N:,;gΜ ?Di_gff?| j.KKKσЪU+Qs8S]iT0lР[is\ZJB͸b ]{aʩ{^c={DEJ"޺ue$&&%4!";vL'11%333Yt"ٳ߼yHMMŭ[3@FFXluZcâ85򍱣5*ָꫮƶj4F]K]j,آk.Xiy~,[-}>5ץ;3Ϝ̜FF544fNgҥKH$ ~~~L'W^e#SYPQ믿::\gLBBvrrœ9srT/4-- 'Oɓ+˗#,,,bȌNéS0j(t 9vcǂ >^B`_xL8;;b1c쌓'OZ3::VT*Ur4A*N6֡Cpc+Tj(Q"O9nڴ D777Z\\ի Ͱa@DF7SL[X,joOJJ q=\t Z֠2,,%>t:Qr4ŋƏ@dd$kc6 Toٲ%q4oܠJA*#Nch4Zh1J[nDٳ_ڵk;w.ɼ4k 7n$$%%o xݛ]#FXLٳUTaٓU;465 Dyg޼y "?~<# SWAL422~~~lҥKCRXQC0r 6l؀={ĉFxxMMFg f9v>}^b ('LX6""`ٳg{ZzVk PX1O_(hw%! }.3">>%GK.K.vHFz fRlY! ɹhgv2>˕+g6ɝJbŊ{T$x\p8ytR;vrV7ȸwLzz:]e˖gϞ(^oǗ_~is;}jj*j5vݻwQBT\Ϟ=÷~ A* j.c Jzp8B$H###Yu.1_f!.QV7XRJL2(U4i-[,4ϟopFFawRPH֩S]rAThK`zc "|7۳gk 1h VUmJw8cĈF B=~<wp6t2tM}A&$jѢET*M> e ,9.w(j VrH,_… rz|r,[ :uJr=ӧOwaֶxKr&i+VPbE<|3gD׮]sNRRRRгgOܾ}۶myvp8NnbjҥKP~}c NƍstԩSm x ]ףVZ쁲e˖ذa4 ֯_Zmvb... kׂPNщ&)B!*Τ$ܾ}w1{#-[p 2Ԝ;w%tݻU[ Xe68Z*T*U ^^^&? h&> '/(CaF%Mz@9!TM)SFXBd ѡܿUX˕+WP~}kh͚5lA=bmb+_ڼyxT:@*Bј{)R)u`ǁpB 452.ͺ-[ih"b:Oax®D(JVMadN%Ye#GLnӧO62ADh߾q>Y{8z(\uYfMiӆ%,-Cݺuѻwo̞='Dxbnݚ Kڵ1m4:\.;z .d-Z0hJMMJbcϟ2LԤUpp02$>p8MvƄ?UܹU@I,߿?n^? "zh4tѠ*޼y'b͒޼yco( 5LJJbw޵9vpr &H> "B"EPeZ/_fyٸ#s-LIII쁳z8uvNj ^?>:u}^z(C<=55BBBT*Ν;CT"((111ٳg-;BVC&A"0##SKШQ#BP 1,rH6o "N6laÆ %ٺZP*(Y$n5F[oA&\~z{Vf-οiӦ(CQgggvbo߾eɶѣXz5N ooo4hЀi0ZQ^= 4JǏZdd$֬YDºB ;v,>#2Ybo޿A2V;w7f۴lAAAٲe Z\8 qQӧT"˗۷/l>͛71bhN ½W^EnݠP(ꊰ^ ..χD"ӧMn:XdJ*'8Yi׮]Ad..\XPeםlWl] R=||UekÆ Xќ-y۷oǰaeZh4( x{{vFTJ*r #zHbbb̊g^6HH$߼yBŋ16m Fc 020ETTumfs\σ B]Z59(PƎp}/^st:?~`C˖-\3/5j@߾}dJ~ll,n XK(AaΝ6ry*=zLˇe˖T*eRJARYp<}tFmp8σի0|5 ҥ гgOt6l̙uȑ#>Nٴi(۷t(R/_n̄C&1MO$Ps1Ç˗/I *J ;v4;r[dß188,AVh؇7o}> z=kȨ3 ѣكixͳp¢3kktTTZ-T*KTX*UkWT YCA ">|ge=)նmۂ J,$$Jz r%Ɖ2N8aXٹhD]Ԙ&MaȐ!V|HKKc}}/2իW[}LN0ߘ7ozJ**X ڵkiӦk>|}aLXX *=z`Æ q1?6Gee˖:^iMadĉ0j|ǎ˗/M&޿ha޽_1qDt^^^?~<˗/7y?]v9˗/1gH$l_~ޢ[Aa֬Y&>|JwwwD( )|||@$ތp8cǎk׮A6GeR ..5eZlxӷ~ ZmSKFaZgŬ:Bo=Tqqq:t0V̺dڴi":- "hPױcG޽{~~~,oooޟ?LfС"BϞ=Ewe;wfv8v} ҥKzˋ/pa,X^^^r-Z`8zCiii8q&MddD/_>tVs uXx1i֬Ç!HX\uԱur 26p8 ['~p TV {6Z6m ͛7NJ+PJsZ%C45 :wWfmZZVZN:a&+Twww111x~LV4p>' }-k \XpVN*UPB>][kI_kŜDp}/RVݻwYRڙ3g6vUCUM$O IDAT$ ]&j/^4{ަ#miNtDˇӧzox_pm]~~~T0k,Ybw5z\zsa-ZHZȌ b ?BP @&ٝ@~-;^tt]cq8bȑ#Q|y޽իXbh4HHHVB@>} \{ήڵkAD֭]Ur\nѣGu֙'uuRiMNNݻ~Iܾ}.|IEpC#_~f#͛xn޼iO… cʔ)za}Ʌ {v튅 Z]MYc`ʪ *[DY=tkwbŹN_|"-Q͎͛QfMɚ(5ך,{ϏUR=<2M6f>CE;wMۼ{UVE˖-EUY}*OAxx8֯_A K qOjDq=,^[6]6O@Q1 6 D ЬY3V*̙3LDw!0T\9p&_ŋ2&˕+ *NMJJٳgpBx{{GXx16իW[ics"C L&61|!ܠ d2ATZrɎ7p>'N6kHZ-ʼc$9s&cƌ1^CRO>=nJJ K^5`P)9c {3_X1Ђ QFȒxG JAD={vn' &Mdnƌ `ĬY@!]qxxxK Jj /KdW4͵P Z&LΒL<D%*>5:7n܀RDT\ӦM?i\ϟ?իiP.L;G1[Y4n8f͚ ,Pl.]GGGϏ}G.]f sZd ¾:uN:"E^zVݛ~ǎ鉡Cb֭9^ž` fP(1bD߿?=zdq"##RЭ[7d2߷۷oc;A&b֭&h00pqqL&S ZNբm۶&iMpBv=2'XTl[PEl.쌺uP(ؽ{7/W\aU }ɱv r1<{UF 'otٓ 6W^(RwhFpB<} F???#-hx{{ߠzhڴi "L8j%Y4h{1&o??? .\ZFܒd6?$$$%{޼y"€PhQ4jnHlڴ F׮]xbܻwϮ1M!S2Ǔ'OеkWرvXr%q)'%%A`۷/4AǏhЯ_?HRJ]^Ϻp8'/cwUVFU{B[pٲeEW̭EL&ӚdBqٲeٳgpuuɓ'ݶZj "ԭ[KZ]m'e^ xzzbԩظq#]&J0HOOGDDN>M6~1cлwo޽YcA㪜۷,U0==XiӦ "kZEB.[U1U6maÆ&xۤI֯_oswfbM/rD_޽A3qOڂd<@*B  D"Jw} %a}^۷o3}mZX7FB\\mcmذSp8yׯ_xǫR 2佄V{G@d2t2 Z!fOԢEDhХK/ceBTUlJUH YYd 8{,ߖ'ο"e_1cư) ys׹?{l ȑ#8t "tҰ0 rܤ)"""X BB-p>?~>u=z;viгgOt zԩSj*jׯM>oŋ>N87BP`РAҥ zyV@P2 Vi2 3]dd$umKӧѹsl%i~ajFqA* juy=-[fС_`ҥܽp8=6'H#""؃P*˲\3WFE)|||@d_FvnNx4x}ʔ) m۶Kgll,݋ɓ'SN̙3w^k3>>jJf ޿J+$F흈p8'/asT0x\7`֮g*)esT#ZE@DjPX.\^OKKcv5~bb"Kb>ܪĐǏC.SN޽;,Yk׮9DZA*W\2L D\nqu11jvl $Jesfzz: = 4;E/TNO:զ8?Qs;?` ,@RRRt} :Fƍguƅ бcG޽{[/|J*er}ZZZN'''1btRiҨB~v홍Luz(JZ|ٵkqoܸ)SK.5E;uZm6əR&4Rk_6%HPdId݇1D"1Gp{qa*2W-[ "'N`z> ر&Mbڙ˔)c]@@%K}CT*5EX,o޼֭[1`tÙ3g۷Yv^bԨQ,sѠAcƢPBV}*ʠFP6'SSSvZfJctlĉ " :TԸ 0\*/]$jDݺuÀw_Ş={pex"+AaΝŒPzuܹGL2 1jD\\d2 d#!C@Dm}.\ky9rGD"Uҵv 6A;ӧT*1n8L2d?`߽{ZΝ;C*Z4ҌRDҥ ~T*IulJT*TZ_Ν~@o߾mÇYжm[ZiM RGcZCbb";7oZ7|SN!11iMfN/RT5j`745k֠K.ªUaט9g{!ZlAрPjU☘lE"ɓP* EHl4 hϘ1#GddWصpTBpa6d=+=z3=z6l?#F{򂗗Ə+W㢌,r N?`cǎxAΝ;L?Xۭ,L!!ijsNp8ySN9SF={b&;"t5iuftq)ǣwXhk( 5j(p':AXҥKk%JaÆf}d2Au [Bh%[\O#R ޽{p̞=D+r޽{"EpyQcצ6n܈ݻ{ظqc3J||<;2B1E||>3gd...XzuTn۶i)Sk֬a+1í6aDjj*;p8yGZ%ݻc0a{,Yjj4 V˖ . ((->DXXg@+WrQRD/KRJH;wgϒR$\nnɒ%4{lruu@c$''SŊ)>>=J^^^6Z|9\>~HDD[yQ^DIիW'S^6{Ҝ9shѢEَ`k׮t1NYSV-;vEA'ORҥiE }dz*;w^JԢE JTvm[O*>|H#GK.D"͛7SժU2ƍI*^'wwwںu+_R&M֭[lvtZf ;v/_N5rH\!!!ԴiS*X  ,q9NcCM6۷o[o||;>} TʌɊgZo̙ "ϟ+FV׳ɍt.Ϗ]ve+b P(puuƍ`ظqc?Lwر,f*Wl`ݻw0aFi %rFne˖- "v(VG9s ]v7|ݻw| "}XCӡJ* "l۶1>yd)7T8y$%J0kլY3֮]+*QF:C2AD>| aRT@WB:8ViDaf.ixx8ʔ)" >-2EWۊ01T*s8'Q "xzzv(v+WӦM3 \.GΝP(NzJ%:v\.4ɓ'F>͛7Ip8+111lQ0;vUe2Ѻ;wחooo!$J3W^DZ_dI[8}4KTTJ%\]]!˭ Ő!C :\]]qq[p8Q d֢.%&&@ "tŪ0p@Q_|^H}:||| WWW`Xf 9;D '@FrʱĢ\.7puu?KLLdMZօ1cd;VF@DXzuNjĥK > °vZ֪etv9"xٶs$''\!'KQf}aaapqqaժU_gϞ 9[.^Ⱥ/ҵp89byVu!44_]qFP-:tP|7J.8p󆻻;N:e)p8GTTQ/_ދ/еkWc &szRRJ,iUR)**7gQڵ ;v˗s̈S!//^???v]ԩc믿Ah%jv߿D O "k.'5ܹ3 Đ!C{822~Caĉ#$:t9yǏ `a*YXx1Y2*۴ic*TsXtZ_9!Ұc7523+Vڵk͛"ZK,.]ƿ~AхPW~8 m޼9Q"6jȪ?D777 40\f+o߾B`II GeS;v` 'Ntbthܸ1T*UF!g!СC<,K,iBNNN&gϞ "B޽?E4mڔmcccA@HHԩ"6'qA4h`~pvv$:,Y*r!?/e &4iDCrrEpJ&mR D"Eϟ-[L&FAllln-֭[ȼg]uYW\q/mW^F-!UVe?t FO7jVVZ%JlFƍ3&Mحٶm[eid&**5܈[l+OMDDn݊ɓ'uA-&A3/ FʕѺukxyy1mX",?--Sh -g\DuSRR7bرp9%t:k/Uي;dQN8DC:2\NC.nӦU%%%}(\p׎$s .@k`tJ~9@~_ɓpssѣGm_| "={8ο4AR\UXˇFA*Ϟ="h{uy1'''x{{ڵkp8],&H=zGW^ˆ˖-}?* 6Z-!0J^~ \Z3ߐԮ]>>>P(c >k;n8uK.E*N ,'͔CѰaC)S}e(PeʔA1l0,YgΜaɸ$rvM+T@dd$;u{ JӧOcRXׯf͚FIsG{y6o*A֭%J-T*UmbXb.]Ī>}j`d%peԯ_kCK@D(]î%?n+223-RZn TkҥKHLLdbpߛ\clYŋG'A d/_uĒ&w7ng̙VWdzS]JJ T*zi ap8[ ?R)ʺTT PT rhX_& ޽{7p8 RA/Ԓ?~^ZN×_~ "_1XbvU@.PBlܒ%KRJfNNNS{eB͚53?88]_6mbVSS%J0Fj. ZjUm֠4 kll'%%󈈈0$LMMB`JŸi9mٲDַC-@Dߍʈ"E 44l!gPW_ݦf͚ "ܹSԘ={a޼y ؽ{7+E?Ȟ;w Bǎd}N L8;;C"(ñ^5kD-H$(JGNcz >D///Qr8^xFL-[//Qn V9k<Z-ʜ5%p81M޿X(Q 2T!&&87od 6+g-+&LQ6֬Y Vο3gΘ&ܳg<<<0bh4{8۷olu`t$,Z춂9 >@P_~J###ٳg#11v:~-pY ">>Zr:t`VE2=V$!H 7mZ-3t"ʐp0 2d2YƍK۷a9s"ue1)S "oK<{ 2̠]v`ۄc駟0`fNQD 4TXѪ wb?~<\]]QjUEE51c`ժUfu^|i #::7od$$$w5hOKKRd׻DPf3-- k֬A۶m~u>ĺu"… E_?{p5{[vU۶m˩pE#LD\!SStt<@hh(.7Ett41x`t #3-aܹ9ra{’fufOK)ܹ3>,zjnnncȑo:8DBTB"H"FG˖-!ɠh2,JAD>}<_(X #p8G&<`mgΜ 8h?@Fkwf7m"Bǎ ~̯\ ǏSLjj*QNsٲeP(u;ӧO:t'&<Ȓ~bgg͚ŎsuB%T*E۶mNu 9b3--(enUtxѬ۷ *RRRp6^ŋq-$ZXXsB'"Z|O8qڵB0htPTpssChh(V "p([,G1T]ɓADd+Z(ZjJ*[ZQdIԫW]tԩSm۶l+޿ׯ#55IIIFJJ ޾}#55>|@xxAK{zz:\b`ӧOkp9DDDmz=T*wqqU̶>| ÍBBB HpBTV}jJ{ҤI&wDڪ OJ,I,qQe^K̜"82TNBH8vUARW^ѣT*?y\ժiR,ID+Y+5 3̈1Kچ116bd')-Mڴuu[n s}}ߟu=s=ikkSLLL=}6ڵڲ۶mT:t(%&&8T=@EEo߮50Ƙb[Nz333#/// _?qoWۻ+̩A3f21X# gϞ%"ӧ ?CCCNB#GBMMMV+R  n׮\^{/!̬r2jݺ5 lBhU]7mTIkTk۸gei߿O4c !yzzEGG/++v%&&RHH jkkOc1p*H[p>}*dUʕ+2߈JkJJJ( @ :t@~~~}6M0R'Wkn޼ FUݴښMF֭ε***ѣGB{ɓ'TQQAED/2كK@D"K.ݻwku9sʄ~7Uf%9rlll(00P+-- 6P~hϞ= M0Nݿl|||k ~Qxb=z$d+YXh WQQAׯ'HTev]RXX͛7lmmJ>IkNm֋/ҠA;5)**aÆ [FWf"55BBBLJ*~(5dZr%® 萏桌1k2Xs[(!HGK导ݱcG 2"..ijjV/md+4_EE7` #???"+++Z2dP`` EGGW.**ׯS~~>b*((DZ)99Y܇ YD/:$ #!Nz{A׮]rqq!EEE@#*{E~ 'V^-֐!C'((PNaOں1={$6z4Mz/af hBKC"[_3h̘1ԢE =Ņ222S^^N111^BJk}X, ҤInhXXXHC 2؛ 33~xeeeKKnnn2ٱ‚<==iɒ%taOaaLTQQAqqq 2 V)>>^'O޽{/)))M!!!Զm[^tͥs璛P/--mF'v{mFF5P]f`V;ONNjyv![w}G]9@\*k"JKKIGGvQ"277.]j'@ii)ڵ\\\WFFF4}t:tT{~II /Lͣ׽bccC(((Veee{eQ444*MaY~~>}e1c"!;ҥKDD'f-ZdjjJU7gϞђ%K!!!fIk]YF~i=MMMYFxly}9 IDAT%''RiUulmmiƌGhƌ:uCuHHߟN:%sMH$@JB=AEԗ>G}$5$ $TBtc %Զm*@ ͙3G9,YBh\*k"˅@޶m۔:wBBocϞ=#'''@JcϏ\]]i̘1m۶3Gk֬!GGGo/eK._\8XL| ͙3V%u222qZU)c1c5 "!j<{L ID҆KmgϞr/_2/_Oۗr-Q^^NqqqLGB uޝ>CZ|99rԩSԩS'aܴid,IرcǧRϏD"ժ Ν;^Cfrsay077<:zzz5գGo~T_ UeLMMSHCCC|{{HˍtڵV%L1vXՕy$%%9ijj֭[FEAƍԩvYYѠAhRU&k5wzz𾣫+|c1co\vMȆ2m&l 'H$diiIh…&ח'߿LcYf𐙣Hmz1755UZxRudggә3gh4}tׯikkW8 []zұH$1c*8#0aPoufe)//Yf P˖-iΜ9H[l*")>>FEյCZtYYYBWE2+**FcVrY#'ܹSsGFFNdddDmڴqM( ۛj]T"Б#GhС#NY-qqqr߹s\TJܸqCe1c} ݺu#sss㢊 =zH/v7Ѿ}?7Y }.C!md``pH$ %$$иqhŴguVHև sݻ/^L|LmSkkkThĉ#7Sjݺ5_BZr%999ɓ'mxxk_<{V\)Y+VM6Qnn.;wNFW&*}M5_~EwP鵭^Z9LB>S2YY j_~dnnNH[[[iA[nѷ~KNNNtRJNN4vXVj32_4h za|𡰛nݺ2c1X˵4&Np&+++@ͫe7 l$:uTzׯ1bJ#ԻwJǤLJFA$777@Vkx}ڵ+GV:v%6vܹFo߮$ Ѐ( *** icccoUXLҿ|{9S՛*у&NH}/|RU+VrrrRhΐﮮ]YӖ%xY`R޴i0/ڼyR'zPXX}4vX:yd_QQQ4n8:u*ݻw#HW;v)SgBrrPkаTc1{S.]JVBaa!@$UVͅV^ٳg*'OĈ#ظʱo#-- 7*s޽{Xl@D4iv)Ȁ1b1nܸ޽{x׮]`Ν4iBA\\;w //mڴ%,,,`ii=z@CC@ݴh2MDXnN8???4k сxw0|p޽ǹ|2o߾X`ZhpttĹs`,_~/5ݺu 6l2~]]]{իz޽{ҲAjsHJJŸ>D֭h۶-qMիю;0ydcG!^k޴4`nn84kLI,==8y$ӧCOOOcccVKm ?\XXmmZ͗gggcǎ8}4:w\1c1X@"LJTTTC| dSTTHCPWX!Ǐ͛2V^MG~#4i=~X?@"D"ݹsGiU222hܹR뭰i̙&O\iI={(4?UF"Tʠե#GҥKɓ[e~%jeee"*}A}äך+>>ڷoOc1cBDD$ ޸qsAdd$^^^صk?~,Up!0`ڵksEa8q"*_|9-Z}ʕ+2zYϯ$ QRR#G⯿RuՌӻwڵM-,,н{wjniƠ qU|Xn z* 2ۈGvv6bcc+b1֮]PZ UTT`Μ9_s΅k>S&|wyr3g͚͛75ιzj|CDDD},5O>?s!66bXxfb0`lllн{3B׮] ƍظq#cĈ[uuu#!!uƍ6l233annSNH+e1cFjTZCB։}W2`TUUiDD4rH@&Lhn6Yz+H$ԥK@7n9v59LKKS豤ԺӰUc1cUe˖Xf nܸgggc8}̸\\\гg:汾9#c۷o=mڴx{{+8ŸqSKo4O<tСұ | Y_}r.)lĈ'N=޼ys8::;М...>۷'lDDD000$%%aΝ7o֭[}=gϞ5uhhhO>C̝;)))J{ uuui <x}}}1c1ƚZXX <<!!!055B˖-UUU|&yBPؿ?`ԩ28qhٲ%z_~ *cFZNʥ;w."""&:'##SL={p1[n 000ٳgkX#G\xrǸ}ĉ¿ 殮;;;̝;!!!HOOGJJ 9___BCC8<~'C__;w7qUH$_f0j(pww֭[6w]|aaa2_1c1DUсo|b}@rr2@PHHadd!CȌ߱c`h޼B!lff&X|A#x{{cɕ" V1OѣsĉaMQ=бcG2j(|~:RRRjl2x`hiiٳgr lllk ־}{oFիx".^(#))I4^߿? zzz nggg8;;+m>MMM/v#(ĉ3f agg -ZPzc1c1U R'O򞗗PoOIIÇ]̛7R P?6l18c… 28#Gׯ.^&`f߱cGXZZpk޼9("`oooDZZ,ϟ#,, ˖-+ڴi#eƾI+))qc0zhǏ(c1cr&$$''#22%3688011B/^9sT;pwwGiiiH%%%!88aaaѣGݻ;w.%w?&O2 6 h׮ү՞4ϗ;~w3{4tʕD~~>///ejoo===aK޽{ٸR iqѣQRR#Fi%2c1XRx=cǎ ݶmvvv޽Xɓ'C$)4Шv?N:a㏱~!dgg0qϡӧOC[[[ ̝;7n-[/廉O>,j chٲ%\]]j*Ç8t<ׯ^w5u`ee%ܗW󈌌իWQPPXnvvv'lLiu_$ bطo51c1vAzM ǏC"AW߻w.\H$YZ(3g_}1ob1? ())Aii)кukU"r&yN>?Xtԧ*)))pqqALL g%klmm]]]={vq1ܽ{d-[5ƍ ѢE #** ~]LKKKqUDEEŋ“'O*eo߾8p lll`cc/jҴyf̞=D & 001c%$TTTL>""IIIԄBرCh$mTׯ8 7@(\t x!>|,WSSTTTвeK4k ZnZ9O<RXd p1iӦǎȑ#iGZ7k}:6oތf͸o#c1c߉JL2w2x_~```^zi]X,*?~;v`݈"۵kW;v;]]]֢E C[[[.җ3H0{lL8QڡF<֭[ TUU3$&&Vjb8;;ĉh۶mQ)gggԩSXti}-jX,Fbb̶k:88@ǕH?Pw̙شiSjc1c JvO>DfII @EEE{^^"##ѳgOtQu aaaX~=N< "=VXo߾T֭[Ca۶mҥK߿&MBII vwBSWR8qµz t?-[*m͌'UUUXZZ3f!::Z&˴Puejmm]ˏ~ _5V^Wc1cMH>WXj . 776lBX"-!..v£GaÆafIιM6X`&OΝ;ב4TSS#F!H0tP߿_+{3 iӦ#cii CCCٳBvco"CCC5Yjjjի0uttDN@x={,.]U(k1c1 ӧՎQUUũS*m)))xw/{9;"-- :ujڝ۶môi ???”L[[EEEXp!Ǝ{sBɓuVVڻw/>Ctݓ; (**˗amm]^^^ lƚTDEE… µkP\\\i) $DEE sp1c1dq5eggeeePQQ\TTTdKE"|}}?V9_rr26m¬Y}ݻ۷o1fϞ @qq1._ GG׽זmx)---Ŕ)S sǵ\Bo^?y1eXXX ..Nkf+//%N>|(wUL1c1^kpBt 2]g̘[O>HLLDQQӫ4ĉ{nDR7oĆ 5V]7\r͛7&Zj 0667nO>FAAAsssٳPQQ 0sZ?.kzt#$$rH$zxxСC5Ι ###/jݾ4DEEŋ8~8'`֭4c1kr8@]믿>}N:Ǐ 6--->|Xh0#ղeK O;444`bbSSSOSSSӱn:={]sss3塸%%%ӧOTzߠ IDAT#77"ҥ vnݺGhݺܹCCC1j(#!!u7oބ[dԩ?0w\si8;;COO -,,pm*1c1c4]v!-- zzz9ÇH$Bll,KÆ ̙3a4k gΜA~~>`…D8z(rrr$FZZrrrPVV h߾=.\K֪Vg֭ pʓ;wݻ Ŋ+sgϞpuu}3..#GǏ#GV5"##3`#;;qqqٳg2oFxx8Hc1c10 `̙#YXX7|#S3sڵprrQZZ_gϞũSxE, +++XYYUZD"Ajj*s F\077WYB~~ܒ ͛7G߾}q={V"Z1c1SHĉFژ[lAVV1~Jzxx޽{ǃΝ;:all ccc!$s4@ZU0qDW^믿bnnvӧСCspp /y 555#>>^:c1c1B _~wyG_, _~%ollDL88Ç1k,CŸ۷S";;D hkk]vG`bb"__[e]_5$ }AGGH$q!?ʕ+q9mѢqs1c1c iܸqD/eÇhٲ%>jQWW޽{e̜9~Y{Ne]vxwPTT wKKK޽;TM^D"7|#'O[֪n*{3СC! qt֭y "HΝ%3c1c-];w=*sXbBн{w> ŸhѢz>jTE[[EEEAϞ=QZZ)S 88`庑QQQ0` UUW^֭[1mڴ獈Zh,3c1cF  gΜpɓ1b444 CCCܺu+f̘ccc28 lW^UARR-55iiiHHH@aa!233+WT:_SSfff HU(.. nܸ ܹѯ_?9s ,ԩSvɌڵkq̙Jsؽ{7PQQESgg:c1c1;"77'>#͛7ѧO@ll,,,,3!!.D"޽Ν;ځ5?ҐdܻwO斝]:::ҥ[u= wwwB,hӦ 9*2⧟~w}'H$B޽1l0 2(**>$ }]va„ |uW)ݻw222=WUUbЧO;vV%ؿCRRӧO#''G֭[c̘14iѬY^)c1c1& U4ӳ:***رc΃b̘1EJJ 7o6JS-55U׭[7DGGEZ&@tt4°~ܼyz¢E MM^"c1c1 Uƍ_' ЬY3!CzPtС9GP̞=7nu7B#""gc1c1kR8@Z )))yfTTT@MM f²eOOOayf駟" @nTt bѰja1c1c_Ղ6l؀+WXn:wk׮!%%˗/*$ n ===9r\b1zQc1c1 HO>8{,9SSSdeea޼y=ͅ{=fF;wL6Q1c1c1[_[qq1֭[~777[qqqBnn.MΝ www899A]])))h۶mc^c1c1cZ UǏ "4o| &N___>|ҧZ]]eee?~<y1c1c{qTΟ?y!::ƱpuumU1c1c1i=H$ ĢEDhݺ5b1ѪU+CUUWc1c1ؿ:fIENDB`paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex000066400000000000000000000077331417573700700266330ustar00rootroot00000000000000\documentclass[10pt,a4paper]{article} \usepackage[utf8]{inputenc} \usepackage[T1]{fontenc} \usepackage{lmodern} \usepackage{float} \usepackage{graphicx} \usepackage{wrapfig} \date{} \title{Welcome to Paperwork !} \setlength{\parskip}{\baselineskip}% \begin{document} \maketitle \begin{figure}[h] \includegraphics[width=\linewidth]{data/intro.png} \end{figure} They are going to drive you crazy. Your phone operator, your bank, your daughter's school, your dog's veterinarian, even your ISP; it seems like all of them are trying to drown you under tons of papers. Papers you have to read, classify, and memorize just in case you may need them later. Most of the time you won't, which means you waste your energy for nothing. Paperwork will help you get rid of all those papers by turning them into searchable documents. It's simple: just scan and forget. Looking for a specific paper? Just type in a few keywords and tada \pagebreak \section{Documents and pages} \begin{figure}[h] \includegraphics[width=\linewidth]{out/main_window_split.png} \end{figure} Paperwork's interface is composed of two panels. On the left (green) is the list of all your documents sorted by the date they were imported. On the right (blue) are the pages of the currently selected paper. You can add papers from several sources, depending on the devices connected to your computer: scanner flatbed, scanner feeder, camera, etc. You have no scanner at home? You can still use the scanner you have at work. Paperwork will easily import PDF and image files. \section{Find} \begin{wrapfigure}{r}{0.5\textwidth} \centering \vspace{-20pt} \includegraphics[scale=0.5]{out/search.png} \end{wrapfigure} Find what you need, when you need it. Type a few keywords in the search bar and the list of papers will shrink to only the relevant content. This is where the magic happens: Paperwork uses optical character recognition (OCR) to convert your papers into simple text files, so it's easy to search for text. \section{Export} \begin{figure}[h] \centering \includegraphics[scale=0.5]{out/page_actions.png} \includegraphics[scale=0.5]{out/doc_properties_button.png} \end{figure} Sometimes you may want to export a document to send it to someone else. Multiple formats are supported: .pdf, .jpg, .txt, etc. And of course, paper (requires a printer, sold separately). \section{Labels and additional keywords} \begin{figure}[h] \centering \includegraphics[scale=0.3]{out/doc_labels.png} \includegraphics[scale=0.3]{out/doc_extra_text.png} \end{figure} You answered an important email and you want to keep track of it? The paper you scanned was so unreadable that Paperwork failed to recognize some important keywords? Add keywords to your paper so you won't miss anything! All the keywords you add will be searchable, as if they were directly written on the paper you scanned. You would like to organize your documents a bit more? You can also add labels to your documents. Each label has its own color. With time, Paperwork will learn which labels go on which documents and will automatically apply them on new documents. \section{Your first documents} \begin{figure}[h] \centering \includegraphics[scale=0.5]{out/doc_new_button.png} \includegraphics[scale=0.5]{out/page_add.png} \end{figure} Click the + button, the scan button, and that's all folks! You are now aware of the main features of Paperwork. You can start using it by adding your first own paper. This document will automatically disappear from your document list as soon as you have created or imported your first document. \section{Need more help ?} \begin{figure}[H] \centering \includegraphics[width=\linewidth]{out/app_menu_opened.png} \end{figure} If you need more help, there is a comprehensive manual you can find in the help section of Paperwork. We hope that you'll enjoy this piece of software. If you like it please tell us, and if you don't please tell us why! \begin{figure}[b] \includegraphics[width=\linewidth]{out/paperwork_going_up.png} \end{figure} \end{document} paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/data/l10n/000077500000000000000000000000001417573700700255165ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/data/l10n/de.po000066400000000000000000003225331417573700700264560ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-09 12:56+0200\n" "PO-Revision-Date: 2021-10-25 05:49+0000\n" "Last-Translator: Andreas Forster \n" "Language-Team: German \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.4\n" #. type: Plain text #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:11 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "\\date{}" msgstr "" "\\date{}\n" "\\usepackage[german]{babel}" #. type: title{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:11 msgid "Welcome to Paperwork !" msgstr "Willkommen bei Paperwork!" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:20 msgid "" "They are going to drive you crazy. Your phone operator, your bank, your " "daughter's school, your dog's veterinarian, even your ISP; it seems like all " "of them are trying to drown you under tons of papers. Papers you have to " "read, classify, and memorize just in case you may need them later. Most of " "the time you won't, which means you waste your energy for nothing." msgstr "" "Sie treiben dich in den Wahnsinn: dein Handyanbieter, deine Bank, die Schule " "deiner Tochter, der Tierarzt deines Hundes, selbst dein Internetanbieter. Es " "scheint, als wollten sie dich in einer Flut aus Papier ertränken. Dokumente, " "die du lesen, sortieren und deren Inhalt du dir merken sollst. Nur für den " "Fall, dass du sie später noch mal brauchst – die meisten brauchst du " "allerdings so bald nicht wieder und du verschenkst deine Energie völlig " "umsonst." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:24 msgid "" "Paperwork will help you get rid of all those papers by turning them into " "searchable documents. It's simple: just scan and forget. Looking for a " "specific paper? Just type in a few keywords and tada" msgstr "" "Paperwork wird dir helfen, dieser Papierflut Herr zu werden, indem es lose " "Zettel in durchsuchbare Dateien verwandelt. Ganz einfach: Dokument scannen " "und vergessen. Du suchst ein bestimmtes Schreiben? Einfach ein paar " "Schlüsselworte eintippen und tada!" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:28 msgid "Documents and pages" msgstr "Dokumente und Seiten" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:36 msgid "" "Paperwork's interface is composed of two panels. On the left (green) is the " "list of all your documents sorted by the date they were imported. On the " "right (blue) are the pages of the currently selected paper." msgstr "" "Die Oberfläche von Paperwork teilt sich in zwei Bereiche auf: Auf der linken " "(grün hervorgehobenen) Seite befindet sich eine Liste aller Dokumente, " "sortiert nach Datum. Auf der rechten (blauen) Seite wird das gerade " "ausgewählte Dokument angezeigt." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:41 msgid "" "You can add papers from several sources, depending on the devices connected " "to your computer: scanner flatbed, scanner feeder, camera, etc. You have no " "scanner at home? You can still use the scanner you have at work. Paperwork " "will easily import PDF and image files." msgstr "" "Du kannst Dokumente aus verschiedenen Quellen importieren, abhängig vom " "angeschlossenen Gerät: Flachbettscanner, Dokumentenscanner mit Einzug, " "Digitalkamera usw. Du hast keinen Scanner zu Hause? du kannst auch den " "Scanner bei der Arbeit verwenden – Paperwork importiert ohne Weiteres auch " "bereits existierende PDF- und Bilddateien." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:44 msgid "Find" msgstr "Suchen" #. type: wrapfigure #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:50 msgid "{r}{0.5\\textwidth}" msgstr "{r}{0.5\\textwidth}" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:56 msgid "" "Find what you need, when you need it. Type a few keywords in the search bar " "and the list of papers will shrink to only the relevant content. This is " "where the magic happens: Paperwork uses optical character recognition (OCR) " "to convert your papers into simple text files, so it's easy to search for " "text." msgstr "" "Finde ganz einfach, wonach du suchst: Tippe ein paar Schlüsselworte ins " "Suchfeld und die Dokumentenliste zeigt dir nur noch die relevanten Dateien " "an. Hier passiert die Magie: Paperwork verwendet optische Zeichenerkennung " "(engl.: optical character recognition, OCR), um deine Dokumente in einfache, " "durchsuchbare Textdateien umzuwandeln." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:59 msgid "Export" msgstr "Exportieren" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:69 msgid "" "Sometimes you may want to export a document to send it to someone else. " "Multiple formats are supported: .pdf, .jpg, .txt, etc. And of course, paper " "(requires a printer, sold separately)." msgstr "" "Manchmal wirst du ein Dokument aus Paperwork exportieren wollen, um es " "jemandem zu senden. Verschiedene Formate werden unterstützt: .pdf, .jpg, ." "txt usw. Oder natürlich Papier (setzt einen Drucker voraus, nicht im " "Lieferumfang enthalten)." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:72 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:113 msgid "Labels and additional keywords" msgstr "Labels und zusätzliche Schlüsselworte" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:84 msgid "" "You answered an important email and you want to keep track of it? The paper " "you scanned was so unreadable that Paperwork failed to recognize some " "important keywords? Add keywords to your paper so you won't miss anything! " "All the keywords you add will be searchable, as if they were directly " "written on the paper you scanned." msgstr "" "Du hast eine wichtige E-Mail beantwortet und willst sie im Blick behalten? " "Das eingescannte Dokument war so unlesbar, dass Paperwork einige der " "enthaltenen Schlüsselworte gar nicht erkennen konnte? Füge eigene " "Schlüsselworte hinzu, damit dir nichts entgeht! Alle Wörter die Du dazugibst " "werden suchbar, als ob sie auf dem gescannten Dokument geschrieben waren." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:89 msgid "" "You would like to organize your documents a bit more? You can also add " "labels to your documents. Each label has its own color. With time, Paperwork " "will learn which labels go on which documents and will automatically apply " "them on new documents." msgstr "" "Du willst deine Dokumente noch gründlicher organisieren? Dann nutze Labels. " "Jedes Label hat seine eigene Farbe. Mit der Zeit wird Paperwork lernen, " "welche Labels an welches Dokument gehören und sie beim Erfassen neuer " "Dokumente selbstständig ergänzen." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:92 msgid "Your first documents" msgstr "Deine ersten Dokumente" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:102 msgid "" "Click the + button, the scan button, and that's all folks! You are now aware " "of the main features of Paperwork. You can start using it by adding your " "first own paper." msgstr "" "Klick auf den \"{}+\"{}-Knopf, dann auf den \"{}Scan\"{}-Button und das " "war’s. Damit kennst du bereits die Hauptfunktionen von Paperwork. Du kannst " "mit der Benutzung direkt starten, indem du das erste Dokument hinzufügst." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:105 msgid "" "This document will automatically disappear from your document list as soon " "as you have created or imported your first document." msgstr "" "Dieses Dokument wird automatisch aus Deiner Dokumentenliste verschwinden, " "sobald Du dein erstes Dokument erstellt oder importiert hast." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:108 msgid "Need more help ?" msgstr "Mehr Hilfe benötigt?" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:116 msgid "" "If you need more help, there is a comprehensive manual you can find in the " "help section of Paperwork." msgstr "" "Wenn du mehr Hilfe benötigst, schau ins ausführliche Handbuch. Du findest es " "im Menü \"{}Hilfe\"{} von Paperwork." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:119 msgid "" "We hope that you'll enjoy this piece of software. If you like it please tell " "us, and if you don't please tell us why!" msgstr "" "Wir hoffen, dass dir dieses Stück Software Freude bereiten wird. Wenn du " "Paperwork magst, sag es uns – und wenn nicht, verrat uns, warum nicht!" #. type: hypersetup{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "" "colorlinks, citecolor=black, filecolor=black, linkcolor=black, " "urlcolor=black, linktoc=all," msgstr "" "colorlinks, citecolor=black, filecolor=black, linkcolor=black, " "urlcolor=black, linktoc=all," #. type: title{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "Paperwork manual" msgstr "Paperwork Handbuch" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:32 msgid "Introduction" msgstr "Einleitung" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:41 msgid "" "Most personal documents are fairly recurrent: earning statements, rent " "bills, electricity bills, etc. For most unorganized people, having to find " "them back later is worrisome, at best. For most organized people, naming and " "sorting them is as tedious as watching paint dry." msgstr "" "Die meisten privaten Dokumente sind ziemlich wiederholend: " "Gehaltsabrechnungen, Mietabrechnungen, Stromrechnungen usw. Für die meisten " "unorganisierten Menschen ist es bestenfalls beunruhigend, diese später " "wiederfinden zu müssen. Für die meisten organisierten Menschen ist das " "Benennen und Sortieren so mühsam, wie Farbe beim Trocknen zuzusehen." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:45 msgid "" "The main idea behind Paperwork is that managing documents is a computer " "job. Humans should do as little as possible while machines do most of the " "work. The end goal here is \"scan \\& forget\"." msgstr "" "Die Grundidee hinter Paperwork ist es, dass die Verwaltung von Dokumenten " "ein Computerjob ist. Menschen sollten so wenig wie möglich tun, während " "Maschinen den Großteil der Arbeit erledigen. Das Ziel ist hier \"{}scannen " "\\& vergessen\"{}." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:49 msgid "" "If you're looking for a software that will let you name each document " "individually, organize them in complex hierachy, tag them manually each " "time, fix OCR minor glitches, etc, then Paperwork is not for you." msgstr "" "Wenn Sie eine Software suchen, mit der Sie jedes Dokument einzeln benennen, " "in einer komplexen Hierarchie organisieren, jedes Mal manuell markieren, " "kleinere OCR-Probleme beheben usw. können, dann ist Paperwork nichts für Sie." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:52 msgid "Definitions" msgstr "Definitionen" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:54 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:158 msgid "Work directory" msgstr "Arbeitsverzeichnis" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:57 msgid "" "Paperwork stores all your documents in a single directory: the work " "directory. In this directory, each document has its own sub-directory." msgstr "" "Paperwork speichert alle Ihre Dokumente in einem einzigen Verzeichnis: dem " "Arbeitsverzeichnis. In diesem Verzeichnis hat jedes Dokument sein eigenes " "Unterverzeichnis." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:61 msgid "" "While this makes Paperwork hard to use with other tools, it has one major " "advantage: You don't have to worry about file names and directory structures " "anymore." msgstr "" "Dies erschwert zwar die Verwendung von Paperwork mit anderen Programmen, hat " "aber einen großen Vorteil: Sie müssen sich nicht mehr um Dateinamen und " "Verzeichnisstrukturen kümmern." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:63 msgid "Document" msgstr "Dokument" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:71 msgid "" "In Paperwork, a document is a set of pages. On disk, it can either be a set " "of JPEG files or a PDF file." msgstr "" "In Paperwork ist ein Dokument eine Ansammlung von Seiten. Auf der Festplatte " "kann dies entweder eine Ansammlung von JPEG-Dateien oder eine PDF-Datei sein." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:74 msgid "" "Documents are identified only by a date. It can either be the date you " "imported them (default) or some date of your choosing." msgstr "" "Dokumente werden nur durch ein Datum identifiziert. Es kann entweder das " "Datum sein, an dem Sie sie importiert haben (voreingestellter Standard), " "oder ein von Ihnen gewähltes Datum." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:77 msgid "" "They are displayed on the left side of the main window (green part on the " "screenshot above)." msgstr "" "Sie werden auf der linken Seite des Hauptfensters angezeigt (grüner Teil auf " "dem Screenshot oben)." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:80 msgid "Page" msgstr "Seite" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:82 msgid "" "In Paperwork, a page is just an image and the word positions on this image." msgstr "" "In Paperwork ist eine Seite nur ein Bild und Wortpositionen auf diesem Bild." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:88 msgid "" "Images can come from a scanner or be imported. In those cases, it is stored " "as a JPEG files and text is extracted using OCR (Optical Character " "Recognition). OCR is a fairly long process. It can take up to a few minutes " "for each page. So the text extracted from images is stored in hOCR files " "beside the JPEG files." msgstr "" "Bilder können von einem Scanner kommen oder importiert werden. In diesen " "Fällen wird es als JPEG-Datei gespeichert und der Text wird mit OCR (Optical " "Character Recognition, englisch für Zeichnerkennung) extrahiert. OCR ist ein " "ziemlich langwieriger Prozess. Er kann bis zu ein paar Minuten pro Seite " "dauern. Daher wird der aus den Bildern extrahierte Text in hOCR-Dateien " "neben den JPEG-Dateien gespeichert." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:91 msgid "" "Pages can also be the pages from a PDF file. In that case, by default, " "Paperwork just stores a copy of the PDF file." msgstr "" "Seiten können auch die Seiten aus einer PDF-Datei sein. In diesem Fall " "speichert Paperwork standardmäßig nur eine Kopie der PDF-Datei." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:95 msgid "" "Paperwork does not track whether a page is recto or verso. Paperwork does " "not track the paper size corresponding to a page (A4, Letter, etc)." msgstr "" "Paperwork kümmert es nicht, ob eine Seite Vorder- oder Rückseite ist. " "Paperwork berücksichtigt auch nicht das Papierformat einer Seite (A4, " "Letter, usw.)." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:98 msgid "" "Pages are displayed on the right side of the main window (blue part on the " "screenshot above)." msgstr "" "Die Seiten werden auf der rechten Seite des Hauptfensters angezeigt (blauer " "Teil auf dem Screenshot oben)." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:101 msgid "Indexation and Keywords" msgstr "Indexierung und Schlüsselwörter" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:108 msgid "" "Of course, you need a way to find back your documents. Paperwork manages an " "index with all the keywords found in your documents." msgstr "" "Natürlich brauchen Sie eine Möglichkeit, Ihre Dokumente wiederzufinden. " "Paperwork verwaltet einen Index mit allen Schlagwörtern, die in Ihren " "Dokumenten gefunden wurden." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:110 msgid "Just type in a few keywords, and you will get your documents back." msgstr "" "Geben Sie einfach ein paar Schlüsselwörter ein, und Sie erhalten Ihre " "Dokumente zurück." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:128 msgid "" "Unfortunately, sometimes, documents don't contain the keywords needed to " "find them back. Also OCR is not a perfectly realiable process and may not " "work." msgstr "" "Leider enthalten Dokumente manchmal nicht die erforderlichen " "Schlüsselwörter, um sie wiederzufinden. Außerdem ist OCR kein perfekt " "zuverlässiger Prozess und funktioniert möglicherweise nicht." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:131 msgid "" "To mitigate those issues, you can add labels (or tags) on your documents and " "provide additional keywords. Both are added to the index." msgstr "" "Um diese Probleme abzumildern, können Sie Ihre Dokumente mit Etiketten (oder " "Tags) versehen und zusätzliche Schlüsselwörter angeben. Beides wird in den " "Index aufgenommen." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:134 msgid "" "Labels are displayed beside documents. Additional keywords are almost never " "displayed." msgstr "" "Etiketten werden neben den Dokumenten angezeigt. Zusätzliche Stichwörter " "werden fast nie angezeigt." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:137 msgid "Settings" msgstr "Einstellungen" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:139 msgid "Accessing the settings" msgstr "Zugriff auf die Einstellungen" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:166 msgid "" "The work directory is the directory where you want all your documents " "stored. It can be a standard folder, a folder synchronized across multiple " "computers or on a network share." msgstr "" "Das Arbeitsverzeichnis ist das Verzeichnis, in dem Sie alle Ihre Dokumente " "speichern möchten. Das kann ein normaler Ordner, ein über mehrere Computer " "synchronisierter Ordner oder eine Netzwerkfreigabe sein." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:169 msgid "" "Once you close the settings dialog, the work directory will be scanned and " "Paperwork index will be updated according to its index." msgstr "" "Nachdem Sie den Einstellungsdialog geschlossen haben, wird das " "Arbeitsverzeichnis gescannt und der Paperwork-Index wird entsprechend seinem " "Index aktualisiert." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:172 msgid "" "Each time Paperwork starts, it will look for changes in this folder and " "synchronize its index accordingly." msgstr "" "Jedes Mal, wenn Paperwork startet, sucht es nach Änderungen in diesem Ordner " "und synchronisiert seinen Index entsprechend." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:175 msgid "Scanner" msgstr "Scanner" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:182 msgid "Device" msgstr "Gerät" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:189 msgid "" "When starting, Paperwork looks for scanners. The scanner to use can be " "selected in the settings." msgstr "" "Beim Start sucht Paperwork nach Scannern. Der zu verwendende Scanner kann in " "den Einstellungen ausgewählt werden." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:191 msgid "Webcams, file storage, etc, cannot be used. Only paper-eaters." msgstr "" "Webcams, Dateispeicher usw. können nicht verwendet werden. Nur Papierfresser." #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:194 msgid "Scan Mode" msgstr "Scanmodus" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:202 msgid "" "Most modern scanners scan in color in a reasonable time. However some older " "scanners scan much faster in grayscale or even in black\\&white. Here you " "can select the mode to use." msgstr "" "Die meisten modernen Scanner scannen in Farbe in einer vertretbaren Zeit. " "Einige ältere Scanner scannen jedoch viel schneller in Graustufen oder sogar " "in Schwarz-Weiß. Hier können Sie den zu verwendenden Modus auswählen." #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:205 msgid "Scan Resolution" msgstr "Scan-Auflösung" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:212 msgid "" "Scanner resolution defines how detailed the images coming from your scanner " "must be." msgstr "" "Die Scan-Auflösung legt fest, wie detailliert die von Ihrem Scanner " "kommenden Bilder sein müssen." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "Higher resolutions mean" msgstr "Höhere Auflösungen bedeuten" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "longer scans," msgstr "längere Scans," #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "longer OCR," msgstr "längere OCR," #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "more time to display," msgstr "mehr Zeit für die Anzeige," #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "more space used on disk," msgstr "mehr Speicherplatz auf der Festplatte verwendet," #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "but also better OCR." msgstr "aber auch eine bessere OCR." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "Lower resolutions mean" msgstr "Niedrigere Auflösungen bedeuten" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "shorter scans," msgstr "kürzere Scans," #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "shorter OCR," msgstr "kürzere OCR," #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "less time to display," msgstr "weniger Zeit für die Anzeige," #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "less space used on disk," msgstr "weniger Speicherplatz auf der Festplatte," #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "but also inferior OCR," msgstr "aber auch eine minderwertige OCR," #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "and possibly unreadable image (even by a human)." msgstr "und ein möglicherweise unlesbares (auch von einem Menschen) Bild." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:234 msgid "" "300 dpi is considered a good trade-off. You may want to reduce it to 200 dpi " "on slow computers." msgstr "" "300 dpi wird als guter Kompromiss angesehen. Auf langsamen Computern sollten " "Sie eventuell auf 200 dpi reduzieren." #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:237 msgid "Scanner calibration" msgstr "Scanner-Kalibrierung" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:247 msgid "" "Scanners tend to provide images actually bigger than the scanned pages. " "Since most of the time, you will always scan pages having the same size (A4 " "or Letter usually), Paperwork provides an option called scanner calibration. " "Scanner calibration in Paperwork is simply an area that will always be " "cropped out of images coming from the scanner." msgstr "" "Scanner neigen dazu, Bilder zu liefern, die größer sind als die gescannten " "Seiten. Da Sie in den meisten Fällen immer Seiten mit der gleichen Größe " "(normalerweise A4 oder Letter) scannen werden, bietet Paperwork eine Option " "namens Scannerkalibrierung. Die Scannerkalibrierung in Paperwork ist einfach " "ein Bereich, der aus den die vom Scanner kommenden Bildern jedesmal " "ausgeschnitten wird." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:250 msgid "OCR" msgstr "OCR (optical character recognition, englisch für Texterkennung)" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:257 msgid "" "By default, Paperwork uses Tesseract for the OCR. If unavailable, it falls " "back on Cuneiform." msgstr "" "Paperwork verwendet standardmäßig Tesseract für die OCR. Wenn das nicht " "verfügbar ist, greift es auf Cuneiform zurück." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:260 msgid "" "On Linux, if installed with Flatpak, Paperwork is always provided with " "Tesseract. On Windows, Paperwork is always provided with Tesseract." msgstr "" "Unter Linux wird Paperwork, wenn es mit Flatpak installiert wurde, immer mit " "Tesseract ausgeliefert. Unter Windows wird Paperwork immer mit Tesseract " "installiert." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:263 msgid "" "To get better results, OCR tool need to know the language used in the " "document(s)." msgstr "" "Um bessere Ergebnisse zu erzielen, muss das OCR-Programm die in den " "Dokumenten verwendete Sprache kennen." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:268 msgid "" "The language available in the settings dialog of Paperwork are those " "understood by the OCR tool. If your language is not in the list, it means " "the OCR tool doesn't have the data required to read your language and you " "must install them." msgstr "" "Die im Einstellungsdialog von Paperwork verfügbaren Sprachen sind " "diejenigen, die vom OCR-Programm beherrscht werden. Wenn Ihre Sprache nicht " "in der Liste enthalten ist, bedeutet dies, dass das OCR-Programm nicht über " "die erforderlichen Sprachdaten verfügt und, dass Sie diese installieren " "müssen." #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:271 msgid "Adding languages" msgstr "Sprachen hinzufügen" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:273 msgid "Flatpak" msgstr "Flatpak" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:279 #, no-wrap msgid "" "# is a list of 2-letters language codes separated ';'\n" "# ex: en;fr;de\n" "flatpak config --user --set languages \"\"\n" "flatpak update --user" msgstr "" "# ist eine Liste von zweibuchstabigen Ländercodes getrennt durch ';'" "\n" "# Beispiel: en;fr;de\n" "flatpak config --user --set languages \"\"\n" "flatpak update --user" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:283 msgid "Debian" msgstr "Debian" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:288 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:304 #, no-wrap msgid "" "# is a 3-letter language code\n" "# ex: 'fra' for French\n" "$ sudo apt-get install tesseract-ocr tesseract-ocr-" msgstr "" "# ist ein dreibuchstabiger Sprach-Code\n" "# Beispiel: 'fra' für Französisch\n" "$ sudo apt-get install tesseract-ocr tesseract-ocr-" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:291 msgid "Fedora" msgstr "Fedora" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:296 #, no-wrap msgid "" "# is a 3-letter language code\n" "# ex: 'fra' for French\n" "$ sudo dnf install tesseract tesseract-langpack-" msgstr "" "# ist ein dreibuchstabiger Sprach-Code\n" "# Beispiel: 'fra' für Französisch\n" "$ sudo dnf install tesseract tesseract-langpack-" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:299 msgid "Ubuntu" msgstr "Ubuntu" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:307 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:815 msgid "Windows" msgstr "Windows" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:310 msgid "" "Tesseract and all its data files are provided by Paperwork's installer. You " "can rerun the installer to install other languages." msgstr "" "Tesseract und alle seine Datendateien werden vom Installationsprogramm von " "Paperwork bereitgestellt. Sie können das Installationsprogramm erneut " "ausführen, um andere Sprachen zu installieren." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:314 msgid "" "If a language is not available in the installer, it either means it hasn't " "been packaged (in which case you can request it), or there is no data file " "available yet for this language." msgstr "" "Wenn eine Sprache im Installationsprogramm nicht verfügbar ist, bedeutet " "dies entweder, dass sie nicht mit eingepackt wurde (in diesem Fall können " "Sie sie anfordern), oder es ist noch keine Datendatei für diese Sprache " "verfügbar." #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:317 msgid "Disabling OCR" msgstr "OCR deaktivieren" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:323 msgid "" "When you scan a page using Paperwork, Paperwork will immediately run the OCR " "on it. This process may take a while for each page. In case you want to scan " "a lot of pages quickly (for instance, the first time you use Paperwork), OCR " "can be temporarily disabled. To disable OCR, you simply have to unselect all " "OCR languages." msgstr "" "Wenn Sie eine Seite mit Paperwork scannen, führt Paperwork sofort die OCR " "auf der Seite durch. Dieser Vorgang kann für jede Seite eine Weile dauern. " "Für den Fall, dass Sie viele Seiten schnell scannen möchten (z. B. bei der " "ersten Verwendung von Paperwork), kann die OCR vorübergehend deaktiviert " "werden. Um OCR zu deaktivieren, können Sie einfach alle OCR-Sprachen " "abwählen." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:326 msgid "Updates" msgstr "Updates" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:334 msgid "" "If you enable this option, when Paperwork starts, Paperwork will look for " "updates if it hasn't done so for a week or more. To know if a new version is " "available, it has to send an HTTPS query to 'openpaper.work'." msgstr "" "Wenn Sie diese Option aktivieren, sucht Paperwork beim Start nach Updates, " "falls es dies eine Woche oder länger nicht getan hat. Um zu prüfen, ob eine " "neue Version verfügbar ist, wird eine HTTPS-Anfrage an 'openpaper.work' " "gesendet." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:336 msgid "If an update is found, it will notify you but it won't install it." msgstr "" "Sie werden benachrichtigt, wenn ein Update gefunden wurde, aber es wird " "nicht installiert." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:339 msgid "New document" msgstr "Neues Dokument" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:345 msgid "" "By default, in the document list, Paperwork includes a document called \"New " "document\". If you open it, it always appears empty. This document actually " "doesn't exist yet on disk, but will exist as soon as you put a page in it. " "You can add pages in it by scanning, importing file(s) or dropping a page " "from another in it." msgstr "" "Standardmäßig enthält Paperwork in der Dokumentenliste ein Dokument namens " "\"{}Neues Dokument\"{}. Wenn Sie es öffnen, erscheint es immer leer. Dieses " "Dokument existiert tatsächlich noch nicht auf der Festplatte, aber es wird " "existieren, sobald Sie eine Seite hineinlegen. Sie können ihm Seiten " "hinzufügen, indem Sie scannen, Dateien importieren oder eine Seite aus einer " "anderen Datei darin ablegen." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:350 msgid "" "As soon as you put any content in it, this document will get its own date " "(the current one by default). In the document list, \"New document\" will be " "replaced by this date, and a new \"New document\" will be added to the " "document list." msgstr "" "Sobald Sie einen Inhalt hineinlegen, bekommt dieses Dokument ein eigenes " "Datum (standardmäßig das aktuelle). In der Dokumentenliste wird \"{}Neues " "Dokument\"{} durch dieses Datum ersetzt, und ein neues \"{}Neues Dokument" "\"{} wird der Dokumentenliste hinzugefügt." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:359 msgid "" "If you are currently searching something (see the chapter \"Searching\"), " "only search results are displayed and therefore this \"New document\" isn't " "displayed. You can get it back by clicking the button \"+\" in the top left " "corner of the main window." msgstr "" "Wenn Sie gerade etwas suchen (siehe Kapitel \"{}Suchen\"{}), werden nur die " "Suchergebnisse angezeigt und daher wird dieses \"{}Neue Dokument\"{} nicht " "angezeigt. Sie können es zurückholen, indem Sie auf die Schaltfläche \"{}+" "\"{} in der linken oberen Ecke des Hauptfensters klicken." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:362 msgid "Scanning" msgstr "Scannen" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:368 msgid "" "If a scanner has been selected in the settings, you can use it to scan pages." msgstr "" "Wenn ein Scanner in den Einstellungen ausgewählt wurde, können Sie diesen " "zum Scannen von Seiten verwenden." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:372 msgid "" "In the header bar, there is a button to add pages. The small arrow on the " "right gives access to possible page sources. Those page sources include your " "scanner sources (Flatbed, Feeder)." msgstr "" "In der Kopfleiste befindet sich eine Schaltfläche zum Hinzufügen von Seiten. " "Der kleine Pfeil auf der rechten Seite ermöglicht den Zugriff auf mögliche " "Seitenquellen. Zu diesen Seitenquellen gehören Ihre Scannerquellen " "(Flachbett, Einzug)." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:375 msgid "" "Once you've selected the scanner source you want to use, you can click on " "the button \"Scan from ...\"." msgstr "" "Nachdem Sie die gewünschte Scannerquelle ausgewählt haben, können Sie auf " "die Schaltfläche \"{}Scannen von ...\"{} klicken." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:377 msgid "This will start a scan session:" msgstr "Dadurch wird eine Scan-Sitzung gestartet:" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "" "Scanned pages are appended at the end of the current document. If you use a " "feeder, Paperwork will scan pages until the feeder is empty." msgstr "" "Gescannte Seiten werden an das Ende des aktuellen Dokuments angehängt. Falls " "Sie einen automatischen Papiereinzug verwenden, scannt Paperwork Seiten, bis " "der Einzug leer ist." #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will then crop them according to scanner calibration." msgstr "" "Die Papiere werden dann entsprechend der Scannerkalibrierung beschnitten." #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will run OCR on them" msgstr "Paperwork lässt die OCR laufen" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will index them" msgstr "Paperwork wird sie indizieren" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:389 msgid "" "If this scan session creates a new document, Paperwork will try to set " "labels automatically on the document." msgstr "" "Wenn in dieser Scan-Sitzung ein neues Dokument erstellt wird, versucht " "Paperwork, automatisch Schlagwörter im Dokument zu setzen." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:392 msgid "Importing" msgstr "Import" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:399 msgid "Images" msgstr "Bilder" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:402 msgid "" "Paperwork supports a lot of file formats. It supports JPEG, PNG, GIF, BMP, " "TIFF, etc." msgstr "" "Paperwork unterstützt eine Vielzahl von Dateiformaten. Es unterstützt JPEG, " "PNG, GIF, BMP, TIFF, usw." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:404 msgid "Each image file is considered as a page." msgstr "Jede Bilddatei wird als eine Seite behandelt." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:408 msgid "" "Images are always appended to the document currently opened. Simply select " "an empty document (\"New document\") to create a new document while " "importing." msgstr "" "Bilder werden immer an das aktuell geöffnete Dokument angehängt. Wählen Sie " "einfach ein leeres Dokument (\"{}Neues Dokument\"{}), um beim Importieren " "ein neues Dokument zu erstellen." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:412 msgid "" "OCR is always run on imported images. If the imported image is the first " "page of a new document, Paperwork will automatically apply documents labels." msgstr "" "Auf importierten Bildern wird immer eine OCR ausgeführt. Falls das " "importierte Bild die erste Seite eines neuen Dokuments ist, erzeugt " "Paperwork automatisch Schlagwörter." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:416 msgid "" "Note that Paperwork is a document manager. While it can, it is not designed " "to handle images with only very little text or photos. Automatic labeling " "will not work correctly on such documents." msgstr "" "Beachten Sie, dass Paperwork ein Dokumentenmanager ist. Es kann zwar Bilder " "mit nur sehr wenig Text oder Fotos verarbeiten, ist aber nicht dafür " "ausgelegt. Die automatische Verschlagwortung wird bei solchen Dokumenten " "nicht korrekt funktionieren." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:420 msgid "" "The OCR (Tesseract) works very well with black text on white background. " "Automatic labeling uses recognized text and requires as many keywords on the " "first page as possible." msgstr "" "Die OCR (Tesseract) arbeitet sehr gut mit schwarzem Text auf weißem " "Hintergrund. Die automatische Verschlagwortung verwendet den erkannten Text " "und benötigt viele Schlüsselwörter auf der ersten Seite." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:423 msgid "PDF" msgstr "PDF" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:429 msgid "" "Each PDF is always considered as a whole document. They are never appended " "to existing document. They are copied and renamed in the work directory, but " "their content is not modified. Paperwork always keeps the original PDF file " "as is, even if you edit some of its pages: the edited pages are stored " "beside the PDF file." msgstr "" "PDF-Dateien werden immer als ein ganzes Dokument betrachtet. Sie werden nie " "an ein bestehendes Dokument angehängt. Sie werden in das Arbeitsverzeichnis " "kopiert und umbenannt, aber ihr Inhalt wird nicht verändert. Paperwork " "behält die ursprüngliche PDF-Datei immer so bei, wie sie war, auch wenn Sie " "einige ihrer Seiten bearbeiten: Die bearbeiteten Seiten werden neben der PDF-" "Datei gespeichert." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:434 msgid "" "Paperwork will look for pages with no text attached. On those pages, it will " "automatically run OCR. Once all the pages have been examined, it will " "automatically apply document labels. Note that this process may take a few " "minutes for big PDFs files." msgstr "" "Paperwork sucht nach den Seiten, die keinen Text beigefügt haben. Auf diesen " "Seiten wird automatisch eine OCR durchgeführt. Nachdem alle Seiten " "untersucht wurden, wird automatisch verschlagwortet. Beachten Sie, dass " "dieser Vorgang bei großen PDF-Dateien einige Minuten dauern kann." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:437 msgid "" "If the PDF is already part of your documents, Paperwork will simply ignore " "it." msgstr "" "Wenn eine PDF-Datei bereits Teil Ihrer Dokumente ist, wird sie von Paperwork " "einfach ignoriert." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:440 msgid "Many PDFs in one shot" msgstr "Viele PDFs auf einen Schlag" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:445 msgid "" "When importing, if you select a folder, Paperwork will browse this folder " "and look for PDFs to import. Already-imported PDFs are simply ignored. " "Folder is browsed recursively (all the folders inside the folder are also " "examined)." msgstr "" "Wenn Sie beim Importieren einen Ordner auswählen, durchsucht Paperwork " "diesen Ordner und sucht nach zu importierenden PDF-Dateien. Bereits " "importierte PDF-Dateien werden einfach ignoriert. Der Ordner wird rekursiv " "durchsucht (alle Ordner innerhalb des Ordners werden ebenfalls abgearbeitet)." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:448 msgid "Labels" msgstr "Schlagwörter" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:464 msgid "" "There is currently one constraint in Paperwork: Each label must be on at " "least one document. Otherwise, when you will restart Paperwork, labels " "without documents will disappear." msgstr "" "Derzeit gibt es eine Einschränkung in Paperwork: Jedes Schlagwort muss sich " "in mindestens einem Dokument finden. Andernfalls werden beim Neustart von " "Paperwork Schlagwörter ohne Dokumentenbezug verschwinden." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:467 msgid "Creating new labels" msgstr "Neue Schlagwörter einrichten" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:475 msgid "" "You can click on the gray rectangle on the left side to pick the label " "color. You can enter the label name in text field between the gray " "rectangle and the button \"+\"." msgstr "" "Sie können auf das graue Rechteck auf der linken Seite klicken, um die " "Schlagwortfarbe auszuwählen. Sie können den Schlagwortnamen in das Textfeld " "zwischen dem grauen Rechteck und der Schaltfläche \"{}+\"{} eingeben." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:478 msgid "" "Once you click on the button \"+\", the label will be added to the current " "document." msgstr "" "Sobald Sie auf die Schaltfläche \"{}+\"{} klicken, wird das Schlagwort zum " "aktuellen Dokument hinzugefügt." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:481 msgid "" "The label is actually added once you close document properties. Paperwork " "will then update its index accordingly." msgstr "" "Das Schlagwort wird endgültig hinzugefügt, sobald Sie die " "Dokumenteigenschaften schließen. Paperwork wird dann seinen Index " "entsprechend aktualisieren." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:484 msgid "Setting labels on documents" msgstr "Schlagwörter für Dokumente setzen" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:488 msgid "" "When you open document properties, the label list appears. On the left side " "of each label color, you have a button. This button allows you to add or " "remove labels on the current document." msgstr "" "Wenn Sie die Dokumenteigenschaften öffnen, wird die Schlagwortliste " "angezeigt. Auf der linken Seite jeder Schlagwortfarbe finden Sie eine " "Schaltfläche. Mit dieser Schaltfläche können Sie Schlagwörter zum aktuellen " "Dokument hinzufügen oder davon entfernen." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:491 msgid "" "The changes are actually written on disk once you close the document " "properties. Paperwork will update its index accordinly." msgstr "" "Die Änderungen werden endgültig auf die Festplatte geschrieben, wenn Sie die " "Dokumenteigenschaften schließen. Paperwork wird dann seinen Index " "entsprechend aktualisieren." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:494 msgid "Modifying a label color" msgstr "Ändern der Schlagwortfarbe" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:497 msgid "" "When you open document properties, you can click on a label color to change " "it. A dialog will let you pick the new color." msgstr "" "Wenn Sie die Dokumenteigenschaften öffnen, können Sie auf eine " "Schlagwortfarbe klicken, um sie zu ändern. In einem Dialogfeld können Sie " "die neue Farbe auswählen." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:501 msgid "" "Label color will actually be changed on disk when you close the document " "properties. Paperwork will then update the label on all the documents that " "use it." msgstr "" "Die Schlagwortfarbe wird endgültig auf der Festplatte geändert, wenn Sie die " "Dokumenteigenschaften schließen. Paperwork aktualisiert dann das Schlagwort " "für alle Dokumente, die dieses verwenden." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:504 msgid "Modifying a label name" msgstr "Ändern eines Schlagwortnamens" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:507 msgid "" "When you open document properties, you can click on a label string to change " "it. A dialog will let you type in the new name." msgstr "" "Wenn Sie die Dokumenteigenschaften öffnen, können Sie auf einen " "Schlagwortnamen klicken, um diesen zu ändern. In einem Dialogfeld können Sie " "den neuen Namen eingeben." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:511 msgid "" "Label name will actually be changed on disk when you close the document " "properties. Paperwork will then update the label on all the documents that " "use it and then reindex them all." msgstr "" "Der Schlagwortname wird endgültig auf der Festplatte geändert, wenn Sie die " "Dokumenteigenschaften schließen. Paperwork aktualisiert dann das Schlagwort " "für allen Dokumente, die dieses verwenden, und indiziert sie dann alle neu." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:514 msgid "Deleting a label" msgstr "Löschen eines Schlagwortes" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:517 msgid "" "To the right of each label is white-on-black cross button. Clicking on it " "will allow you to delete a label." msgstr "" "Rechts von jedem Schlagwort befindet sich eine weiß-schwarze Kreuztaste. " "Wenn Sie darauf klicken, können Sie das Schlagwort löschen." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:520 msgid "" "Once you will close the document properties, the label will be removed from " "all the documents having it. Paperwork will then update its index " "accordingly." msgstr "" "Nachdem Sie die Dokumenteigenschaften geschlossen haben, wird das Schlagwort " "für alle Dokumente entfernt, die es enthalten hatten. Paperwork wird dann " "seinen Index entsprechend aktualisieren." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:523 msgid "" "Beware: Once you have closed document properties, there is no way to put " "back the deleted label." msgstr "" "Achtung! Wenn Sie die Dokumenteigenschaften geschlossen haben, gibt es keine " "Möglichkeit, das gelöschte Schlagwort wieder einzufügen." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:526 msgid "Automatic label guessing" msgstr "Automatische Verschlagwortung" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:531 msgid "" "Paperwork does use artificial intelligence. It uses a fairly simple method " "actually: \\href{https://en.wikipedia.org/wiki/Naive_Bayes_classifier}{Naive " "Bayes classifiers}. It's the same technology used by email clients to " "classify mails as spam/non-spam." msgstr "" "Paperwork nutzt künstliche Intelligenz. Es verwendet sogar eine ziemlich " "einfache Methode: \\href{https://en.wikipedia.org/wiki/" "Naive_Bayes_classifier}{Naive Bayes classifiers}. Dies ist die gleiche " "Technologie, die von E-Mail-Clients verwendet wird, um Mails als Spam/Nicht-" "Spam zu klassifizieren." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:537 msgid "" "Based on all the keywords in all your documents that have (or haven't) a " "label, it can estimate a probability that a document containing the same " "keywords should have or shouldn't have this same label. If the probability " "is high enough, it puts the label on the document automatically when you " "import it or scan it." msgstr "" "Basierend auf allen Schlüsselwörtern in all Ihren Dokumenten, die ein " "Schlagwort haben (oder nicht), kann es eine Wahrscheinlichkeit abschätzen, " "dass ein Dokument, das dieselben Schlüsselwörter enthält, dasselbe " "Schlagwort haben oder nicht haben müsste. Wenn die Wahrscheinlichkeit hoch " "genug ist, wird das Dokument beim Importieren oder Scannen automagisch mit " "dem Schlagwort versehen." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:541 msgid "" "Of course, this approach means that Paperwork needs enough samples to work " "reliably. You can expect it to start working once you have about 100 " "documents or more (and only for labels that are on more than 10 documents or " "more)." msgstr "" "Natürlich bedeutet dieser Ansatz, dass Paperwork genügend Muster benötigt, " "um zuverlässig zu arbeiten. Sie können davon ausgehen, dass dies ab etwa 100 " "Dokumenten funktioniert (und nur für Schlagwörter, die für mehr als 10 " "Dokumente oder mehr gesetzt sind)." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:544 msgid "Searching" msgstr "Suchen" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:546 msgid "Simple search" msgstr "Einfache Suche" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:553 msgid "" "You simply enter keywords in the search field. In a few seconds, you will " "get all the documents containing those keywords." msgstr "" "Geben Sie einfach Stichwörter in das Suchfeld ein. In wenigen Sekunden " "erhalten Sie alle Dokumente, die diese Stichwörter enthalten." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:557 msgid "" "Paperwork does a \"fuzzy\" search: documents with keywords close to the one " "you gave but not identical are also returned (for instance, 'flech' instead " "of 'flesch')." msgstr "" "Paperwork führt eine \"{}unscharfe\"{} Suche durch: Es werden auch Dokumente " "mit Stichwörtern zurückgegeben, die dem von Ihnen angegebenen ähnlich sind, " "aber nicht identisch sind (z. B. \"{}flech\"{} statt \"{}flesch\"{})." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:562 msgid "" "You can also use \\href{https://whoosh.readthedocs.io/en/latest/querylang." "html}{Whoosh query language} to make more complex queries. If you want " "examples, you can use the advanced search dialog described below." msgstr "" "Sie können auch die \\href{https://whoosh.readthedocs.io/en/latest/querylang." "html}{Whoosh-Abfragesprache} verwenden, um komplexere Abfragen zu erstellen. " "Wenn Sie Beispiele brauchen, können Sie den unten beschriebenen Dialog für " "die erweiterte Suche verwenden." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:565 msgid "Advanced search" msgstr "Erweiterte Suche" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:582 msgid "" "The advanced search dialog helps creating complex search queries. You can " "specify various criterias and once you click on the apply button, it will " "generate a search query for you and put it immediately in the search field. " "Search results will immediately be refreshed as well." msgstr "" "Der erweiterte Suchdialog hilft bei der Erstellung komplexer Suchanfragen. " "Sie können verschiedene Kriterien angeben und sobald Sie auf die " "Schaltfläche \"{}Anwenden\"{} klicken, wird eine Suchanfrage für Sie " "generiert und sofort in das Suchfeld eingefügt. Die Suchergebnisse werden " "ebenfalls sofort aktualisiert." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:585 msgid "Viewing" msgstr "Anzeigen" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:591 msgid "Zoom level" msgstr "Zoomstufe" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:597 msgid "" "You can change the scale at which pages are displayed using this control." msgstr "" "Mit diesem Steuerelement können Sie die Vergrößerung ändern, mit der die " "Seiten angezeigt werden." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:600 msgid "View pages as grid" msgstr "Seiten im Raster anordnen" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:608 msgid "" "When clicking this button, Paperwork will try to display pages on 3 " "columns. In this mode, you can drag'n'drop pages to move them inside the " "document or to another document." msgstr "" "Wenn Sie auf diese Schaltfläche klicken, versucht Paperwork, die Seiten in 3 " "Spalten anzuzeigen. In diesem Modus können Sie Seiten per Drag'n'Drop " "innerhalb des Dokuments oder in ein anderes Dokument verschieben." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:611 msgid "View pages as list" msgstr "Seiten in Listenform anordnen" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:619 msgid "" "When clicking this button, pages will be scaled so their width is the " "maximum width allowed by the main window. In this mode, you can select text " "in the page (and then copy it)." msgstr "" "Wenn Sie auf diese Schaltfläche klicken, werden die Seiten so skaliert, dass " "ihre Breite der maximal zulässigen Breite des Hauptfensters entspricht. In " "diesem Modus können Sie Text auf der Seite auswählen (und dann kopieren)." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:621 msgid "Highlight all words" msgstr "Alle Wörter hervorheben" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:629 msgid "" "This option allows to see quickly all the words identified by OCR. Sometimes " "(rarely) OCR misses entire chunk in a page. This option allow to see such " "chunk quickly." msgstr "" "Mit dieser Option können Sie schnell alle von der OCR erkannten Wörter " "sehen. Manchmal (selten) verpasst OCR ganze Abschnitte auf einer Seite. Mit " "dieser Option können Sie solche Abschnitte schnell erkennen." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:631 msgid "Moving pages" msgstr "Verschieben von Seiten" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:633 msgid "Inside a document" msgstr "Innerhalb eines Dokuments" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:635 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:644 msgid "You must display the document pages as a grid (See \\ref{layout:grid})." msgstr "" "Sie müssen die Dokumentseiten als Raster anordnen (siehe~\\ref{layout:grid})." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:640 msgid "" "You can then grab a page (hold the left click button), drag it and drop it " "wherever you want in a document. While dragging, a blue marker will show you " "where the page would drop if you release the left click button of your mouse." msgstr "" "Sie können dann eine Seite greifen (halten Sie die linke Maustaste " "gedrückt), ziehen und an einer beliebigen Stelle im Dokument ablegen. " "Während des Ziehens zeigt Ihnen eine blaue Markierung an, wo die Seite " "landen würde, wenn Sie die linke Maustaste loslassen." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:642 msgid "From a document to another" msgstr "Zwischen Dokumenten" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:647 msgid "" "You can then grab a page (hold the left click button), drag it and drop it " "in the document list, on the document in which you want the page to go." msgstr "" "Sie können dann eine Seite greifen (halten Sie die linke Maustaste " "gedrückt), ziehen und in der Dokumentenliste auf dem Dokument ablegen, in " "dem die Seite erscheinen soll." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:649 msgid "Copying text" msgstr "Kopieren von Text" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:655 msgid "" "You must display the document pages as a list (See \\ref{layout:paged})." msgstr "" "Sie müssen die Dokumentseiten als Liste anordnen (siehe~\\ref{layout:paged})." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:658 msgid "" "You can then select text in a page. Hold the left click button to start " "selecting, mouse the mouse cursor to select more words, then release it." msgstr "" "Sie können dann Text auf einer Seite auswählen. Halten Sie die linke " "Maustaste gedrückt, um die Auswahl zu starten, bewegen Sie den Mauszeiger, " "um weitere Wörter auszuwählen, und lassen Sie dann die Taste los." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:662 msgid "" "You can then copy the selected text, either by pressing Ctrl-C or by using " "the page menu at the bottom right of the main window. Once copied, you can " "paste the selected text in any other application (Ctrl-V)." msgstr "" "Sie können dann den ausgewählten Text kopieren, entweder durch Drücken von " "Strg-C oder über das Seitenmenü unten rechts im Hauptfenster. Nach dem " "Kopieren können Sie den ausgewählten Text in jeder anderen Anwendung " "einfügen (Strg-V)." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:664 msgid "Editing a page" msgstr "Bearbeiten einer Seite" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:670 msgid "Paperwork includes a very simple image editor. It provides 4 functions:" msgstr "" "Paperwork enthält einen sehr einfachen Bildeditor. Er bietet 4 Funktionen:" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Cropping" msgstr "Schneiden" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Rotating the page by 90\\degree (can be rotated multiple times)" msgstr "Drehen der Seite um 90\\degree (es darf mehrfach gedreht werden)" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Rotating the page by -90\\degree (can be rotated multiple times)" msgstr "Drehen der Seite um -90\\degree (es darf mehrfach gedreht werden)" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "" "Automatic Color Equalization: An algorithm that adjust the image brightness, " "contrast and colors to make it as readable as possible." msgstr "" "Automatische Farbabgleich: Ein Algorithmus passt die Bildhelligkeit, den " "Kontrast und die Farben an, um das Bild so gut wie möglich lesbar zu machen." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:681 msgid "Reseting a page" msgstr "Zurücksetzen einer Seite" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:684 msgid "" "Reseting a page returns it to its state when it was scanned or imported, " "before any pre-processing did occur." msgstr "" "Das Zurücksetzen einer Seite setzt diese in den Ausgangszustand zurück, in " "dem sie gescannt oder importiert wurde, bevor irgendeine Vorverarbeitung " "stattfand." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:688 msgid "" "This can be helpful if you made a bad modification on the page (cropped a " "wrong area for instance), if the calibration settings weren't appropriate or " "if pre-processing algorithms messed up the page." msgstr "" "Dies kann hilfreich sein, wenn Sie eine falsche Änderung an der Seite " "vorgenommen haben (z. B. einen falschen Bereich beschnitten haben), wenn die " "Kalibrierungseinstellungen nicht passed gewählt waren oder falls " "Vorverarbeitungsalgorithmen die Seite verkorkst haben." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:691 msgid "Deleting" msgstr "Löschen" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:694 msgid "" "When deleting either documents or pages, they are actually moved in the " "trash bin of your computer." msgstr "" "Wenn Sie ein Dokument oder eine Seite löschen, werden diese in den " "Papierkorb Ihres Computers verschoben." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:698 msgid "" "\\textbf{Important note regarding Flatpak:} A bug may prevent Paperwork from " "moving files to the trash (we are working on it). In that case, Paperwork " "will delete the file directly (no recovery possible)." msgstr "" "\\textbf{Wichtiger Hinweis zu Flatpak:} Ein Fehler kann Paperwork daran " "hindern, Dateien in den Papierkorb zu verschieben (wir arbeiten daran). In " "diesem Fall wird Paperwork die Datei direkt löschen (dann ist keine " "Wiederherstellung möglich)." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:701 msgid "Exporting" msgstr "Export" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:703 msgid "You can export both documents or single pages." msgstr "Sie können sowohl Dokumente als auch einzelne Seiten exportieren." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:707 msgid "" "In both cases, various transformations can be applied before actually " "exporting them. For instance, you can turn color pages into grayscale pages " "before putting them in a brand new PDF (making the resulting PDF smaller)." msgstr "" "In beiden Fällen können vor dem eigentlichen Exportieren verschiedene " "Transformationen vorgenommen werden. Zum Beispiel können Sie Farbseiten in " "Graustufen umwandeln, bevor Sie sie in eine neue PDF-Datei einfügen (wodurch " "die resultierende PDF-Datei kleiner wird)." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:710 msgid "Printing" msgstr "Drucken" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:712 msgid "You can print both documents or single pages." msgstr "Sie können Dokumente und auch einzelne Seiten drucken." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:715 msgid "" "Beware that pages are always sent as images to your printer. So for very big " "documents, a few minutes may go by before the actual printing start." msgstr "" "Beachten Sie, dass die Seiten immer als Bilder an Ihren Drucker gesendet " "werden. Bei sehr großen Dokumenten können also einige Minuten vergehen, " "bevor der eigentliche Druckvorgang beginnt." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:718 msgid "Backup" msgstr "Backup" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:720 msgid "Synchronisation between multiple computers" msgstr "Synchronisation zwischen mehreren Computern" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:726 msgid "" "While Paperwork is a personal document manager, it is not a file " "synchronization application. They are applications dedicated to file " "synchronization that already do that very well. Therefore Paperwork is " "designed to be used with such applications (Nextcloud, Dropbox, OneDrive, " "SparkleShare, etc)." msgstr "" "Paperwork ist zwar ein persönlicher Dokumentenmanager, aber keine Anwendung " "zur Dateisynchronisation. Es gibt Anwendungen, die sich der " "Dateisynchronisation widmen und das bereits sehr gut tun. Daher ist " "Paperwork für die Verwendung mit solchen Anwendungen (Nextcloud, Dropbox, " "OneDrive, SparkleShare, etc.) konzipiert." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:733 msgid "" "When you start Paperwork, one of the first things it does is check the " "content of the work directory. It looks for any changes and updates its " "document list and index accordingly, automatically. So if another instance " "of Paperwork on another computer modified something in the work directory " "and if this change has been synchronized on another computer, the other " "Paperwork will automatically pick up this change when starting." msgstr "" "Nach dem Start prüft Paperwork als erstes den Inhalt des " "Arbeitsverzeichnisses. Es sucht nach Änderungen und aktualisiert seine " "Dokumentenliste und auch den Index automatisch. Wenn also eine andere " "Instanz von Paperwork auf einem anderen Computer etwas im Arbeitsverzeichnis " "geändert hat und diese Änderung auf einem anderen Computer synchronisiert " "wurde, wird das andere Paperwork diese Änderung beim Start automatisch " "übernehmen." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:736 msgid "USB key / USB drive" msgstr "USB-Stick / USB-Platte" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:739 msgid "" "This is the simplest way to share documents. Simply copy your work directory " "to an USB key, tell Paperwork to use it, and you're done." msgstr "" "Dies ist der einfachste Weg, um Dokumente gemeinsam zu nutzen. Kopieren Sie " "einfach Ihr Arbeitsverzeichnis auf einen USB-Stick, weisen Sie Paperwork an, " "es zu verwenden, und schon sind Sie fertig." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:741 msgid "" "Beware: You should backup your USB key from time to time on another one." msgstr "" "Achtung: Sie sollten Ihren USB-Stick von Zeit zu Zeit auf einem anderen " "sichern." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:744 msgid "File Synchronization applications" msgstr "Anwendungen zur Dateisynchronisation" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:750 msgid "" "Those applications synchronize a local directory with a remote server (or " "cloud). All the changes you do in your folder are applied on the server. All " "the changes applied on the servers are applied to the computers that connect " "to it. The server can belong to you or to someone else (usually a company)." msgstr "" "Solche Anwendungen synchronisieren ein lokales Verzeichnis mit einem Remote-" "Server (oder einer Cloud). Alle Änderungen, die Sie in Ihrem Ordner " "vornehmen, werden auch auf dem Server vorgenommen. Alle Änderungen, die auf " "dem Server angewandt wurden, werden auch auf allen verbundenen Computern " "angewendet. Der Server kann Ihnen oder einer anderen Person (normalerweise " "einer Firma) gehören." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:754 msgid "" "Beware: If you choose to host your documents on someone else server " "(DropBox, OneDrive, etc), they can access all your documents. Paperwork does " "not encrypt them." msgstr "" "Achtung: Wenn Sie sich dafür entscheiden, Ihre Dokumente auf einem fremden " "Server zu hosten (DropBox, OneDrive, usw.), kann dieser auf alle Ihre " "Dokumente zugreifen. Paperwork verschlüsselt diese nicht selber." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:758 msgid "" "Paperwork is tested daily with Nextcloud. While this is not the easiest one " "to install, Nextcloud let you host your files yourself. There are other self-" "hosted alternatives that exist: SparkleShare, Syncthing, etc." msgstr "" "Der Paperwork wird täglich mit Nextcloud getestet. Das ist zwar nicht die " "einfachste Variante, aber mit Nextcloud können Sie Ihre Dateien selbst " "hosten. Es gibt noch andere selbst gehostete Alternativen, die existieren: " "SparkleShare, Syncthing, usw." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:761 msgid "" "Using DropBox or OneDrive can make sense if you're sharing not-so-" "confidential documents with others (associations, etc)." msgstr "" "Die Verwendung von DropBox oder OneDrive könnte dann sinnvoll sein, wenn Sie " "nicht ganz so vertrauliche Dokumente mit anderen teilen (Vereine usw.)." #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:764 msgid "Shared folder" msgstr "Gemeinsamer Ordner" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:769 msgid "" "If all your computers are on the same network, you can share your work " "directory. However, be really careful regarding permissions. Being too " "permissive could let a pirate access all your personal documents ! And " "setting them correctly is tricky." msgstr "" "Wenn sich alle Ihre Computer im selben Netzwerk befinden, können Sie Ihr " "Arbeitsverzeichnis gemeinsam nutzen. Seien Sie jedoch sehr vorsichtig, was " "die Zugriffsrechte angeht. Wenn Sie zu freizügig sind, könnte ein böser " "Angreifer Zugriff auf alle Ihre persönlichen Dokumente erhalten! Und diese " "richtig einzustellen ist knifflig." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:772 msgid "" "Beware: Using a shared folder means having a single copy of your work " "directory. You should do regular backups of your work directory." msgstr "" "Achtung! Die Verwendung eines gemeinsamen Ordners bedeutet, dass Sie nur " "eine einzige Kopie Ihres Arbeitsverzeichnisses haben. Sie sollten regelmäßig " "Backups Ihres Arbeitsverzeichnisses erstellen." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:775 msgid "Encryption" msgstr "Verschlüsselung" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:777 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1024 msgid "GNU/Linux" msgstr "GNU/Linux" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:779 msgid "" "GNU/Linux distributions include many tools to encrypt whole directories." msgstr "" "GNU/Linux-Distributionen enthalten viele Programme zum Verschlüsseln ganzer " "Verzeichnisse." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "With Paperwork, there are 2 directories that should be encrypted to protect " "your privacy:" msgstr "" "Für Paperwork sollten 2 Verzeichnisse zum Schutz Ihrer Privatsphäre " "verschlüsselt werden:" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "Your work directory (by default \\textasciitilde /papers, can be changed in " "the settings)" msgstr "" "Ihr Arbeitsverzeichnis (standardmäßig ist das \\textasciitilde /papers, dies " "kann aber in den Einstellungen geändert werden)" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "The cache directory (\\textasciitilde /.local/share/paperwork2, cannot be " "changed) (it contains index files from which the content of your documents " "could be partially recovered)" msgstr "" "Das Cache-Verzeichnis (\\textasciitilde /.local/share/paperwork2, dies kann " "nicht geändert werden) enthält Indexdateien, aus denen der Inhalt Ihrer " "Dokumente teilweise wiederhergestellt werden kann" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:793 msgid "" "Note that if you want to be sure that your data are always encrypted, it's " "recommended to encrypt your whole home directory or even your whole system " "if possible." msgstr "" "Beachten Sie: Wenn Sie sicher sein wollen, dass Ihre Daten immer " "verschlüsselt sind, empfiehlt es sich, wenn möglich Ihr gesamtes Home-" "Verzeichnis oder sogar Ihr gesamtes System zu verschlüsseln." #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:795 msgid "cryptsetup" msgstr "cryptsetup" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:799 msgid "" "Most GNU/Linux distribution installer now provide an optio4n to encrypt your " "whole system or your whole /home with cryptsetup . This is the recommended " "method to protect your documents." msgstr "" "Die meisten GNU/Linux-Distributionen bieten mittlerweile eine Option an, um " "Ihr gesamtes System oder Ihr gesamtes /home mit cryptsetup zu verschlüsseln. " "Dies ist die empfohlene Methode, um Ihre Dokumente zu schützen." #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:801 msgid "Encfs" msgstr "Encfs" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:803 msgid "Encfs can also be used to create encrypted directories easily." msgstr "" "Zur einfachen Erstellung verschlüsselter Verzeichnisse kann auch Encfs " "verwendet werden." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:807 msgid "" "Beware that Encfs seems to have some security weaknesses. So, while it's " "probably enough to prevent a laptop thief from accessing your documents, " "it's likely to be not enough to prevent the NSA or the police from doing " "so ;-)." msgstr "" "Beachten Sie, dass Encfs einige Sicherheitslücken zu haben scheint. Während " "es also wahrscheinlich ausreicht, um eine Laptop-Diebin daran zu hindern, " "auf Ihre Dokumente zuzugreifen, wird es wahrscheinlich nicht ausreichen, um " "die NSA oder die Polizei daran zu hindern ;-)." #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:811 #, no-wrap msgid "" "$ encfs ~/.local/share/.paperwork2 ~/.local/share/paperwork2\n" "$ encfs ~/.papers ~/papers" msgstr "" "$ encfs ~/.local/share/.paperwork2 ~/.local/share/paperwork2\n" "$ encfs ~/.papers ~/papers" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:818 msgid "" "On Windows, you're strongly advised to enable BitLocker to protect your " "documents. If unavailable, there are other applications (Veracrypt, etc)." msgstr "" "Unter Windows wird zum Schutz Ihrer Dokumente dringend empfohlen, BitLocker " "zu aktivieren. Falls nicht verfügbar, gibt es andere Anwendungen (Veracrypt, " "usw.)." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:821 msgid "Keyboard shortcuts" msgstr "Tastenkürzel" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:832 msgid "" "Keyboard shortcuts can be seen by opening the application menu, selecting " "\"Help\" and then \"Shortcuts\"." msgstr "" "Die Tastenkürzel können Sie anzeigen, indem Sie das Anwendungsmenü öffnen, " "\"{}Hilfe\"{} und dann \"{}Tastenkürzel\"{} wählen." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:835 msgid "Paperwork's files locations" msgstr "Dateispeicherorte bei Paperwork" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "By default:" msgstr "Voreingestellt:" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "Configuration: \\textasciitilde /.config/paperwork2.conf" msgstr "Einstellung: \\textasciitilde /.config/paperwork2.conf" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "Index: \\textasciitilde /.local/share/paperwork2" msgstr "Index: \\textasciitilde /.local/share/paperwork2" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "Documents: \\textasciitilde /papers" msgstr "Dokumente: \\textasciitilde /papers" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "" "(same paths are used on Windows; \\textasciitilde{} = C:\\textbackslash " "Users{[}login{]} ; folders are hidden)" msgstr "" "(unter Windows werden dieselben Pfade verwendet; \\textasciitilde{} = C:" "\\textbackslash Users{[}login{]} ; Ordner sind ausgeblendet)" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:848 msgid "" "The index is always updated according based on the documents in the work " "directory. When Paperwork starts, the modification time of each file is used " "to detect changes on the documents." msgstr "" "Der Index wird auf der Grundlage der Dokumente im Arbeitsverzeichnis ständig " "aktualisiert. Beim Start von Paperwork wird die Änderungszeit jeder Datei " "genutzt, um Änderungen an den Dokumenten zu erkennen." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:850 msgid "Work directory layout" msgstr "Innere Struktur des Arbeitsverzeichnisses" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:852 msgid "workdir$|$rootdir = \\textasciitilde /papers (by default)" msgstr "workdir$|$rootdir = \\textasciitilde /papers (by default)" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:854 msgid "Global organisation" msgstr "Globale Struktur" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:856 msgid "In the work directory, you have folders, one per document." msgstr "Im Arbeitsverzeichnis gibt es pro Dokument einen separaten Ordner." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:860 msgid "" "The folder names are (usually) the scan/import date of the document: YYYYMMDD" "\\_hhmm\\_ss{[}\\_{]}. The suffix 'idx' is optional and is just a " "number added in case of name collision." msgstr "" "Die Ordnernamen sind (normalerweise) das Scan-/Importdatum des Dokuments: " "YYYYMMDD\\_hhmm\\_ss{[}\\_{]}. Das Suffix 'idx' ist optional und ist " "einfach eine Zahl, die im Falle einer Namenskollision hinzugefügt wird." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "In every folder you have:" msgstr "In jedem Ordner haben wir:" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "For image documents:" msgstr "Für Bilddokumente:" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "paper.$<$X$>$.jpg: The original page in JPG format (X starts at 1)" msgstr "paper.$<$X$>$.jpg: Die Originalseite im JPG-Format (X beginnt bei 1)" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "paper.$<$X$>$.edited.jpg (optional): The page as edited by the user (X " "starts at 1)" msgstr "" "paper.$<$X$>$.edited.jpg (optional): Die vom Benutzer bearbeite Seite (X " "beginnt bei 1)" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "" "paper.$<$X$>$.words (optional): A hOCR file, containing all the words found " "on the page using the OCR (optional, but required for indexing ; can be " "regenerated with the options \"Redo OCR\")." msgstr "" "paper.$<$X$>$.words (optional): Eine hOCR-Datei, die alle per OCR auf der " "Seite gefunden Wörter enthält (optional, aber für die Indizierung " "erforderlich; kann mit der Option \"{}Wiederhole OCR\"{} neu erzeugt werden)." #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "" "paper.1.thumb.jpg (optional, generated automatically): A thumbnail version " "of the page (faster to load)" msgstr "" "paper.1.thumb.jpg (optional, wird automatisch generiert): Eine " "Miniaturversion der Seite (lädt schneller)" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "labels (optional): a text file containing the labels applied on this document" msgstr "" "labels (optional): eine Textdatei, die alle für dieses Dokument vergebenen " "Schlagwörter enthält" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "extra.txt (optional): extra keywords added by the user" msgstr "" "extra.txt (optional): vom Benutzer hinzugefügte zusätzliche Schlüsselwörter" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "For PDF documents:" msgstr "Für PDF-Dokumente:" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "doc.pdf: the document" msgstr "doc.pdf: das Dokument" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "paper.$<$X$>$.words (optional): A hOCR file, containing all the words found " "on the page using the OCR. Some PDF contains crap instead of the real text, " "so running the OCR on them can sometimes be useful." msgstr "" "paper.$<$X$>$.words (optional): Eine hOCR-Datei, die alle durch die OCR auf " "der Seite gefunden Wörter enthält. Einige PDFs enthalten Mist anstelle des " "echten Textes, so dass es manchmal nützlich sein kann, die OCR über sie " "laufen zu lassen." #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "passwd.txt (optional): PDF password, if the PDF is password-protected." msgstr "" "passwd.txt (optional): PDF Passwort, falls das PDF passwortgeschützt ist." #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "doc.docx / doc.odt / ... (optional): Original file. Converted into PDF (doc." "pdf) so Paperwork can parse and display it more quickly." msgstr "" "doc.docx / doc.odt / ... (optional): Originaldatei. Konvertiert nach PDF (doc" ".pdf) damit Paperwork sie schneller verarbeiten und anzeigen kann." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:894 msgid "Here is an example a work directory organisation:" msgstr "Im folgenden ein Beispiel für eine Arbeitsverzeichnisorganisation:" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:926 #, no-wrap msgid "" "$ find ~/papers\n" "/home/jflesch/papers\n" "/home/jflesch/papers/20130505_1518_00\n" "/home/jflesch/papers/20130505_1518_00/paper.1.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.1.thumb.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.1.words\n" "/home/jflesch/papers/20130505_1518_00/paper.2.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.2.edited.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.2.words\n" "/home/jflesch/papers/20130505_1518_00/paper.3.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.3.words\n" "/home/jflesch/papers/20130505_1518_00/labels\n" "/home/jflesch/papers/20110726_0000_01f\n" "/home/jflesch/papers/20110726_0000_01/paper.1.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.1.thumb.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.1.words\n" "/home/jflesch/papers/20110726_0000_01/paper.2.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.2.words\n" "/home/jflesch/papers/20110726_0000_01/extra.txt\n" "/home/jflesch/papers/20130106_1309_44\n" "/home/jflesch/papers/20130106_1309_44/doc.pdf\n" "/home/jflesch/papers/20130106_1309_44/paper.1.thumb.jpg\n" "/home/jflesch/papers/20130106_1309_44/paper.2.edited.jpg\n" "/home/jflesch/papers/20130106_1309_44/paper.2.words\n" "/home/jflesch/papers/20130106_1309_44/labels\n" "/home/jflesch/papers/20130106_1309_44/extra.txt\n" "/home/jflesch/papers/20130106_1309_44/passwd.txt\n" "/home/jflesch/papers/20130520_1309_44\n" "/home/jflesch/papers/20130520_1309_44/doc.pdf\n" "/home/jflesch/papers/20130520_1309_44/doc.docx\n" "/home/jflesch/papers/20130520_1309_44/labels" msgstr "" "$ find ~/papers\n" "/home/jflesch/papers\n" "/home/jflesch/papers/20130505_1518_00\n" "/home/jflesch/papers/20130505_1518_00/paper.1.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.1.thumb.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.1.words\n" "/home/jflesch/papers/20130505_1518_00/paper.2.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.2.edited.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.2.words\n" "/home/jflesch/papers/20130505_1518_00/paper.3.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.3.words\n" "/home/jflesch/papers/20130505_1518_00/labels\n" "/home/jflesch/papers/20110726_0000_01f\n" "/home/jflesch/papers/20110726_0000_01/paper.1.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.1.thumb.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.1.words\n" "/home/jflesch/papers/20110726_0000_01/paper.2.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.2.words\n" "/home/jflesch/papers/20110726_0000_01/extra.txt\n" "/home/jflesch/papers/20130106_1309_44\n" "/home/jflesch/papers/20130106_1309_44/doc.pdf\n" "/home/jflesch/papers/20130106_1309_44/paper.1.thumb.jpg\n" "/home/jflesch/papers/20130106_1309_44/paper.2.edited.jpg\n" "/home/jflesch/papers/20130106_1309_44/paper.2.words\n" "/home/jflesch/papers/20130106_1309_44/labels\n" "/home/jflesch/papers/20130106_1309_44/extra.txt\n" "/home/jflesch/papers/20130106_1309_44/passwd.txt\n" "/home/jflesch/papers/20130520_1309_44\n" "/home/jflesch/papers/20130520_1309_44/doc.pdf\n" "/home/jflesch/papers/20130520_1309_44/doc.docx\n" "/home/jflesch/papers/20130520_1309_44/labels" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:929 msgid "hOCR files" msgstr "hOCR Dateien" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:931 msgid "With Tesseract, the hOCR file can be obtained with following command:" msgstr "Mit Tesseract kann die hOCR-Datei mit folgendem Befehl erzeugt werden:" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:933 #, no-wrap msgid "tesseract paper..jpg paper. -l hocr && mv paper..html paper..words" msgstr "tesseract paper..jpg paper. -l hocr && mv paper..html paper..words" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:935 msgid "For example:" msgstr "Beispiel:" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:937 #, no-wrap msgid "tesseract paper.1.jpg paper.1 -l fra hocr && mv paper.1.html paper.1.words" msgstr "tesseract paper.1.jpg paper.1 -l fra hocr && mv paper.1.html paper.1.words" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:940 msgid "Label files" msgstr "Schlagwort-Dateien" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:942 msgid "Here is an example of content of a label file:" msgstr "Beispiel für den Inhalt einer Schlagwort-Datei:" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:944 #, no-wrap msgid "facture,#0000b1588c61 logement,#f6b6ffff0000" msgstr "Rechnung,#0000b1588c61 Wohnung,#f6b6ffff0000" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:947 msgid "" "It's always $[$label$]$,$[$color$]$. For a same label, the color should " "always be the same." msgstr "" "Es ist immer $[$label$]$,$[$color$]$. Bei gleichem Schlagwort sollte die " "Farbe immer gleich sein." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:950 msgid "Getting support" msgstr "Unterstützung bekommen" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:953 msgid "" "A forum dedicated to Paperwork exists: \\href{https://forum.openpaper.work}" "{https://forum.openpaper.work}." msgstr "" "Es gibt ein Forum für Paperwork: \\href{https://forum.openpaper.work}" "{https://forum.openpaper.work}." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:956 msgid "" "There is also an IRC channel for live discussions: \\href{https://webchat." "freenode.net/}{Freenode}, channel \\#openpaperwork" msgstr "" "Ebenso gibt es einen IRC-Kanal für Live-Diskussionen: \\href{https://webchat." "freenode.net/}{Freenode}, channel \\#openpaperwork" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:959 msgid "" "If you have questions regarding Paperwork or simply want to chat, those are " "the places to go." msgstr "" "Das sind die Anlaufstellen, wenn Sie Fragen zum Paperwork haben oder einfach " "nur plaudern wollen." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:961 msgid "Reporting issues" msgstr "Probleme melden" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:964 msgid "" "If you noticed a bug in Paperwork (and you are sure it's a bug), you can " "make a bug report." msgstr "" "Wenn Ihnen ein Fehler in Paperwork aufgefallen ist (und Sie sicher sind, " "dass es ein Fehler ist), können Sie einen Fehlerbericht erstellen." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:966 msgid "Bug Tracker" msgstr "Fehlerverfolgung" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:969 msgid "" "One way to create bug reports is to create tickets on \\href{https://gitlab." "gnome.org/World/OpenPaperwork/paperwork/issues}{Paperwork bug tracker: " "https://gitlab.gnome.org/World/OpenPaperwork/paperwork/issues}." msgstr "" "Eine Möglichkeit, Fehlerberichte zu erstellen, ist das Erstellen von Tickets " "auf \\href{https://gitlab.gnome.org/World/OpenPaperwork/paperwork/issues}" "{Paperwork bug tracker: https://gitlab.gnome.org/World/OpenPaperwork/" "paperwork/issues}." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:972 msgid "" "This is the recommended way to submit a bug report if you would like to " "discuss it with Paperwork developpers." msgstr "" "Dies ist der empfohlene Weg, um einen Fehlerbericht einzureichen, falls Sie " "ihn mit den Entwicklern von Paperwork besprechen möchten." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:975 msgid "" "To make sure you include all the required informations, you can use the tool " "integrated in Paperwork (see below)." msgstr "" "Um sicherzustellen, dass Sie alle erforderlichen Informationen angeben, " "können Sie das in Paperwork integrierte Werkzeug verwenden (siehe unten)." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:978 msgid "Automatic bug report" msgstr "Automatischer Fehlerbericht" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:985 msgid "" "Paperwork includes a tool to make reporting bugs easier. It allows you to " "get easily all the required information to make a perfect bug report." msgstr "" "Paperwork enthält ein Werkzeug zur Erleichterung beim Melden von Fehlern. Es " "ermöglicht Ihnen, auf einfache Weise alle erforderlichen Informationen " "zusammenzustellen, um einen perfekten Fehlerbericht zu erstellen." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:989 msgid "" "All attachments are automatically censored to protect your privacy: Document " "contents are blurred in screenshots and logs are censored to remove your " "user name." msgstr "" "Um Ihre Privatsphäre zu schützen, werden alle Anhänge automatisch " "anonymisiert: Dokumentinhalte werden in Screenshots unscharf dargestellt und " "Protokolldateien werden durch Entfernen Ihres Benutzernamens anonymisiert." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:992 msgid "" "If the bug you want to report is related to scanners, please include " "\"Scanner info.\" in the bug report files." msgstr "" "Falls der zu meldende Fehler mit Scannern zusammenhängt, fügen Sie bitte \"{}" "Scanner info.\"{} in den Fehlerbericht ein." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:995 msgid "" "If te bug you want to report is related to a display problem, please include " "\"App. screenshots\" in the bug report files." msgstr "" "Wenn der zu meldende Fehler mit einem Darstellungsproblem zusammenhängt, " "fügen Sie den Fehlerberichtsdateien bitte \"{}App. screenshots\"{} bei." #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:998 msgid "ZIP file" msgstr "ZIP Datei" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1002 msgid "" "You can then obtain a ZIP file with all the data. Please make sure the " "content of the ZIP file does not contain private information (it shouldn't, " "but better safe than sorry). Then you can add this ZIP file to a ticket on " "Gitlab." msgstr "" "Sie erhalten dann eine ZIP-Datei mit allen Daten. Bitte stellen Sie sicher, " "dass der Inhalt der ZIP-Datei keine privaten Informationen enthält (sollte " "er eigentlich nicht, aber sicher ist sicher). Dann können Sie diese ZIP-" "Datei einem Ticket auf Gitlab beifügen." #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1005 msgid "Automatic submission" msgstr "Automatische Übertragung" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1009 msgid "" "You can also let the tool submit the bug report to openpaper.work " "automatically. In that case, you won't be able to discuss the bug with " "developers (or you have to leave a way to contact you in the bug report)." msgstr "" "Sie können auch zulassen, dass das Programm den Fehlerbericht automatisch an " "openpaper.work sendet. In diesem Fall können Sie den Fehler nicht mit den " "Entwicklern besprechen (oder Sie müssen im Fehlerbericht eine Möglichkeit " "zur Kontaktaufnahme mit Ihnen hinterlassen)." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1016 msgid "" "If you use the automatic submission , the tool will give you an URL to see " "the submitted bug report. This URL is private and shouldn't be shared until " "you made sure there is no private information in the bug report. If there is " "private information, you can request deletion of the bug report by sending " "an email to jflesch@openpaper.work (please specify the private URI in your " "mail so we can be sure that you are the one who submitted the bug report)." msgstr "" "Wenn Sie die automatische Übertragung verwenden, gibt Ihnen das Programm " "eine URL, um den übermittelten Fehlerbericht zu sehen. Diese URL ist privat " "und sollte nicht weitergegeben werden, bis Sie sichergestellt haben, dass " "der Fehlerbericht keine privaten Informationen enthält. Wenn es private " "Informationen gibt, können Sie die Löschung des Fehlerberichts beantragen, " "indem Sie eine E-Mail an jflesch@openpaper.work senden (bitte geben Sie die " "private URI in Ihrer E-Mail an, damit wir sicher sein können, dass Sie " "derjenige sind, der den Fehlerbericht eingereicht hat)." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1019 msgid "Uninstalling" msgstr "Deinstallation" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1022 msgid "" "Paperwork can be uninstalled. Uninstalling Paperwork \\emph{will never} " "remove your work directory or your documents." msgstr "" "Paperwork kann deinstalliert werden. Bei der Deinstallation von Paperwork " "werden Ihr Arbeitsverzeichnis oder Ihre Dokumente niemals entfernt." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1028 msgid "" "If you installed Paperwork using the package manager from your distribution " "(the recommended way), the uninstallation method depends on the package " "manager." msgstr "" "Wenn Sie Paperwork mit dem Paketmanager Ihrer Distribution installiert haben " "(der empfohlene Weg), hängt die Deinstallationsmethode vom Paketmanager ab." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1031 msgid "" "For instance, on GNU/Linux Debian or GNU/Linux Ubuntu, the following command " "will take care of it:" msgstr "" "Beispielweise kümmert sich unter Unter GNU/Linux Debian oder GNU/Linux " "Ubuntu folgende Befehl darum:" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1034 #, no-wrap msgid "sudo apt remove --purge paperwork-\\*" msgstr "sudo apt remove --purge paperwork-\\*" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1037 msgid "If you installed it using Flatpak, you can use the following command:" msgstr "" "Wenn Sie es mit Flatpak installiert haben, können Sie den folgenden Befehl " "verwenden:" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1040 #, no-wrap msgid "flatpak --user uninstall work.openpaper.Paperwork" msgstr "flatpak --user uninstall work.openpaper.Paperwork" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1043 msgid "Windows 10" msgstr "Windows 10" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1047 msgid "" "Paperwork can be uninstalled as any Windows applications, by going in " "Windows Control Panel, clicking on \"Applications\", finding Paperwork in " "the list, and then clicking on \"uninstall\"." msgstr "" "Paperwork kann wie jede Windows-Anwendung deinstalliert werden, indem Sie in " "der Windows-Systemsteuerung auf \"{}Anwendungen\"{} klicken, Paperwork in " "der Liste suchen und dann auf \"{}deinstallieren\"{} klicken." paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/data/l10n/fr.po000066400000000000000000002267461417573700700265060ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-09 12:56+0200\n" "PO-Revision-Date: 2021-11-29 21:07+0000\n" "Last-Translator: Jerome Flesch \n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 4.9\n" #. type: Plain text #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:11 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "\\date{}" msgstr "" "\\date{}\n" "\\usepackage[french]{babel}" #. type: title{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:11 msgid "Welcome to Paperwork !" msgstr "Bienvenue sur Paperwork !" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:20 msgid "" "They are going to drive you crazy. Your phone operator, your bank, your " "daughter's school, your dog's veterinarian, even your ISP; it seems like all " "of them are trying to drown you under tons of papers. Papers you have to " "read, classify, and memorize just in case you may need them later. Most of " "the time you won't, which means you waste your energy for nothing." msgstr "" "Ils vont vous rendre fou. Votre opérateur téléphonique, votre banque, " "l'école de votre fille, votre vétérinaire, votre FAI, etc. Ils semblent tous " "essayer de vous noyer sous des tonnes de papiers. Papiers que vous devez " "lire, classer et mémoriser, juste au cas où vous en auriez besoin plus tard. " "La plupart du temps, ce ne sera pas le cas, ce qui signifie que vous avez " "gâché votre énergie pour rien." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:24 msgid "" "Paperwork will help you get rid of all those papers by turning them into " "searchable documents. It's simple: just scan and forget. Looking for a " "specific paper? Just type in a few keywords and tada" msgstr "" "Paperwork va vous aider à vous débarrasser de tout ces papiers en les " "transformant en documents cherchables. C'est simple : scannez et oubliez. " "Vous cherchez un papier spécifique ? Tapez juste quelques mots-clés, et " "tada !" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:28 msgid "Documents and pages" msgstr "Les documents et les pages" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:36 msgid "" "Paperwork's interface is composed of two panels. On the left (green) is the " "list of all your documents sorted by the date they were imported. On the " "right (blue) are the pages of the currently selected paper." msgstr "" "L'interface de Paperwork est composé de 2 panneaux. Sur la gauche (en vert), " "la liste de tout vos documents, triés par date d'import. Sur la droite (en " "bleu), les pages du document actuellement sélectionné." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:41 msgid "" "You can add papers from several sources, depending on the devices connected " "to your computer: scanner flatbed, scanner feeder, camera, etc. You have no " "scanner at home? You can still use the scanner you have at work. Paperwork " "will easily import PDF and image files." msgstr "" "Vous pouvez ajouter des papiers depuis plusieurs sources, en fonction des " "périphériques connectés à votre ordinateur : scanner simple, scanner à bac " "d'alimentation, caméra, ... Vous n'avez pas de scanner à la maison ? Vous " "pouvez utiliser celui de votre travail. Paperwork peut importer des PDFs et " "des fichiers images sans problème." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:44 msgid "Find" msgstr "Trouver" #. type: wrapfigure #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:50 msgid "{r}{0.5\\textwidth}" msgstr "{r}{0.5\\textwidth}" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:56 msgid "" "Find what you need, when you need it. Type a few keywords in the search bar " "and the list of papers will shrink to only the relevant content. This is " "where the magic happens: Paperwork uses optical character recognition (OCR) " "to convert your papers into simple text files, so it's easy to search for " "text." msgstr "" "Trouvez ce dont vous avez besoin, quand vous en avez besoin. Tapez quelques " "mots-clés et la liste des documents se réduira à ce que vous cherchez. C'est " "là que la magie s'opère : Paperwork utilise la reconnaissance optique de " "caractères (ROC) pour convertir vos papiers en simples fichiers textes pour " "qu'il soit facile d'y chercher des mots." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:59 msgid "Export" msgstr "Exporter" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:69 msgid "" "Sometimes you may want to export a document to send it to someone else. " "Multiple formats are supported: .pdf, .jpg, .txt, etc. And of course, paper " "(requires a printer, sold separately)." msgstr "" "Parfois, vous allez vouloir exporter un document pour l'envoyer à quelqu'un " "d'autre. Plusieurs formats sont supportés : .pdf, .jpeg, .txt, ... et bien " "entendu, le format papier (nécessite une imprimante, vendue séparément)." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:72 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:113 msgid "Labels and additional keywords" msgstr "Étiquettes et mot-clés additionnels" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:84 msgid "" "You answered an important email and you want to keep track of it? The paper " "you scanned was so unreadable that Paperwork failed to recognize some " "important keywords? Add keywords to your paper so you won't miss anything! " "All the keywords you add will be searchable, as if they were directly " "written on the paper you scanned." msgstr "" "Vous avez répondu à un e-mail important et vous souhaitez en garder une " "trace ? Le papier que vous avez scanné était si illisible que Paperwork n'a " "pas pu le relire ? Ajoutez vous-même des mots-clés à vos papiers pour que " "vous puissiez les retrouver ! Tous les mots que vous ajouterez pourrons être " "retrouvés, comme si ils avaient été écrits dans les pages que vous avez " "scannées." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:89 msgid "" "You would like to organize your documents a bit more? You can also add " "labels to your documents. Each label has its own color. With time, Paperwork " "will learn which labels go on which documents and will automatically apply " "them on new documents." msgstr "" "Vous souhaitez trier un peu vos documents ? Vous pouvez aussi leur ajouter " "des étiquettes. Pour plus de lisibilité, chaque étiquette a sa propre " "couleur. Avec le temps, Paperwork va apprendre quelles étiquettes vont sur " "quels documents, et il les placera automatiquement pour vous sur chaque " "nouveau document." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:92 msgid "Your first documents" msgstr "Vos premiers documents" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:102 msgid "" "Click the + button, the scan button, and that's all folks! You are now aware " "of the main features of Paperwork. You can start using it by adding your " "first own paper." msgstr "" "Cliquez sur le bouton +, cliquez sur le bouton \frquote{scanner}, et c'est " "tout ! Vous connaissez désormais les principales fonctionnalités de " "Paperwork. Vous pouvez maintenant commencer à l'utiliser en ajoutant vos " "premier documents." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:105 msgid "" "This document will automatically disappear from your document list as soon " "as you have created or imported your first document." msgstr "" "Ce document disparaîtra automatiquement de votre liste de documents aussitôt " "que vous aurez créé ou importé votre premier document." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:108 msgid "Need more help ?" msgstr "Besoin d'aide ?" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:116 msgid "" "If you need more help, there is a comprehensive manual you can find in the " "help section of Paperwork." msgstr "" "Si vous avez besoin de plus d'aide, il y a un manuel plus complet intégré " "dans Paperwork." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:119 msgid "" "We hope that you'll enjoy this piece of software. If you like it please tell " "us, and if you don't please tell us why!" msgstr "" "Nous espérons que vous apprécierez ce logiciel. Si tel est le cas, dites-le " "nous, et sinon, dites-nous comment l'améliorer !" #. type: hypersetup{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "" "colorlinks, citecolor=black, filecolor=black, linkcolor=black, " "urlcolor=black, linktoc=all," msgstr "" "colorlinks, citecolor=black, filecolor=black, linkcolor=black, " "urlcolor=black, linktoc=all," #. type: title{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "Paperwork manual" msgstr "Manuel de Paperwork" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:32 msgid "Introduction" msgstr "Introduction" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:41 msgid "" "Most personal documents are fairly recurrent: earning statements, rent " "bills, electricity bills, etc. For most unorganized people, having to find " "them back later is worrisome, at best. For most organized people, naming and " "sorting them is as tedious as watching paint dry." msgstr "" "La plupart des documents personnels sont plutôt récurrents : fiches de " "paies, quittances de loyer, facture d'électricité, etc. Pour la plupart des " "gens mal-organisés, devoir les retrouver plus tard est, au mieux, " "inquiétant. Pour la plupart des gens organisés, les nommer et les trier est " "aussi ennuyeux que regarder la pluie tomber." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:45 msgid "" "The main idea behind Paperwork is that managing documents is a computer " "job. Humans should do as little as possible while machines do most of the " "work. The end goal here is \"scan \\& forget\"." msgstr "" "L'idée principale derrière Paperwork est que gérer les documents est un " "travail d'ordinateurs. Les humains devraient faire aussi peu que possible " "pendant que les machines font le gros du travail. L'objectif final ici est " "\"scanner \\& oublier\"." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:49 msgid "" "If you're looking for a software that will let you name each document " "individually, organize them in complex hierachy, tag them manually each " "time, fix OCR minor glitches, etc, then Paperwork is not for you." msgstr "" "Si vous cherchez un logiciel qui vous laissera nommer les documents " "individuellement, les organiser dans des hiérarchies complexes, les marquer " "manuellement à chaque fois, corriger les ratés mineurs de l'OCR, etc, alors " "Paperwork n'est pas pour vous." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:52 msgid "Definitions" msgstr "Définitions" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:54 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:158 msgid "Work directory" msgstr "Répertoire de travail" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:57 msgid "" "Paperwork stores all your documents in a single directory: the work " "directory. In this directory, each document has its own sub-directory." msgstr "" "Paperwork enregistre tous vos documents dans un seul répertoire : Le " "répertoire de travail. Dans ce répertoire, chaque document a son propre sous-" "répertoire." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:61 msgid "" "While this makes Paperwork hard to use with other tools, it has one major " "advantage: You don't have to worry about file names and directory structures " "anymore." msgstr "" "Bien que cela rende Paperwork difficile à utiliser avec d'autres outils, ça " "a un avantage majeur : Vous n'avez pas à vous inquiéter des noms des " "fichiers et de la structure des répertoires." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:63 msgid "Document" msgstr "Document" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:71 msgid "" "In Paperwork, a document is a set of pages. On disk, it can either be a set " "of JPEG files or a PDF file." msgstr "" "Dans Paperwork, un document est ensemble de pages. Sur le disque, ça peut " "être un jeu de fichiers JPEG ou un fichier PDF." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:74 msgid "" "Documents are identified only by a date. It can either be the date you " "imported them (default) or some date of your choosing." msgstr "" "Chaque document est identifié uniquement par une date. Il peut s'agir de la " "date à laquelle vous l'avez importé (par défaut) ou une date de votre choix." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:77 msgid "" "They are displayed on the left side of the main window (green part on the " "screenshot above)." msgstr "" "Ils sont affichés sur le coté gauche de la fenêtre principale (partie en " "vert sur la capture d'écran ci-dessous)." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:80 msgid "Page" msgstr "Page" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:82 msgid "" "In Paperwork, a page is just an image and the word positions on this image." msgstr "" "Dans Paperwork, une page est juste une image et les positions des mots sur " "cette image." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:88 msgid "" "Images can come from a scanner or be imported. In those cases, it is stored " "as a JPEG files and text is extracted using OCR (Optical Character " "Recognition). OCR is a fairly long process. It can take up to a few minutes " "for each page. So the text extracted from images is stored in hOCR files " "beside the JPEG files." msgstr "" "Les images peuvent venir d'un scanner ou être importées. Dans ces cas, elles " "sont enregistrées sous forme de fichiers JPEG et leur texte est extrait " "grâce à la ROC (Reconnaissance Optique de Caractères). ROC est un processus " "assez long. Il peut prendre jusqu'à plusieurs minutes pour chaque page. Par " "conséquent, le texte extrait est enregistré dans des fichiers hOCR à coté " "des fichiers JPEG." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:91 msgid "" "Pages can also be the pages from a PDF file. In that case, by default, " "Paperwork just stores a copy of the PDF file." msgstr "" "Les pages peuvent aussi être les pages d'un fichier PDF. Dans ce cas, par " "défaut, Paperwork enregistre simplement une copie du fichier PDF." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:95 msgid "" "Paperwork does not track whether a page is recto or verso. Paperwork does " "not track the paper size corresponding to a page (A4, Letter, etc)." msgstr "" "Paperwork ne mémorise pas si une page est le recto ou le verso. Paperwork ne " "mémorise pas la taille du papier (A4, Letter, etc) correspondant à la page." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:98 msgid "" "Pages are displayed on the right side of the main window (blue part on the " "screenshot above)." msgstr "" "Les pages sont affichées sur le coté droit de la fenêtre principale (la " "partie en bleu sur la capture d'écran ci-dessus)." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:101 msgid "Indexation and Keywords" msgstr "Indexation et mots-clés" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:108 msgid "" "Of course, you need a way to find back your documents. Paperwork manages an " "index with all the keywords found in your documents." msgstr "" "Bien sûr, vous avez besoin de pouvoir retrouver vos documents. Paperwork " "gère un index avec tout les mots-clés trouvés dans vos documents." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:110 msgid "Just type in a few keywords, and you will get your documents back." msgstr "Tapez juste quelques mots-clés, et vous retrouverez vos documents." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:128 msgid "" "Unfortunately, sometimes, documents don't contain the keywords needed to " "find them back. Also OCR is not a perfectly realiable process and may not " "work." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:131 msgid "" "To mitigate those issues, you can add labels (or tags) on your documents and " "provide additional keywords. Both are added to the index." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:134 msgid "" "Labels are displayed beside documents. Additional keywords are almost never " "displayed." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:137 msgid "Settings" msgstr "Réglages" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:139 msgid "Accessing the settings" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:166 msgid "" "The work directory is the directory where you want all your documents " "stored. It can be a standard folder, a folder synchronized across multiple " "computers or on a network share." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:169 msgid "" "Once you close the settings dialog, the work directory will be scanned and " "Paperwork index will be updated according to its index." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:172 msgid "" "Each time Paperwork starts, it will look for changes in this folder and " "synchronize its index accordingly." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:175 msgid "Scanner" msgstr "Scanner" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:182 msgid "Device" msgstr "Périphérique" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:189 msgid "" "When starting, Paperwork looks for scanners. The scanner to use can be " "selected in the settings." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:191 msgid "Webcams, file storage, etc, cannot be used. Only paper-eaters." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:194 msgid "Scan Mode" msgstr "Mode du scanner" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:202 msgid "" "Most modern scanners scan in color in a reasonable time. However some older " "scanners scan much faster in grayscale or even in black\\&white. Here you " "can select the mode to use." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:205 msgid "Scan Resolution" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:212 msgid "" "Scanner resolution defines how detailed the images coming from your scanner " "must be." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "Higher resolutions mean" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "longer scans," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "longer OCR," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "more time to display," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "more space used on disk," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "but also better OCR." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "Lower resolutions mean" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "shorter scans," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "shorter OCR," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "less time to display," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "less space used on disk," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "but also inferior OCR," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "and possibly unreadable image (even by a human)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:234 msgid "" "300 dpi is considered a good trade-off. You may want to reduce it to 200 dpi " "on slow computers." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:237 msgid "Scanner calibration" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:247 msgid "" "Scanners tend to provide images actually bigger than the scanned pages. " "Since most of the time, you will always scan pages having the same size (A4 " "or Letter usually), Paperwork provides an option called scanner calibration. " "Scanner calibration in Paperwork is simply an area that will always be " "cropped out of images coming from the scanner." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:250 msgid "OCR" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:257 msgid "" "By default, Paperwork uses Tesseract for the OCR. If unavailable, it falls " "back on Cuneiform." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:260 msgid "" "On Linux, if installed with Flatpak, Paperwork is always provided with " "Tesseract. On Windows, Paperwork is always provided with Tesseract." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:263 msgid "" "To get better results, OCR tool need to know the language used in the " "document(s)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:268 msgid "" "The language available in the settings dialog of Paperwork are those " "understood by the OCR tool. If your language is not in the list, it means " "the OCR tool doesn't have the data required to read your language and you " "must install them." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:271 msgid "Adding languages" msgstr "" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:273 msgid "Flatpak" msgstr "Flatpak" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:279 #, no-wrap msgid "" "# is a list of 2-letters language codes separated ';'\n" "# ex: en;fr;de\n" "flatpak config --user --set languages \"\"\n" "flatpak update --user" msgstr "" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:283 msgid "Debian" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:288 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:304 #, no-wrap msgid "" "# is a 3-letter language code\n" "# ex: 'fra' for French\n" "$ sudo apt-get install tesseract-ocr tesseract-ocr-" msgstr "" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:291 msgid "Fedora" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:296 #, no-wrap msgid "" "# is a 3-letter language code\n" "# ex: 'fra' for French\n" "$ sudo dnf install tesseract tesseract-langpack-" msgstr "" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:299 msgid "Ubuntu" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:307 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:815 msgid "Windows" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:310 msgid "" "Tesseract and all its data files are provided by Paperwork's installer. You " "can rerun the installer to install other languages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:314 msgid "" "If a language is not available in the installer, it either means it hasn't " "been packaged (in which case you can request it), or there is no data file " "available yet for this language." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:317 msgid "Disabling OCR" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:323 msgid "" "When you scan a page using Paperwork, Paperwork will immediately run the OCR " "on it. This process may take a while for each page. In case you want to scan " "a lot of pages quickly (for instance, the first time you use Paperwork), OCR " "can be temporarily disabled. To disable OCR, you simply have to unselect all " "OCR languages." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:326 msgid "Updates" msgstr "Mises à jour" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:334 msgid "" "If you enable this option, when Paperwork starts, Paperwork will look for " "updates if it hasn't done so for a week or more. To know if a new version is " "available, it has to send an HTTPS query to 'openpaper.work'." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:336 msgid "If an update is found, it will notify you but it won't install it." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:339 msgid "New document" msgstr "Nouveau document" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:345 msgid "" "By default, in the document list, Paperwork includes a document called \"New " "document\". If you open it, it always appears empty. This document actually " "doesn't exist yet on disk, but will exist as soon as you put a page in it. " "You can add pages in it by scanning, importing file(s) or dropping a page " "from another in it." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:350 msgid "" "As soon as you put any content in it, this document will get its own date " "(the current one by default). In the document list, \"New document\" will be " "replaced by this date, and a new \"New document\" will be added to the " "document list." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:359 msgid "" "If you are currently searching something (see the chapter \"Searching\"), " "only search results are displayed and therefore this \"New document\" isn't " "displayed. You can get it back by clicking the button \"+\" in the top left " "corner of the main window." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:362 msgid "Scanning" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:368 msgid "" "If a scanner has been selected in the settings, you can use it to scan pages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:372 msgid "" "In the header bar, there is a button to add pages. The small arrow on the " "right gives access to possible page sources. Those page sources include your " "scanner sources (Flatbed, Feeder)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:375 msgid "" "Once you've selected the scanner source you want to use, you can click on " "the button \"Scan from ...\"." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:377 msgid "This will start a scan session:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "" "Scanned pages are appended at the end of the current document. If you use a " "feeder, Paperwork will scan pages until the feeder is empty." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will then crop them according to scanner calibration." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will run OCR on them" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will index them" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:389 msgid "" "If this scan session creates a new document, Paperwork will try to set " "labels automatically on the document." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:392 msgid "Importing" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:399 msgid "Images" msgstr "Images" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:402 msgid "" "Paperwork supports a lot of file formats. It supports JPEG, PNG, GIF, BMP, " "TIFF, etc." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:404 msgid "Each image file is considered as a page." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:408 msgid "" "Images are always appended to the document currently opened. Simply select " "an empty document (\"New document\") to create a new document while " "importing." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:412 msgid "" "OCR is always run on imported images. If the imported image is the first " "page of a new document, Paperwork will automatically apply documents labels." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:416 msgid "" "Note that Paperwork is a document manager. While it can, it is not designed " "to handle images with only very little text or photos. Automatic labeling " "will not work correctly on such documents." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:420 msgid "" "The OCR (Tesseract) works very well with black text on white background. " "Automatic labeling uses recognized text and requires as many keywords on the " "first page as possible." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:423 msgid "PDF" msgstr "PDF" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:429 msgid "" "Each PDF is always considered as a whole document. They are never appended " "to existing document. They are copied and renamed in the work directory, but " "their content is not modified. Paperwork always keeps the original PDF file " "as is, even if you edit some of its pages: the edited pages are stored " "beside the PDF file." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:434 msgid "" "Paperwork will look for pages with no text attached. On those pages, it will " "automatically run OCR. Once all the pages have been examined, it will " "automatically apply document labels. Note that this process may take a few " "minutes for big PDFs files." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:437 msgid "" "If the PDF is already part of your documents, Paperwork will simply ignore " "it." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:440 msgid "Many PDFs in one shot" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:445 msgid "" "When importing, if you select a folder, Paperwork will browse this folder " "and look for PDFs to import. Already-imported PDFs are simply ignored. " "Folder is browsed recursively (all the folders inside the folder are also " "examined)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:448 msgid "Labels" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:464 msgid "" "There is currently one constraint in Paperwork: Each label must be on at " "least one document. Otherwise, when you will restart Paperwork, labels " "without documents will disappear." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:467 msgid "Creating new labels" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:475 msgid "" "You can click on the gray rectangle on the left side to pick the label " "color. You can enter the label name in text field between the gray " "rectangle and the button \"+\"." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:478 msgid "" "Once you click on the button \"+\", the label will be added to the current " "document." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:481 msgid "" "The label is actually added once you close document properties. Paperwork " "will then update its index accordingly." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:484 msgid "Setting labels on documents" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:488 msgid "" "When you open document properties, the label list appears. On the left side " "of each label color, you have a button. This button allows you to add or " "remove labels on the current document." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:491 msgid "" "The changes are actually written on disk once you close the document " "properties. Paperwork will update its index accordinly." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:494 msgid "Modifying a label color" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:497 msgid "" "When you open document properties, you can click on a label color to change " "it. A dialog will let you pick the new color." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:501 msgid "" "Label color will actually be changed on disk when you close the document " "properties. Paperwork will then update the label on all the documents that " "use it." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:504 msgid "Modifying a label name" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:507 msgid "" "When you open document properties, you can click on a label string to change " "it. A dialog will let you type in the new name." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:511 msgid "" "Label name will actually be changed on disk when you close the document " "properties. Paperwork will then update the label on all the documents that " "use it and then reindex them all." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:514 msgid "Deleting a label" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:517 msgid "" "To the right of each label is white-on-black cross button. Clicking on it " "will allow you to delete a label." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:520 msgid "" "Once you will close the document properties, the label will be removed from " "all the documents having it. Paperwork will then update its index " "accordingly." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:523 msgid "" "Beware: Once you have closed document properties, there is no way to put " "back the deleted label." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:526 msgid "Automatic label guessing" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:531 msgid "" "Paperwork does use artificial intelligence. It uses a fairly simple method " "actually: \\href{https://en.wikipedia.org/wiki/Naive_Bayes_classifier}{Naive " "Bayes classifiers}. It's the same technology used by email clients to " "classify mails as spam/non-spam." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:537 msgid "" "Based on all the keywords in all your documents that have (or haven't) a " "label, it can estimate a probability that a document containing the same " "keywords should have or shouldn't have this same label. If the probability " "is high enough, it puts the label on the document automatically when you " "import it or scan it." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:541 msgid "" "Of course, this approach means that Paperwork needs enough samples to work " "reliably. You can expect it to start working once you have about 100 " "documents or more (and only for labels that are on more than 10 documents or " "more)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:544 msgid "Searching" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:546 msgid "Simple search" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:553 msgid "" "You simply enter keywords in the search field. In a few seconds, you will " "get all the documents containing those keywords." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:557 msgid "" "Paperwork does a \"fuzzy\" search: documents with keywords close to the one " "you gave but not identical are also returned (for instance, 'flech' instead " "of 'flesch')." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:562 msgid "" "You can also use \\href{https://whoosh.readthedocs.io/en/latest/querylang." "html}{Whoosh query language} to make more complex queries. If you want " "examples, you can use the advanced search dialog described below." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:565 msgid "Advanced search" msgstr "Recherche avancée" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:582 msgid "" "The advanced search dialog helps creating complex search queries. You can " "specify various criterias and once you click on the apply button, it will " "generate a search query for you and put it immediately in the search field. " "Search results will immediately be refreshed as well." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:585 msgid "Viewing" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:591 msgid "Zoom level" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:597 msgid "" "You can change the scale at which pages are displayed using this control." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:600 msgid "View pages as grid" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:608 msgid "" "When clicking this button, Paperwork will try to display pages on 3 " "columns. In this mode, you can drag'n'drop pages to move them inside the " "document or to another document." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:611 msgid "View pages as list" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:619 msgid "" "When clicking this button, pages will be scaled so their width is the " "maximum width allowed by the main window. In this mode, you can select text " "in the page (and then copy it)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:621 msgid "Highlight all words" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:629 msgid "" "This option allows to see quickly all the words identified by OCR. Sometimes " "(rarely) OCR misses entire chunk in a page. This option allow to see such " "chunk quickly." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:631 msgid "Moving pages" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:633 msgid "Inside a document" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:635 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:644 msgid "You must display the document pages as a grid (See \\ref{layout:grid})." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:640 msgid "" "You can then grab a page (hold the left click button), drag it and drop it " "wherever you want in a document. While dragging, a blue marker will show you " "where the page would drop if you release the left click button of your mouse." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:642 msgid "From a document to another" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:647 msgid "" "You can then grab a page (hold the left click button), drag it and drop it " "in the document list, on the document in which you want the page to go." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:649 msgid "Copying text" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:655 msgid "" "You must display the document pages as a list (See \\ref{layout:paged})." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:658 msgid "" "You can then select text in a page. Hold the left click button to start " "selecting, mouse the mouse cursor to select more words, then release it." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:662 msgid "" "You can then copy the selected text, either by pressing Ctrl-C or by using " "the page menu at the bottom right of the main window. Once copied, you can " "paste the selected text in any other application (Ctrl-V)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:664 msgid "Editing a page" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:670 msgid "Paperwork includes a very simple image editor. It provides 4 functions:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Cropping" msgstr "Recadrage" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Rotating the page by 90\\degree (can be rotated multiple times)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Rotating the page by -90\\degree (can be rotated multiple times)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "" "Automatic Color Equalization: An algorithm that adjust the image brightness, " "contrast and colors to make it as readable as possible." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:681 msgid "Reseting a page" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:684 msgid "" "Reseting a page returns it to its state when it was scanned or imported, " "before any pre-processing did occur." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:688 msgid "" "This can be helpful if you made a bad modification on the page (cropped a " "wrong area for instance), if the calibration settings weren't appropriate or " "if pre-processing algorithms messed up the page." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:691 msgid "Deleting" msgstr "Effacer" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:694 msgid "" "When deleting either documents or pages, they are actually moved in the " "trash bin of your computer." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:698 msgid "" "\\textbf{Important note regarding Flatpak:} A bug may prevent Paperwork from " "moving files to the trash (we are working on it). In that case, Paperwork " "will delete the file directly (no recovery possible)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:701 msgid "Exporting" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:703 msgid "You can export both documents or single pages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:707 msgid "" "In both cases, various transformations can be applied before actually " "exporting them. For instance, you can turn color pages into grayscale pages " "before putting them in a brand new PDF (making the resulting PDF smaller)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:710 msgid "Printing" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:712 msgid "You can print both documents or single pages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:715 msgid "" "Beware that pages are always sent as images to your printer. So for very big " "documents, a few minutes may go by before the actual printing start." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:718 msgid "Backup" msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:720 msgid "Synchronisation between multiple computers" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:726 msgid "" "While Paperwork is a personal document manager, it is not a file " "synchronization application. They are applications dedicated to file " "synchronization that already do that very well. Therefore Paperwork is " "designed to be used with such applications (Nextcloud, Dropbox, OneDrive, " "SparkleShare, etc)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:733 msgid "" "When you start Paperwork, one of the first things it does is check the " "content of the work directory. It looks for any changes and updates its " "document list and index accordingly, automatically. So if another instance " "of Paperwork on another computer modified something in the work directory " "and if this change has been synchronized on another computer, the other " "Paperwork will automatically pick up this change when starting." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:736 msgid "USB key / USB drive" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:739 msgid "" "This is the simplest way to share documents. Simply copy your work directory " "to an USB key, tell Paperwork to use it, and you're done." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:741 msgid "" "Beware: You should backup your USB key from time to time on another one." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:744 msgid "File Synchronization applications" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:750 msgid "" "Those applications synchronize a local directory with a remote server (or " "cloud). All the changes you do in your folder are applied on the server. All " "the changes applied on the servers are applied to the computers that connect " "to it. The server can belong to you or to someone else (usually a company)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:754 msgid "" "Beware: If you choose to host your documents on someone else server " "(DropBox, OneDrive, etc), they can access all your documents. Paperwork does " "not encrypt them." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:758 msgid "" "Paperwork is tested daily with Nextcloud. While this is not the easiest one " "to install, Nextcloud let you host your files yourself. There are other self-" "hosted alternatives that exist: SparkleShare, Syncthing, etc." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:761 msgid "" "Using DropBox or OneDrive can make sense if you're sharing not-so-" "confidential documents with others (associations, etc)." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:764 msgid "Shared folder" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:769 msgid "" "If all your computers are on the same network, you can share your work " "directory. However, be really careful regarding permissions. Being too " "permissive could let a pirate access all your personal documents ! And " "setting them correctly is tricky." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:772 msgid "" "Beware: Using a shared folder means having a single copy of your work " "directory. You should do regular backups of your work directory." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:775 msgid "Encryption" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:777 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1024 msgid "GNU/Linux" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:779 msgid "" "GNU/Linux distributions include many tools to encrypt whole directories." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "With Paperwork, there are 2 directories that should be encrypted to protect " "your privacy:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "Your work directory (by default \\textasciitilde /papers, can be changed in " "the settings)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "The cache directory (\\textasciitilde /.local/share/paperwork2, cannot be " "changed) (it contains index files from which the content of your documents " "could be partially recovered)" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:793 msgid "" "Note that if you want to be sure that your data are always encrypted, it's " "recommended to encrypt your whole home directory or even your whole system " "if possible." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:795 msgid "cryptsetup" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:799 msgid "" "Most GNU/Linux distribution installer now provide an optio4n to encrypt your " "whole system or your whole /home with cryptsetup . This is the recommended " "method to protect your documents." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:801 msgid "Encfs" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:803 msgid "Encfs can also be used to create encrypted directories easily." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:807 msgid "" "Beware that Encfs seems to have some security weaknesses. So, while it's " "probably enough to prevent a laptop thief from accessing your documents, " "it's likely to be not enough to prevent the NSA or the police from doing " "so ;-)." msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:811 #, no-wrap msgid "" "$ encfs ~/.local/share/.paperwork2 ~/.local/share/paperwork2\n" "$ encfs ~/.papers ~/papers" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:818 msgid "" "On Windows, you're strongly advised to enable BitLocker to protect your " "documents. If unavailable, there are other applications (Veracrypt, etc)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:821 msgid "Keyboard shortcuts" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:832 msgid "" "Keyboard shortcuts can be seen by opening the application menu, selecting " "\"Help\" and then \"Shortcuts\"." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:835 msgid "Paperwork's files locations" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "By default:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "Configuration: \\textasciitilde /.config/paperwork2.conf" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "Index: \\textasciitilde /.local/share/paperwork2" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 #, fuzzy #| msgid "Documents and pages" msgid "Documents: \\textasciitilde /papers" msgstr "Les documents et les pages" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "" "(same paths are used on Windows; \\textasciitilde{} = C:\\textbackslash " "Users{[}login{]} ; folders are hidden)" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:848 msgid "" "The index is always updated according based on the documents in the work " "directory. When Paperwork starts, the modification time of each file is used " "to detect changes on the documents." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:850 msgid "Work directory layout" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:852 msgid "workdir$|$rootdir = \\textasciitilde /papers (by default)" msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:854 msgid "Global organisation" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:856 msgid "In the work directory, you have folders, one per document." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:860 msgid "" "The folder names are (usually) the scan/import date of the document: YYYYMMDD" "\\_hhmm\\_ss{[}\\_{]}. The suffix 'idx' is optional and is just a " "number added in case of name collision." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "In every folder you have:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "For image documents:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "paper.$<$X$>$.jpg: The original page in JPG format (X starts at 1)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "paper.$<$X$>$.edited.jpg (optional): The page as edited by the user (X " "starts at 1)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "" "paper.$<$X$>$.words (optional): A hOCR file, containing all the words found " "on the page using the OCR (optional, but required for indexing ; can be " "regenerated with the options \"Redo OCR\")." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "" "paper.1.thumb.jpg (optional, generated automatically): A thumbnail version " "of the page (faster to load)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "labels (optional): a text file containing the labels applied on this document" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "extra.txt (optional): extra keywords added by the user" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "For PDF documents:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "doc.pdf: the document" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "paper.$<$X$>$.words (optional): A hOCR file, containing all the words found " "on the page using the OCR. Some PDF contains crap instead of the real text, " "so running the OCR on them can sometimes be useful." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "passwd.txt (optional): PDF password, if the PDF is password-protected." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "doc.docx / doc.odt / ... (optional): Original file. Converted into PDF (doc." "pdf) so Paperwork can parse and display it more quickly." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:894 msgid "Here is an example a work directory organisation:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:926 #, no-wrap msgid "" "$ find ~/papers\n" "/home/jflesch/papers\n" "/home/jflesch/papers/20130505_1518_00\n" "/home/jflesch/papers/20130505_1518_00/paper.1.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.1.thumb.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.1.words\n" "/home/jflesch/papers/20130505_1518_00/paper.2.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.2.edited.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.2.words\n" "/home/jflesch/papers/20130505_1518_00/paper.3.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.3.words\n" "/home/jflesch/papers/20130505_1518_00/labels\n" "/home/jflesch/papers/20110726_0000_01f\n" "/home/jflesch/papers/20110726_0000_01/paper.1.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.1.thumb.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.1.words\n" "/home/jflesch/papers/20110726_0000_01/paper.2.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.2.words\n" "/home/jflesch/papers/20110726_0000_01/extra.txt\n" "/home/jflesch/papers/20130106_1309_44\n" "/home/jflesch/papers/20130106_1309_44/doc.pdf\n" "/home/jflesch/papers/20130106_1309_44/paper.1.thumb.jpg\n" "/home/jflesch/papers/20130106_1309_44/paper.2.edited.jpg\n" "/home/jflesch/papers/20130106_1309_44/paper.2.words\n" "/home/jflesch/papers/20130106_1309_44/labels\n" "/home/jflesch/papers/20130106_1309_44/extra.txt\n" "/home/jflesch/papers/20130106_1309_44/passwd.txt\n" "/home/jflesch/papers/20130520_1309_44\n" "/home/jflesch/papers/20130520_1309_44/doc.pdf\n" "/home/jflesch/papers/20130520_1309_44/doc.docx\n" "/home/jflesch/papers/20130520_1309_44/labels" msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:929 msgid "hOCR files" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:931 msgid "With Tesseract, the hOCR file can be obtained with following command:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:933 #, no-wrap msgid "tesseract paper..jpg paper. -l hocr && mv paper..html paper..words" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:935 msgid "For example:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:937 #, no-wrap msgid "tesseract paper.1.jpg paper.1 -l fra hocr && mv paper.1.html paper.1.words" msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:940 msgid "Label files" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:942 msgid "Here is an example of content of a label file:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:944 #, no-wrap msgid "facture,#0000b1588c61 logement,#f6b6ffff0000" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:947 msgid "" "It's always $[$label$]$,$[$color$]$. For a same label, the color should " "always be the same." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:950 msgid "Getting support" msgstr "Obtenir de l'aide" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:953 msgid "" "A forum dedicated to Paperwork exists: \\href{https://forum.openpaper.work}" "{https://forum.openpaper.work}." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:956 msgid "" "There is also an IRC channel for live discussions: \\href{https://webchat." "freenode.net/}{Freenode}, channel \\#openpaperwork" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:959 msgid "" "If you have questions regarding Paperwork or simply want to chat, those are " "the places to go." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:961 msgid "Reporting issues" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:964 msgid "" "If you noticed a bug in Paperwork (and you are sure it's a bug), you can " "make a bug report." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:966 msgid "Bug Tracker" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:969 msgid "" "One way to create bug reports is to create tickets on \\href{https://gitlab." "gnome.org/World/OpenPaperwork/paperwork/issues}{Paperwork bug tracker: " "https://gitlab.gnome.org/World/OpenPaperwork/paperwork/issues}." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:972 msgid "" "This is the recommended way to submit a bug report if you would like to " "discuss it with Paperwork developpers." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:975 msgid "" "To make sure you include all the required informations, you can use the tool " "integrated in Paperwork (see below)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:978 msgid "Automatic bug report" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:985 msgid "" "Paperwork includes a tool to make reporting bugs easier. It allows you to " "get easily all the required information to make a perfect bug report." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:989 msgid "" "All attachments are automatically censored to protect your privacy: Document " "contents are blurred in screenshots and logs are censored to remove your " "user name." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:992 msgid "" "If the bug you want to report is related to scanners, please include " "\"Scanner info.\" in the bug report files." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:995 msgid "" "If te bug you want to report is related to a display problem, please include " "\"App. screenshots\" in the bug report files." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:998 msgid "ZIP file" msgstr "Fichier ZIP" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1002 msgid "" "You can then obtain a ZIP file with all the data. Please make sure the " "content of the ZIP file does not contain private information (it shouldn't, " "but better safe than sorry). Then you can add this ZIP file to a ticket on " "Gitlab." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1005 msgid "Automatic submission" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1009 msgid "" "You can also let the tool submit the bug report to openpaper.work " "automatically. In that case, you won't be able to discuss the bug with " "developers (or you have to leave a way to contact you in the bug report)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1016 msgid "" "If you use the automatic submission , the tool will give you an URL to see " "the submitted bug report. This URL is private and shouldn't be shared until " "you made sure there is no private information in the bug report. If there is " "private information, you can request deletion of the bug report by sending " "an email to jflesch@openpaper.work (please specify the private URI in your " "mail so we can be sure that you are the one who submitted the bug report)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1019 msgid "Uninstalling" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1022 msgid "" "Paperwork can be uninstalled. Uninstalling Paperwork \\emph{will never} " "remove your work directory or your documents." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1028 msgid "" "If you installed Paperwork using the package manager from your distribution " "(the recommended way), the uninstallation method depends on the package " "manager." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1031 msgid "" "For instance, on GNU/Linux Debian or GNU/Linux Ubuntu, the following command " "will take care of it:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1034 #, no-wrap msgid "sudo apt remove --purge paperwork-\\*" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1037 msgid "If you installed it using Flatpak, you can use the following command:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1040 #, no-wrap msgid "flatpak --user uninstall work.openpaper.Paperwork" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1043 msgid "Windows 10" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1047 msgid "" "Paperwork can be uninstalled as any Windows applications, by going in " "Windows Control Panel, clicking on \"Applications\", finding Paperwork in " "the list, and then clicking on \"uninstall\"." msgstr "" paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/data/l10n/messages.pot000066400000000000000000002117571417573700700300660ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE # Copyright (C) YEAR Free Software Foundation, Inc. # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "POT-Creation-Date: 2021-10-09 12:56+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #. type: Plain text #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:11 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "\\date{}" msgstr "" #. type: title{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:11 msgid "Welcome to Paperwork !" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:20 msgid "" "They are going to drive you crazy. Your phone operator, your bank, your " "daughter's school, your dog's veterinarian, even your ISP; it seems like all " "of them are trying to drown you under tons of papers. Papers you have to " "read, classify, and memorize just in case you may need them later. Most of " "the time you won't, which means you waste your energy for nothing." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:24 msgid "" "Paperwork will help you get rid of all those papers by turning them into " "searchable documents. It's simple: just scan and forget. Looking for a " "specific paper? Just type in a few keywords and tada" msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:28 msgid "Documents and pages" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:36 msgid "" "Paperwork's interface is composed of two panels. On the left (green) is the " "list of all your documents sorted by the date they were imported. On the " "right (blue) are the pages of the currently selected paper." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:41 msgid "" "You can add papers from several sources, depending on the devices connected " "to your computer: scanner flatbed, scanner feeder, camera, etc. You have no " "scanner at home? You can still use the scanner you have at work. Paperwork " "will easily import PDF and image files." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:44 msgid "Find" msgstr "" #. type: wrapfigure #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:50 msgid "{r}{0.5\\textwidth}" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:56 msgid "" "Find what you need, when you need it. Type a few keywords in the search bar " "and the list of papers will shrink to only the relevant content. This is " "where the magic happens: Paperwork uses optical character recognition (OCR) " "to convert your papers into simple text files, so it's easy to search for " "text." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:59 msgid "Export" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:69 msgid "" "Sometimes you may want to export a document to send it to someone else. " "Multiple formats are supported: .pdf, .jpg, .txt, etc. And of course, paper " "(requires a printer, sold separately)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:72 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:113 msgid "Labels and additional keywords" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:84 msgid "" "You answered an important email and you want to keep track of it? The paper " "you scanned was so unreadable that Paperwork failed to recognize some " "important keywords? Add keywords to your paper so you won't miss anything! " "All the keywords you add will be searchable, as if they were directly " "written on the paper you scanned." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:89 msgid "" "You would like to organize your documents a bit more? You can also add " "labels to your documents. Each label has its own color. With time, Paperwork " "will learn which labels go on which documents and will automatically apply " "them on new documents." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:92 msgid "Your first documents" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:102 msgid "" "Click the + button, the scan button, and that's all folks! You are now aware " "of the main features of Paperwork. You can start using it by adding your " "first own paper." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:105 msgid "" "This document will automatically disappear from your document list as soon " "as you have created or imported your first document." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:108 msgid "Need more help ?" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:116 msgid "" "If you need more help, there is a comprehensive manual you can find in the " "help section of Paperwork." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:119 msgid "" "We hope that you'll enjoy this piece of software. If you like it please tell " "us, and if you don't please tell us why!" msgstr "" #. type: hypersetup{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "" "colorlinks, citecolor=black, filecolor=black, linkcolor=black, " "urlcolor=black, linktoc=all," msgstr "" #. type: title{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "Paperwork manual" msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:32 msgid "Introduction" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:41 msgid "" "Most personal documents are fairly recurrent: earning statements, rent " "bills, electricity bills, etc. For most unorganized people, having to find " "them back later is worrisome, at best. For most organized people, naming and " "sorting them is as tedious as watching paint dry." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:45 msgid "" "The main idea behind Paperwork is that managing documents is a computer " "job. Humans should do as little as possible while machines do most of the " "work. The end goal here is \"scan \\& forget\"." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:49 msgid "" "If you're looking for a software that will let you name each document " "individually, organize them in complex hierachy, tag them manually each " "time, fix OCR minor glitches, etc, then Paperwork is not for you." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:52 msgid "Definitions" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:54 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:158 msgid "Work directory" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:57 msgid "" "Paperwork stores all your documents in a single directory: the work " "directory. In this directory, each document has its own sub-directory." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:61 msgid "" "While this makes Paperwork hard to use with other tools, it has one major " "advantage: You don't have to worry about file names and directory structures " "anymore." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:63 msgid "Document" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:71 msgid "" "In Paperwork, a document is a set of pages. On disk, it can either be a set " "of JPEG files or a PDF file." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:74 msgid "" "Documents are identified only by a date. It can either be the date you " "imported them (default) or some date of your choosing." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:77 msgid "" "They are displayed on the left side of the main window (green part on the " "screenshot above)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:80 msgid "Page" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:82 msgid "In Paperwork, a page is just an image and the word positions on this image." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:88 msgid "" "Images can come from a scanner or be imported. In those cases, it is stored " "as a JPEG files and text is extracted using OCR (Optical Character " "Recognition). OCR is a fairly long process. It can take up to a few minutes " "for each page. So the text extracted from images is stored in hOCR files " "beside the JPEG files." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:91 msgid "" "Pages can also be the pages from a PDF file. In that case, by default, " "Paperwork just stores a copy of the PDF file." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:95 msgid "" "Paperwork does not track whether a page is recto or verso. Paperwork does " "not track the paper size corresponding to a page (A4, Letter, etc)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:98 msgid "" "Pages are displayed on the right side of the main window (blue part on the " "screenshot above)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:101 msgid "Indexation and Keywords" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:108 msgid "" "Of course, you need a way to find back your documents. Paperwork manages an " "index with all the keywords found in your documents." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:110 msgid "Just type in a few keywords, and you will get your documents back." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:128 msgid "" "Unfortunately, sometimes, documents don't contain the keywords needed to " "find them back. Also OCR is not a perfectly realiable process and may not " "work." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:131 msgid "" "To mitigate those issues, you can add labels (or tags) on your documents and " "provide additional keywords. Both are added to the index." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:134 msgid "" "Labels are displayed beside documents. Additional keywords are almost never " "displayed." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:137 msgid "Settings" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:139 msgid "Accessing the settings" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:166 msgid "" "The work directory is the directory where you want all your documents " "stored. It can be a standard folder, a folder synchronized across multiple " "computers or on a network share." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:169 msgid "" "Once you close the settings dialog, the work directory will be scanned and " "Paperwork index will be updated according to its index." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:172 msgid "" "Each time Paperwork starts, it will look for changes in this folder and " "synchronize its index accordingly." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:175 msgid "Scanner" msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:182 msgid "Device" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:189 msgid "" "When starting, Paperwork looks for scanners. The scanner to use can be " "selected in the settings." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:191 msgid "Webcams, file storage, etc, cannot be used. Only paper-eaters." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:194 msgid "Scan Mode" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:202 msgid "" "Most modern scanners scan in color in a reasonable time. However some older " "scanners scan much faster in grayscale or even in black\\&white. Here you " "can select the mode to use." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:205 msgid "Scan Resolution" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:212 msgid "" "Scanner resolution defines how detailed the images coming from your scanner " "must be." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "Higher resolutions mean" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "longer scans," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "longer OCR," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "more time to display," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "more space used on disk," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "but also better OCR." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "Lower resolutions mean" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "shorter scans," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "shorter OCR," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "less time to display," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "less space used on disk," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "but also inferior OCR," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "and possibly unreadable image (even by a human)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:234 msgid "" "300 dpi is considered a good trade-off. You may want to reduce it to 200 dpi " "on slow computers." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:237 msgid "Scanner calibration" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:247 msgid "" "Scanners tend to provide images actually bigger than the scanned pages. " "Since most of the time, you will always scan pages having the same size (A4 " "or Letter usually), Paperwork provides an option called scanner " "calibration. Scanner calibration in Paperwork is simply an area that will " "always be cropped out of images coming from the scanner." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:250 msgid "OCR" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:257 msgid "" "By default, Paperwork uses Tesseract for the OCR. If unavailable, it falls " "back on Cuneiform." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:260 msgid "" "On Linux, if installed with Flatpak, Paperwork is always provided with " "Tesseract. On Windows, Paperwork is always provided with Tesseract." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:263 msgid "" "To get better results, OCR tool need to know the language used in the " "document(s)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:268 msgid "" "The language available in the settings dialog of Paperwork are those " "understood by the OCR tool. If your language is not in the list, it means " "the OCR tool doesn't have the data required to read your language and you " "must install them." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:271 msgid "Adding languages" msgstr "" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:273 msgid "Flatpak" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:279 #, no-wrap msgid "" "# is a list of 2-letters language codes separated ';'\n" "# ex: en;fr;de\n" "flatpak config --user --set languages \"\"\n" "flatpak update --user" msgstr "" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:283 msgid "Debian" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:288 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:304 #, no-wrap msgid "" "# is a 3-letter language code\n" "# ex: 'fra' for French\n" "$ sudo apt-get install tesseract-ocr tesseract-ocr-" msgstr "" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:291 msgid "Fedora" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:296 #, no-wrap msgid "" "# is a 3-letter language code\n" "# ex: 'fra' for French\n" "$ sudo dnf install tesseract tesseract-langpack-" msgstr "" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:299 msgid "Ubuntu" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:307 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:815 msgid "Windows" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:310 msgid "" "Tesseract and all its data files are provided by Paperwork's installer. You " "can rerun the installer to install other languages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:314 msgid "" "If a language is not available in the installer, it either means it hasn't " "been packaged (in which case you can request it), or there is no data file " "available yet for this language." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:317 msgid "Disabling OCR" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:323 msgid "" "When you scan a page using Paperwork, Paperwork will immediately run the OCR " "on it. This process may take a while for each page. In case you want to scan " "a lot of pages quickly (for instance, the first time you use Paperwork), OCR " "can be temporarily disabled. To disable OCR, you simply have to unselect all " "OCR languages." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:326 msgid "Updates" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:334 msgid "" "If you enable this option, when Paperwork starts, Paperwork will look for " "updates if it hasn't done so for a week or more. To know if a new version is " "available, it has to send an HTTPS query to 'openpaper.work'." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:336 msgid "If an update is found, it will notify you but it won't install it." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:339 msgid "New document" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:345 msgid "" "By default, in the document list, Paperwork includes a document called \"New " "document\". If you open it, it always appears empty. This document actually " "doesn't exist yet on disk, but will exist as soon as you put a page in it. " "You can add pages in it by scanning, importing file(s) or dropping a page " "from another in it." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:350 msgid "" "As soon as you put any content in it, this document will get its own date " "(the current one by default). In the document list, \"New document\" will be " "replaced by this date, and a new \"New document\" will be added to the " "document list." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:359 msgid "" "If you are currently searching something (see the chapter \"Searching\"), " "only search results are displayed and therefore this \"New document\" isn't " "displayed. You can get it back by clicking the button \"+\" in the top left " "corner of the main window." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:362 msgid "Scanning" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:368 msgid "" "If a scanner has been selected in the settings, you can use it to scan " "pages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:372 msgid "" "In the header bar, there is a button to add pages. The small arrow on the " "right gives access to possible page sources. Those page sources include your " "scanner sources (Flatbed, Feeder)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:375 msgid "" "Once you've selected the scanner source you want to use, you can click on " "the button \"Scan from ...\"." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:377 msgid "This will start a scan session:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "" "Scanned pages are appended at the end of the current document. If you use a " "feeder, Paperwork will scan pages until the feeder is empty." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will then crop them according to scanner calibration." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will run OCR on them" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will index them" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:389 msgid "" "If this scan session creates a new document, Paperwork will try to set " "labels automatically on the document." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:392 msgid "Importing" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:399 msgid "Images" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:402 msgid "" "Paperwork supports a lot of file formats. It supports JPEG, PNG, GIF, BMP, " "TIFF, etc." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:404 msgid "Each image file is considered as a page." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:408 msgid "" "Images are always appended to the document currently opened. Simply select " "an empty document (\"New document\") to create a new document while " "importing." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:412 msgid "" "OCR is always run on imported images. If the imported image is the first " "page of a new document, Paperwork will automatically apply documents labels." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:416 msgid "" "Note that Paperwork is a document manager. While it can, it is not designed " "to handle images with only very little text or photos. Automatic labeling " "will not work correctly on such documents." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:420 msgid "" "The OCR (Tesseract) works very well with black text on white background. " "Automatic labeling uses recognized text and requires as many keywords on the " "first page as possible." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:423 msgid "PDF" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:429 msgid "" "Each PDF is always considered as a whole document. They are never appended " "to existing document. They are copied and renamed in the work directory, but " "their content is not modified. Paperwork always keeps the original PDF file " "as is, even if you edit some of its pages: the edited pages are stored " "beside the PDF file." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:434 msgid "" "Paperwork will look for pages with no text attached. On those pages, it will " "automatically run OCR. Once all the pages have been examined, it will " "automatically apply document labels. Note that this process may take a few " "minutes for big PDFs files." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:437 msgid "" "If the PDF is already part of your documents, Paperwork will simply ignore " "it." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:440 msgid "Many PDFs in one shot" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:445 msgid "" "When importing, if you select a folder, Paperwork will browse this folder " "and look for PDFs to import. Already-imported PDFs are simply " "ignored. Folder is browsed recursively (all the folders inside the folder " "are also examined)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:448 msgid "Labels" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:464 msgid "" "There is currently one constraint in Paperwork: Each label must be on at " "least one document. Otherwise, when you will restart Paperwork, labels " "without documents will disappear." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:467 msgid "Creating new labels" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:475 msgid "" "You can click on the gray rectangle on the left side to pick the label " "color. You can enter the label name in text field between the gray " "rectangle and the button \"+\"." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:478 msgid "" "Once you click on the button \"+\", the label will be added to the current " "document." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:481 msgid "" "The label is actually added once you close document properties. Paperwork " "will then update its index accordingly." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:484 msgid "Setting labels on documents" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:488 msgid "" "When you open document properties, the label list appears. On the left side " "of each label color, you have a button. This button allows you to add or " "remove labels on the current document." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:491 msgid "" "The changes are actually written on disk once you close the document " "properties. Paperwork will update its index accordinly." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:494 msgid "Modifying a label color" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:497 msgid "" "When you open document properties, you can click on a label color to change " "it. A dialog will let you pick the new color." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:501 msgid "" "Label color will actually be changed on disk when you close the document " "properties. Paperwork will then update the label on all the documents that " "use it." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:504 msgid "Modifying a label name" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:507 msgid "" "When you open document properties, you can click on a label string to change " "it. A dialog will let you type in the new name." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:511 msgid "" "Label name will actually be changed on disk when you close the document " "properties. Paperwork will then update the label on all the documents that " "use it and then reindex them all." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:514 msgid "Deleting a label" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:517 msgid "" "To the right of each label is white-on-black cross button. Clicking on it " "will allow you to delete a label." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:520 msgid "" "Once you will close the document properties, the label will be removed from " "all the documents having it. Paperwork will then update its index " "accordingly." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:523 msgid "" "Beware: Once you have closed document properties, there is no way to put " "back the deleted label." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:526 msgid "Automatic label guessing" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:531 msgid "" "Paperwork does use artificial intelligence. It uses a fairly simple method " "actually: \\href{https://en.wikipedia.org/wiki/Naive_Bayes_classifier}{Naive " "Bayes classifiers}. It's the same technology used by email clients to " "classify mails as spam/non-spam." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:537 msgid "" "Based on all the keywords in all your documents that have (or haven't) a " "label, it can estimate a probability that a document containing the same " "keywords should have or shouldn't have this same label. If the probability " "is high enough, it puts the label on the document automatically when you " "import it or scan it." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:541 msgid "" "Of course, this approach means that Paperwork needs enough samples to work " "reliably. You can expect it to start working once you have about 100 " "documents or more (and only for labels that are on more than 10 documents or " "more)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:544 msgid "Searching" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:546 msgid "Simple search" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:553 msgid "" "You simply enter keywords in the search field. In a few seconds, you will " "get all the documents containing those keywords." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:557 msgid "" "Paperwork does a \"fuzzy\" search: documents with keywords close to the one " "you gave but not identical are also returned (for instance, 'flech' instead " "of 'flesch')." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:562 msgid "" "You can also use " "\\href{https://whoosh.readthedocs.io/en/latest/querylang.html}{Whoosh query " "language} to make more complex queries. If you want examples, you can use " "the advanced search dialog described below." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:565 msgid "Advanced search" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:582 msgid "" "The advanced search dialog helps creating complex search queries. You can " "specify various criterias and once you click on the apply button, it will " "generate a search query for you and put it immediately in the search field. " "Search results will immediately be refreshed as well." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:585 msgid "Viewing" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:591 msgid "Zoom level" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:597 msgid "You can change the scale at which pages are displayed using this control." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:600 msgid "View pages as grid" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:608 msgid "" "When clicking this button, Paperwork will try to display pages on 3 " "columns. In this mode, you can drag'n'drop pages to move them inside the " "document or to another document." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:611 msgid "View pages as list" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:619 msgid "" "When clicking this button, pages will be scaled so their width is the " "maximum width allowed by the main window. In this mode, you can select text " "in the page (and then copy it)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:621 msgid "Highlight all words" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:629 msgid "" "This option allows to see quickly all the words identified by OCR. Sometimes " "(rarely) OCR misses entire chunk in a page. This option allow to see such " "chunk quickly." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:631 msgid "Moving pages" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:633 msgid "Inside a document" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:635 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:644 msgid "You must display the document pages as a grid (See \\ref{layout:grid})." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:640 msgid "" "You can then grab a page (hold the left click button), drag it and drop it " "wherever you want in a document. While dragging, a blue marker will show you " "where the page would drop if you release the left click button of your " "mouse." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:642 msgid "From a document to another" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:647 msgid "" "You can then grab a page (hold the left click button), drag it and drop it " "in the document list, on the document in which you want the page to go." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:649 msgid "Copying text" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:655 msgid "You must display the document pages as a list (See \\ref{layout:paged})." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:658 msgid "" "You can then select text in a page. Hold the left click button to start " "selecting, mouse the mouse cursor to select more words, then release it." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:662 msgid "" "You can then copy the selected text, either by pressing Ctrl-C or by using " "the page menu at the bottom right of the main window. Once copied, you can " "paste the selected text in any other application (Ctrl-V)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:664 msgid "Editing a page" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:670 msgid "Paperwork includes a very simple image editor. It provides 4 functions:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Cropping" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Rotating the page by 90\\degree (can be rotated multiple times)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Rotating the page by -90\\degree (can be rotated multiple times)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "" "Automatic Color Equalization: An algorithm that adjust the image brightness, " "contrast and colors to make it as readable as possible." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:681 msgid "Reseting a page" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:684 msgid "" "Reseting a page returns it to its state when it was scanned or imported, " "before any pre-processing did occur." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:688 msgid "" "This can be helpful if you made a bad modification on the page (cropped a " "wrong area for instance), if the calibration settings weren't appropriate or " "if pre-processing algorithms messed up the page." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:691 msgid "Deleting" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:694 msgid "" "When deleting either documents or pages, they are actually moved in the " "trash bin of your computer." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:698 msgid "" "\\textbf{Important note regarding Flatpak:} A bug may prevent Paperwork from " "moving files to the trash (we are working on it). In that case, Paperwork " "will delete the file directly (no recovery possible)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:701 msgid "Exporting" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:703 msgid "You can export both documents or single pages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:707 msgid "" "In both cases, various transformations can be applied before actually " "exporting them. For instance, you can turn color pages into grayscale pages " "before putting them in a brand new PDF (making the resulting PDF smaller)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:710 msgid "Printing" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:712 msgid "You can print both documents or single pages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:715 msgid "" "Beware that pages are always sent as images to your printer. So for very big " "documents, a few minutes may go by before the actual printing start." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:718 msgid "Backup" msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:720 msgid "Synchronisation between multiple computers" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:726 msgid "" "While Paperwork is a personal document manager, it is not a file " "synchronization application. They are applications dedicated to file " "synchronization that already do that very well. Therefore Paperwork is " "designed to be used with such applications (Nextcloud, Dropbox, OneDrive, " "SparkleShare, etc)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:733 msgid "" "When you start Paperwork, one of the first things it does is check the " "content of the work directory. It looks for any changes and updates its " "document list and index accordingly, automatically. So if another instance " "of Paperwork on another computer modified something in the work directory " "and if this change has been synchronized on another computer, the other " "Paperwork will automatically pick up this change when starting." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:736 msgid "USB key / USB drive" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:739 msgid "" "This is the simplest way to share documents. Simply copy your work directory " "to an USB key, tell Paperwork to use it, and you're done." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:741 msgid "Beware: You should backup your USB key from time to time on another one." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:744 msgid "File Synchronization applications" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:750 msgid "" "Those applications synchronize a local directory with a remote server (or " "cloud). All the changes you do in your folder are applied on the server. All " "the changes applied on the servers are applied to the computers that connect " "to it. The server can belong to you or to someone else (usually a company)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:754 msgid "" "Beware: If you choose to host your documents on someone else server " "(DropBox, OneDrive, etc), they can access all your documents. Paperwork does " "not encrypt them." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:758 msgid "" "Paperwork is tested daily with Nextcloud. While this is not the easiest one " "to install, Nextcloud let you host your files yourself. There are other " "self-hosted alternatives that exist: SparkleShare, Syncthing, etc." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:761 msgid "" "Using DropBox or OneDrive can make sense if you're sharing " "not-so-confidential documents with others (associations, etc)." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:764 msgid "Shared folder" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:769 msgid "" "If all your computers are on the same network, you can share your work " "directory. However, be really careful regarding permissions. Being too " "permissive could let a pirate access all your personal documents ! And " "setting them correctly is tricky." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:772 msgid "" "Beware: Using a shared folder means having a single copy of your work " "directory. You should do regular backups of your work directory." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:775 msgid "Encryption" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:777 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1024 msgid "GNU/Linux" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:779 msgid "GNU/Linux distributions include many tools to encrypt whole directories." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "With Paperwork, there are 2 directories that should be encrypted to protect " "your privacy:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "Your work directory (by default \\textasciitilde /papers, can be changed in " "the settings)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "The cache directory (\\textasciitilde /.local/share/paperwork2, cannot be " "changed) (it contains index files from which the content of your documents " "could be partially recovered)" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:793 msgid "" "Note that if you want to be sure that your data are always encrypted, it's " "recommended to encrypt your whole home directory or even your whole system " "if possible." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:795 msgid "cryptsetup" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:799 msgid "" "Most GNU/Linux distribution installer now provide an optio4n to encrypt your " "whole system or your whole /home with cryptsetup . This is the recommended " "method to protect your documents." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:801 msgid "Encfs" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:803 msgid "Encfs can also be used to create encrypted directories easily." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:807 msgid "" "Beware that Encfs seems to have some security weaknesses. So, while it's " "probably enough to prevent a laptop thief from accessing your documents, " "it's likely to be not enough to prevent the NSA or the police from doing so " ";-)." msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:811 #, no-wrap msgid "" "$ encfs ~/.local/share/.paperwork2 ~/.local/share/paperwork2\n" "$ encfs ~/.papers ~/papers" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:818 msgid "" "On Windows, you're strongly advised to enable BitLocker to protect your " "documents. If unavailable, there are other applications (Veracrypt, etc)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:821 msgid "Keyboard shortcuts" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:832 msgid "" "Keyboard shortcuts can be seen by opening the application menu, selecting " "\"Help\" and then \"Shortcuts\"." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:835 msgid "Paperwork's files locations" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "By default:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "Configuration: \\textasciitilde /.config/paperwork2.conf" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "Index: \\textasciitilde /.local/share/paperwork2" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "Documents: \\textasciitilde /papers" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "" "(same paths are used on Windows; \\textasciitilde{} = C:\\textbackslash " "Users{[}login{]} ; folders are hidden)" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:848 msgid "" "The index is always updated according based on the documents in the work " "directory. When Paperwork starts, the modification time of each file is used " "to detect changes on the documents." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:850 msgid "Work directory layout" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:852 msgid "workdir$|$rootdir = \\textasciitilde /papers (by default)" msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:854 msgid "Global organisation" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:856 msgid "In the work directory, you have folders, one per document." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:860 msgid "" "The folder names are (usually) the scan/import date of the document: " "YYYYMMDD\\_hhmm\\_ss{[}\\_{]}. The suffix 'idx' is optional and is just " "a number added in case of name collision." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "In every folder you have:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "For image documents:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "paper.$<$X$>$.jpg: The original page in JPG format (X starts at 1)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "paper.$<$X$>$.edited.jpg (optional): The page as edited by the user (X " "starts at 1)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "" "paper.$<$X$>$.words (optional): A hOCR file, containing all the words found " "on the page using the OCR (optional, but required for indexing ; can be " "regenerated with the options \"Redo OCR\")." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "" "paper.1.thumb.jpg (optional, generated automatically): A thumbnail version " "of the page (faster to load)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "labels (optional): a text file containing the labels applied on this " "document" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "extra.txt (optional): extra keywords added by the user" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "For PDF documents:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "doc.pdf: the document" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "paper.$<$X$>$.words (optional): A hOCR file, containing all the words found " "on the page using the OCR. Some PDF contains crap instead of the real text, " "so running the OCR on them can sometimes be useful." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "passwd.txt (optional): PDF password, if the PDF is password-protected." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "doc.docx / doc.odt / ... (optional): Original file. Converted into PDF " "(doc.pdf) so Paperwork can parse and display it more quickly." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:894 msgid "Here is an example a work directory organisation:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:926 #, no-wrap msgid "" "$ find ~/papers\n" "/home/jflesch/papers\n" "/home/jflesch/papers/20130505_1518_00\n" "/home/jflesch/papers/20130505_1518_00/paper.1.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.1.thumb.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.1.words\n" "/home/jflesch/papers/20130505_1518_00/paper.2.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.2.edited.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.2.words\n" "/home/jflesch/papers/20130505_1518_00/paper.3.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.3.words\n" "/home/jflesch/papers/20130505_1518_00/labels\n" "/home/jflesch/papers/20110726_0000_01f\n" "/home/jflesch/papers/20110726_0000_01/paper.1.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.1.thumb.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.1.words\n" "/home/jflesch/papers/20110726_0000_01/paper.2.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.2.words\n" "/home/jflesch/papers/20110726_0000_01/extra.txt\n" "/home/jflesch/papers/20130106_1309_44\n" "/home/jflesch/papers/20130106_1309_44/doc.pdf\n" "/home/jflesch/papers/20130106_1309_44/paper.1.thumb.jpg\n" "/home/jflesch/papers/20130106_1309_44/paper.2.edited.jpg\n" "/home/jflesch/papers/20130106_1309_44/paper.2.words\n" "/home/jflesch/papers/20130106_1309_44/labels\n" "/home/jflesch/papers/20130106_1309_44/extra.txt\n" "/home/jflesch/papers/20130106_1309_44/passwd.txt\n" "/home/jflesch/papers/20130520_1309_44\n" "/home/jflesch/papers/20130520_1309_44/doc.pdf\n" "/home/jflesch/papers/20130520_1309_44/doc.docx\n" "/home/jflesch/papers/20130520_1309_44/labels" msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:929 msgid "hOCR files" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:931 msgid "With Tesseract, the hOCR file can be obtained with following command:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:933 #, no-wrap msgid "" "tesseract paper..jpg paper. -l hocr && mv paper..html " "paper..words" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:935 msgid "For example:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:937 #, no-wrap msgid "tesseract paper.1.jpg paper.1 -l fra hocr && mv paper.1.html paper.1.words" msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:940 msgid "Label files" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:942 msgid "Here is an example of content of a label file:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:944 #, no-wrap msgid "facture,#0000b1588c61 logement,#f6b6ffff0000" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:947 msgid "" "It's always $[$label$]$,$[$color$]$. For a same label, the color should " "always be the same." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:950 msgid "Getting support" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:953 msgid "" "A forum dedicated to Paperwork exists: " "\\href{https://forum.openpaper.work}{https://forum.openpaper.work}." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:956 msgid "" "There is also an IRC channel for live discussions: " "\\href{https://webchat.freenode.net/}{Freenode}, channel \\#openpaperwork" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:959 msgid "" "If you have questions regarding Paperwork or simply want to chat, those are " "the places to go." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:961 msgid "Reporting issues" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:964 msgid "" "If you noticed a bug in Paperwork (and you are sure it's a bug), you can " "make a bug report." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:966 msgid "Bug Tracker" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:969 msgid "" "One way to create bug reports is to create tickets on " "\\href{https://gitlab.gnome.org/World/OpenPaperwork/paperwork/issues}{Paperwork " "bug tracker: https://gitlab.gnome.org/World/OpenPaperwork/paperwork/issues}." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:972 msgid "" "This is the recommended way to submit a bug report if you would like to " "discuss it with Paperwork developpers." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:975 msgid "" "To make sure you include all the required informations, you can use the tool " "integrated in Paperwork (see below)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:978 msgid "Automatic bug report" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:985 msgid "" "Paperwork includes a tool to make reporting bugs easier. It allows you to " "get easily all the required information to make a perfect bug report." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:989 msgid "" "All attachments are automatically censored to protect your privacy: Document " "contents are blurred in screenshots and logs are censored to remove your " "user name." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:992 msgid "" "If the bug you want to report is related to scanners, please include " "\"Scanner info.\" in the bug report files." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:995 msgid "" "If te bug you want to report is related to a display problem, please include " "\"App. screenshots\" in the bug report files." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:998 msgid "ZIP file" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1002 msgid "" "You can then obtain a ZIP file with all the data. Please make sure the " "content of the ZIP file does not contain private information (it shouldn't, " "but better safe than sorry). Then you can add this ZIP file to a ticket on " "Gitlab." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1005 msgid "Automatic submission" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1009 msgid "" "You can also let the tool submit the bug report to openpaper.work " "automatically. In that case, you won't be able to discuss the bug with " "developers (or you have to leave a way to contact you in the bug report)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1016 msgid "" "If you use the automatic submission , the tool will give you an URL to see " "the submitted bug report. This URL is private and shouldn't be shared until " "you made sure there is no private information in the bug report. If there is " "private information, you can request deletion of the bug report by sending " "an email to jflesch@openpaper.work (please specify the private URI in your " "mail so we can be sure that you are the one who submitted the bug report)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1019 msgid "Uninstalling" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1022 msgid "" "Paperwork can be uninstalled. Uninstalling Paperwork \\emph{will never} " "remove your work directory or your documents." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1028 msgid "" "If you installed Paperwork using the package manager from your distribution " "(the recommended way), the uninstallation method depends on the package " "manager." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1031 msgid "" "For instance, on GNU/Linux Debian or GNU/Linux Ubuntu, the following command " "will take care of it:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1034 #, no-wrap msgid "sudo apt remove --purge paperwork-\\*" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1037 msgid "If you installed it using Flatpak, you can use the following command:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1040 #, no-wrap msgid "flatpak --user uninstall work.openpaper.Paperwork" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1043 msgid "Windows 10" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1047 msgid "" "Paperwork can be uninstalled as any Windows applications, by going in " "Windows Control Panel, clicking on \"Applications\", finding Paperwork in " "the list, and then clicking on \"uninstall\"." msgstr "" paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/data/l10n/oc.po000066400000000000000000002205151417573700700264640ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE # Copyright (C) YEAR Free Software Foundation, Inc. # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "POT-Creation-Date: 2021-01-03 10:47+0100\n" "PO-Revision-Date: 2021-10-09 10:57+0000\n" "Last-Translator: Quentin PAGÈS \n" "Language-Team: Occitan \n" "Language: oc\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 4.4\n" #. type: Plain text #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:11 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "\\date{}" msgstr "" #. type: title{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:11 msgid "Welcome to Paperwork !" msgstr "Vos desiram la benvenguda sus Paperwork !" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:20 msgid "" "They are going to drive you crazy. Your phone operator, your bank, your " "daughter's school, your dog's veterinarian, even your ISP; it seems like all " "of them are trying to drown you under tons of papers. Papers you have to " "read, classify, and memorize just in case you may need them later. Most of " "the time you won't, which means you waste your energy for nothing." msgstr "" "Vos faràn venir craba. Vòstre operator de telefòn, vòstra banca, l'escòla de " "la petita, vòstre veterinari, vòstre provesidor Internet, etc. Sembla " "qu'ensajan totes de nos encombrar de tonas de papièrs. Papiers que vos cal " "legir, triar e memorizar, se per cas vos fa mestièr mai tard. La màger part " "del temps, non, e aquò vòl dire qu'avètz perdut vòstra energia per de prunas." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:24 msgid "" "Paperwork will help you get rid of all those papers by turning them into " "searchable documents. It's simple: just scan and forget. Looking for a " "specific paper? Just type in a few keywords and tada" msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:28 msgid "Documents and pages" msgstr "Los documents e las paginas" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:36 msgid "" "Paperwork's interface is composed of two panels. On the left (green) is the " "list of all your documents sorted by the date they were imported. On the " "right (blue) are the pages of the currently selected paper." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:41 msgid "" "You can add papers from several sources, depending on the devices connected " "to your computer: scanner flatbed, scanner feeder, camera, etc. You have no " "scanner at home? You can still use the scanner you have at work. Paperwork " "will easily import PDF and image files." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:44 msgid "Find" msgstr "Trobar" #. type: wrapfigure #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:50 msgid "{r}{0.5\\textwidth}" msgstr "{r}{0.5\\textwidth}" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:56 msgid "" "Find what you need, when you need it. Type a few keywords in the search bar " "and the list of papers will shrink to only the relevant content. This is " "where the magic happens: Paperwork uses optical character recognition (OCR) " "to convert your papers into simple text files, so it's easy to search for " "text." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:59 msgid "Export" msgstr "Exportar" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:69 msgid "" "Sometimes you may want to export a document to send it to someone else. " "Multiple formats are supported: .pdf, .jpg, .txt, etc. And of course, paper " "(requires a printer, sold separately)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:72 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:113 msgid "Labels and additional keywords" msgstr "Etiquetas e mots claus addicionals" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:84 msgid "" "You answered an important email and you want to keep track of it? The paper " "you scanned was so unreadable that Paperwork failed to recognize some " "important keywords? Add keywords to your paper so you won't miss anything! " "All the keywords you add will be searchable, as if they were directly " "written on the paper you scanned." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:89 msgid "" "You would like to organize your documents a bit more? You can also add " "labels to your documents. Each label has its own color. With time, Paperwork " "will learn which labels go on which documents and will automatically apply " "them on new documents." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:92 msgid "Your first documents" msgstr "Vòstres primièrs documents" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:102 msgid "" "Click the + button, the scan button, and that's all folks! You are now aware " "of the main features of Paperwork. You can start using it by adding your " "first own paper." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:105 msgid "" "This document will automatically disappear from your document list as soon " "as you have created or imported your first document." msgstr "" "Aqueste document dispareirà automaticament de vòstra lista de documents tre " "qu’auretz creat o importat vòstre primièr document." #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:108 msgid "Need more help ?" msgstr "Besonh d'ajuda ?" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:116 msgid "" "If you need more help, there is a comprehensive manual you can find in the " "help section of Paperwork." msgstr "" "Se vos fa mestièr un pauc mai d’ajuda, i a un manual mai complèt integrat a " "Paperwork." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:119 msgid "" "We hope that you'll enjoy this piece of software. If you like it please tell " "us, and if you don't please tell us why!" msgstr "" "Esperam que vos agradarà aqueste logicial. S’es lo cas, digatz-nos-lo, " "autrament, digatz-nos cossí lo melhorar !" #. type: hypersetup{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "" "colorlinks, citecolor=black, filecolor=black, linkcolor=black, " "urlcolor=black, linktoc=all," msgstr "" "colorlinks, citecolor=black, filecolor=black, linkcolor=black, " "urlcolor=black, linktoc=all," #. type: title{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "Paperwork manual" msgstr "Manual de Paperwork" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:32 msgid "Introduction" msgstr "Introduccion" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:41 msgid "" "Most personal documents are fairly recurrent: earning statements, rent " "bills, electricity bills, etc. For most unorganized people, having to find " "them back later is worrisome, at best. For most organized people, naming and " "sorting them is as tedious as watching paint dry." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:45 msgid "" "The main idea behind Paperwork is that managing documents is a computer " "job. Humans should do as little as possible while machines do most of the " "work. The end goal here is \"scan \\& forget\"." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:49 msgid "" "If you're looking for a software that will let you name each document " "individually, organize them in complex hierachy, tag them manually each " "time, fix OCR minor glitches, etc, then Paperwork is not for you." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:52 msgid "Definitions" msgstr "Definicions" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:54 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:158 msgid "Work directory" msgstr "Repertòri de trabalh" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:57 msgid "" "Paperwork stores all your documents in a single directory: the work " "directory. In this directory, each document has its own sub-directory." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:61 msgid "" "While this makes Paperwork hard to use with other tools, it has one major " "advantage: You don't have to worry about file names and directory structures " "anymore." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:63 msgid "Document" msgstr "Document" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:71 msgid "" "In Paperwork, a document is a set of pages. On disk, it can either be a set " "of JPEG files or a PDF file." msgstr "" "Dins Paperwork, un document es un ensem de paginas. Sul disc, pòt èsser un " "jòc de fichièrs JPEG o un fichièr PDF." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:74 msgid "" "Documents are identified only by a date. It can either be the date you " "imported them (default) or some date of your choosing." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:77 msgid "" "They are displayed on the left side of the main window (green part on the " "screenshot above)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:80 msgid "Page" msgstr "Pagina" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:82 msgid "" "In Paperwork, a page is just an image and the word positions on this image." msgstr "" "Dins Paperwork, una pagina es just un imatge e las posicions dels mots sus " "aqueste imatge." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:88 msgid "" "Images can come from a scanner or be imported. In those cases, it is stored " "as a JPEG files and text is extracted using OCR (Optical Character " "Recognition). OCR is a fairly long process. It can take up to a few minutes " "for each page. So the text extracted from images is stored in hOCR files " "beside the JPEG files." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:91 msgid "" "Pages can also be the pages from a PDF file. In that case, by default, " "Paperwork just stores a copy of the PDF file." msgstr "" "Las paginas pòdon tanben èsser de paginas d’un fichièr PDF. Dins aqueste " "cas, per defaut, Paperwork enregistra simplament una còpia del fichièr PDF." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:95 msgid "" "Paperwork does not track whether a page is recto or verso. Paperwork does " "not track the paper size corresponding to a page (A4, Letter, etc)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:98 msgid "" "Pages are displayed on the right side of the main window (blue part on the " "screenshot above)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:101 msgid "Indexation and Keywords" msgstr "Indexacion e mots clau" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:108 msgid "" "Of course, you need a way to find back your documents. Paperwork manages an " "index with all the keywords found in your documents." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:110 msgid "Just type in a few keywords, and you will get your documents back." msgstr "Picatz sonque d’unes mots claus e traparetz vòstres documents." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:128 msgid "" "Unfortunately, sometimes, documents don't contain the keywords needed to " "find them back. Also OCR is not a perfectly realiable process and may not " "work." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:131 msgid "" "To mitigate those issues, you can add labels (or tags) on your documents and " "provide additional keywords. Both are added to the index." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:134 msgid "" "Labels are displayed beside documents. Additional keywords are almost never " "displayed." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:137 msgid "Settings" msgstr "Paramètres" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:139 msgid "Accessing the settings" msgstr "Accès als paramètres" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:166 msgid "" "The work directory is the directory where you want all your documents " "stored. It can be a standard folder, a folder synchronized across multiple " "computers or on a network share." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:169 msgid "" "Once you close the settings dialog, the work directory will be scanned and " "Paperwork index will be updated according to its index." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:172 msgid "" "Each time Paperwork starts, it will look for changes in this folder and " "synchronize its index accordingly." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:175 msgid "Scanner" msgstr "Numerizador" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:182 msgid "Device" msgstr "Periferic" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:189 msgid "" "When starting, Paperwork looks for scanners. The scanner to use can be " "selected in the settings." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:191 msgid "Webcams, file storage, etc, cannot be used. Only paper-eaters." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:194 msgid "Scan Mode" msgstr "Mòde del numerizador" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:202 msgid "" "Most modern scanners scan in color in a reasonable time. However some older " "scanners scan much faster in grayscale or even in black\\&white. Here you " "can select the mode to use." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:205 msgid "Scan Resolution" msgstr "Resolucion del numerizador" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:212 msgid "" "Scanner resolution defines how detailed the images coming from your scanner " "must be." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "Higher resolutions mean" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "longer scans," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "longer OCR," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "more time to display," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "more space used on disk," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "but also better OCR." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "Lower resolutions mean" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "shorter scans," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "shorter OCR," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "less time to display," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "less space used on disk," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "but also inferior OCR," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "and possibly unreadable image (even by a human)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:234 msgid "" "300 dpi is considered a good trade-off. You may want to reduce it to 200 dpi " "on slow computers." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:237 msgid "Scanner calibration" msgstr "Calibratge del numerizador" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:247 msgid "" "Scanners tend to provide images actually bigger than the scanned pages. " "Since most of the time, you will always scan pages having the same size (A4 " "or Letter usually), Paperwork provides an option called scanner calibration. " "Scanner calibration in Paperwork is simply an area that will always be " "cropped out of images coming from the scanner." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:250 msgid "OCR" msgstr "ROC" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:257 msgid "" "By default, Paperwork uses Tesseract for the OCR. If unavailable, it falls " "back on Cuneiform." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:260 msgid "" "On Linux, if installed with Flatpak, Paperwork is always provided with " "Tesseract. On Windows, Paperwork is always provided with Tesseract." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:263 msgid "" "To get better results, OCR tool need to know the language used in the " "document(s)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:268 msgid "" "The language available in the settings dialog of Paperwork are those " "understood by the OCR tool. If your language is not in the list, it means " "the OCR tool doesn't have the data required to read your language and you " "must install them." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:271 msgid "Adding languages" msgstr "Apondon de lengas" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:273 msgid "Flatpak" msgstr "Flatpak" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:279 #, fuzzy, no-wrap #| msgid "" #| "# is a list of 2-letters language codes separated ';'\n" #| "# ex: en;fr;de\n" #| "flatpak config --user --set languages \"\"\n" #| "flatpak update" msgid "" "# is a list of 2-letters language codes separated ';'\n" "# ex: en;fr;de\n" "flatpak config --user --set languages \"\"\n" "flatpak update --user" msgstr "" " es una lista de lenga amb un còdi de doas letras separadas per « ; »\n" "# ex : en;fr;oc\n" "flatpak config --user --set languages \"\"\n" "flatpak update" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:283 msgid "Debian" msgstr "Debian" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:288 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:304 #, no-wrap msgid "" "# is a 3-letter language code\n" "# ex: 'fra' for French\n" "$ sudo apt-get install tesseract-ocr tesseract-ocr-" msgstr "" "# es una lista de lenga amb un còdi de tres letras\n" "# ex : « fra » pel francés\n" "# ex : « oci » per l'occitan\n" "$ sudo apt-get install tesseract-ocr tesseract-ocr-" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:291 msgid "Fedora" msgstr "Fedora" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:296 #, no-wrap msgid "" "# is a 3-letter language code\n" "# ex: 'fra' for French\n" "$ sudo dnf install tesseract tesseract-langpack-" msgstr "" "# es una lista de lenga amb un còdi de tres letras\n" "# ex : « oci » per l'occitan\n" "$ sudo apt-get install tesseract-ocr tesseract-ocr-" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:299 msgid "Ubuntu" msgstr "Ubuntu" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:307 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:815 msgid "Windows" msgstr "Windows" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:310 msgid "" "Tesseract and all its data files are provided by Paperwork's installer. You " "can rerun the installer to install other languages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:314 msgid "" "If a language is not available in the installer, it either means it hasn't " "been packaged (in which case you can request it), or there is no data file " "available yet for this language." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:317 msgid "Disabling OCR" msgstr "Desactivacion de la ROC" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:323 msgid "" "When you scan a page using Paperwork, Paperwork will immediately run the OCR " "on it. This process may take a while for each page. In case you want to scan " "a lot of pages quickly (for instance, the first time you use Paperwork), OCR " "can be temporarily disabled. To disable OCR, you simply have to unselect all " "OCR languages." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:326 msgid "Updates" msgstr "Mesas a jorn" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:334 msgid "" "If you enable this option, when Paperwork starts, Paperwork will look for " "updates if it hasn't done so for a week or more. To know if a new version is " "available, it has to send an HTTPS query to 'openpaper.work'." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:336 msgid "If an update is found, it will notify you but it won't install it." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:339 msgid "New document" msgstr "Document novèl" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:345 msgid "" "By default, in the document list, Paperwork includes a document called \"New " "document\". If you open it, it always appears empty. This document actually " "doesn't exist yet on disk, but will exist as soon as you put a page in it. " "You can add pages in it by scanning, importing file(s) or dropping a page " "from another in it." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:350 msgid "" "As soon as you put any content in it, this document will get its own date " "(the current one by default). In the document list, \"New document\" will be " "replaced by this date, and a new \"New document\" will be added to the " "document list." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:359 msgid "" "If you are currently searching something (see the chapter \"Searching\"), " "only search results are displayed and therefore this \"New document\" isn't " "displayed. You can get it back by clicking the button \"+\" in the top left " "corner of the main window." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:362 msgid "Scanning" msgstr "Numerizacion" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:368 msgid "" "If a scanner has been selected in the settings, you can use it to scan pages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:372 msgid "" "In the header bar, there is a button to add pages. The small arrow on the " "right gives access to possible page sources. Those page sources include your " "scanner sources (Flatbed, Feeder)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:375 msgid "" "Once you've selected the scanner source you want to use, you can click on " "the button \"Scan from ...\"." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:377 msgid "This will start a scan session:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "" "Scanned pages are appended at the end of the current document. If you use a " "feeder, Paperwork will scan pages until the feeder is empty." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will then crop them according to scanner calibration." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will run OCR on them" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will index them" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:389 msgid "" "If this scan session creates a new document, Paperwork will try to set " "labels automatically on the document." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:392 msgid "Importing" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:399 msgid "Images" msgstr "Imatges" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:402 msgid "" "Paperwork supports a lot of file formats. It supports JPEG, PNG, GIF, BMP, " "TIFF, etc." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:404 msgid "Each image file is considered as a page." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:408 msgid "" "Images are always appended to the document currently opened. Simply select " "an empty document (\"New document\") to create a new document while " "importing." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:412 msgid "" "OCR is always run on imported images. If the imported image is the first " "page of a new document, Paperwork will automatically apply documents labels." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:416 msgid "" "Note that Paperwork is a document manager. While it can, it is not designed " "to handle images with only very little text or photos. Automatic labeling " "will not work correctly on such documents." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:420 msgid "" "The OCR (Tesseract) works very well with black text on white background. " "Automatic labeling uses recognized text and requires as many keywords on the " "first page as possible." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:423 msgid "PDF" msgstr "PDF" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:429 msgid "" "Each PDF is always considered as a whole document. They are never appended " "to existing document. They are copied and renamed in the work directory, but " "their content is not modified. Paperwork always keeps the original PDF file " "as is, even if you edit some of its pages: the edited pages are stored " "beside the PDF file." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:434 msgid "" "Paperwork will look for pages with no text attached. On those pages, it will " "automatically run OCR. Once all the pages have been examined, it will " "automatically apply document labels. Note that this process may take a few " "minutes for big PDFs files." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:437 msgid "" "If the PDF is already part of your documents, Paperwork will simply ignore " "it." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:440 msgid "Many PDFs in one shot" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:445 msgid "" "When importing, if you select a folder, Paperwork will browse this folder " "and look for PDFs to import. Already-imported PDFs are simply ignored. " "Folder is browsed recursively (all the folders inside the folder are also " "examined)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:448 msgid "Labels" msgstr "Etiquetas" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:464 msgid "" "There is currently one constraint in Paperwork: Each label must be on at " "least one document. Otherwise, when you will restart Paperwork, labels " "without documents will disappear." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:467 msgid "Creating new labels" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:475 msgid "" "You can click on the gray rectangle on the left side to pick the label " "color. You can enter the label name in text field between the gray " "rectangle and the button \"+\"." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:478 msgid "" "Once you click on the button \"+\", the label will be added to the current " "document." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:481 msgid "" "The label is actually added once you close document properties. Paperwork " "will then update its index accordingly." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:484 msgid "Setting labels on documents" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:488 msgid "" "When you open document properties, the label list appears. On the left side " "of each label color, you have a button. This button allows you to add or " "remove labels on the current document." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:491 msgid "" "The changes are actually written on disk once you close the document " "properties. Paperwork will update its index accordinly." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:494 msgid "Modifying a label color" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:497 msgid "" "When you open document properties, you can click on a label color to change " "it. A dialog will let you pick the new color." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:501 msgid "" "Label color will actually be changed on disk when you close the document " "properties. Paperwork will then update the label on all the documents that " "use it." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:504 msgid "Modifying a label name" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:507 msgid "" "When you open document properties, you can click on a label string to change " "it. A dialog will let you type in the new name." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:511 msgid "" "Label name will actually be changed on disk when you close the document " "properties. Paperwork will then update the label on all the documents that " "use it and then reindex them all." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:514 msgid "Deleting a label" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:517 msgid "" "To the right of each label is white-on-black cross button. Clicking on it " "will allow you to delete a label." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:520 msgid "" "Once you will close the document properties, the label will be removed from " "all the documents having it. Paperwork will then update its index " "accordingly." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:523 msgid "" "Beware: Once you have closed document properties, there is no way to put " "back the deleted label." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:526 msgid "Automatic label guessing" msgstr "Deduccion automatica de las etiquetas" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:531 msgid "" "Paperwork does use artificial intelligence. It uses a fairly simple method " "actually: \\href{https://en.wikipedia.org/wiki/Naive_Bayes_classifier}{Naive " "Bayes classifiers}. It's the same technology used by email clients to " "classify mails as spam/non-spam." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:537 msgid "" "Based on all the keywords in all your documents that have (or haven't) a " "label, it can estimate a probability that a document containing the same " "keywords should have or shouldn't have this same label. If the probability " "is high enough, it puts the label on the document automatically when you " "import it or scan it." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:541 msgid "" "Of course, this approach means that Paperwork needs enough samples to work " "reliably. You can expect it to start working once you have about 100 " "documents or more (and only for labels that are on more than 10 documents or " "more)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:544 msgid "Searching" msgstr "Recèrca" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:546 msgid "Simple search" msgstr "Recèrca simpla" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:553 msgid "" "You simply enter keywords in the search field. In a few seconds, you will " "get all the documents containing those keywords." msgstr "" "Picatz pas que qualques mots dins la camp de recèrca. En unas segondas, " "trapatz totes los documents que contenon aqueles mots." #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:557 msgid "" "Paperwork does a \"fuzzy\" search: documents with keywords close to the one " "you gave but not identical are also returned (for instance, 'flech' instead " "of 'flesch')." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:562 msgid "" "You can also use \\href{https://whoosh.readthedocs.io/en/latest/querylang." "html}{Whoosh query language} to make more complex queries. If you want " "examples, you can use the advanced search dialog described below." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:565 msgid "Advanced search" msgstr "Recèrca avançada" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:582 msgid "" "The advanced search dialog helps creating complex search queries. You can " "specify various criterias and once you click on the apply button, it will " "generate a search query for you and put it immediately in the search field. " "Search results will immediately be refreshed as well." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:585 msgid "Viewing" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:591 msgid "Zoom level" msgstr "Nivèl de zoom" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:597 msgid "" "You can change the scale at which pages are displayed using this control." msgstr "" "Podètz cambiar l’escala de las paginas afichadas en utilizant aqueste boton." #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:600 msgid "View pages as grid" msgstr "Veire las paginas coma gresilha" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:608 msgid "" "When clicking this button, Paperwork will try to display pages on 3 " "columns. In this mode, you can drag'n'drop pages to move them inside the " "document or to another document." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:611 msgid "View pages as list" msgstr "Veire las paginas coma lista" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:619 msgid "" "When clicking this button, pages will be scaled so their width is the " "maximum width allowed by the main window. In this mode, you can select text " "in the page (and then copy it)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:621 msgid "Highlight all words" msgstr "Suslinhar totes los mots" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:629 msgid "" "This option allows to see quickly all the words identified by OCR. Sometimes " "(rarely) OCR misses entire chunk in a page. This option allow to see such " "chunk quickly." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:631 msgid "Moving pages" msgstr "Desplaçar las paginas" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:633 msgid "Inside a document" msgstr "A l’interior d’une document" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:635 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:644 msgid "You must display the document pages as a grid (See \\ref{layout:grid})." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:640 msgid "" "You can then grab a page (hold the left click button), drag it and drop it " "wherever you want in a document. While dragging, a blue marker will show you " "where the page would drop if you release the left click button of your mouse." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:642 msgid "From a document to another" msgstr "D’un document cap a un autre" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:647 msgid "" "You can then grab a page (hold the left click button), drag it and drop it " "in the document list, on the document in which you want the page to go." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:649 msgid "Copying text" msgstr "Copiar de tèxt" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:655 msgid "" "You must display the document pages as a list (See \\ref{layout:paged})." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:658 msgid "" "You can then select text in a page. Hold the left click button to start " "selecting, mouse the mouse cursor to select more words, then release it." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:662 msgid "" "You can then copy the selected text, either by pressing Ctrl-C or by using " "the page menu at the bottom right of the main window. Once copied, you can " "paste the selected text in any other application (Ctrl-V)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:664 msgid "Editing a page" msgstr "Modificar una pagina" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:670 msgid "Paperwork includes a very simple image editor. It provides 4 functions:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Cropping" msgstr "Retalhatge" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Rotating the page by 90\\degree (can be rotated multiple times)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Rotating the page by -90\\degree (can be rotated multiple times)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "" "Automatic Color Equalization: An algorithm that adjust the image brightness, " "contrast and colors to make it as readable as possible." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:681 msgid "Reseting a page" msgstr "Reparametrar una pagina" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:684 msgid "" "Reseting a page returns it to its state when it was scanned or imported, " "before any pre-processing did occur." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:688 msgid "" "This can be helpful if you made a bad modification on the page (cropped a " "wrong area for instance), if the calibration settings weren't appropriate or " "if pre-processing algorithms messed up the page." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:691 msgid "Deleting" msgstr "Supression" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:694 msgid "" "When deleting either documents or pages, they are actually moved in the " "trash bin of your computer." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:698 msgid "" "\\textbf{Important note regarding Flatpak:} A bug may prevent Paperwork from " "moving files to the trash (we are working on it). In that case, Paperwork " "will delete the file directly (no recovery possible)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:701 msgid "Exporting" msgstr "Export" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:703 msgid "You can export both documents or single pages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:707 msgid "" "In both cases, various transformations can be applied before actually " "exporting them. For instance, you can turn color pages into grayscale pages " "before putting them in a brand new PDF (making the resulting PDF smaller)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:710 msgid "Printing" msgstr "Imprimir" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:712 msgid "You can print both documents or single pages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:715 msgid "" "Beware that pages are always sent as images to your printer. So for very big " "documents, a few minutes may go by before the actual printing start." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:718 msgid "Backup" msgstr "Salvagardar" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:720 msgid "Synchronisation between multiple computers" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:726 msgid "" "While Paperwork is a personal document manager, it is not a file " "synchronization application. They are applications dedicated to file " "synchronization that already do that very well. Therefore Paperwork is " "designed to be used with such applications (Nextcloud, Dropbox, OneDrive, " "SparkleShare, etc)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:733 msgid "" "When you start Paperwork, one of the first things it does is check the " "content of the work directory. It looks for any changes and updates its " "document list and index accordingly, automatically. So if another instance " "of Paperwork on another computer modified something in the work directory " "and if this change has been synchronized on another computer, the other " "Paperwork will automatically pick up this change when starting." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:736 msgid "USB key / USB drive" msgstr "Clau USB / Disc USB" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:739 msgid "" "This is the simplest way to share documents. Simply copy your work directory " "to an USB key, tell Paperwork to use it, and you're done." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:741 msgid "" "Beware: You should backup your USB key from time to time on another one." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:744 msgid "File Synchronization applications" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:750 msgid "" "Those applications synchronize a local directory with a remote server (or " "cloud). All the changes you do in your folder are applied on the server. All " "the changes applied on the servers are applied to the computers that connect " "to it. The server can belong to you or to someone else (usually a company)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:754 msgid "" "Beware: If you choose to host your documents on someone else server " "(DropBox, OneDrive, etc), they can access all your documents. Paperwork does " "not encrypt them." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:758 msgid "" "Paperwork is tested daily with Nextcloud. While this is not the easiest one " "to install, Nextcloud let you host your files yourself. There are other self-" "hosted alternatives that exist: SparkleShare, Syncthing, etc." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:761 msgid "" "Using DropBox or OneDrive can make sense if you're sharing not-so-" "confidential documents with others (associations, etc)." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:764 msgid "Shared folder" msgstr "Dossièr partejat" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:769 msgid "" "If all your computers are on the same network, you can share your work " "directory. However, be really careful regarding permissions. Being too " "permissive could let a pirate access all your personal documents ! And " "setting them correctly is tricky." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:772 msgid "" "Beware: Using a shared folder means having a single copy of your work " "directory. You should do regular backups of your work directory." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:775 msgid "Encryption" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:777 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1024 msgid "GNU/Linux" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:779 msgid "" "GNU/Linux distributions include many tools to encrypt whole directories." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "With Paperwork, there are 2 directories that should be encrypted to protect " "your privacy:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "Your work directory (by default \\textasciitilde /papers, can be changed in " "the settings)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "The cache directory (\\textasciitilde /.local/share/paperwork2, cannot be " "changed) (it contains index files from which the content of your documents " "could be partially recovered)" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:793 msgid "" "Note that if you want to be sure that your data are always encrypted, it's " "recommended to encrypt your whole home directory or even your whole system " "if possible." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:795 msgid "cryptsetup" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:799 msgid "" "Most GNU/Linux distribution installer now provide an optio4n to encrypt your " "whole system or your whole /home with cryptsetup . This is the recommended " "method to protect your documents." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:801 msgid "Encfs" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:803 msgid "Encfs can also be used to create encrypted directories easily." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:807 msgid "" "Beware that Encfs seems to have some security weaknesses. So, while it's " "probably enough to prevent a laptop thief from accessing your documents, " "it's likely to be not enough to prevent the NSA or the police from doing " "so ;-)." msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:811 #, no-wrap msgid "" "$ encfs ~/.local/share/.paperwork2 ~/.local/share/paperwork2\n" "$ encfs ~/.papers ~/papers" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:818 msgid "" "On Windows, you're strongly advised to enable BitLocker to protect your " "documents. If unavailable, there are other applications (Veracrypt, etc)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:821 msgid "Keyboard shortcuts" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:832 msgid "" "Keyboard shortcuts can be seen by opening the application menu, selecting " "\"Help\" and then \"Shortcuts\"." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:835 msgid "Paperwork's files locations" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "By default:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "Configuration: \\textasciitilde /.config/paperwork2.conf" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "Index: \\textasciitilde /.local/share/paperwork2" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 #, fuzzy #| msgid "Documents and pages" msgid "Documents: \\textasciitilde /papers" msgstr "Los documents e las paginas" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "" "(same paths are used on Windows; \\textasciitilde{} = C:\\textbackslash " "Users{[}login{]} ; folders are hidden)" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:848 msgid "" "The index is always updated according based on the documents in the work " "directory. When Paperwork starts, the modification time of each file is used " "to detect changes on the documents." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:850 msgid "Work directory layout" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:852 msgid "workdir$|$rootdir = \\textasciitilde /papers (by default)" msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:854 msgid "Global organisation" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:856 msgid "In the work directory, you have folders, one per document." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:860 msgid "" "The folder names are (usually) the scan/import date of the document: YYYYMMDD" "\\_hhmm\\_ss{[}\\_{]}. The suffix 'idx' is optional and is just a " "number added in case of name collision." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "In every folder you have:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "For image documents:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "paper.$<$X$>$.jpg: The original page in JPG format (X starts at 1)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "paper.$<$X$>$.edited.jpg (optional): The page as edited by the user (X " "starts at 1)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "" "paper.$<$X$>$.words (optional): A hOCR file, containing all the words found " "on the page using the OCR (optional, but required for indexing ; can be " "regenerated with the options \"Redo OCR\")." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "" "paper.1.thumb.jpg (optional, generated automatically): A thumbnail version " "of the page (faster to load)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "labels (optional): a text file containing the labels applied on this document" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "extra.txt (optional): extra keywords added by the user" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "For PDF documents:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "doc.pdf: the document" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "paper.$<$X$>$.words (optional): A hOCR file, containing all the words found " "on the page using the OCR. Some PDF contains crap instead of the real text, " "so running the OCR on them can sometimes be useful." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "passwd.txt (optional): PDF password, if the PDF is password-protected." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "doc.docx / doc.odt / ... (optional): Original file. Converted into PDF (doc." "pdf) so Paperwork can parse and display it more quickly." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:894 msgid "Here is an example a work directory organisation:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:926 #, no-wrap msgid "" "$ find ~/papers\n" "/home/jflesch/papers\n" "/home/jflesch/papers/20130505_1518_00\n" "/home/jflesch/papers/20130505_1518_00/paper.1.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.1.thumb.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.1.words\n" "/home/jflesch/papers/20130505_1518_00/paper.2.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.2.edited.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.2.words\n" "/home/jflesch/papers/20130505_1518_00/paper.3.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.3.words\n" "/home/jflesch/papers/20130505_1518_00/labels\n" "/home/jflesch/papers/20110726_0000_01f\n" "/home/jflesch/papers/20110726_0000_01/paper.1.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.1.thumb.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.1.words\n" "/home/jflesch/papers/20110726_0000_01/paper.2.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.2.words\n" "/home/jflesch/papers/20110726_0000_01/extra.txt\n" "/home/jflesch/papers/20130106_1309_44\n" "/home/jflesch/papers/20130106_1309_44/doc.pdf\n" "/home/jflesch/papers/20130106_1309_44/paper.1.thumb.jpg\n" "/home/jflesch/papers/20130106_1309_44/paper.2.edited.jpg\n" "/home/jflesch/papers/20130106_1309_44/paper.2.words\n" "/home/jflesch/papers/20130106_1309_44/labels\n" "/home/jflesch/papers/20130106_1309_44/extra.txt\n" "/home/jflesch/papers/20130106_1309_44/passwd.txt\n" "/home/jflesch/papers/20130520_1309_44\n" "/home/jflesch/papers/20130520_1309_44/doc.pdf\n" "/home/jflesch/papers/20130520_1309_44/doc.docx\n" "/home/jflesch/papers/20130520_1309_44/labels" msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:929 msgid "hOCR files" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:931 msgid "With Tesseract, the hOCR file can be obtained with following command:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:933 #, no-wrap msgid "tesseract paper..jpg paper. -l hocr && mv paper..html paper..words" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:935 msgid "For example:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:937 #, no-wrap msgid "tesseract paper.1.jpg paper.1 -l fra hocr && mv paper.1.html paper.1.words" msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:940 msgid "Label files" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:942 msgid "Here is an example of content of a label file:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:944 #, no-wrap msgid "facture,#0000b1588c61 logement,#f6b6ffff0000" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:947 msgid "" "It's always $[$label$]$,$[$color$]$. For a same label, the color should " "always be the same." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:950 msgid "Getting support" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:953 msgid "" "A forum dedicated to Paperwork exists: \\href{https://forum.openpaper.work}" "{https://forum.openpaper.work}." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:956 msgid "" "There is also an IRC channel for live discussions: \\href{https://webchat." "freenode.net/}{Freenode}, channel \\#openpaperwork" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:959 msgid "" "If you have questions regarding Paperwork or simply want to chat, those are " "the places to go." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:961 msgid "Reporting issues" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:964 msgid "" "If you noticed a bug in Paperwork (and you are sure it's a bug), you can " "make a bug report." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:966 msgid "Bug Tracker" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:969 msgid "" "One way to create bug reports is to create tickets on \\href{https://gitlab." "gnome.org/World/OpenPaperwork/paperwork/issues}{Paperwork bug tracker: " "https://gitlab.gnome.org/World/OpenPaperwork/paperwork/issues}." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:972 msgid "" "This is the recommended way to submit a bug report if you would like to " "discuss it with Paperwork developpers." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:975 msgid "" "To make sure you include all the required informations, you can use the tool " "integrated in Paperwork (see below)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:978 msgid "Automatic bug report" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:985 msgid "" "Paperwork includes a tool to make reporting bugs easier. It allows you to " "get easily all the required information to make a perfect bug report." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:989 msgid "" "All attachments are automatically censored to protect your privacy: Document " "contents are blurred in screenshots and logs are censored to remove your " "user name." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:992 msgid "" "If the bug you want to report is related to scanners, please include " "\"Scanner info.\" in the bug report files." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:995 msgid "" "If te bug you want to report is related to a display problem, please include " "\"App. screenshots\" in the bug report files." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:998 msgid "ZIP file" msgstr "Fichièr ZIP" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1002 msgid "" "You can then obtain a ZIP file with all the data. Please make sure the " "content of the ZIP file does not contain private information (it shouldn't, " "but better safe than sorry). Then you can add this ZIP file to a ticket on " "Gitlab." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1005 msgid "Automatic submission" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1009 msgid "" "You can also let the tool submit the bug report to openpaper.work " "automatically. In that case, you won't be able to discuss the bug with " "developers (or you have to leave a way to contact you in the bug report)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1016 msgid "" "If you use the automatic submission , the tool will give you an URL to see " "the submitted bug report. This URL is private and shouldn't be shared until " "you made sure there is no private information in the bug report. If there is " "private information, you can request deletion of the bug report by sending " "an email to jflesch@openpaper.work (please specify the private URI in your " "mail so we can be sure that you are the one who submitted the bug report)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1019 msgid "Uninstalling" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1022 msgid "" "Paperwork can be uninstalled. Uninstalling Paperwork \\emph{will never} " "remove your work directory or your documents." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1028 msgid "" "If you installed Paperwork using the package manager from your distribution " "(the recommended way), the uninstallation method depends on the package " "manager." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1031 msgid "" "For instance, on GNU/Linux Debian or GNU/Linux Ubuntu, the following command " "will take care of it:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1034 #, no-wrap msgid "sudo apt remove --purge paperwork-\\*" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1037 msgid "If you installed it using Flatpak, you can use the following command:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1040 #, no-wrap msgid "flatpak --user uninstall work.openpaper.Paperwork" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1043 msgid "Windows 10" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1047 msgid "" "Paperwork can be uninstalled as any Windows applications, by going in " "Windows Control Panel, clicking on \"Applications\", finding Paperwork in " "the list, and then clicking on \"uninstall\"." msgstr "" paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/data/l10n/sv.po000066400000000000000000002124361417573700700265160ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE # Copyright (C) YEAR Free Software Foundation, Inc. # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "POT-Creation-Date: 2021-10-09 12:56+0200\n" "PO-Revision-Date: 2021-01-05 17:31+0000\n" "Last-Translator: Åke Engelbrektson \n" "Language-Team: Swedish \n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.4\n" #. type: Plain text #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:11 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "\\date{}" msgstr "" #. type: title{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:11 msgid "Welcome to Paperwork !" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:20 msgid "" "They are going to drive you crazy. Your phone operator, your bank, your " "daughter's school, your dog's veterinarian, even your ISP; it seems like all " "of them are trying to drown you under tons of papers. Papers you have to " "read, classify, and memorize just in case you may need them later. Most of " "the time you won't, which means you waste your energy for nothing." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:24 msgid "" "Paperwork will help you get rid of all those papers by turning them into " "searchable documents. It's simple: just scan and forget. Looking for a " "specific paper? Just type in a few keywords and tada" msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:28 msgid "Documents and pages" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:36 msgid "" "Paperwork's interface is composed of two panels. On the left (green) is the " "list of all your documents sorted by the date they were imported. On the " "right (blue) are the pages of the currently selected paper." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:41 msgid "" "You can add papers from several sources, depending on the devices connected " "to your computer: scanner flatbed, scanner feeder, camera, etc. You have no " "scanner at home? You can still use the scanner you have at work. Paperwork " "will easily import PDF and image files." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:44 msgid "Find" msgstr "Sök" #. type: wrapfigure #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:50 msgid "{r}{0.5\\textwidth}" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:56 msgid "" "Find what you need, when you need it. Type a few keywords in the search bar " "and the list of papers will shrink to only the relevant content. This is " "where the magic happens: Paperwork uses optical character recognition (OCR) " "to convert your papers into simple text files, so it's easy to search for " "text." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:59 msgid "Export" msgstr "Exportera" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:69 msgid "" "Sometimes you may want to export a document to send it to someone else. " "Multiple formats are supported: .pdf, .jpg, .txt, etc. And of course, paper " "(requires a printer, sold separately)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:72 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:113 msgid "Labels and additional keywords" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:84 msgid "" "You answered an important email and you want to keep track of it? The paper " "you scanned was so unreadable that Paperwork failed to recognize some " "important keywords? Add keywords to your paper so you won't miss anything! " "All the keywords you add will be searchable, as if they were directly " "written on the paper you scanned." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:89 msgid "" "You would like to organize your documents a bit more? You can also add " "labels to your documents. Each label has its own color. With time, Paperwork " "will learn which labels go on which documents and will automatically apply " "them on new documents." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:92 msgid "Your first documents" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:102 msgid "" "Click the + button, the scan button, and that's all folks! You are now aware " "of the main features of Paperwork. You can start using it by adding your " "first own paper." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:105 msgid "" "This document will automatically disappear from your document list as soon " "as you have created or imported your first document." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:108 msgid "Need more help ?" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:116 msgid "" "If you need more help, there is a comprehensive manual you can find in the " "help section of Paperwork." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:119 msgid "" "We hope that you'll enjoy this piece of software. If you like it please tell " "us, and if you don't please tell us why!" msgstr "" #. type: hypersetup{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "" "colorlinks, citecolor=black, filecolor=black, linkcolor=black, " "urlcolor=black, linktoc=all," msgstr "" #. type: title{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "Paperwork manual" msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:32 msgid "Introduction" msgstr "Introduktion" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:41 msgid "" "Most personal documents are fairly recurrent: earning statements, rent " "bills, electricity bills, etc. For most unorganized people, having to find " "them back later is worrisome, at best. For most organized people, naming and " "sorting them is as tedious as watching paint dry." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:45 msgid "" "The main idea behind Paperwork is that managing documents is a computer " "job. Humans should do as little as possible while machines do most of the " "work. The end goal here is \"scan \\& forget\"." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:49 msgid "" "If you're looking for a software that will let you name each document " "individually, organize them in complex hierachy, tag them manually each " "time, fix OCR minor glitches, etc, then Paperwork is not for you." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:52 msgid "Definitions" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:54 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:158 msgid "Work directory" msgstr "Arbetsmapp" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:57 msgid "" "Paperwork stores all your documents in a single directory: the work " "directory. In this directory, each document has its own sub-directory." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:61 msgid "" "While this makes Paperwork hard to use with other tools, it has one major " "advantage: You don't have to worry about file names and directory structures " "anymore." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:63 msgid "Document" msgstr "Dokument" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:71 msgid "" "In Paperwork, a document is a set of pages. On disk, it can either be a set " "of JPEG files or a PDF file." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:74 msgid "" "Documents are identified only by a date. It can either be the date you " "imported them (default) or some date of your choosing." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:77 msgid "" "They are displayed on the left side of the main window (green part on the " "screenshot above)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:80 msgid "Page" msgstr "Sida" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:82 msgid "" "In Paperwork, a page is just an image and the word positions on this image." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:88 msgid "" "Images can come from a scanner or be imported. In those cases, it is stored " "as a JPEG files and text is extracted using OCR (Optical Character " "Recognition). OCR is a fairly long process. It can take up to a few minutes " "for each page. So the text extracted from images is stored in hOCR files " "beside the JPEG files." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:91 msgid "" "Pages can also be the pages from a PDF file. In that case, by default, " "Paperwork just stores a copy of the PDF file." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:95 msgid "" "Paperwork does not track whether a page is recto or verso. Paperwork does " "not track the paper size corresponding to a page (A4, Letter, etc)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:98 msgid "" "Pages are displayed on the right side of the main window (blue part on the " "screenshot above)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:101 msgid "Indexation and Keywords" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:108 msgid "" "Of course, you need a way to find back your documents. Paperwork manages an " "index with all the keywords found in your documents." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:110 msgid "Just type in a few keywords, and you will get your documents back." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:128 msgid "" "Unfortunately, sometimes, documents don't contain the keywords needed to " "find them back. Also OCR is not a perfectly realiable process and may not " "work." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:131 msgid "" "To mitigate those issues, you can add labels (or tags) on your documents and " "provide additional keywords. Both are added to the index." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:134 msgid "" "Labels are displayed beside documents. Additional keywords are almost never " "displayed." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:137 msgid "Settings" msgstr "Inställningar" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:139 msgid "Accessing the settings" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:166 msgid "" "The work directory is the directory where you want all your documents " "stored. It can be a standard folder, a folder synchronized across multiple " "computers or on a network share." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:169 msgid "" "Once you close the settings dialog, the work directory will be scanned and " "Paperwork index will be updated according to its index." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:172 msgid "" "Each time Paperwork starts, it will look for changes in this folder and " "synchronize its index accordingly." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:175 msgid "Scanner" msgstr "Skanner" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:182 msgid "Device" msgstr "Enhet" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:189 msgid "" "When starting, Paperwork looks for scanners. The scanner to use can be " "selected in the settings." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:191 msgid "Webcams, file storage, etc, cannot be used. Only paper-eaters." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:194 msgid "Scan Mode" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:202 msgid "" "Most modern scanners scan in color in a reasonable time. However some older " "scanners scan much faster in grayscale or even in black\\&white. Here you " "can select the mode to use." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:205 msgid "Scan Resolution" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:212 msgid "" "Scanner resolution defines how detailed the images coming from your scanner " "must be." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "Higher resolutions mean" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "longer scans," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "longer OCR," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "more time to display," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "more space used on disk," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "but also better OCR." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "Lower resolutions mean" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "shorter scans," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "shorter OCR," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "less time to display," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "less space used on disk," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "but also inferior OCR," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "and possibly unreadable image (even by a human)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:234 msgid "" "300 dpi is considered a good trade-off. You may want to reduce it to 200 dpi " "on slow computers." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:237 msgid "Scanner calibration" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:247 msgid "" "Scanners tend to provide images actually bigger than the scanned pages. " "Since most of the time, you will always scan pages having the same size (A4 " "or Letter usually), Paperwork provides an option called scanner calibration. " "Scanner calibration in Paperwork is simply an area that will always be " "cropped out of images coming from the scanner." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:250 msgid "OCR" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:257 msgid "" "By default, Paperwork uses Tesseract for the OCR. If unavailable, it falls " "back on Cuneiform." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:260 msgid "" "On Linux, if installed with Flatpak, Paperwork is always provided with " "Tesseract. On Windows, Paperwork is always provided with Tesseract." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:263 msgid "" "To get better results, OCR tool need to know the language used in the " "document(s)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:268 msgid "" "The language available in the settings dialog of Paperwork are those " "understood by the OCR tool. If your language is not in the list, it means " "the OCR tool doesn't have the data required to read your language and you " "must install them." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:271 msgid "Adding languages" msgstr "" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:273 msgid "Flatpak" msgstr "Flatpak" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:279 #, no-wrap msgid "" "# is a list of 2-letters language codes separated ';'\n" "# ex: en;fr;de\n" "flatpak config --user --set languages \"\"\n" "flatpak update --user" msgstr "" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:283 msgid "Debian" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:288 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:304 #, no-wrap msgid "" "# is a 3-letter language code\n" "# ex: 'fra' for French\n" "$ sudo apt-get install tesseract-ocr tesseract-ocr-" msgstr "" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:291 msgid "Fedora" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:296 #, no-wrap msgid "" "# is a 3-letter language code\n" "# ex: 'fra' for French\n" "$ sudo dnf install tesseract tesseract-langpack-" msgstr "" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:299 msgid "Ubuntu" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:307 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:815 msgid "Windows" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:310 msgid "" "Tesseract and all its data files are provided by Paperwork's installer. You " "can rerun the installer to install other languages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:314 msgid "" "If a language is not available in the installer, it either means it hasn't " "been packaged (in which case you can request it), or there is no data file " "available yet for this language." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:317 msgid "Disabling OCR" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:323 msgid "" "When you scan a page using Paperwork, Paperwork will immediately run the OCR " "on it. This process may take a while for each page. In case you want to scan " "a lot of pages quickly (for instance, the first time you use Paperwork), OCR " "can be temporarily disabled. To disable OCR, you simply have to unselect all " "OCR languages." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:326 msgid "Updates" msgstr "Uppdateringar" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:334 msgid "" "If you enable this option, when Paperwork starts, Paperwork will look for " "updates if it hasn't done so for a week or more. To know if a new version is " "available, it has to send an HTTPS query to 'openpaper.work'." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:336 msgid "If an update is found, it will notify you but it won't install it." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:339 msgid "New document" msgstr "Nytt dokument" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:345 msgid "" "By default, in the document list, Paperwork includes a document called \"New " "document\". If you open it, it always appears empty. This document actually " "doesn't exist yet on disk, but will exist as soon as you put a page in it. " "You can add pages in it by scanning, importing file(s) or dropping a page " "from another in it." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:350 msgid "" "As soon as you put any content in it, this document will get its own date " "(the current one by default). In the document list, \"New document\" will be " "replaced by this date, and a new \"New document\" will be added to the " "document list." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:359 msgid "" "If you are currently searching something (see the chapter \"Searching\"), " "only search results are displayed and therefore this \"New document\" isn't " "displayed. You can get it back by clicking the button \"+\" in the top left " "corner of the main window." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:362 msgid "Scanning" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:368 msgid "" "If a scanner has been selected in the settings, you can use it to scan pages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:372 msgid "" "In the header bar, there is a button to add pages. The small arrow on the " "right gives access to possible page sources. Those page sources include your " "scanner sources (Flatbed, Feeder)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:375 msgid "" "Once you've selected the scanner source you want to use, you can click on " "the button \"Scan from ...\"." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:377 msgid "This will start a scan session:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "" "Scanned pages are appended at the end of the current document. If you use a " "feeder, Paperwork will scan pages until the feeder is empty." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will then crop them according to scanner calibration." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will run OCR on them" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will index them" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:389 msgid "" "If this scan session creates a new document, Paperwork will try to set " "labels automatically on the document." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:392 msgid "Importing" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:399 msgid "Images" msgstr "Bilder" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:402 msgid "" "Paperwork supports a lot of file formats. It supports JPEG, PNG, GIF, BMP, " "TIFF, etc." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:404 msgid "Each image file is considered as a page." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:408 msgid "" "Images are always appended to the document currently opened. Simply select " "an empty document (\"New document\") to create a new document while " "importing." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:412 msgid "" "OCR is always run on imported images. If the imported image is the first " "page of a new document, Paperwork will automatically apply documents labels." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:416 msgid "" "Note that Paperwork is a document manager. While it can, it is not designed " "to handle images with only very little text or photos. Automatic labeling " "will not work correctly on such documents." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:420 msgid "" "The OCR (Tesseract) works very well with black text on white background. " "Automatic labeling uses recognized text and requires as many keywords on the " "first page as possible." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:423 msgid "PDF" msgstr "PDF" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:429 msgid "" "Each PDF is always considered as a whole document. They are never appended " "to existing document. They are copied and renamed in the work directory, but " "their content is not modified. Paperwork always keeps the original PDF file " "as is, even if you edit some of its pages: the edited pages are stored " "beside the PDF file." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:434 msgid "" "Paperwork will look for pages with no text attached. On those pages, it will " "automatically run OCR. Once all the pages have been examined, it will " "automatically apply document labels. Note that this process may take a few " "minutes for big PDFs files." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:437 msgid "" "If the PDF is already part of your documents, Paperwork will simply ignore " "it." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:440 msgid "Many PDFs in one shot" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:445 msgid "" "When importing, if you select a folder, Paperwork will browse this folder " "and look for PDFs to import. Already-imported PDFs are simply ignored. " "Folder is browsed recursively (all the folders inside the folder are also " "examined)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:448 msgid "Labels" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:464 msgid "" "There is currently one constraint in Paperwork: Each label must be on at " "least one document. Otherwise, when you will restart Paperwork, labels " "without documents will disappear." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:467 msgid "Creating new labels" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:475 msgid "" "You can click on the gray rectangle on the left side to pick the label " "color. You can enter the label name in text field between the gray " "rectangle and the button \"+\"." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:478 msgid "" "Once you click on the button \"+\", the label will be added to the current " "document." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:481 msgid "" "The label is actually added once you close document properties. Paperwork " "will then update its index accordingly." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:484 msgid "Setting labels on documents" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:488 msgid "" "When you open document properties, the label list appears. On the left side " "of each label color, you have a button. This button allows you to add or " "remove labels on the current document." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:491 msgid "" "The changes are actually written on disk once you close the document " "properties. Paperwork will update its index accordinly." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:494 msgid "Modifying a label color" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:497 msgid "" "When you open document properties, you can click on a label color to change " "it. A dialog will let you pick the new color." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:501 msgid "" "Label color will actually be changed on disk when you close the document " "properties. Paperwork will then update the label on all the documents that " "use it." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:504 msgid "Modifying a label name" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:507 msgid "" "When you open document properties, you can click on a label string to change " "it. A dialog will let you type in the new name." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:511 msgid "" "Label name will actually be changed on disk when you close the document " "properties. Paperwork will then update the label on all the documents that " "use it and then reindex them all." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:514 msgid "Deleting a label" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:517 msgid "" "To the right of each label is white-on-black cross button. Clicking on it " "will allow you to delete a label." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:520 msgid "" "Once you will close the document properties, the label will be removed from " "all the documents having it. Paperwork will then update its index " "accordingly." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:523 msgid "" "Beware: Once you have closed document properties, there is no way to put " "back the deleted label." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:526 msgid "Automatic label guessing" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:531 msgid "" "Paperwork does use artificial intelligence. It uses a fairly simple method " "actually: \\href{https://en.wikipedia.org/wiki/Naive_Bayes_classifier}{Naive " "Bayes classifiers}. It's the same technology used by email clients to " "classify mails as spam/non-spam." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:537 msgid "" "Based on all the keywords in all your documents that have (or haven't) a " "label, it can estimate a probability that a document containing the same " "keywords should have or shouldn't have this same label. If the probability " "is high enough, it puts the label on the document automatically when you " "import it or scan it." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:541 msgid "" "Of course, this approach means that Paperwork needs enough samples to work " "reliably. You can expect it to start working once you have about 100 " "documents or more (and only for labels that are on more than 10 documents or " "more)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:544 msgid "Searching" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:546 msgid "Simple search" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:553 msgid "" "You simply enter keywords in the search field. In a few seconds, you will " "get all the documents containing those keywords." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:557 msgid "" "Paperwork does a \"fuzzy\" search: documents with keywords close to the one " "you gave but not identical are also returned (for instance, 'flech' instead " "of 'flesch')." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:562 msgid "" "You can also use \\href{https://whoosh.readthedocs.io/en/latest/querylang." "html}{Whoosh query language} to make more complex queries. If you want " "examples, you can use the advanced search dialog described below." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:565 msgid "Advanced search" msgstr "Avancerat sök" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:582 msgid "" "The advanced search dialog helps creating complex search queries. You can " "specify various criterias and once you click on the apply button, it will " "generate a search query for you and put it immediately in the search field. " "Search results will immediately be refreshed as well." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:585 msgid "Viewing" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:591 msgid "Zoom level" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:597 msgid "" "You can change the scale at which pages are displayed using this control." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:600 msgid "View pages as grid" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:608 msgid "" "When clicking this button, Paperwork will try to display pages on 3 " "columns. In this mode, you can drag'n'drop pages to move them inside the " "document or to another document." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:611 msgid "View pages as list" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:619 msgid "" "When clicking this button, pages will be scaled so their width is the " "maximum width allowed by the main window. In this mode, you can select text " "in the page (and then copy it)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:621 msgid "Highlight all words" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:629 msgid "" "This option allows to see quickly all the words identified by OCR. Sometimes " "(rarely) OCR misses entire chunk in a page. This option allow to see such " "chunk quickly." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:631 msgid "Moving pages" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:633 msgid "Inside a document" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:635 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:644 msgid "You must display the document pages as a grid (See \\ref{layout:grid})." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:640 msgid "" "You can then grab a page (hold the left click button), drag it and drop it " "wherever you want in a document. While dragging, a blue marker will show you " "where the page would drop if you release the left click button of your mouse." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:642 msgid "From a document to another" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:647 msgid "" "You can then grab a page (hold the left click button), drag it and drop it " "in the document list, on the document in which you want the page to go." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:649 msgid "Copying text" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:655 msgid "" "You must display the document pages as a list (See \\ref{layout:paged})." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:658 msgid "" "You can then select text in a page. Hold the left click button to start " "selecting, mouse the mouse cursor to select more words, then release it." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:662 msgid "" "You can then copy the selected text, either by pressing Ctrl-C or by using " "the page menu at the bottom right of the main window. Once copied, you can " "paste the selected text in any other application (Ctrl-V)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:664 msgid "Editing a page" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:670 msgid "Paperwork includes a very simple image editor. It provides 4 functions:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Cropping" msgstr "Beskär" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Rotating the page by 90\\degree (can be rotated multiple times)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Rotating the page by -90\\degree (can be rotated multiple times)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "" "Automatic Color Equalization: An algorithm that adjust the image brightness, " "contrast and colors to make it as readable as possible." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:681 msgid "Reseting a page" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:684 msgid "" "Reseting a page returns it to its state when it was scanned or imported, " "before any pre-processing did occur." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:688 msgid "" "This can be helpful if you made a bad modification on the page (cropped a " "wrong area for instance), if the calibration settings weren't appropriate or " "if pre-processing algorithms messed up the page." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:691 msgid "Deleting" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:694 msgid "" "When deleting either documents or pages, they are actually moved in the " "trash bin of your computer." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:698 msgid "" "\\textbf{Important note regarding Flatpak:} A bug may prevent Paperwork from " "moving files to the trash (we are working on it). In that case, Paperwork " "will delete the file directly (no recovery possible)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:701 msgid "Exporting" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:703 msgid "You can export both documents or single pages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:707 msgid "" "In both cases, various transformations can be applied before actually " "exporting them. For instance, you can turn color pages into grayscale pages " "before putting them in a brand new PDF (making the resulting PDF smaller)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:710 msgid "Printing" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:712 msgid "You can print both documents or single pages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:715 msgid "" "Beware that pages are always sent as images to your printer. So for very big " "documents, a few minutes may go by before the actual printing start." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:718 msgid "Backup" msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:720 msgid "Synchronisation between multiple computers" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:726 msgid "" "While Paperwork is a personal document manager, it is not a file " "synchronization application. They are applications dedicated to file " "synchronization that already do that very well. Therefore Paperwork is " "designed to be used with such applications (Nextcloud, Dropbox, OneDrive, " "SparkleShare, etc)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:733 msgid "" "When you start Paperwork, one of the first things it does is check the " "content of the work directory. It looks for any changes and updates its " "document list and index accordingly, automatically. So if another instance " "of Paperwork on another computer modified something in the work directory " "and if this change has been synchronized on another computer, the other " "Paperwork will automatically pick up this change when starting." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:736 msgid "USB key / USB drive" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:739 msgid "" "This is the simplest way to share documents. Simply copy your work directory " "to an USB key, tell Paperwork to use it, and you're done." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:741 msgid "" "Beware: You should backup your USB key from time to time on another one." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:744 msgid "File Synchronization applications" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:750 msgid "" "Those applications synchronize a local directory with a remote server (or " "cloud). All the changes you do in your folder are applied on the server. All " "the changes applied on the servers are applied to the computers that connect " "to it. The server can belong to you or to someone else (usually a company)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:754 msgid "" "Beware: If you choose to host your documents on someone else server " "(DropBox, OneDrive, etc), they can access all your documents. Paperwork does " "not encrypt them." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:758 msgid "" "Paperwork is tested daily with Nextcloud. While this is not the easiest one " "to install, Nextcloud let you host your files yourself. There are other self-" "hosted alternatives that exist: SparkleShare, Syncthing, etc." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:761 msgid "" "Using DropBox or OneDrive can make sense if you're sharing not-so-" "confidential documents with others (associations, etc)." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:764 msgid "Shared folder" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:769 msgid "" "If all your computers are on the same network, you can share your work " "directory. However, be really careful regarding permissions. Being too " "permissive could let a pirate access all your personal documents ! And " "setting them correctly is tricky." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:772 msgid "" "Beware: Using a shared folder means having a single copy of your work " "directory. You should do regular backups of your work directory." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:775 msgid "Encryption" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:777 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1024 msgid "GNU/Linux" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:779 msgid "" "GNU/Linux distributions include many tools to encrypt whole directories." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "With Paperwork, there are 2 directories that should be encrypted to protect " "your privacy:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "Your work directory (by default \\textasciitilde /papers, can be changed in " "the settings)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "The cache directory (\\textasciitilde /.local/share/paperwork2, cannot be " "changed) (it contains index files from which the content of your documents " "could be partially recovered)" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:793 msgid "" "Note that if you want to be sure that your data are always encrypted, it's " "recommended to encrypt your whole home directory or even your whole system " "if possible." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:795 msgid "cryptsetup" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:799 msgid "" "Most GNU/Linux distribution installer now provide an optio4n to encrypt your " "whole system or your whole /home with cryptsetup . This is the recommended " "method to protect your documents." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:801 msgid "Encfs" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:803 msgid "Encfs can also be used to create encrypted directories easily." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:807 msgid "" "Beware that Encfs seems to have some security weaknesses. So, while it's " "probably enough to prevent a laptop thief from accessing your documents, " "it's likely to be not enough to prevent the NSA or the police from doing " "so ;-)." msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:811 #, no-wrap msgid "" "$ encfs ~/.local/share/.paperwork2 ~/.local/share/paperwork2\n" "$ encfs ~/.papers ~/papers" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:818 msgid "" "On Windows, you're strongly advised to enable BitLocker to protect your " "documents. If unavailable, there are other applications (Veracrypt, etc)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:821 msgid "Keyboard shortcuts" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:832 msgid "" "Keyboard shortcuts can be seen by opening the application menu, selecting " "\"Help\" and then \"Shortcuts\"." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:835 msgid "Paperwork's files locations" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "By default:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "Configuration: \\textasciitilde /.config/paperwork2.conf" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "Index: \\textasciitilde /.local/share/paperwork2" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "Documents: \\textasciitilde /papers" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "" "(same paths are used on Windows; \\textasciitilde{} = C:\\textbackslash " "Users{[}login{]} ; folders are hidden)" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:848 msgid "" "The index is always updated according based on the documents in the work " "directory. When Paperwork starts, the modification time of each file is used " "to detect changes on the documents." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:850 msgid "Work directory layout" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:852 msgid "workdir$|$rootdir = \\textasciitilde /papers (by default)" msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:854 msgid "Global organisation" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:856 msgid "In the work directory, you have folders, one per document." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:860 msgid "" "The folder names are (usually) the scan/import date of the document: YYYYMMDD" "\\_hhmm\\_ss{[}\\_{]}. The suffix 'idx' is optional and is just a " "number added in case of name collision." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "In every folder you have:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "For image documents:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "paper.$<$X$>$.jpg: The original page in JPG format (X starts at 1)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "paper.$<$X$>$.edited.jpg (optional): The page as edited by the user (X " "starts at 1)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "" "paper.$<$X$>$.words (optional): A hOCR file, containing all the words found " "on the page using the OCR (optional, but required for indexing ; can be " "regenerated with the options \"Redo OCR\")." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "" "paper.1.thumb.jpg (optional, generated automatically): A thumbnail version " "of the page (faster to load)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "labels (optional): a text file containing the labels applied on this document" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "extra.txt (optional): extra keywords added by the user" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "For PDF documents:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "doc.pdf: the document" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "paper.$<$X$>$.words (optional): A hOCR file, containing all the words found " "on the page using the OCR. Some PDF contains crap instead of the real text, " "so running the OCR on them can sometimes be useful." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "passwd.txt (optional): PDF password, if the PDF is password-protected." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "doc.docx / doc.odt / ... (optional): Original file. Converted into PDF (doc." "pdf) so Paperwork can parse and display it more quickly." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:894 msgid "Here is an example a work directory organisation:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:926 #, no-wrap msgid "" "$ find ~/papers\n" "/home/jflesch/papers\n" "/home/jflesch/papers/20130505_1518_00\n" "/home/jflesch/papers/20130505_1518_00/paper.1.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.1.thumb.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.1.words\n" "/home/jflesch/papers/20130505_1518_00/paper.2.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.2.edited.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.2.words\n" "/home/jflesch/papers/20130505_1518_00/paper.3.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.3.words\n" "/home/jflesch/papers/20130505_1518_00/labels\n" "/home/jflesch/papers/20110726_0000_01f\n" "/home/jflesch/papers/20110726_0000_01/paper.1.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.1.thumb.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.1.words\n" "/home/jflesch/papers/20110726_0000_01/paper.2.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.2.words\n" "/home/jflesch/papers/20110726_0000_01/extra.txt\n" "/home/jflesch/papers/20130106_1309_44\n" "/home/jflesch/papers/20130106_1309_44/doc.pdf\n" "/home/jflesch/papers/20130106_1309_44/paper.1.thumb.jpg\n" "/home/jflesch/papers/20130106_1309_44/paper.2.edited.jpg\n" "/home/jflesch/papers/20130106_1309_44/paper.2.words\n" "/home/jflesch/papers/20130106_1309_44/labels\n" "/home/jflesch/papers/20130106_1309_44/extra.txt\n" "/home/jflesch/papers/20130106_1309_44/passwd.txt\n" "/home/jflesch/papers/20130520_1309_44\n" "/home/jflesch/papers/20130520_1309_44/doc.pdf\n" "/home/jflesch/papers/20130520_1309_44/doc.docx\n" "/home/jflesch/papers/20130520_1309_44/labels" msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:929 msgid "hOCR files" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:931 msgid "With Tesseract, the hOCR file can be obtained with following command:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:933 #, no-wrap msgid "tesseract paper..jpg paper. -l hocr && mv paper..html paper..words" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:935 msgid "For example:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:937 #, no-wrap msgid "tesseract paper.1.jpg paper.1 -l fra hocr && mv paper.1.html paper.1.words" msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:940 msgid "Label files" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:942 msgid "Here is an example of content of a label file:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:944 #, no-wrap msgid "facture,#0000b1588c61 logement,#f6b6ffff0000" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:947 msgid "" "It's always $[$label$]$,$[$color$]$. For a same label, the color should " "always be the same." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:950 msgid "Getting support" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:953 msgid "" "A forum dedicated to Paperwork exists: \\href{https://forum.openpaper.work}" "{https://forum.openpaper.work}." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:956 msgid "" "There is also an IRC channel for live discussions: \\href{https://webchat." "freenode.net/}{Freenode}, channel \\#openpaperwork" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:959 msgid "" "If you have questions regarding Paperwork or simply want to chat, those are " "the places to go." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:961 msgid "Reporting issues" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:964 msgid "" "If you noticed a bug in Paperwork (and you are sure it's a bug), you can " "make a bug report." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:966 msgid "Bug Tracker" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:969 msgid "" "One way to create bug reports is to create tickets on \\href{https://gitlab." "gnome.org/World/OpenPaperwork/paperwork/issues}{Paperwork bug tracker: " "https://gitlab.gnome.org/World/OpenPaperwork/paperwork/issues}." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:972 msgid "" "This is the recommended way to submit a bug report if you would like to " "discuss it with Paperwork developpers." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:975 msgid "" "To make sure you include all the required informations, you can use the tool " "integrated in Paperwork (see below)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:978 msgid "Automatic bug report" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:985 msgid "" "Paperwork includes a tool to make reporting bugs easier. It allows you to " "get easily all the required information to make a perfect bug report." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:989 msgid "" "All attachments are automatically censored to protect your privacy: Document " "contents are blurred in screenshots and logs are censored to remove your " "user name." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:992 msgid "" "If the bug you want to report is related to scanners, please include " "\"Scanner info.\" in the bug report files." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:995 msgid "" "If te bug you want to report is related to a display problem, please include " "\"App. screenshots\" in the bug report files." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:998 msgid "ZIP file" msgstr "ZIP-fil" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1002 msgid "" "You can then obtain a ZIP file with all the data. Please make sure the " "content of the ZIP file does not contain private information (it shouldn't, " "but better safe than sorry). Then you can add this ZIP file to a ticket on " "Gitlab." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1005 msgid "Automatic submission" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1009 msgid "" "You can also let the tool submit the bug report to openpaper.work " "automatically. In that case, you won't be able to discuss the bug with " "developers (or you have to leave a way to contact you in the bug report)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1016 msgid "" "If you use the automatic submission , the tool will give you an URL to see " "the submitted bug report. This URL is private and shouldn't be shared until " "you made sure there is no private information in the bug report. If there is " "private information, you can request deletion of the bug report by sending " "an email to jflesch@openpaper.work (please specify the private URI in your " "mail so we can be sure that you are the one who submitted the bug report)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1019 msgid "Uninstalling" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1022 msgid "" "Paperwork can be uninstalled. Uninstalling Paperwork \\emph{will never} " "remove your work directory or your documents." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1028 msgid "" "If you installed Paperwork using the package manager from your distribution " "(the recommended way), the uninstallation method depends on the package " "manager." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1031 msgid "" "For instance, on GNU/Linux Debian or GNU/Linux Ubuntu, the following command " "will take care of it:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1034 #, no-wrap msgid "sudo apt remove --purge paperwork-\\*" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1037 msgid "If you installed it using Flatpak, you can use the following command:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1040 #, no-wrap msgid "flatpak --user uninstall work.openpaper.Paperwork" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1043 msgid "Windows 10" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1047 msgid "" "Paperwork can be uninstalled as any Windows applications, by going in " "Windows Control Panel, clicking on \"Applications\", finding Paperwork in " "the list, and then clicking on \"uninstall\"." msgstr "" paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/data/l10n/zh_Hans.po000066400000000000000000002141271417573700700274570ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE # Copyright (C) YEAR Free Software Foundation, Inc. # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "POT-Creation-Date: 2021-10-09 12:56+0200\n" "PO-Revision-Date: 2021-03-24 23:17+0000\n" "Last-Translator: 玉堂白鹤 \n" "Language-Team: Chinese (Simplified) \n" "Language: zh_Hans\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Weblate 4.4\n" #. type: Plain text #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:11 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "\\date{}" msgstr "" #. type: title{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:11 msgid "Welcome to Paperwork !" msgstr "欢迎使用 Paperwork !" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:20 msgid "" "They are going to drive you crazy. Your phone operator, your bank, your " "daughter's school, your dog's veterinarian, even your ISP; it seems like all " "of them are trying to drown you under tons of papers. Papers you have to " "read, classify, and memorize just in case you may need them later. Most of " "the time you won't, which means you waste your energy for nothing." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:24 msgid "" "Paperwork will help you get rid of all those papers by turning them into " "searchable documents. It's simple: just scan and forget. Looking for a " "specific paper? Just type in a few keywords and tada" msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:28 msgid "Documents and pages" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:36 msgid "" "Paperwork's interface is composed of two panels. On the left (green) is the " "list of all your documents sorted by the date they were imported. On the " "right (blue) are the pages of the currently selected paper." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:41 msgid "" "You can add papers from several sources, depending on the devices connected " "to your computer: scanner flatbed, scanner feeder, camera, etc. You have no " "scanner at home? You can still use the scanner you have at work. Paperwork " "will easily import PDF and image files." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:44 msgid "Find" msgstr "查找" #. type: wrapfigure #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:50 msgid "{r}{0.5\\textwidth}" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:56 msgid "" "Find what you need, when you need it. Type a few keywords in the search bar " "and the list of papers will shrink to only the relevant content. This is " "where the magic happens: Paperwork uses optical character recognition (OCR) " "to convert your papers into simple text files, so it's easy to search for " "text." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:59 msgid "Export" msgstr "导出" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:69 msgid "" "Sometimes you may want to export a document to send it to someone else. " "Multiple formats are supported: .pdf, .jpg, .txt, etc. And of course, paper " "(requires a printer, sold separately)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:72 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:113 msgid "Labels and additional keywords" msgstr "标签和其他关键字" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:84 msgid "" "You answered an important email and you want to keep track of it? The paper " "you scanned was so unreadable that Paperwork failed to recognize some " "important keywords? Add keywords to your paper so you won't miss anything! " "All the keywords you add will be searchable, as if they were directly " "written on the paper you scanned." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:89 msgid "" "You would like to organize your documents a bit more? You can also add " "labels to your documents. Each label has its own color. With time, Paperwork " "will learn which labels go on which documents and will automatically apply " "them on new documents." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:92 msgid "Your first documents" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:102 msgid "" "Click the + button, the scan button, and that's all folks! You are now aware " "of the main features of Paperwork. You can start using it by adding your " "first own paper." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:105 msgid "" "This document will automatically disappear from your document list as soon " "as you have created or imported your first document." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:108 msgid "Need more help ?" msgstr "需要更多帮助 ?" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:116 msgid "" "If you need more help, there is a comprehensive manual you can find in the " "help section of Paperwork." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/intro.tex:119 msgid "" "We hope that you'll enjoy this piece of software. If you like it please tell " "us, and if you don't please tell us why!" msgstr "" #. type: hypersetup{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "" "colorlinks, citecolor=black, filecolor=black, linkcolor=black, " "urlcolor=black, linktoc=all," msgstr "" #. type: title{#1} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:20 msgid "Paperwork manual" msgstr "Paperwork 手册" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:32 msgid "Introduction" msgstr "介绍" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:41 msgid "" "Most personal documents are fairly recurrent: earning statements, rent " "bills, electricity bills, etc. For most unorganized people, having to find " "them back later is worrisome, at best. For most organized people, naming and " "sorting them is as tedious as watching paint dry." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:45 msgid "" "The main idea behind Paperwork is that managing documents is a computer " "job. Humans should do as little as possible while machines do most of the " "work. The end goal here is \"scan \\& forget\"." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:49 msgid "" "If you're looking for a software that will let you name each document " "individually, organize them in complex hierachy, tag them manually each " "time, fix OCR minor glitches, etc, then Paperwork is not for you." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:52 msgid "Definitions" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:54 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:158 msgid "Work directory" msgstr "工作目录" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:57 msgid "" "Paperwork stores all your documents in a single directory: the work " "directory. In this directory, each document has its own sub-directory." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:61 msgid "" "While this makes Paperwork hard to use with other tools, it has one major " "advantage: You don't have to worry about file names and directory structures " "anymore." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:63 msgid "Document" msgstr "文档" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:71 msgid "" "In Paperwork, a document is a set of pages. On disk, it can either be a set " "of JPEG files or a PDF file." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:74 msgid "" "Documents are identified only by a date. It can either be the date you " "imported them (default) or some date of your choosing." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:77 msgid "" "They are displayed on the left side of the main window (green part on the " "screenshot above)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:80 msgid "Page" msgstr "页面" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:82 msgid "" "In Paperwork, a page is just an image and the word positions on this image." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:88 msgid "" "Images can come from a scanner or be imported. In those cases, it is stored " "as a JPEG files and text is extracted using OCR (Optical Character " "Recognition). OCR is a fairly long process. It can take up to a few minutes " "for each page. So the text extracted from images is stored in hOCR files " "beside the JPEG files." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:91 msgid "" "Pages can also be the pages from a PDF file. In that case, by default, " "Paperwork just stores a copy of the PDF file." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:95 msgid "" "Paperwork does not track whether a page is recto or verso. Paperwork does " "not track the paper size corresponding to a page (A4, Letter, etc)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:98 msgid "" "Pages are displayed on the right side of the main window (blue part on the " "screenshot above)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:101 msgid "Indexation and Keywords" msgstr "索引和关键字" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:108 msgid "" "Of course, you need a way to find back your documents. Paperwork manages an " "index with all the keywords found in your documents." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:110 msgid "Just type in a few keywords, and you will get your documents back." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:128 msgid "" "Unfortunately, sometimes, documents don't contain the keywords needed to " "find them back. Also OCR is not a perfectly realiable process and may not " "work." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:131 msgid "" "To mitigate those issues, you can add labels (or tags) on your documents and " "provide additional keywords. Both are added to the index." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:134 msgid "" "Labels are displayed beside documents. Additional keywords are almost never " "displayed." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:137 msgid "Settings" msgstr "设置" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:139 msgid "Accessing the settings" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:166 msgid "" "The work directory is the directory where you want all your documents " "stored. It can be a standard folder, a folder synchronized across multiple " "computers or on a network share." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:169 msgid "" "Once you close the settings dialog, the work directory will be scanned and " "Paperwork index will be updated according to its index." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:172 msgid "" "Each time Paperwork starts, it will look for changes in this folder and " "synchronize its index accordingly." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:175 msgid "Scanner" msgstr "扫描仪" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:182 msgid "Device" msgstr "设备" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:189 msgid "" "When starting, Paperwork looks for scanners. The scanner to use can be " "selected in the settings." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:191 msgid "Webcams, file storage, etc, cannot be used. Only paper-eaters." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:194 msgid "Scan Mode" msgstr "扫描模式" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:202 msgid "" "Most modern scanners scan in color in a reasonable time. However some older " "scanners scan much faster in grayscale or even in black\\&white. Here you " "can select the mode to use." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:205 msgid "Scan Resolution" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:212 msgid "" "Scanner resolution defines how detailed the images coming from your scanner " "must be." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "Higher resolutions mean" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "longer scans," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "longer OCR," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "more time to display," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "more space used on disk," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:221 msgid "but also better OCR." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "Lower resolutions mean" msgstr "较低的分辨率意味着" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "shorter scans," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "shorter OCR," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "less time to display," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "less space used on disk," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "but also inferior OCR," msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:231 msgid "and possibly unreadable image (even by a human)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:234 msgid "" "300 dpi is considered a good trade-off. You may want to reduce it to 200 dpi " "on slow computers." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:237 msgid "Scanner calibration" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:247 msgid "" "Scanners tend to provide images actually bigger than the scanned pages. " "Since most of the time, you will always scan pages having the same size (A4 " "or Letter usually), Paperwork provides an option called scanner calibration. " "Scanner calibration in Paperwork is simply an area that will always be " "cropped out of images coming from the scanner." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:250 msgid "OCR" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:257 msgid "" "By default, Paperwork uses Tesseract for the OCR. If unavailable, it falls " "back on Cuneiform." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:260 msgid "" "On Linux, if installed with Flatpak, Paperwork is always provided with " "Tesseract. On Windows, Paperwork is always provided with Tesseract." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:263 msgid "" "To get better results, OCR tool need to know the language used in the " "document(s)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:268 msgid "" "The language available in the settings dialog of Paperwork are those " "understood by the OCR tool. If your language is not in the list, it means " "the OCR tool doesn't have the data required to read your language and you " "must install them." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:271 msgid "Adding languages" msgstr "添加语言" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:273 msgid "Flatpak" msgstr "Flatpak" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:279 #, fuzzy, no-wrap #| msgid "" #| "# is a list of 2-letters language codes separated ';'\n" #| "# ex: en;fr;de\n" #| "flatpak config --user --set languages \"\"\n" #| "flatpak update" msgid "" "# is a list of 2-letters language codes separated ';'\n" "# ex: en;fr;de\n" "flatpak config --user --set languages \"\"\n" "flatpak update --user" msgstr "" "# 是使用 ';'分开的双字母语言代码列表\n" "# 例如: en;fr;de\n" "flatpak config --user --set languages \"\"\n" "flatpak update" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:283 msgid "Debian" msgstr "Debian" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:288 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:304 #, no-wrap msgid "" "# is a 3-letter language code\n" "# ex: 'fra' for French\n" "$ sudo apt-get install tesseract-ocr tesseract-ocr-" msgstr "" "# 是一个三字语言码\n" "# 例如: 'fra' 表示 French\n" "$ sudo apt-get install tesseract-ocr tesseract-ocr-" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:291 msgid "Fedora" msgstr "Fedora" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:296 #, no-wrap msgid "" "# is a 3-letter language code\n" "# ex: 'fra' for French\n" "$ sudo dnf install tesseract tesseract-langpack-" msgstr "" "# 是一个三字语言码\n" "# 例如: 'fra' 表示 French\n" "$ sudo dnf install tesseract tesseract-langpack-" #. type: paragraph{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:299 msgid "Ubuntu" msgstr "Ubuntu" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:307 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:815 msgid "Windows" msgstr "Windows" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:310 msgid "" "Tesseract and all its data files are provided by Paperwork's installer. You " "can rerun the installer to install other languages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:314 msgid "" "If a language is not available in the installer, it either means it hasn't " "been packaged (in which case you can request it), or there is no data file " "available yet for this language." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:317 msgid "Disabling OCR" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:323 msgid "" "When you scan a page using Paperwork, Paperwork will immediately run the OCR " "on it. This process may take a while for each page. In case you want to scan " "a lot of pages quickly (for instance, the first time you use Paperwork), OCR " "can be temporarily disabled. To disable OCR, you simply have to unselect all " "OCR languages." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:326 msgid "Updates" msgstr "更新" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:334 msgid "" "If you enable this option, when Paperwork starts, Paperwork will look for " "updates if it hasn't done so for a week or more. To know if a new version is " "available, it has to send an HTTPS query to 'openpaper.work'." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:336 msgid "If an update is found, it will notify you but it won't install it." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:339 msgid "New document" msgstr "新建文档" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:345 msgid "" "By default, in the document list, Paperwork includes a document called \"New " "document\". If you open it, it always appears empty. This document actually " "doesn't exist yet on disk, but will exist as soon as you put a page in it. " "You can add pages in it by scanning, importing file(s) or dropping a page " "from another in it." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:350 msgid "" "As soon as you put any content in it, this document will get its own date " "(the current one by default). In the document list, \"New document\" will be " "replaced by this date, and a new \"New document\" will be added to the " "document list." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:359 msgid "" "If you are currently searching something (see the chapter \"Searching\"), " "only search results are displayed and therefore this \"New document\" isn't " "displayed. You can get it back by clicking the button \"+\" in the top left " "corner of the main window." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:362 msgid "Scanning" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:368 msgid "" "If a scanner has been selected in the settings, you can use it to scan pages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:372 msgid "" "In the header bar, there is a button to add pages. The small arrow on the " "right gives access to possible page sources. Those page sources include your " "scanner sources (Flatbed, Feeder)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:375 msgid "" "Once you've selected the scanner source you want to use, you can click on " "the button \"Scan from ...\"." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:377 msgid "This will start a scan session:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "" "Scanned pages are appended at the end of the current document. If you use a " "feeder, Paperwork will scan pages until the feeder is empty." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will then crop them according to scanner calibration." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will run OCR on them" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:386 msgid "Paperwork will index them" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:389 msgid "" "If this scan session creates a new document, Paperwork will try to set " "labels automatically on the document." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:392 msgid "Importing" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:399 msgid "Images" msgstr "图像" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:402 msgid "" "Paperwork supports a lot of file formats. It supports JPEG, PNG, GIF, BMP, " "TIFF, etc." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:404 msgid "Each image file is considered as a page." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:408 msgid "" "Images are always appended to the document currently opened. Simply select " "an empty document (\"New document\") to create a new document while " "importing." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:412 msgid "" "OCR is always run on imported images. If the imported image is the first " "page of a new document, Paperwork will automatically apply documents labels." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:416 msgid "" "Note that Paperwork is a document manager. While it can, it is not designed " "to handle images with only very little text or photos. Automatic labeling " "will not work correctly on such documents." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:420 msgid "" "The OCR (Tesseract) works very well with black text on white background. " "Automatic labeling uses recognized text and requires as many keywords on the " "first page as possible." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:423 msgid "PDF" msgstr "PDF" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:429 msgid "" "Each PDF is always considered as a whole document. They are never appended " "to existing document. They are copied and renamed in the work directory, but " "their content is not modified. Paperwork always keeps the original PDF file " "as is, even if you edit some of its pages: the edited pages are stored " "beside the PDF file." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:434 msgid "" "Paperwork will look for pages with no text attached. On those pages, it will " "automatically run OCR. Once all the pages have been examined, it will " "automatically apply document labels. Note that this process may take a few " "minutes for big PDFs files." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:437 msgid "" "If the PDF is already part of your documents, Paperwork will simply ignore " "it." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:440 msgid "Many PDFs in one shot" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:445 msgid "" "When importing, if you select a folder, Paperwork will browse this folder " "and look for PDFs to import. Already-imported PDFs are simply ignored. " "Folder is browsed recursively (all the folders inside the folder are also " "examined)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:448 msgid "Labels" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:464 msgid "" "There is currently one constraint in Paperwork: Each label must be on at " "least one document. Otherwise, when you will restart Paperwork, labels " "without documents will disappear." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:467 msgid "Creating new labels" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:475 msgid "" "You can click on the gray rectangle on the left side to pick the label " "color. You can enter the label name in text field between the gray " "rectangle and the button \"+\"." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:478 msgid "" "Once you click on the button \"+\", the label will be added to the current " "document." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:481 msgid "" "The label is actually added once you close document properties. Paperwork " "will then update its index accordingly." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:484 msgid "Setting labels on documents" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:488 msgid "" "When you open document properties, the label list appears. On the left side " "of each label color, you have a button. This button allows you to add or " "remove labels on the current document." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:491 msgid "" "The changes are actually written on disk once you close the document " "properties. Paperwork will update its index accordinly." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:494 msgid "Modifying a label color" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:497 msgid "" "When you open document properties, you can click on a label color to change " "it. A dialog will let you pick the new color." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:501 msgid "" "Label color will actually be changed on disk when you close the document " "properties. Paperwork will then update the label on all the documents that " "use it." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:504 msgid "Modifying a label name" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:507 msgid "" "When you open document properties, you can click on a label string to change " "it. A dialog will let you type in the new name." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:511 msgid "" "Label name will actually be changed on disk when you close the document " "properties. Paperwork will then update the label on all the documents that " "use it and then reindex them all." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:514 msgid "Deleting a label" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:517 msgid "" "To the right of each label is white-on-black cross button. Clicking on it " "will allow you to delete a label." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:520 msgid "" "Once you will close the document properties, the label will be removed from " "all the documents having it. Paperwork will then update its index " "accordingly." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:523 msgid "" "Beware: Once you have closed document properties, there is no way to put " "back the deleted label." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:526 msgid "Automatic label guessing" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:531 msgid "" "Paperwork does use artificial intelligence. It uses a fairly simple method " "actually: \\href{https://en.wikipedia.org/wiki/Naive_Bayes_classifier}{Naive " "Bayes classifiers}. It's the same technology used by email clients to " "classify mails as spam/non-spam." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:537 msgid "" "Based on all the keywords in all your documents that have (or haven't) a " "label, it can estimate a probability that a document containing the same " "keywords should have or shouldn't have this same label. If the probability " "is high enough, it puts the label on the document automatically when you " "import it or scan it." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:541 msgid "" "Of course, this approach means that Paperwork needs enough samples to work " "reliably. You can expect it to start working once you have about 100 " "documents or more (and only for labels that are on more than 10 documents or " "more)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:544 msgid "Searching" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:546 msgid "Simple search" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:553 msgid "" "You simply enter keywords in the search field. In a few seconds, you will " "get all the documents containing those keywords." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:557 msgid "" "Paperwork does a \"fuzzy\" search: documents with keywords close to the one " "you gave but not identical are also returned (for instance, 'flech' instead " "of 'flesch')." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:562 msgid "" "You can also use \\href{https://whoosh.readthedocs.io/en/latest/querylang." "html}{Whoosh query language} to make more complex queries. If you want " "examples, you can use the advanced search dialog described below." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:565 msgid "Advanced search" msgstr "高级搜索" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:582 msgid "" "The advanced search dialog helps creating complex search queries. You can " "specify various criterias and once you click on the apply button, it will " "generate a search query for you and put it immediately in the search field. " "Search results will immediately be refreshed as well." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:585 msgid "Viewing" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:591 msgid "Zoom level" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:597 msgid "" "You can change the scale at which pages are displayed using this control." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:600 msgid "View pages as grid" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:608 msgid "" "When clicking this button, Paperwork will try to display pages on 3 " "columns. In this mode, you can drag'n'drop pages to move them inside the " "document or to another document." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:611 msgid "View pages as list" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:619 msgid "" "When clicking this button, pages will be scaled so their width is the " "maximum width allowed by the main window. In this mode, you can select text " "in the page (and then copy it)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:621 msgid "Highlight all words" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:629 msgid "" "This option allows to see quickly all the words identified by OCR. Sometimes " "(rarely) OCR misses entire chunk in a page. This option allow to see such " "chunk quickly." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:631 msgid "Moving pages" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:633 msgid "Inside a document" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:635 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:644 msgid "You must display the document pages as a grid (See \\ref{layout:grid})." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:640 msgid "" "You can then grab a page (hold the left click button), drag it and drop it " "wherever you want in a document. While dragging, a blue marker will show you " "where the page would drop if you release the left click button of your mouse." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:642 msgid "From a document to another" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:647 msgid "" "You can then grab a page (hold the left click button), drag it and drop it " "in the document list, on the document in which you want the page to go." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:649 msgid "Copying text" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:655 msgid "" "You must display the document pages as a list (See \\ref{layout:paged})." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:658 msgid "" "You can then select text in a page. Hold the left click button to start " "selecting, mouse the mouse cursor to select more words, then release it." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:662 msgid "" "You can then copy the selected text, either by pressing Ctrl-C or by using " "the page menu at the bottom right of the main window. Once copied, you can " "paste the selected text in any other application (Ctrl-V)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:664 msgid "Editing a page" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:670 msgid "Paperwork includes a very simple image editor. It provides 4 functions:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Cropping" msgstr "裁剪" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Rotating the page by 90\\degree (can be rotated multiple times)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "Rotating the page by -90\\degree (can be rotated multiple times)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:679 msgid "" "Automatic Color Equalization: An algorithm that adjust the image brightness, " "contrast and colors to make it as readable as possible." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:681 msgid "Reseting a page" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:684 msgid "" "Reseting a page returns it to its state when it was scanned or imported, " "before any pre-processing did occur." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:688 msgid "" "This can be helpful if you made a bad modification on the page (cropped a " "wrong area for instance), if the calibration settings weren't appropriate or " "if pre-processing algorithms messed up the page." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:691 msgid "Deleting" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:694 msgid "" "When deleting either documents or pages, they are actually moved in the " "trash bin of your computer." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:698 msgid "" "\\textbf{Important note regarding Flatpak:} A bug may prevent Paperwork from " "moving files to the trash (we are working on it). In that case, Paperwork " "will delete the file directly (no recovery possible)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:701 msgid "Exporting" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:703 msgid "You can export both documents or single pages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:707 msgid "" "In both cases, various transformations can be applied before actually " "exporting them. For instance, you can turn color pages into grayscale pages " "before putting them in a brand new PDF (making the resulting PDF smaller)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:710 msgid "Printing" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:712 msgid "You can print both documents or single pages." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:715 msgid "" "Beware that pages are always sent as images to your printer. So for very big " "documents, a few minutes may go by before the actual printing start." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:718 msgid "Backup" msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:720 msgid "Synchronisation between multiple computers" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:726 msgid "" "While Paperwork is a personal document manager, it is not a file " "synchronization application. They are applications dedicated to file " "synchronization that already do that very well. Therefore Paperwork is " "designed to be used with such applications (Nextcloud, Dropbox, OneDrive, " "SparkleShare, etc)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:733 msgid "" "When you start Paperwork, one of the first things it does is check the " "content of the work directory. It looks for any changes and updates its " "document list and index accordingly, automatically. So if another instance " "of Paperwork on another computer modified something in the work directory " "and if this change has been synchronized on another computer, the other " "Paperwork will automatically pick up this change when starting." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:736 msgid "USB key / USB drive" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:739 msgid "" "This is the simplest way to share documents. Simply copy your work directory " "to an USB key, tell Paperwork to use it, and you're done." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:741 msgid "" "Beware: You should backup your USB key from time to time on another one." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:744 msgid "File Synchronization applications" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:750 msgid "" "Those applications synchronize a local directory with a remote server (or " "cloud). All the changes you do in your folder are applied on the server. All " "the changes applied on the servers are applied to the computers that connect " "to it. The server can belong to you or to someone else (usually a company)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:754 msgid "" "Beware: If you choose to host your documents on someone else server " "(DropBox, OneDrive, etc), they can access all your documents. Paperwork does " "not encrypt them." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:758 msgid "" "Paperwork is tested daily with Nextcloud. While this is not the easiest one " "to install, Nextcloud let you host your files yourself. There are other self-" "hosted alternatives that exist: SparkleShare, Syncthing, etc." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:761 msgid "" "Using DropBox or OneDrive can make sense if you're sharing not-so-" "confidential documents with others (associations, etc)." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:764 msgid "Shared folder" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:769 msgid "" "If all your computers are on the same network, you can share your work " "directory. However, be really careful regarding permissions. Being too " "permissive could let a pirate access all your personal documents ! And " "setting them correctly is tricky." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:772 msgid "" "Beware: Using a shared folder means having a single copy of your work " "directory. You should do regular backups of your work directory." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:775 msgid "Encryption" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:777 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1024 msgid "GNU/Linux" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:779 msgid "" "GNU/Linux distributions include many tools to encrypt whole directories." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "With Paperwork, there are 2 directories that should be encrypted to protect " "your privacy:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "Your work directory (by default \\textasciitilde /papers, can be changed in " "the settings)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:789 msgid "" "The cache directory (\\textasciitilde /.local/share/paperwork2, cannot be " "changed) (it contains index files from which the content of your documents " "could be partially recovered)" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:793 msgid "" "Note that if you want to be sure that your data are always encrypted, it's " "recommended to encrypt your whole home directory or even your whole system " "if possible." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:795 msgid "cryptsetup" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:799 msgid "" "Most GNU/Linux distribution installer now provide an optio4n to encrypt your " "whole system or your whole /home with cryptsetup . This is the recommended " "method to protect your documents." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:801 msgid "Encfs" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:803 msgid "Encfs can also be used to create encrypted directories easily." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:807 msgid "" "Beware that Encfs seems to have some security weaknesses. So, while it's " "probably enough to prevent a laptop thief from accessing your documents, " "it's likely to be not enough to prevent the NSA or the police from doing " "so ;-)." msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:811 #, no-wrap msgid "" "$ encfs ~/.local/share/.paperwork2 ~/.local/share/paperwork2\n" "$ encfs ~/.papers ~/papers" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:818 msgid "" "On Windows, you're strongly advised to enable BitLocker to protect your " "documents. If unavailable, there are other applications (Veracrypt, etc)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:821 msgid "Keyboard shortcuts" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:832 msgid "" "Keyboard shortcuts can be seen by opening the application menu, selecting " "\"Help\" and then \"Shortcuts\"." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:835 msgid "Paperwork's files locations" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "By default:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "Configuration: \\textasciitilde /.config/paperwork2.conf" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "Index: \\textasciitilde /.local/share/paperwork2" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "Documents: \\textasciitilde /papers" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:844 msgid "" "(same paths are used on Windows; \\textasciitilde{} = C:\\textbackslash " "Users{[}login{]} ; folders are hidden)" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:848 msgid "" "The index is always updated according based on the documents in the work " "directory. When Paperwork starts, the modification time of each file is used " "to detect changes on the documents." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:850 msgid "Work directory layout" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:852 msgid "workdir$|$rootdir = \\textasciitilde /papers (by default)" msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:854 msgid "Global organisation" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:856 msgid "In the work directory, you have folders, one per document." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:860 msgid "" "The folder names are (usually) the scan/import date of the document: YYYYMMDD" "\\_hhmm\\_ss{[}\\_{]}. The suffix 'idx' is optional and is just a " "number added in case of name collision." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "In every folder you have:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "For image documents:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "paper.$<$X$>$.jpg: The original page in JPG format (X starts at 1)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "paper.$<$X$>$.edited.jpg (optional): The page as edited by the user (X " "starts at 1)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "" "paper.$<$X$>$.words (optional): A hOCR file, containing all the words found " "on the page using the OCR (optional, but required for indexing ; can be " "regenerated with the options \"Redo OCR\")." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 msgid "" "paper.1.thumb.jpg (optional, generated automatically): A thumbnail version " "of the page (faster to load)" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "labels (optional): a text file containing the labels applied on this document" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:875 #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "extra.txt (optional): extra keywords added by the user" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "For PDF documents:" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "doc.pdf: the document" msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "paper.$<$X$>$.words (optional): A hOCR file, containing all the words found " "on the page using the OCR. Some PDF contains crap instead of the real text, " "so running the OCR on them can sometimes be useful." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "passwd.txt (optional): PDF password, if the PDF is password-protected." msgstr "" #. type: itemize #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:892 msgid "" "doc.docx / doc.odt / ... (optional): Original file. Converted into PDF (doc." "pdf) so Paperwork can parse and display it more quickly." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:894 msgid "Here is an example a work directory organisation:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:926 #, no-wrap msgid "" "$ find ~/papers\n" "/home/jflesch/papers\n" "/home/jflesch/papers/20130505_1518_00\n" "/home/jflesch/papers/20130505_1518_00/paper.1.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.1.thumb.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.1.words\n" "/home/jflesch/papers/20130505_1518_00/paper.2.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.2.edited.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.2.words\n" "/home/jflesch/papers/20130505_1518_00/paper.3.jpg\n" "/home/jflesch/papers/20130505_1518_00/paper.3.words\n" "/home/jflesch/papers/20130505_1518_00/labels\n" "/home/jflesch/papers/20110726_0000_01f\n" "/home/jflesch/papers/20110726_0000_01/paper.1.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.1.thumb.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.1.words\n" "/home/jflesch/papers/20110726_0000_01/paper.2.jpg\n" "/home/jflesch/papers/20110726_0000_01/paper.2.words\n" "/home/jflesch/papers/20110726_0000_01/extra.txt\n" "/home/jflesch/papers/20130106_1309_44\n" "/home/jflesch/papers/20130106_1309_44/doc.pdf\n" "/home/jflesch/papers/20130106_1309_44/paper.1.thumb.jpg\n" "/home/jflesch/papers/20130106_1309_44/paper.2.edited.jpg\n" "/home/jflesch/papers/20130106_1309_44/paper.2.words\n" "/home/jflesch/papers/20130106_1309_44/labels\n" "/home/jflesch/papers/20130106_1309_44/extra.txt\n" "/home/jflesch/papers/20130106_1309_44/passwd.txt\n" "/home/jflesch/papers/20130520_1309_44\n" "/home/jflesch/papers/20130520_1309_44/doc.pdf\n" "/home/jflesch/papers/20130520_1309_44/doc.docx\n" "/home/jflesch/papers/20130520_1309_44/labels" msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:929 msgid "hOCR files" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:931 msgid "With Tesseract, the hOCR file can be obtained with following command:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:933 #, no-wrap msgid "tesseract paper..jpg paper. -l hocr && mv paper..html paper..words" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:935 msgid "For example:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:937 #, no-wrap msgid "tesseract paper.1.jpg paper.1 -l fra hocr && mv paper.1.html paper.1.words" msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:940 msgid "Label files" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:942 msgid "Here is an example of content of a label file:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:944 #, no-wrap msgid "facture,#0000b1588c61 logement,#f6b6ffff0000" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:947 msgid "" "It's always $[$label$]$,$[$color$]$. For a same label, the color should " "always be the same." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:950 msgid "Getting support" msgstr "获得支持" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:953 msgid "" "A forum dedicated to Paperwork exists: \\href{https://forum.openpaper.work}" "{https://forum.openpaper.work}." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:956 msgid "" "There is also an IRC channel for live discussions: \\href{https://webchat." "freenode.net/}{Freenode}, channel \\#openpaperwork" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:959 msgid "" "If you have questions regarding Paperwork or simply want to chat, those are " "the places to go." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:961 msgid "Reporting issues" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:964 msgid "" "If you noticed a bug in Paperwork (and you are sure it's a bug), you can " "make a bug report." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:966 msgid "Bug Tracker" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:969 msgid "" "One way to create bug reports is to create tickets on \\href{https://gitlab." "gnome.org/World/OpenPaperwork/paperwork/issues}{Paperwork bug tracker: " "https://gitlab.gnome.org/World/OpenPaperwork/paperwork/issues}." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:972 msgid "" "This is the recommended way to submit a bug report if you would like to " "discuss it with Paperwork developpers." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:975 msgid "" "To make sure you include all the required informations, you can use the tool " "integrated in Paperwork (see below)." msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:978 msgid "Automatic bug report" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:985 msgid "" "Paperwork includes a tool to make reporting bugs easier. It allows you to " "get easily all the required information to make a perfect bug report." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:989 msgid "" "All attachments are automatically censored to protect your privacy: Document " "contents are blurred in screenshots and logs are censored to remove your " "user name." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:992 msgid "" "If the bug you want to report is related to scanners, please include " "\"Scanner info.\" in the bug report files." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:995 msgid "" "If te bug you want to report is related to a display problem, please include " "\"App. screenshots\" in the bug report files." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:998 msgid "ZIP file" msgstr "ZIP 文件" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1002 msgid "" "You can then obtain a ZIP file with all the data. Please make sure the " "content of the ZIP file does not contain private information (it shouldn't, " "but better safe than sorry). Then you can add this ZIP file to a ticket on " "Gitlab." msgstr "" #. type: subsubsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1005 msgid "Automatic submission" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1009 msgid "" "You can also let the tool submit the bug report to openpaper.work " "automatically. In that case, you won't be able to discuss the bug with " "developers (or you have to leave a way to contact you in the bug report)." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1016 msgid "" "If you use the automatic submission , the tool will give you an URL to see " "the submitted bug report. This URL is private and shouldn't be shared until " "you made sure there is no private information in the bug report. If there is " "private information, you can request deletion of the bug report by sending " "an email to jflesch@openpaper.work (please specify the private URI in your " "mail so we can be sure that you are the one who submitted the bug report)." msgstr "" #. type: section{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1019 msgid "Uninstalling" msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1022 msgid "" "Paperwork can be uninstalled. Uninstalling Paperwork \\emph{will never} " "remove your work directory or your documents." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1028 msgid "" "If you installed Paperwork using the package manager from your distribution " "(the recommended way), the uninstallation method depends on the package " "manager." msgstr "" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1031 msgid "" "For instance, on GNU/Linux Debian or GNU/Linux Ubuntu, the following command " "will take care of it:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1034 #, no-wrap msgid "sudo apt remove --purge paperwork-\\*" msgstr "sudo apt remove --purge paperwork-\\*" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1037 msgid "If you installed it using Flatpak, you can use the following command:" msgstr "" #. type: verbatim #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1040 #, no-wrap msgid "flatpak --user uninstall work.openpaper.Paperwork" msgstr "" #. type: subsection{#2} #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1043 msgid "Windows 10" msgstr "Windows 10" #. type: document #: /home/jflesch/git/paperwork/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex:1047 msgid "" "Paperwork can be uninstalled as any Windows applications, by going in " "Windows Control Panel, clicking on \"Applications\", finding Paperwork in " "the list, and then clicking on \"uninstall\"." msgstr "" paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/data/paperwork_going_up.svg000066400000000000000000000300621417573700700313670ustar00rootroot00000000000000 image/svg+xml paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/data/usage.tex000066400000000000000000001014401417573700700265720ustar00rootroot00000000000000\documentclass[10pt,a4paper]{article} \usepackage[utf8]{inputenc} \usepackage{amsfonts} \usepackage{amssymb} \usepackage{float} \usepackage{gensymb} \usepackage{graphicx} \usepackage{hyperref} \usepackage{wrapfig} \hypersetup{ colorlinks, citecolor=black, filecolor=black, linkcolor=black, urlcolor=black, linktoc=all, } \date{} \title{Paperwork manual} \begin{document} \maketitle \pagebreak \tableofcontents \pagebreak \section{Introduction} \begin{figure}[H] \includegraphics{data/user_manual_intro.png} \end{figure} Most personal documents are fairly recurrent: earning statements, rent bills, electricity bills, etc. For most unorganized people, having to find them back later is worrisome, at best. For most organized people, naming and sorting them is as tedious as watching paint dry. The main idea behind Paperwork is that managing documents is a computer job. Humans should do as little as possible while machines do most of the work. The end goal here is "scan \& forget". If you're looking for a software that will let you name each document individually, organize them in complex hierachy, tag them manually each time, fix OCR minor glitches, etc, then Paperwork is not for you. \section{Definitions} \subsection{Work directory} Paperwork stores all your documents in a single directory: the work directory. In this directory, each document has its own sub-directory. While this makes Paperwork hard to use with other tools, it has one major advantage: You don't have to worry about file names and directory structures anymore. \subsection{Document} \begin{figure}[H] \includegraphics[scale=0.33]{out/main_window_split.png} \end{figure} In Paperwork, a document is a set of pages. On disk, it can either be a set of JPEG files or a PDF file. Documents are identified only by a date. It can either be the date you imported them (default) or some date of your choosing. They are displayed on the left side of the main window (green part on the screenshot above). \subsection{Page} In Paperwork, a page is just an image and the word positions on this image. Images can come from a scanner or be imported. In those cases, it is stored as a JPEG files and text is extracted using OCR (Optical Character Recognition). OCR is a fairly long process. It can take up to a few minutes for each page. So the text extracted from images is stored in hOCR files beside the JPEG files. Pages can also be the pages from a PDF file. In that case, by default, Paperwork just stores a copy of the PDF file. Paperwork does not track whether a page is recto or verso. Paperwork does not track the paper size corresponding to a page (A4, Letter, etc). Pages are displayed on the right side of the main window (blue part on the screenshot above). \subsection{Indexation and Keywords} \begin{figure}[H] \includegraphics[scale=0.33]{out/search.png} \end{figure} Of course, you need a way to find back your documents. Paperwork manages an index with all the keywords found in your documents. Just type in a few keywords, and you will get your documents back. \subsection{Labels and additional keywords} \begin{figure}[H] \centering \begin{minipage}{.5\textwidth} \centering \includegraphics[scale=0.33]{out/doc_labels.png} \end{minipage}% \begin{minipage}{.5\textwidth} \centering \includegraphics[scale=0.33]{out/doc_extra_text.png} \end{minipage} \end{figure} Unfortunately, sometimes, documents don't contain the keywords needed to find them back. Also OCR is not a perfectly realiable process and may not work. To mitigate those issues, you can add labels (or tags) on your documents and provide additional keywords. Both are added to the index. Labels are displayed beside documents. Additional keywords are almost never displayed. \section{Settings} \subsection{Accessing the settings} \begin{figure}[H] \centering \begin{minipage}{.5\textwidth} \centering \includegraphics[scale=0.33]{out/app_menu.png} \end{minipage}% \begin{minipage}{.5\textwidth} \centering \includegraphics[scale=0.5]{out/app_menu_opened.png} \end{minipage} \end{figure} \begin{figure}[H] \includegraphics[scale=0.33]{out/settings.png} \end{figure} \subsection{Work directory} \begin{figure}[H] \includegraphics[scale=0.33]{out/settings_storage.png} \end{figure} The work directory is the directory where you want all your documents stored. It can be a standard folder, a folder synchronized across multiple computers or on a network share. Once you close the settings dialog, the work directory will be scanned and Paperwork index will be updated according to its index. Each time Paperwork starts, it will look for changes in this folder and synchronize its index accordingly. \subsection{Scanner} \begin{figure}[H] \includegraphics[scale=0.33]{out/settings_scanner.png} \end{figure} \subsubsection{Device} \begin{figure}[H] \includegraphics[scale=0.33]{out/settings_scanner_device.png} \end{figure} When starting, Paperwork looks for scanners. The scanner to use can be selected in the settings. Webcams, file storage, etc, cannot be used. Only paper-eaters. \subsubsection{Scan Mode} \begin{figure}[H] \includegraphics[scale=0.33]{out/settings_scanner_mode.png} \end{figure} Most modern scanners scan in color in a reasonable time. However some older scanners scan much faster in grayscale or even in black\&white. Here you can select the mode to use. \subsubsection{Scan Resolution} \begin{figure}[H] \includegraphics[scale=0.33]{out/settings_scanner_resolution.png} \end{figure} Scanner resolution defines how detailed the images coming from your scanner must be. Higher resolutions mean \begin{itemize} \item longer scans, \item longer OCR, \item more time to display, \item more space used on disk, \item but also better OCR. \end{itemize} Lower resolutions mean \begin{itemize} \item shorter scans, \item shorter OCR, \item less time to display, \item less space used on disk, \item but also inferior OCR, \item and possibly unreadable image (even by a human). \end{itemize} 300 dpi is considered a good trade-off. You may want to reduce it to 200 dpi on slow computers. \subsubsection{Scanner calibration} \begin{figure}[H] \includegraphics[scale=0.33]{out/settings_calibration_dialog.png} \end{figure} Scanners tend to provide images actually bigger than the scanned pages. Since most of the time, you will always scan pages having the same size (A4 or Letter usually), Paperwork provides an option called scanner calibration. Scanner calibration in Paperwork is simply an area that will always be cropped out of images coming from the scanner. \subsection{OCR} \begin{figure}[H] \includegraphics[scale=0.33]{out/settings_optical_character_recognition.png} \end{figure} By default, Paperwork uses Tesseract for the OCR. If unavailable, it falls back on Cuneiform. On Linux, if installed with Flatpak, Paperwork is always provided with Tesseract. On Windows, Paperwork is always provided with Tesseract. To get better results, OCR tool need to know the language used in the document(s). The language available in the settings dialog of Paperwork are those understood by the OCR tool. If your language is not in the list, it means the OCR tool doesn't have the data required to read your language and you must install them. \subsubsection{Adding languages} \paragraph{Flatpak} \begin{verbatim} # is a list of 2-letters language codes separated ';' # ex: en;fr;de flatpak config --user --set languages "" flatpak update --user \end{verbatim} \paragraph{Debian} \begin{verbatim} # is a 3-letter language code # ex: 'fra' for French $ sudo apt-get install tesseract-ocr tesseract-ocr- \end{verbatim} \paragraph{Fedora} \begin{verbatim} # is a 3-letter language code # ex: 'fra' for French $ sudo dnf install tesseract tesseract-langpack- \end{verbatim} \paragraph{Ubuntu} \begin{verbatim} # is a 3-letter language code # ex: 'fra' for French $ sudo apt-get install tesseract-ocr tesseract-ocr- \end{verbatim} \paragraph{Windows} Tesseract and all its data files are provided by Paperwork's installer. You can rerun the installer to install other languages. If a language is not available in the installer, it either means it hasn't been packaged (in which case you can request it), or there is no data file available yet for this language. \subsubsection{Disabling OCR} When you scan a page using Paperwork, Paperwork will immediately run the OCR on it. This process may take a while for each page. In case you want to scan a lot of pages quickly (for instance, the first time you use Paperwork), OCR can be temporarily disabled. To disable OCR, you simply have to unselect all OCR languages. \subsection{Updates} \begin{figure}[H] \includegraphics[scale=0.33]{out/settings_updates.png} \end{figure} If you enable this option, when Paperwork starts, Paperwork will look for updates if it hasn't done so for a week or more. To know if a new version is available, it has to send an HTTPS query to 'openpaper.work'. If an update is found, it will notify you but it won't install it. \section{New document} By default, in the document list, Paperwork includes a document called "New document". If you open it, it always appears empty. This document actually doesn't exist yet on disk, but will exist as soon as you put a page in it. You can add pages in it by scanning, importing file(s) or dropping a page from another in it. As soon as you put any content in it, this document will get its own date (the current one by default). In the document list, "New document" will be replaced by this date, and a new "New document" will be added to the document list. \begin{figure}[H] \includegraphics[scale=0.33]{out/doc_new_button.png} \end{figure} If you are currently searching something (see the chapter "Searching"), only search results are displayed and therefore this "New document" isn't displayed. You can get it back by clicking the button "+" in the top left corner of the main window. \section{Scanning} \begin{figure}[H] \includegraphics[scale=0.33]{out/page_add.png} \end{figure} If a scanner has been selected in the settings, you can use it to scan pages. In the header bar, there is a button to add pages. The small arrow on the right gives access to possible page sources. Those page sources include your scanner sources (Flatbed, Feeder). Once you've selected the scanner source you want to use, you can click on the button "Scan from ...". This will start a scan session: \begin{itemize} \item Scanned pages are appended at the end of the current document. If you use a feeder, Paperwork will scan pages until the feeder is empty. \item Paperwork will then crop them according to scanner calibration. \item Paperwork will run OCR on them \item Paperwork will index them \end{itemize} If this scan session creates a new document, Paperwork will try to set labels automatically on the document. \section{Importing} \begin{figure}[H] \includegraphics[scale=0.33]{out/page_add.png} \end{figure} \subsection{Images} Paperwork supports a lot of file formats. It supports JPEG, PNG, GIF, BMP, TIFF, etc. Each image file is considered as a page. Images are always appended to the document currently opened. Simply select an empty document ("New document") to create a new document while importing. OCR is always run on imported images. If the imported image is the first page of a new document, Paperwork will automatically apply documents labels. Note that Paperwork is a document manager. While it can, it is not designed to handle images with only very little text or photos. Automatic labeling will not work correctly on such documents. The OCR (Tesseract) works very well with black text on white background. Automatic labeling uses recognized text and requires as many keywords on the first page as possible. \subsection{PDF} Each PDF is always considered as a whole document. They are never appended to existing document. They are copied and renamed in the work directory, but their content is not modified. Paperwork always keeps the original PDF file as is, even if you edit some of its pages: the edited pages are stored beside the PDF file. Paperwork will look for pages with no text attached. On those pages, it will automatically run OCR. Once all the pages have been examined, it will automatically apply document labels. Note that this process may take a few minutes for big PDFs files. If the PDF is already part of your documents, Paperwork will simply ignore it. \subsection{Many PDFs in one shot} When importing, if you select a folder, Paperwork will browse this folder and look for PDFs to import. Already-imported PDFs are simply ignored. Folder is browsed recursively (all the folders inside the folder are also examined). \section{Labels} \begin{figure}[H] \centering \begin{minipage}{.5\textwidth} \centering \includegraphics[scale=0.33]{out/doc_properties_button.png} \end{minipage}% \begin{minipage}{.5\textwidth} \centering \includegraphics[scale=0.33]{out/doc_labels.png} \end{minipage} \end{figure} There is currently one constraint in Paperwork: Each label must be on at least one document. Otherwise, when you will restart Paperwork, labels without documents will disappear. \subsection{Creating new labels} \begin{figure}[H] \includegraphics[scale=0.33]{out/doc_new_label.png} \end{figure} You can click on the gray rectangle on the left side to pick the label color. You can enter the label name in text field between the gray rectangle and the button "+". Once you click on the button "+", the label will be added to the current document. The label is actually added once you close document properties. Paperwork will then update its index accordingly. \subsection{Setting labels on documents} When you open document properties, the label list appears. On the left side of each label color, you have a button. This button allows you to add or remove labels on the current document. The changes are actually written on disk once you close the document properties. Paperwork will update its index accordinly. \subsection{Modifying a label color} When you open document properties, you can click on a label color to change it. A dialog will let you pick the new color. Label color will actually be changed on disk when you close the document properties. Paperwork will then update the label on all the documents that use it. \subsection{Modifying a label name} When you open document properties, you can click on a label string to change it. A dialog will let you type in the new name. Label name will actually be changed on disk when you close the document properties. Paperwork will then update the label on all the documents that use it and then reindex them all. \subsection{Deleting a label} To the right of each label is white-on-black cross button. Clicking on it will allow you to delete a label. Once you will close the document properties, the label will be removed from all the documents having it. Paperwork will then update its index accordingly. Beware: Once you have closed document properties, there is no way to put back the deleted label. \subsection{Automatic label guessing} Paperwork does use artificial intelligence. It uses a fairly simple method actually: \href{https://en.wikipedia.org/wiki/Naive_Bayes_classifier}{Naive Bayes classifiers}. It's the same technology used by email clients to classify mails as spam/non-spam. Based on all the keywords in all your documents that have (or haven't) a label, it can estimate a probability that a document containing the same keywords should have or shouldn't have this same label. If the probability is high enough, it puts the label on the document automatically when you import it or scan it. Of course, this approach means that Paperwork needs enough samples to work reliably. You can expect it to start working once you have about 100 documents or more (and only for labels that are on more than 10 documents or more). \section{Searching} \subsection{Simple search} \begin{figure}[H] \includegraphics[scale=0.33]{out/search.png} \end{figure} You simply enter keywords in the search field. In a few seconds, you will get all the documents containing those keywords. Paperwork does a "fuzzy" search: documents with keywords close to the one you gave but not identical are also returned (for instance, 'flech' instead of 'flesch'). You can also use \href{https://whoosh.readthedocs.io/en/latest/querylang.html}{Whoosh query language} to make more complex queries. If you want examples, you can use the advanced search dialog described below. \subsection{Advanced search} \begin{figure}[H] \centering \begin{minipage}{.5\textwidth} \centering \includegraphics[scale=0.33]{out/advanced_search_button.png} \end{minipage}% \begin{minipage}{.5\textwidth} \centering \includegraphics[scale=0.33]{out/advanced_search.png} \end{minipage} \end{figure} The advanced search dialog helps creating complex search queries. You can specify various criterias and once you click on the apply button, it will generate a search query for you and put it immediately in the search field. Search results will immediately be refreshed as well. \section{Viewing} \begin{figure}[H] \includegraphics[scale=0.33]{out/docview_layout.png} \end{figure} \subsection{Zoom level} \begin{figure}[H] \includegraphics[scale=0.33]{out/docview_layout_scale.png} \end{figure} You can change the scale at which pages are displayed using this control. \subsection{View pages as grid} \label{layout:grid} \begin{figure}[H] \includegraphics[scale=0.33]{out/docview_layout_grid.png} \end{figure} When clicking this button, Paperwork will try to display pages on 3 columns. In this mode, you can drag'n'drop pages to move them inside the document or to another document. \subsection{View pages as list} \label{layout:paged} \begin{figure}[H] \includegraphics[scale=0.33]{out/docview_layout_paged.png} \end{figure} When clicking this button, pages will be scaled so their width is the maximum width allowed by the main window. In this mode, you can select text in the page (and then copy it). \subsection{Highlight all words} \begin{figure}[H] \includegraphics[scale=0.33]{out/docview_layout_show_all_boxes.png} \end{figure} This option allows to see quickly all the words identified by OCR. Sometimes (rarely) OCR misses entire chunk in a page. This option allow to see such chunk quickly. \section{Moving pages} \subsection{Inside a document} You must display the document pages as a grid (See \ref{layout:grid}). You can then grab a page (hold the left click button), drag it and drop it wherever you want in a document. While dragging, a blue marker will show you where the page would drop if you release the left click button of your mouse. \subsection{From a document to another} You must display the document pages as a grid (See \ref{layout:grid}). You can then grab a page (hold the left click button), drag it and drop it in the document list, on the document in which you want the page to go. \section{Copying text} \begin{figure}[H] \includegraphics[scale=0.33]{out/page_menu_opened.png} \end{figure} You must display the document pages as a list (See \ref{layout:paged}). You can then select text in a page. Hold the left click button to start selecting, mouse the mouse cursor to select more words, then release it. You can then copy the selected text, either by pressing Ctrl-C or by using the page menu at the bottom right of the main window. Once copied, you can paste the selected text in any other application (Ctrl-V). \section{Editing a page} \begin{figure}[H] \includegraphics[scale=0.33]{out/page_actions.png} \end{figure} Paperwork includes a very simple image editor. It provides 4 functions: \begin{itemize} \item Cropping \item Rotating the page by 90\degree (can be rotated multiple times) \item Rotating the page by -90\degree (can be rotated multiple times) \item Automatic Color Equalization: An algorithm that adjust the image brightness, contrast and colors to make it as readable as possible. \end{itemize} \section{Reseting a page} Reseting a page returns it to its state when it was scanned or imported, before any pre-processing did occur. This can be helpful if you made a bad modification on the page (cropped a wrong area for instance), if the calibration settings weren't appropriate or if pre-processing algorithms messed up the page. \section{Deleting} When deleting either documents or pages, they are actually moved in the trash bin of your computer. \textbf{Important note regarding Flatpak:} A bug may prevent Paperwork from moving files to the trash (we are working on it). In that case, Paperwork will delete the file directly (no recovery possible). \section{Exporting} You can export both documents or single pages. In both cases, various transformations can be applied before actually exporting them. For instance, you can turn color pages into grayscale pages before putting them in a brand new PDF (making the resulting PDF smaller). \section{Printing} You can print both documents or single pages. Beware that pages are always sent as images to your printer. So for very big documents, a few minutes may go by before the actual printing start. \section{Backup} \section{Synchronisation between multiple computers} While Paperwork is a personal document manager, it is not a file synchronization application. They are applications dedicated to file synchronization that already do that very well. Therefore Paperwork is designed to be used with such applications (Nextcloud, Dropbox, OneDrive, SparkleShare, etc). When you start Paperwork, one of the first things it does is check the content of the work directory. It looks for any changes and updates its document list and index accordingly, automatically. So if another instance of Paperwork on another computer modified something in the work directory and if this change has been synchronized on another computer, the other Paperwork will automatically pick up this change when starting. \subsection{USB key / USB drive} This is the simplest way to share documents. Simply copy your work directory to an USB key, tell Paperwork to use it, and you're done. Beware: You should backup your USB key from time to time on another one. \subsection{File Synchronization applications} Those applications synchronize a local directory with a remote server (or cloud). All the changes you do in your folder are applied on the server. All the changes applied on the servers are applied to the computers that connect to it. The server can belong to you or to someone else (usually a company). Beware: If you choose to host your documents on someone else server (DropBox, OneDrive, etc), they can access all your documents. Paperwork does not encrypt them. Paperwork is tested daily with Nextcloud. While this is not the easiest one to install, Nextcloud let you host your files yourself. There are other self-hosted alternatives that exist: SparkleShare, Syncthing, etc. Using DropBox or OneDrive can make sense if you're sharing not-so-confidential documents with others (associations, etc). \subsubsection{Shared folder} If all your computers are on the same network, you can share your work directory. However, be really careful regarding permissions. Being too permissive could let a pirate access all your personal documents ! And setting them correctly is tricky. Beware: Using a shared folder means having a single copy of your work directory. You should do regular backups of your work directory. \section{Encryption} \subsection{GNU/Linux} GNU/Linux distributions include many tools to encrypt whole directories. With Paperwork, there are 2 directories that should be encrypted to protect your privacy: \begin{itemize} \item Your work directory (by default \textasciitilde /papers, can be changed in the settings) \item The cache directory (\textasciitilde /.local/share/paperwork2, cannot be changed) (it contains index files from which the content of your documents could be partially recovered) \end{itemize} Note that if you want to be sure that your data are always encrypted, it's recommended to encrypt your whole home directory or even your whole system if possible. \subsubsection{cryptsetup} Most GNU/Linux distribution installer now provide an optio4n to encrypt your whole system or your whole /home with cryptsetup . This is the recommended method to protect your documents. \subsubsection{Encfs} Encfs can also be used to create encrypted directories easily. Beware that Encfs seems to have some security weaknesses. So, while it's probably enough to prevent a laptop thief from accessing your documents, it's likely to be not enough to prevent the NSA or the police from doing so ;-). \begin{verbatim} $ encfs ~/.local/share/.paperwork2 ~/.local/share/paperwork2 $ encfs ~/.papers ~/papers \end{verbatim} \subsection{Windows} On Windows, you're strongly advised to enable BitLocker to protect your documents. If unavailable, there are other applications (Veracrypt, etc). \section{Keyboard shortcuts} \begin{figure}[H] \includegraphics[scale=0.33]{out/app_menu_opened.png} \end{figure} \begin{figure}[H] \includegraphics[scale=0.33]{out/shortcuts.png} \end{figure} Keyboard shortcuts can be seen by opening the application menu, selecting "Help" and then "Shortcuts". \subsection{Paperwork's files locations} By default: \begin{itemize} \item Configuration: \textasciitilde /.config/paperwork2.conf \item Index: \textasciitilde /.local/share/paperwork2 \item Documents: \textasciitilde /papers \end{itemize} (same paths are used on Windows; \textasciitilde{} = C:\textbackslash Users{[}login{]} ; folders are hidden) The index is always updated according based on the documents in the work directory. When Paperwork starts, the modification time of each file is used to detect changes on the documents. \subsection{Work directory layout} workdir$|$rootdir = \textasciitilde /papers (by default) \subsubsection{Global organisation} In the work directory, you have folders, one per document. The folder names are (usually) the scan/import date of the document: YYYYMMDD\_hhmm\_ss{[}\_{]}. The suffix 'idx' is optional and is just a number added in case of name collision. In every folder you have: \begin{itemize} \item For image documents: \begin{itemize} \item paper.$<$X$>$.jpg: The original page in JPG format (X starts at 1) \item paper.$<$X$>$.edited.jpg (optional): The page as edited by the user (X starts at 1) \item paper.$<$X$>$.words (optional): A hOCR file, containing all the words found on the page using the OCR (optional, but required for indexing ; can be regenerated with the options "Redo OCR"). \item paper.1.thumb.jpg (optional, generated automatically): A thumbnail version of the page (faster to load) \item labels (optional): a text file containing the labels applied on this document \item extra.txt (optional): extra keywords added by the user \end{itemize} \item For PDF documents: \begin{itemize} \item doc.pdf: the document \item labels (optional): a text file containing the labels applied on this document \item paper.$<$X$>$.edited.jpg (optional): The page as edited by the user (X starts at 1) \item extra.txt (optional): extra keywords added by the user \item paper.$<$X$>$.words (optional): A hOCR file, containing all the words found on the page using the OCR. Some PDF contains crap instead of the real text, so running the OCR on them can sometimes be useful. \item passwd.txt (optional): PDF password, if the PDF is password-protected. \item doc.docx / doc.odt / ... (optional): Original file. Converted into PDF (doc.pdf) so Paperwork can parse and display it more quickly. \end{itemize} \end{itemize} Here is an example a work directory organisation: \begin{verbatim} $ find ~/papers /home/jflesch/papers /home/jflesch/papers/20130505_1518_00 /home/jflesch/papers/20130505_1518_00/paper.1.jpg /home/jflesch/papers/20130505_1518_00/paper.1.thumb.jpg /home/jflesch/papers/20130505_1518_00/paper.1.words /home/jflesch/papers/20130505_1518_00/paper.2.jpg /home/jflesch/papers/20130505_1518_00/paper.2.edited.jpg /home/jflesch/papers/20130505_1518_00/paper.2.words /home/jflesch/papers/20130505_1518_00/paper.3.jpg /home/jflesch/papers/20130505_1518_00/paper.3.words /home/jflesch/papers/20130505_1518_00/labels /home/jflesch/papers/20110726_0000_01f /home/jflesch/papers/20110726_0000_01/paper.1.jpg /home/jflesch/papers/20110726_0000_01/paper.1.thumb.jpg /home/jflesch/papers/20110726_0000_01/paper.1.words /home/jflesch/papers/20110726_0000_01/paper.2.jpg /home/jflesch/papers/20110726_0000_01/paper.2.words /home/jflesch/papers/20110726_0000_01/extra.txt /home/jflesch/papers/20130106_1309_44 /home/jflesch/papers/20130106_1309_44/doc.pdf /home/jflesch/papers/20130106_1309_44/paper.1.thumb.jpg /home/jflesch/papers/20130106_1309_44/paper.2.edited.jpg /home/jflesch/papers/20130106_1309_44/paper.2.words /home/jflesch/papers/20130106_1309_44/labels /home/jflesch/papers/20130106_1309_44/extra.txt /home/jflesch/papers/20130106_1309_44/passwd.txt /home/jflesch/papers/20130520_1309_44 /home/jflesch/papers/20130520_1309_44/doc.pdf /home/jflesch/papers/20130520_1309_44/doc.docx /home/jflesch/papers/20130520_1309_44/labels \end{verbatim} \subsubsection{hOCR files} With Tesseract, the hOCR file can be obtained with following command: \begin{verbatim} tesseract paper..jpg paper. -l hocr && mv paper..html paper..words \end{verbatim} For example: \begin{verbatim} tesseract paper.1.jpg paper.1 -l fra hocr && mv paper.1.html paper.1.words \end{verbatim} \subsubsection{Label files} Here is an example of content of a label file: \begin{verbatim} facture,#0000b1588c61 logement,#f6b6ffff0000 \end{verbatim} It's always $[$label$]$,$[$color$]$. For a same label, the color should always be the same. \section{Getting support} A forum dedicated to Paperwork exists: \href{https://forum.openpaper.work}{https://forum.openpaper.work}. There is also an IRC channel for live discussions: \href{https://webchat.freenode.net/}{Freenode}, channel \#openpaperwork If you have questions regarding Paperwork or simply want to chat, those are the places to go. \section{Reporting issues} If you noticed a bug in Paperwork (and you are sure it's a bug), you can make a bug report. \subsection{Bug Tracker} One way to create bug reports is to create tickets on \href{https://gitlab.gnome.org/World/OpenPaperwork/paperwork/issues}{Paperwork bug tracker: https://gitlab.gnome.org/World/OpenPaperwork/paperwork/issues}. This is the recommended way to submit a bug report if you would like to discuss it with Paperwork developpers. To make sure you include all the required informations, you can use the tool integrated in Paperwork (see below). \subsection{Automatic bug report} \begin{figure}[H] \includegraphics[scale=0.33]{out/bug_report.png} \end{figure} Paperwork includes a tool to make reporting bugs easier. It allows you to get easily all the required information to make a perfect bug report. All attachments are automatically censored to protect your privacy: Document contents are blurred in screenshots and logs are censored to remove your user name. If the bug you want to report is related to scanners, please include "Scanner info." in the bug report files. If te bug you want to report is related to a display problem, please include "App. screenshots" in the bug report files. \subsubsection{ZIP file} You can then obtain a ZIP file with all the data. Please make sure the content of the ZIP file does not contain private information (it shouldn't, but better safe than sorry). Then you can add this ZIP file to a ticket on Gitlab. \subsubsection{Automatic submission} You can also let the tool submit the bug report to openpaper.work automatically. In that case, you won't be able to discuss the bug with developers (or you have to leave a way to contact you in the bug report). If you use the automatic submission , the tool will give you an URL to see the submitted bug report. This URL is private and shouldn't be shared until you made sure there is no private information in the bug report. If there is private information, you can request deletion of the bug report by sending an email to jflesch@openpaper.work (please specify the private URI in your mail so we can be sure that you are the one who submitted the bug report). \section{Uninstalling} Paperwork can be uninstalled. Uninstalling Paperwork \emph{will never} remove your work directory or your documents. \subsection{GNU/Linux} If you installed Paperwork using the package manager from your distribution (the recommended way), the uninstallation method depends on the package manager. For instance, on GNU/Linux Debian or GNU/Linux Ubuntu, the following command will take care of it: \begin{verbatim} sudo apt remove --purge paperwork-\* \end{verbatim} If you installed it using Flatpak, you can use the following command: \begin{verbatim} flatpak --user uninstall work.openpaper.Paperwork \end{verbatim} \subsection{Windows 10} Paperwork can be uninstalled as any Windows applications, by going in Windows Control Panel, clicking on "Applications", finding Paperwork in the list, and then clicking on "uninstall". \end{document} paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/data/user_manual_intro.png000066400000000000000000011256751417573700700312210ustar00rootroot00000000000000PNG  IHDR'b&zTXtRaw profile type exifxڭi\7s$A-Hɲ,RȈ7@r_{[.֫-[{uOw~>ԿoL:s䇯y=|Fv+_΍؅I!y:K7wL79r ^;ǟo+izGRhV5z=_[(|1>=R}ݔ:79U~n"bhN "}C;pyWX\b'6ǸbzԢŕpcKKh,x9~k|+tμ>/?_tR7-z e(rwpִ}_{~&"X2wnp9,J/ΉݷZn,.\LHDאJ[c'>+)IB)qwMJs{cQRl:+BɡQRɮRK+X5\KUahVZmfms/;`6,ZŠUkκI|z1fiYfmis,gUV]mekӦwmt)v3.vͷz>juW~/ApR3"s M b{9*rHQHBQpvPĈ`>!e\+n"FB?Q1JzRjM}o8}>I}C}8us'JX'.e#SAzdŸjHe-mr/!#ZɥZUac5 L]&:q'-Ǚ>i4X m1R?vGF]l5rk`FJ_DgY`6Vĺy\섻ռsrM,U:/?wnEJ˽KoX-e1uݽ&BqfFF*إ `c4'KKur)a`vW:9S+sڵR',-w/sJ9L:&Anۖ]~g\ogKjSNڡqkxnmB`jqz.Y3::f.6P..&C20rJXC]\=ʾa[{RA9ҁ.]m7 v853 93٣L.Kx2P@@Xيk\W8}Ynk@XӺ>׵``YCzKZ'jJ5 L: Y4'JV>zK}.6IeN 3l$GmG=p_lprgr{멭Yctk"`ktaW m!E9(:qTAF%`wGƒФJu)-h\aA }_]W&(4%Nd |\H{5QU0;rl;R %7!# 9]ظۓ)MZbܭ>o"?:yUj0[F|t)=cUdէV1Rb0[RV smh)JܡC&tu+N}w6v *{1:NvҺ Jr']Kvx0/7_76Ȭ hS*e>CB³"2`z@N&n6Bѡ_aH-p|QVIt؁[l`e/I%Kȣ$ [*H rj.@Qm 8egI977B]*ew_PJ APGbha %1AWܻ~s7n/RPr pIﳒSh$F"Y<ՀRwOY /ZUdMUX `VHO% bZoء0rav5YȂ `PpQۤE[Hpr,"͑Q*gCC7!ߨ[Ú uԆ^q8CECR/]%3/nY c3o|2JU 拝%3bK8,i$gr(3N'((!(_ d${W!gv+U']D+W.rG >k@}>YfrF\b2k\CP`߆G G b\hfp#T OK%g".!zQ t|F kx8NB,y'V3n]R(Ҋ`Nu Lq, b^_ 5z 7B,7h Aph酻r$ :AYˣc*2jJPEN)E9 WxGtՠ.u,, } !|XK_ʰ+#_H kr,R ڃ *nۀVdu40pd;dP2p s YdP8&!IׇA!k:HamM]XS#ACd9{4I:PBXmoQ( (vnnpV({Ѿy^#90_K4Be]W"uDR\MdGǦAw,ضYT ׷jꟋ^ F܀qZ=r\KKhN\ zjJ6%H/ Q#TPUzN# =Ұ%Y[H 񼈲%w:4YQ.nUT|e}TT 4ڵ\W."uMPf"(''0 m  KxXxk+>)M&lWe:9LKcf OP99c:>4L,e+py Z}f]{6nn6RWƙPo4%exYXMA驨pSe Y6'Wo227kyn Ekm *TBtRSFYrPS WVIr:[A2yq癝$LK#5&HH]NJr3wŅM,WyETSޭ^hl/rœke[RauTCGWbVmgoT9^i@GpTv +A *J;MמFC[Ma[@HS\JnQdQS5ɺ_ չI`u x,5yuDj ,aZX|b${.tۆPMEG@PT=jqga=ٯXI~btz8;}F2]/XU K ".0ʸdcS}KTYҴǧY rٸv>Gu$c$ؘ W|b7d5? G/CfX5 B yKDRdW_>kjqMfx#ƪk^( f!E Wa{!Hlg,ջe xP9X1 *h|;%fNA~xd.I(#Ԑng07~0~<CٱxbvKRQʪ@L a܆; !5dQ ~mL5I*kI,^-zzLh {;u|6( BwHA0:  Ü;-n&V͹s5غZI}vN, >"HD#~ šPZs6AP&A$r d6yJS!wΤ a"S i @%vTNjnThzSj)IHNk[lG}#j󋺓n_ڍ =8 pq{s$ЀIfB򐻜G2hѺV]TFkzjWXۆygEs&\MPo(Y=dhjL I}UMa !;@NJiCO>ğk;e6dsr˔eI=Jkn85 *0l=?QH= E{H>?GVT^Nr&ڐWE  9 Bm?E9 K^xt懖 Tݣ>:/dȭq30r@'%Fu(ss,p:5`%NqMu}B8бkxǾZ7`GT*o5t Q6E7+%]_b*&9}[ VMy80!D""K|^ ^%[C"bq-"'Ō8? u@AGj{񳩪,BD̈ΧLCeH"lI! @\?ujjD]!2.\cTU[pb+iqmVY+DE/qgr*L64UmKS_?k[\.B]'DG6z(ϦqPQlG [/&Ih\:.fVxvvh]_(\EAJ`$jTmL)VƋB X[U{:lY8XޓzpRK 6 ?=iH{YSF`Khj<]34*qQ7e򰣶Su`v(Ց-KSA,ӄ6<X-d&);] $)4t0pQۅx̤,XW-'MvKdJuufP Ơ`,M"N`9<[7g@#g_bT-&g>چzTǡLuw Q5Bj4Xelm[VcYjUӾ>gS#ID> q>O3kԀln\diS;H̅xrgyWN:\Mcu0ECtHzV !gQYFٍRh:i)dl\+ @:M)t4L@qSy״CP/n[664WH ㅭx#eږ Ȕ%sϬmia#QX3|?="Yu:2ķ6 GUMC'5hE¢ekINv+R0q Qv!E1V!kiLVů|E񓚉Zd= S{QCi7|6 |>lg-sev7E 9RlڰE@N/⊀>t #7AbSs aQa'{ʛ2ڡ $ƀl5ku79&Fʏg9LzRD zT84@f xkHMfͨX[ցtٛpy$;$t8*_v&fjDTWL9!uTp>ShCY7xJOI<氅<`ܓjժMi.4cgV%ȉP7h3Jf:U[v: D α6y}fIs~8ND_AG$VYD; ,){u Jp{9MfZ2^tA5k04C.~Mpt|DfDe2lWù=h#;>H``G;ï w68CǍvv&P;\&ӎt{9^.h1B8\9z{d#x'(9u=Ecb C/HHuYWPd {zC T%Rq3=wh6&!!=G[Ez%H- Lh}kTk3aV(5oV:K4q}^&VOf i%Wj,o(Ah[j$12+4aFT|.e]R,eruH% JpJ257AiF%С>BwBH!O"Ɇ>sUKV0NЄ.AN,E-ɮC^艄#gр"gJ*J:ԥ]+5= !PzOSZKv*4$9Qg~a8AԄ-TfLJoi =2RkEsA]d5VYm:1b5z7$+\xw_rGXɺ˷`W=|T^X%M jMj|ڤ);C"K~S7wJiwX|Ņ畸ce<<94ՃYǣ6)Yno.ޝP<+&!T5h:\4 H]j$Wµd=Ƕ{tSl5g.lY¼ǽ\)/;.L ozmP˒Sjʼoi?~w? Cr5SJ@y4r3氃"Yƀ6Z5c|PF&"nKOh{jORvz8['JsDrc2eҖƀՐ3 ũnhja[nCO$ 8<Mt/=/)ի'6!t&|ѠIi{.Zm%ѕ{5c蛘mAn՘3I?/@Hnɿp.^y詛9Me]4ϪQIuru[7-)?W= f} EA'k)Cgns8ЈzTo'btEDl{TóJ528Gk%bx#Ia3~zISpWYwo }Pg]:=>ilfԠ99ހ6sSBa|Bŋh^aYx,?'2* *5)3Ck91 |fM-t[ZlFK7= Šσ2n_'Q}ËP/"k pzP0=,!9 g[BQڠSLl!PYՍCKrjQJZ;rUS6}xN?)BKsHK{U{/iǭ8EnFTOik^WAB7{riN"PQOWd-Z5W} уkN}q,t9,dwgf|3̙s_}_7c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1<3討61c1cz*`,pp3c1c18xX 4-81c1cJE%0&p'0XDwo&{1c1]jIO F`>C`w`- ^Cx(1c1*f׀x)0 eTth [1c1cVA%2ʊhDYO.(o?(c1c1`*ppX9H`(5hT ebc1c1 `8-UdT9KWPñG=.cQ928c1czl1l1wCf@eSPY `p3 ,@m@;$$@5ܐ˵ux&;_Cb1c1eC`1=F ꘱7RhCuȠZV$6> | *+Pb1cLp1Ɣ`$ ~``KPJ6^A7󀕝y%G" 9`\n^7{ $P߱ c1c1Ɣn>ƅ`~}(Kb 2lG]7fξQFp@fR-{gא˵teu|+h1cLWpY1tJ`(ixgG@_Ĉ9MGb)k(| s(Sb$*p&wuF>\ <ՍF0*xxuOZ![(28u֘ې˵V'g7rM̕ǁ뻱@a1cJ cYu`>M"& /V`!BwOt#3"ąj;dA[(5FV5rw15g!Ώbc1ƔS];#@oI.$H<R' e|At$J{# [rEV5>xec1S40 fPkmWz0e! Vr|+}g>4#70$c1S4Μ0 6 =C퀱1.nE󨔡2$9>gMa@)XaQ؝| x*k1c1(9a akT7'Q_:TV% ӁS?!z獛g3o N#P{gQF3x s̯Eb ܜ.ƩO淺} 5}c1Ƙb3P"ψ:0qp?0jXvD>Ij22'B^4r>cP.#w7r%HCe0ǣc1c:1)m<eG哤u<>):cѐ54h|ezԡH)k`42\:}UϿ8eQ!>|_c18s38]B/sPF>I*HA. smkQ'+Pz(.~t0 xe,lF\ 8c1i9a(TDAmkwAd|T!͏Ǣ&+ԖtI7E'Py-rmIq8YڐDۿ'ˀS$1cLW3#GDugE (ar>xC.Ul梬E([be2#:d2ԚĘc1F,Nc P2 (sԩdP䓤xx  j;җ1cY.0 v@搷N  (x 8.,.Q1W아>ئc1-,Nc|}%Q p, ͨ|\PTh4*oF c11wF 3Q[EG'!bp=ţ qPrrZTDƘGcc1ƬgNcS"~k5?mEE p52'I'p(^ִ}F+/dy&*8u1c1]X0 6@`WN,UDž@-*HP]1'ƣuqFG2}|VNa9 eGVĘׁ+xS'-HY :^j1ccL_Q eEz`~w06"p&CtHw,U_EseUGSӃQ1L=#ƠQ<\\_/Ů!Iq~^n1c,Nc:C%S|U&H1_e~652AeFh/ TPbhC;_7ce)"_ ,FGCx(FT#Qv6j~e,oȵ4uJ$Q c1&:1Y1b$* :f`!8H#@>Rq!NH33BvAs-򌀐+!,[ 2"~we 'K*Fg:$ J5v&8.^ !Z0. \ <c1ƀ3'Ԡr"5]Aۨ,`sTmlfB* 1DT0Eƕߋg2D* |ݖ9O:OB'|r`NDU ["5qO$z5h) BoCc1 c(aZ0"yM;BÊ+Ǎ2eQFN8mmUUU^rԩܓ>UqiG,$f`d315!+c%XX/SQܕ̥$Y u8L#nna)U$^}ʠx5fUsv]o|c8aϤ#0$#~ e7ǚ ᱠΊwG邼Dyu1g9n5ǁs,PFGS|"8دϲeGe7||ޏ:20`#H L&c1'1}*TڕX;$7"% OFΖ|R(H (3KivFє RQE~<7"v@ёFW <#zFO 2xWc³oߓ܄ʠA#=-]pcLǞ jca'\MÃe9C]>&mA_E[G8Ky>v,; Dơ;0s:uUې--_O 38_Cbuy/2fj{+p 2'>$榩(k1xף6gTc8aw(2M "āf5o)RLAv+oNbUeȻc70aq=}$e- F5H: fAe-(@lJ q.Ĉe]L=W1!OGA8D2opY1oq;*ƃC؀9 ] %^ 4x 毎"HǵuxOx\S^ g~ l繥.]Pݮp%zx݆:,b"rKCg#owAe| 1 ~cNcL$F#y(;FUJO֣ix4SHŏWPk22wԡ"y(tF3pd QJp61 0piǬu@YU/p-Ծ>?Q:l/>l2ZQIݝ1g+)reכG!zjԁ☧ScLp1} qT,r׎{ eG |)Jw[fM)v"Cm":Trd/ C{\gwu#Aծo^ qO %w,cbL I#F{(^:"q:u<1~sǀG'$20c,N3Hڷ Ab?}PEi@QΔb!q1 CY5+u#[<qTsl n鹲Uq L7C8_ESMc1}1# uܨ .`,{?"ڗ[Tem%Vu[EvȲ! 1CQ Y(zq7_2d #FC- 7 =ǵu 2Лې˭c&b#$= r퐷þHԏN!ᶉ-G6}9`(D~.* (u7"xx9 tT+bn2#gĈ'f$p,bD-ʂ8>u^vd xjM_TŽ 2 [ƽ"8uFZHc$C?u].F1c8eS\s1X0Wc"@>!#bQ_Tq=J h9 /@} rkoJLm;!Hf8_|kA)`fkR|43b\;1(`q@%^H<bKe\oB+QfKzMVO1H{!q"'QK (hF=j0v>E1X0Rhi azH-HFvh qb92JLE׍񳍱8]Ms (nT"#X3ro2zbh !$}8=>\k{K/їmj"YfB8GȌGЮRV- ա#|SuP*hBXw!SȇQS+ya2^9>>m1O<ِ˕chi=2~gc8aLa(2[<%GF/ z{evǢ‘֠!8*;xɶ|![Xb$ y+̣gSOEEN/BVݍsUAeTWJNv"ʊlYX~cqΖw<N!!BJ6u׸6{\!}4_oIN3yo) c czϡ ~2|.v m ZXD9$4k]wAKM2$V~ r? Q q!۟zElB.H,T條{㳿2XV센4ׁGY665c!q]H [̹}z)`I2e}e7  \Doߡב(&ߟc,NsTPhx~ef.Z\tEƖ@NMX\҉g7RO~ 91y(}X~wy\lwGYWD3*b|Cx@ M c:$6B/ myeV]gVd,*AŶRP \+㳜|?~1X0[C"\\ nCCPhs}TVx:%>SP˽,_#a&Ĉ-| O"FSXIyŸ"pa <$4 %Ov!^7讏!ͅŔc䓤&F}7aB PrQ/Q5)sKec1'@~ևuQƫtc?vjt6Ob(b)!NjPnV!N2 ~}!Pǝn abT_cu"(+bE vG"T!^2%(g"Oͣ1Ba'#߈*y"TVTG>I o e2LiHx 2c1')z5.#hnwDm>E(M>od3yguXύWf^#}VE7EĂ6>cuHmRq"9ўQ0p荱_/Оty=(;a:*YFբ֡Nލ딍"023sL`5c$/@+$#(>: ]DX4AM1X0kTPWI݁W)G?F1eD װhOP?Q9Ȓ2_ˣ#Fl Vs(CU$t%~2ZN^Sr(X OqS@ lKd^Jn2mB ,)EUPJ41*ĔX\ߌҕJbV먣Vc_(C9]J$Aۆ%?ӄ2##^b5/*EYk1 e"CɃiz1ǽϔ'Q@cIRʮN~Lܻ7Lr.U 7$gfw` c1'7Cf]ArhݝCAr)Vχqhv~ IDATQڱbHJK!J:Yq=/*c6E5F7y T6q]H=[{TXc1'̠n#=5_ԡҁaKc~52 }7S >HJ5:C%8PN_aK8Bg&T1 B(}PrO279 T2,7M=XL5my)j:ymIŅPޢ0|_p*x\%Q1y<|msXD!DT1X0? yjx@v>" `3TZ8ӂ\h'T"aȸ^݊ĝs+ʸֲn.7KQ63ފv͙6٨VDAE@*hgQ)($Z]7$6G%%qåe,eF?i;]2v3e.m1 ȨpZPߡVoF睛U/&w@~{GXc$2v6p<[ΏQcvR/iܯBƇbC.'s6x`29J]FQ0C;lymq{;>JLp#Q{CS=t/?3Ą"視D"Ϣl+7AE9#!($LD%j6cq tP#HsQ*\Ϣ. bA-g]~}MxS蜑n>l[E neX \(ٴtg%di v 'EHD l ^8pL5r;+G^D]~4WOH=GBֶte[Cr҇3QJx{v3bDڱfJZ̽[+)a@;k9DP2NF%4O!̦죸GRϢN9CQ(knsY$_נLG1Ƙ~H @ZQ=bA9ک4)XQњ [3"B #rҚݶku =ډ!]։{QvlaG?'ɯ;뙦tmW rXYcQ3c>^.f,dۑq@K(dacYMC;-HtIr.wRΒm wŹZ\.n Z1X0Z+䉐F3 G;ckFa&vWc8oc~<R0kG6ՙN3_ţ*s.puiFM9ۂ%MF~WE]tK~Ҿېdg>bq]lQ2֏c"]1vgPҊ"ۛ NCG>ۢ2P{O,ަ)Am7)@I1djy-ʤ떿HC.3߷DvayY?E7ޕ2'ҒRRN~x!LsD㥞z](xM=q]ߓPQY灓Pw2|uO5DWPiBI&Sd*Yi11E1O In!Ir_=[5t!B| i ڴmKq?WBk\YeO̜ *,J;Ofq\PKWhFe<11#Bhۨ,f2Q{Bh&yhK)RJCVc,N3XLD'L]=Ip1*q88a"RL9F*U#1**HE!:9L꣒vIOJ:[b(v?BKT{B^x&QW2`L`ʰH.&=_WJ4G:iָ_F^#en*׊9k40c,NAG/2r:ʼn1hǽ*IXם/|]c>щ )89!ў(kfcdɑq9fn| e1"j˟)kP0o[nGǽxm1EB, D8'q*ᚋJ4zb܊1X0.Ѯx/D[v۲XTƅ+"`j<בֿ`@4}Nw+3/P*lu,2 TT_>̊ =Z3Īvdgן2xC;3vMPQ.pH>I L@>I3SrE%8RoŹ[] qDDBU(շHԄPjgR1joE+{ti*Y [롬QVŧO$ x.3 <c^D=y 1sW > T"&Ԇc8a-+(3D3mT_<R <rH<Ѯn4!̈́ TH4blfFk]_{؛Do1Fg!OxuU$FN'D\TmRӝȚ"_g~<-.Z)߬.wed*ĉ!I*1ދ2Bm^Kv'c8à-oF;Nׯi5-b.ޮ}vk:l3ˢmMXTB_9| @b''oW;P;($א˽ZkTlwt~owu(>CexW3TyMFU[E@wCb<^ 1S!my8wv@i׈VqCe%Gǽ~c>Iǵx4!1uAQםG!m6Pȣ!F<,ӈaC(q&; 2)g\38?b1X0"9hǂhpL/$?D' ӏ\ ;E0|ć:K2y'N(r/J?{ Chep ς Q~(lL"C-eK-ʠhsJ#98@`_ \_Z~: PK} AUuWGcRq_D]x <$WE0~8ʪ x.BN6%HHӍ{n'}VKNģ(s" T"c8a=-(̃zetCi~C!RQwZv{eO 8Ut B)BCzt|ڹ4!q7^̎caZBtp +ǪϼaY{4ʈ2D iL&oB"#?q*~1jB$ AiF I*j^'O!WQ׭$=poC.o{U(;hg$(=t1#ĉ&z. jX%oc118굃|, o3"Em,7q"4V9e:] PiW5qMlZov*!$وvkF 9Ȁ뢧krIFh uH, BC*,yd%]|ƭPɑC{0Ql F| QJm oiGjh'$Q{OrOJ`^/|T&TvQęq.Zz8 fK 1cq\%[S\WF{$9hgu&ܸ,Wz୚1+"{34vglF!e5s,M2RbT4K"8]VkA1E@]B\`~u_oG 1ЕDcA'E^qSv7G4eD|PwΖdUFYT.P'2QIƦpmdJ/Ej|=v.)87YN.@Y\OY , H;$!gqm˿XQ^1 #X8ѐ5,)dc`I&mU<㌵P( :Hhu؇ o|gt7x~'a6h{p2ʎp7D QQvKgYqx>\IlFmECL!~Xq"3/!oIrsL<>2(ӡؒI|G:I3">ۛ c1][D3X8ՠ.>|:]4{XVxf&2n\Ya joQ A]#C (1b!HcVJZ>"#P˙(cu/G"nBϠPkַ NkPO$e"2&̉juQi˓ ?{% fǸϟ!WߑCTq<Ԟ,N2[~p$64 9a/iXd\ GH3'Bo ԏ$V?QJѵn=M%L!lBؚQS!F܍2ތZc;|Ε F"1ixlv+P(#vC![D"GQoI\na7^M{AG%?BH$|)C$pwC.U}|Go 1k{Źޚa< mXc>G?d1pL\ 8cDg̸vYqa;0wGT\;ў"R(O%ĖMQ@%,1&=Ѻj&! |$DbćvE> ;]xϙoB fs]>z#C;?ADknA86t P{e.,1EF/urjP'>GM\;ƵkXruov2SDq]?c,NN6 .2λ9dGT^pI,,Kym,{TֱvB#)=zakQiƴvEiQiFE PKS!8C0T W.b<6hA>++wGqm7?mu^t8wx exUh'e)䓤eP|~kE,=p*} `i7JwD%Ga1X0&C*;y26>)NdכTXr1\8x&Y~fs[|9(hVR]yP*c<q|q}e |wQv" _$^nz!,{(t7D4␸r:، uUH׭H}#$~1O0ƽu|2*$>n7*[U$L_y%&^ .$cʼH5fAh_}-!o~[l IDATca%ի?"+bƉs){eG.hv#kvH +oBlŔִLD01*G2/@7ލD:kbŞ\OKiԐ-'Ʌ1Fg3ItۨnH#/sНnF;IA*6!{;$gy Gc*Zz;$^ Hk8.FO=<+յ]V.+cLpL B Tli."Hw iP.A;WWr'"ej!>@ Զ7<@6vݻ~˗onilkkkj]2bĈY'Lxnu}jڴi/O95gc,N&d>El~=w.oꞬcׯAkEXMAԐǚA^BL$Am,$f e(JVuxg ±P۟vkf㑶h=jTĉ+6`iӦM:mԨQum]u>"oK|/Rh<7VԇCeHDzf s<ݥ] x$~ I}T~-ovfWvC]AD%E8|0Đ!\ ,kcd,Ęl~l|Q[=pqec1'i?yn|sR>q5D[^Q22\?[y "|ƠFXPo $!bzwCڍ5̷;hJdT߯kn+3K)񮚚?eKرcCq,Q&k2sĹHM/3hpY"I2?DJpB:jU:'2VF xz|=aEoĸ 0v `#k%΢:5ׅrx5D;b72*m)1V!Qc \7^,c@SXQƄgw]H{v+z(xtAb1X0]v姬& 탽CJ_- `QVs# Ԓ"ݮm)cpfo0?2 P*C!H>z=բ܍㳼މ !юl=1Z7D=LAfyBKvɷQJTHKl6OU;Ѭߐ,bB9 NƊ\F0xnY9c䏁9%̢Fq pӻP19]J=&$*=#c>u$C(R(ըcG%3e=:qADc1'y?vYbyMWeAVfQXL2uSbfi֎y;1c &GkeqP 뢌}Ѯ87ivG{s(A$k]]ң0. Q(+cp2?g=:Ŷ."л'> ` 0 PPKs gmrȸOq<R7qLRoiq rY%|PVNLsE6@Ou|k #<UҎ.PkP*.نQJ6]t]~g%Jup#shsg0/7AÁfVĹZ z4JEoLA$9yp$ p{)+<$B'֎%VRqiEYƘKÓγ-\s k>XJ5V5ߧ^[1{} Rǡo#a`5$CIf#5^|W#l }z>I.NF!?PIbſ\ne>I. w A[X8>P^ ぽPN(k)%q =m]cc cVM9n8ج.X: boT݀_x}>-7S4SPg1͔vWyV:zaԾB(I ZV?@Q)Ir*:,A~(g䇝l.Q/$.3 Obſ(zc}}@3Y;R ܒ2dM ;(xĔ--Q~Ǻ*P㹥oeN7?~Ԑ-nH]B-sؐ˽^qAdiu2pMejk<Ar9蠲D-*ayss2?^uλ[7 :f11N8.Ag}Q~^.K= h>1n] Gힶss_)!#8ʖ8t/X;(}}Y'Ǩc1ȗB8x m;{( UioQgPţݽIR:0MOe0E!cb{`ACqO9Q3$D(䭲nQWQVc>kvec8aL_bX7yH(/CmQc܈Ħ󁻀EwK'Ʉ.՘z)J_xnh̿[2OҞvدFYA^!zqC.cD:6l큿6rkBB8> |#@%kwlQ}I2:Os*p*JB:+ʈ(-A17(üRQ9`sQzLm㘣DnqyN`nwU0 y 9ϷI5cq˜`љҝyH~oQ{$*QAhq n:p)*x$V%\ػf&=wPD.`A,ر( ]:g}]A`Aw %=k\IB8曔)k7{o,,=|H {v=5.>pn:D& ~,z_Ӱnr5 `i]{=vy{Sq Ts%mNz= %0e1ޗ `B"LxRI]/潸7g@x@jw'Xw5 i*^p(4%]zD<Q; <.p&@Fys*TF}{҇]n~zCl ]sRn/Xe0%jwI5ߵ0\Xbo=ZXbŊ'XY_n)pbudO SanrbYbh:kdgj7)G0Gf|9c&}6fLjg;\ ֥x7G9~8"U?o64ZJ~f6u1.Fn!N0:3o D @fCiט G xd|ح?3!/!}=ZXbŊ'X_3V:m 䨌p2tz04[LX@' ulU@uRH#? .l+rAo RYYS\?I~@zi~84/Xc\4&Hqzx08!EF@9KqZ_tfy`ZqI/\`W$^p>?+4\[:aVXbŊ'X 7ȗ*yd(eX{|RH)И[. t"8Aϥ:p_gq o|("> *}` -9`@.0/gXΦ8VGDi0Y:+d`QN` Ԕшfm ;l{0MP$ de6b5'Z=VYbŊ NXYN#p#d,+9``57T.YOπ&K(x\8S-Oryl ,w!ObrhWv>q;@r,T Bf&Ri&q<5..`@If t?'J|_h.5AFEEyisv"VECP=/[GVXbłVl2D7\w9[k#a |zp:4ePn+(rg:il`kV M99RsqF49}v{E&(VӒ\K&!}%x'<5 t8NѾYl|z'n&mVe CQ}N}{5{S6Y%Om,xQ`P/`@` v! ϱ¸.A5upؼIbŊ+d!B^w@=3P2Ѥ}ǹ7ӹ۪FY`%"ke+*1{悬tQ-Dp45C?|x,h~;LyHf=3J}{Uq~DS֓b28aq0#ʚ-VӕzLH\@A2{C98kHPHm[)F̯g쨵͋iaz B-LvlwA#a 9aŊ+V,8aJ W^ 0[RWQ҄++zԻ6` 8b?Ƨ\S,HJ{Y%}HX{A w8úLcw07q?|. ;{yr!h\(r.hr;u#UNw`s n'4rX1ǹ=vpKqr< #)$9/%bfq.Y>9bäӄ`Y``O'=/iAkN^/p_N2{ Xbe#aec]޹S3f:BQzPA/XngN :Bd)FiS1`<cs0/>H|2x"=Z鹵҅r] PFZOz uJ[dBԸ}s ?/n%~rte'KJb-yAo'z>;khk`iZ S\ Ljy|nQwA v֥& cy- RE 38Mwy *DW 6ѹ0  .@bsO,9w` -9Q&LS$04/t=)8A&5M&6S̫}:ԳN|YJNTd L-:ПY<L^/ӕH6J C d 3q9p:}}~i{+z$%@%/kfc^i` ç썎XN@%aDLϘ" ݥO0R_+yC@-:'ҽ@Q0}4M@ RxA^JZk\f 9/L>G.1s;nx3`jVXҢŦuX٘e͝A gE82Pp&0F/yL+߈Ae` ح1k*?:l?jJaҞGA-bg^*zmGi~2MN04,9e`}_S;]TY~`ցi@F l_1uX4]l" Mq,;3X9Q:sc12?Y$]NJ+Vu' [-8a:2 nPJ?'8ǃTփ892 $;ilxNnDDc嚛g>j6IqQSbK0l0M@>2>E|T. ,̵` 5Ӑ dsŽrA0@ΒC[L ^O ح`1Ԇ\FD p d kL>>0 Ӛ5z O|&3h ;fQ*Gvv܀e`*2OS|Kkܬs0F~zh%Yق+e(z#wpFU2/nŰqZ%Jd)AyZޖ#z.^qG[B:yir+?,xAp1]  멿kBxL2~ýLh:޺,'9p(XJk4_+R&< m7ѡ`ʒIC") I+U۩ %S:x(تS+Vl@0Xc_`-b;DnVlt*üQ%8@"ϥ]b2ZN 6]S6u1^'L0GVnVc9d4Hr|K6F !{Qkb^_h~J 9#@WqZ=W`A FE8^[̇~'Ȃh^^$hs@6DBD/y JB:#(}@P镟XW-2JV`M/0 0#2.rr r\zëgA=jT3lLjRU:=Z`un1gXbJSH=m9'ivk c +V;k*vpU?-DZ0rؕc^'ؽdo$"9Ƒ0 ѣmh-P s:9Oh) CpQ4GE{b379t& -f,آ4>m vTc2=-`jUTVvXYFQ5ZKzQ[H{=xR_9 Ng0Lk Ow7ӗ@zX]ڼa{ze%b].^1l{`N@γ+V6#y`wNSF_XbJs =e׸HzI NXh[l7\{TMD+ʹ5y_u3b "R*R8,R->M-me1]C,"0B'\$8gqC Y[np"1~\ &PŤ!MEM5/3sƵ`!՗|p;LHS͵ecՁdv`au~]3N9熹T 3UzE^YK@V1X@&QO`Nz2{,՘6R2%J!3o`gmdk;_@uV4KLۈTmJ;LlVlr}?#wVprѭ`dDkEvHDv}lW(ᕳoY`5e4 ,|X UxUs a- FqIOm`Ku#~w@(tҚv׾9Qdr LQ8[Q},GVSK @fB0B[zxa J8uҗ@$}W&9KR|Mzd< 8* hQc7;.T  ӳBkGwC-XaJSA NF ]#['X)q>⋿G`924 \l&6!meЖA"a),*/Hptt .M]@3_r8> *Ύb~~VAbJ0;SvF"*. @CN,M_׀̘;LMb)Im2N1mZDvb.(0>@//w*D" \a-|#37DfL!C#ܾ4 8N.)6בh\1%OK51$,״>|_)`V4v| XX`5wgz9ьpkZ偔rzyț, D2 J@z`q?H\tDu>uq~! [E`b0Lƍ66w;~'X7bmFX#M 05):385j``v[E`E`% 'j/1ՈȖ9Fva#8dl\26ثO@W 3'N$Wg'4F8Z 3Z mu0lgl΄{@պh)i:{+g 4N czsύtFԺbѸ =_f굮ڀ0O[`aPkoMb#|mOf`ؚV6N j݁u Hj"8I96*ˠV`9i0ZF 8Λi0}q>i&<=W&_#0g5ȬxFt6`JI0Rt@-[4F@0ӭiؾde FFףF!@BtjmVk ]JVqbmp@&EO$5:Eq` >}u4~[ hl&T5Cl溧ޗ>v9F8Nk}0>?|JokHFV7^E"י:ڣ;){zݗ >A$+V4,m.v,8ae.``jHkEƸ)Y*P; 8eHlB:+b7*Q> 8r"V9U*v+&qAFŸ͠[dxUz"z-H>C 5o2X,)̉5BE6ȘoOS1X>b-F/'LR#=`DzF"URH}saOcV`TBywTi7GΎIA[6<:!ب;ؽ(#$u)2O/@iss"fo;AFl*{Dgҗ@p ?= ieւIvcJVHJ NXٸ=X\cAz*0DLDX΃B*b0Zs4'_~9{СHQN 6bƠ"s9`l)PjXgiLug* RAW kb9( `1st@,K.rLd7;6Cܴ̉2y`~ ~"-KkZker$g>?KmZV9 ;H")#ZS/WHl5GIx,*8SHNᚣA-ٲ*0@+`>cNv8-8ae{42pX@Ey !Ëz=_Muu5x/m-0)7ɐfuSQ ]F N9r^ poxVxhԎ> H_Zv3Df7y2f1ŋk }'^Yy|9S@R_]5)~+SaaZ:u\! LUa@n -ߵ޶zƕ8E. ~ ~ d-=$xMԂ)O`!t@~)Zu_{hr݅ <d5eml NX(ѳ_9u(&:|ϗ^-,A˩N0> 8kt<Ǔu\u5jStr(%`)Ld+Tx ,p ,B0A.s(׵ B}z7V_`l+cr'*^*G@cub߆1n2MT ؟k7* yXHP08\+'mcjZJPKGl1k{` 1\.jL ͭ֏dmQ!i3LdZh@I:ٔx^KK|0խD.؜ekJ\L c>@>Ô+-I@ [bq?!p*X<#oST$1kT(BBnhMz!,d@  \0~)#g^ʶzZρi<0;C#pM: i ɟNrD`d(0H ;`iTc t70k$4oп﯌㭎 }ܤr)bm~] 0ZmPW40f[B arZǁ) (7W\+ !FM`>uS&) 'źVKcݯɁ\aWg۹`P ȶ[aQ4_Pjg,S*s`P0%^Lm.Y]GKg%#-@rXiI,9KˉIW]1e8ntK&:wi_#]{%Y$q6B,U[h/C%k8N*0o dK|\X'>*gtH_6LFS6i>rݝ8F5hqgqZ>I?[<)8\]$q|<`[;N)Uk^44G`zŽ*~x{|\{E LLo0Eb'8ʸV`^ 8έѫ$஡ͺV:`K;kʹAw9VXpP&VNFWui,o̪= _EXm׾ F{J<1I4P- >/g~@ 㼘AV=K `m}3R׺(z+er ޑ]yZoSej&}zzmWV$9KI+ֹ@r0:S p¼ Ҝ'^b?&p 5r)<=nX sA$Z0=w/A46{#nxn IDATS)'3Y2xL(%+v1~d; 8U_$G7G rc"d< i]=bMX91Xh>2q (r8O)9PQW4q /!Z zP?7t%-+&):Fj= kA:0AзtS/[:oZpWat~oQ >FArpy{ "?71tΘ-u.P/$}KG'kg _tn} ƷioЙ1I9 = RH=wtw5LՙnjH-xl?#Asl:q^[!8zG6^|~$y[ AvKv)[pJ~[v!Kn ukD_qqnc,Kz^rs@8 & B=dTǸ (9X8 TU:&9'y`tf9wBU:DR+@d@) tV u^2r.\ Xyz2n| {b?N79`ׄ~t`'@$RIp cH_MzGɹ1?@G@ű`M@iL \f~] DR5rb<pr#X,X: l<i5ZoE^H+;dg0Ck2g&[ 80lԛ@vM\ T?F'N(Uv'5 y h 2(v왝c89 4:U{;ğ>!f}Ȏ? ruo)%9}`ǔv)[pJ˔<9tG*蓢;D>bmqc &\{ɕsv3H؇^ 2QjZ&`7h]65qtQ^O@:e_Al  (LJSWe/nϐX-c@_Ld+e~ 2_XpJ ^ pPvtHwސ+UP--r6Sjo@z=Pt%1"9U0GN e"enԼV 8AʿϫthZ{}w2PmFv0 iIךL4$ ZF#MgZme ?dİ>/vY +=TS 5MG%Ż@ؔdx E7KFѿ."k/Ζ\f@ Fsi]0``"zA0aZ. ZV2B(^QLJi@Pi+qβ`[Gx h轭^P˗a}ߣGLҴ{o#Q^X`x`(|!Zrk0tN<_j5pDXp5t?~PdmXmełVZx]aRmB|e 9BnQdS9Ḅ&F&DMAN~A*k,'B #׏)*S;!ϓs ޵,mOB3{ &B]#̩f& Xyd.csrv=Խ.$yQ 5{@ >LCoQ5XssqLh)ӏI)̍WkN秓~7 (OrG_'7zd FKT7Q;KSGk@AIc0 y`D;%]ȹzB?'0  ~U+ΚO$[|)*{XJI9˛l9A6oC_ӏvwDW6^$E:q+}DKzdu`b T~+ɲ]]߯!TׯٰHv~͂V4zư 8RɞFSg4rcibHچPt. JI>ze`|w3vqaPEt!sj9 xR% eWYw0#@9ueIF."||t}P Dլ[7'[+#H偑ûm**[ƹL3q\0&}?1rA@Kwx9]"5rjqT @JM`#is`BBw9; f52HL[W'ePzy]=YXDJ]ߏ{]n v'%Wxw $몳{kE9 xF0 IZc'J̞,I"_:_d3"z9VNv?LXyo :H p#AVEu>ko[餱 ޫd҂zAaF:ݲxĒ6 #_4܍)AV^2J0G2+Y'ELJO=*ȟ#&P}k\$z|K;|":eQ`= r9xC>zil-g9+d RL{~)4]2 b>%(~:uc[w^yFpzbb3j2C'ҡya"ѷ(\U)1dѮ'gq0zwL'[7h 6Q`@~֤<$r4@;rR { <1pMZ?}d.Y ͽBCJoXiZ9TP&nJS:A Ή+ $%}(}3d_u6Gow/0P 2T@0J{i޺ ԚpK I+}P]`X6)[8j]'pXݎP`Mp9 ܜ HD{m]b8ћ (2H&:M F*"Rk/^&CW@ EG̚=mwܑV[]x}>x9t7(`v B 0d#kmu WhOF+rz&*k쒇AFlC2L TXV>Z?鼋%:t߭ψpL%B6Og X+눉3 H:GJ7Z幜I{l 8*V-D,)92 k SeGrm7T8=>ꨙ 8>98NB)qR:Y "@*#i#QC_Ri0;{"FW\IkHhPWWd74*xA=&=.{yF:lZh]-qW FrdMG{e$*Z/Q@ `sh8sw tĀAhl$0"QV YNK}ג bjT tW:34`eڣ7E2L q; Cy_i3|'KYc@;댻d;߀`=EԦhޤk&Ra Eګo ؈%Wlj](:,"Lv(Ɗ+r=Jq}bLZ}/p=ɡ"g N`$w4Mz]<s] Qz}5{Z(% |o_a_'9G'_ԗu},`Db`5[(*0B=t߂hi6BG\$sip {r568!tn&OWZӎ@>#‹ERG߀/@A=H!o=H fl2@[PFȦW`}; A.ml'u5}R~Ղ4hk0%p11Iiq#mƤG x\(~ 0JLT'0pk\Ҵ |7댬a+]'NLqgs3uH<>Ϩ?ɱZ1d駠lX"B=X`'k7cmvÕZcA>zMD6|b'XIFvCI>aYt!Xw+''amcWgbRFVT2ykyO 13^+s {'hLzʠyYFU"|? c#HiF/w=!0# y KK蔙#9: +(mqoR`K14;}.}(j pDCQ^yu5_@^Ku [76{ -z#8Tb$8W&$D̝-u͌q=ƺ {2w` D"k3`* P_p7Aqsli_tmxl9O ]uk&]n2Gi~ cΖOsyZos㱩fm"{*}<-4Q} #eWldb +V\v 99c5I0mFqGREP&"mP;w-[9Lm >WFK-9[QA::s8b/b2Z4M. (]etNh @;p/w[)>d#7Y9Ig0r' [Ȏ蚀if%]5ˈ2DźR٨$]. *A(W9rk9sQpxǮ"8߀Qu)9 dptp+ٲN?i^QJZMl\"Gj[ ~vi 1!ΌX5tVkO~xuV8L6Jm suw cXpŠ+Ȏ EGɤjli,{tmꢱX 3TzO L/hVJe{ȘFY>OA akwe$-сz-Hز/D)B|ݡ`zsֆG\mD yļ8 kk`f_)i ]#R0՞ `k EQk^df-` *3r;^5U;-l$2LGQg0o΂ڲeH, dRJ^A/ 8W@\W/KA', 8C,?J#=2 \5)49)'VRlnY c/g(]n WIݴ#['0\ MwCwiev08Ѷ;'XIX|`$R˛Z\)ĴU v+HQR+”;wـy@ꭃj79j9 a;=󞀊]!6ֺP]c_B2D_BQ߀j@}5_;_t(YGx_H%>NH/$Md7OcW( 6R1mO}.,/'<#hE37.'QAndYqk6Tץ@ On F5'(B<]z@@3ZIlv25sI05#Ur88,0ry#Ji;X> 8H"2T`5.A%0U(qP-si#;5.lc|oyQ^^ES!1 AlEgdtGCkdIYRnM NsMH;:ҵ+J?-3771+w FA"Wz@[#~Nء#((#pAܮ`nlbGzP K+黿}먋 *㇃!}J|JZF\Me2ͳ3&< ӵׁy[v;'UZJMhA<dۜM݆4o?R֪ @v x ƘMn#A ְ[ pJuMkVbK>\~~Ih/},ʞu0/=S@޷b.8@ %|{48︺d<WE<׹ -Zi:lzYۂ"\v:YfoEcԬОL$3Xdr΍hβiAEnI]$CN$ zMCb +V6Og"R Q? 2T˹Ҟg A<b]LF)2W{> t3\l4&!G鸝u F(+ޛ+ů}.ر#Yô ȾX# Waemvt2bG4$`[#=`xJZ7QW{h\e@: :+3n5m4,,;#GsfY]y&urB!rGNzѢ ~&WnJ = ɀYHR9^gte1snR-c(gq~Vս2k"=3WD%$)^.7t5 y9R2am7$p"8e54Yv5iu˶3]j]:ܴ^ p[y̴ur4 IDAT: 4{yAl<\,zsRf7ܧ=tȮzu Bd^a^ښM+sӑ6Q^S$,G].z,ceg/`P0X6_wjT'GjԆpWt2,8aJsaR/#z$RAv 71_NUk9ʩ` dTsqRF"s8"~[0{Uzi v>SyfU 0BN++OU`3`z˲4VoüZGkH+)EM,Zv9gij_*_b[g?CԢ1DZ3h:"Q]d|9qUiC@vP\) ֯Ț>ijEWk+w;]A:('.!_1`"~v^`eq2=03Mg\Xg `:M}b^ ҤE{ܯ^:+N\pLn!JwHH }^;ϤAS7I-d%1ZgA:no$IBo0I:w0ŷe/e;L}Ly2:2 .HýL1L[`w k֊'Xi-cϋljg) Ǥ>LQWQXR'p{00*:d7 v jZfm]9\e,B( AZL9|8rh+ux7W?!ZmJ(t Ӹ?%@t4-n2l~EK K(;v՚;)]HBc&7=c`Bcbkk n>;ݧ0M"]FIҘw SJ}|0|?2>f7ҹS ? @9LuBeBcQfBy nKs|֤6OF` ĵH-Ed4 t`+jA|Rp0HN܏Gj1F_tr%|d0odnHO [0ٮ#@r3S\S{ wc^r++IՈ3=si}dlmEˈq%%!D .Xper.oK0̃(])%530/BWFC B |RfF}K]qUz]"+]$PB]J x0@C^Z>5bhZ#K]WƧ6{֪8 (2HװJ*F*rt~^ @|e3?s[pIKCƷY l^l2v NXt\O2WObr}dw[=zM0`#B|.rbUe3A#*t'}L`Q$@M{H1YpPcs46bu=R2Vךx9̯q}\0u/*DXK?øD(P/4?& B 3)M+Là j?3cP ˏpLY1 wKA Pkhon@Aںi XdtT4ѰrSWju_KoIH>:Oj ސѼc>p$^):ƃ ww?%5:BFEhM+M'1*MWj4#2BW" iłV#О֘lzX/0"r<E ]?Tt#[j< )] ||x`uJ76mLzO Bl ӭRi`D D| Aj^rqOf "z`TLsK#| zm4|$.Na3iP h^m@Z1{}Ξ;tt 41`pߕJ^^ҴbҢ&ءhȒ. zb%lZMiUHʶaN$ H>AWn0﯉-NU tdEVL@:Nr.#gNr9sv0m9<Oi/_y˽{x̗%Z-ﴕ'e''ܺ tJGaDK@AՏ-+] L2Zp7d$c=@:ͩH@H͛֠G6}Ms_CW!f@A`0:J?6C\1ΧKArka+$` ڎw/X`‚֬5e}."ap|ACoC7x ̈^'+ nSxȔ^l6exbA`)F3%g&``Kq0qv#sr O3 (5F?hYV$Z9'׃,A̓fWAHoI ^R_>0@ N׽|[f'(*0* Ζ-ps` k}$^ g0~Q GSc\ִ"+@v_"@fZ!8f*oI%A߉M$b>Ha93{ ڍM?a k֚nr&]&_ T&ѷg+V`27a W5RP PJ0巁Ttp 1@@:<03lZ8x7Xq(X>P =4h-5`g@1 (] `BVհ`Vi<"fƨ_s+fmvb]<`B!3\}zX˃x@!of.m݃ HW_MSYݎS{KR+  <r#o} {֙n nqI0X5U 5k,8aZZJD@mqgJ%Xr`vqU-i3$Q Aa̎+Su}sH-kX0RGgWmF깠Di:c} 4b2Gzmiw?(xHT8!3dlBp hA:6Ysck`,Ž9"-ҕҌlM' !,k:[NW5QH|2mWm 8Q a-Ps,{HvKFm @`BSX|@r}!Lu''$" 0Q &Q- -,9a͚5 NX {xpqn[+0 ~vcjVO!P!gE/9'_5bԸ M;((H].L@P}m'g+֤_kԋ ZscA )ۨ)? n;E`Ȏ.װnV6UV#2tA t+3e&ޥ'iռlz./whV#曌 u-,s-m+Pf ki!L\W5?̜n!wѤ(1OdIdDD7%k/StO#Phң {lz*&j[j}sAq[&:hX ky5vjcC9`kS [ ^0$Iz{jU)h͓S@PrȊ W$H5?\ &K[@:;ԭYf knrHE`-bXk)@u{̿SаzNk~@u>nxoF?} x 3ȿ V xJvsY/uqj{YoIZl[@*X8dY/dj̤Jby4^ZFk z& l{!e%?AsO;O$'S 6%YsXP{eiqh>]?]c2X鳿>&ͦ5I Wr7oW3\7=Ȋt'Z5k,8aZphd,PZJAMW\zyy%&G( b3l#@`n#:Fh1] 0;,i1Ѭ{.haZObd¬L8HpКv ZzuY# Pۺv/=Abdؠ"  zD| Aqk@l> #fNR I)k5COIPD?6 ƙůg{ CRXf͂֬5j`m +*7\ptx#sliQ؀5 Z *)?4悙!a2 `fyKJ4V倷@p4 񵝍g>sͥ#vdY|jnTlhNYZhA^V_[y #uW*ڠm5<{{zgDWS3[o*e5/ x]c Ľ/Qyoڎ>^Ŝckm+cNޏ,I|Ôl~{_kA\'pȖRYf͂֬Ű`6m{%Kkr ((=9wM+&1E9\ẙ~xFh hl'i@>[\1GöX3 .M~=<_pYQ>Ob?swzʧЖEa͚fv86*m}= V;k_9#]Hx.s`bSǽV ]n6 {<`sL+ `Ft*%+l IDAT 6='qyO ޭ#P4@? z XtR1`ɻU4ҳ<${?#Q_B$ @T~﹘q|b=H +[0ixXՊkF=.r z˓Fzzc qC~LV:6=&\Os zxGϩj獖2J<7w+yz(snϞ'~y߿ l p"Ԗ֜uK.XdM9ȺX=4y3فo]ǀ"ѬJ3-`ߨf^$ߤrz@>G, J(P./~ηmVXkO5kl#ARڙ5x o@r'h4B,Y3[V kjBN@ǬTxnQ;р5z5)@9M?_Э &toǁynk˒u3F̎>]; G~4+t,A eq9Qz&ÊiXƈ 8ݐ 6aXaS*=c> h`v8ޗ]ޚ߭[DBs]AV\SvEq;}Wcn@&E5Xpf5}nt[*`J64Ӛ2_ %t70bO9HQim 7$:F?`ҷsʱ);d0Q^1;8 S3﮹atSż (drZֳ@`b{+Yf-Om39;x-H Nkcf+Љ`ݠ s.r>7f}BrJSQY-b&9l \{bOֳxq@ Tַ&nx/H}ȔEy1@rY`n5im+p$(pZz ̋{wNց4mblAmu7 l*d= AUีLF,о)DOݗ7+l}/O>}; &GoCtš5k>p *|ˡy f 6ȥb4<b RU9` =F;slr趇d |j+,5n:B  I/NWa^{8 } >sxr Secw>cgz:9Dv8ޠuz+V _$^j0F VWÌ `Zg4Js,g^\)bďil{AV͏?4YHulGk\ɇ'78ybI$b=Ao%% :g,"i { kɬ愵la]\pb4Bzʡ͑rSN'Wc(PasEcz8h'Pv4HuB Ȗ~OJ<) <ACWA| <#mi?j?{ ,?,x?8e&M5UaJ)P6L?ԑKz4k֬YpšF9#ojKե=`F:``^3,$9 [)9AT!{P }e)d/䂥& [>RYp PcxX\0;֙߃]APDqƀ` :(u|S jt 4ܶJֵ; ,g=6JA:nk9f-C/ 79lm,3xjl' +^HGeM9N0߅آ~׃L<w xj Dh$&* !ǹ, *g8.1CY# h[ `e HM3~Zßо<)?!c|=Zf=L|2{KYpš k+)(̶Rn SQ`#H%@ c]D;+y]z7 # E0Gٵ`|oRrV g8=G0 9?0g8Yut'h;-j/VP෠``qA6B:|g"q{jcN{gP4),3@D esRA??X}kdE ym`=>5kmc ^r^ AܸF"Լ^{, ">fw {PADkurtއ`"cC,Şn)V!*!ж ^.\ >dO O2-Am z>ǀ:9GIJi] {[YpšZõAߛp. H7 To0F`Ser'GdKG,V5^ kUL vѵ:Q@ϕSQAvCvqMF  U MݴO(͠ph ,shtWt}-ـٮ:܉hL|$r5gFh, 3ZX uu7rHJ͗aN#T /ff)j9A۴"~0 d/@GL_ 9[/Pp}=(J52O{%jNuW `~퍙R3@ N$o=g-f kަ(`n)v@}~6 iaZҦXz6R4u-FTdM'a+X݋Z93N3M? x]]=@mC9SD-H~aR0c/Yrd>_H} WYɞer+J,;u0 Z q%c4Mr#H]kͯqxhκ Ax zPG+ tAw@'`2\CGƃlIa hzX,~z!I!ǹK |p̞=?#; j/| 1Bq,4"|֕ 3Ɣ #^TJKY5k`FT /2Bv͛nr|*H7iPb1VˉFrNկ&%O::`b9;>9:Er(ޯ36x߫q@ 1HrHc ||w&Y}>P09 97lS&S:Lź=c703 >Xn+pjg9ё0' Zz͌8޻UK*p+5wW}=\VM2vTpW"{*q{j~yT{ yc@TVIL>Qs 28Ԓx.1+ 9M`ĉ  ̟??7xY(О:PH 0؉6Pd2ӻLuܼBfȘ|Kn( N$nAN%,8aZfr<W* D'2 G8,-TzJ/HAKH$ 1%͓V[9719mk 0p 0#\ `/uX-B^P5¥vW(u'j;T糍GA/{RSBܮ)id x{ aN,#_e`Ɔmc/56X`̰6PS ̐kAqXګ1?W^DּjdӫBJϫFѱžhg<-G[缢5.}c|srt_̽1C7m`|o@]zju?0ڇWp=FkJ=OhsGV& A`T _Hz<8xVg جk׮t1@Uv>Z_}:A?E_;Ɉ봗VzPnuCkM2Y#[b=ZVW`&XP-W3b {!q;  `f: ",B`{I6EjJ:s)t>s7-7MK6Š.p8FEH6wGz%:rd3HU|)-Qh vDb.zmjgP az@<@/8z_ ƙōWh\/ڏVk/B;O_ ^CiO~#@VNoq`N jv.d| XdٮԞd Og:(\u {45-?$ydmf͂֬e+RVej4kO2MNj*K`Ŷ`6 ޓtDZV./35(/3~v1l|g_PO |0/<9W\==%to@YN/]@H6p s 3貙O0&-SQ+u@`wHN:]kk6:NNs+l'3`zdanr1u P6Y HkEgꕯ{ 4`SFEgHj#\3 7s`pԱͫ!n5]%\h/5`[9.soZ,%MZ2 A L͵P_'}6nlO,evw~2k,8aY`s9\nr Fz LJ{i ܵ'-8ǝms鶽f<O)ǘGL#%*@HϼO*`֤X]PSbP266{Gy8ih׀@Zd\rӀ"F8@)^R|i=5. t٨3Xyuҍݳ|U> ༌2ZW6׺ D@l'@槠f@gr~ji+qhB,E,4{D({"|1ր0 p&P٣"Pa2q:\yr4^a9Л!=_L0>k ̦gL ד@vͰD 5! /VoVtX@E@N$4-$zG(,8gLܶ\º_`~OЙmftv A}; 8Hc>4,ezu|@pYq~h9dbt얕A@yiiiSϢ'j<]5nPœ8ʶHwY|w tP"dz^xiOS`C?WEȶoWL,ʚ5 NXcb(k: h. Ԇ0\ 'r-ž5Wi>[vlk4ϵxx1ds/ad.tߑ37vIW1}`S_($zxTc `jXO:Y_w5%$AO!yA)-:\c\b)e9k[YX;g*t 0@Hg+족:To i]_9f32GY˲L@6y tWZ`4#{ִ/ρ "i `*~g_+--mnm)>s> ,{ ,^b4\W+8:݇Zb>e,ACWIYp"g0FsdVbo(֬e P @vK,UjD,@Tg,:n ~;]KG6A/pQ00s@x"=p=-{˨@ M$6lDgG0#(8a(_4CftNKzA@FEE+2*H)2:b^_,{ b-΀u\SdFe.lR\@w!(Ȕz% \Q͓rl 2*@q|c/-- ~:ǗA`,b3˵ A˵nm=.>E~Cj$@|nXvO{U|Tk,8aCs(9 ް j\#̒#6 6D%.1bF0G΅ԗxd x7_aPo8ONPMwb Ѷ'{CPg (L뭱2_A+f@={qw ^FiL1d2<̐\ $l "i ׂ Xf[KA1s.W\4 ,PbU aw3:Zk:Ȇ]^b͂֬e=&0EձY`pP,'AN2 (цcZ5f[HXS:Z⧞uWy 0n:4V9@Λ$LO ROf/G9Ǡy&D <PEླǏ >%ily7(Fv,(yo IDAT*X9dI и6lW,N}9Mw)y,Hjf[ۥrս&fWF 8: J Qpz,D.\+A 5z]D̵z Knm"ApYYYwDbm3[tk} vu^uNu?Ah_ӾtLo7mq80{5".-*G:))᷂T+ CydCP rkA]c)~ l <D:Y7d<K%[NAзЌAiii첲rk@/++q `i}` dzσop[jCfogs<|19\~DlXr>ۂA< V[b͂֬mc.ta9j@WKye`j_@n)9ɰ-Lm6r\JtoY8> %h!p PfM珀ҦV VrZo>3Rwpf6Q;e9GՍ t5QZc C֐g-iA46(;i yyrz4#>0[=`|uӅ!ٱf2ua틟j<9 Lè)++; d-^\L+++{dţIѠ1tQp](@NtߐKHѠg}XBuc{$ [b͂֬3vxsZ-F .b;`0K_3R'i6:mz&: os#G`i=l񶓮'u9 uCFf̦/> 98TɆn83-$<L֖w T˸& N/GT XIUvIDaU` ]ٔZ^ϡn:OPl .KyrZT-V?-úQ }MDp]fˎ p)UoG'XP f6t2XK (Pd'T:A H $nVsݯ8$.v9ԴhkQ) C#ۂap5y:iNx0`HgbYK<? 0:E~9n._D37Le ռ_" G"Nm8lP - y s'Am t`=xueƂ%M>|1kֲ,sZќVirDswdžBK`>rz(k<4hsNr!,S/u !  Qfj/ 9_?|d;$N59^9뀌 ~,ːGup3d9ϛp͐@3OAKW}uHkP6-20Fΰ x &} /sr=kkz+T\""Q\/x_Ƣ(#9>з Ai|A{?)9d= ^,Py=FMLeee5u벲LQZZ{vmѶA{,8A= sgX kfݙ)-("̖}qsA:.is eYjA`Qv0{>; ҍu-as; n~`ֽy)UZ_iU],{V[3n$U>(0`jP$F6@ xQcg$P0sxX1̖6U^a{h9oa=iq_t+F4:kJ{, aOTk^uFjϰAR j+S HQ`AYYv^3&]L1 lMlGdND=VvbGCJ?]׬Ypš NDбZZT솙`[_] }alQ`_A [aKNIOnϛRW;TNqO9xPs.%`{5`UHSAmQwp^#q3[x& f?s{y CeZ `v9|jLįK{]%k?dGւwBxi>=q]eY knt[@wk3\*'c[՝ietm0=9̠ߦ7-IAZ\0SUjڨE"s`=i˩LMX3VM+Uu& K#4Ί@Щ,)+x.("S`rƗ3 *9Q C f)lk05ؽQr 8[sքtub dk%#] zuwYZZZ!2N5ɚVNFf Mn5Z:p*9j͚'Ykv;[K9A8E`vΔmv~8&{764iof#`\ݫO@& -w %\{V: z@ 6HPszFW(?fO p/LNm/fKY`?[W~Uc@SñzFNKG/Zsͧqv ({G2`KYYY,m<]kD KKK9^qs?o/#") 5 :rǎ[A re͂֬_; /Zj5H^5ڧ "9v-r#*Y]-gHT.cs w R%ujHsLׁ- ; 4 uҍO0˘p `f4G伞>-dFn}}Hk =F~Y<{ +K : [ъט (_+Л.++3H'h )mdE{:/0EY}# 5g.Y:֬Ypšvj*zBZAri4⋙!*p""z}RgB0ǽAE.\͊)+3m r~0Bk; 'PbBI0ѣ 2 ;P;eI>Pc{On%(oVz N5~+PbW+@I oLW_Vzk{N @^ fڃ Hܾd ɳ: =cu(R~:إ"1& qj:u5uh_K$ԳJht8W{j?͌Ob5ht~qXhX kf]`rrPא'AB6X@(-Jf2| ϭX1!g*KnB-We :{|ʂ&D .FmFGY,/4e`y޷Z`37S@06և WSg V *Z $ݲ@r~x|Dj la~Oefat=eQ= `Xޱ+X"d?mmc"O5Zi/ >C 4E,&hkQ,hGYϻDG5ә\czt,˩NQc-v밖V KEiҴW` K8!6DC󴜹A` $PkĐ| ,1 WKH5`6RRUw76oɱI `]`X-1̺^ g2&=ή2{yAi&X>08ؾry'qՀӇ,Gll+C! _ϼv΀ LX5kau -86{< +U@ät}PI0X'[߆a9L`80[_f&S%"U)+  v™ƭSkZ DV랞\>>9}$]@zD Pқ4Ah[ҷ>lj j~NXnXq6޷\uq5܉ y <b^VCp#˦ޗ_]@1IZ6zh x֯ H> ' Pz/=dֵm׀c`y)`G&c~$? dn1!X6Q,8e>*o^jw1tL`W3?n֬Ypš,ZucAD(6,}ܚ^߆_ z2 nVwr:8"]@H]cD=`0L&:"$_.b!qaiπj{TQ2јJ!L@` ~`+k,'jFP7RxK-i|Q!Fj@pm+ N+sm[[kFֱ (fdj9[kf Pw NiK4' Xޒs 57"9JYpš:Oⷷ#-Nش|9EC(LJ>9Na 9V$DF)Ifׁ4w' )?o }?+9dQ s#Ti̝ 2z+~ֺdv刀] {g#vw8(%:oc_clazs }Lf{-\LjAsd)is~EZY,3`Lhi-:ViG_̀֬Ypš,'°̉te=f:ڶ-AW-^r_O0Vpk;z^єtS0̕@vkMǙ`-` jMOgv-l{ 疃2Y>z&Z@Ds}(Pm!gެ4O+CecmaUn>_Tys*hn7 d]=)#-Lz"IkxaѺ,8aZVs"=V#gs lQ.uk?3VLU֥)Ԑ =+ooh<+ 9N-uz`F|3_kTx2kX)34)8YQ)1g03z+AOX(r(X*:{J`#:0dt_ `͚'Y*3aROF3gvThyD{ : Jfα݆a[" z<Ԗ8H8Pk hM44!k<QDa-4lo"?u^ FGb2guK\sJ́X[M hz_ mZ߇[vڟ' N'_ NV#sSe&{u'@ϳFs'-xC+@zP`aE2Y d1@@яM=#gޞ? V9A=z$>nPu@ ȐrgA z_nuSP%~ԧ`Yu:n{/tis+q|oQMi5IqTe<q8ęYs?W8*8W< kP ` c'/'6ւX]U%N@P2#lב^=k,8aZ Z ?26C`#`3X?0YҼ.pB 8ZNnKO ~"Σ\֡/l׀؝ 6)3yNI&NAK5V5<Qg`0MiIwHt}Vs&ox}]2K0]S _uwO"8^]ќ\afE@vlX5kk8a-}JD, -ðe 9οHaR N;pJd@@=: :0Y f ?LR #\MܧCl8x,/T IDATA߃%*P, Q'|`rAJyd><^Aco>jw|Ą-[b?[ Y-l: o]ͬ֙h.ؔX%0#3>$s݋^Ht&͙~@_g68xt*N{aWF۴f͂֬5xEk'ij+TLP1?T!y$ZSNǔ`on* [#u5~01|4D[G벏Oo!,?G PxRQ|׻5s<|XWwq?V@ʲ6@-~]Z8+P_}vҘ@2ģ.`SM5ܞڒjWGDوpK$'h, kB6޹\c!g) S`&gq6s4} }lfpAe4F̲fCIa$䳥FHAe/DuC۱r`'2`wUz0t&RDWz X]vxk=uΫ-ɞ78G~(`m ey\UPb~]6׽<d 7_2G -0Q? ug8secAaζ" 443޶,{RHFF ]hUzUU>ro \o's,M3k02AY.p7?*j {ֿz]r- f4 E]=WiPs\Z\|!Kt$!w R=?4%γɧvihh8A3,_kݛÚn9a#WZz̴ 2ѪZ&NapCs3ij,-`XAUh%MgMjl Pv/Թ6'|;z+:O0IGpE@59# .P 4`Ua+=t((y:;e:nkm4g@&<XkkˌeSa^]([Q__c N 6` A:-ttCH󯠾D RpBw!rH?!y$liנ4,zԡj< (gbKdo({@LH ?',!gVTWWWDݖv"Ȩ4^X`š_o֬eu>ȯ$-.5Ytm+PL3VRoSN#% b0^'Kׁv.&]AFLcAQ 0뛪5%Oq )0\ Ԣ z 338MH]ZLi>]@Z~XT=@ qN{d9ܬ71'XJ֒?ݏ зYd! o$\kM`Aȷ+ l':V̝ݚ5kl- S^tW-v0~JvlM= ;L3"-77Vۧ ). [Id6$8~o<n'aJNr%U`lW@‰ a %_i'@ 34Òdm"^eUXQ{jͳeQhy=O c@@7dтu)O㖺Y16mbcKrN 2 ս yW|4lG͇y:تܚ5k29a#5nz9h#Ld8r}+~޹vsU6t^qU|Bs)8hhud 0;V{~9M.IG* F߂_U {ZA {AͅrsA&A`jW,osNBmazӱ);JWnzkV}鼈`:8o`WrLKb}!=8! z6W%HPHu>m}3,S~P؀wZf͂:`˲M.,fYr~-?TܩӨ;4^/P Z㽬%g?n9%` ʄm!,y i=Ш蟎 H>LA 5wEC\ 5PD977eAP`+u׹ ZuQP {z^fAnsokW/mg, /` KO0'}(~ڨ 48׀-*{x"8XKQ?-Al~% Xz=5ޣgk֬YpZ2#(_kMtA%lUᆆ+c֯/٬dPNAw!y^\ &9^rr?lU_{kA݄xiNdNf4W.8(3]A/K*TA #a2Xcs/@{.^dm,_+ EaIIJzl`Pp!Cӑƴ4_#yzt0Ո`Ff6Gy[WMS+QXK|@`7`דn-p³28j,[8!ǙI_p Hq6Xp,Һ,OxFw~,il h]55k,8ai/h[fyEqw*JZ|s#^ϿX'P(9v}PPq&%`UqE)#C^S!顠O wLYG؆qJ$R\@fοA\p X\'q:`v<3WeV -򑀜,YOL=QHP u?9KD9~$χzj-stv뉚 <{>Is`]9S`+ ۜks܆ZZz'dHq5-V =HnZ$Mԅ &QXifmj,:VGǚl֬7!( Hw ַ/6fѵ= s(` ֑"kgJ9N9qєX7̮> 'Te6YcLJ1["d*pKP_aj3g1 c@ ׳M2$߲4ָgHEdg9~ 5 0L 6I\D,0Ӵl|Ŧ, ШϼMѫԎIW~ 7{hu>}r}{Μ93c@GpC"5$GL.? qC Wjm&0S[{IYcŊ'lDc YI1`(֯v}f ܰ9i9Q|r2Rq0r'R7U?n] a,S.` ͙zO ]!y3)= `,;L mae|~(s/=ް~>:B 9R @V)κo$*=nk+V4!6leCZ`SFʨ~3At݁ 87rrߖc<8z~2r(Nbu>r_2 y7P[ p`:h0dMuہ,<#'0\H,8o;44߲%ȁ`LVba:+Lj T2*} snK!Ε. 瘢&ţn] 4jפD7 MLoI[eO_nMru v)Ϯ7hLQ϶Hl-T87r@UZsxUuK dnZg "LtcS`bXbłV6hyOp#VR#[ѶAzr_Tqt2;iN9"}ZcS(^0i˓lFs˓mt '`n%!:OQXX9>`n=@6Gz&[̷@J`=Yq-d @_zx;|}9^,~9z/Q:̐Jb*v7aouvhSDՀ9pkT:>)ޖfѯp _}{`ޘiπLw-0hO_A B9gfo_-Z3:b)Yt7It(%X[ 9Pk#Y-[t#SAV٩`G?Xps;%`g@135YrtYK<ֳM1RgSO Ê+q;:)󉍓z=_e؏Di(ﷇ Se`ezQt)22o#mGd`[AB%Ȗ?F=&#ՑT0"X(v2ǂ7ՒAuxEM,F_AEI6vk|甞;vp`b=. %9 `ML. Z#]gSӀ @8 Ԩ#_n|Dߙ16^@x}ey8( 8S-ah=#r2'97+(lU}#be < 2딋z2'ZM͋v 0ڕ5#N[a:rvipzOJgaSBoFXb +)a/g%9rz]$$e4F,+hu@k^$,<%F%LUN ãgwkD?0{+܎''ʈ`Q '`}Z9 #y['r(GNuZYr&IVD8cmh"Q_T;rZsa\, % E] j@`uU^Էό4[GO# tQq02먟tXP:txRN2O<?z ]6Xab.pLVR/_l}YSr FN( 08]reX]୐<`Z;23F\p]?X>`;4M ܢSAX^%05bq*Aj2fˠ LE(LBRqtnYzhnϖ#+!Vc&.. ^x"զaa b=X*T{B@;VsәsqG:Mhiݿ 9ZzI'HŪ'%`#`K4/Sa 5aŊ NX8a*+ 5x}-b3,Gݐ| `HU=JǃHŗ(rh`TF>i;z 1x3e$ߣq_!#6N Y:Ԁ:'#8y0y7yZz;aLQף/I18V*ԖjS(.h:X+M)ׂuANSZ{~n 9s`!Y7898݊3\Z.lq048A̫rT jtz" $_ >_ ۤv,l9>Tɩ>Uɦ` 4_P,cB `lq4o}vI9b/6K0-aX+zUg'+&_`=`*VnUp[(MЕrW-TY Wfx~f^YpHDALL>+ކg G1k0'`+0e!ǹL Bs ϒR%1|EU 9iґ.H$.GiFzCRQw ^kNKSLzM 4rt`d+VXpŠ.-J% FΎ1|uJ ̑BXA ,0zRSxDaND@z`meq'~2_:]| v0z1H埉Gn IDATW~9WG)RS4/Wׯ@{im/8}Q{0{NDϧ)^Y-y95}veifuMLowL XPW^7=3 Eljn$DYön?3j}lϻG U/Nl0#8g5/oqn0)V"Jc8\r.%~ dH')Oa<{QYgM3|CRpn?|j+b)BVt*{};Gؾl3:s`f RLgUD|-'U<'4Yp0;0& 1܍O l֘\J1d 9MrYoH',{=?L)l,#cje zUg PR9d>G|+V,8aeQ̱Snl*碤ߋ_g8G` rOT`LǥGr,g_ A4Lާg%rVjSL%e.VFg2dUT1Ioa]B{ǰLJr8 (\ˀ(X>F7}_ZP=Gw҈}-z^iI rq&ȾJ{1ZC3Iz20:CsX7:LϮa Z?뜛f35ikW!gys?L{G$?pBƳ+VXbIJFv*M2R[v{3F-nv $le ut#olcxS^z-w ȑ1d,,'`K[e }k2:hq:OAJ AK>FL[IDl"0'ȩu rr!ǩ9μ\r$FAc33}pYU X:8LM2 u&27ZMOhXЊ]L5NEI#+ nK"`MT6{u2ͦS FU|"J{,!MS47ho 6=-An`X99I֝ kDI*5il]ǕT#u`=r"ߔ`hkn Yz 8'zJL8L@4 v9LOSYN0Wv0M/^}s/RTVXbJrSTpj{r|́Bhmd. Ww!b|]?Os[anYT6K?lc[ ınXǢRʺN2"-n#eM1bE! SarRk/R KrlrBSrҐrv0 dt n0-_KtAT ^vP vxzdOtNX`' "KެSXbŊ$x٩hWMNHoVX#M`4$/g{wπ]$$Xs٭϶`j.fr__ƀ/Q ^:8u@!S@Xr)j z߀Qn~vq!988߆瓐 {ZI8K`]ϡh?d DrA:wOɴG&, [bŊT NEf2~Fz]8xJ|2 `x&ňXFo}ʠI}ܑkf~gk>[23v K =`ƑHM˜xL! QrN4} V]vHyM{avpfI/;!%QNgq B58$aGs02 O_ k::ov`z9|,ݝ Ν7kR;)&V%XvTJ+{F+'kH/D =k_ )2)Aڸ'ts#^)P?5'ˉ8Ew^dW }a`i ageX!]ZwzOX%FmϕsZDw\Y;U4)BoP,RW dRp;5:K> S6k k`yl@Ⱥ(}M sM;7MYH XQ$p'zO?/ZGhR K6Xb +VɲU ]SǓvqd( nmCh ICX^v@[eL^sfM[cd>H#؊/0|E.3&9vŲ΅iAY 4ZZ"P&J_}mZ`{IXr;  0SmR}Qi:X4ޥ}d`] "<Ϳ 0I>K~!4N LI[U k8P@= 9 I?~?N/Di 5Wx^VXpŠXA6rtuKjeWC=42z=pU;B`D0v0 ce\,Vѫ-`92Vg#() %W@Ĺr/'5)l9r52'.6j[1n^!{^: Lig Ga0҃9a%G |0%_kVΦi[Y(UW{@:h0 Fq0ؘDPg=}'h%X/twiN1w `܀ς|`,Ӝo^D6ح0M0,՟N:ς,6B~8SyxTs1#*MJcw=vc}hˊ NX Fv GɈ[ F'qmS&i#& Z3L+eL̔MW0ʴ'XL^9.%77e߄soFd}AZnWVh_0xؚI0$l,='VȡMcq̫"k,`iXmiV$9fj^ibuU2%z.U3aLZ/sf֣S"MF08LUN<{"`:P>=HQQngȹW}2XZ{tA9OxPrTgE_sAbG\ɧ3lk9-dS'aRXN~^YkmU:d7J+fŊ NXԁ>Y`a#0j~ e*Aj,#a9hN`9KB lUg3HL8H]ϐgrs9(N (gm_-^ezC;0]və)O4Dbygko>n2 ܫː}Ws#\Mɫ<$3G,bQG-n9mZKA@)С=>9SR?;s  tr׾Hk!@q̵0j FOyw6S`d}ajx:(Iٖ2?/~6L2LTW&uH0qS=σlթ& 8yU-AIWR`xǘ35VX+ 9!.Q\z ,rƁE W03Or%\ꬕ/tL@z 9 h)vXpo>DLLf?OLNdnJ<dTٿѠVkCF#QL{2֥9A/={trȀ2<L*sa5jm &NYWuWR4rg%y~TAfYn0xRϸs~s"% iq^E08(}dH+[VXpŠĀ*p:XpCr9D쏐AU/'TR{Sln.p:l8-#Nh2^dlޭptP0u. `jXnlajv]U`TGYQ2ϗ e S%`:ڃ^Htn- ۥPF!z+z^g)I=ܑAE`!>DG"s#GgieeW!H%}bBP Rnk).؞Yྞ"0dU|:ƀivLYtaL=jnf()Kc 9N'owK⨟s8S$`H VX+q j^#P_`h#/ ?`w xﭞn`nT9s(@z LEd<0H,RPkgdfxU 2&l Sn`Tkje62ԾL(if[JzpSk. d֦NI7bqA![+ (SGl["+e a/DQ `4M1N:!8?ǜ9[m;|)(Ef1 J S` * 9N@` cq2@0.[ Zר* NXDFEztuME.:Afp`-<dWW"`EC/߂4_AT?@;aݷ/ #@& 4Ȱ˂KoJJ5.Ic,LIWϚu\tьMarkvZO8HD׋e%ZDԵFɮԞ;db%yzz 9_knG Nt( `:w1M8h8{-ZO}}~`~0d1XS隥H4L@*b-:H9TsPrto.S ZF'i EF mPHݴ6JZ &ڎ߮?S@}=r0oc] v*>ysCpt„G:FF"@zU<,J.7̰+A&`*ōrFҫ%ү}':̷˥G"ft,\ϯi!;h"Kvϭl(k}N0s:t?执, 4ܬX+JH< JDF#"]52Lv0Z`qk+sqKU 'uS.[~=ځ= ykJw_  { y,u'0 &o0nR}]a\uuu"C[rfF@c"` Zzaܻư dq̐iRNNݨX{"]"6k,\ 2c1L԰,5BjIkO'M39`|?N?hkNW ֌1yGeyYE9Wv$ <|dz!{;fM'͛ͷPYYiw-FtdI#eqzi/ $?&=9=ƽ3Xw'}H S{)-O,,o+0n3:i>h (_<ժU#JϷKuu8l?X" +X˗1plr$#a0R^h&bw!{U`T8C~%ҫs)E6])g,Zvja49kG]%e4=` =p .nRBvO ey0W훳W5X'.[^V/S#~Si"@{ϝ:mi:NqM/\jo+VRYґF2T0.G{4KN`0Ҳٞp =˱] W6K1| cgdc >K0.OҾ1LD{Ԓ0j}vN,~@:LoX LiC)*jYbłV ZníT,9NSKc>Y ˢ 822Xj5}-m%oC{Qex'%Cm *{*#lƽ\F +4]y܀dn`W֌)Z{\nϠȮYӳ LzZN{1?gAZ|ei&d(0l b&|0z;[{>`VM:,~ dȀdVe:7& 8@@ItĿ@&GI"'ʁ#)evS(w}ʵ$ּb.m< 7x  iEd;l+V,8aJ48񍀇hE8jgLͅa27A.ӢnKǙr~q׎-! {`HiȒsX2,:n[!2{i+hG r:nv Indkw`54%2c2ͰYWjJl|YZ'#@f|93gQ+Y[_hW \C)(EnCJ/ ԖixR"rwoS`T8egQ!'c__`t4<>97A7i-\ F4Wȡɒ|=2yGIdi;"ovsb DKr \L-0lkv:.{/a5-2`nq@vF|2 ' #P!`t0fUF@S-b9SA[mp9uLt0U `K5R} 9N1Q|c~f" p[5D ϻvU`kt5:-HNM9Zlb9F/If"yZ]l,Gw^iMF7̉_G#zt7jl=:gu)!ݑ(,~-$)<+]FxwH(HbII lji; L1*Ser7ej?2Cg Zgv~ ^Si l2\|Ph;!_)p"i=gx~&\:KY^l?x vD\*˥㝃Au9Quw\ϭTz#$0z + H9]~}Wp:?s&2 Fk/IG@i sJ# O LiPuWᣤjl+5ρl >@/`m6sj⸦)W0sS``> |u` =jŊ NXQ'2~&Lq?ҫZ: B*|m!ϡf)/K=y F|8(u_ R#}e>TjϚD3jUH2#&=\`^uw Hy~DJV9;nAK71`i vX&Sד93AvIe?>Ԁ\9ǂG/ u1^I#fx]`D:KD@XU`CpΜ9Lzn HQq]Ϝ0W]5%K'53 ૓}ϒxfP_W*O2A.שQt|0!2VX)CBO4ʈ&gf7Ctf `e< 1hɑ7ӡY8FL8! 5r9rB^ c㌆H\1eWFE1R(ḙFr&R]Y1$ AJ`gLQrukQqi!yD>෩R{rjjtʲ֜]L-jdw<$#zuDn3f*^>ML0# Wi'%``ʶsͯI9Hze[BE3#tNihX *A9J-Sky_XY7iJN:o90>6ȿ]e]bCrRDl0s u/#5)4&Z]kH݇2YgN|NMq br隭@`ƀ=d`D2&C l\(G1 @Gm (RG&S2: |8(ic`JDoxJHih 2FMJc:'h>R#q͗t0J\LgRϠQz|#ǫ|r4h{_Bs X/`Mx'8!a̼XQQ;po~3灺^NŭdT $/d/;, LL%[gx@ԌZӋ5O5 P< k{Sz2l*+bU/Ǡ9^JWuh "[ʩF88b9֕X)#[3,'Nckt<΀9v@g{LQe&& XE~n0` :FQϾ`ǐc4ǷȰ;S*c^sXH0ʰ"㲾p"Ose,su (b6^2cp5R۝  z̜M׽2 jȀ.xBGGQDzo,?P{"A(pJ]`t nKs? 9`~:r@&:Svt~9+`͞4}5[~k@pfIYJ(fйIw*136~0h;|Kŭ9N >8 >_~OqXԕzVXpŠXq QoiS;>QFTt6)Ս0yނC5p#\&X7ÛʱL Vӹ,8xKXOH-#FPX03c`kz&Wx|>ϢKJJR164_!1)S99CAv0McT 2!&k, `~2! `9Ii6ت(`~,}(p"[rǂP 7F?)]FOYQ1otcg,p!y[s0L'l 45l 8LҽO:`z 0}%dw2L,y,Dõ 1JԞaaπJi:4C"Ȁ4͟?s$9SکT2LXb +Vr>[L{p+=+ˇ[7"W?+A\W~\u+E%`QH+ n2i +2Y][㋃dS@ig9 4=\PRRRҚ?SF}'3"Y^U%pf`m0plH/{& P8dieyqT=@?MS9 dpuYߘ"ɒ)f.9S1a%yȰ8fn pY%.Pl@9:~ 9C]֘1`ȏ`0`  ]9|-/`Z)6iR1[:26&Фl!0Us^ n.`BekAg1/ll0gq q&Z;U'?9o55{)w[ي ٳb%^]crb۲p[˭m]{tCC<"C)#ۚ@u`d2pV&|BS҆ϓv#Gtg7>`x}Z'`N05ʞt=Bzǁ.2':6^'dpA:0rR,~[+NLP`9J~k<k̟dD;\9y~JG Ӱ@d Aqpk+ 4;Q>Ozv)-rLVNp-DM@0 W=]+f&*L\9z8յF 9*TI\\?^H_=Y UFki~@:ZY?3[$5/ &Bk``O/gd+l3+Xb +(9`"poA7u:јNϐ!q0ٸI`BΖ:^2 IDATܧ.#͟&W"Lic21O`n]t3r`.,!T=n:\=sac4v0gvTn- J'0ͥXceͺe`5*9FXhr(Xp2Xg 3@|Iͬ~ i.=CS/20%.k`0眜m d1F­IYlLT].h cwQ}X+pI 266W,"TĒWw d qj 8P6z&!"X3bx, Bg`[׊ NXl=HR"kW9z}+4WP^FFZӕ]F  Nd ˗1zK"2@:Ȼnd9 }`͑T|Zm ``4Y  5\TS^˴p ĚⱵZ|hNrdl)GZߢVɖ\9 -O3(e-'`Zӳ N5jXlc:oLgn;\]*p3L#<݀s XhQ$=[`ó!9L8Vzg;`8ۃlL~?N5BcΒS\&I_9+7[;}֌OkJzpw S5B^ ?Vz8A6+}.6i| p_ aڽAVxd-kbw1])ImŊ'XU(w 7y#00l8\0/64Jx1_bh+(Qm%QN28*9`8,k>SF6`j_&v6%@k)_+r0w*e%rRh l}(qCr|s_Fز \U@=<2@lLm"]PBdLu._+gW4vނ[)Jb9݋ԃ5 5xW0{lqZu>(=#?de&B hk, ȤyMÚ>QzqX;1d6{gE5_Zsr5Nv3WXwY+)fL{jL#v+05U <3<(P}eŊ'XCp+~^Pt~=@~6֡Mƃ@ Ql/LHE+t'@@ s榭z1x-ֶeZdEb0L `:i-SH](Z5[LXjܽ6K08@'9# F>Nd%"`Ƶb^!'( `{nr;U Lko$VL5wp ͐ ke { \IQxΏ[ MY9Ӫt *NZU$H_ m5/GԔftf_ hLH/M5[N ;ob7 E8AH}xj?줳sOr3{:b &'GϢA6O:LIu )0/4Ok -u5rσ@eMXb +Vn:*& Rޚf ,Ɲ5`jH=t.Q dN`L{ X$MM# `2 8d Xq08+N#AVZrُدorK}#͌,mSᴏi tyrnS yd, ^#rrǁ,(r~c6RƤNNFhC-c&ǤMh.#\ Bk~I@iLGkJV ^9Hc\jä8Guxhpf3׻Tuzq0l MNz׋={*ݣΙ O Z~2.*<00BWzI:uu+`+V,8aJ\'`VH̭䰵nES鸑:OFaa p!>b' be^RyMF5hA %UMҎC~.?nL{}|kX~ Ffg)^d- _ұ7ݕ?Nl|EMCטONF'Az`>593D7`lȒ~ MGX)@aK L]=TfHM޵ ťhcMuJL3|_0 @_bs{iuIfǰγ@&7J>q q@Z$2JKIw# JWDL5Tem 6љ\`0<0Eϟ_~y~x )"dVX Rv: rz@53u&[G#XWD@Ke.6oq X 8]*@e0.B^ ЭHrsRn^KVdNZ--0TlL dI&~ .I\[s7CqN`4z\i=KW'I_ F۟񿲕*6O*Z ^BGo 0Ȗ뒒ml9E:co~>5.8⇃*0e0~@AC`t8%1d0lDs <:=kѽ xW }`e2BD~{"<:ZYۊ NX:}OHF.*nn`Dy`qNԹF&~li DHUOQpD ̋=먆B!`T$́9 wۃ,%`VQ3F2:QdL+b*PD?C`M}הL[I^ Έsc)T%}_GIrörGLD|1OO)'i~0h8CWϖTB `I,Ir]:/8%ր0Xp| uuUc(XHl?S4} UO'F%#G:H/---㮻0o޼pXUCVXpŠDy1d >`'n>mXTp(Q%16NlӴAYN/r91~V׃iI*`wy}"MhJ.g`Iq02I Rh7;S\Q VP. [!)X(#$ `WCp֒$vѿȖK_$g3S8m@@us2W`UҧjjH~r&ʡ=n":t .ik-X{/'2 E5%if?,;gAw/lLs[p CHTU E$?GW3 O,Nt.zlvn9XzBQ-IbI@H,hWپW!`33O?}nZ$>/剀rsvz^/Gj ~)|Qƽ{: dRtg}q? ZJWB*@0-]5Q]6uFɎ]: <]@¤R ^-t=tnX.`pKM?ˇ+UGVXpŠDj9'QT? ȃۚ^=Aiw zBSIvrX`v0 =*0&&2.[a0@yÑkE2Bg)p%8ݴLe1&־iѸ]z Z Z02I#מCkddG<,wl`oi8dcã,6~LzE`G+{NJ~v\uU_W콷E`WD"Ezҿ?yMIf\\ !ySs}sNB $l},B?:[ P8O|Y#igyvC(uП@&&/a Guk|X񽌁𮤩hYRFfLqIbxkim;B/~]P0dq'7^ N1d*-IŽLZ-KAyͧ:jl~,cY(fM $' $U9Uҝr\K) ׻ }Lu pbadY=)Pj Y~~&ۄ<=ԁ:m5u8{d) vd(1-Kq4x>{*e6Uc$mS1yb?-cL]cd98$ʊL~"iqXYuh$,}1,ƣ;8l20ws+41 na2`=:e[{{akKo79%JSGǬ"=e@\),@Ҭ8meSd g{3izUXu7mdSy|%ԟhr`K7Y:ۻH_4!w0Zˀ_н/K^9C^ 4 PɵH 8H )ˁ2fY@Lrd9LRA2 Y$u8htf=@Ƭ>{d#vށ裚kDdu$HNV9hA~8duO<RkGbYz{]Ҽ&Ю;y_ґIFK@,"1\׀-Qw?%c.I1u>JxJO sv2k2`tf?ʘJ*^h2u+ y P^wY)KRƐ>@g%QU#@+:i6Qsjgae cHl|Wz/Kz^)m`Oc}ՠ?:ـ<;ɘ**@p"@Reuk֬Qj'PYtX,cC_>͡7.Q\VjKY^h31 -˫^+ r$:c\S_ᮘm"G$[<F%2:xYD뫆 !$?)f&BpgK:[t`6 ;gh,{,duem -iXq4Z_jn5PaɓQƯb/+ O>.tc'Ȋ,ul uQ:G񔏿Ȋ5VZNӕf3M{*GX3XO꫉d,ۋv2Y5tuĨIûe}e샗d5”=;@X 735O[dimpw1nwԱrig9l+tܗ $$TH^ :pY9c2Zf,b_F<9[ ?abtf)$(>]1˙#2Gd{$23Cw)Α:[o.i2vĮ8>þ}ˊn(/J]ǽcDog`i&dt/еJH !;AʏaL <9{Ȋ]񽌂isS,w,ًyd54正gD('`O,?CV(55p`Z y^Y^vYlٛKR~~c#;l `}cyq4zf=@Z$YH%@YT$'#u3޳B ~3+eQeӣEhcB+`Jݎ2qc&$8]dﮕ$ޘ^< IDAT#IPs0Y|Jta+bNHT%zrd,<9U=M8-V]T0UU1dϗ>Yp:F5'o";?}ɒEvdk>| гḛNnyU»?Uۺ Do<;"K=: 3w(ȢJk(~GWe2ԞRI?<]Y{:tcKLy};LD^^#dzDThHfbΔ{-Ձ?+Q(*rw:Γ)KǟUt@Y$`N.ɽ2j -+uzsI/cbۊz'chʢHocLdѺ0>MD[i]H sj,m;1"Y280`Nպf[_EUVٽ;c26ϧ5 8~ameQi-Ԃx=u@:z+u탳]x1PvDkHQPºz~W3&y>0;UVPV hQ\e>U p&0&߁7| Ș g8盟'KȖȼ8G?COpl1XV3&˵_1'q e;zǁF+_xr<DQ|}wZfƜ(iKwe.I\˥ (я#zUts*VXKNU\UMMM8 BWUUX е(J?J>3#e5T"F"cd*b=8$ !(Fg5ѨꅁyN)@͸d5%EfkfJDx>8%R:VY&8|F6PW'\eK$VU,x8g.:ց|<'xB ή 9UJkp >V&TH,{, YNmV>?ΡZdP@PcD& /cnRNT+Yw.T^0ݭW֓8^,DxRY~e//$0駟_kb6pBPH 8H k/]=UuӐR,TUFoN99_V+ݓ'v4:1k#U.HbTo/̝$kh YjIGߧ؂9 W)`mwG1wz;U Mi9PSdEAG[,7.욘 @1V҈Z .s[.vC}Ju-δYmNxcǹ5Gyi³SВߧSJPp#5yqlo33')*ܩ"W+gp ~"KI.iPh+cQ ٕ41Bk;`e}F@"0$L^VprKqcd1q d5>Rb6@8Ǻp'55$an{1 )GGR{~(}f~H 8H u2HYC &?H!2*2jc@0F Ytv_Yx2kTy^Y*Ѿ8g}p"{aIWGץQsdtQ8yp\ٲ(^/=q"BT cp,آ 0Jڡ)$J'ih_&+f7]y r/j N`H+}eE8I*Q OoOTėׯe۲bՍ'Y譾e)|VJȆij('cxU dd/K$MKSaNtv26*3F@g: {_!i\p"@. b.I $.0Z KcZaVsO1Za,*c*1;A.s{konH{r_m#b]-%L-[lQ>0JdQȷeѰ uil08 /{ J ֵF#g.LMp'*Ŗ8Nj%E.Yc_/ ot W"NG^N,l>>@H,OGp<#Hwr:v/K;EVkX>g'4\y_vH3ơ2I?-5"5KgT0T@\;2WJ9#de>\mq ؿ5Hd]*H\\w,_:jsLcL UgiwY] J38-yޓ7 *鉘*_:bY|Y͕FAdQd_Ɋaθ6RcXn%iCQCHV#b)Q}/up, bu9=F@Y@Fq:gY~Cٲ2$dѽ]))\#Y3ձlQ|[+Y]/ElHc$]1˒FR7 B Zd᤬Jpkp7sksBRS&SPT}N\H-{o+e,[.Sd>0uiY[\9*U{K`8p)kemI5Hyov4=.Qpv43dY2fc8 (D_F_Gq,U9Y51;U.;X;0d"4%⟾qq(9獹fsܧk h4U{*4$hK>*q־BсYb,rc1S#>5铢Ҕ2!y*J8K3ˢnIJ"ke:cX |$+]FWV^#5EPbk4ֹ ѮfcgLhi*x?BC GjO}Y3~H{ʊήwQh:'.;NL|QZ?~:TΗ-03}ή J~k~~S*|ck([|KEX`O‰{ogzdB"{9JB ȌyuȵI;&҃#;)a0go.>YA]eAmW9zYJp&/mQƼLyFW5RGx"cE{8Ks!I7<ٌݐy+z s}6Hٟ6I 򬬀2zdsr[_\l E JC,޼8(e1*tiq4:'KQϗU]Ɓ)MCyn1[*c$cx5J#te"cs|~(͖UǯhX ,E)q"Br;eQX3Sּ[[IGS0y8Cee[JDd,ղH]2yu+Š*ʹcR,\Gd̛dER9SO]ݐ!t4)$pΏV<;Rhr؛{o.~s~5Z`-,MG"k/nLݱ X<>^+ixvzpnQl-w(hʕ:VmdYv $$n+ʢɁ,*4_8_--52>Y8]? +Ԅ ^OY.sT-_!:I%IW a (l6c"UP"$`b ` `ks^S5 [yb~0bFz0j0̓T@0]^6A8N IYme.W"8Ւ'ߙ#IgH{~,`n6#ڽ'%5p *Pk%*a\ގޠ[VV6[o3όBNOzoqƭld1Q+cüu/Or^pLE@Oe5k>(j= xy"|5L9\Vy{2]DϐVeee섓OԜa|H 8Dǩ| .4ytNk.\-+1I>[P |? 1˷2FŧH8eDj:)) #S #}/w}:Q[c`"ȢdߩLJ:LR46ΕE WrR$&b5`4;ô0B%su1B89Oɨ­Wrܓ1R;Cei$̩H!8=Ūmeu >4.Þm'9 ذXFM0b zh ٥=c9:$+2|[%E$;m%cl1Ʊhހ gA vZ#նwC\.;Gsޱa\1CqL1="۽a0Ͼ"YfDRM(zGqۏ҇R5և2jJY ^ HЭ#uI(KC+鋘!h4'1\d*EX,K] r53!:ZUòt">y~|zc$їNt|ɵ]}jq/0XdX`|x,TmqP+J?~=ư PGևs6J^L.^و!*dD5s眏c7NjCƬڟp utI_<hO`YרK%p|RV˘&ee k,C¥) l^,8xg}y*v0r7s3SHy<;8vD @ϼcm\O. j ]8xK,n@96yikd[px1rrpF"Y6Ioe0lJ bFݷtՍpFBo7~|2 o ˾|6+U@\g:N26<* :t15h$XXΖծDóW4AV}?a2Et=J-e <޲:?6q<cSwq7E_~?M/~éI5flr`"epkY u뻀_r9W _Z81s32ټt n}0]t T\̽a!MA|޲qыjH?s'—ѣ$Iw rO*)WsEh#ǎLH liCe $' 8+MuvQ`/ /#gP2[VW+="8ڽ0~wxA2Y8_(^ H+yVۓk͖1*ėn&c$.KcF8Iڶ=P(^{z* x( `S $F@%q= p$p">.aU]sJH$ԚWFJ[%L ErY$ߊIDƮAJq+&93 "Md6}2vD/Spa*o׃L8BltQ<-gh!`7dj8ՔBKyIjY-q/f:tFj%2a]cNd>N`cM ^n"F|z%,c/DUBM3/ |Y7GuJ K{25Nx%QgtO8?EG]dEWJ6yˢ3R+fj1ϻ]?DZu_c,-k`zC#pb @=Ύ B8蜉)ͬgtxeex^nyyyH6hD5ޙ1*& d퓀9Ⱥ$]ehHE?Ƒ)IF_ټmsT27'I͸ߠ^L D 4Rʢ쫂!dE|? IXɕE/`)]d#80]UEAh֚)֝e-1Tޚz}Z'Æv p*Noϊp&}8?QG!e,z\ d6{R--gj%k+IGqdUy)KQ cdONkr-Vkp&H6fHݍ*#jgt*Y#2 \0bSYJث6gfKWIS,íp D 4J eс|eHByGb_,4NŁxY.h-ȤMrd, m r:O2CeX=eBU\?w>]qz+ 㷈BuX%j@D0_Mu(` `諸{P|'OՉ c awc=7U xKYs۸rqG =8U\qŁ 2_{~GO!!|gJ0鎩eݻáh.iz*a5t%]ƞGkg\ڏ1XqF֝zl 1a1$`Fqů7->3ߎbvgHMWY/3}='LE\Z4FFH D `ɓQɢ!d9 Wkg Hp7P xbP-Tt "#m{c6t R2\[Q18⚹| p4:8lQցhV.QuEwlJsFx" o8.Ou˕Q "Ic9$!cɢߏ6˖c\!Wˢ7N %!ߜxsHg(},9:GřZxX +-qD4Ǧ#odC]Nn]TtԽ1{?c2\}uI1$82!t2'2ิ)m}Q<{_&3ʸ(e5zmq7p9BMHEE3 d- f T"v9>+[BwrL݀(΄~M8y<I"S쳦8>u6. U~--QpAδKp!j~Xug8M8XĺXtE %5mO+{`4kmz0֙fEy{| WZ͟S:$}ZRo߾ >yޣ26C qFJ>y?Z7'eO^yދ"x*IKc}Jz8m4 E.-c)S-NR0q:`MSbKtɳxȡskKh D0CEl R׼TYHl@ I_<4Ye/zx!yaT,/kv ^2˓a QxZ$Yodl @dB-\1  sޯ8sg=ɂB(Z/Y B#K2i/Ҹκkќ^psフZ8;iY?穐v,ڛncX0.YAƂ+cH fWHcZ;aRV+E';fcV_f>If/irժխFȊYΔrc8P# z| DYߦIz3ykWwysC)2|Ewَ}T!)05I} ENX\+iI=wO'/H\ga2ƀȧ2 N%AZG d]et@p<TV- fs0z"^QRYz?$}Rмޔg97ѡ p>bm (xE8IE{!"` S"0G`,\k;Du-϶–|.h2C2Z8O8u)!+'Z+R<µ}GF]pN2i|?[VS)ta}8 :)|{EطW_\AY`,SY,?/ݣròtiW kcsKg%?<6i I$ݶMipX@$T-$M*awlxaʹӗC ,1ɘ-19.FW'wa}0]nMVd2=C{ceiN혷ɲ:/ڸ'v|'5p܆{O߁UXGa섞y $@p"@!%( Ct2̅8-Rq7`uENEIa$/C/r`ї(L5#nO0&QC;[J<'LT]v S(@L՗ JWU"98H10i+crT\e#^mXp01bTKG#c4KbQ#A;r(lO@dAeY@{p|{uřnMDZ^[1@+LSp( (=p>ނ؍ΐɤm&sJ)ye=d5!佅(c 'lK3Up du:czMijDm&ߖtT}=J|=ԝ0G(SnH>zU0NuIf<֗Sjy\2f`"W+@Y+$Hd]EPW M ݿü}l0ya K+YN~s~&|cwcL0\5FXWYtGڵ,U i\V[ad]wdOp>]7T^i:FH:@H[R;CV1-0X).)1G~*VVOc]UPѺ%]g&¯c_l%Ju\TK `>GɀGbINNB;)❘罂KkK3$͈y<ˮeR~46yor,HE[tϜ 2&!26ZD2xI2UPn26q@@ } =RO{ɓe*YN*,mdC!8@UuC7*NZ (ˀ95 $@p"?#6uRM/ XRR,e.c-˖1(GU~H6ije,<7Y+,z,*XNX=B[ *5irRspS!#8S,:}2_&-kYG<̄|smwj/ ,HZ+-iv^c0 EϒuY(s~ctEަkO:LLVk(YsQ cvg̸6żk  –dl6>X]sj|x٥m<`@7YNc};b"T2cڧV'{3Xɷd>x$FHȺ&'R;F*b7d4s@ c8|(W_|mm0uUXg "0ʢ.R[.K3YƈK*[,:[a k?,y 0drC;Td}dLq_t){Z\ql$T7rR8m~ *KSk F*Kw'(@>\ʕX"ms`W4~s`gVX{s6EF׼t%sOY {NjAT Ie2z%쵦A c uLRuٜ  |qu3VG2߅BjP D 4\`B>y[|򨌎ׁOG m+ מ+ ciY1 b Msv)8Gb Lg+ (rȊ UJ9"apO͐" ïd9Į3HR9dyC1{|Y$EQ"Y$WIÙzY <)e8BdsjiS,G}G%=k;[""߁ü;S{EKO{9&rΠ-e`Y4,O e-lw}9[s}YƤZG;Ȋ1.1ӝ8 e _dq2c8 cY1T֤8d0aMe3e7s2S-}gÆ8G%3=]@`tw{g{ȒP(=v}UcߗGc49g0UERǗ 1\d:7s#;f*H$z%9Hs0DFt|>y>0!x*Rcj[%V7w:SyFki4;E) 3%Yb(l83),cEgcYny ztjYm/c,Ya&>q?P<|:Zy񦬰e&~e:+Ēpyt\IIɖOPTRRGH1 S l(eHz1{%Wb3dlrM@؁nLH0'i. >\WRTFߟ#Cg,8gb|VsJ~{݊AZA5^d>ws>IITj /d)ȢjdȐ?gÐ6D&1wyRiØl/o2#ZZœ}ڟC ]p 8fr/e H*Oh*E`\(V$׾8ƙUy,2V=0ZCtk\=fm,Y:X#T#-w:LE2|#$,L>KR1f5IFxSWQsF8(18 sZi=ϳ,kE?@pZ/+f؈d{|n#tΉM/[y'̚P sA}{yRdG]2كjҤ?fܷUu[ $' 'F Y=ppP\J'Ay2 (=nRᢶ%84Se"gdճ?WQf2&L'2iGWqٮ8,%8ˆ\Izl=˻P/X; \;. [F|:1^0[)3yc"2]YoOϨPhxD R5QӺ3\ Gb6{5\eQteEvISFa_B:5S8U[a8-ΒEyK(y^)_q41۰/Δ48ȳ v~Sީc]1s\ݾMƲ)F|33pC;r;r 8o%e\#דgX(c o&+-_G)Tfgs3e^Gw{r}yqa-cCH$r2Vkϥ-(=D*SIYFYh4EShUizŧ{q(qFGh#qh"Y8m4/cf<;4z_,$"_+&"Ws^+)Z{VRs9{f>=\ Iک8~ϒއg.Xq˘x<%KVV{7\wݻu;/n~S TmYs0k=II4b%,{Xr,+y󜩳 gV=sb{Z ;w"džl~%FV!@ɛWbڼO0zVc$$A5`P[_U\?xnMz0JU(FY߉Q[ $ 9r ji1uEaY T!-cN4fnfqpQýop?Lp'K)K`gm,:r9vZF8]vW1ZVj ъя} m';xT0So%#DReYS3zf}@ LtqiIz :+8/f)l_ϖIͮrse8Z@uCB1ϫ yWFQV<,嬘rW?TVP3}y;}U*p2z ˺=GFj={#_ԻqhJcz%kp’v,Fg>$X6s{31?37(Fgޝei%`dF½Z1(^b gkzYL(:Yt9C'tKIm6kAzZS(bQ_U;H0M/bK Q$+r6҆R39/Q6lv1"#{RG&qy ɒcAO=4l:*|CdHwNr9hU!\Jo%ѣvňq}M҈#G&M/$:I3\]h#OVNjE`_hSu468Lgh\> % CF+0dQ"5<dkrH gaaXd6f4ü[&egJ4-p+CU Cpsko5F䗲t,rw1 SK3d7悗1k}6: w,s|,}@p;;tǷ0Qb=mD;EwnW%+xD 8q?뉜!Ygj02*ÒVR@Ƽ'Ξ8 U2V?9[0N5pv1ax_b׊t.˼Oڡz4\YDz<~b~>:BVӪϵ#/eMdu){[5 YltЛN+F$\3'vJ%c>׀HM|s%77+e Yqt(p7qeLY{++*|?es9gif;m^q{`s]oH 8H%[o PJi,r,_`W-D6>'5ڮȑ#m~Y98rXɕEĨ,Iz@VdoZRXh[*ZMP`J`>`0N|QƘZf>'nO#y˞C76Oin;ΚĠ[*YՄz,rXW3έб7cM]9J]Oy.ԝ/c@BϽɺȢ8ތ57@F-ss۫g2_<_<}zR4'Y#]y[9+$rY&t@12vhԟ9OKs$Uh g|#0Ws%+)%w-VR 7EǼUVS3"!8vYnmD|3TKfOE6'89ymqdNSM8W36Zmv,m)lbQWрcсӹY,~Qznvms]dXs58/{]p\in>jdva@ͨI 5z59f9;w/?Vhf"z^߁\0V`smR~e@ijbC>֚2E&0nceu+60uIdH@k8GANe쏣Aa g$@p"FEmYѦ!yceȩ8~Mt"P἟Ws:z-puOR\7rȿc#Y!.~ 3æ5eT(a:ƚ|;s1W0Gw$kd1`,.QE:lX… y+**I NnGWsd,ʵ B5+ߔB ^ȵ'Z[S"N< ~(ga ;'/Yۆu?2鐣uKgq1xI2PEyvKX wg3XkḋU8Χ4d9vHuUU(֩NtuU7M2es`x9:sFE/l>l s_cSؘKm1Cƅ<[O VzgZZ%c\-c!oAfs=YV"c.9{91LE~UUOVChTdpd&v{)/hI#vء~׷ݺuZXXx@;26]،{Gy" w];˖-;Ƽ@ }] v؎}<쉾~ PJ"pug(6Cd.o#;eNlPCbS[1@Va]&':Wɀ`s(Ϩ7}Z9;ΏkZ!3;,{|*++{rv8 S 9u؅.\%{JxcD"@ hf??/6p,N2vl~1F.^k2C,B#r6g#a#Y&;c]*;6AA-К%Jh:zwY~Ah^ٌ9ߞ$u*8 Xq"eHb =Y\K?b0o֦kqѺDGKɈ]kdnj"ԃ7RIbvv4FY[1v"Zߊ 1mWY8SҊ61%N_<^mN dWWU=3ɕW6@_PVWN2BO!'K! y?mt]7iڃB_ b+ibG.u髀3B1=|f,Enl+[ĺ] Xi$ i,=h-_e)& qF. P4:rYz@3Nֵ8|9I%=]]Uk>\V9y~睟tWߧ?>!|_񽚐-u"~ht؊+8aBc==7ϣ'%:uf.Ww%3oah$\Rs4_̀.s<7Iy8< du.C*c1ey-dͳ|y3#9؋O(M5#;Dvt(FX^ 1 n guo29$yO<faVKK Nβ2 pb~'eE3QTXp3#`y0p[vS`c(X~,ޚ5"\۽qoY#8{c~ɓ]ٗ4fsp!_⹬1""?1Zj%(]8%sXJ$ʔy?yWMȲ_odo.D@xe7_+pP'&ߖQ@Sd]m͗EɢkeCoeGXnȞ#ǾxTƢ^ ^ ^Ii?:;Xy^u:](חõ-pq=ptC]06 6H29Eos9Z&pƧbLfn5Myp g_ X }SԴISS;>`_I^S-*^ˑee\!ceF`~M  zW3ak~t)oȂT3] s^\՝Yg5]ws4Gߕ aBNvIuU-or DΑդPqz>{皱C3W+@ <畀=MȎȂsF%w`~d]dQ듔\2,ݲ(U𒊊8@ͭWpR)D=@unReGrϒQ]NHwhGnpY@㺹l-Q w@v$|&8ʣ_^`de2 Ņ@8֕$ns!Jk&7S!N +`9slɂ% pV oVJ/"qJb﫺Dջy N)jKq^eM\j].Ϊʮ \:x,=e9JAϑ8F砫zc1Nt 9 I^}8r(;:N_,TT]UU^W65*t|֍_#iw^r[op&'Fjljjnljr:y1h}r|{N^ E|?S<O{p99]ۇsZv yO8l3xc8]F.%W߆ӷw\vN0D/mL IDATjY*)W#/_9žΓr͗W~Y^<E|oq4BiHKY4}@|L03C-׹AFOu͚sa=\T|c.]#3OdiA0%pإ[0 #+@i|*T.aOq9|`8ϥ}q>ﲴe* X|? |?C22/|c |J~5k{p58t0޲/*O4Kq`{<돜D"yEz #s"Ox_V0a,Ra; ?'c3u]hʀnp5,Y'ws1WӾȦyN WW8\cug9$iC8:/yϮ~,HS%-Az>+K19^ّ'#rH,/W/D"^ʢ.+&ahuEW5Wձaܸq5'RzhAߡPaNO_o Zn-pxm <|Ο{"pE>pT^aR~W{oWo&Њc 质hG:#\1Y4.WRfIW`%SD 㺅ϯ'>IԺEOur`ig|'s.g9>2F#<8!o~v7-Ȳ28-B8*FT0.8}eed:$fm6C{窫d" d̆ dr_VUU=dЇ12D b@?wUu֒vܱ,p, W>VF9k]&2r9I/.gs^[P;^~ 1Bȩfc?!>c9gek2DhXsv`+l=fuUUp_mnBޛsu56xEV:vm3-1OC59ϧ18\-8g},wm1hyP?UVyNsCG~ >9O׀-G8H':/ 2,qi-܇ܚ5{Dҵ;9g]ƾ8.R̾؛PG a=Kp%jqɘYt*to2]@(7שԝ0 5\&{8{9@%0bVpUhFR^ y1ZʨWˋY%n^JPyO֖8J1O:ej#f"+8N#EgǥO <y >" IS7͹3 8z)W&wnz4Ygq˺`.2yslY$I,5o㲴耻}1߿9Cʦ~n7{dd`8gl0KPwdy9ANQfۣwȕHN^M֟rw`װ05A׆s,H^ɗa~ǾrvчqLkPq_}]:=KexaZ硜Vl:;# Nl`+/rz4<+"T2XƍApNXF]VtBL3 L]edt C(Hku8 8OaG[8ΐsehomY\Y0xRX\ـY4PUWџ&g8šð\Q_KXsb#11P,/ѷ_c5k.g ^ ?W୔WV^MX !O XO ^8o.}>j_gfY͊ddup Cʺ[^TS7dYߛծqۃ\}zLKKCE9'ؽ9R* NܓJO[<@]{KJx19}@Oy3(@xG/OIbQB.<ϫ Hco+ò2Ʋ5dlCsx@ў~,3߻N*Ȃ)ЕSowxDv(;# NdǏ8re2zFvЪ.ͣioڢ-Ǩ?CH菵q@8݃KqH1O^+ֿ, 5PFY d8i!BYTJ"ʫ?IUp I/VzXv23.G` =x0>3 q~! E}`<-,f90ĥT4א#[mUh- /W^KUEO:˫]Q$/%1D\7kY,u3j- ؿGɨ.vWb\&5(W4'p yӐo`4&Աj *]elYtA<ټ%td0YZH.rF^|JnYJKk^^nSQvd1+u9(^X"dd( ǰ=Jpeȑ=| `-ǀIqOG~6I-, Ի08=κ Du;Y r ߕ(FkԾ}/֫&$6 >FcuTsè'k`D⫫pdW 0nM7 u- ?uAAA / K5׺pdZ)hSPi#uKC)pxI;쭜q2#i<Ad4ZDey84 b!tYD"^Tr^)6M19>2(8tV$Nh&~g9|\ yMeZNw7R c)5ƈ<{u;K i-Dz]+K}\m7|dصȆtȯN׭so1[3#bG)D79I<.<_/QkfTI2K1rcJ\{瑃,y>1T2F~Ty. ~g~=EqTHѱKJ,Zl-9q@ړޝs΍muwr.%ʝW⾷^_Nu7뇝V"Y1?Qk]%c(gҍRLɊ6 o:I{]62Ȗ( ǓiND'8f[t@ ,[_^l':݁LĹә̥7B*;# NdG9=08c&!^ι,vcX$7,_3qi7n\sĥ\YDchr>JD'YA]뇈`Y+ C}@3}UUŲU"`P7c4'x~,2Wyg _x,ҙNK8R(?z3}+q΢Hw2 뜗>E"v:>k_Yv)΅^EG3~ik-K6|Ys&ďdn.9ϖf34ꪪS XG ڠis8OxE?Ti8ce@ *u|},"?=~ωbHٖN$@ ~'Dnhr[Bꀳq(vU!W.${{/CFDΖ< (^{௝=u)F2Fh[3|h9)#d]pXƱ#83czozuD\8gŜU0a![*m?g82›.or%Dj=1ސsWiO,љsHS 6VRbsS E_3EkؘeL'%2;CՇkKّ'#PF|%GKƍrmBSg\VǴ N(i(2z1 σh=ety>CSyh=1Hn:d]-뒸N1[c$rd9nN~׾8g01^xcMy]NƁZh|D4d|dȒe醩<~f5{+shi>gPָp) .˷7;t=:KH)a2fXצ֦=R{IǿHMk.E\+ϖY9LYq2zdؚvȚt^^<4+;?/qsi$ >DѣGB9XW8 ǖgnum?w]fMWI*++k:C)..<;,}f6N4||֗3c. NKvm@fk23;ޛȎ,8i.:| ]:@k&L*ϒ=d}Ot (E?V|7N2ð*;^Fy|Cd ia@cgMQ 1p"jšPW%e$s";1aR" + Ћ95bOE_ՐX a-:soˢhMý#7GR}}CA>8ie8iЄP)5#w5{dc- =pMHeDc*Fd> ,<wm8)#{f_Wpwav{~.߹wvS88G֍tcXQ>>pgrbjRĶ p߮sw<op5c)`8ӿdOOF*62Y dZ^tdd +MVj,WAB[lk Y=GÏ>655USSSץK4/e̓/%ܐ_ش Cvcg>?*''g/cMN=CMOdWk6DU!!K^# x֝ʎ,8r@v`Cw,Zlˢ'11,~;\NNFwH-( q? IDATHOXfk;;B^e8RE? XQ'Lm4tQMY^~"] Pgbxb4): kRq4ap{U3(sI#ns-KY+@2 Chq2j:,qCgy$| 0Qu#H`-=GYȂ%JNKe߻0}+2I[j,r,鱶@2Ֆ8Qrm߿"Khbg¬ ^zGZޯ fC]"cB50ȁ6ɜtÓ|12FУ)-8n5gp-ꊂ9'ٓ\:"0yYZOW||駛?裷qi)lV^ș}YfI /^w=a7Ijjj3xgݎ 8]U{Йa$k @g%r52Z@=Ua&]vdG@F%8Ȣc6}P!TF;PVtm5,(¹YyE2(VQ͖ `1'kIx2n0͓$dKz*G\FCZ':g(G"RW} +Qs>nX)*cF|K& X"˫6fΰs/dvX^a]x_ϹtY4x*ٷ(KطGsAͮSbKeu_Q/K)oϽ݂ pű1[, Wx˞p4ڳwGɘaKNj8o&sײ3jF^H 2"j+Hgm83FD PR 2dd[$޿- 2V2`ATp26[c8U 0sDӢwޫO~VSSӳzkIoFir1G8g3d >`3#ȱ\599Svd"c^c``ROK\ sHO]+8q>@̯J'1ua@N\wv y ?\Bi藕DJH)/c3~Z/ \jӤ7Fw>JpY삎ǾB^ge)ّYp";!|,z,!q8Pߵ‹(eQ?Qi;Eʇɢ߲FS={n;ym,0gL1‰ev[l/k-+Fe,eEmIs,f}x20`7Ȩ"Lk}ah>'[';`"GG]z< *8Yێh_&ؕÍUUO!K?:MFHJ΃릱@ KZ'`o13 ¼_KpŞܯz Dwu؞yyץľB-ΚkeOYڽ싩~;F:%2l~:~wiC 1FxB3=^n}Qt[~0s896ru]22i H:R~vf/uץg L~0C*KKvcG솬PF;0dew!W?{'(HAh]왢'GbLP͎,8IBeȿaL.@ x)ӂp N Ԯ8YxdbYVk,{л7_ƂyL1(~%E.Wa,1P.Zt,jJy΁.ơ~k\-o*"p1}Y,E~-W-yn/&ceem%T'9wh~x4>ýz[C5Xj8Jnހ2=8{i('p.UuZ qL,@vۓpxG~ fM4{'Eb+u,kKؚ㲫pbwt,B1D "jacK:$(9w sayUqg[=1Vѯd섍CՔxDx pE׫da>gyZw:tR[KSO|Q>`y"Ȃ\mY׬gC~sw`޻1 #1fcfksdLj{Jlv(|gP̷6&.(treGvdH`4Cd | LϕE%`b+Y0!Yý>@6z({[Y䳵~+?vEziSY?HX`,:p i&Ġ^#wVl>[.2p0v9di>%7E7 %8*뫫.wvG+/sh%3YޙsN8q0T)9{}m%Kaړ?^pb QR8< P,B~ʼnE)o`]fŜqP8>/iӪ#Y]U'pNS r(_^Wx̅xN8Byc!Gb^8gQ벛`>4\"iRy䃫/zOg79V6N[!NjӖ&uAhI|Eߙ}|qLyz{/1yxnȀ| ȫKFڌ@~;pdc,q㑱cuUp]]3 'KXLbFW#3מwP'bdlP^KJdPy[k&L,ţ<79C5mD3G .L!r kɈuK1Ec@ ` 7Ũܘg3Ye %jGQ zDKXGW,b0] ʌȧan}`+Kn26VC'HVYj^aq\oC9+se̓8m[O`u6Ts"~"c@t|qf[2ZkDӰ:93ysu'\+[2Al@Ӎ؋0 ;,AZ{Iה\ưOX~}mg]d?AŲUʢ*uV"c (cSo6 }c:X sP\\S 6A2V+(c<.JjG"c%cnOowf~nv+z H5v##t];# NdGrVMwsYD=NV+ƕpTEcb`0"ߐa i-E^+G&$sdO5|]d%q-dDp5uixG֯=r! ]d)s9'Z@;`JOkħE޸2d fzC2|Ki vYgq) }zLG_ƴ鋞 RVm@O $F *nQfS*;# NdGP##3Vlo`2`O87,8m`-@.Wٿ*ʣLY G,un=ZFGe[0ڒ4k0bX1 B me2YÇ +Xpa0vOtQܓ0|KЈu8gdL-9RVQ %|ږ1,c E7ca|i"ξcx,b< *N=#֮pU>YNv| Bgsekso<]R{Q x ="|~(V]UՉsy./?󐝧sf.=aʑ7"bՉgB?r8z(C;VdS 7'gMq/b7ճw;_k`l<~$ J,yLH PFRSTPk$=ĩ|SpFZ2g@.{oF_.LJ|pȕuAZ5kJj, N44уyP<3*dd9.nš{G d;֥- -cS 22YYL:h` 1b=q'QyWvXxq$Dq~]&n,WuxY/1Eڒ!tuǬ?}LyD>w,{ILȎ,81eyŇewṗ"LO qc0.cEq^*>w^9ϼ}Уg9mfvYbSF@*'D2z- mM93o=[+w]d v;/Źwd`n˫J_dIng `Awo-c$u$n&cBNx8[C'u]dEwn̑>FQIM}w{֤Y>"@N"_PP hMaKöM(ea#etdWQ?Y¾y1`S2{my$;7`|} ,yO7j+:EHʎ)a+ :gW'g$x3^ 0xZ2VX(pF}l22Qd&;`toW0| 2 @DA\]> Z'KѸ<x%.Ȟ\:+ V,͋PYTuq?Mo:@6e 6#x梏?&36bs9uɀNvsxf36K 0`s }|}< 9qnmyFp7Y`-V\N;#8ke[Ȩ=C6@YQ `reGv F9a;ڙ8ro }wpD~2 ư=l3kQ {?g+Bk,UFIXF]|7C_eTɷKKKꫣBȢ @i")L'z\B>& `"(覩0 &/`9?&I3ƌW5/D_%+.{Qx_c f\*c`:뽐iKYˡ;etU/"k`W槜+%TA "goI_É Dx/sdpxe+2`cd  ChcP^DiO@f4!UUdq˫kT*rb]9KdLFuqlʈ48'%lB]3IB8hQY. 4ˢy t6 c><\FaO *>isVKn:)gd7R,=sxckhozDX#7S^#*^˳jIN~y 3󪗁ׯk9W?l7ҸP)XgGD##^gM2ܛ.i^ ȑ~睂`7]NG^:QPqhH&ÙYw-;~Μȑ'n>Nٿ\dݢ7FA8RP3 a1 IDATVa>B;5̱NjnѸUTT4 2'p^4qYB`6;>{f~V֬/xnL֪EL?>É>%Ƕ$J$ $ieaDRϞ*vh!nѾ!:#/4MYt߲mcqnyp Z.>c?$`ݝa[&c4Y=0]FeL1ȅqHgdT0HK@(`'ZGp()C),UUEs7g2k@ϝ'ir{4ϣXH I90˺;^M>1њZFw/֙ 8cb/pݥl,Jظ-v_|N9e.;|Py8%w?2I[m7>Wo04jeGvdF/1q椠(dqw8dRyyw ,Z cjt(|O?J#+**bCRNc SFne0B2ׅx>o|p=Fsx?%-3fŲ(%NQ*UUT 1GnHy8\{NMЗC+Yd* e왞8b =p{jSd\;:LƺCFYt],Y1IG6t8k=|AF^R e_c\yGH?Ӡ)NOӂNJ8%c>*c=fb-N;3Y`9/64w")WOwb52pnku#֯6AT'tצi(Sc9' X 0ab\ܣhr˂EO ґ(rȁ$pFb SJI_ތD"뮕,86Zt;,Tj),AA0fvdн,* 8Eù/,'9k"FK^ɢ+: 2åfSY B5 \92J{9n***{J; S߰?}Əb/S"K^cƌYcx 7 ڜux]VsAxkd/y;:" ʧnr9 tE2z9Ra(tKǦF1,tu\NȆȺ>'d4gha'f⽗}.`L̛܂1^,E($P#Ow HmϓC :n(IJdeχ*;|~`r|Xց9{ }k:|9v[GF\ɾg G6T ytӝ'tYGny,63񅤚pY]hV"lݓei]Bv, 3g jw"kvvgGi#a,o3G^ѥz0V***^Mrl H3_#/L(rE}dmi|wEC2fɭ2JWB.hׯGsM01 f%Ysኁ~=>k'^2~YET|1čy{>F 3dE&(m.m!(hG`4c ˠ|YjTSge59y>yrscaQ )U듫~Η1f[!j,O^kTԍPy&[cF.v"WͿ?(\^@ +e e>ҟ+- s:ϗN95q8'#[=e\B{jƀ pVY4 ƭ3(Do)!q( WwlQ}1`HJ(Ym2Y`aaE'K{%3,Hs \.F_f>/Gro_,8ّ'6EOкQ>i+xB ajAW3t>; wyv&zI ߵKV\kr~q tX{Zn^ /Oh8Q\7:Qt!| 1zr1EVTT4WVVhzǣJNsŕ>瀔͚m."y$>"KTxkV᤬M(c <ſA}<:ƏQ{1`Ҙ1cƼ|X/GQzu˨钏 PQa3 6b?;[Fÿs"P^]BB)~@u//y9}7!-P_P~:}ˢid1E{ uwN<d2DRUvW{FЧ~պ5{{ F}:!r@~ԧ##18H>+{ۨ=ӑvS' \EX]UxD"m֮hyX'g;%K# ʺqI@T8$@bz_@v阊LMZBRݎ#FpgDߏ"j:!rqgdLѫZPYmm2!Kg[%ػ+F.5sζ͎,8~+E36E֖!aHeJ}e7`-NO_$c'4A8`F K1 9_QQQєZ:]Lkcz!(&\YYEQ<,{,A>4^sqEHbO1oiEEE `7sXu‹Ǫ(u99Vz23׍dѥӴweO<0~<eq٘1cZhg}Z+arNU9wbU(hx Fw&d92ӀT)#F\0fhO:y>/9u`wTs,bNZPV y[pk-̗E.pč-1e Ae'MI-'{s}SP?њ= 2ľb@xEx5kN~6KY>=դuۃ6jO*e‡J zd>^'Iϙ3gW]eYYw|)vɕ6S5XV %_' K|طIɳu9Uo-! CR' \̙G9쳝.Xx[lE%2qȍYjTNݪd3d,2&K6DldgöB2 Pl[Ȃ?q,Rp,v=P ZI]v=]Q"{9"Y'N$IJ"v)2dŴF,H?]"8'Qݗ?yET+֮2+~ w|=X_ɢ1Ub?Y/B2J2F+5_.z n(  éc8٫cH(` ug=ߝ9_żG/^!g8g]=!K9y3h곬mPuk`<×)2VH1#7(+[Yx1X0:]$l(ѺIF8I΁rUKX R33Jp["c٢i|N[ah˸_?\jhv>)ɣe(^wyC>zplYTYt~2c*-cü^u-wٓQ0]{ *~GNM ξ/18`D,]gx\=ҪK\$FǦcSB15 @ ,I!/@NB 5CH.Y5bz1`{eK˖ǜᾺ^y,v}˼3g:@O_PLa1Wcx5gyγ>}r罼tWo^bŏ J7P.NNE<\a IAsh}k1 aTi0b70MX]æڽ e}w~$.xRG}Jz mtnÔM@Zjo^ Iw\EGdn.$|s?w2ӊk\ײ˟_Czz.?ud9\_Cٔێ0RtCgʹ>+.8/d:{!yC}2 xiptz]$aڑ~-TlgҰʅԞ-ʘP!T$,$5`>$u%cL 쐩}K.s+8_Љ·`X qu$k3S #,F9ؖl:%tyi4Zk7946lHgp ju41lw irL/~%4 ioIARs=-z޿ѵR 3&mt֡,&W[wpmnjT;>DL):3Ő(]Wk'9 q"۱ktS|u#ok)YaBqoBZ@ʨ+.0 X`^ t$]iOx1+EL3>U~,e$]s Ʋ'J[Jv6 6Bh3 0tv7@TT ڋ Ǯi!/ w}[O%Jts#xsm?߃N^:| IKKcd w/cm#G:GAh-qUp$}b좟P /x+-0R W%BЀq(4\ ƞ,ˡ¥QӔA#Ѕ'1ZG5Eu`3jFtXc8;!QFFܻ\ӯ! 4s=NؗA=[ ̀u i^Lւa~Gqifjr 8n2$ѱa4vm)6 %^6N%<#![9&p++ y!8؋7hZe.ZE1)jNV-QdT8w=ض޳ Ľ1-[3Z A!T8v (LVLv XRC):t`[ yB`ַؕc 0@nSVS I7ީO0`rKi4}=J`sMYsnDy<ڳV2v-Z<VPofu|8֓I#vZtmjW\q^.PB@6 =L Ud==|,$(2?䮸}HDdAitNSPe/yW&6C=M=(PGCyS*9F U As0ŝxr\4V껨љ\LcH~b2?InPiZ[n*rBc l%{_ 7uv1)C':z^)>_Bl!MvBZӘpuP,52 ϩ]:NBá,ufg톢aeŽFkIaȝב4` h*lԉ`GVZz.ޭ~6:xj#@U5դvQg7F&aJ8/k̬ xmUm\[` iA]c>cvJ4<N)fO:0l|WV%M8Hݚax%29Zۢ+19Gp*s C+$e`=$hQ.m)^s8&pkT#aR_/Cc4f1P "`\&_4gǖ8Z|oG+6xnhkv4 *kas/6@2HpdGH77- Nͱ tZ)~P.-YbE1Bvz gi|yioktQ_b(;QRћ<>hѢ;Sj!-?oq>!Q8O@"ckFt-h Qx  Ԋ 6w sюS.zMUzg=Js[aP `z[0>_JCt $tIYPGLX+h祩ضL=~VXYO|7an:?<`Y$e$TߟL G!xS?0?OD`0$Eeig/& ̐C#0sKYJoBE!{ѩչʅPGjr+Yr!T"y7?d_. i RJi`RpwbJ!6+ѬVk u.C(Z,.EFCA/$ry Tk.K4'@aW„YiӦ х\mC @nA+Gǽ@}: -2:_ jxA5S ^5@@hPr@LW& uԇF1V yD!;7gp_B}@Y$4hvtD҂C5JvRX,8D1Z0!TEj Z-] , CV1aBOi ز®s_Z˔ J,t&S[N `gh h?"i!lԦW@^]]Jа#]h3 m0$-&2w\ ҸJ9~.ˠ7r-XNبF>hb>:5Dߢ.U:Sf=p1R]%hL}lL}1n˱ j<{FF y-Bl8-&R0#y=/s`qt6@~R_RlyqBC")r,I aLy=;]RH&OG\H3!Hrmdصk>ծ aN"q(.♱vkmf_HĖІ/u$`灰n])-O%Jo9 uބ I G x^K_s!'_@ 9p=Va\;'|3lq"Zz8P1}}p0,̜9sӈ|>#s$TnI$_10dA|e E[t87 w\M)Uʷ)6cE8!4MUDmU>}xO3s,]HM9Ǭ Rw~.8we  l"*#xp)|WiO V$4}LV I|'`֕a$p+c6kŠ:v[V<vW 0sh ϨzN~E, Ͼss4Hzx!7F@Ҥv|±rG:jW Ƶ<:AjXh.]w^Om|J.өىp'lX@NZ6u|Fb(Z:sr a4pMhiesє w{Ca3!-VGz%ia3=\%I#pxeB^^B_3pw$*x'kɔ4xnrc ; V .GmqW\q~$T,hxB^Ex /:-T`x1Jw1|Baxtl~6S-AMxRMȎC^HT>8]|As!'@:WB0N|*gFr?"maO)Q <-=al Hʟc\&h ^Cy |?Nә˄cJx#!@ǟxgiENB}f͉4-y㺚 b5,5ָn/Nۅ:eh P<96dS8t጗6CtJ(YpR Xx ;S k!0XXll݊s#O8qF򕗗 >/Nt4)0ֽng| s?{ѱz5p7аHN[> DCXX8Cŧ' "H5hǜ nN{*s9)4֋l`iʑװqt~l؝VW_kQ~s<|dN'`Y@M1kc ,C,~VBkGJ=wᲜ qxx>S_|i+4b(?Y.ǨeOthmuuYG ) p bW5a6qD>vrHįw iD]kET^v(z{s;Ui$ [IGXOΆ0<=qB4^ 8El`$PZ'C;toGTWޖ@,m?Hr:w{ggs{XVs"UսaswDԏ\Ah! y\smU~w:3sx5uzH-\v{pݼHuECt~3+`%4_1>+;$H4~tSxD0<vϹtH~Do= qw3$8Ǩx$SȮ x +D?|W!omh+镐dFxOCTχždj-u*,n;®a:t%{CXI_AXILFs6g}>H*ϵqp-%,ROc9Gs영Y_ L38:S hp!Q"GCҫhJʐ,NՌ5"c1c`Mxd!쒞j?,gAhŬ=OW&u4}oRNcu'Ajtlv!q$82Ys. fbe|vl!ȰRl~h˹ aQF~W#lVKA 4r=?I4@CM>hu )ceCt]OG‹`z_a*8Rch<YK5i=C=_A /p? 't?aN :H=Ql$ND?:/ 6^.sgUtfPEEh ]F!=A#<45E jB/̢Zal[_c4^H|s L/ ~Ew)O q 4bCwAF6V!jK,pFPj<5+ʸVCr d[Dc=pWZa\ NQ 9BT Aml6 su,^;//\阠loc Ro;s G] P!0̽|'kX\:_uv詅D2Nx{ ! (t%$I7/YRǽ9;19:YdI8”'4bMT!-CGr>cF>ӐԎ@Z㮸+qV؏71 R"k}m{ƫ#ùi@I.CNk觩74Zk> Hx)8Q(mt# u $9[SIhCY͒g8(`a1[|5fc<t^0:v(c$B7< B=VAۅz)Qΐ >??uZ4!_H`yun-vםtbH!nvH]yOdHg[q5j dAPd4uCA: c4@#K b lփ0 upEk\%`Yss,ot/,~y:8j?,0,'4>km iGZ:[Hom^t7K )0 ̅K)K! h`;z8v.FKHvt-{T! Bmof}>Œj%>6 S?G_,^ Nb)t67\Ca07 : d($:ZI@h-ĩcB7`ht8a{WD1b[ Ѫ+uX@(etJ(M\KƼ(sƹfك8gaj\_ei(.Ϫl:X:K=J~BzDK!Őq]O*H1_zBc~|0CǢ nX9y s$ng:;ҩe=hOs .*3MNʛ#<'Gj7o~RsfcA /x'm7v}HY@ݺ6| ϗHS:`Y>ëNxRJ!ťgߓ]ϋa+M买#|3?*b{[/܈!E p´!?n5W\pJ$5r!MBM1u|E[K`8iw} ~F(121vn7@a`f(x ).6BS,CFp @iJ"JչQCblƌvp)5;ט23[rb*xq֔\|le@F Mf@yѢZCgڄ3=&0Zt8¤"Aǚ$X:A~W!E)˧hpG>4mȘh`2n񾠱ý>@c<ߢŋOZ@g)(9F{!lVVK;6īs}nS!`Y u=-˚ aD 31`YTk"HzCh|Q B#DpBj\ aDlzd%rl Ź!2C?Boc]st3<Ͽ?li'Hf"vȑR!m9H )x;A tdN8+k9?0Bk N҉ PI~E%]:HE<躵 8(-'ʫ89RHdwId6  x>E abFHG$J7`L. L=h-d~ [9er $P06vjZ9_`mm\c^$sCQ RToD ?ǻJMp/|i\E7:$:Y2<Ϡ3@+%QOD>TF:зau⸌͘L\{p4ILCe?sa6#"PW'J M| *} aZ H Žh}tb|]9 R,\V#vGARy߯\atT3QH zu:Lt+.8GdM _}ɖCh@lDKZL@u=:$WDq6\Ɏ@"j`%8zJIkDlo!) _zH#O&]=`v^4Btpδ4 Hu(jTh^Cw8>u hg@~GᏤCWe}Dg.X{ 䛏XLu*Lg\';I'MU~}Ky<2Pb/sjpSU}eg)ci*Z6TQh*i^:.z҂f:e`xq5rݯ14_3 )Y?F`N)Z&}jza 5g$H3lc<Sx.s]=3\AAT]kg)=x 9usqR HOR q aP3syxC@ LtOcEH9~Bw$Eu6 ^HAW<7 )ƚA8 8:C"t$Jp9AlԜ!u,K@C IARxyy oAJRH)V8vK!^86-;f-p(QɃa{.)X@{#)2xrx2) u h'NFZ/z'A-3#L3h aLnTog^^+{R([ ǐ)GWzx!HY@`Qje$j:iT|;56OA"]+if9Zif͋x8 xoug!Q$>cjɂu6Q|"2-u t^JH.dH Ys4,~vu־}:~:Q^^m7< FX PJHQ8:' *gRJ pߞhn4pT0=d4\?@;VI⚹*|w:95I| C)c |[9@6qs½tJJH$^=Zahx{y?͍tTŹV#i*˹ѥ蹬K vy<E& ME⩗Z7bX/C8g׊7)P¦4|fNl EOZκ N2) 앂64vP7DR¦dkiL,$p6 T3Ǹrжڶts1;h ~f"WW2"eBiAfDdvA?ğ<^+_ IJ7'!ugDf(K%2nL13M6eHJZ) Z)w:dPр?~^w'8*@1ٞF A"翍`ߧ/ާ8Vw5u uv^POGG%$O׾^9 :RtlLW7!9#!)GA͟Б '+n&v;l[* )"!LzHHނ0\_+ IDATxh}>r}7oي9{BjT XJpbNdOHJػeaңMH]ڨlG!NPs Hڒs0ޕ<jjH :+h8,JnDzjv0b^Vw-0.md TBZ/;ҫQ|֋3'9?$Z3R0pw* Q dd$!aHiKrʓ  Cl?v<`!`:w&A:js~.t_Ggn6$#LuPF 左`k&d1W XOh^_o%@ߒXuHg-k 0E`{~k0כ Tx>؂)-t_ƽ:*hCN:ͳ!̃I  n<4uXwTM0aKQ$9D5 | H'g x?ڦEoxTף0~=6{CZؾV&c^[³+.8ѧEۍMAosWD4] QDu,:!yt6Ao2~8Q = ^i#Lߣ9Q: ~5VtdЉ}+~zW1N:FC ;U)S[,& o'ѐR/Pg2}RT z-z4j AT QL:}BQqiDTa|68ms $E#{tF6ÎOg+8s8$ SֈӍ/|)]S,y:+/yQCB=FH9/~nM !u}tK8QavL~+Juč/bjEOgv2s 9m oe{ l9B_[ `S S {n!@HksʹA\O͐ԙ4~څ'ܾOv NCP1 蕦H-PɃd Cv۪ az='q8k8@S!͎{@:-J B!n1uҒmp-Fߥ3V7e;,2/7@a rfIyh!Pt3 q? Vb90_j![ _ A2w#p+$R|-Wczuq ?|B6L:Z4y $;uFQ <;1 xQe$Zt[ S;pNM}wMq2(k@r~Ig8HMUƏ<`ӽ|7Bhܭ8F+H5\A0eHPZD uK|c{%v.Ϲ )!y"lZy@'&6ncRGwqL;-#s!I iBb$N `G:q!c JD :`xJv[a7Q7Bm5Q HsH=)aĝH=8!6Wwh^0Ρwq5sN>LT\-;=QUTq(ߘgB$q]K;7!}m_vu,YKݱ3$Pu' s:o-Sdw} DNy9NÏ 4Mu1i8&&Bh9tm>:8~A49(!9Hs[ t,ݺ?pM1~b[&v[rH]hb֖EqcxbS"ʋ0>cj"Gr|C"AXQt3ԕʶПk? uFk6{9vFpO~B/Ἕ)6ꅰGCRr `\@-5]q(^CsZu)9zCA^=õ9u<~֯!u\Dt ,LT]W!s'xy`3{*|k~LgTdqܑ9v$Ѭ1lÀ`j߿4+DaF4ğDC m=#yo3Љ,"8~ FYc: d09{@Y`)ו\A"7ܛJ9nDkCE5R>> RN#`y.Z_;`( {smC](]Gu< zkmL L?ڃk,r=?;d*o>3Ե:kC)iݮq:;N4Bh3y+849=X;}LBpӦM[}D<..!~$WSHD6Zg)fh@ns YiϘk^1uL&% kߺ$C CAꅬPvSXAZ>BДjؓi6Ko2m8 LpsGwD$cJ:xiT@:_ԤyO7lU> X֋t6T5n8'ޑ[A Xqڵh= Ѡu7BO`|=[eXtVps!;/Df"Of1]Ы:(S-1 )LHgHp '?ݒ`3Fë]vеy+SG+3S> SoE"bZk7OrKy)CEJ]AĨ^: D s_GQsy^oN}yVl"`='Nx !Z sĵu-v5E D&@3|v!-w&h\1 ^=Vq}d(eNL;p:PuvuڴiS8 BA<Β*hё.Z3:Tf@l7oZF8L\]5ovø;r`9`˦e<uA/6S> \/wv8H HV3V S $|F(Hf2m !Qi1 (\4$$U/HqCXȺdJǽ4" ʭ|C䡞) %fxHǟUht?~Q'R4발ߣ It؊yA:ы! 7Gj >B;N]):]Dz #:vC9:w5&c?7tt0hykpjԙi]@S\z};'F3=UFq>\ֺ(3 Vhw 13Up X-sZܳΣ!!treb}V}oA8>`ۖj0.ek#)r! Hpdeb/K fU Ϗ䪎@| P+fRg6gCRS#i<ͿqS]q2'ⓉRdž6mYfy:mڴt|"5hV~ ՖG#v6AK:?2fvMeڴi[MɃDȿN!ԂZ!wL8t?& KC@Jȇt(3XZ`e@r Nk !̈E4$2hED He0oIC>XsXc"8MzwHf!Bg_ Ku|RGQ' X?nƑF^99rHdhߟ:a3$:a59w:IFk7rmS̄CWj}:td#3So88uS!@j$ DlEv2H X|I'N9W-ܴ3DB:ax*q*$R..(QD žϒ{ ՞u HWh:G0Bk}@b=2@:)(@Ӏe4y !ůZ )6,UZ5,#:C"G9zPS5M}>$.8Hx_%#x^l ivދ[ Z\Q"R߁D X=>O9™ 7`ܬ͞9HsMP'V:2Z90Rln1Fl4Mpђe$U"Y{3DrǍ|' /9$u\8TkǤCg H C4aieZrgnZ a̶,۟'+>H^i]) 5/RP{{,}oC8g|{u\鉛ɕ !tC[ Q3S@ґ8ηҏk!iTdĴiHd:&@-6 3n6um󦠇Vr/!"gɁTBq Nh4g& ˀe ~$;[ ~Ƴ/J i ɳ idX~,waRVBィ ņ1jY{4%g_M1WkcoJ; y,8yY؝y}=fo#kߘ'ro>HPg!0]Bbˠ@Ȱ6e{%4RyHy`EaAA6jedO~ϳڢ+sy*/R?<^s3o,d #|}F( 5j$0Y!2fC*ݎ2|iK*בا#ᗭ]LԤ<|}v騑#k ZKɊ .xp%{7,뗊n?y6iy,|Eւ:ґ##P/a.-Q4p,4US _!J'vAqZB=+ۖD۶=etH-'";7"YsƿKS#AN]p v]%-o")c1]j чQ J3XRY @RWSNeA-I vbp,gY`,/D.CO]0N,?`Yp񀾷wI,<`?#y*7,D@9% NpstZ,s0dŁ2]yO9{0z睤ڝd#C06`/PNΏn"iHp?yWתy#+ g]/d+s+&Gh Jpg(IT)Y~YE^ٰCDmQDbڥȯy_ITY0Qy1l`&q>"N K~$>1 Vkߎ}=D,GoteWD}J:}q#s8XVhdސwYg=OUxؚ7.M >D^ S$^tsi;+eyQUQQt>RVVGΝ[z-v OxѲ(́4yiC%<ܰ;˫I/8ڊuH$󘗱nZ-VIYR^KHRTߖEN,l3"yO3H,hkǪvF>s2{AV IDATrY%i#GcgH9 F{Y YZ!*q蟍ԩS7yd:BP $iO0 ʷqLYQ"UW7zB,Pidz;]Yȕh$]0x7z\&0퍪]5㱟̫?]wyȼdQۜSS {L`OvbYd'ER@?9@_ #Yּ`P = ` Z͚dGdaAOTЗ˫s,?#F;y]䓕cQIsbtI2~sӒ򖂜^FV O2F6ȼM@'oE{$!X% VתL:V7#!C|tgytCv,wC.Yn9r\;xB>ǵ?-5l]!K+zL:H\d#?'rin2g{> X^ۖ0 ,RslXd!"64BD^%"BHț[.#-zO p~2{2g/x{b$ȉV1~%eZcE31:ʼZF!ȼ (\􇺢(!D n>B=w3IAdD}# /0L4PvF wEE۱ޮ^/~^0X ?o`Fs=|, 9d5#Z$M;T _.im40dE. NtKJޮD̕Μ,EETsӮ始 *c#ɼI}:kSr4`P@ ݫUH5R&y@6d^%/)Ty2ok !FrkhVߑ^ӳ!^V`%IJj !k,{*яKoB<[6S؏ SEs|+j#)1QL>}sDW8kB_͞$K;uid"c o^²9`7 tײʯ&WB|c{ײT$ڧҗ3w=Y@},g4=s"ֵ; tG}u2KHq:eLoeᱯa' FBR0n +LԩS+{'e9.Bb-k㈐}0+%5KM* C,_uFo~^tMC|_0ovuaAAFYCQC{j90=>:ZdwfOH2`x/^΀pd,5 /U` ޓd}XGWMo!zZBe^3n?juCt !%jET0'OȼL ݔ@\;|} $b<Q17W'~Ox2/β(Yt0pl;p,Twgz`?D+j#[OG;+C^Q}<5oHGe@(IV7te #iއЫ|rq/l7dd!%4ؿgZzÞu E6?\YQYHuU(ۋ>];|R-߆w_VW4*s"ΎH9#í-wcdC*F`^K?~, 180(еE.Bbm XFcth DS;uK:\@toYn`Y]^mDzE a_|SQA͒=e)_#x5s0 DheFFIž.O{Y gHdQ vM @,r%` d̳.u/Tծ-VNg>7lc= R=zkHoWz?@L"tnM^0XQt߱o^S픾{%K:Fr;{Ks|,J{[8:s+-reA Y\YyϓE\ ITHEܬFNmSّFN},-hmG#ANlc [Oy۰'h(a,c/ ؤ(r#rwyq!Q1gi"mIR[0:`vA\e^}.DGB, .k!p8iP@y] B!xK#*|DI;\_7fB\lnESiug$Ea,ⷀ$SIwsh-槃:rYɣjݹc!׹ ~c~,=- #^̍ys,7j {% 67? ٱP= GF6 ϳ${>9 `>OE\aE-ٿ瑊mF US6sLu;^Ws|@q{gakMȑ9`;c?s|f'LH;`|?qfw.xY>cB[&w m| BZ1l7˼s%m3.bm\o1Ke 1mPtx1Nr]mu\tUװn?d~$['~T " j"Ԁ3s[GIB=1 eprp_VQr@ek׃g&e30ΧqM 94 H.5sHY%U=e2!S56Qgti d@ÏCGw2on'Y]ךYy{0KQc^^#Y}$]FG Ўu䢁:F߅).8YN~,{*r\~:ԅEJYɺy;\w) ہҪP] ӝjr@hlOBf]yYPgo`0sVfY]c,s r{5_ $F[tA{b:<-yx:|Y֡n>s4މ{~E6GS2}lg8R韲-ɵd]#1D3P:W(# umӹ[)/O#eSNT8$w8(%~́fdpCZ>/4d%9q) z#cIVa' qhg2N9j@@ó}TŪIaAA I5DEw{4ɼDy$d5mu^00F*;^,#Ip< hw2\QM[{ 5/C| Ȣ8yozimJL^B'{HӀ yuK>KbvWEݹ]#kwIE:l鶋2u KIz:$ċoGy}قL?RYdTc$lف,*–)@DtoS$U6s%+p,~C8B?\n4y$ɁQu): K&6DH;ȵKyR.܍@wEO_&^w]@F9%/|$gݲE`Oy 'pYr8õ\:[h.I@e-aad_^(dy=ӷK1(rh,rjCdm8~k/l,5i (tax+(K*ܻ"kb8@PfHGllp>zkWwTs0L"%XӶTmgo[Gy]%^#z%Z Vp40(Jy4]mu91vom3w׶y,R,"gWqS-Սp,QIQ4@u,`yOQQQ_~yTA.ms;B** Yȉd>{@,蟜!+wW3oI;!&"N7ˢodV.k9ڊ\lQ iLe./ECD֬/ɢZ\5ϝyv_)EBχ@m[(2oz/t(ek}{#mj O0)>2OB%B~cٜCPvR9r嵀M^ߝb':t0eb q\#3YţcXW ̗A/ Ξ$F&'Fq,pՓQʘ?j@dp( q # iU^eިDHN+k5G>dP:Zp|phaV`Xr-.U+ä h I;Qԛ{w(ނ%g}sdZ+%!A{s,`"k_,8z Ç$M'u`'c/%-$wɁsadP[!^g } ޔv .j|ϑ|T?$Kg2Yt` uh.97iv, c))iQDFbwJKKԳgC;w4n͝-_?8Fh2y4=eJ.ÁJYs_ԝ=$DpS7c? Rc6(:9ȕd'_W Cd"b:!g0w;#鷜9 GBc{$He>pj{os&YpO`']$Eet˕컕zdO'u>v=5W5PKIV[!J h_P!KdwʋHEi.2h::xdmg"c.Wicg)(6 r"} GH8-#}tjY'H2G_Y4ʳpQ Ϋ2}|C$H8Wafz3w$X/@w.`(Ixp9;_\N.`-`g&|VAFݡ5B+VNwRƽ |+(&*=ߍF}PZ_{_K< ,>3 lE-+|Q9Xj'yN y*w (:s]VO"Tne6 5 ZTlZ =^B :-ҤBf S Yd,΢UMݓ~$lu/+ˆ.| gr&zRSOwi=}<=ꯓF,EdW9U;ݴƧ+}?+|٥Ul]}\~yo >B¥VvlMOt-+m[BVW?E6ܺ_At:^:z-:("ysi,pYH9g.YQB1Q2f)|!AX Km&=e}?HnB3}pMŀsWq w W銳H( Y0CPjTC<'cẍ́ %h6?s&ckR<,ij]OQ\Q k0ύ``̽!i%D 5| (γ 4u)R{O:O^ռ WWȵb.Io΅[W[?@G^1;} =#mڕ7S!ݾ'@r^nTgQ3n׹T wĐsA{(#x-E<pA7ؑ#esseke)aLeM~ .= 8[yec I=7: YFU:!x dN v,4,Brȯ4dfy!kroԻ2I_5m X^KqG#!UFY5U۾i;t`tkq>.{,gjS83&1D٨0<{yj [PbȼdQ3Qg|;RL]אJsy֫@Zbg22oʋ̓k; Q3EW|,u<VsA"s~, @JD9M^j}F^0q$;?-{ewY$Z]]dݺuw?*//*؍yYfN.`֡{[x,H !0Yz}Aa0?|$ _A5%>a/^)ˇ_DH>u 6Y0ZgȺ-o@(,Y gZ:簿O 7}mS/UÅ'GCL? ( 'p5`]BV]b4 Gp~>@^zbϖttɢfl#М̹vP?S$wl榎Syˀ21@X`}=f}.61*~,Ť6mzxT׮]+pU={,e %= F!΁ gjH<yQ|8[\❜qMط!pV9^˹6Q@Vgv X#ռt4l=ULH5&Xe5$]8t\yh\kuwZ]"Rzy%WZ6R%/s }:ª+`p3G[@i\Cn,y: sYb* ޑE@R{`a2O>㮽{9_ȟtYUG6>#^s{Ihxx,Jocf] E!--W۶mvݺ1XV%))9|ܮ35s˙92bǭ-dÞ7 m U({ϩf!WGv1vHǥ2d}V8L;"z/=<(tY:.X 11#d 3&^CN: ]y_<ʕE|\s ~Xik׭?/0"֛o^ֳGg df%Q Jdogllw=}Tވߑŵ,o5Of|#2Ov⋲(}q<Kc9Va ڶ2/铪]~%dhBepddWVS10paݫ1?y#>y>\d1a72?Z}@FYXPp2 gdX!#A&|5‚x0wyKmY }=|GOYw!-^a;}c+O`/ k ~ٺC*̑TMdD >]=©g/‚9\k,bc/Y}0Qmejׂ؅!3׬YOqQ+Y\]MMzYFG<P-+$F~oMpɢ(>T"M䥎= t, TycOny^d(UE),˥::I$z"2IF!Ry,"uj(FߔD8N$k"k~tm)u1='FbHDN=và{]֧r8&q0HHv R||$.__$f"!s*EW WKiq` IP-tn|L]Y- U^*' ׏9Wv4a-ĸymG?%:sR%}PglOy{v&p;h[: t;uMe߯dNސy3]tw Fެ_$)ǵp,I=*++ڴiSw=mšmFY K°.Ej1>Y|$z>3kbZ%yyBDhV@6r:rgG٣5ijuDyWR͋>@=R_@ZHD%o5 m9$E/u_ ?&޸js{q4@̵|5L=W6x,_!\R/C )Կؗsk4|̥]JL9'7Ju̫[2gr8(*do"*ڝ `z ʯ->@BHZy+ b8[yV̖Wܱ@d".ZWe]*/KIKcP{YxvSΤ7ɰ?Z7y2]1C9F:yC.5yG" 3.dt$ś\k .(g@*L;>,KQXqq;yEc K(Ke^RKG=)ϲkaԾHG2Ȳ.lI"PcDq+sxFY洢Id&eˊ({@I!DH$ɢ0r_鲊}eD0Ti,9JGBX)tMu gCGrIK=sLHdLDDYbFYJ2~Y5Nɕ$m.d'sI]ϖW+OW#K*j.(\sd#1D\"A^P T0p]v _ץ̺׮ry] si$T +Z)1,tE=  W_8aR^z['/_m`z9Fcü,Ua$ŎrnKd KxjyK˫[q( @ڼ|uo8jApd#ޮgP,/%i{g1W{+AA#"˫0:xvWL;t_HeCOl؜NtQ3CxGDXXWbS+$!=0Cƕ@h׽0Ȣ,!ߞ}<$q?`{3* O,IIރܟ+Mp\" }ι U/`dĹ^D[/ke7F7‚{YatIB<|L֩%RʜĜ,4gHibN׏dNz?09!EC.y έ']sMђK?C>(B7{ҐxĚlOCR$2gA)ؖ{,p 6sIOZlBE'Jcq1ca?HO҅& r"F2ތQy=Lx:[ס@a8o| 6ǿ]z?qp1<6sm@ G)34~IǰՅy#8~ѕM&M_h]e^(T@UoY('l5 Z#?Ch25~ܸM5633XYR FE iPZ:KE?D_^FyzTe0LtS<ˮ~R0v􃿨粑y^ fpDBnq9븈%2g\X344 p, ^] Dݢ0^|EB"˼+2rs ݹ׏5_~nJ Tr;LȺ5. ysK }V_sY޲HYp!G#dmx Hb>ɚ<( yoz`d.=8-ʙp,z.yyк(z4GtTy`/lOǣ"~2g~CE'9(]#9 `(F9{Oۙ~ɢӯWʜMՋ9R39ICȸL! =J '|Cy,T7dlw/&b' y$K1p=$yB2ok2OqJ_/ڬe"@'H]!H3LT^1IAVrtIK{ݸOBLYcdW=?\PXPg~dYJѿ䥑Ivg MuC>fˢmbMYGpQ*"j.[eѴ2 ,]w43%03c(6`9E^c'論YEסs"7ގn`$#W|riQi-W,V41D|Y8dg~@h$sgrxg@`jӥT^wps1Wӵ>q#1Z`$!@~#e0Y#,?X"/<WI:qwWB^u.Y-FykvKdN c鉘- : ƀ?BFλsosb:6s=,7 IDATY{O5r^Vb /+R,Kwg!YGL\ƽUK44zÚJNANΑETl@ݘVXPpÌH(e>&7@J]!Fk:>Ee| "Mu8]#[Ŝ-X(,u"uexyo/iK I$q.폞8o m63ϖ\Ǜݾiȣ2'XȁlRZvvN<:h ջяj^Egcsƹҕ;rdQ@341>*ym}%cAyŦ!Y8So HEEvE~ߕR^}$YM,r2>)'v6q6]9\{y:{c zϐwT± rUr2/G dy(2~!h5ABR!*w5 wqCL@t&Ma,R9(w` ^; &t$ߜ?10~b;ڣ?,bXt?ړnafȋ"\/48dA|)]#tShU,WrB*k!BΑ!?Zɵz+.a}X{R!RW4g?G"EH!*uПhwr,Z림`pE#! 2Oi/ reu.Do] kgܘs֕*m`rϑQS‚:\&i\aAe-PWۏy\Ii%W o2hY=})d5`OlИViN-lk.FGTk f! "Uߕ'Ydٲv݅#Yٛz?X=ԎʢNY!ϳOyW%́ĈI(sd00<2z@;D8^%/V^T)V]]i ~WvEREpFE.$ƼC(_'g!{f C&"??u,Zum90"m."g̋Bu]zBOŀ)0n{?A<.dMf@0Xr:{*~?y) Cʼ·Hx cs"ԃ x^[B$-qcW>FhU=u2߻I9/E06bQH;FeZu^.(Khr86rɣ@s:1b'@ly8gu{YtFj'Y(2pCߵ/[IA6u,ce.nz>y>V?%M<[Vֱs:}gxT@ qRd6W"VZ`{=He)t]|߱{[Z=' ~Ffɋ,UZ囦Bn -t{˼#eg7DL0~u\@e"{aήNȼ?Lv`Kc$|rF ~e {C Q|ܶ᫥~b6ŷ]Sak5P궲0\ жY2"zoo;c~  LiF;Y*v4R"Dvfy]ҩM\q$wsԥA$4ʿ-k= hQ^uEYw>3/}׫ !dN$Rn/ӲtTIESXPٔ> sU Es *2kdpAn;x,Uym[RSY]xl#~8XFĺ%q_w^|^,K<[5dy!#<Bt0`5I ȣ|t Yzn:ت* 'Z芡02Gwّ˘h]L>E0e2Vw3l-Jn-/EPH5'H#c~$āTe-ȼ+ a#~<9.Id1^tB丳,:| Olͳdiݰa>L]ʖavcN]Yhe.=2R#S%F,4YeT=AƜ6ydD{w?{CT!$Tބ3]-yĉ+1g. C$PY?+Ӌ)&U;#J}$FyukV~|`ɋcFRU3"u8<t)xMyC{ yBx4R[ar?G$VYVU+ZTw" q/5w#*y+ ?`~z1_t+ .m0j >;3?mYDa1go\s1;nz"_=dEaM9&% <=tj׌C-yc\Q˾2byq!1zm{(|̻#q'Q.}eN)+d O[NH- ae5%:"giȢ֡;A2ɲN=8 { ]7tH6'o9f;'"~~o v?\O@_+Ka%@l\ђ Y,5E@7$W.(FS8389].C])SFlu  r">G}E%Vۗky<dSIÁTy$8qb4 ayk܌I&Z]' @&IW$t,˖"X">9K Woq!>H^NbYD!m`bvvnh#/ yyud\jtG:lZ%Krmjaxo`](y[rtHE0[/K =Y}eY$^!˓~ʗ-'r W}^ӽVZX_ooLYn~@0Y% #Mu6i(#S'=IR9y$!An_gKy4ލڛNՋX\ $ XxncUUU j!p8:J^m eWueD#"I*QFJ8E"Qs1F.gٛḪʜWO/(uET  Kyw9k"CǡSO@H7A!7e!#e^i0_F]7Kj41F^@/Ieap=1V~/ia,Di 1s[˜{¸-qXLQOy*b4Oj9tyKV^#]SVl`Y B^Ȯ#ZQHpYd^΂N2^TKӨ+ A7"Ar8؉:ݹ׎x϶EnP:AMU~S(逑Y1Եn_Vԑ}c}AKZn(ϗJzy"m]X? "< hW,#r,۹2;P,;J:nvܹi@Êqpɟd5-BDaEeSGY+t4Yt_'YDZ󐁥˰H v2,+jx] },c{ f2B1_H9WcO6{c?PƠS86SNNy)n්AI; d^uײzwoG;^4Cd" dJ/ yIZGx7+g ΒWCLpJ:Vyb]-ȆrF(ѰR^A\VQ@pue*ՃNIp) 7BG[z̻۞(E݂A(M0,ܝ#]ef5~,O~s 鞐.gϻ@ҁ uh8G6T'!!rX3v7Dimd!E|hDgy/s4nOEQ;ڰΆs%$Yy`s2tVRRRu.]v^rdb I֛#e5o~hoj4M,M_t0\tKw7"%lVv0.Oɜ}97HdNU1axc#1ʈ٧K#%MyGUnoɤbĮETl.XFŮ{)*R! I?~S A置̞߲ʳ!Eb ?GNf RE2ckY4"^`B8QZk\lȑΡpE᪵vOg8Ge/w-LO7nܺs Ǝ[6f̘0Q8~6CHq*?#x2Iҝ^qRץȷ0w1P.zi$rAm5~8,4 O'<0[Vc`&xlY*c? #bXdQodiHj,69+1wvCGu&yhY;>Ls/\&c*xsl~}2=q.^VxcDz/-J#*QIWC|m5ecNctI+PA2H,[emK#deAJ0`;q>iNȮ.ǜwV\u18dngsd?L7 IDATMPw/jF"Ȃm!ÐJGd)rȂ<n\|2[)KcU{G>p"q ] D ȎVV`Sj;6(>WM[2+F{n(щd8?s3|m#d̘1"ȢQsea?с.= f8pK8 'GY}?U.1bNP#3Œ^2+p]ZTK]2z4yE_tai$Lkd[FG?P3+p({{o! TbkZЖB%K|EƆpm^eA YJhdi$R w&{{gaK62`AtղIȆ>/ C>͒ԽhM3#po3NԥhÜɍ"f_Y*QX慲>k#G֡Nqj8+ZXYY1cs e.׍x&qƭ+JHYvP43}ر+ny(|Ͼq+dу'Q}ݏ Nۛ(T S $/ENCfa߁r]Qz3cβ2\)=n(Y(vYgeL<7\}n~-ctd06U"[KZX?\i$r P)oٵМ%Z|Fə\sY20sܐx/ PcvoqǽO3xBElM\.I&Tw~6\ĩf̹ȓvBwo#RkƟJZNk :)@h,YR,]lEQov 7%_Oaڦ]=dȘ`Y$ubSps2#32:&`H&J)!p -3̐"}'3I)"Yon0D!q2 4obYa W$*Dj1^Q^e4ғ;+Z|1T!3FDzB *o! @T%-m"ִ_[[U Vg~F, Nw/h9;kT#e:ppb%E3qӰ'7^ҾP܇kLtϬ\ÐMaˊ~+Xg ˕Za}?@ե>ʄx%= -T@n=7Nr=e۱l+_p1o'L/tPhA1,ش"=*p'>(BDƉ~5zV\;KYG`ۜ0yY*T~lht g YשDZ.K+m?O&ksm% >Uy2v͖ȕ|Ys ~s‧[.5083#IH^( #|uhrgW7QC)cmc,PBY?6&vE 8`zčQ#cKSc7j>?4y%=EdF"Ȑb)K4y.\8Eڲ}ޛaCd۳K1 ~ˈVȀ1Q +9]dI{ <1Fdr]]}GVpH;UC/Yeuncn.M\gF  jJ#pX f6p/H\k0[ih Rt {art[NC%&e_{2pQd턔+ ܀M2PDי7W VȂw'FZdRlFÞiL {j2@W)g,Yυ!32ĺmxeԸd2՝/w'j98zEhYgEw *SbNH:F.{,Ec*Wr,9G$4jrJ iᔋ+ dLJ#eETQ!r}p߱<ruci$Z#y劦-tj.*H aȨ 1 !A8`%yc3|0{s)\ht1U?4, Y??xށFF"Oܵ3E FTC,b*4iuICDv6%ȦD@YW84@O+D#$X Bu{d0Mɲ~6 U A (pz dfIZ݄ i*9Ց%cG,cK1nqe8ӒWǼ/RBL%Dt nNwacQV`aÞ[!cUʌȀM>"D/)e 8~UrLB/ D ԛWL9"eNĸ_BH;$\ue0Vs؅yXcHH'36BIJ#eT%(Ci$r"sw 8F"/8I_.D.4cѴKR$clAԒ9!J"/mƆ_i8}rSa*n9gz9pԟ3 )Qր,/`u~`(Ty-P*Vqd]KO9u,'wykhsӟOI&!hٛjܛT;[$hOd+ʘ{rAi$r2YH Phmq*Яdٕgy_ItCY:Ç %2\ ]83] yv1`d$2v[gYKм/.?= -Yku3j]:Q9d c,%Yʰ3#CٍNGbYrkIC ݌|Xę}>>Uqn2vBkst3$Ȕr;Ns`kMJOGt!ÂTΉ"Ɏs?THC%dg^`oy}DbP,}u.l8el̫+b_&=;oK*M:pZ.Vy-D@˗e]vV2`c5h=쾗pb}`81BW{bYc ·5V4_ NdձE%Br9Y~x߇}uL;/TO쪎cZX*"$ ⶕt NF9Tc?8I㏿ 0ukB*ckGAlEkL1#^ ,n Ɏ|drc&|pd;+?b+,tNt nLubz!4}P"+@y\+/=p?j|dBgFȌ /eyCSPDq6g˘pHbBغ>^D bp=o;<|e8a| 3U:Z,"EQJKmQ&}h?*\AEz;U!{5Yɰ)@ ƭqˢ1ɢ)+dȾby(Y-'5eIWxp-;*tf]=uvqi,Lk" 8/~6Hr>pNj( 8IrTF9P6W幻+}v62S-•>( e4 _h%js^cL2).Y0[S!_CXݎd@\#r3>;kҥh.::!,cJ7 cK$cj}"cI7sp!p,+ё#WNb^v#YG>|(["z' HT|z 8d@y'co2vHfd̨c.YG$Qnܯ\s-A:r<,ElSpk9B{d}{X°]#Q&\9nܸdk4晦3ZVRYЍYN'YCَ9Fj3dE+֒Bah72K:)"c%i>FYkg7f{ZEV&Iw6缾s>7gyB&T8[Rq,0ֱ{wo_hQv-曻ІBtWY pxDfQئ_pcdm6v2e,pv+R GN[o?2Ą h޹ ;r }}ihס'TʌȀiH`C{Y9<.<.2'rg!ctq?0ǭ׺0'cT|5cZU~g[DSVƟ,c"ȢS|tLQĻJϩEEEE ⋋1jzqsrWɀN͝}^Y3CQ֕K[FQ3Psexm.cwO;E 9&b[P*9}do>z5p" c&8qF+9 @b@+{k4Wf ỒFcuvf{)'!Bte"?rq\/p;pJUbLb%&pt~Ϟ=}S;u4aW8|}x}i9g=s[i_8|]E .7^m{ ηEc=Rt/aUµ"+,JE9Zғ)샮dOSj'J$*?(z{ȢAÝҟg,1ֳ9wdmRsyxGVo7uK#.Y'KS, _bZ)+T9;1KGD'?,tb>P"(YzLcO݀fE'OKמ{n5jU3-7Zkukhd#Ǯ1V!@Ķ=MvF/cUM=,Ƚq·R3vm2^]apWʘƷK5])xd>Jy3#NeG,{A96SW"OF/rO88dEXkGeñgb"r7  Y.ce9dS`H ;<>#fyMUr)cS. ]jĥsp`f#bU73L9Ý$ Hҭqk `C>R]7e xJv؉g}59UrRE12bO I{Ci\ di=qOa/dȳq|.P%XO}~1ǑSd$( WstY$9ޱ Ngdɢe8~{n2IX,Vh/k}E/ǞȶQ2|,-DPds}q]n`%Ph "2:"KmmJ3 c_+ |zNW932#NicPsdf?7"V3gS8nEY;e'!Fy c,3o(åIUkov)6K3rͥ8iKhx,/vRZx&M}M7ĉ/}pL0x>: Bl!clTvؖgǧɀ2*^5xX#2 YZ`dm02~i 7"/+t)W( [Iֵ/sqvzmdjIÃw)\3s&c#e~hQæ>aGJ\&c;zR~1Q!+C2Y mpw9r P}/`3792YA+%uJ<(VI$ϲg}IKyl:Ooamug,x(;Yg3cfd W72* CCxU)ږ+d[!X͗!d,q&.U/$ߌ1Gϑr=uh/LOX/q suK#2vwBv1岂mlOzb,Mh<%i*!#9}xB@z'}eVm 2bÏlޔ t|S)~,w"azT)\{ڋ يVϑgRݳdѳ-dlk՞yTX?P,`th)sEgd,|Yǖ ~ZŹ"Q"Keih[c%r(b)DddbS$=[ZeXg 3a]Q̓ef;CPϙ+\r d>D8OF?",MPb$2ESԋd>55(S332DYDmfc (dB8G"ej)Zdq,r=K Y C0]ĸA{ õ#KSe^EVk1(áW &onhpa+Cԧfo q=F2vY;qL_- IDATvpU R1.\n)۔oyEo*:-d+2do #qrᩂ}-zZ133yQ|{Rݏ*ڽe0E k[L9#aj8u8H;I^~e,KӠpg:}=DboNg^qt9o"'BԠ.ȅK͆)L-Gx @ϗ#n. \ 3Mѥ=v՞Yvݡ?gV+32Dzp ;< 1&踑 ` J ǫ៭h=ע bG9X0\E[Q]5\ۿDdyG"`G@$S͜dJss0^ m ƲZі0侓Ȑ&626Q(yO`xZU<8Xh'ǟe5ͼzc'7yd)ľ^9-I,:'kwuˢT|ֻ#/C'$' p&n|YʵVѮH1ِOFH: Y͈$qW)i`(4#[x=ϞW ,Y@`Z;U'O+jN L{PW~_Ɋ c\-CNE?t-dd)Q=GsUZ,wrc"m;|+8Ȁ9BY ٥"foV',cU?{#_Ď{7 @d)ڢCBp Ȕp"=4 J#oY8/p(Aye+%# h%On FpE,PQ'j+1>V98@de0>@-dTq2mg":cMOE'!Ŋ,*1B~@Β"݀X{Y4pY䯃i4, ګ~0x 2{l#URf(2*z BxRTX2ʰKQ)P|=c[Yt>w%^DX]YywC(F}<]m6\`*@]tiI߹2P"d=W`)iL NJ6:c4?KKx)-0 u<ycsY1x^2tӆF9CeQ'v׍e5JOSviwP"8{BYF!{y쯩Js@ius~KV.Da,,co"3H1H09]R8I6Y{> tjdm ZDNrfdX{€=EFKk+l^­lt϶h ` E#+^!_,g1gs Y%$<^ @B$+gP?q0~rm[}+EY]/(eL?d)!itF>EVPjimy6k_!6c;+G9?%MhFFd6NpspQbyz?`] 0oG;12%ܺg^Vh7haoh^KXRdOtEdLu|_,(z j6ڙ8՗Ϻu)zm8ȻMs[xr'}< EKu^BY*4˅H. ,!c2!K!7,Y})(dĎ 9Wƞaݎ!/U>Q2vlKEPUs+EXg`o~7i%VSxNZqN{1{3*G_+ZmoYw3ufd GȢ™.C71n!%92 r%E7pXE5V*XKWcYt+J#,ݑ(wDnws3daOָ#G\"՟1SHX i:<-cN܏3=Zg8Kwc!c ,o8ޑ#c=NgY̟&þ87+$ ~H: t] { &E+p&dܹ $]q[~}glxol~a6TP>@v\ƚs)-')菍el%8~G"-jָx2t@Yv`MȪ>_zG1kwdϒV6PYVWu.g1@*'GRmD Y1瑩p^G/ei@nCv-Dd,PTfdpcpa e$3\a_K5(ղ6ik ˂>JCo,{7ڕʢ,ljhr໲"lSҠH`w?h)tv~}[jUUv ƚMd\K ?u2@ϧ0̞hA^׳tF;KєH#g=R#I7YSxcpB\/mxq:  -J:1Gm+GGzd׮w/YdTISpAwX)b>`IddTO(DXWľ?HQ͒Wtmx/wYbEV~ϳ:P҂&+ʨ{!< mdY>KE~ HBt8#>C^O}gwhA7p^Ŀ[~ = 0PʑnG?<=^σ&s謥2qi7ϡdNq7dv򇭕p"3<YXz k't}HeWC2 Y[,"T*1 }q8v*pV ›#c\b^E6do'pQ,% q0 BwY볒&3G7]\9o@BnUdlLŜKZ,NE. eyUJib]y)eѶd /w `ke]2 %bzC1dE:>]/(R{41\D[}{(ޅx`(@c8RôvI.9Iz/ ǯHezl'N{d=dlTGgl? Wu*N" [2l[9}Ӈ e9̤@ 8lى/P+V < 4\q ~2}-Υv=%:C'2&_ߝ9zw^檑_rΚi-d2P엉swܹ2.]P W{0ʂ2)֙'|C19ȋQ!b4.XhIQZ7F;VU2|Cqug_'TF\I_RgIGY%oK eL<>Ui`S4`P,Ԋ|Lar[c\Z+}S}x+@ %~a,Ey ,zbU!W*؃Gʨحv"WtxkMe|\wwF"E)bRYYyet]@˺-*Dv^-idEF~χ%+Q++<_8?OsnEgY-7X^RRr,"[Z_4lqief{,1vLsf|&fI26yȺ+e>fWqJO{ʀ @-znزTA L2{޵l%K U(\Lún+8r]5ߕնOSvW3dFXFoY!VvP_Hb=ňeגϯvƿpq2j]{r7] %v=/0#1n`#9t,8o3 <"cw)1Nۀ[64 uuqyR|YMm=qffI# KK&˜zOQP- J))±h]3BV-ÂFvZ,B~;V쬐է4ME."?ȞęZ=+ =0u m 99;R:T\ Pty{36<=y:] OgWļǵA=r0(D$gsξsTL鞫IZ6ED^/Kud~td0-ͷ8[Y/pf}a֕F"fgd)C ^+z uR_N!PhOWPx%ϸ$+ ;Nfn쉞?*ccdFq 7sDkc.v^"Wm/Cl{adA(uV~ d)p: R+<^e(2U5\ T1w;`pPk'w8 Gj)qe1T&8?Ǯ(QoETRE2u0 d (D_H_K[O.s5nBI~4/c@ibnqٟ s,w/󱺩'[{0חPҜuB?¹F=cb2{{Ck߮Fk2o㈈b->݉s1GoZɞi,:/ џSz= Cv>iZEC ~~+o$ A+3;Z W@ <{$%ן P3$1Wx:n0 + Z ^B]!BW֣{˘#vzx [lu}0JО}eX E[vv_&`(p^iBdA72+Y ]_䰏B?'h1>jȓ/ BS|xY0 lZl+CEȕ dL$V^mȌ 86($e2l8V{b@Quv 6(bb/#sZJN]ڨ@c%JdѩӸNYfg  <Ⱥϯ 9OO8=dL2 P`,2=aJ*`X=1hⰴ|(18fʢy;[FRu)(8S{X[->hq8Fݲದ4P(O#?T ˢ4)< dt?s{@2<@_Dc`D$ړ]˕9Gdf^KA8ݯ#gGVc 8]i)4PZ5cԟ!6F$HaU"ʀܝq(1{4d\-2&C @Ap+4%J`z, PY*c??!xo&_, @QG7+s`S]^}8۝̽VVsƲ;1@xP"UVIhI'YuvWC>Ƭ62CGlz30&tF8 I xcd컰˚}Ľf=$&wA B c$LNLSeߕEO*3I em1 {K9K}@Nz6yQfW^y` z0 ,J#@c0)E;["Z20Zn >GWhg)vȣ瑅pT^4'd.EV1 KGx)Yz]`M2|K &1}eQхk֡,FQ?д&"V|eAY/l?K ^&c f:R922:  z7BglJ?Yo(EH%qhZRZ #dT3d xse͡8EL4BS"OB鈁ٝ0UnȍWd6K(<'cxԷCe)uW}1ϕwJ8C4و7Gt7q>us|1%ɒ\gaJ8G\`G2& 5&qBzqNj:WH /SO2F-2vW觟t]u3mceЯM|EivRk$j#?k3d2^ `tD=0`$hYaM kq,0!t=9g}9À~~ ?NUp G },jQ(xOxߗK1~]_qק^\7vQj2\WYBEkNT^7nFA}7&;s{0K1 Xgu))n^j685lWne#Va%5un6 2sY&"S7-\N(20eXƎڄ=7Hz;,G.M`<ɘ͕Epe_?q {̌E8 x]Ѣ9"d-mȞ<4" IDATIzO_jH8B &%pݘp<|aW&\|$ Xf)в4,m${a*5lU:j x78XI$v)G\5@c tF0JHOYĸNI޷KX^WZ&ls spEGX}ewlW,c"Kw:Q4/Z`'*`(kẌʃWeWK=칚$Nm?tAd@u TF"{pJ:7 -oLڇ}ߕ?' _\Zu]֊e e-Xj>%sl2#N4ȗQǯD~,,|bSpEeSL.Bete=7y pÒk؋ːddT{QhmyֽdQPW"@IdFF6薲DLe^ňpJ% y,|5q$B}0+w˲Ь8~tEaoڥ=Enm|Aa=\w!u{+^dr%e\9Jt;\MޗEҟYu}+<dQen Y., K#!2]*϶H^#R;ChjPlOL$j{Hz1\5C 7]#9XŁY,0s,XX`#ȃZy,S#<@M8\U>ɡ|*x~IfDǝɆȪg#% TBhŞ]qA#ΜZ %cDݖ|:yo%K\-%s,x8MӰ rf`^ r^MdFh`bW3YY*B}JZ cH$Z/Jѳg!oY*ȱ>/u ͌pb^,K9 3e"ͧPӴ@ɢ5;91ߔ[8V[jm"["@>Ğ\,g9Ͷ<Se>wTffdHqd\"(FaTxn125`YG8+a8:U ukhʑe͵{9 N՜؈UmKXc{o8ѐ@/^tKkkΠ}08{8桺g0,@-:_&q/gwXKd>8s֋dEatIۉ3T=]+YHȆsҦW˂Oa{5w tBNwem\zJ>ĻdlZNU +YwH*/+eWgS%RKGo hf̝Ȝ1@FdpF2{Humjht|>cf?!cgFHEjcwQ,ŨW kl8jtCc̼>CB.bg.?PSyrJCۓQtϑKюEvf-Xki)Z/ K֙<- F1P2w`WĶN]r:}Fkm,Khj[dmU,Md kTrfT={" VQ1eY4M6s3[{8 %wߐ&^G~}EEktD?ÖՋʘ$ ʕ'2"}0?G7Su(5+/`<է4!Pa=C?U*gFz˧y5mPZ(P}\c"yct lZ6`Q]k!ϴ?zapME,ӆThN2+CkבejpWXKLy2f_1@<.g/jg ឈ|olt`UFinlOeGN hw9ͲTd 6HVÓr  y Ml]*K7jcapFHuEFְWʢ <蛧elvl"Mo\r*1N+Tj5Ȇ8s K5 % pW4b1 g8Ԯp Jxa Wp S3Gk/tȵS:$YAm2VBR=Rme)Kv]/]hsp6gg: -e(18<8؟M Bm 13NEH_R*tYnf<(NFzk˗?#K8##)2dR\ Q`'Ȩm1TEߓ]4C`(4CS _tg s9㦘UuE[ϗcPb1DerׇqُCEg 8c㹾}"3-Iv9Z,9McTdJlWq$ՖF"Ŋ֝& M6AeRg]AG擎YTbW@5;9jD2;ȺC>D$Nirzz&@9ÕJ?S4 H{qi@ӡli uɤx1>_dq5 =[V"@>ϵd,)>d{RQ|%Jֵ.X~{*,3$E鷙h+5Q@Sᡲer,J2UM+.SF`2OJRnnn^95ZSO[dr%iR8.oT~LR-܏?)u+3=Gs`2֌7uBY9P'k _g0514hJoN[|J doțZ;JV2cD=Gn;9P=_V(/XY*L}ZW^Sq@1ԀCN+'xXn_8KVe񒆗F"&MI9>\ӶS[60Yz%*Y#%ƖO3VXi$V@,&"sg_M9.l*Y ms%[D78ce1eF>1~dOnlΓK#pY{궝!H`(lɝlkR: K*CCV?/wE5S'xogq̌ 8S$ #Q3ejKG7}UӍ0+1JV4xs=1[=Ų!sdH<^eQjxԠF1_+8S$7l09zh̙?07Bhry)c<BtP1.?8$}:(9msLMU$,*5"qw@#CX{dm's~OA_=3ר]V_% 6\!"Y=zg.:,)ZS%hĺƣܫC;fL( ՖF"jTAãdJ .QcG՝~؇jF|]s2N<^7'Ɉ78Y?[\g| ;jIsΚ[\.Uw8[qYˆ"`,\|o"cl+rM7 nP3L҇3e]6nt\i$2F&ȟdLe?5}ƛKw-M2PˌȤu(PGBwc{u ]tʼn]fwN g jጨzHޟ5,@(#1H:Ih"|V#;cc,3J@gݿ  w(p~i1ϒEHU8Q0YH87~,V_lYZ2:왕2J앹a @̿dk Z;~ 8uE[܅!:s};k\F|0orzdqJ:sZbw.y{vy֣G]Mӳdə8Y 7=\1M8o42 %,\ܝg6ˡSptg@Q5'Լ<3P}œV:,ϊåMe)gI_#rg,<#\&Kj6n M[%tauf-v]Y35 cXޫj`?rX%Lkp&rj1dL,%rY,kmAݜF+ַ/xrpC֊}0ZF8RQQ4S0-bES^UWuwڕjĢ!\np +؉5amVe)9Mup8p8q= fϲ= }'P[٣e'ᰋu@I.E1:ա1")n[FA2s_o1 CJiƛ&ٖl'e4Қ[XF\Yž2m.#cOY1KF&Kzgð57Nvr=:ɢ@FB_|3YA'T)2ly |9du|YA o~ /u]?X% K遞[sp¦x0 -ȶ9!Q4u@leg$KJUIf郞2b {gg<{xnS Iwk,mAGpO_Xz|\>sݰɘ0+2fCfd FdtxLI Ub)n']EwyıvNs^+ xǩV{/8Y!^gf\# {Y˳Ͻ lhAJ>{EY.^\d#`dPWkG9 P["J`d&=-iVHH۪ 0ˢsO'E ޿)(j+N_ Ei$2\FQȿM Ff<9dt8W[#~:@m =I?9Q)?7d m:Տt0*;}o3!kOde :seAWd쟟U]dsP͚(hj=$Ͳ 4Nd0Z-x2(hx\%|76x /_@#`pO|y&KZUW:J8ǙJyKb|^( `WdէK^EVʘ!`P/cX|ڈ*Ydϱ }g$ IF";p{Y1c:eVŰ->3^钱^Of1p"3|e3dxDž%LfˢK(dTA\w0"Y$i?F2e%{sHZkpWYKek%=P;X.ݞhO2K@meQ2( Y8s*;P Nxr ^I_fmejqU tXƶ8fSd \p^<{3BhTGki*gF0[CY ^ z (m,%d umz[Ζa-gD;]d8YI [Vӽ 0u'a;TwW#h֭s cYm4|w(bBƞ! IDATN, i۟.]wSe~*]QQVQl+*V\*(vkAwu-jԱݵ "*HӓI27 4=9i Q}]&9=.Dl-gu~{|#"x'-$5rZ>Q'HO0ӳ7[45_QRO@EXqrNe$@pHf0aB^0q8WJyi=.C `oʁZ.t2lDSH$쩽o}VR 4=i1'ܷOn`Vp^*`mK@fӍ]o0cZsF u nTpUʴ : >0鑍ad~O} z& rA0`@0X$=ّ\k~86r؟%Q§@q.έsg\D YMW`Y6,{i+:'(0NaR+Ɍ=V~qvX GX{Ƶdz9ͯ6`Ӧ=AF\0WS q5 0֙_CRSr Կ$ɳ=8I e"X + :?Y&L 6X 6S09#Y  &HGeЌsаƾA2Ǧst.׭g"9C(kT%*h}<}ᒋ.ZւḑZoY̲@im#Bُ۴z|cAZo:d'!]tm!x P( ;CA) *@7_a`2du ]`m';r3}Čt-TT+.ْ{J5vyeh HeW8v݅\v:w'{ؤ!;@4 -cL"[`x3ƙQr*X76^)3 i32l- Y#_U?U{480 ]N}p G'M})vp /toQp֘1+k)rl>3?U^OBL5Ʀ+WNSFmڗ SyE<-IA1HzHtz\{ͦp[]@0 NIP̘C"[R!{\|-80=RU~V^n7wD@[”N $vwX@:[s,8OG@<jlk\@3Lu+{[ X ߒ&P1Dz,{iC éP]- kc|/-'-d NuDY^S f.QYO*-|l_W` (zQYneX '(_.hA)o{ M"{nϞ=|]t'דw@k9õ73ǵ>jLѾ|V@@ ~Bב.^:ҽ߆@! ,W\KNȲYemd-=_OnH0_tl_u,TeQEeOܹs]n=@UȲ~ Y !m6+h^,d';;M:wҁ;HުXQ\t%{2W{fNY1uV:<`3 䘙Qi_5vK-|\]Tѓtzz◃Y$tEo9I}r c8Ǟ> d4Hf܈Nff(۝79s:ff8``gBX9_V\P``0޾ {~l8+>߂umu1U?f7Xq sb=Ȅ};QRRҵrF믻т,`DHZ(,Jõv{K[>|_a F.ϓ>,WY:3"j>S% ֹlڃb[>L>#ڂNU/dӆ8 K'Jw29ܞ '5ѿ3靎Ow18hUNHD'>׽-j~) 8أH >س`rn30Iϟm'x@$ۡӣPC;ݿR>`l+9{L_-=zx*jJ` ؛{ 0A[,)K3Q MNktlqB]|0ߡ:Qx>;)qO^R9=? j9`Ȳ@Ppgxkҏ{0=9b`bCW} sbtM:H:o|>9sfUBe:+0vSTK`htK/8+"x"?b]}R%um|E;!Wٞ n 2n:~\$gۍǃ=5@ u:؛JHm3Y".YRzYAd0wiMس$D=߈|`,R ^Ebv;;FYfBB۪)2?k  w {7VN%`0v>5Q i/ 2.>kV*NM?*;@=ϥQsl댞M2)p-*x߇,sg(KXcW.Z '8'0`Rk9w >0ߣFw_LZ :)(D>Z !˺LAH,_`jeK7 B>bs6c)sʥO;q  Y P'dY7;\fȎD"U3g +6\b][t%tY&'7,eOaE{a?񽀎|u `\sw|(w4t׀}BJ$ǀL:ÿyFbA'9~x vR-OtqcX6>Wx03`rT'̉' YRWn?r_U΂l?`r@:d.8Xw .Dq!'C0kn]{*_!gwtn>*d9}HpWNY=wH аln @Ǿ46b֫?|t^{ C!ce`pIȲPWBt+SHg=!:*NFG X?؄UtK$tLO3~3N>dY/j?Nq`pC5K\$`-X pPgg4i$Ѯx A8>K%s-k <=ޖ,6b}tZ仯r>t?r2; &m(J0G A&H>o/ׯ{'IIiSbMw Mp W}W v'm,4$D,Nd^ I~lQ8 n40aiUUK1G5E)# lZr"4E(_kwș|d5LO6z%6C v ^Z";eG{WMr7ZNw>q[Ȳ&%9w&*7lȲk-" Cfv_? d \S}.~ll;5 ,sكG@MϐeqEbx>ǣGdqY?@Gh~|@ȷX(s'١z| A}X +ҿOV:t>x XZ3RVz洀9Mt|< Ox:9pu`669v>1#jRX&%Tg^` v.HϾ&4ax'ݫbG@cpsG0a``pK ϫQk~΄1`وo2Anr3 Z0 r(_{LOy@ Q1Bm@hts,<½) PC竻AKu(` :ZZv_QG cSFV*d0C þϢ6íiYx]`7[kR\N(ĹnR?9H ߬A [ΐ.@EȲ&':WG&- Y֗Pl Y֍^pYȲ \O>2 Z|.}Og!3$ ]`sOJS8s7Y) X}8ԭRcʯ\@eM_S@V_]{;lÜH^׵`p?®W oD [ Ȓ}?(+,_j u.6s=JXp"3WxDFHE Q-QeVZaG)A\ QƲ]30{bG`|!;YNX lM ~T!M Kj*@D Ou 8{#1]2ylanr'<5r$_V'a4Q F^!b9=d,4,2ey1tҚp<t(r_u65yWhMJ= Zg* s) ܡP|HC~vll6)3vQzҷ(ӯu; 4z]_ |{N {- PY|k&Q<.:ky81dYw 8%)NU @@ȞiհSA@Pxd$ KXc#أa s2FՂ}< f7ρ`pJ|pvzsI'm (w%P/Ph=sYfO :#R{~y=%7Zq߄u م`?=~᧩kDZ Td{deHn1f3cc$'x:ܑ}{+H5 ~: 2[lB7rwkNM0vsc%љX4HGd򢂽}#=> |f{0xsZxr=| nYM|3@Ǧ=>Av)cX/ǫv''g70ۥ/daCkp|a'm%f,:ݟ+RS2d _(@qy+8~k Gݛ ek,}K&z/ &=BcN#_u38E1} kP%`VV4,1 8ѓ$ -69))ٺwg2.@lg "Q7 xrXLnrܣƚ? 7eE L `(nZ,]1}. ICkUiYntiTckiւoN`c@n׋r~=:~*@XYĞ_`Cn `>.+F<@Ly]Ȳ^KAQ[.ך YV(y>@0^:0,wʷw[ ,_ M>\ڱ.U7Ȝ8@`4ĘF=pWF"|XK]Ck'Ȯ:@znd%.U"r'^b.d$ 3/ih|CdreO~Lsi+$ O>A!QIΫa7vdLFٯPJ4Г_׭ȸU[ʔD``tk6s NXl {1+)e+@`pNT\gƿؘQh8”ŚI&vyDt υ9vY ،F1Ɇڨ}rW*{ 3jC\]4u^Ot P-|,L$`3A `pFD2ɹ D3>ό)b_>c>_>5.-CokMzNNNo۷o;CT'`0j AtY` dIݧ={e{H7㜵͞)'$_sA/Oq{id(0٬J7\d7=@ɲITJ`QL=O`Rag:kfK`Sa9,5 gLo&2H fUbIG7*X"#g' X]A.f=5Vq|jC5XUҔhJ&LyBȚfg6uf{4K`Ӡ}:C3_P*X#LN@0;;o\u=l=j逹F;!:D`0`m["2TN ˩ Xo Ȫ(NSG!X !5t[wX,??PHNei=vF] So-4+, (P}/Zf{01TX:_? &: tI?sk-5Ҙ@feS -mz[6__ c}:WؿZIe[$$~҃;^Dφ\ `ӆo2N 0V3v?P̜5Ύ^= 6 M`Nq=`n٬QO0s@083RK!ؠ2~>M WGVfnMV<@fu k @3KL0}{7шtiPX*V^*33Z0^EQ}CCM!~[3MMy 6hV3/Xzo1)53ICHms@vq5r:7` `+l̚g;B0 ĥ ů~_;|Kݱߣ3z96Q@Z]MDq|\=)0z`v~?HA𚔦C#s'XncZ]|#rovih=r%gl[{'w+ \ 9[YLH3RtVw)l@5Zt SjQ"!DbmfRxRUlj* P,pyIpdPL9'>n'`b[U:!z>k@q#X* HU) YVGWǫA fVԍ묕9LO4^@I +*X{YzPNKAzK`rf%+-3z8z) X\"9[uO N;ވ \NhX2`sfb,%:LԤMȲ xd.koYf'N&?o֚]-'#@~/06L|'OX~`o2cx9أZ0)L .aj{')0R9ǁLʸAJ :W/|}dŔQυK|h0,)15ALX݈a7<&Z Bo\;i` |ut8-ݭ{ X7| AݳL8}}W%~+ 2zk-{a-F=oL#MS ,c)쑢fiO:X{ Ȧ 1N)y*K8 TwK:(;Hi:; -@1Vr*Ӓ řr'ֈ$QNGi.KC/hkwP-p jiOznR4ZH" ү)0Ig"E@ Y}%}ޜ} ί d5G,m QO?ˤ[9ukqY4ŮW?y d^d4 dY}}I1c'r2lV yNx^"S,tAyyyk}u65*)=v#2DΎ'l91/[ :HN Q M5UAXFw9%s}03`I}a)9fjsR)23S6ZppeYosX 6`m}r~/Ɲl9PߑAJ6i>d7rG%[~ KAp4ij_4Y>?NV71엿f@ Rvm?CJՙn/ P9T""Q'[/3׺DQ} ܣ k dDgn5~]+x9$ߣTك^1mk`A@C\M%*@6#R֠] rd<ւ|9,Y, 1v] -ȆXLIi]{ȁێl}O((Q4_4`T`6% deܦ$0+J'u]] j@u. dPp 2[T> .) L_G .nL^7P@9z%1d[/ߛl`.; ="魻M ,2)>u^ {s:ɑ)PⱫ|nc(Ev$^T7K{%$JA`Lژk(wH,$]NDL0j=B~̲ǾKǍ[ӠA%?|Z j An`϶ YgVY֒sxtȓ8$)%2((HSF2#@jRVV,//oN9]k V f@W(&:(of;XLzPmJ o.]c_tR!]:&\3fKwKPΆuC}y\2{7dYNf->,@ F@ r `3e$Nf/#í׎>п[Oʖ= ,[,#Ȗ ?okMTVu2WE r|`ƢVlwuѾgcIM3@v2b< [6d&y,O$5|g|@Zw{LN*boهo5q}#(0ofuhw؞5v:@/HIvnC>솇= h 5~4,q p6LuRxBأoMH% >C4gJI*#Gj9m'54\" :xYSrAѹr:L7lw1UiXm}{CQatpuOC5l7!`"'UȲ:+8>\3Mt0oP 6`?i P[zdP ADM]g}W toLmm;i﬽ӻw{ͥ2)4++xGlLe `9-`|XIDK RBA/Q$$QpPQ.)jqO)=heee51c=A `LV~9;:G V^{Z[=\ [458 #xvNJ1QbF:̚M96_RSS`= IC/نQ:u '܄.xWlxu$٥SAF`) 0Gýr,0~`Hj_{D J)]AvRp"{YrkֿwN0Yf8q?Rv` g{j@ >oC߿"?? (6]C.\o1) 쾄XS9G3*&y& N! fJ@J# i&f'8hFU^^^/g Q ~ Xk2Ts&I{)lr/QW(XMr퍰Ff9Z_a/Vɶ;xDW% e1KJY? * O\ZtH-)Ok/nڏ m|('`*ކ͞X#o1X~ 5 N2)5/luM@0X 3mt 0 ǩՐt9!z`b;8Oޑuֵ N̆]RVnնȎD"ׯ_S?u}DĪ0,6L6 B``2@qϭ GƧw]@4p1Lړ% l"u8{rRmڃ >Nm"/3fW+ f?Gfvl&v3Y&HΔ`XH3:5R?뺧*(YfzGP]@#Ö"&pS`׵z Ҵr=/6qԙ{ƻWs}ߊDOdLYK}@W3u߂Ns-nQA2헄Fw]6ntqKeN^[CSڪl+KYV=XfVrtL~-~_>V\C F% X8gfL n4J>N6z>k>}> /,_ hy'Ml)3g ,meee~G"W\9O>ɮw~%@Ngw3=ۛYح+tn6Q R `<pMW}yc5 =lrO`җ1% 9]PxN0qһR4,/ډd+c\Q`wY*`,C64 2Wxg1Rw6nq=͚W=1kiWtTVegpj Tf]AkS<[aST2d6M$'1edYʒe+>\z]˯G`ٚ h+h>.Ie 5i+B؝g4YjV֦D?WWWw:׵k׺]v%Cg|1h؇l(ϗ.//=||r"KQ;X,[dV {O]E)E ct}daYt%E O \NaLUhxt0U cOu^WI,xRA kb]ԙX.5X&ȄO^kxEga2+_w5'aw ;[AE`,D恴S4P뿟lm TZ,S^ G6$}ws"Zk}2F, Y`pȻq`ܑn(sH,c8T׿⋻C *r(K.Do.ǘ>uc& @2%˦Y["N]܎(_k>=f&fߖrL}}葓S1p;ò3y'X'gJj8#@ DGOQYY=^u|W`Cs*p 0)sVK1ףBʲv]AspE;=Ja߆*`ݗFhբm[+ `@5ce 6{z6#6`i&ݘGf'?dVuf6JC`"Dw9Re-N;_-f'd">c#X2Rp%X gK2ȭe>2͕}'s~ nt]ۑ .+.SNe!dYko{.%HDtL Y p{t= YSp!y2{gz(0LCG%Rt,O {u9` ZDr?X60f}}~nNNNpY ,(.`}w>H|dY<AAB7`]?9n]>&7bc4 /XҒ $ۚ;t+]&)dH |(M )agra ׈5A #`ʨsX7B{ nX o5r?0eeֶi1 v6_9LzSP=Owss5cõ5<,Kv~"@̀ & Zuszy$}[':i.O"دW<̐e=O/A Y+QȲ>qo`XȲ`6'V> y#<6'8tVB}p- gMI)]|p pjƌ1cF\oTVV&3^VV6u -d鐝o,n)\+`6ؙ4h\v<,6''2˓9 ά[kv+X;^eޗ"'x.Q?N 9-.2BfBNUz1 zyFf?-gҔ1"v1,hMɦdE,P (1ktOV5rZ s/M7'v<7vkv6c2S(͸i'=l^V6nAzIUZ~M|?)$1{**-}#K"8$ZszȲdW CY6T@pHǒ'@w2={!|/BuGɁ(OsNQzK'i>.z5OPPr@ */idOk|݉i|f\G5}.=D]jVa#8Ȥ(Ss&{6By[,>J)Z lC`FQl<~zd7}@6)t IN@plԖNI^ PΕ/Y)duMo h`D=G.7[S~ Hkw0UbE#AF؝S&,RaϗsGo$yw,Ma{s{-KW}m]!aVȲ.pDe=m\7a<'␎Z F\ۀ̃)p9 viBn}rɑ,#z -iIFd ,//_{T(sM#Ny 5QKN=lEHp\FT'Z ֗GaNanMg*8p:療Gstɴ.肆4wƑV<[/ީDK ܠ:X^F DQ8Rvl8Hh.,[hJL"ӧځb:r5a D|1p W4ДFʨ4ܛ&G.+RX8vҿE{$6s޳Wٛ{Z_!S 6#16Dw؍3]R0/dYׁ%ǁɓ e`NcH](8 dҖy'8l%%8&NOR )2u`]w3H3~m8n05jr[yyykrcS&DWqr1FiJ9ꞏE;!*~UJm.@t< IDAT_f$nм`W;A:tv Eb#dA$ ƮCuX'`KTY'w|K<yVgc^ޙҀdth⼦xmG!4A=Ui~rycMK='lexIsc''seW]4ؗsSka; Eeݯ~g& >I/x'8B]ݣ i2n#Z\mz]&:μ46z8M 5rSւ "+ρLI ^pW*474tEb|i`?} e= Zh"pzҽ%< k3&jZLH;!@h'{S4ۑ8.)txxI)댣|@oAFE70{5(~(#N78BFkV68`p8"}f g >o{4TK>6{DsR zYAg"yʪ# i @keI#'!k \zTj*|?` 8~1Lɮ`F?4{<(9 !3 h[@em3y`:0vH͗ҙ~@ e'T).s@W\ <@Pfm*,-#;M Yֳ}jq h1G-fN,+l@{ֿ'~%< ]F=ץo~ Y`I@ǂep֓)nZ xDZ d6_epw"8:g,k{zn ݌E`ve U T`;,KZN44cˁ=:vb{[s6v,؍Mcǎ5 kA9D@Mr70 X|Q \KM#nZDP 4dYc2ץ黕aP@e)|=lΆiVwĭ^m/]o|X!a.HR€Ypl&lbis~N3RǁWX F{Pd%`@m@X",_ioLȞire&# _,\qy3 L\`e M,'q=>?z/YR ˟ZFl'ZZi!dBX ~%i2$  /$>KL{`p}M ql}c:l9f@j}f^|ߵU6Ed3 6D؍ gs\4lh1Q"b`7kk̇ eP`6v#dlաKrZߋs^{y&z ̯KSs~فu^liYU -ki@Q;B9K:]Tdr#͐e倥V.F. @zKXӮ`1`]4>Lj6c)GDzdGh]#qbWZ{04>O

ٵh`yǶ%~Hpq7/,]|tX?>e*r4ɕ_񹀖Oîuˏge÷K Xy))8_eQ`?M}7x J U0 Dǃg v{Kf&n_ϔ,T6&MD+: Yq3ٍ͙;-e ƕHMġ ^Ol5,H뜝{3@,3ln tZ :0̸,MG5#.XnlfLkhB9!3wXr(ؔ>=ג=`EbCA/ bO(ڄd}L d~+WH&#{,gR!(t춍 n)^e*( _L@0ؘL-=lrApr `v+7=`6p#{l&>NlR@3`K{ OAr9[oXe;5ZI&/q5U em fEpgK+G 0| q0X;ATI?O6 Z#^ 2U)+Gpl ({iqOWL_3@̛dm)px='Y{;o9`v `v׺t}`^/|eu{H2FњYg`q`pMoS f`D06 @=>:GHg~!`sAN=d_g~LnKg]:pHE,㹲Օ)>a.ς _OohyH 05rN,oݑ88`C^kYMYFNUv#A@cZ6**bؘyb0dfI jݲL֦Ň,k]2ޢYbYPߝV)Ƞi 7׵a Op$2g,r|eu `yR `@VԸ8f\`~3~ [Om^3Ceކ'8YNHȲ:F9W]Tm8Nv({ Ɏ@fW;X:d6&elLi^^0~'dctjw:WNXbFȑpEuVMpQ`0>fgfOFJ@V; -eȲl 0R&M6$lx=NAdihw.vӿ U򿧂eS Wn)}`'xDb~ha@ f/UȂMmlDN dV8 ZSG]q?q<0os>;?F8=sdI6:S,`b"Јw!^]Tò-Zz-L f]@bdGaaiz" ,Yȑ6d>.Iqf9&A:dɉ.s,`b@jKkTPh:_:zMg4VpXXBPk O7(iUz @qֹV чdv}]y:-4l*4/]>c~/UI~i_&tVvCc7cyGk1iO<O`f9͉5HmDpg)x{8Mޯ<]n/gTNFWc|}~_~7  P vrJ230`P=kkX%~n3LF(gvEEnjs2">5QZsNd0, ͖1(6]utW`Vv8`S9%)Ϫ=,f':9~Wm~NEWe+ f  k 0Ci8+A>3s)X3N١,Xs b[niͩ8sy`F]5n.,骳jNYJ& %.s-e :8>G/ˇXQ؃VToJA Ya&HT9ozYG3bR~ي+ N瓏7W]]}]Ax>lk=v=煘xDrbj}_)}.7,5PM&4kn@'?x)몀n}QN9t^d~F#x1B9`AO9:])24Z`ltEZ7EDHsnؿ 82!(po 5%rTORsr ̪i"ǰ>_g`Ǵ8r6=BLe XW@CoDg>ntvt*`6mty?xh]aX5vֽ, 0qѬHw1eW_Mqr( ܔ\t>vφǀ.p/w ܐ֗f;H V 4S^ҝU|@Lc|:"X~:IؖK:*&쟙'wqJt{&dR=}tv\4Lo@q޳N| Ӵl9\u[ٜwgvU:ǭ1>t$ sbO~ @X%6dT Px.1~t_.q@q2U _0" NKp(=l|Ip1P +,`W6Td$8!Y@0?-V` <&CRds<ؼ,lgf :lL[,; N}?MҤX9[Ƥʥf"`#t V-쬢*}ze C5|v:L(' .;ۏ %RNl/'rf1(޺P׵@E2|[Ba 90sZ%`^0k6&J+:dwh%G jHIy1H}NMlbW?*fd- YV~x˿j$&ZHVxJkS@պ\>65'8+*ϸ3*eeeIPS^^6>(~r~vz9t^%g20ie]w~wfg{EzQDE~(*PXcI4Y3vE޻(1j݈ 6βlo?XLݙٽy晭3Ss@$6Sh;L0dGc3\@PTj|?=6CD$ͫ* 7}|f>|-_.`@6fYVZ C.Uw&kKLEP$Уphٌ<3gNƗtq> nSX8(n P @v _~ RB̿]flED>% =;~ְ/Jl1Eyq&ȨXf`)owpBv><^ ]"mG:&d⇕mQ|?ȷ&.O{KM>!?ן`N| ,8ałO@a***]AK$g "L/#'@p>rVxf/vG}!.'`?RA5¡IZq/ zb&aЏK0X/%s,SrJ)9T 1hy+vo[r>SL ΉչscTY;n%m 9d2 ZޤX-0YDUZRL z֏'R 1{3AJ~(G/} ɑQrr(hOdp"dd>/0(@Fg0AX5o\L8Omx3RBzuQFSN] Ǝ=PO>\N>v/{$G~0#ӵ!2+ |B ']dy P)HLGV3dT!J` H{,)r܂- 0 LMӘ3QkX Sw/ôTlgIS@qCܹ~Y B(0զ z -`w3|TqY!-R^ 8R<=uU'Q.wh%i ОkIY\ W}@/[@v)}P2dk}*z$(_$( x?X D+ +3O -oо_!9% %`6kg ._lAn"=ԡ> >d',dX.BT^+]~_$9MEh@yw&W%|+SS8!***2gfwSn g) ʦ)S'J:D¡=#e:Hq9+gJd̈tħ(EΈT0stx;Vo3*G9٫XԼT㺇py~QC@`(_N8Ā*p|DdiNY-=_`sMУ΂q{;J =Cg&M%7D3}A@2 [K@̢Y? ̔FAn9]{N`)}.-| ޑ$6>O6pl.bC5AI.`IaPFeWhzڳ>}8d c^h 6JZ~Wֵ/=N݊'—\x-EEE^)+**05 N۱gE6䞣3:olh fQڤIy!.1 eAD<$,R,`nOvs f4Oacl_uWS{2}u5R7B91ڏ4rJw8PAk?ockO)\/'ӌ\oߕ\gP7"Ӿ=\Ν9+&EAq-84͕S-"iQ i` pMឩ>*>:-c68ߐ6dM9Y/xlG e矗pAP,u]0+1'ww]GnEXz䇞(ΒM쫠yg2[LB#KzP!D;r=uvAHY ˮ@`:Xb:w hD`E~r>d/lSfF+TM2% @GY R:H_),չRt+6-ct9\Y1#] LIy? 㕢ԐP|(7Ó#U8gfj?Bt_)<(YWMSkW>r]^Lrۈl9ڸֵ+0 G\?0uvZ&K@T},3r *v L,3v;`f`xd*dViI(K]hdL/znp%;ӈ-G"7v Px0kl G~9?LlY?Zum9^ `c`f-IK-Aץ#ul5zҳVi?dI_Uj/Os`9i~W$/ѓ~ sb6EW(}Qےd'0镭]ᅢl-*[x2Jو+pD$l =brn3&EEEiRCfVHV1j-<]O Ry<\Qgv;DLmd_!5Ѫ0`ӹn=*療e&1.3d`u3SAXAyA9n&5 O4ȑ:[=`#Ntk\C lSS.dPL}6F,KO^R@ k"7w b{=N׽R!2`se7sٸ9`F9uJ{W2NB^`ya_uX /XԗAzofYsWJFl{Agc_dg֚ה&ڦP( bmtFVpbXp""9Rr"Ml$g}) ŀU f;d,TYm3ZU/0S=(Yrf鐻Wf콫7,wi?$}ԥw`Y0"?Ct|PgG@p1 ]Z ^`O_NJ[սdJ-r>S .IT&@.2gSưC(32[u3Xl%9mLvY>&M<*Oc ˚be0s"M`5 \.0&})1Pi%"I|0=ȲX#'% Jl@i`toݓhK0szto[0x' 0D^A3WJ|Am-.pd82zZXqD.XVt`Y`] tz֓=< ], 839S lL*TP:&C(3jxz^ DʔFVv< ۋ"\pb&Xhoz'O=U@x!kT&P^Wy|x^cdy~0&˖&3&Ma1V,8U*8U)Sr(NYP0k ?|]☝@ '(g"2 mQ >oABPݜǿֳҊA`̀Yk]5d2o3.1ZV/U4ܩYm\>ܝfOFpzAF# %X3rbd}ףcL8hMX3B+R\mߘQf*d}F&3~ j3aY\q(W=ߟ] iiAǢYIz𕀅=;d\ Y Q߿2=U?JZpd\ K^ 3t-) KLy<7ˌ-~d=nYpX]3L{yX2$ ZdG✍N"#@'Y c/% :&֣RY륣>`1o^:v&_et1:GLoBm< G<ˤcTV,8nOlh0Sf!VLB9TM2X= mHy,P )B&u#_3t- R fa~okDVW0X`Wa8;AN&Ȳ8lJ 0di^gw| j^Z|Zp} ñ |cMCA`ߏ|w3Ѥf} vX&0f@j8% 8^{R-j"hpb>'8q4Mس t쬑`R~W)@Dj51*1R jpBGgX:g |A_ 巔ɫ V,8%Hc,**&cˮ ׂ}f싹 +LYO=HC8!c2@o|(]$}HG82lm$AM7F9)pk=W/g*3gZ5.gь+ I :k < , z*POcm'>S-+V g^M0Id|a(LfwY_'34\b^{=t`rho0it:ARDG{sh=i>[(|Q*iͿAᘂq]u. )!HS#3AyEHF NthF4)#C6N|^OU%birBփ!pn)PpSҚ'?͵(}83{mTq5kjILs!)Zf8Nq< dqr/Gy? k89N:}+"uJwS NK9i%BBNDY}9~^vϗ TRyhR 5 7Nz3Y,U#&C9G;QpZl˺ ?[c'ePc3dve#^SGk" 8:bQóD7̱'21YWj}ҤJ$.VR'PV󚚑&S`:Ap9 e uQQ'BV +,5AD7&T׽3Co.N p:`7{8<ߏZyB݃dk*-G,Yf['x9\ pgE &$x+8;&符e+2AnҹN>.W`#ilXL(Tz3 21ޟ ;`H?+׆zK" ytҕ}BH֢C2]Rq@L{^>5 @? z0B@  Z*NI:/5{}+F+fJּ1*rMNJz> {\كA hKrǍ5Xl SsDof}HQlb8 lf8,+{Ի7pַXuxrڥC>Ȍ{OPAM&*e6T d6d@wT@ H f7Ft3>xZ~?6,f^`,X:iWAYu)b:Rtahtyw<ߛҺ:UԆϿLV'Y`3iyʝ6$x^@}Ei,KLp]tB/0ye&y #@ր_J/K 27 It6=`&,鮞 L^h_׹\{V_NYttuV,@OК$Q͵cSlve]7]F OzjM+H\@'p"j͵İHWLj9ٳdt1Ip5زQ1A[2 :)x ̌ a4&Q#w IDATӚtS-@(X ga4%UFէ@aj}5b°6Y6H#0烴Bu~̀S¹l0ٰpfg:5I~.&,xeﶕ=L]Vgk6Xn ɬe]Ks(?2B9`ϫd?S>Wð"h~-'نr$ԋγ/P }_M@ q1xHpq]߿:Nd[ڷ%!8qN KG &jpVSȔ"}4 G{t/ VMfqbėt0cGDFjDiV]判RJ_:\}+s~(dʡxy+{,X fS@׀R9`@J}T8z<)g%94kEO97nzatTbiW k^ K{\ 0Bk ,B~oM^mK܀BtX (6)2F8#f=Li]5zNG'~ &8ܹkG.:>%o{p9XO 1 -Bm 3j߭mgj[!>NKkA(b1 :J~)*)AV~3K85~6]%`b6: + 9Hׂ,9Sd_SE7tѲ߁x9L0MAR{LXp"ii7)**2` ) ID`ʔ)~dOmK3ER2 l-#ێОrӓMg`P큓Eu}ݔo2Y םE7(-w5 ~ jM$9 }TJl ~pzx<2g}yk EI +Wd pJ(}6=To.X0^7 ^&\ RPxd*0O0"[@TZ{N Y`5"ÕҪd#P Z,"AU3T̶ ol铕$;ckiTdFUgVܢ8ǂYz9R jeZ|lbݞaI }] `T8hQ L׻M`%áJUרi0FWplF=>f c>߶QzM`&k;?np ']:R 2 ' KA9O>XFNT/>cApoz^6z0Ѱ̰ߒ@[`DAl- . d 27~oɹ/+6){%q(?u <mG 7_"zeJlPK/a@н&V̒wәM& (]`%6=/020qo׳a+IXZL9Hf/aE}+r匌я2eJt^A8Gp.ϴ[3u Rfa-jr=V¡Jy6lɊlR)eg ]e YYפZ2dM 6RM o(H#?Jd}| Ow<΄R=1d"}sas2 A {Mɕ";ϧ'㮮A d#ݛ tfQ}9U||$Sw<:CA( sYZl1; w}|:g]2rd. 0[s\$}|X4-6lA6_m `\+V,8Šݦ0˴ݮQ ];q-% 5N<f?}*Ε+WF q|98fE R X4 5 M F +Ltvy@M3;om: &"(v}}O'4 ާ+02rBn #$0BGx0?,fC 3 P7@t T'(@p0@ Vh9'ҔՑ4jx! ,X)Z~}UX ;gIh  %}]LlZAsu*Zg|~:Ys_%tкd, J3XD'4)Q xnC8g& Tg'XD0zuf,چ'8M{%Nlp&WTc>up&'g@ %r5!8ug8Y}zRJ?iٕaYpJS'#슱)8<7 64"Gp{*;X:ؔm`_W ^U*P 4٫W@Hf_|̄6Hk@\`_[q?pF')$aKZ޳l{fM X²p e:0y2Kgsn~}KƌcZ2I_驕 a8t8XjxTi ?X*#6(8}[@ N;SGȜh 'cj+M!:o''VFdӽ̴=7']"g = 2NoWiiDJwNWp>L{ҭw `!|r$;l I(=JAx-cWySv/`A6s8jL3d HVAe^הyEraߠ-~:NkWQ98]ԏd%RAfΙWA+m"86a7xӶZ4aEYE"ٞuw*;f' YA` |~*Xd&Zpz~$.g ZWŠ'e , #qu 8ᓣgAZARΝkE`& QպxT^ ҳlWIr.6 v`>rWppb8D0~,mX?AuԈR>>m&/*oK" u^R@9T_5->%{st $2*NW:"#,KYR|@.Of%#fY mSt.ExMor]lѾ5Mgn]$pdj8c# cxgkcn {|̔ &Re{˟ZG+_+Qp$o^F}XR3ϖQ|M At0Z}`ܝR-TQFg49w5AHD9گfX6bSs XOx8 3\s+3R괿LJ9z Jla˲*bEqyLt}6oM`+6YO9+Tr{߬p@^ƞUhgXpJ^`)әs0Kb 5HdNKsdl/;Pw3nXq0vZL$A/ sX9lH#?R4倔mLPvL5Jhzr9/\~T@G+Ԝ<}z͵5HZ'Z&a$р;(0lLи2(?{@`wk.n'0UWm_-:d쩠A`N9YZ)8{@P y:+B8Wjf g1%17Jo X_ڷ=@|~3jw k >@-)w=K2',l>{O i^Av,UkFktBĶ\:P$pbY]u)7iŊ'@2ܝS@ur=,S#qsёrB[A J f2GS8%)`3{*xJkV 6&d/$`R]OY @zX ]FdDJף6w'fj ~j sv RL{1IIsdX;` dmOmShM[jk9d3O͚W&xp<~($\P a?T+<W{:S-v=K3;Xۊ:#QSu,H˔?64邁`YA sPX*h\m1 Z*#+V,8aM2,dh^3e*&MRJ@Ƿe(0V}2b)(ma3)|3l>ru9Eū38b $/kj9^R~3"Az^49V@n@n5 `9?sti`60IИ0IFy謍Y;Zߗ,mFz@Pb+/ֵڌf|~~0S@ۻ`ƺ7KPm^ǁLD0,\X X H<nv8LZ-:ZUAFd\2ZR7i[6+H(IS}ظl)x 5jObҤI=@z`93ܽ`Cx{\$#uyxocH&绋co2:v[R5 ̪(Cy>W¡ʞqi`M\VϿX Suu=6, Fv^ֱݣyi+Vܒ'2L lA?48y f+˖+Ф=%#ǁNYLkZѥwpikA nvt'+TWpm<*im~}g* IJmAp}rO(.g4O15K(#f#.x@ÏGb/_lXğA;m,+7:8YM̠(Sg=W5ƛ;^ f}\p& nP}>fd(;Z`ٕRg@vP),GeI@.{M1 `}@|'+$9c)f:PZE`2 NQdhL,XO0g}*H3"X14iRXwAM㩯Ǐ_R_ yZ9 d+x5h3+ D\=MD\`Fz6c7YS/6TNlVb<"T2d4;PgV{Kgz] IDAT:F Zj8LtޏQ u$0ӳl1֦8,3sG_tsa:K)~ncfGi?A`VpȈK^9)- k, i,8AQ-+{{ {43daQL*dV͠3SsYK{$ 6\$b{Xa{tPݨwGEm{ d<+hA5+V,8!$M!`-24C݋4i$Tl/GmN8qʕ+?TyR2Fۣ R k*âh30Gi4fYF|up{c38WX;%@q]ƃ)Fx1\f] (W@dF>ַCF[``|4F/{5h5qMStfdXqOzsgc~nYK9E37 ۫!E ,:BA65:۳@?dӚWW.B,ۈH#F@<)po}@hƢG܇DM$Lb+|\{ƅ%^@bŊ':9P1 f}|T|\9ʡkOno02 dZ|Rb:dt ' NIC.^]<-,T)/Y=,Q+> Ni \ K.+AH9 q,j Q>Gݵk\S׊ hJS:Ȭ!G7CJF/#Dw 7ʩ=W~ 76,N26H]Ako@CZY+a Ѱyfll=͝x dsw0=+6YϾ1,q(zta`ݠ~]/{2콥R+VXpL0'޽ 1BN4H~'Ϛ5i p5@x @ 2ʳA B޴yaF aaX)0_? ln\< :^1Y;FU`V3{?]A<fPB2䁙V}YiF7w> ֔oٗ2>|N=s3N`ئfƎ #e Ȕ-\ooj4K_Imd_g%<׵#?0*K[Xl"tnKи)%w8R@OX:([+V,8a%l*l^9JAטk'MrEill7Hɻ.V f7e{{ȁOF:]:91 /  35-/{,8L!= 9π~rw.8!EMmVGƫcs@iS PxZ\݁u/f&FΩ 1@d=Mzp/$2ͺǸ-\^Sdb+d}8 GswA=u/w7\|**8 Ln M#r@&eBVXpʖ*De(f UI n{k?)N l0#apjs9Yz.V_nGV—t,lp1r]{ X:@>`8tMPV(L@JuV `y) 4mrd',\cG9 rtSӋLG Eu~q]@ݞ{*p{l #d {h \SxMlAI z,ro13+6ү̀S/$蕽[8,jY) &Ê+o ~9 #b0NC f 瀍wկTǯ@w`M)p[4"`4k@:u4JL@x@`,v (f {Q[shE~_ز>(SyN31ƔdyL1L1ߛ`4ˎ62/}]pny+@k7lYDkqo@~ 4t女5,p볯.xc|dC 2fڟ ,TQ6Ts{0#~Kԙd6޽ 8UZVX* R2&{'HVt840s<eRGN|lY"w I魁r`qp s\MUs+ϫkzB{" $̀"f2mLО#@n0[W9~d6?DHl]c _kCCyoooK TGm=1߬{%~ir. ݹH ?R0!-`:9sLpTdg4dG #vp-Ȏx,g1숤aJg<L ^o߃ )M5[4Aс 0EXW/bŊ't1L ¡r<3ynI"Gı{I 0toFj-PJ,ִ;X;BF rN͖hUш̫o`AN4K2HR+A9jOzG4i `\z(Pr\ʹ, ݠB#񽦹a4*6 x7UMj]k;ݬsMrWzk=O9@&P@l`M? ,?;"| ;A C| *Q6$̬>'t,&_ @vӑ8Ru>T.~2`G*Qt`do\-@f"j?uXsy{oΥ`Š+LRt:Q f3*л[gB8cBur>;ݖ,0$x#*Ƶf}"(H]<^$0Ev֡zւf6Y z@Zy2^V nh= N8@')wpp&Ըp=#]))0@*ҀRV*|`2Ը:H`Ld, `ZP65p>]e;| : T¹hnyY sn!0{ d}$;[M' jSt A@u?]ߎpJ,_HoW2W ႒@8}FCu$,_$xd-*;s^:X.sS|0%5It+VbhtnT v#} 0›>E_Wu#'d 0)=/WP_?BdK\f**g#WTw}{+8ʂC7f|*~ /^.@- >x,u#n~ 1NwQ#g\ ]U3הFo*K#0/Kζ\Y{4njɳ`F׹IrLa N7a4jmܠMG`i(hKvGDOpb$E/TNy*H0+6=Pgm?fOq}@'z ?=Z%1JnYUAɣh~fW(>l^ #Y"g&Xb5>N6WhofDZ('tluѭX+ c 9 DE}d\'4 r~!()*@~rDہ\E`<9X}fwlQқ`M@\TPT}ueHP4dO8wW|h12 w [7Btf~w)C A~gkF)pɳQH)\6p\{d m߹A+NA<9ӵF]qU`w r h{@O/z<:ۃ% D07R;JYk661zL?If:-H= _udULEt"zu/SPmX:NjAs]u ۶i:[;%#BWwp"E.j6ĬhCWS{eiw+ҹū@9^{RWa0 1`{lb"rJ 6:1Q`G 9>4e;T*\}d.77 ÿVU& ˒)FQ0@x\B0rv u_zD0&+Sa!NjP H TVt݃ kݍv@gNxu{v[ ~6|+t.>m|`x) t~_ި |=[6'4͆gjO'Kxr{6lco, ̦Ystl_kwK)#gX]w!W#t|ufl6*,kL-ŕ iŊ NX4w7XdT3)Bx1r{ da-kGGq<֜ν8gJ֣@׸@6r^p2K/+,x@ +ȾNMnWb"HǭǏޯϛqJ@1Թ tKP-)toMW 7u|GA偀e3%pb1btzA=+u;>3C XLNZz't ]+Q@FnYǺ6־@%D  ެ{ \Ps@6Gk0IɲOLk1Xf?0 *x`a`ݵehŊ NX??W{i9Pk[l9Ar~Ca L.>`I&j9ÌЋFAY` C4M.3eXLAL}4}/<' k\ϏWlD!Yb}_,Q XX{DM1VUWL.)~,ۡ`PepJs6r<:G@3]b~S GiuWLG&[3H?3-*z^!T @Z J?+VfkAyl`Ⱥs\ z X6Ox=y ~zqڱ`kcҹ uvGgH5ϊ+rg+Xތ!)3d`v+z#AC`-&ݥ%M7`qIm)[?sq/( rLPl9:C<+rIwn:qfv׀tg|S? ̐O.Wvfktp NG`(mm| x4*̔=2\h)flm>X4z%.iy0-lY XJz2^#Zyhl2~? ҋAH%uI \s [f߿ Vs/v.:d.f߁l3L6`2l` G#֭Xb +퓞rUi6rVMBvU?fuhr; 55#~ Ԗ_ F׹I6f>Fdí 3,!!O7P(G(px?!Ms &Xef̽ @06b|p6WO`)E]MğtՙY`UΫ46Mfsuۃ0䭗~gV݃^se@ u p=7x=@XRVx:%\K.۶> 7gc`bmV=LI`y+`s#37Ȱ=d6׳Sݛy,? Xpɷ`ij$WH}ȔdJz^^@vڃe!>D}[@ևbŊ'D-NpMƫ`uw3X_8Q-nyzqr3򆿀 @,DtS@5ёTwi 2r"/NK8&ZfE 4ke=e@  `~,Y .`TJxoZ Xt>oYi:VQ!`f-r&@M N? S?Y = e%f| XGk0,O4. _l>dw8HG3 '\g,um_\%ZȺH\3L|Ɋ+#ɒ3|۩` 0>TriXlиBvH3w;_Jt.20lH:MV``Cp[dnQdUfpD/HU} V% (=H p/ =` "[J}'P {&=*Azl\p: z0Tvֵ~2>S Yy 6鱣4ك |/[JTlh鄞 8GXdZOŌ {koI)3F1S 1dAiw`J`;YbłVA`&i6!3٦F}Nol փ4[ACeT HC~3\.d0Sf%zܭ e5Rpn .Ĩ$"l,[*uPH0sGXeE^}LJN3y7x68< VPP<&I߅*7}4Ap&NNj־Y7?#<;X-XWpzM 7ȮKlTٿU}6hԂ,ZAs?TZ2@o`)W-5x]$aD`Bʣ__8[KXb +-y o`<8KUhPs料 0l.rh 6r_K@$Jt$[A`Xo8Ao:8Ro"<1%F}M3?85=s0 dhYP ZAϟ2#OJ0%Xz1+}<@@Ρ`X\2o~9+W IJz݅ `p*Fal L ~0@ k_޿JOde+ $jKIA.l!81 PzǤhwA>^i/8+VX?|}37L N*l>f|(jlLo#Gg є:zuyXp@S 3g/e8QZoGA.E& IDATU#)`GPS0<.#0qXpi߿4^:HuŞT@,9~Ik]fC `rA1 d s;/Ql36MӚ6+Jegzzv3l t}ùΗL7;)@G E{RgU_wFI<\`n>GA{y#x.FAdŊ NXd9RuH)lԴOV0 gA6S"̊n# ]3ϧgogH-fy 6<dœ"G)T)iȱ \>F YDҴOa,q+: bA_֞,Ebm; .~i!1 HN pv ]N M/+cR<>2RpU߿=6:Q `x/u`)+9(F{>fwÀyZ! ŻLN3}*@dv/͔f-z kӤˤOg,L/;\p*{K]dM-W'% XeJ 7 ^ 'I/J~fǵ1j/aXF+b%y Xcz7:`&;E,`RFL0z9 .<\Znpq&<`6I2д2{}~1O0wUf6 K]" vDPT EXxEz}U("QH]ٞǹn;d7m&\I2y~~ιuΑ4V[|QEE^cG 2z.I7Hɹ |HR"@S#lAqq<*$MŠkc;OObAH"b;V)Wfa2tgm51,"(H9;I;U.X1 +z鿔BvZ$8})/M1QGqPdv4ZeZ#:GPnMtJxzz)R9AwV,oT}Fce,I;䏔v IR8%FTlsjyqnhAM:&1X*EC'CJ1"rtN"#DJH`UYtV 'ϤBY?=/HG|`$)"Su"/VʹX9 dnFnc]==q(R?t~ڝnX=T [bޯkw'K{ j'umzT$-$ur&`$,cHf9k4ʾxIO^CprzV~[L|Y\uts? kJ(ۓDKg H(xLNpw0Xz9ѻ=uw{ 壍>o{gǢ~\@.BDU_"~:6Ԥp^rGb?ɐͧygbj0?d4d m eBޙuՅC˼ȡVIS`CRښu; #mG؟ʢ!#<'rrI|z-!4 ku)z/`&_5M}`6Ls/=b{>R^^ TcX5wZ@k/$eDFX$dkt- mNc9ݛWjm;NN89D,% bkߊx}76dpiY&gZ$LN9Km0?IF(ԸX&C,St'"_-k2\'`Ewc_SG_a۱z @VIXGV7 >{ c'g⳻p$E)qB8,8Vh[D |dCbmq }pGa2P4x4IG:zX*:Ne>q6Z뇈)ɲr.}:kg-lEHؠ^)wnX{b9l"u,V}11ӑu ;>]N7TcQw`JeӤR6en:U'!#`SUךe 7)2VaGrۣ>K4Vb8?ZL,bM!a I4)qA$󱴚W4dĎ8̹Ep׎ RyqHˁ_[>5{gtYGkYK! SӜh/ɥ&)(תG,U _טXnr1sr::?p!AYXE Z^iG6b!5ZfJ!]bkߦs BJa$Y^C"cb*]1Z߭$UZ1:y\{&oϿǔ& 㾛12ry+ ]ܱe1;01 2o%~@X(#CX'% ړ:PSl!W{,9HC.FEoG/To"{`lq.VP7y?HbaO.n(D0S"].Au!C0j P_Dd_jȶbQ)t1Ekk SR'$-,[|S`'*m:(b䩺/+0~t_]Wu?XjƮr^c0bLuFڇk\LƊt;uCR1K@sE{UVhѯ(!p 1:m G`9ci!v bF= (wR TidC 5id9zDDXd\4Zc$ʇr[ pSm煢p1gH*rwZ]: 5¡q"ܚgHOb[b(΂rA]OyNN9{w`>~\RxX!FSu&T)Zoaʞm0"}:I,U),(n%O$tAN$oFt}@̓"lVhm^LA"""6F.wU]1E{5mx!I%c8 GoEĒhhpyơ|Cq;+cDDHTDB\>[Jܑyr(HJL 71êȹi8LN^*#[Xb Im:))'OGAꉒB> \)5N80"Bvպ[^rVQiQފ)Ocb"ޥǮ0/ bqϋv(Lŷm]($^HBvBu *IutкHR\."|Oc: @Q0Je "[>ֈH0UE0[bctݟTwO"Uwrk1B!G Co;{\ =hMmz6].(86ZsD38rNâc]R>8kN#CRnI]?NLYwrŎPaLՌ]w5rE8gadArNcGJ9bpR3EN,辰Bɘ!p{l0]N,b~ݲ0 1_8XIKżO]Nd*D |#=gj1a@%+BN\ #v8p8"_ 2I}نQ@i"0:p.&YX'KѽKP![|\1ğV/ y&3&GoM20%?? HmDN௱NaR1" v 5$e1k\ݛpYjCFu: Hkf%(بis!>bby7Q-7}CwjEChVc.Wd+]:לp }zoG E1Yy=={$wr9jI߭Š݀)b#r;TU{˹ڀ՞>S\|WkkOl,l9WG>5Wi.QۊJ1z] PעFSHcIcIZexV,v,=RWw=o` FjLu q'tO}uXj3EdKk3+\OR.q{pr3<)-Ga]/`"<#r@UM,%wq/<#42CNUzꘄbm9 } Щee2^U2e]9TXTAXCĀ92u"(v"/;;?b)Sc1[r:oAyP-a*9[$DH5HEZL*i$!%$љ|̥<Y1XW3pVfpOM:$gqm&s$&t~$ PLaj`}?|v S\%"2D/} #\g߂A BpGꨒ!=+JU|dd^;2"*,B!|~I2ޝgud>AiRsz6wx}·,?.,rׂErR%i6Qo IUXQy]]80phSS<\wjE6bX<u򉏓?N 2RoSXdkKuOPk-i|މ՘xlbJ9^ss,Ecc'=d=qmv:$J"R&N [7wDxH:B@EUKtcdkL S-JwI4c9XDm p"m8Qc?MACmGp,ͬE "+j:%:M`,.&?"79?%gUb k+7GHڿD G9\t**r"" 9Ewi$}"㪙DVܩП~SJͳE:7 Ifga W"${dKjHc(%(;FR}F[X'|׳µaJnaΘ||`"82Wgѵ*QXQXdxѰ-g,ͣNuF$ǁX1}z 혒9,p޻Ɋ7>Q&nW=T/ߩI6-bhtz]cJ2LLޥpr(c&KR7X$b, &? D|$W|H5DEw#rCo5 IN(ē1EB7Xr]:OLuBc1V{殌~@ '[pElakR4ZpuK02f]`nXz?ut]",%c+Wk_:KNjX L.gKY)~m ?h:_Xu˱PcDGj3_X%2b+L1_{tXZX;VhM쒱X/㘒X]e~v8 .PQNa ͹:z]`їDF,򚛀!+64AKIr";Շ\Hwz Ti IDAT`Q"E#4o _[|/R*Lf:u>Fz#fM:ݞ籺_Nn")vǔP!L7r>%$)Q# k툩`5>.guRX9i1ܐ\VGbM*=_cwTP:m)Bݘl*I؍y8pX֭P~Xt&=}1oLTOl{()Q&{J+1bLqȣmpr(ƹL/k tN!I^ϧo$EZh< '#`<;BW1 rvLa!QG9ILhEW/^G~[0Y빽,ޟ{+,.,b }"hwuq*վC5I էJ;0)6w`$XmwUXswk5?!_SL̑3u1+\98CNpLXhߓ00t`$JL8F%La3B)FZo!``/`6FjzDXm` ՞ˆ:Wt@c#%X9a)=b6ǂR'5AOp  m솳RE'am"an#I_{E${ל!ԘEn;VX[雱 ;cDx&cʖ=:{y pLԀ)!B&Q@ cܜdǬǂ,d,Ͳ y4~X/[ :N(F>ֈ)];9p89p\/zSu^,E(iK0QN:fHMdlrb5P_RR2ܳqvj"GY눜7uPBxj3[Hl0K8˰p Qس6kyjM:h.I E$FS;WrX'xߑXnb ?_â0%#NL).ELS.hn~MOIBY嫺}Ec,\&>#H]ܥ=3<ERj6F iC;!4(<uI }zֹ2`J#tz @{ñG0nQ4(ѺUxb$,J3r%waz`IyyܩӦݥqTu3k3pm&+ކ~~pú-F f2_I{R@L|&.z/_q&Z`µZ#㽨2^k ZxD}t PI‡I0QdLq"?3s,(&~ͧhAQuHN *W Sd |oRɔcj5LŶX6,{U`Tv/7pȷߦb)W׳if_r^˚kY2N6 S`l5Cp/6}Ҡy1"+Z# +I F9Q4@t Qg>k @o,% c b: &c%XKܜ_ \RZZzj1kJL,іEHđOrgVy2\@F55NG`֤ӫ 4(8XF=Z|,}dXWxъb} ]tAϒ uu,w K8d"m\9S}ٰ@ӒlzS1^:3|RIwGH.Xu"c zs1Fu'}I,mY}+r8p8 DNQч$o , %GQAv&I#G{O}XPF-TOm&SE3OäUjLÔˀ?nNG%rDF2XX|DFKh>$R%8`*;02Ctq c>XX]1; gcFJGk3ݰT)X5"C++{j}˱7)fiMo/g.F8}m%$ƐQIQ[SAq ^~E5gt9X'Ñ79Q?T#"rt$`˛黎r夾"f%ERPS1.%`]JɁxr7,BX̶/c9GԤ{I(MR"NAq.\7tVߎcv'S$4)Oz+ZNӮ3)@е5pW5HK"׉d8'Ĉ\^.#䫀r5ލq1\D<)j9VkX r`֜uIiV DLx_ OUb5 (y }9AS?GG~-2jT;+NVEÊlXy֍@תɂ;uX 9QuE$<d.Út>P8DDXdbw\)xqz(Vg.jL~| VXLiR7X'2,}IDJ4n|sMhZAn+2bI11RV/y4 KymOi.X2\֘hoj]\[ƈGb)Q0%`]S;]D0STHM'1ry$p\c=S}VD{'&''DhF89~$'tzc7ޣL|2EIsM#,0]lJ!9IDF6wΫ+gx,:^ ?ȆRE Ft$airRӹc@L6d=IuSKao#IFOޫ%zQl,'0Q|=4/>iE 5tc߷/mLqNKG`mz?kKWD,%gD #z1hU,q|8IEH QkYE2QbXO˱\"h r<!O6QBQd"#Y9G"{C?g}!>yLu<&^KaQ  .ervĔNd|,~%G8Qtք堍 5J"U}ϕ7c<+,¡Cysˡ/Eu_xOĵqUbk8&^,'D̓5_~ziڣ7Y8QseRk=Kg!h>on IQeȊ.bbYC>׮SrsWi?[ֽ 5.4'!Tpr8JF2z !"a-BԽ!qiMCP brETLâڕb+∻CSPw ߑa{!i S? gM:]"'`Qt޿IvqϜɑ?#GoO^YҨyMLGgxI7Gs8c%+H =ghD=g+tRҹ!(Z"'jHe:`rer  jA9?3܁DXfo&vs=ڱзai{v{c 02!YnԔwFw儇$1MXxݷ D*{dKG3&:AkJu%pLM:zaX"~H7kGaUEecY $h4Șd<.A:LXR\t/p{P݅E_#y1&Fx #I9kgY,:$<SQM|[хcU)[bAp!OR󰴢Ss8NX"H)+,H1V$xH쨈pE(rr=Q;I!aI>b]"NDD=I:E*,d7y+9`*Tվxڑ&4DM:l7ZC<S,-{X]Qva )^:w̕y9Mk;#mO Au1ʰ"^8&~> QJ^` cPb 9/^l:'lxk49z 6bո"U#dMZ&^ߏ5k'Z*ǿ۷*5`_, e?9Wt p__N$_?׶ا1 9ʿs (]Ng Rv_Ir^Zޓá+Vd"f:GUcw\[!>dK;E11}K ]o-7T1^9eD.=)Iսb qOgF] 0V"ҨL?Zѐt,5,1"a~UW$iXq'4mMP8,>SԉElb^cP:C e>a:[b,T*eEv-INvT dTɐ'2(m7dW|8|DNᕘ/naRr>>#d&iZiOȉzȮD~|cʣL]cdh^qIfT6ުRb@=FL9mށI0oð5tcm&M[.d'ld`)5oO{<X0RI9K]MQN6TDuWN13Ih!%,gSkڄw᫜y҄Xnubpr'v]/6YV!#bIn|"IvbѪ2ӟhZ&#gHbku̷hږ2h ȱ2"'J(pm(}x9_)Q\&I ~<]HLOqwFZ9`hGqS+GU]?c@>P-AqTɜOWNwy\cd;]PXi:A6HI"\蘭0ۍSS6-$"6`dh8ޫ?;s6)&Fk.ܝ#S$.g~V{ǐ0_>Y$r6 e|Z<տxA$AP|屳D R&O qָj `?y~&^X{@KZDǐC9˒O1rpXY9@ߣ~\ߒJ#j鵵Xu3_L 5_z0r7)DWk 97b-mqf`U˰` /jZwNX:r`Rr}֨o%_f꘡kN^H~sk0pН}o%ΦЁ_']H9pIM:]7A~(gX}Xm@Lh^a јbe:FdLL1JN~XM$*4uU34Xj;zL/"r9ȳ.Hݠbʈe ށnGӺ"SEtXo$)~y'(k3K5gª6&^[ 2 K>!gb0nDfGϒ:Ka~0cR,9!\]ȯ$n 3$]QT{Nj`\m&S®IC̓8XR`Oyz,cJ8r5,dWϽ!Fs5(LB@J3̯2`ljh :> xN"z /dzXUЏ[:Fpmp [8fb!_Ƣy1HX,J=&MNM-QoLSF vx,Bو} ̩&.-%,b`e){xߧ |PS]<"XRZ5r ZIU| K+'( XwB1BQ:6M$sكcr؂S7YdTÄ u\h^L: e[ZK;:T{@+Vy*rR~X?eb\߅uhNՓ ׬Tc֜EZ䲣;]tDdBwE,iׁL) ceth_cHa-9wTM::6G`mec 8aWN8uLr6ĢPb$㓰Kn-XcA,a>(l~%'ϔ#RI4"f2`T?"j3y9 e]=ĺnMkMLUp#0׹?aDeD#;Z0âxX̀)lQJgIs 59}G6kX,'M9t $]iZ{:S?)ΐ{nom>2a F侤]-Z47אn-3c0.>&B VW[L& DtE(ޱ8#"&ʵ}C,, 1 ЄE(4O6w5I2`}Fϗs[@>1LѲ1[ g~`dD Se(hV omG Ɏ::@(:Lm&Ngaj3?b<'kLfuHaCycWVuKa/aU\/p5p>V9 'Psmm1%C8b8.ΗE_GD\Wr2u:k݆5^$$L }4/ ÷-s:ϛEP|+8 ޯ9?\=K[iz 3fc:F\DuZ"޿\C|tݟ?{u8p89 yW$AX+"7R9b9[8 c$/CM;t{ȐX$/8YFfGdD%Q2 j3 ©f2_3aLM Wj3;:Y9!%F`8ٜ.M,䭧4>'Bf&?=i/Vt`E O.ay1iA$ͣL Z1( RuȖwD#VֵiB3B%Vs )Sh ׇ?;i#F^ON< s4lb^/Ɗj{G1ԉĄ15'?ǪHP.be(c;T`^702VZENQP=2eH1"P#vDtT(sfG"R_ŪQ:L0$ʟXТ9!/:tXHدXGi,RӽX$cE%ǣ m|TX7 P C3u µ9zG !߅LSu=Vg""ϭw6<(ZHI~<2zMeo+-gsDz?z5N 9WN8%DVJ"Zd,Lje6x#HZd.WEDjqjD0r's6ފƅkeGycþ$>E(0!kPbc5/ҹ5bi/3;KBFI FL[z/|؂M}R8V*ąX'X˫5g[Dv,xۢ9Kprx&؀E|)?+.Z +*%S< 1"o$E!;}1t)ba$drxTD*.pG;ڎh_jA:FDm[ca -&4뜊ΩSB ^18g@D{EDXRqȉln^[j,mp89XogyrU~$H*OQׇPU| nh`z:i&9$ U"LA1SL RĦ:͐)"ˑoŪ_2X?`i7e9hK}x Z)[h?QN??톥R:D/a_f`J|Ga/+5w~[\sS[rYk=ԍ MYG[="A H65Gks!֖,"M{PF'n#VS_)jH}r<L<~%Y:5uO:,sZ}H,}h !pr/K?esH+q 1qRq=2dc71+2Ĥ鿊S} ]sdg8¹℡;9JM Hzm+S"XC%7GXwm!&cMN~yMȉ{K)K%oT'aueZV75)FJ1qI ש=#$g"nMYE? A"NRj}+{0z[VAtй+Mkct6<6FMx jv[M{# u.Ta-vםyGÁIUY.э| f(q`䄈P XXh9 ]+B m-Q:BWw?qѓ׌)n󳮟d)X%Lew3H\#[cobFNf2LV_n\S,յXIH/cy gjНU绖-s\P@R[\1]uu{en),0JWoK;iluB$^?Mu0Ԓ-վܜ5K=uBy S6pr5ri AU%h3H(#{ ~UBLm{M^nh}F)r/rbAC9 ׂE ]!K#N毐C/1N{"vTd9,L2|Ek3 ͷuN/aiT^s{/1$Vҡ?ȉYr ʞ:ꬔ_?#F9}yyDo_3h.PqeCAs)NBHH_jS Ip89puLؔ!ZtU+*g1YZW#i[Yq`<-Y,⶚¤\qh[ ',:ڲq{>(/:| 1|F1ǁj =18!-fb'uRL4rɉBn}yD>Lc;K7CR7B7:9'x+L=s(VL5tg`L>C;5t.GC"(q+TȖ}87c $tp> ]";hngQ{{V\CUcR)3D :4x:j??jxKdN1Hr$ctq?# &}eL[$äa;r$cWD<)DVse=nE&}.Zjnfc*#XdVc#2wsmAPm%ў,9=EGy<&opʁ[}r:&&`Vf2R-6xy3E[7GLT>qrK6ԎN -W{yZ)LItQL!Tr>ý-l⭝5q :(] 7>s;.WШȆ{ƒ:OԪur=^pp 7c:d6N*`"#lIOС 0q>74;"IaL]m1xnn /w{́:֩u 组R9;SM94l QdGߵDNj]oʹ#BF/ f2"pꜬäWK{RKX֤uxr_ ggx}XZRoÀZ|Gt R}"rKۣ#(A@ҙ"8#(CqX8D R~|#q1JiES=vqC(Ftn.ly4ӧch؏eGFXj2<߳1Efar]8[(9[%qΓ/iס1E6alIҚ^FTpLci 9CPl#rz:kXd"ZaIQɌ7.Mº<DXqj9Vg]2 +SƊi!10JG"S,$=s-:b:tW͇km]$^pU:˜MĈ©Q8)USEPJ&rAm~urZD^ƢkbBHVbj599Vb2, {(}w"TaiAI~&^l׼GjvIwh8rݗ9S杆!)f0vp&(!iv؀HRGBQo"IT⪗ah_zqטly0KT:HleR;oU4tVE+\@TFDLj.Hp>0zBdu8NN8CSeN!.*Xmu I$1ixNҢhré(uۓsAJx]tm c<3PB< P <w졔r<%~|Aސ:&M].΅Z#I[9V!<&ku>=H,{2yPɔ!Ak (v:_߁a}\&^Ǐk k^ʙ{VrSuBRZ:SBRqFc@.c¡"r㴔pSEbDXћjl{N{Pr8NN8GgҢZl,ZӱVUD\@m!OAGȳ kf1xkbkG;gɩ:SK|S¬$#߀m|/QohA7ҹCkBal8r y=peM:BHX1u!7-KTc>,|T4|υj5WS{)7N8>9$$ԲE^7:UD<,+~0Ip8}u8CmKMgr{'c:BGq$E&o©ɍfv:.ٹmlR!D*,"yc?ܞE>z\KR6JkqqNtf󀷉Ĺ2}BQx0K94 w7̋SԤӍ?j3E|p{M:ЏLg-*=}r"ԽV#nXj#Ba=$&tMճn \)>;*",D$DZ$C@ 6:">.r ERx۳7LI\ Z).W/x<+cynO.3٣H@Uyڰ$_Kj;*9KOG*?"rV6,Z;]w_PEgbėŁv,CX@NHÐVX,bp%p81hb(*)!.),5Y̋FPT9p8ct,ut0eS%] \ lE :p8QtON$B(h([MRce"A%n][p8p8ǐB9I/g1֣1t $zSWDQ (3wlKֹŭ0"cv'計CO> xxk'"-p8p ;wENDsi<8#h15@LTe9N4jIDATqז GH,>ۡd@"W܎G5:6 XkEaprp8!f."(섥l L0*"# E%Yz{]w&aѬc:`mpCG&C_Iw,- M$-zg٭|[5g[[DX߉ p8p8cH~]"'"bUw|:JԋX%`6HhX%JEtDD'>n3p8''~7a X"r5"∿Ap8QP81d&a!V%8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8GV̈́ NLIENDB`paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/intro.py000066400000000000000000000062411417573700700255430ustar00rootroot00000000000000import logging import openpaperwork_core import paperwork_backend.sync LOGGER = logging.getLogger(__name__) DOC_ID = "help_intro" class Plugin(openpaperwork_core.PluginBase): PRIORITY = -100000 # see storage_get_all_docs() def __init__(self): super().__init__() self.doc_url = None self.thumbnail_url = None self.opened = False self.deleted = False def get_interfaces(self): return [ 'doc_labels', 'document_storage', 'syncable', ] def get_deps(self): return [ { 'interface': 'help_documents', 'defaults': ['paperwork_gtk.model.help'], }, ] def init(self, core): super().init(core) def storage_get_all_docs(self, out: list, only_valid=True): if self.deleted or len(out) > 0: self.doc_url = None return self.doc_url = self.core.call_success("doc_id_to_url", DOC_ID) if (self.doc_url is None or not self.core.call_success("fs_exists", self.doc_url)): LOGGER.error( "Introduction document %s not found." " Was Paperwork packaged correctly ?", DOC_ID ) self.doc_url = None return out.append((DOC_ID, self.doc_url)) if not self.opened: self.opened = True self.core.call_success( "mainloop_schedule", self.core.call_all, "doc_open", DOC_ID, self.doc_url ) self.core.call_success( "mainloop_schedule", self.core.call_all, "docview_set_layout", "paged" ) self.core.call_success( "mainloop_schedule", self.core.call_all, "doc_goto_page", 0 ) def labels_get_all(self, out: set): if self.doc_url is not None: self.core.call_all("help_labels_get_all", out) def storage_delete_doc_id(self, doc_id): if doc_id != DOC_ID: return self.doc_url = None self.deleted = True def _fake_delete_doc(self): if self.doc_url is None: return all_docs = [] self.core.call_all("storage_get_all_docs", all_docs) all_docs = [doc for doc in all_docs if doc[0] != DOC_ID] if len(all_docs) <= 0: return self.deleted = True # make sure the index is up-to-date self.core.call_success( "transaction_simple", (("del", DOC_ID),) ) def doc_transaction_start(self, out: list, total_expected=-1): class FakeDeleteDocTransaction(paperwork_backend.sync.BaseTransaction): priority = -100000 def commit(s): if self.doc_url is None: return self._fake_delete_doc() out.append(FakeDeleteDocTransaction(self.core, total_expected)) def sync(self, promises: list): if self.doc_url is None: return promises.append(openpaperwork_core.promise.Promise( self.core, self._fake_delete_doc )) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/out/000077500000000000000000000000001417573700700246425ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/out/__init__.py000066400000000000000000000000001417573700700267410ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/po4a.conf000066400000000000000000000002361417573700700255460ustar00rootroot00000000000000[po_directory] data/l10n [type: LaTeX] data/intro.tex $lang:out/translated_intro_$lang.tex [type: LaTeX] data/usage.tex $lang:out/translated_usage_$lang.tex paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/model/help/screenshot.sh000077500000000000000000000074101417573700700265510ustar00rootroot00000000000000#!/bin/sh set -e # Explicitly set the revision of the test documents here, so we can figure out # exactly which version of Paperwork was tied to which version of the test # documents. TEST_DOCS_TAG="2.1" TMP_DIR="$(mktemp -d --suffix=paperwork)" echo "Temporary directory: ${TMP_DIR}" OUT_DIR="${PWD}/out" mkdir -p "${TMP_DIR}/config" mkdir -p "${TMP_DIR}/local" mkdir -p "${TMP_DIR}/papers" export XDG_CONFIG_HOME="${TMP_DIR}/config" export XDG_DATA_HOME="${TMP_DIR}/local" BASE_WORKDIR="${TMP_DIR}/papers" WORKDIR="${BASE_WORKDIR}/paperwork-test-documents/papers" cd "${BASE_WORKDIR}" if [ -d "${PAPERWORK_TEST_DOCUMENTS}" ]; then rm -rf paperwork-test-documents echo "Copying test documents from ${PAPERWORK_TEST_DOCUMENTS} to $(readlink -f .)/paperwork-test-documents ..." cp -r --no-preserve=mode "${PAPERWORK_TEST_DOCUMENTS}" ./paperwork-test-documents else echo "Downloading test documents to $(readlink -f .)/paperwork-test-documents ... If internet access is forbidden, set PAPERWORK_TEST_DOCUMENTS env var to the path to a pre-fetched copy." git clone --depth 1 --branch "${TEST_DOCS_TAG}" https://gitlab.gnome.org/World/OpenPaperwork/paperwork-test-documents.git fi echo "Updating Paperwork database ..." paperwork-cli config put workdir str "file://${WORKDIR}" paperwork-cli sync echo "Making screenshots ..." paperwork-gtk plugins add openpaperwork_core.interactive paperwork-gtk << EOF wait() core.call_all("doc_open", "20990307_0000_00", "file://${WORKDIR}/20990307_0000_00") core.call_all("search_set", "label:contrat conditions generales") wait() core.call_all("open_bug_report") wait() core.call_all("screenshot_snap_all_doc_widgets", "file://${OUT_DIR}") core.call_all("close_bug_report") core.call_all("mainwindow_focus") core.call_all("gtk_show_shortcuts") wait() core.call_all("screenshot_snap_all_doc_widgets", "file://${OUT_DIR}") core.call_all("gtk_hide_shortcuts") core.call_all("mainwindow_focus") core.call_all("gtk_open_layout_settings") wait() core.call_all("screenshot_snap_all_doc_widgets", "file://${OUT_DIR}") core.call_all("gtk_close_layout_settings") core.call_all("mainwindow_focus") core.call_all("gtk_open_advanced_search_dialog") wait() core.call_all("screenshot_snap_all_doc_widgets", "file://${OUT_DIR}") core.call_all("gtk_close_advanced_search_dialog") core.call_all("mainwindow_focus") core.call_all("gtk_open_settings") wait() core.call_all("settings_scroll_to_bottom") wait() core.call_all("screenshot_snap_all_doc_widgets", "file://${OUT_DIR}") core.call_all("settings_scroll_to_top") wait() core.call_all("screenshot_snap_all_doc_widgets", "file://${OUT_DIR}") core.call_all("display_calibration_screen") wait() core.call_all("screenshot_snap_all_doc_widgets", "file://${OUT_DIR}") core.call_all("hide_calibration_screen") wait() core.call_all("screenshot_snap_all_doc_widgets", "file://${OUT_DIR}") core.call_all("close_settings") core.call_all("mainwindow_focus") core.call_all("open_doc_properties", "20990307_0000_00", "file://${WORKDIR}/20990307_0000_00") wait() core.call_all("screenshot_snap_all_doc_widgets", "file://${OUT_DIR}") core.call_all("docproperties_scroll_to_last") wait() core.call_all("screenshot_snap_all_doc_widgets", "file://${OUT_DIR}") core.call_all("mainwindow_show_default", side="left") wait() core.call_all("screenshot_snap_all_doc_widgets", "file://${OUT_DIR}") core.call_all("gtk_open_app_menu") wait() core.call_all("screenshot_snap_app_menu", "file://${OUT_DIR}/app_menu_opened.png") core.call_all("page_menu_open") wait() core.call_all("screenshot_snap_page_action_menu", "file://${OUT_DIR}/page_menu_opened.png") core.call_all("doc_menu_open") wait() core.call_all("screenshot_snap_doc_action_menu", "file://${OUT_DIR}/doc_menu_opened.png") EOF set +e echo "Cleaning up the mess ..." sleep 5 rm -rf "${TMP_DIR}" echo "All done !" paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/new_doc.py000066400000000000000000000021171417573700700237740ustar00rootroot00000000000000import openpaperwork_core class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.new_doc = None def get_interfaces(self): return ['new_doc'] def get_deps(self): return [ { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, ] def config_put(self, opt, value, *args, **kwargs): if opt != "workdir": return None # Bug report 170: When the work directory has been changed, # we have to make sure to drop any reference to it so the user doesn't # use it by accident. # (keep in mind that self.new_doc = (doc_id, doc_url), and doc_url # includes the work directory path) self.new_doc = None return None def get_new_doc(self): if self.new_doc is not None: if self.core.call_success("is_doc", self.new_doc[1]) is None: return self.new_doc self.new_doc = self.core.call_success("storage_get_new_doc") return self.new_doc paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/notifications/000077500000000000000000000000001417573700700246545ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/notifications/__init__.py000066400000000000000000000000001417573700700267530ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/notifications/dialog.glade000066400000000000000000000070651417573700700271210ustar00rootroot00000000000000 False dialog False vertical 2 False end gtk-ok True True True True True True 1 False False 0 True False 20 False 20 20 20 20 gtk-missing-image False True 0 False 20 20 20 20 True 0 True True 1 True True 1 button1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/notifications/dialog.py000066400000000000000000000053551417573700700264750ustar00rootroot00000000000000import logging try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_gtk.deps LOGGER = logging.getLogger(__name__) class DialogBuilder(object): def __init__(self, plugin, title): self.plugin = plugin self.widget_tree = self.plugin.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.notifications", "dialog.glade" ) dialog = self.widget_tree.get_object("dialog") dialog.set_title(title) dialog.connect("response", self._on_response) label = self.widget_tree.get_object("message") label.set_text(title) label.set_visible(True) def _on_response(self, dialog, response): dialog.destroy() def set_message(self, message): label = self.widget_tree.get_object("message") label.set_text(message) return self def set_icon(self, icon): self.widget_tree.get_object("dialog").set_icon_name(icon) return self def add_action(self, action_id, label, callback, *args, **kwargs): buttons = self.widget_tree.get_object("buttons") button = Gtk.Button.new_with_label(label) button.connect("clicked", self._on_click, callback, args, kwargs) button.set_visible(True) buttons.add(button) return self def _on_click(self, button, callback, args, kwargs): self.widget_tree.get_object("dialog").destroy() callback(*args, **kwargs) def set_image_from_pixbuf(self, pixbuf): img = self.widget_tree.get_object("image") img.set_from_pixbuf(pixbuf) img.set_visible(True) return self def show(self): dialog = self.widget_tree.get_object("dialog") dialog.set_transient_for(self.plugin.windows[-1]) dialog.set_visible(True) class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.windows = [] def get_interfaces(self): return [ 'chkdeps', 'gtk_window_listener', 'notifications', ] def get_deps(self): return [ { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, ] def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def get_notification_builder(self, title, need_actions=False): return DialogBuilder(self, title) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/notifications/notify.py000066400000000000000000000050271417573700700265420ustar00rootroot00000000000000import logging try: import gi gi.require_version('Notify', '0.7') from gi.repository import Notify NOTIFY_AVAILABLE = True except (ImportError, ValueError): NOTIFY_AVAILABLE = False import openpaperwork_core import openpaperwork_gtk.deps LOGGER = logging.getLogger(__name__) class NotifyBuilder(object): def __init__(self, title): self.title = title self.msg = None self.icon = None self.pixbuf = None self.actions = [] self.notification = None def set_message(self, message): self.msg = message return self def set_icon(self, icon): self.icon = icon return self def add_action(self, action_id, label, callback, *args, **kwargs): self.actions.append((action_id, label, callback, args, kwargs)) return self def set_image_from_pixbuf(self, pixbuf): self.pixbuf = pixbuf return self @staticmethod def _call_callback(notification, action, args): (callback, args, kwargs) = args return callback(*args, **kwargs) def show(self): self.notification = Notify.Notification.new( self.title, self.msg, self.icon ) if self.pixbuf is not None: self.notification.set_image_from_pixbuf(self.pixbuf) for (action_id, label, callback, args, kwargs) in self.actions: self.notification.add_action( action_id, label, self._call_callback, (callback, args, kwargs) ) self.notification.show() class Plugin(openpaperwork_core.PluginBase): PRIORITY = 1000 def __init__(self): # WORKAROUND(Jflesch): Keep a reference to the notifications. # Otherwise we never get the action from the user. self.notification_refs = [] def get_interfaces(self): return [ 'chkdeps', 'notifications', ] def get_deps(self): return [ ] def init(self, core): super().init(core) if NOTIFY_AVAILABLE: Notify.init("Paperwork") def chkdeps(self, out: dict): if not NOTIFY_AVAILABLE: out['notify'].update(openpaperwork_gtk.deps.NOTIFY) def get_notification_builder(self, title, need_actions=False): caps = Notify.get_server_caps() if len(caps) <= 0: return None if not need_actions or "actions" in caps: r = NotifyBuilder(title) self.notification_refs.append(r) return r return None paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/print.py000066400000000000000000000240341417573700700235140ustar00rootroot00000000000000import logging try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_core.promise import openpaperwork_gtk.deps from . import _ DELAY = 0.1 LOGGER = logging.getLogger(__name__) class SurfacePreloader(object): def __init__(self, core, doc_id, doc_url, page_indexes): self.core = core self.doc_id = doc_id self.doc_url = doc_url self.page_indexes = page_indexes self.page_surfaces = [] def start(self): LOGGER.info("Will load %d pages", len(self.page_indexes)) for (page_nb, page_idx) in enumerate(self.page_indexes): page_url = self.core.call_success( "page_get_img_url", self.doc_url, page_idx ) promise = openpaperwork_core.promise.Promise( self.core, self.core.call_all, args=( "on_progress", "print_load", page_nb / len(self.page_indexes), _("Loading {doc_id} p{page_idx} for printing").format( doc_id=self.doc_id, page_idx=(page_idx + 1) ) ) ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(openpaperwork_core.promise.DelayPromise( self.core, DELAY )) promise = promise.then(self.core.call_success( "url_to_cairo_surface_promise", page_url )) promise = promise.then(self._add_surface) self.core.call_success("work_queue_add_promise", "print", promise) promise = openpaperwork_core.promise.Promise( self.core, self.core.call_all, args=( "on_progress", "print_load", 1.0 ) ) self.core.call_success("work_queue_add_promise", "print", promise) def _add_surface(self, surface): LOGGER.info( "Got page rendering %d/%d", len(self.page_surfaces), len(self.page_indexes) ) self.page_surfaces.append(surface) def get_nb_pages(self): return len(self.page_indexes) def get_nb_surfaces(self): return len(self.page_surfaces) def has_page_surface(self, page_nb): return page_nb < len(self.page_surfaces) def get_page_surface(self, page_nb): return self.page_surfaces[page_nb] def cancel(self): self.core.call_all("work_queue_cancel_all", "print") self.page_surfaces = [] class PrintJob(object): def __init__( self, core, preloader, window, doc_id, nb_pages, active_page_nb, job_name, default_filename): self.core = core self.preloader = preloader self.window = window self.doc_id = doc_id self.nb_pages = nb_pages print_settings = Gtk.PrintSettings() self.print_op = Gtk.PrintOperation() self.print_op.set_print_settings(print_settings) self.print_op.set_n_pages(nb_pages) if active_page_nb >= 0: self.print_op.set_current_page(active_page_nb) self.print_op.set_use_full_page(True) self.print_op.set_job_name(job_name) self.print_op.set_export_filename(default_filename) self.print_op.set_allow_async(True) self.print_op.set_embed_page_setup(True) self.print_op.set_show_progress(True) self.print_op.connect("status-changed", self._status_changed) self.print_op.connect("paginate", self._paginate) self.print_op.connect("preview", self._preview) self.print_op.connect("ready", self._preview_ready) self.print_op.connect("got_page_size", self._preview_got_page_size) self.print_op.connect("begin-print", self._begin) self.print_op.connect("request-page-setup", self._request_page_setup) self.print_op.connect("draw-page", self._draw) self.print_op.connect("end-print", self._end) self.print_op.connect("done", self._done) def _status_changed(self, print_op): LOGGER.info( "Print status: %s: %s", print_op.get_status(), print_op.get_status_string() ) def _preview(self, print_op, print_preview, print_context, win_parent): LOGGER.info("User requested a preview") def _preview_got_page_size(self, print_preview, print_context, page_setup): LOGGER.info("Preview: got page size") def _preview_ready(self, print_preview, print_context): LOGGER.info("Preview ready") def _paginate(self, print_operation, print_context): pagination = self.preloader.get_nb_surfaces() running = (pagination < self.nb_pages) return not running def _begin(self, print_op, print_context): LOGGER.info("Printing has begun") self.core.call_all( "on_progress", "print", 0.0, _("Printing %s") % self.doc_id ) def _request_page_setup( self, print_op, print_context, page_nb, page_setup): LOGGER.info( "Computing page setup for %d/%d", page_nb, self.nb_pages ) surface = self.preloader.get_page_surface(page_nb) img_width = surface.surface.get_width() img_height = surface.surface.get_height() # take care of rotating the page if required img_portrait = (img_width <= img_height) LOGGER.info( "Page %d/%d: Orientation portrait = %s", page_nb, self.nb_pages, img_portrait ) page_setup.set_orientation( Gtk.PageOrientation.PORTRAIT if img_portrait else Gtk.PageOrientation.LANDSCAPE ) def _draw(self, print_op, print_context, page_nb): LOGGER.info( "Printing of %s %d/%d ; DPI: %fx%f", self.doc_id, page_nb, self.nb_pages, print_context.get_dpi_x(), print_context.get_dpi_y() ) self.core.call_all( "on_progress", "print", page_nb / self.nb_pages, _("Printing {doc_id} ({page_idx}/{nb_pages})").format( doc_id=self.doc_id, page_idx=page_nb, nb_pages=self.nb_pages ) ) surface = self.preloader.get_page_surface(page_nb) img_width = surface.surface.get_width() img_height = surface.surface.get_height() scaling = min( print_context.get_width() / img_width, print_context.get_height() / img_height, ) cairo_ctx = print_context.get_cairo_context() cairo_ctx.scale(scaling, scaling) cairo_ctx.set_source_surface(surface.surface) cairo_ctx.paint() def _end(self, print_op, print_context): self.refs = [] LOGGER.info("Printing has ended") self.core.call_all("on_progress", "print", 1.0) def _done(self, print_op, print_op_result): LOGGER.info("Printing done") self.preloader.cancel() def run(self): self.print_op.run(Gtk.PrintOperationAction.PRINT_DIALOG, self.window) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_doc = None self.active_page_idx = None self.windows = [] # WORKAROUND(Jflesch): keep a ref on the print operation to avoid # premature garbage collecting self._ref = None def get_interfaces(self): return [ 'chkdeps', 'doc_open', 'doc_print', 'gtk_window_listener', ] def get_deps(self): return [ { 'interface': 'cairo_url', 'defaults': [ 'paperwork_backend.cairo.pillow', 'paperwork_backend.cairo.poppler', ], }, { 'interface': 'work_queue', 'defaults': ['openpaperwork_core.work_queue.default'], }, ] def init(self, core): super().init(core) if not GTK_AVAILABLE: return self.core.call_success("work_queue_create", "print") def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def doc_open(self, doc_id, doc_url): self.active_doc = (doc_id, doc_url) def on_page_shown(self, page_idx): self.active_page_idx = page_idx def doc_print(self, doc_id, doc_url, page_indexes=None): active_page_idx = 0 if self.active_doc[1] == doc_url: # prefer the current page when it makes sense active_page_idx = self.active_page_idx if page_indexes is None: nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", doc_url ) if nb_pages is None: raise Exception("No page in the document, Nothing to print") page_indexes = list(range(0, nb_pages)) job_name = "Paperwork " + doc_id default_filename = doc_id + ".pdf" else: job_name = "Paperwork {} p{}".format( doc_id, ",".join((str(p) for p in page_indexes)) ) default_filename = "{}_p{}".format( doc_id, "_p".join((str(p) for p in page_indexes)) ) try: active_page_nb = page_indexes.index(active_page_idx) except ValueError: active_page_nb = -1 preloader = SurfacePreloader(self.core, doc_id, doc_url, page_indexes) print_job = PrintJob( self.core, preloader, self.windows[-1], doc_id, len(page_indexes), active_page_nb, job_name, default_filename ) LOGGER.info( "Opening print dialog for document %s (pages: %s)", doc_url, page_indexes ) preloader.start() print_job.run() self._ref = print_job paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/000077500000000000000000000000001417573700700236435ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/__init__.py000066400000000000000000000126131417573700700257570ustar00rootroot00000000000000import logging import openpaperwork_core LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.active_windows = [] self.sections = {} self.widget_tree = None def get_interfaces(self): return [ 'chkdeps', 'gtk_settings_dialog', 'gtk_window_listener', 'screenshot_provider', ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'gtk_app_menu', 'defaults': ['paperwork_gtk.mainwindow.doclist'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'screenshot', 'defaults': ['openpaperwork_gtk.screenshots'], }, ] def on_gtk_window_opened(self, window): self.active_windows.append(window) def on_gtk_window_closed(self, window): self.active_windows.remove(window) def gtk_open_settings(self, *args, **kwargs): self.core.call_success( "gtk_load_css", "paperwork_gtk.settings", "settings.css" ) global_widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.settings", "settings.glade" ) self.widget_tree = global_widget_tree self.core.call_all('complete_settings', global_widget_tree) settings = global_widget_tree.get_object("settings_window") settings.set_transient_for(self.active_windows[-1]) settings.set_modal(True) settings.connect("destroy", self._save_settings, global_widget_tree) settings.set_visible(True) self.core.call_all("on_gtk_window_opened", settings) def close_settings(self): if self.widget_tree is not None: dialog = self.widget_tree.get_object("settings_window") dialog.set_visible(False) self.widget_tree = None def on_quit(self): self.close_settings() def _save_settings(self, window, global_widget_tree): LOGGER.info("Settings closed. Saving configuration") self.core.call_all("config_save") self.core.call_all("on_gtk_window_closed", window) self.core.call_all("on_settings_closed", global_widget_tree) self.widget_tree = None def add_setting_to_dialog( self, global_widget_tree, title, widgets, extra_widget=None): """ Add a setting or a set of settings to the main screen in the settings dialog. """ # We have many setting boxes to add to the settings box. # --> we need many copies of the setting box --> we load many times # the widget tree widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.settings", "settings_section.glade" ) widget_tree.get_object("setting_section_name").set_text(title) if extra_widget: box = widget_tree.get_object("settings_title_box") box.pack_start(extra_widget, expand=False, fill=False, padding=0) inner = widget_tree.get_object("setting_box") for widget in widgets: inner.pack_start(widget, expand=False, fill=True, padding=0) global_widget_tree.get_object("settings_box").pack_start( widget_tree.get_object("setting_section"), expand=False, fill=True, padding=0 ) self.sections[title] = widget_tree.get_object("setting_section") return True def add_setting_screen( self, global_widget_tree, name, widget_header, widget_body): global_widget_tree.get_object("settings_stack_header").add_named( widget_header, name ) global_widget_tree.get_object("settings_stack_body").add_named( widget_body, name ) def show_setting_screen(self, global_widget_tree, name): global_widget_tree.get_object( "settings_stack_header" ).set_visible_child_name(name) global_widget_tree.get_object( "settings_stack_body" ).set_visible_child_name(name) def screenshot_snap_all_doc_widgets(self, out_dir): if self.widget_tree is None: return self.core.call_success( "screenshot_snap_widget", self.widget_tree.get_object("settings_window"), self.core.call_success("fs_join", out_dir, "settings.png") ) for (name, section) in self.sections.items(): name = name.lower().replace(" ", "_") self.core.call_success( "screenshot_snap_widget", section, self.core.call_success( "fs_join", out_dir, "settings_{}.png".format(name) ), margins=(100, 100, 100, 100) ) def settings_scroll_to_top(self): scroll = self.widget_tree.get_object("settings_scrolled_window") vadj = scroll.get_vadjustment() vadj.set_value(vadj.get_lower()) def settings_scroll_to_bottom(self): scroll = self.widget_tree.get_object("settings_scrolled_window") vadj = scroll.get_vadjustment() vadj.set_value(vadj.get_upper()) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/ocr/000077500000000000000000000000001417573700700244265ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/ocr/__init__.py000066400000000000000000000000001417573700700265250ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/ocr/selector_popover.glade000066400000000000000000000027161417573700700310240ustar00rootroot00000000000000 125 False True True never in True False True False 10 10 10 10 vertical 7 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/ocr/selector_popover.py000066400000000000000000000060171417573700700303760ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = -500 def __init__(self): super().__init__() def get_interfaces(self): return [ 'gtk_settings_ocr_langs', ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'i18n_lang', 'defaults': ['paperwork_backend.i18n.pycountry'], }, { 'interface': 'ocr_settings', 'defaults': ['paperwork_backend.pyocr'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, ] def complete_ocr_settings(self, parent_widget_tree): widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.settings.ocr", "selector_popover.glade" ) active_langs = set(self.core.call_success("ocr_get_active_langs")) LOGGER.info("Looking for available OCR languages ...") all_langs = self.core.call_success("ocr_get_available_langs") LOGGER.info("Found %d languages. Translating ...", len(all_langs)) all_langs = [ ( lang, self.core.call_success( "i18n_lang_iso639_3_to_full", lang ) ) for lang in all_langs # Skip Tesseract data file for page orientation guessing if lang != 'osd' ] all_langs.sort(key=lambda l: l[1]) LOGGER.info("OCR languages: %s", all_langs) LOGGER.info("Active OCR languages: %s", active_langs) box_parent = widget_tree.get_object("ocr_selector_box") for lang in all_langs: w_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.settings.ocr", "selector_popover_box.glade" ) check = w_tree.get_object("ocr_selector_box") check.set_label(lang[1]) check.set_active(lang[0] in active_langs) check.connect('toggled', self._on_toggle, lang[0]) box_parent.pack_start(check, expand=False, fill=True, padding=0) LOGGER.info("OCR selector ready") popover = widget_tree.get_object("ocr_selector") parent_widget_tree.get_object("ocr_langs").set_popover(popover) def _on_toggle(self, checkbox, lang): active_langs = self.core.call_success("ocr_get_active_langs") lang_enabled = lang in active_langs LOGGER.info("Language toggled: {} ({} -> {})".format( lang, lang_enabled, not lang_enabled )) if lang_enabled: active_langs.remove(lang) else: active_langs.append(lang) self.core.call_all("ocr_set_active_langs", active_langs) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/ocr/selector_popover_box.glade000066400000000000000000000004251417573700700316670ustar00rootroot00000000000000 True paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.glade000066400000000000000000000034061417573700700272670ustar00rootroot00000000000000 True settings_ocr_langs_button none True False 16 True False Languages 0 True True 0 True False xxxx 1.0 True True 1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/ocr/settings.py000066400000000000000000000042561417573700700266470ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = -500 def get_interfaces(self): return [ 'gtk_settings', ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'i18n_lang', 'defaults': ['paperwork_backend.i18n.pycountry'], }, { 'interface': 'ocr_settings', 'defaults': ['paperwork_backend.pyocr'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, ] def complete_settings(self, global_widget_tree): widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.settings.ocr", "settings.glade" ) label = widget_tree.get_object("ocr_langs_label") self._update_langs(label) self.core.call_all("complete_ocr_settings", widget_tree) def refresh(*args, **kwargs): self._update_langs(label) def disable_refresh(*args, **kwargs): self.core.call_all("config_remove_observer", "ocr_langs", refresh) self.core.call_all("config_add_observer", "ocr_langs", refresh) global_widget_tree.get_object("settings_window").connect( "destroy", disable_refresh ) self.core.call_success( "add_setting_to_dialog", global_widget_tree, _("Optical Character Recognition"), [widget_tree.get_object("ocr_langs")] ) def _update_langs(self, label): langs = self.core.call_success("ocr_get_active_langs") langs = [ self.core.call_success("i18n_lang_iso639_3_to_full", lang) for lang in langs ] langs = ", ".join(langs) if langs == "": langs = _("OCR disabled") LOGGER.info("OCR languages: %s", langs) label.set_text(langs) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/scanner/000077500000000000000000000000001417573700700252745ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/scanner/__init__.py000066400000000000000000000000001417573700700273730ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.glade000066400000000000000000000220011417573700700305540ustar00rootroot00000000000000 0.01 1 0.01 0.01 0.1 True False 16 16 16 16 vertical 16 True False True False True True 0 True False vertical True False edit-find-symbolic 3 False True 0 True True vertical calibration_scale_adjustment True 1 False True True 1 False True 1 400 True False always always in False True False False True 2 True False True True 3 True True 0 True False Maximize True False True True False True 0 Automatic True False True True False True 1 True False True True 2 Scan True False True True False True 3 False True 1 True False Scanner Calibration False True False False True True False go-previous-symbolic 1 True False True calibration_sources 0 0 1 1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/scanner/calibration.py000066400000000000000000000364071417573700700301470ustar00rootroot00000000000000import logging import pillowfight import openpaperwork_core import openpaperwork_core.promise import paperwork_backend.cairo.pillow from ... import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 10000 def __init__(self): super().__init__() self.widget_tree = None self.settings_widget_tree = None self.size_allocate_connect_id = None self.scan_height = 0 self.scan_width = 0 self.scan_img = None def get_interfaces(self): return [ 'gtk_settings_calibration', 'screenshot_provider', ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'gtk_drawer_calibration', 'defaults': ['paperwork_gtk.drawer.calibration'], }, { 'interface': 'gtk_drawer_pillow', 'defaults': ['openpaperwork_gtk.drawer.pillow'], }, { 'interface': 'gtk_drawer_scan', 'defaults': [ 'openpaperwork_gtk.drawer.scan', 'paperwork_gtk.drawer.calibration', ], }, # Optional: # { # 'interface': 'gtk_zoomable', # 'defaults': [ # 'paperwork_gtk.gesture.zoom', # 'paperwork_gtk.keyboard_shortcut.zoom', # ], # }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'gtk_settings_dialog', 'defaults': ['paperwork_gtk.settings'], }, { 'interface': 'gtk_settings_scanner', 'defaults': ['paperwork_gtk.settings.scanner.settings'], }, { 'interface': 'scan', 'defaults': ['paperwork_backend.docscan.libinsane'], }, { 'interface': 'screenshot', 'defaults': ['openpaperwork_gtk.screenshots'], }, ] def complete_settings(self, settings_widget_tree): self.settings_widget_tree = settings_widget_tree self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.settings.scanner", "calibration.glade" ) self.size_allocate_connect_id = self.widget_tree.get_object( "calibration_scroll" ).connect( "size-allocate", self._update_calibration_scroll_area_size ) self.value_changed_connect_id = self.widget_tree.get_object( "calibration_scale_adjustment" ).connect( "value-changed", self._update_calibration_area_size_based_on_scale ) self.widget_tree.get_object("calibration_back").connect( "clicked", self.hide_calibration_screen ) self.widget_tree.get_object("calibration_scan").connect( "clicked", self._start_scan ) self.widget_tree.get_object("calibration_maximize").connect( "clicked", self._on_maximize ) self.widget_tree.get_object("calibration_automatic").connect( "clicked", self._guess_scan_borders ) drawing_area = self.widget_tree.get_object("calibration_area") self.core.call_all("draw_scan_start", drawing_area) self.core.call_all( "add_setting_screen", settings_widget_tree, "calibration", self.widget_tree.get_object("calibration_header"), self.widget_tree.get_object("calibration_body"), ) def complete_scanner_settings( self, settings_widget_tree, parent_widget_tree, list_scanner_promise): assert(self.widget_tree is not None) def set_sensitive(): dev_id = self.core.call_success("config_get", "scanner_dev_id") parent_widget_tree.get_object("scanner_calibration").set_sensitive( True if dev_id is not None and dev_id != "" else False ) set_sensitive() self.core.call_all( "config_add_observer", "scanner_dev_id", set_sensitive ) parent_widget_tree.get_object("scanner_calibration").connect( "clicked", self.display_calibration_screen, settings_widget_tree ) def on_settings_closed(self, settings_widget_tree): drawing_area = self.widget_tree.get_object("calibration_area") self.core.call_all("draw_scan_stop", drawing_area) if self.value_changed_connect_id is not None: drawing_area.disconnect(self.value_changed_connect_id) self.value_changed_connect_id = None if self.size_allocate_connect_id is not None: self.widget_tree.get_object("calibration_scroll").disconnect( self.size_allocate_connect_id ) self.size_allocate_connect_id = None def display_calibration_screen(self, *args, **kwargs): LOGGER.info("Switching to calibration screen") self.core.call_all( "show_setting_screen", self.settings_widget_tree, "calibration" ) sources = self.widget_tree.get_object("calibration_sources") sources.clear() sources.append(("", _("Loading ..."))) combobox = self.widget_tree.get_object("calibration_source") combobox.set_active(0) combobox.set_sensitive(False) buttons = [ 'calibration_automatic', 'calibration_maximize', 'calibration_scan', ] for button in buttons: self.widget_tree.get_object(button).set_sensitive(False) self.core.call_all( "on_zoomable_widget_new", self.widget_tree.get_object("calibration_scroll"), self.widget_tree.get_object("calibration_scale_adjustment") ) promise = openpaperwork_core.promise.Promise( self.core, self.core.call_all, args=("on_busy",) ) promise = promise.then( # drop the return value of call_all() lambda *args, **kwargs: None ) promise = promise.then(self.core.call_success( "scan_get_scanner_promise" )) promise = promise.then(self._show_sources) promise = promise.then(openpaperwork_core.promise.Promise( self.core, self.core.call_all, args=("on_idle",) )) self.core.call_success("scan_schedule", promise) def _show_sources(self, dev=None): sources = [] if dev is not None: sources = [ ( source.get_name(), self.core.call_success( "i18n_scanner_source", source.get_name() ), ) for source in dev.dev.get_children() ] LOGGER.info("Found %d sources", len(sources)) sources_widget = self.widget_tree.get_object("calibration_sources") sources_widget.clear() for src in sources: LOGGER.info("Source: %s ; %s", src[0], src[1]) sources_widget.append(src) combobox = self.widget_tree.get_object("calibration_source") combobox.set_active(0) combobox.set_sensitive(True) self.widget_tree.get_object("calibration_scan").set_sensitive(True) def hide_calibration_screen(self, *args, **kwargs): LOGGER.info("Switching back to settings") self.core.call_all( "show_setting_screen", self.settings_widget_tree, "main" ) self.core.call_all( "on_zoomable_widget_destroy", self.widget_tree.get_object("calibration_area"), self.widget_tree.get_object("calibration_scale_adjustment") ) def _start_scan(self, button): combobox = self.widget_tree.get_object("calibration_source") source_idx = combobox.get_active() sources = self.widget_tree.get_object("calibration_sources") source = sources[source_idx][0] LOGGER.info("Starting calibration scan on %s ...", source) promise = openpaperwork_core.promise.Promise( self.core, self.core.call_all, args=("on_busy",) ) promise = promise.then( # drop the return value of call_all() lambda *args, **kwargs: None ) # calibration is always done at 75 DPI promise = promise.then( self.core.call_success( "scan_promise", source_id=source, resolution=75 )[1] ) promise = promise.then(openpaperwork_core.promise.ThreadedPromise( self.core, self._scan )) promise = promise.then(self._on_scan_end) promise = promise.then(openpaperwork_core.promise.Promise( self.core, self.core.call_all, args=("on_idle",) )) self.core.call_success("scan_schedule", promise) self.widget_tree.get_object("calibration_scan").set_sensitive(False) def _scan(self, args): (source, scan_id, img_generator) = args # just unroll the image generator. # --> we catch the content using the the on_scan_XXXX callbacks. img = None for (idx, img) in enumerate(img_generator): LOGGER.info("Page %d scanned: %s", idx, img.size) return img def on_scan_page_start(self, scan_id, page_nb, scan_params): (self.scan_width, self.scan_height) = ( paperwork_backend.cairo.pillow.limit_img_size(( scan_params.get_width(), scan_params.get_height(), )) ) if self.widget_tree is None: return self._update_calibration_area_size_based_on_scroll() self._update_calibration_scroll_area_size() def _update_calibration_area_size_based_on_scroll(self, *args, **kwargs): scroll = self.widget_tree.get_object("calibration_scroll") widget_height = scroll.get_allocated_height() factor = widget_height / self.scan_height self.widget_tree.get_object("calibration_scale_adjustment").set_value( factor ) # signal 'value-changed' will trigger the update and redraw self.widget_tree.get_object("calibration_scale_adjustment").set_lower( factor ) def _update_calibration_area_size_based_on_scale(self, *args, **kwargs): adj = self.widget_tree.get_object( "calibration_scale_adjustment" ) factor = adj.get_value() LOGGER.debug( "Scale: %f < %f < %f", adj.get_lower(), factor, adj.get_upper() ) widget_width = int(self.scan_width * factor) widget_height = int(self.scan_width * factor) LOGGER.debug( "Calibratrion widget size: (%d, %d)", widget_width, widget_height ) self.widget_tree.get_object("calibration_area").set_size_request( widget_width, widget_height ) self.widget_width = widget_width def _update_calibration_scroll_area_size(self, *args, **kwargs): # scroll area must have the same proportion than the scanned image scroll = self.widget_tree.get_object("calibration_scroll") widget_height = scroll.get_allocated_height() if self.scan_height <= 0: ratio = 1.0 / 1.414 else: ratio = self.scan_width / self.scan_height widget_width = max(widget_height * ratio, 300) LOGGER.debug( "Calibration scroll window size: (%d, %d)", widget_width, widget_height ) scroll.set_size_request(widget_width, -1) if self.scan_height > 0: factor = widget_height / self.scan_height adj = self.widget_tree.get_object("calibration_scale_adjustment") if adj.get_value() < factor: adj.set_value(factor) adj.set_lower(factor) def _on_scan_end(self, scan_img=None): if self.widget_tree is not None: buttons = [ 'calibration_automatic', 'calibration_maximize', 'calibration_scan' ] for button in buttons: self.widget_tree.get_object(button).set_sensitive(True) drawing_area = self.widget_tree.get_object("calibration_area") self.core.call_all("draw_scan_stop", drawing_area) if scan_img is None: LOGGER.info("No page scanned. Can't do calibration") return (self.scan_width, self.scan_height) = ( paperwork_backend.cairo.pillow.limit_img_size(scan_img.size) ) self.scan_img = scan_img LOGGER.info("Calibration scan ready") if self.core.call_success("config_get", "scanner_calibration") is None: # Put the frame a little bit inside the image to make the # corner handles more visible calibration = [ min(50, self.scan_width), min(50, self.scan_height), max(self.scan_width - 50, 0), max(self.scan_height - 50, 0), ] LOGGER.info("Setting default calibration area: %s", calibration) self.core.call_all( "config_put", "scanner_calibration", calibration ) self.core.call_all("draw_pillow_start", drawing_area, scan_img) self.core.call_all( "draw_calibration_start", drawing_area, (self.scan_width, self.scan_height) ) def _on_maximize(self, button): if self.scan_height <= 0 or self.scan_width <= 0: return self.core.call_all( "config_put", "scanner_calibration", [0, 0, self.scan_width, self.scan_height] ) def _guess_scan_borders(self, button=None): if self.scan_img is None: return LOGGER.info("Guessing scan borders") def find_scan_borders(scan_img): frame = pillowfight.find_scan_borders(scan_img) return frame promise = openpaperwork_core.promise.Promise( self.core, self.core.call_all, args=("on_busy",) ) promise = promise.then(lambda *args, **kwargs: self.scan_img) promise = promise.then(openpaperwork_core.promise.ThreadedPromise( self.core, find_scan_borders )) promise = promise.then( lambda frame: self.core.call_all( "config_put", "scanner_calibration", list(frame) ) ) promise = promise.then(lambda *args, **kwargs: None) promise = promise.then(self.core.call_all, "on_idle") promise.schedule() def screenshot_snap_all_doc_widgets(self, out_dir): if self.widget_tree is None: return if self.settings_widget_tree is None: return body = self.widget_tree.get_object("calibration_body") if body is None: return self.core.call_success( "screenshot_snap_widget", body, self.core.call_success( "fs_join", out_dir, "settings_calibration_dialog.png" ), margins=(100, 100, 100, 100) ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/scanner/dev_id_popover.py000066400000000000000000000057661417573700700306700ustar00rootroot00000000000000import logging import openpaperwork_core from ... import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'gtk_settings_scanner_setting', ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'gtk_settings_scanner', 'defaults': ['paperwork_gtk.settings.scanner.settings'], }, { 'interface': 'scan', 'defaults': ['paperwork_backend.docscan.libinsane'], }, ] def complete_scanner_settings( self, global_widget_tree, parent_widget_tree, list_scanner_promise): widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.settings.scanner", "popover.glade" ) widget_tree.get_object("settings_stack").set_visible_child_name( "spinner" ) widget_tree.get_object("spinner").start() parent_widget_tree.get_object("scanner_device").set_popover( widget_tree.get_object("selector") ) list_scanner_promise.then(self._on_scanner_list, widget_tree) def _on_scanner_list(self, devs, widget_tree): widget_tree.get_object("spinner").stop() widget_tree.get_object("settings_stack").set_visible_child_name( "selector" ) box = widget_tree.get_object("selector_box") radios = [] # because of the way radio buttons work, we need always at least # one choice --> add "no scanner" for dev in ([(None, _("No scanner"))] + devs): radio = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.settings.scanner", "popover_box.glade" ) radio = radio.get_object("radio") radio.set_label(dev[1]) box.pack_start(radio, expand=False, fill=True, padding=0) radios.append((dev[0], radio)) for (dev_id, radio) in radios[1:]: radio.join_group(radios[0][1]) active = self.core.call_success("config_get", "scanner_dev_id") for (dev_id, radio) in radios: if active == dev_id: radio.set_active(True) break for (dev_id, radio) in radios: radio.connect( "toggled", self._on_toggle, widget_tree, dev_id, radio.get_label() ) def _on_toggle( self, checkbox, widget_tree, dev_id, dev_name): LOGGER.info("Selected scanner: %s - %s", dev_id, dev_name) widget_tree.get_object("selector").popdown() self.core.call_success("config_put", "scanner_dev_id", dev_id) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.glade000066400000000000000000000122051417573700700277140ustar00rootroot00000000000000 True True preferences-system-details-symbolic True True True image_flatpak_info True Flatpak 500 300 dialog True True vertical 10 True True end gtk-ok True True True True True True 0 False False 3 True True 20 20 20 20 You are using Paperwork from a Flatpak container. Paperwork needs Saned to access your scanners. Important: the following procedure will only work for local (non-network) scanners ! To enable Saned on the host system, you must copy and paste the following commands in a terminal: True False True 0 True True 0 True True True False False 1 True True in True True False word textbuffer_instructions True True 2 buttonOk paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/scanner/flatpak.py000066400000000000000000000066531417573700700273020ustar00rootroot00000000000000import openpaperwork_core INSTRUCTIONS = { # 'Arch Linux / Manjaro / …': ( # "# TODO\n" # ), 'Debian / Ubuntu / Mint / …': ( "sudo apt install sane-utils\n" "sudo sh -c \"echo 127.0.0.1 >> /etc/sane.d/saned.conf\"\n" "sudo systemctl enable saned.socket\n" "sudo systemctl start saned.socket\n" "sudo adduser saned plugdev\n" "sudo adduser saned scanner\n" "sudo adduser saned lp\n" "# reboot\n" "\n" "# If your scanner is still not recognized, please check\n" "# https://gitlab.gnome.org/World/OpenPaperwork/paperwork/-/blob" "/develop/doc/install.flatpak.markdown#faq\n" ), 'Fedora / CentOS / RHEL / …': ( "sudo dnf install libinsane sane-backends-daemon\n" "sudo sh -c \"echo 127.0.0.1 >> /etc/sane.d/saned.conf\"\n" "sudo systemctl enable saned.socket\n" "sudo systemctl start saned.socket\n" ), } class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.windows = [] def get_interfaces(self): return [ 'gtk_settings_scanner_flatpak', 'gtk_window_listener', ] def get_deps(self): return [ { 'interface': 'flatpak', 'defaults': ['openpaperwork_core.flatpak'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'gtk_settings_scanner', 'defaults': ['paperwork_gtk.settings.scanner.settings'], }, ] def settings_scanner_get_extra_widget(self): if not self.core.call_success("is_in_flatpak"): return None widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.settings.scanner", "flatpak.glade" ) if widget_tree is None: return None button = widget_tree.get_object("button_flatpak_info") button.connect("clicked", self._on_clicked) return button def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def _on_clicked(self, button): widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.settings.scanner", "flatpak.glade" ) if widget_tree is None: return dialog = widget_tree.get_object("flatpak_info_dialog") dialog.set_transient_for(self.windows[-1]) dialog.set_modal(True) dialog.connect("response", self._on_close) dialog.connect("destroy", self._on_close) dialog.set_visible(True) selector = widget_tree.get_object("flatpak_info_selector") for k in sorted(INSTRUCTIONS.keys()): selector.append_text(k) selector.connect("changed", self._on_changed, widget_tree) selector.set_active(0) self._on_changed(selector, widget_tree) def _on_changed(self, selector, widget_tree): selected = selector.get_active_text() instruction = INSTRUCTIONS[selected] txt_buffer = widget_tree.get_object("textbuffer_instructions") txt_buffer.set_text(instruction) def _on_close(self, dialog, *args, **kwargs): dialog.set_visible(False) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/scanner/mode_popover.py000066400000000000000000000047321417573700700303520ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.promise LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): RECOMMENDED = 300 MODES = [ ('radioColor', 'Color'), ('radioGrayscale', 'Gray'), ('radioLineart', 'Lineart'), ] def get_interfaces(self): return [ 'gtk_settings_scanner_setting', ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'gtk_settings_scanner', 'defaults': ['paperwork_gtk.settings.scanner.settings'], }, { 'interface': 'scan', 'defaults': ['paperwork_backend.docscan.libinsane'], }, ] def complete_scanner_settings( self, global_widget_tree, parent_widget_tree, list_scanner_promise): widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.settings.scanner", "popover_mode.glade" ) active = self.core.call_success("config_get", "scanner_mode") for (widget, mode) in self.MODES: if mode == active: widget_tree.get_object(widget).set_active(True) for (widget, mode) in self.MODES: widget_tree.get_object(widget).connect( "toggled", self._on_toggle, widget_tree, mode ) selector = widget_tree.get_object("selector") # WORKAROUND(Jflesch): set_sensitive() doesn't appear to work on # GtkMenuButton --> we have to play with set_popover() def reset_popover(): dev_id = self.core.call_success("config_get", "scanner_dev_id") parent_widget_tree.get_object("scanner_mode").set_popover( selector if dev_id is not None and dev_id != "" else None ) reset_popover() self.core.call_all( "config_add_observer", "scanner_dev_id", reset_popover ) def _on_toggle(self, checkbox, widget_tree, mode): LOGGER.info("Selected mode: %s", mode) widget_tree.get_object("selector").popdown() self.core.call_success("config_put", "scanner_mode", mode) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/scanner/popover.glade000066400000000000000000000040371417573700700277700ustar00rootroot00000000000000 False 225 300 False True never False True True False True False 64 64 spinner True False vertical 18 selector paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_box.glade000066400000000000000000000004721417573700700306370ustar00rootroot00000000000000 True False paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/scanner/popover_mode.glade000066400000000000000000000050161417573700700307720ustar00rootroot00000000000000 False True False vertical 18 Color True False True True True False True 0 Grayscale True True False True radioColor False True 1 Black & White True True False True radioColor False True 2 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/scanner/resolution_popover.py000066400000000000000000000123461417573700700316310ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.promise from ... import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): RECOMMENDED = 300 def get_interfaces(self): return [ 'gtk_settings_scanner_setting', ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'gtk_settings_scanner', 'defaults': ['paperwork_gtk.settings.scanner.settings'], }, { 'interface': 'scan', 'defaults': ['paperwork_backend.docscan.libinsane'], }, ] def complete_scanner_settings( self, global_widget_tree, parent_widget_tree, list_scanner_promise): widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.settings.scanner", "popover.glade" ) selector = widget_tree.get_object("selector") # WORKAROUND(Jflesch): set_sensitive() doesn't appear to work on # GtkMenuButton --> we have to play with set_popover() def reset_popover(): dev_id = self.core.call_success("config_get", "scanner_dev_id") parent_widget_tree.get_object("scanner_resolution").set_popover( selector if dev_id is not None and dev_id != "" else None ) reset_popover() self.core.call_all( "config_add_observer", "scanner_dev_id", reset_popover ) selector.connect("show", self._on_show, widget_tree) def _on_show(self, popover, widget_tree): LOGGER.info("Scanner resolution selector is visible") widget_tree.get_object("settings_stack").set_visible_child_name( "spinner" ) widget_tree.get_object("spinner").start() box = widget_tree.get_object("selector_box") for child in box.get_children(): box.remove(child) dev_id = self.core.call_success("config_get", "scanner_dev_id") if dev_id is None: # TODO(Jflesch): better display self._display_resolutions([], widget_tree) return promise = self.core.call_success("scan_get_scanner_promise", dev_id) promise = promise.then(openpaperwork_core.promise.ThreadedPromise( self.core, self._collect_resolutions )) promise = promise.then(self._display_resolutions, widget_tree) promise = promise.catch(self._on_error, widget_tree) self.core.call_success("scan_schedule", promise) def _collect_resolutions(self, dev): resolutions = set() try: children = dev.dev.get_children() for child in children: opts = {o.get_name(): o for o in child.get_options()} if 'resolution' not in opts: continue constraint = opts['resolution'].get_constraint() resolutions.update(constraint) finally: dev.close() LOGGER.info("Got resolutions: %s", resolutions) resolutions = list(resolutions) resolutions.sort() return resolutions def _display_resolutions(self, resolutions, widget_tree): widget_tree.get_object("spinner").stop() widget_tree.get_object("settings_stack").set_visible_child_name( "selector" ) box = widget_tree.get_object("selector_box") radios = [] for resolution in resolutions: radio = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.settings.scanner", "popover_box.glade" ) radio = radio.get_object("radio") if resolution != self.RECOMMENDED: radio.set_label(_("{} dpi").format(resolution)) else: radio.set_label(_("{} dpi (recommended)").format(resolution)) box.pack_start(radio, expand=False, fill=True, padding=0) radios.append((resolution, radio)) for (resolution, radio) in radios[1:]: radio.join_group(radios[0][1]) active = self.core.call_success("config_get", "scanner_resolution") for (resolution, radio) in radios: if active == resolution: radio.set_active(True) for (resolution, radio) in radios: radio.connect( "toggled", self._on_toggle, widget_tree, resolution ) def _on_toggle(self, checkbox, widget_tree, resolution): LOGGER.info("Selected resolution: %d", resolution) widget_tree.get_object("selector").popdown() self.core.call_success("config_put", "scanner_resolution", resolution) def _on_error(self, exc, widget_tree): LOGGER.error("Fail to get scanner resolutions", exc_info=exc) # TODO(Jflesch): better display widget_tree.get_object("spinner").stop() widget_tree.get_object("settings_stack").set_visible_child_name( "selector" ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.glade000066400000000000000000000145311417573700700301360ustar00rootroot00000000000000 True none True False 16 True False Device 0 True True 0 True False xxxx 1.0 True True 1 True none True False 16 True False Resolution 0 True True 0 True False xxxx 1.0 True True 1 True none True False 16 True False Mode 0 True True 0 True False xxxx 1.0 True True 1 True none True False 16 True False Calibration 0 True True 0 True False Re-calibrate 1.0 True True 1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/scanner/settings.py000066400000000000000000000137241417573700700275150ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.deps from ... import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = -400 MODES = { 'Color': _("Color"), 'Gray': _("Grayscale"), 'Lineart': _("Black & White"), } def __init__(self): super().__init__() self.widget_tree = None self.extra_widget = None self.config = [] def get_interfaces(self): return [ 'gtk_settings_scanner', 'screenshot_provider', ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'l10n_init', 'defaults': ['paperwork_gtk.l10n'], }, { 'interface': 'scan', 'defaults': ['paperwork_backend.docscan.libinsane'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'screenshot', 'defaults': ['openpaperwork_gtk.screenshots'], }, ] def init(self, core): super().init(core) self.config = [ ( 'settings_scanner_name', 'scanner_device_value', _("No scanner selected"), "{}".format ), ( 'scanner_resolution', 'scanner_resolution_value', _("No resolution selected"), _("{} dpi").format ), ( 'scanner_mode', 'scanner_mode_value', _("No mode selected"), self._translate_mode ), ] opt = self.core.call_success( "config_build_simple", "settings_scanner", "name", lambda: self.core.call_success("config_get", "scanner_dev_id") ) self.core.call_all("config_register", "settings_scanner_name", opt) self.core.call_all( "config_add_observer", "scanner_dev_id", self._update_scanner_name ) def _update_scanner_name(self): def set_scanner_name(devs): active = self.core.call_success("config_get", "scanner_dev_id") for dev in devs: dev_id = dev[0] dev_name = dev[1] if dev_id == active: self.core.call_success( "config_put", "settings_scanner_name", dev_name ) break return devs promise = self.core.call_success("scan_list_scanners_promise") promise = promise.then(set_scanner_name) self.core.call_success("scan_schedule", promise) def complete_settings(self, global_widget_tree): widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.settings.scanner", "settings.glade" ) self.widget_tree = widget_tree extra_widget = self.core.call_success( "settings_scanner_get_extra_widget" ) self.core.call_success( "add_setting_to_dialog", global_widget_tree, _("Scanner"), [ widget_tree.get_object("scanner_device"), widget_tree.get_object("scanner_resolution"), widget_tree.get_object("scanner_mode"), widget_tree.get_object("scanner_calibration"), ], extra_widget=extra_widget ) def refresh(*args, **kwargs): self._refresh_settings(widget_tree) def disable_refresh(*args, **kwargs): for c in self.config: self.core.call_all("config_remove_observer", c[0], refresh) for c in self.config: self.core.call_all("config_add_observer", c[0], refresh) global_widget_tree.get_object("settings_window").connect( "destroy", disable_refresh ) self._refresh_settings(widget_tree) list_scanners_promise = self.core.call_success( "scan_list_scanners_promise" ) self.core.call_all( "complete_scanner_settings", global_widget_tree, widget_tree, list_scanners_promise ) self.core.call_success("scan_schedule", list_scanners_promise) def _translate_mode(self, mode): if mode in self.MODES: return self.MODES[mode] return mode def _refresh_settings(self, widget_tree): for (config_key, widget_name, default_value, fmt) in self.config: value = self.core.call_success("config_get", config_key) if value is not None: value = fmt(value) else: value = default_value widget_tree.get_object(widget_name).set_text(value) active = self.core.call_success("config_get", "scanner_dev_id") active = active is not None and active != "" buttons = [ 'scanner_resolution', 'scanner_mode', 'scanner_calibration', ] for button in buttons: # WORKAROUND(Jflesch): set_sensitive() doesn't appear to work on # GtkMenuButton widget_tree.get_object(button).set_sensitive(active) def screenshot_snap_all_doc_widgets(self, out_dir): if self.widget_tree is None: return buttons = [ "scanner_device", "scanner_resolution", "scanner_mode", "scanner_calibration", ] for button_name in buttons: button = self.widget_tree.get_object(button_name) self.core.call_success( "screenshot_snap_widget", button, self.core.call_success( "fs_join", out_dir, "settings_{}.png".format(button_name) ), margins=(100, 100, 100, 100) ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/settings.css000066400000000000000000000011231417573700700262120ustar00rootroot00000000000000.section_box > * { border: 1px solid @borders; padding: 16px; border-radius: 0px; border-bottom: 0px solid @borders; border-top: 1px solid @unfocused_borders; background-color: @theme_base_color; } .section_box > *:disabled { background-color: @insensitive_bg_color; } .section_box > :first-child { border-radius: 5px 5px 0px 0px; border-top: 1px solid @borders; } .section_box > :last-child { border-bottom: 1px solid @borders; border-radius: 0px 0px 5px 5px; } .section_box > :first-child:last-child { border-radius: 5px 5px 5px 5px; } .settings_name { } .settings_value { } paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/settings.glade000066400000000000000000000052121417573700700265010ustar00rootroot00000000000000 False 600 600 True False True False Settings True main True False True True never in True False 400 True False 100 100 50 50 vertical 50 main paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/settings_section.glade000066400000000000000000000036471417573700700302370ustar00rootroot00000000000000 True False vertical 16 True False True False Section title 0 True True 0 False True 0 True False vertical True True True 1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/stats.glade000066400000000000000000000160311417573700700260000ustar00rootroot00000000000000 True False vertical True False 16 True False Send metrics <span foreground="gray">Give us clues about how you use Paperwork</span> True True 0 True True 0 True False vertical True False True True 0 True True True none True False preferences-system-details-symbolic False True 1 True False True True 2 False True 1 True False False True 2 True False vertical True False True True 0 True True False True 1 True False True True 2 False True 3 False True 0 True <span foreground="gray">Those clues will help us to make Paperwork an even better piece of software, for you. Statistics also show us that people are actually using our work, keeping us motivated to improve it. Here are the data we gather: - Hardware: CPU, RAM, screen resolution. - Software: Version of Paperwork, Operating system, desktop environment, system language. - Data metrics: number of documents, maximum and average number of pages, number of labels. - Number of times you used each feature. We do not collect document content nor any other sensitive or personal information. Still we think it's fair to request your authorization ;-). Collected statistics are visible on <a href="https://openpaper.work/paperwork/statistics">openpaper.work</a>. </span> True True 0 False True 1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/stats.py000066400000000000000000000037201417573700700253550ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.deps from .. import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = -1000 def __init__(self): super().__init__() def get_interfaces(self): return [ 'gtk_settings', ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'stats_post', 'defaults': ['openpaperwork_core.beacon.stats'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, ] def init(self, core): super().init(core) def complete_settings(self, global_widget_tree): widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.settings", "stats.glade" ) active = self.core.call_success("config_get", "send_statistics") LOGGER.info("Statistics state: %s", active) button = widget_tree.get_object("stats_state") button.set_active(active) button.connect("notify::active", self._on_stats_state_changed) button = widget_tree.get_object("stats_infos") details = widget_tree.get_object("stats_details") button.connect("clicked", self._on_info_button, details) self.core.call_success( "add_setting_to_dialog", global_widget_tree, _("Help Improve Paperwork"), [widget_tree.get_object("stats")] ) def _on_info_button(self, info_button, details): details.set_visible(not details.get_visible()) def _on_stats_state_changed(self, switch, _): state = switch.get_active() LOGGER.info("Setting stats state to %s", state) self.core.call_all("config_put", "send_statistics", state) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/storage.glade000066400000000000000000000033771417573700700263170ustar00rootroot00000000000000 True False folder-symbolic True False 16 True True False Work directory 0 True True 0 some_work_directory True False False image_button True False True 1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/storage.py000066400000000000000000000103031417573700700256560ustar00rootroot00000000000000import logging try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps import openpaperwork_gtk.deps from .. import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = 10000000 def __init__(self): super().__init__() self.windows = [] def get_interfaces(self): return [ 'chkdeps', 'gtk_settings', 'gtk_window_listener', ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_gtk.fs.gio'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'work_queue', 'defaults': ['paperwork_backend.sync'], }, ] def init(self, core): super().init(core) self.workdir = self.core.call_success("config_get", "workdir") def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def complete_settings(self, global_widget_tree): widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.settings", "storage.glade" ) workdir_button = widget_tree.get_object("work_dir_chooser_button") basename = self.core.call_success("fs_basename", self.workdir) workdir_button.set_label(basename) workdir_button.connect("clicked", self._on_button_clicked, widget_tree) self.core.call_success( "add_setting_to_dialog", global_widget_tree, _("Storage"), [widget_tree.get_object("workdir")] ) def _on_button_clicked(self, button, widget_tree): dialog = Gtk.FileChooserDialog( _("Work Directory"), self.windows[-1], Gtk.FileChooserAction.SELECT_FOLDER, ( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT ) ) dialog.set_modal(True) dialog.set_local_only(False) workdir = self.core.call_success("config_get", "workdir") if self.core.call_success("fs_exists", workdir): dialog.set_uri(workdir) dialog.connect("response", self._on_dialog_response, widget_tree) dialog.show_all() def _on_dialog_response(self, dialog, response_id, widget_tree): if (response_id != Gtk.ResponseType.ACCEPT and response_id != Gtk.ResponseType.OK and response_id != Gtk.ResponseType.YES and response_id != Gtk.ResponseType.APPLY): LOGGER.info("User canceled (response_id=%d)", response_id) dialog.destroy() return workdir = dialog.get_uri() dialog.set_visible(False) LOGGER.info("Setting work directory to %s", workdir) self.core.call_all("config_put", "workdir", workdir) # Bug report 170: Make sure the current document (in the old work # directory) is closed so the user cannot use it by accident anymore self.core.call_all("doc_close") basename = self.core.call_success("fs_basename", workdir) widget_tree.get_object("work_dir_chooser_button").set_label(basename) def config_save(self): workdir = self.core.call_success("config_get", "workdir") if workdir != self.workdir: LOGGER.info("Work directory has been changed --> Synchronizing") self.core.call_all("transaction_sync_all") self.workdir = workdir paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/update.glade000066400000000000000000000147121417573700700261300ustar00rootroot00000000000000 True False vertical True False 16 True False Updates <span foreground="gray">Check periodically for new versions of Paperwork</span> True True 0 True True 0 True False vertical True False True True 0 True True True none True False preferences-system-details-symbolic False True 1 True False True True 2 False True 1 True False False True 2 True False vertical True False True True 0 True True False True 1 True False True True 2 False True 3 False True 0 True <span foreground="gray">Look about once a week for new versions of Paperwork. You will be notified when a new version is available but it won't be installed automatically. </span> True True 0 False True 1 paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/settings/update.py000066400000000000000000000037311417573700700255030ustar00rootroot00000000000000import logging import openpaperwork_core import openpaperwork_core.deps from .. import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): PRIORITY = -750 def __init__(self): super().__init__() def get_interfaces(self): return [ 'gtk_settings', ] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'update_detection', 'defaults': ['paperwork_backend.beacon.update'], }, { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, ] def init(self, core): super().init(core) def complete_settings(self, global_widget_tree): widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.settings", "update.glade" ) active = self.core.call_success("config_get", "check_for_update") LOGGER.info("Updates check: %s", active) button = widget_tree.get_object("updates_state") button.set_active(active) button.connect("notify::active", self._on_updates_state_changed) button = widget_tree.get_object("updates_infos") details = widget_tree.get_object("updates_details") button.connect("clicked", self._on_info_button, details) self.core.call_success( "add_setting_to_dialog", global_widget_tree, _("Updates"), [widget_tree.get_object("updates")] ) def _on_info_button(self, info_button, details): details.set_visible(not details.get_visible()) def _on_updates_state_changed(self, switch, _): state = switch.get_active() LOGGER.info("Setting update check state to %s", state) self.core.call_all("config_put", "check_for_update", state) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcuts/000077500000000000000000000000001417573700700240415ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcuts/__init__.py000066400000000000000000000000001417573700700261400ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcuts/app/000077500000000000000000000000001417573700700246215ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcuts/app/__init__.py000066400000000000000000000000001417573700700267200ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcuts/app/find.py000066400000000000000000000022001417573700700261050ustar00rootroot00000000000000import openpaperwork_core from ... import _ class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'shortcuts', 'shortcuts_app', 'shortcuts_app_find', ] def get_deps(self): return [ { 'interface': 'action_app_find', 'defaults': ['paperwork_gtk.actions.app.find'], }, { 'interface': 'app_shortcuts', 'defaults': ['paperwork_gtk.mainwindow.window'], }, ] def init(self, core): super().init(core) self.core.call_all( "app_shortcut_add", _("Global"), _("Find"), "f", "win.app_find" ) # TODO # self.core.call_all( # "app_shortcut_add", # _("Global"), _("Find the next match"), # "G", "win.app_find_next" # ) # self.core.call_all( # "app_shortcut_add", # _("Global"), _("Find the previous match"), # "G", "win.app_find_prev" # ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcuts/doc/000077500000000000000000000000001417573700700246065ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcuts/doc/__init__.py000066400000000000000000000000001417573700700267050ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcuts/doc/new.py000066400000000000000000000014241417573700700257520ustar00rootroot00000000000000import openpaperwork_core from ... import _ class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'shortcuts', 'shortcuts_doc', 'shortcuts_doc_copy_text', ] def get_deps(self): return [ { 'interface': 'action_doc_new', 'defaults': ['paperwork_gtk.actions.doc.new'], }, { 'interface': 'app_shortcuts', 'defaults': ['paperwork_gtk.mainwindow.window'], }, ] def init(self, core): super().init(core) self.core.call_all( "app_shortcut_add", _("Global"), _("Create new document"), "N", "win.doc_new" ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcuts/doc/prev_next.py000066400000000000000000000017411417573700700271750ustar00rootroot00000000000000import openpaperwork_core from ... import _ class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'shortcuts', 'shortcuts_doc', 'shortcuts_doc_prev_next', ] def get_deps(self): return [ { 'interface': 'action_doc_prev_next', 'defaults': ['paperwork_gtk.actions.doc.prev_next'], }, { 'interface': 'app_shortcuts', 'defaults': ['paperwork_gtk.mainwindow.window'], }, ] def init(self, core): super().init(core) self.core.call_all( "app_shortcut_add", _("Document list"), _("Open next document"), "Page_Down", "win.doc_next" ) self.core.call_all( "app_shortcut_add", _("Document list"), _("Open previous document"), "Page_Up", "win.doc_prev" ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcuts/doc/print.py000066400000000000000000000014121417573700700263120ustar00rootroot00000000000000import openpaperwork_core from ... import _ class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'shortcuts', 'shortcuts_doc', 'shortcuts_doc_print', ] def get_deps(self): return [ { 'interface': 'action_doc_print', 'defaults': ['paperwork_gtk.actions.doc.print'], }, { 'interface': 'app_shortcuts', 'defaults': ['paperwork_gtk.mainwindow.window'], }, ] def init(self, core): super().init(core) self.core.call_all( "app_shortcut_add", _("Document"), _("Print"), "P", "win.doc_print" ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcuts/doc/properties.py000066400000000000000000000014611417573700700273560ustar00rootroot00000000000000import openpaperwork_core from ... import _ class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'shortcuts', 'shortcuts_doc', 'shortcuts_doc_properties', ] def get_deps(self): return [ { 'interface': 'action_doc_properties', 'defaults': ['paperwork_gtk.actions.doc.properties'], }, { 'interface': 'app_shortcuts', 'defaults': ['paperwork_gtk.mainwindow.window'], }, ] def init(self, core): super().init(core) self.core.call_all( "app_shortcut_add", _("Document"), _("Edit document properties"), "e", "win.doc_properties" ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcuts/page/000077500000000000000000000000001417573700700247555ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcuts/page/__init__.py000066400000000000000000000000001417573700700270540ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcuts/page/copy_text.py000066400000000000000000000014651417573700700273530ustar00rootroot00000000000000import openpaperwork_core from ... import _ class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'shortcuts', 'shortcuts_page', 'shortcuts_page_copy_text', ] def get_deps(self): return [ { 'interface': 'action_page_copy_text', 'defaults': ['paperwork_gtk.actions.page.copy_text'], }, { 'interface': 'app_shortcuts', 'defaults': ['paperwork_gtk.mainwindow.window'], }, ] def init(self, core): super().init(core) self.core.call_all( "app_shortcut_add", _("Page"), _("Copy selected text to clipboard"), "c", "win.page_copy_text" ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcuts/page/edit.py000066400000000000000000000014151417573700700262550ustar00rootroot00000000000000import openpaperwork_core from ... import _ class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'shortcuts', 'shortcuts_page', 'shortcuts_page_edit', ] def get_deps(self): return [ { 'interface': 'action_page_edit', 'defaults': ['paperwork_gtk.actions.page.edit'], }, { 'interface': 'app_shortcuts', 'defaults': ['paperwork_gtk.mainwindow.window'], }, ] def init(self, core): super().init(core) self.core.call_all( "app_shortcut_add", _("Page"), _("Edit"), "e", "win.page_edit" ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcutswin/000077500000000000000000000000001417573700700245575ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcutswin/__init__.py000066400000000000000000000070601417573700700266730ustar00rootroot00000000000000import collections import logging try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_gtk.deps LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.windows = [] self.groups = collections.defaultdict(list) self.widget_tree = None def get_interfaces(self): return [ 'app_shortcuts', 'chkdeps', 'gtk_shortcut_help', 'gtk_window_listener', 'screenshot_provider', ] def get_deps(self): return [ { 'interface': 'gtk_resources', 'defaults': ['openpaperwork_gtk.resources'], }, { 'interface': 'screenshot', 'defaults': ['openpaperwork_gtk.screenshots'], }, ] def chkdeps(self, out: dict): if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def on_gtk_window_opened(self, window): self.windows.append(window) def on_gtk_window_closed(self, window): self.windows.remove(window) def app_shortcut_add( self, shortcut_group, shortcut_desc, shortcut_keys, action_name): self.shortcut_help_add( shortcut_group, shortcut_desc, shortcut_keys, action_name ) def shortcut_help_add( self, shortcut_group, shortcut_desc, shortcut_keys, action_name): LOGGER.info( "Keyboard shortcut: %s --> %s:%s", shortcut_keys, shortcut_group, shortcut_desc ) group = self.groups[shortcut_group] group.append(( shortcut_desc, shortcut_keys, action_name )) def gtk_show_shortcuts(self): LOGGER.info("Showing shortcuts") self.widget_tree = self.core.call_success( "gtk_load_widget_tree", "paperwork_gtk.shortcutswin", "shortcutswin.glade" ) section = self.widget_tree.get_object("shortcuts_mainwindow") window = self.widget_tree.get_object("shortcuts") groups = {} for shortcut_group in sorted(list(self.groups.keys())): group = Gtk.ShortcutsGroup() group.set_property("title", shortcut_group) group.set_visible(True) groups[shortcut_group] = group section.add(group) for (shortcut_group, shortcuts) in self.groups.items(): group = groups[shortcut_group] shortcuts = sorted(list(shortcuts)) for (shortcut_desc, shortcut_keys, actions_name) in shortcuts: shortcut = Gtk.ShortcutsShortcut() shortcut.set_property("accelerator", shortcut_keys) shortcut.set_property("title", shortcut_desc) shortcut.set_visible(True) group.add(shortcut) window.set_transient_for(self.windows[-1]) window.show_all() def gtk_hide_shortcuts(self): if self.widget_tree is None: return window = self.widget_tree.get_object("shortcuts") window.destroy() def screenshot_snap_all_doc_widgets(self, out_dir): if self.widget_tree is None: return self.core.call_success( "screenshot_snap_widget", self.widget_tree.get_object("shortcuts"), self.core.call_success("fs_join", out_dir, "shortcuts.png"), ) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/shortcutswin/shortcutswin.glade000066400000000000000000000010431417573700700303270ustar00rootroot00000000000000 True True shortcuts-main-window 12 Main window paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/sync_on_start.py000066400000000000000000000032551417573700700252470ustar00rootroot00000000000000import logging import openpaperwork_core LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() def get_interfaces(self): return ['sync_on_start'] def get_deps(self): return [ { 'interface': 'config', 'defaults': ['openpaperwork_core.config'], }, { 'interface': 'doc_labels', 'defaults': ['paperwork_backend.model.labels'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def init(self, core): super().init(core) setting = self.core.call_success( "config_build_simple", "GUI", "sync_on_start", default_value_func=lambda: True ) self.core.call_all("config_register", "sync_on_start", setting) def on_initialized(self): r = self.core.call_success("config_get", "sync_on_start") if r: LOGGER.info("Starting synchronization ...") self.core.call_all("transaction_sync_all") else: LOGGER.info( "Synchronization on start is disabled --> Just loading labels" ) promises = [] self.core.call_all("label_load_all", promises) promise = promises[0] for p in promises[1:]: promise = promise.then(p) # use transaction_schedule to make sure that document imports # are not done at the same time. self.core.call_one("transaction_schedule", promise) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/update_notification.py000066400000000000000000000032571417573700700264140ustar00rootroot00000000000000import random import openpaperwork_core from . import _ class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'chkdeps', 'update_listener', ] def get_deps(self): return [ { 'interface': 'icon', 'defaults': ['paperwork_gtk.icon'], }, { 'interface': 'notifications', 'defaults': ['paperwork_gtk.notifications.notify'], }, { 'interface': 'update_detection', 'defaults': ['paperwork_backend.beacon.update'], }, ] def on_update_detected(self, local_version, remote_version): random_dumbnesses = [ _("Now with 10% more freedom in it !"), _("Buy it now and get a 100% discount !"), _("New features and bugs available !"), _("New taste !"), _("We replaced your old bugs with new bugs. Enjoy."), _("Smarter, Better, Stronger"), # Linus Torvalds citation, look it up :) _("It's better when it's free."), ] notification = self.core.call_success( "get_notification_builder", _("A new version of Paperwork is available: {new_version}").format( new_version=".".join([str(x) for x in remote_version]) ) ) if notification is None: return icon = self.core.call_success("icon_get_pixbuf", "paperwork", 32) notification.set_message( random.choice(random_dumbnesses) ).set_image_from_pixbuf( icon ).show() paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/widget/000077500000000000000000000000001417573700700232665ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/widget/__init__.py000066400000000000000000000000001417573700700253650ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/widget/flowlayout.py000066400000000000000000000210061417573700700260440ustar00rootroot00000000000000import collections import logging try: from gi.repository import GObject GLIB_AVAILABLE = True except (ImportError, ValueError): GLIB_AVAILABLE = False try: import gi gi.require_version('Gdk', '3.0') gi.require_version('Gtk', '3.0') from gi.repository import Gdk from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): # workaround so chkdeps can still be called class Gtk(object): class Box(object): pass class Widget(object): pass GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps import openpaperwork_gtk.deps LOGGER = logging.getLogger(__name__) class WidgetInfo(object): def __init__(self, widget, alignment, size=None): self.widget = widget self.alignment = alignment if size is not None: self.size = (int(size[0]), int(size[1])) else: self.size = (0, 0) self.position = (-1, -1) self.visible = False def update_widget_size(self): if self.widget is None: # test mode return self.size = ( self.widget.get_preferred_width()[0], self.widget.get_preferred_height()[0], ) def is_visible(self): if self.widget is None: # test mode return True return self.widget.get_visible() def __eq__(self, o): if self is o: return True return self.widget == o def recompute_height_for_width(widgets, width, spacing=(0, 0)): height = spacing[1] line_height = 0 line_width = 0 for widget in widgets: if not widget.is_visible(): continue if widget.size == (0, 0): widget.update_widget_size() if line_width + widget.size[0] + spacing[0] > width + 1: height += spacing[1] height += line_height line_width = 0 line_height = 0 line_width += spacing[0] line_width += widget.size[0] line_height = max(line_height, widget.size[1]) height += line_height height += spacing[1] return height def recompute_box_positions(core, widgets, width, spacing=(0, 0)): core.call_all("on_perfcheck_start", "recompute_box_positions") # build lines lines = [] line_heights = [] line = [] line_width = spacing[0] max_line_width = 0 line_height = 0 for widget in widgets: if not widget.is_visible(): continue widget.update_widget_size() if line_width + widget.size[0] + spacing[0] > width + 1: lines.append(line) line_heights.append(line_height) line = [] max_line_width = max(max_line_width, line_width) line_width = 0 line_height = 0 line.append(widget) line_width += spacing[0] line_width += widget.size[0] line_height = max(line_height, widget.size[1]) max_line_width = max(max_line_width, line_width) lines.append(line) line_heights.append(line_height) # sort widgets by alignment in each line def sort_widgets_per_alignments(widgets): out = { Gtk.Align.START: [], Gtk.Align.CENTER: [], Gtk.Align.END: [], } for widget in widgets: out[widget.alignment].append(widget) return out lines = [sort_widgets_per_alignments(line) for line in lines] # position widgets in lines height = spacing[1] for (line, line_height) in zip(lines, line_heights): nb_columns = 0 w_start = 0 w_end = width # start for widget in line[Gtk.Align.START]: nb_columns += 1 w_start += spacing[0] widget.position = (w_start, height) w_start += widget.size[0] # end line[Gtk.Align.END].reverse() for widget in line[Gtk.Align.END]: nb_columns += 1 w_end -= spacing[0] w_end -= widget.size[0] widget.position = (w_end, height) # center w_center = sum(w.size[0] for w in line[Gtk.Align.CENTER]) w_center += spacing[0] * (len(line[Gtk.Align.CENTER]) - 1) w_center = (width - w_center) / 2 w_orig = w_center for widget in line[Gtk.Align.CENTER]: nb_columns += 1 if w_center != w_orig: w_center += spacing[0] widget.position = (int(w_center), int(height)) w_center += widget.size[0] height += line_height height += spacing[1] core.call_all( "on_perfcheck_stop", "recompute_box_positions", nb_boxes=len(widgets) ) return (widgets, max_line_width, height) class CustomFlowLayout(Gtk.Box): def __init__(self, core, spacing=(0, 0)): super().__init__() self.core = core self.widgets = collections.OrderedDict() self.spacing = spacing self.vadjustment = None self.bottom_margin = 0 self.allocation = None self.set_has_window(False) self.set_redraw_on_allocate(False) self.connect("size-allocate", self._on_size_allocate) self.connect("add", self._on_add) self.connect("remove", self._on_remove) def _on_add(self, _, widget): self.recompute_layout() self.queue_resize() def _on_remove(self, _, widget): self.widgets.pop(widget) self.queue_draw() self.queue_resize() def do_forall(self, include_internals: bool, callback, callback_data=None): if not hasattr(self, 'widgets'): return widgets = self.widgets.copy() for widget in widgets: callback(widget) def add_child(self, widget, alignment): w = WidgetInfo(widget, alignment) self.widgets[widget] = w self.add(widget) def do_get_request_mode(self): return Gtk.SizeRequestMode.WIDTH_FOR_HEIGHT def do_get_preferred_width(self): min_width = 0 nat_width = 0 for widget in self.widgets.values(): widget.update_widget_size() min_width = max(widget.size[0], min_width) nat_width += widget.size[0] return (min_width, nat_width) def do_get_preferred_height_for_width(self, width): requested_height = recompute_height_for_width( self.widgets.values(), width, self.spacing ) requested_height += self.bottom_margin return (requested_height, requested_height) def do_get_preferred_height(self): (min_width, nat_width) = self.do_get_preferred_width() return self.do_get_preferred_height_for_width(min_width) def do_get_preferred_width_for_height(self, height): return self.do_get_preferred_width() def _on_size_allocate(self, _, allocation): self.allocation = allocation self.recompute_layout() def recompute_layout(self): if self.allocation is None: return for widget in self.widgets.values(): widget.update_widget_size() ( _, self.requested_width, self.requested_height ) = recompute_box_positions( self.core, self.widgets.values(), self.allocation.width, self.spacing ) self.requested_height += self.bottom_margin for widget in self.widgets.values(): rect = Gdk.Rectangle() rect.x = self.allocation.x + widget.position[0] rect.y = self.allocation.y + widget.position[1] rect.width = widget.size[0] rect.height = widget.size[1] widget.widget.size_allocate(rect) def _on_destroy(self, _): if not hasattr(self, 'widgets'): return for widget in self.widgets.keys(): widget.unparent() self.widgets = collections.OrderedDict() def set_bottom_margin(self, height): self.bottom_margin = height self.queue_resize() if GTK_AVAILABLE: GObject.type_register(CustomFlowLayout) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'chkdeps', 'gtk_widget_flowlayout', ] def chkdeps(self, out: dict): if not GLIB_AVAILABLE: out['glib'].update(openpaperwork_core.deps.GLIB) if not GTK_AVAILABLE: out['gtk'].update(openpaperwork_gtk.deps.GTK) def gtk_widget_flowlayout_new(self, spacing=(0, 0)): assert(GLIB_AVAILABLE) assert(GTK_AVAILABLE) return CustomFlowLayout(self.core, spacing) paperwork-2.1.1/paperwork-gtk/src/paperwork_gtk/widget/label.py000066400000000000000000000101561417573700700247220ustar00rootroot00000000000000import math try: import gi gi.require_foreign("cairo") import cairo CAIRO_AVAILABLE = True except (ImportError, ValueError): CAIRO_AVAILABLE = False try: from gi.repository import GObject GI_AVAILABLE = True except (ImportError, ValueError): GI_AVAILABLE = False try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk GTK_AVAILABLE = True except (ImportError, ValueError): # workaround so chkdeps can still be called class Gtk(object): class DrawingArea(object): pass GTK_AVAILABLE = False import openpaperwork_core import openpaperwork_core.deps import openpaperwork_gtk.deps class LabelWidget(Gtk.DrawingArea): FONT = "" LABEL_HEIGHT = 23 LABEL_TEXT_SIZE = 13 LABEL_TEXT_SHIFT = 3 # Shift a bit to fix valignment LABEL_CORNER_RADIUS = 5 def __init__(self, core, label_txt, label_color, highlight=False): super().__init__() self.core = core self.txt = label_txt self.color = label_color self.highlight = highlight self.connect("draw", self._draw) # we must compute the widget size dummy = cairo.ImageSurface(cairo.Format.RGB24, 200, 200) ctx = cairo.Context(dummy) size = self.compute_size(ctx) dummy.finish() self.set_size_request(size[0], size[1]) def compute_size(self, cairo_ctx): cairo_ctx.set_font_size(self.LABEL_TEXT_SIZE) if not self.highlight: cairo_ctx.select_font_face( self.FONT, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL ) else: cairo_ctx.select_font_face( self.FONT, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD ) (p_x1, p_y1, p_x2, p_y2, p_x3, p_y3) = cairo_ctx.text_extents( self.txt ) return ( p_x3 - p_x1 + (2 * self.LABEL_CORNER_RADIUS), self.LABEL_HEIGHT ) @staticmethod def _rectangle_rounded(cairo_ctx, area, radius): (x, y, w, h) = area cairo_ctx.new_sub_path() cairo_ctx.arc( x + w - radius, y + radius, radius, -1.0 * math.pi / 2, 0 ) cairo_ctx.arc( x + w - radius, y + h - radius, radius, 0, math.pi / 2 ) cairo_ctx.arc( x + radius, y + h - radius, radius, math.pi / 2, math.pi ) cairo_ctx.arc( x + radius, y + radius, radius, math.pi, 3.0 * math.pi / 2 ) cairo_ctx.close_path() def _draw(self, _, cairo_ctx): txt_offset = ( (self.LABEL_HEIGHT - self.LABEL_TEXT_SIZE) / 2 + self.LABEL_TEXT_SHIFT ) (w, h) = self.compute_size(cairo_ctx) # background rectangle bg = self.color cairo_ctx.set_source_rgb(bg[0], bg[1], bg[2]) cairo_ctx.set_line_width(1) self._rectangle_rounded( cairo_ctx, (0, 0, w, h), self.LABEL_CORNER_RADIUS ) cairo_ctx.fill() # foreground text fg = self.core.call_success("label_get_foreground_color", self.color) cairo_ctx.set_source_rgb(fg[0], fg[1], fg[2]) cairo_ctx.move_to( self.LABEL_CORNER_RADIUS, h - txt_offset ) cairo_ctx.text_path(self.txt) cairo_ctx.fill() if GTK_AVAILABLE: GObject.type_register(LabelWidget) class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return [ 'chkdeps', 'gtk_widget_label', ] def chkdeps(self, out: dict): if not CAIRO_AVAILABLE: out['cairo'] = openpaperwork_core.deps.CAIRO if not GI_AVAILABLE: out['gi'] = openpaperwork_core.deps.GI if not GTK_AVAILABLE: out['gtk'] = openpaperwork_gtk.deps.GTK def gtk_widget_label_new(self, label_txt, label_color, highlight=False): assert(CAIRO_AVAILABLE) assert(GI_AVAILABLE) assert(GTK_AVAILABLE) return LabelWidget(self.core, label_txt, label_color, highlight) paperwork-2.1.1/paperwork-gtk/tests/000077500000000000000000000000001417573700700174775ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/tests/widget/000077500000000000000000000000001417573700700207625ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/tests/widget/__init__.py000066400000000000000000000000001417573700700230610ustar00rootroot00000000000000paperwork-2.1.1/paperwork-gtk/tests/widget/tests_flowlayout.py000066400000000000000000000110521417573700700247620ustar00rootroot00000000000000import unittest import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk from paperwork_gtk.widget import flowlayout class DummyCore(object): @staticmethod def call_all(*args, **kwargs): pass class TestPositioning(unittest.TestCase): def test_position_start(self): widgets = [ flowlayout.WidgetInfo(None, Gtk.Align.START, (40, 20)), flowlayout.WidgetInfo(None, Gtk.Align.START, (40, 20)), flowlayout.WidgetInfo(None, Gtk.Align.START, (40, 20)), flowlayout.WidgetInfo(None, Gtk.Align.START, (40, 20)), ] flowlayout.recompute_box_positions(DummyCore(), widgets, width=100) self.assertEqual(widgets[0].position, (0, 0)) self.assertEqual(widgets[1].position, (40, 0)) self.assertEqual(widgets[2].position, (0, 20)) self.assertEqual(widgets[3].position, (40, 20)) def test_position_end(self): widgets = [ flowlayout.WidgetInfo(None, Gtk.Align.END, (40, 20)), flowlayout.WidgetInfo(None, Gtk.Align.END, (40, 20)), flowlayout.WidgetInfo(None, Gtk.Align.END, (40, 20)), flowlayout.WidgetInfo(None, Gtk.Align.END, (40, 20)), ] flowlayout.recompute_box_positions(DummyCore(), widgets, width=100) self.assertEqual(widgets[0].position, (20, 0)) self.assertEqual(widgets[1].position, (60, 0)) self.assertEqual(widgets[2].position, (20, 20)) self.assertEqual(widgets[3].position, (60, 20)) def test_position_center(self): widgets = [ flowlayout.WidgetInfo(None, Gtk.Align.CENTER, (40, 20)), flowlayout.WidgetInfo(None, Gtk.Align.CENTER, (40, 20)), flowlayout.WidgetInfo(None, Gtk.Align.CENTER, (40, 20)), flowlayout.WidgetInfo(None, Gtk.Align.CENTER, (40, 20)), ] flowlayout.recompute_box_positions(DummyCore(), widgets, width=100) self.assertEqual(widgets[0].position, (10, 0)) self.assertEqual(widgets[1].position, (50, 0)) self.assertEqual(widgets[2].position, (10, 20)) self.assertEqual(widgets[3].position, (50, 20)) def test_position_startend(self): widgets = [ flowlayout.WidgetInfo(None, Gtk.Align.START, (40, 20)), flowlayout.WidgetInfo(None, Gtk.Align.END, (40, 20)), flowlayout.WidgetInfo(None, Gtk.Align.END, (40, 20)), flowlayout.WidgetInfo(None, Gtk.Align.CENTER, (40, 20)), ] flowlayout.recompute_box_positions(DummyCore(), widgets, width=150) self.assertEqual(widgets[0].position, (0, 0)) self.assertEqual(widgets[1].position, (70, 0)) self.assertEqual(widgets[2].position, (110, 0)) self.assertEqual(widgets[3].position, (55, 20)) def test_position_startend_spacing(self): widgets = [ flowlayout.WidgetInfo(None, Gtk.Align.START, (40, 20)), flowlayout.WidgetInfo(None, Gtk.Align.END, (40, 20)), flowlayout.WidgetInfo(None, Gtk.Align.END, (40, 20)), flowlayout.WidgetInfo(None, Gtk.Align.CENTER, (40, 20)), ] flowlayout.recompute_box_positions( DummyCore(), widgets, width=150, spacing=(3, 4) ) self.assertEqual(widgets[0].position, (3, 4)) self.assertEqual(widgets[2].position, (107, 4)) self.assertEqual(widgets[1].position, (64, 4)) self.assertEqual(widgets[3].position, (55, 28)) def test_position_center_spacing(self): widgets = [ flowlayout.WidgetInfo(None, Gtk.Align.CENTER, (140, 200)), flowlayout.WidgetInfo(None, Gtk.Align.CENTER, (140, 200)), ] flowlayout.recompute_box_positions( DummyCore(), widgets, width=500, spacing=(50, 50) ) self.assertEqual(widgets[0].position, (85, 50)) self.assertEqual(widgets[1].position, (275, 50)) def test_position_center_spacing_tight(self): widgets = [ flowlayout.WidgetInfo(None, Gtk.Align.CENTER, (100, 100)), flowlayout.WidgetInfo(None, Gtk.Align.CENTER, (100, 100)), flowlayout.WidgetInfo(None, Gtk.Align.CENTER, (100, 100)), flowlayout.WidgetInfo(None, Gtk.Align.CENTER, (100, 100)), ] flowlayout.recompute_box_positions( DummyCore(), widgets, width=350, spacing=(50, 50) ) self.assertEqual(widgets[0].position, (50, 50)) self.assertEqual(widgets[1].position, (200, 50)) self.assertEqual(widgets[2].position, (50, 200)) self.assertEqual(widgets[3].position, (200, 200)) paperwork-2.1.1/paperwork-gtk/tox.ini000066400000000000000000000002601417573700700176460ustar00rootroot00000000000000[flake8] ignore = E722, # allow bare excepts W504 # do not check binary operators VS line breaks exclude = .tox, build, dist, venv*, *.egg*, .git paperwork-2.1.1/paperwork-shell/000077500000000000000000000000001417573700700166575ustar00rootroot00000000000000paperwork-2.1.1/paperwork-shell/ChangeLog000066400000000000000000000016111417573700700204300ustar00rootroot000000000000002022/01/31 - 2.1.1: - Make sure Fabulous is not actually a required dependency on Windows. 2021/12/05 - 2.1.0: - Use system pager to paginate shell About page (thanks to Elliott Sales de Andrade) - doc import: add support for password-protected PDF files 2021/05/24 - 2.0.3: - Fix "paperwork-json show": paperwork-json uses document renderers too - Fix crash when uising "paperwork-cli rename" - Paperwork-cli/-json search: sort the documents by decreasing dates - Swedish translations added - Add LICENSE file in pypi package 2021/01/01 - 2.0.2: - add command 'chkworkdir': Check work directory integrity and fix it - import: Fix: Load the labels before importing documents 2020/11/15 - 2.0.1: - cmd.import: When many importers match, make it clearer to the user that an input is required. - Include tests in Pypi package (thanks to Elliott Sales de Andrade) 2020/10/17 - 2.0: - Initial release paperwork-2.1.1/paperwork-shell/LICENSE000066400000000000000000001045051417573700700176710ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {one line to give the program's name and a brief idea of what it does.} Copyright (C) {year} {name of author} This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: {project} Copyright (C) {year} {fullname} This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . paperwork-2.1.1/paperwork-shell/MANIFEST.in000066400000000000000000000001431417573700700204130ustar00rootroot00000000000000recursive-include src *.py *.mo *.png recursive-include tests * include *.markdown include LICENSE paperwork-2.1.1/paperwork-shell/Makefile000066400000000000000000000041321417573700700203170ustar00rootroot00000000000000VERSION_FILE = src/paperwork_shell/_version.py PYTHON ?= python3 build: build_c build_py install: install_py install_c uninstall: uninstall_py build_py: ${VERSION_FILE} l10n_compile ${PYTHON} ./setup.py build build_c: version: ${VERSION_FILE} ${VERSION_FILE}: echo -n "version = \"" >| $@ echo -n $(shell git describe --always) >> $@ echo "\"" >> $@ doc: upload_doc: data: check: flake8 src/paperwork_shell test: install python3 -m unittest discover --verbose -s tests linux_exe: windows_exe: ${PYTHON} /mingw64/bin/pip3-script.py install . # ugly, but "import pkg_resources" doesn't work in frozen environments # and I don't want to have to patch the build machine to fix it every # time. mkdir -p $(CURDIR)/../build/exe/data (cd $(CURDIR)/src && find . -name '*.mo' -exec cp --parents \{\} $(CURDIR)/../build/exe/data \; ) release: ifeq (${RELEASE}, ) @echo "You must specify a release version (make release RELEASE=1.2.3)" exit 1 else @echo "Will release: ${RELEASE}" @echo "Checking release is in ChangeLog ..." grep ${RELEASE} ChangeLog | grep -v "/xx" endif release_pypi: @echo "Releasing paperwork-shell ..." ${PYTHON} ./setup.py sdist twine upload dist/paperwork-shell-${RELEASE}.tar.gz @echo "All done" clean: rm -f ${VERSION_FILE} rm -rf build dist *.egg-info # PIP_ARGS is used by Flatpak build install_py: ${VERSION_FILE} l10n_compile ${PYTHON} ./setup.py install ${PIP_ARGS} install_c: uninstall_py: pip3 uninstall -y paperwork-shell uninstall_c: l10n_extract: $(CURDIR)/../tools/l10n_extract.sh "$(CURDIR)/src" "$(CURDIR)/l10n" l10n_compile: $(CURDIR)/../tools/l10n_compile.sh \ "$(CURDIR)/l10n" \ "$(CURDIR)/src/paperwork_shell/l10n" \ "paperwork_shell" help: @echo "make build || make build_py" @echo "make check" @echo "make help: display this message" @echo "make install || make install_py" @echo "make uninstall || make uninstall_py" @echo "make release" .PHONY: \ build \ build_c \ build_py \ check \ doc \ exe \ help \ install \ install_c \ install_py \ l10n_compile \ l10n_extract \ release \ test \ uninstall \ uninstall_c \ version paperwork-2.1.1/paperwork-shell/README.markdown000066400000000000000000000321341417573700700213630ustar00rootroot00000000000000# Paperwork-shell ![Paperwork-shell demo](http://storage.sbg.cloud.ovh.net/v1/AUTH_6c4273c748b243c58df3f6942075e0c9/gitlab.gnome.org/paperwork-shell/search.gif) ## Introduction Paperwork-shell provides 2 commands: * `paperwork-cli`: Human-friendly command line interface. For instance, it can be useful if you want to use Paperwork through SSH. * `paperwork-json`: Designed to be used in scripts. Results can be parsed in shell scripts using [`jq`](https://stedolan.github.io/jq/). Both commands takes the same arguments as input. Only their outputs differ. From there, `paperwork-xxx` means either `paperwork-cli` or `paperwork-json`. ## OS/distribution specifics ### On Windows Only paperwork-json is available. Installer doesn't modify your PATH. If you want to invoke paperwork-json, you either have to modify your PATH yourself, or use the command full path (ex: `"c:\program files (x86)\Paperwork\paperwork-json.exe"`). ### In a Flatpak container You have to run the commands from inside the Flatpak container. ```sh flatpak run --command="paperwork-cli" work.openpaper.Paperwork ``` For examples: ```sh flatpak run --command="paperwork-cli" work.openpaper.Paperwork "--help" flatpak run --command="paperwork-cli" work.openpaper.Paperwork chkdeps ``` ## Sub-commands Paperwork-shell's commands expect sub-commands (similar to `git`). You can obtain all the sub-commands and their expected arguments using `--help` (long help) or `-h` (short help). Not all sub-commands are described in this README. Available sub-commands may vary based on what plugins are enabled or not. This README is just here to give you a preview of the most common sub-commands usually available. ### chkdeps ```sh $ paperwork-cli chkdeps Detected system: debian 10 buster Nothing to do. ``` Check for dependencies required by *paperwork-shell*'s plugins. It does NOT check for dependencies required by *paperwork-gtk*'s plugins (for that, try `paperwork-gtk chkdeps` instead). If dependencies are missing, it will try to provide the command to install them. ### config ```sh $ paperwork-cli config show send_statistics = True uuid = 1234567890 statistics_last_run = 1970-01-01 statistics_protocol = https statistics_server = openpaper.work statistics_url = /beacon/post_statistics workdir = file:///home/jflesch/papers scanner_dev_id = libinsane:sane:epson2:net:192.168.42.18 scanner_source_id = flatbed scanner_resolution = 300 ocr_lang = fra check_for_update = True last_update_found = 1.3.0-253-g032699cf update_last_run = 1970-01-01 update_protocol = https update_server = openpaper.work update_url = /beacon/latest $ paperwork-cli config get workdir workdir = file:///home/jflesch/papers $ paperwork-cli config put workdir str file:///home/jflesch/tmp/papers workdir = file:///home/jflesch/tmp/papers ``` `paperwork-xxx config` provides various sub-sub-commands to read and modify Paperwork config and enable/disable `paperwork-shell`'s plugins. The one most important settings is the work directory path: `workdir`. It indicates where documents managed by Paperwork must be stored. It *must* be an URL (`file://xxx`). ### plugins ```sh $ paperwork-cli plugins list openpaperwork_core.logs.print openpaperwork_gtk.mainloop.glib openpaperwork_core.config paperwork_backend.beacon.stats paperwork_backend.beacon.sysinfo (...) paperwork_shell.display.print paperwork_shell.display.progress paperwork_shell.display.scanpaperwork_shell.display.print paperwork_shell.display.progress paperwork_shell.display.scan ``` While most settings are shared between Paperwork UIs (paperwork-shell and paperwork-gtk), plugins lists are *not*. If you want to modify `paperwork-gtk`'s plugin list, you have to use `paperwork-gtk plugins` instead. If you want to enable or disable features, you can simply add or remove the corresponding plugin. For instance, to disable the automatic OCR run on imported documents or scanned pages: ```sh $ paperwork-cli plugins remove paperwork_backend.guesswork.ocr.pyocr Plugin paperwork_backend.guesswork.ocr.pyocr removed ``` `paperwork-cli plugins remove` and `paperwork-cli plugins add` take dependencies into account: `remove` will also remove all the plugins that depends on the one you're removing. `add` will also add all the plugins required for the one you're adding. Some plugins are mandatory and cannot be disabled (mainly all the plugins required to read the configuration file). If something go wrong, you can reset the plugin list to its default with `paperwork-cli plugins reset`. ### sync ![Paperwork-cli sync](http://storage.sbg.cloud.ovh.net/v1/AUTH_6c4273c748b243c58df3f6942075e0c9/gitlab.gnome.org/paperwork-shell/sync.webm) Update the content of search engine index, label guesser training, etc, according to the current content of the work directory. If you modify the content of your work directory manually (without using Paperwork commands), this is the command to run. This operation is also executed every time `paperwork-gtk` is started. ### search ![Paperwork-xxx search](http://storage.sbg.cloud.ovh.net/v1/AUTH_6c4273c748b243c58df3f6942075e0c9/gitlab.gnome.org/paperwork-shell/search.gif) Returns documents that contains keywords identical or close to the one specified. If no keyword is specified, all the documents are returned. Results are always ordered by decreasing dates. ### show ![paperwork-cli show 1](http://storage.sbg.cloud.ovh.net/v1/AUTH_6c4273c748b243c58df3f6942075e0c9/gitlab.gnome.org/paperwork-shell/show_1.png) ![paperwork-cli show 1](http://storage.sbg.cloud.ovh.net/v1/AUTH_6c4273c748b243c58df3f6942075e0c9/gitlab.gnome.org/paperwork-shell/show_2.png) Show a document page images and texts. ### scanner ```sh % paperwork-cli scanner list BROTHER DS-620 (sheetfed scanner ; sane:dsseries:usb:0x04F9:0x60E0) |-- ID: libinsane:sane:dsseries:usb:0x04F9:0x60E0 |-- Source: feeder | |-- Resolutions: [75, 100, 125, 150, 175, 200, 225, 250, 275, 300, 325, 350, 375, 400, 425, 450, 475, 500, 525, 550, 575, 600] Canon CanoScan N1240U/LiDE30 (flatbed scanner ; sane:plustek:libusb:001:024) |-- ID: libinsane:sane:plustek:libusb:001:024 |-- Source: flatbed (Normal) | |-- Resolutions: [150, 175, 200, 225, 250, 275, 300, 325, 350, 375, 400, | | 425, 450, 475, 500, 525, 550, 575, 600, 625, 650, 675, | | 700, 725, 750, 775, 800, 825, 850, 875, 900, 925, 950, | | 975, 1000, 1025, 1050, 1075, 1100, 1125, 1150, 1175, | | 1200, 1225, 1250, 1275, 1300, 1325, 1350, 1375, 1400, | | 1425, 1450, 1475, 1500, 1525, 1550, 1575, 1600, 1625, | | 1650, 1675, 1700, 1725, 1750, 1775, 1800, 1825, 1850, | | 1875, 1900, 1925, 1950, 1975, 2000, 2025, 2050, 2075, | | 2100, 2125, 2150, 2175, 2200, 2225, 2250, 2275, 2300, | | 2325, 2350, 2375, 2400] |-- Source: flatbed (Transparency) | |-- Resolutions: [150, 175, 200, 225, 250, 275, 300, 325, 350, 375, 400, | | 425, 450, 475, 500, 525, 550, 575, 600, 625, 650, 675, | | 700, 725, 750, 775, 800, 825, 850, 875, 900, 925, 950, | | 975, 1000, 1025, 1050, 1075, 1100, 1125, 1150, 1175, | | 1200, 1225, 1250, 1275, 1300, 1325, 1350, 1375, 1400, | | 1425, 1450, 1475, 1500, 1525, 1550, 1575, 1600, 1625, | | 1650, 1675, 1700, 1725, 1750, 1775, 1800, 1825, 1850, | | 1875, 1900, 1925, 1950, 1975, 2000, 2025, 2050, 2075, | | 2100, 2125, 2150, 2175, 2200, 2225, 2250, 2275, 2300, | | 2325, 2350, 2375, 2400] |-- Source: flatbed (Negative) | |-- Resolutions: [150, 175, 200, 225, 250, 275, 300, 325, 350, 375, 400, | | 425, 450, 475, 500, 525, 550, 575, 600, 625, 650, 675, | | 700, 725, 750, 775, 800, 825, 850, 875, 900, 925, 950, | | 975, 1000, 1025, 1050, 1075, 1100, 1125, 1150, 1175, | | 1200, 1225, 1250, 1275, 1300, 1325, 1350, 1375, 1400, | | 1425, 1450, 1475, 1500, 1525, 1550, 1575, 1600, 1625, | | 1650, 1675, 1700, 1725, 1750, 1775, 1800, 1825, 1850, | | 1875, 1900, 1925, 1950, 1975, 2000, 2025, 2050, 2075, | | 2100, 2125, 2150, 2175, 2200, 2225, 2250, 2275, 2300, | | 2325, 2350, 2375, 2400] Epson PID 08C1 (flatbed scanner ; sane:epson2:net:192.168.42.18) |-- ID: libinsane:sane:epson2:net:192.168.42.18 |-- Source: flatbed | |-- Resolutions: [75, 100, 150, 300, 600] % paperwork-cli scanner set "libinsane:sane:epson2:net:192.168.42.18" Default source: flatbed ID: libinsane:sane:epson2:net:192.168.42.18 Source: flatbed Resolution: 300 ``` Provides subcommands to list the available scanners and get and set the scanner to use and its settings. When configuring the scanner, it checks that the provided settings are actually consistent with what the scanner provides. You can bypass those checks by using `paperwork-cli config` instead. If you get warnings and errors from the Libinsane, you can safely ignore them unless you didn't get the scanner you were looking for in the list. ### scan ![Paperwork-xxx scan](http://storage.sbg.cloud.ovh.net/v1/AUTH_6c4273c748b243c58df3f6942075e0c9/gitlab.gnome.org/paperwork-shell/scan.webm) Scan all the page(s) available in the scanner. Append all the pages to the specified document (`-d`). If no document is specified, a new one will be created. If you get warnings and errors from the Libinsane, you can safely ignore them unless the scan didn't work. ### import ```sh $ paperwork-cli import 100227398115.pdf Importing ['100227398115.pdf'] ... [index_update ] Indexing new document 20191113_1255_32 Committing changes in label guesser database ... Done Committing changes in the index ... Done Done Import result: - Imported files: {'file:///home/jflesch/tmp/pdf/100227398115.pdf'} - Non-imported files: set() - New documents: {'20191113_1255_32'} - Updated documents: set() - PDF: 1 - Documents: 1 ``` Import images of PDF files. Images are appended to the specified document (`-d`). If no document is specified, a new one is created. PDF are always imported as a new document, even if a document ID is specified. ### label ![paperwork-cli label list](http://storage.sbg.cloud.ovh.net/v1/AUTH_6c4273c748b243c58df3f6942075e0c9/gitlab.gnome.org/paperwork-shell/label_1.png) ![paperwork-cli label add](http://storage.sbg.cloud.ovh.net/v1/AUTH_6c4273c748b243c58df3f6942075e0c9/gitlab.gnome.org/paperwork-shell/label_2.png) Add and remove labels on documents. When adding a label, if the label already exists on other documents, the existing color will be reused. If it does not exist yet, either the user has specified a color (`-c #abcdef`), or a random one will be generated. ### edit ![paperwork-cli edit -m rotate_clockwise](http://storage.sbg.cloud.ovh.net/v1/AUTH_6c4273c748b243c58df3f6942075e0c9/gitlab.gnome.org/paperwork-shell/edit.gif) Basic editing of page images. Modifiers must be specified. Many can provided in one shot so the OCR is only run once. ### reset ![paperwork-cli reset](http://storage.sbg.cloud.ovh.net/v1/AUTH_6c4273c748b243c58df3f6942075e0c9/gitlab.gnome.org/paperwork-shell/reset.gif) Returns a page image to its initial state. It will also cancel any changes made by post-processing plugins after importing or scanning. ### delete ```sh $ paperwork-cli delete 20191113_1255_32 Delete document 20191113_1255_32 ? [y/N] y Deleting document 20191113_1255_32 ... $ paperwork-cli delete 20191112_2117_09 -p 1 Delete page(s) [1] of document 20191112_2117_09 ? [y/N] y Deleting page 1 of document 20191112_2117_09 ... [WARNING] [paperwork_backend.model.workdir] All pages of document file:///home/jflesch/tmp/papers/20191112_2117_09 have been removed. Removing document ``` Delete page(s) or a whole document. ### export ```sh $ paperwork-cli export 20191107_2343_44 Current filters: [] Next possible filters: - img_boxes $ paperwork-cli export 20191107_2343_44 -f img_boxes Current filters: ['img_boxes'] Next possible filters: - unpaper - swt_soft - swt_hard - generated_pdf - bw - grayscale - bmp - gif - jpeg - png - tiff $ paperwork-cli export 20191107_2343_44 -f img_boxes -f generated_pdf Current filters: ['img_boxes', 'generated_pdf'] 'generated_pdf' is an output filter. Not other filter can be added after 'generated_pdf'. $ paperwork-cli export 20191107_2343_44 -f img_boxes -f generated_pdf -o ~/tmp/out.pdf Exporting to file:///home/jflesch/tmp/out.pdf ... Done ``` Export a document or page(s). An export is seen as processing pipeline (in other words, filter list). Selecting a document or a page (`-p`) represent the input or the pipe. Various processing components (pipes) can be chained. Some of them can be used at the end of the pipe. There are restrictions on which components can follow each other. For instances: - Somewhere before `generated_pdf`, you must have a component `img_boxes` to turn the input document or page(s) into a bunch of images and text boxes. - Only a PDF document can be used as input for the component `unmodified_pdf` and no other components can precede it. paperwork-2.1.1/paperwork-shell/l10n/000077500000000000000000000000001417573700700174315ustar00rootroot00000000000000paperwork-2.1.1/paperwork-shell/l10n/de.po000066400000000000000000000455331417573700700203730ustar00rootroot00000000000000# German translations for PACKAGE package. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2021-11-29 21:07+0000\n" "Last-Translator: LAZIC Anna <0.0.0.0.0.ffff.255.255.255.255@gmail.com>\n" "Language-Team: German \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.9\n" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:57 msgid "Synchronize the index(es) with the content of the work directory" msgstr "Synchronisiert Index(e) mit dem Inhalt des Arbeitsverzeichnis" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:68 #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:112 msgid "Synchronizing with work directory ..." msgstr "Synchronisierung mit des Arbeitsverzeichnis ..." #: paperwork-shell/src/paperwork_shell/cmd/sync.py:79 #: paperwork-shell/src/paperwork_shell/cmd/reset.py:132 #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:107 #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:118 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:184 msgid "All done !" msgstr "Fertig!" #: paperwork-shell/src/paperwork_shell/cmd/scan.py:51 msgid "Scan pages" msgstr "Scanne Seiten" #: paperwork-shell/src/paperwork_shell/cmd/scan.py:55 msgid "Document to which the scanned pages must be added" msgstr "Dokument zu dem die gescannten Seiten hinzugefügt werden sollen" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:55 msgid "Manage scanner configuration" msgstr "Scannerkonfiguration verwalten" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:58 #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:48 msgid "sub-command" msgstr "Unterkommando" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:62 msgid "List all scanners and their possible settings" msgstr "Liste alle Scanner und deren möglichen Einstellungen ab" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:67 msgid "Show the currently selected scanner and its settings" msgstr "Zeige den aktuell ausgewählten Scanner und dessen Einstellungen" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:72 msgid "Define which scanner and which settings to use" msgstr "" "Bestimme welcher Scanner und welche Einstellungen benützt werden sollen" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:75 msgid "Scanner to use" msgstr "Benütze diesen Scanner" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:80 msgid "" "Default source on the scanner to use (if not specified, one will be selected " "randomly)" msgstr "" "Standartquelle zu benützen (when nich festgelegt, wird eine zufällig " "ausgewählt)" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:86 msgid "Default resolution (dpi ; default=300)" msgstr "Standardauflösung (dpi ; default=300)" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:91 msgid "Examining scanner {} ..." msgstr "Überpruft den Scanner {} ..." #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:133 #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:155 msgid "ID:" msgstr "ID:" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:135 #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:156 msgid "Source:" msgstr "Quelle:" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:137 msgid "Resolutions:" msgstr "Auflösungen:" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:157 msgid "Resolution:" msgstr "Auflösung:" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:185 msgid "Source {} not found on device. Using another source" msgstr "Quelle {} nicht gefunden auf den Gerät. Benutzung einer andere Quelle" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:205 msgid "Default source:" msgstr "Standardquelle:" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:219 msgid "Resolution {} not available. Adjusted to {}." msgstr "{} Auflössung nicht verfügbar. Für {} angepasst." #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:82 msgid "Version: " msgstr "Version: " #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:84 msgid "Because sorting documents is a machine's job." msgstr "Weil Unterlagen sortieren die Arbeit einer Maschine ist." #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:158 msgid "About Paperwork" msgstr "Über Paperwork" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:70 msgid "Reset a page to its original content" msgstr "Zurück zur Seiten Grundstellung" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:108 msgid "Reseting document {} page {} ..." msgstr "Zurücksetzen des Dokuments {} Seite {} ..." #: paperwork-shell/src/paperwork_shell/cmd/reset.py:112 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:126 msgid "Original:" msgstr "Originalfassung:" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:118 msgid "Reseted:" msgstr "Zurückgesetzt:" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:123 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:175 msgid "Committing ..." msgstr "Speichern ..." #: paperwork-shell/src/paperwork_shell/cmd/reset.py:131 #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:102 #: paperwork-shell/src/paperwork_shell/cmd/export.py:175 #: paperwork-shell/src/paperwork_shell/cmd/label.py:101 #: paperwork-shell/src/paperwork_shell/cmd/import.py:165 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:183 #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:89 #: paperwork-shell/src/paperwork_shell/display/progress.py:39 msgid "Done" msgstr "Erledigt" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:50 msgid "OCR document or pages" msgstr "Texterkennung (OCR) eines Dokuments oder der Seiten" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:55 msgid "Document on which OCR must be run" msgstr "Texterkennung (OCR) auf dieses Dokument anwenden" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:60 msgid "" "Pages to OCR (single integer, range or comma-separated list, default: all " "pages)" msgstr "" "Texterkennung aud diese Seiten anwenden (Ganzzahl, Bereich oder Komma " "separierte Liste, default: alle Seiten)" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:91 #, python-brace-format msgid "Running OCR on document {doc_id} page {page_idx} ..." msgstr "Texterkennung von dem Dokument {doc_id} Seite {page_idx} ..." #: paperwork-shell/src/paperwork_shell/cmd/delete.py:62 msgid "Delete a document or a page" msgstr "Lösche ein Dokument oder eine Seite" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:67 msgid "" "Pages to delete (single integer, range or comma-separated list, default: all " "pages)" msgstr "" "Diese Seiten löschen (Ganzzahl, Bereich oder Komma separierte Liste, " "default: alle Seiten)" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:74 #: paperwork-shell/src/paperwork_shell/cmd/label.py:68 msgid "Target documents" msgstr "Zieldokumente" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:90 #, python-brace-format msgid "Deleting document {doc_id} ..." msgstr "Lösche Dokument {doc_id} ..." #: paperwork-shell/src/paperwork_shell/cmd/delete.py:91 #, python-brace-format msgid "Deleting page {page_idx} of document {doc_id} ..." msgstr "Lösche Seite {page_idx} von Dokument {doc_id} ..." #: paperwork-shell/src/paperwork_shell/cmd/delete.py:97 #, python-format msgid "Delete document %s ?" msgstr "Lösche Dokument %s?" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:103 #, python-brace-format msgid "Delete page(s) {page_indexes} of document {doc_id} ?" msgstr "Lösche Seite(n) {page_indexes} von Dokument {doc_id}?" #: paperwork-shell/src/paperwork_shell/cmd/show.py:51 msgid "Show the content of a document" msgstr "Stellt den Inhalt eines Dokumentes dar" #: paperwork-shell/src/paperwork_shell/cmd/show.py:81 #: paperwork-shell/src/paperwork_shell/cmd/search.py:93 #, python-format msgid "Document id: %s" msgstr "Dokumenten-ID %s" #: paperwork-shell/src/paperwork_shell/cmd/show.py:87 #: paperwork-shell/src/paperwork_shell/cmd/search.py:98 #, python-format msgid "Document date: %s" msgstr "Dokumentendatum: %s" #: paperwork-shell/src/paperwork_shell/cmd/show.py:99 #, python-format msgid "Page %d" msgstr "Seite %d" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:41 msgid "Check and fix work directory integrity" msgstr "Integrität des Arbeitsverzeichnisses prüfen und beheben" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:45 msgid "Don't ask to fix things, just fix them" msgstr "Nicht fragen, ob etwas repariert werden darf - einfach reparieren" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:63 msgid "Checking work directory ..." msgstr "Arbeitsverzeichnis prüfen ..." #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:70 msgid "No problem found" msgstr "Kein Problem gefunden" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:77 #, python-format msgid "%d problems found:" msgstr "%d Probleme gefunden:" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:92 msgid "- Problem: " msgstr "- Problem: " #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:96 msgid "- Possible solution: " msgstr "- Mögliche Lösung: " #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:102 msgid "" "Do you want to fix those problems automatically using the indicated " "solutions ?" msgstr "" "Möchten Sie diese Probleme automatisch mit den angegebenen Lösungen beheben?" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:111 msgid "All fixed !" msgstr "Alles behoben!" #: paperwork-shell/src/paperwork_shell/cmd/export.py:57 msgid "" "Export a document, a page, or a set of pages. Example: paperwork-cli export " "20150303_2314_39 -p 2 -f img_boxes -f grayscale -f jpeg -o ~/tmp/pouet.jpg" msgstr "" "Exportiere ein Dokument, eine Seite oder eine Folge von Seiten. Beispiel: " "paperwork-cli export 20150303_2314_39 -p 2 -f img_boxes -f grayscale -f jpeg " "-o ~/tmp/pouet.jpg" #: paperwork-shell/src/paperwork_shell/cmd/export.py:63 msgid "Document to export" msgstr "Zu exportierende Dokumente" #: paperwork-shell/src/paperwork_shell/cmd/export.py:67 msgid "" "Pages to export (single integer, range or comma-separated list, default: all " "pages)" msgstr "" "Diese Seiten exportieren (Ganzzahl, Bereich oder Komma separierte Liste, " "default: alle Seiten)" #: paperwork-shell/src/paperwork_shell/cmd/export.py:76 msgid "" "Export filters. Specify this option once for each filter to apply (ex: '-f " "grayscale -f jpeg')." msgstr "" "Exportfilter. Geben Sie diese Option einmal für jeden anzuwendenden Filter " "an (z.B.: '-f grayscale -f jpeg')." #: paperwork-shell/src/paperwork_shell/cmd/export.py:83 msgid "" "Output file/directory. If not specified, will list the filters that could be " "chained after those already specified." msgstr "" "Ausgabedatei/Verzeichnis. Wenn nicht angegeben, werden die Filter angezeigt " "die nach den bereits angegebenen angehängt werden können." #: paperwork-shell/src/paperwork_shell/cmd/export.py:119 #, python-format msgid "Unknown filters: %s" msgstr "Unbekannte Filter: %s" #: paperwork-shell/src/paperwork_shell/cmd/export.py:141 #, python-format msgid "Current filters: %s" msgstr "Aktuelle Filter: %s" #: paperwork-shell/src/paperwork_shell/cmd/export.py:144 msgid "Next possible filters:" msgstr "Nächste mögliche Filter:" #: paperwork-shell/src/paperwork_shell/cmd/export.py:149 #, python-brace-format msgid "" "'{filter_name}' is an output filter. No other filter can be added after " "'{filter_name}'." msgstr "" "'{filter_name}' ist ein Ausgabefilter. Nach '%s' kann kein weiterer Filter " "angegeben werden." #: paperwork-shell/src/paperwork_shell/cmd/export.py:153 msgid "No possible filters found" msgstr "Keine möglichen Filter gefunden" #: paperwork-shell/src/paperwork_shell/cmd/export.py:170 #, python-format msgid "Exporting to %s ... " msgstr "Exportiere nach %s ... " #: paperwork-shell/src/paperwork_shell/cmd/label.py:58 msgid "Commands to manage labels" msgstr "Kommandos zum Verwalten von Labels" #: paperwork-shell/src/paperwork_shell/cmd/label.py:61 msgid "label command" msgstr "Label Kommando" #: paperwork-shell/src/paperwork_shell/cmd/label.py:72 #: paperwork-shell/src/paperwork_shell/cmd/label.py:80 msgid "Target document" msgstr "Zieldokument" #: paperwork-shell/src/paperwork_shell/cmd/label.py:73 msgid "Label to add" msgstr "hinzuzufügendes Label" #: paperwork-shell/src/paperwork_shell/cmd/label.py:76 msgid "Label color (ex: '#aa22cc')" msgstr "Labelfarbe (z.B.: '#aa22cc')" #: paperwork-shell/src/paperwork_shell/cmd/label.py:81 msgid "Label to remove" msgstr "zu löschendes Label" #: paperwork-shell/src/paperwork_shell/cmd/label.py:85 msgid "Label to delete from *all* documents" msgstr "Das Label %s von *allen* Dokumenten löschen" #: paperwork-shell/src/paperwork_shell/cmd/label.py:90 msgid "Loading all labels ... " msgstr "Lade alle Labels ... " #: paperwork-shell/src/paperwork_shell/cmd/label.py:178 #, python-format msgid "Are you sure you want to delete label '%s' from all documents ?" msgstr "" "Sind Sie sicher dass Sie das Label %s von allen Dokumenten löschen möchten?" #: paperwork-shell/src/paperwork_shell/cmd/import.py:67 msgid "Import file(s)" msgstr "Importiere Datei(en)" #: paperwork-shell/src/paperwork_shell/cmd/import.py:72 msgid "Target document for import" msgstr "Zieldokument für Import" #: paperwork-shell/src/paperwork_shell/cmd/import.py:76 msgid "PDF password" msgstr "PDF Passwort" #: paperwork-shell/src/paperwork_shell/cmd/import.py:80 msgid "Files to import" msgstr "Zu importierende Dateien" #: paperwork-shell/src/paperwork_shell/cmd/import.py:108 #, python-format msgid "Don't know how to import file(s) %s" msgstr "Weiß nicht wie die Datei(en) %s importiert werden sollen" #: paperwork-shell/src/paperwork_shell/cmd/import.py:121 #, python-format msgid "Found many ways to import file(s) %s." msgstr "" "Es wurden viele Möglichkeiten zum Importieren von Datei(en) %s gefunden." #: paperwork-shell/src/paperwork_shell/cmd/import.py:122 msgid "Please select the way you want:" msgstr "Bitte wählen Sie den gewünschten Weg:" #: paperwork-shell/src/paperwork_shell/cmd/import.py:140 msgid "Loading labels ..." msgstr "Schlagwörter werden geladen..." #: paperwork-shell/src/paperwork_shell/cmd/import.py:157 #, python-format msgid "Importing %s ..." msgstr "%s Import in Bearbeitung …" #: paperwork-shell/src/paperwork_shell/cmd/import.py:166 msgid "Import result:" msgstr "Importergebnis:" #: paperwork-shell/src/paperwork_shell/cmd/import.py:167 #, python-format msgid "- Imported files: %s" msgstr "- Importierte Dateien: %s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:168 #, python-format msgid "- Non-imported files: %s" msgstr "- Nicht-Importierte Dateien: %s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:169 #, python-format msgid "- New documents: %s" msgstr "- Neue Dokumente: %s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:170 #, python-format msgid "- Updated documents: %s" msgstr "- Aktualisierte Dokumente: %s" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:95 msgid "Edit page" msgstr "Seite bearbeiten" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:99 msgid "List of image modifiers (comma separated, possible values: {})" msgstr "Bild-Modifikatoren Liste (Kommas als Trennzeichen, mögliche Werte: {})" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:122 msgid "Modifying document {} page {} ..." msgstr "Modifizierung vom Dokument {} Seite {} ..." #: paperwork-shell/src/paperwork_shell/cmd/edit.py:147 msgid "Generating in high quality and saving ..." msgstr "Erstellt und speichert in hoher Qualität ..." #: paperwork-shell/src/paperwork_shell/cmd/search.py:63 msgid "Search keywords in documents" msgstr "Suche Schlüsselwörter in Dokumenten" #: paperwork-shell/src/paperwork_shell/cmd/search.py:67 msgid "Maximum number of results (default: 50)" msgstr "Maximale Anzahl an Ergebnissen (default: 50)" #: paperwork-shell/src/paperwork_shell/cmd/search.py:71 msgid "Search keywords (none means all documents)" msgstr "Suchbegriffe (keine Angabe bedeutet alle Dokumente)" #: paperwork-shell/src/paperwork_shell/cmd/move.py:58 msgid "Move a page" msgstr "Ein Seite verschieben" #: paperwork-shell/src/paperwork_shell/cmd/move.py:62 msgid "Source document" msgstr "Quelldokument" #: paperwork-shell/src/paperwork_shell/cmd/move.py:66 msgid "Page to move" msgstr "Seite die verschiebt werden soll" #: paperwork-shell/src/paperwork_shell/cmd/move.py:70 msgid "Destination document" msgstr "Zieldokument" #: paperwork-shell/src/paperwork_shell/cmd/move.py:74 msgid "Target page number" msgstr "Zielseite Nummer" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:50 msgid "Change a document identifier" msgstr "Ändere eine Dokument-Bezeichnung" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:54 msgid "Document to rename" msgstr "Umzubenennendes Dokument" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:58 msgid "New name for the document" msgstr "Neuer Name für das Dokument" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:43 msgid "Manage additional text attached to documents" msgstr "Verwalte die dem Dokument beifefügten zusätzliche Texte" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:52 msgid "Get a document additional text" msgstr "Erhalte den zusätzlichen Text eines Dokuments" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:57 msgid "Set a document additional text" msgstr "Zusätzlicher Text eines Dokuments festlegen" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:79 #: paperwork-shell/src/paperwork_shell/display/docrendering/extra_text.py:32 msgid "Additional text:" msgstr "Zusätzlicher Text:" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:82 msgid "No additional text" msgstr "Kein zusätzlicher Text" #: paperwork-shell/src/paperwork_shell/main.py:68 msgid "command" msgstr "Kommando" #: paperwork-shell/src/paperwork_shell/display/scan.py:86 msgid "Scanning page {} (expected size: {}x{}) ..." msgstr "Scan des Seite {} (erwartete Größe: {}x{}) ..." #: paperwork-shell/src/paperwork_shell/display/scan.py:127 msgid "Page {} scanned (actual size: {}x{})" msgstr "Seite {} gescannt (eigentliche Größe: {}x{})" #: paperwork-shell/src/paperwork_shell/display/scan.py:135 msgid "End of paper feed" msgstr "Ende des Papiereinzugs" #: paperwork-shell/src/paperwork_shell/display/scan.py:151 msgid "Page {} in document {} created" msgstr "Seite {} im Dokument {} erstellt" #~ msgid "Label to remove on *all* documents" #~ msgstr "Label das auf *alle* Dokumente angewendet wird" #, python-format #~ msgid "Found many ways to import file(s) %s:" #~ msgstr "" #~ "Es wurden mehrere Möglichkeiten zum Import der Datei(en) %s gefunden:" #~ msgid "<-- Previous" #~ msgstr "<-- Vorhergehend" #~ msgid "Next -->" #~ msgstr "Nächster -->" #~ msgid "q: quit" #~ msgstr "q:schließen" paperwork-2.1.1/paperwork-shell/l10n/es.po000066400000000000000000000342051417573700700204040ustar00rootroot00000000000000# Spanish translations for PACKAGE package. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2020-05-03 15:37+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=ASCII\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:57 msgid "Synchronize the index(es) with the content of the work directory" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:68 #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:112 msgid "Synchronizing with work directory ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:79 #: paperwork-shell/src/paperwork_shell/cmd/reset.py:132 #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:107 #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:118 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:184 msgid "All done !" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scan.py:51 msgid "Scan pages" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scan.py:55 msgid "Document to which the scanned pages must be added" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:55 msgid "Manage scanner configuration" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:58 #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:48 msgid "sub-command" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:62 msgid "List all scanners and their possible settings" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:67 msgid "Show the currently selected scanner and its settings" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:72 msgid "Define which scanner and which settings to use" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:75 msgid "Scanner to use" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:80 msgid "" "Default source on the scanner to use (if not specified, one will be selected " "randomly)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:86 msgid "Default resolution (dpi ; default=300)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:91 msgid "Examining scanner {} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:133 #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:155 msgid "ID:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:135 #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:156 msgid "Source:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:137 msgid "Resolutions:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:157 msgid "Resolution:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:185 msgid "Source {} not found on device. Using another source" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:205 msgid "Default source:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:219 msgid "Resolution {} not available. Adjusted to {}." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:82 msgid "Version: " msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:84 msgid "Because sorting documents is a machine's job." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:158 msgid "About Paperwork" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:70 msgid "Reset a page to its original content" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:108 msgid "Reseting document {} page {} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:112 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:126 msgid "Original:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:118 msgid "Reseted:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:123 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:175 msgid "Committing ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:131 #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:102 #: paperwork-shell/src/paperwork_shell/cmd/export.py:175 #: paperwork-shell/src/paperwork_shell/cmd/label.py:101 #: paperwork-shell/src/paperwork_shell/cmd/import.py:165 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:183 #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:89 #: paperwork-shell/src/paperwork_shell/display/progress.py:39 msgid "Done" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:50 msgid "OCR document or pages" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:55 msgid "Document on which OCR must be run" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:60 msgid "" "Pages to OCR (single integer, range or comma-separated list, default: all " "pages)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:91 #, python-brace-format msgid "Running OCR on document {doc_id} page {page_idx} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:62 msgid "Delete a document or a page" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:67 msgid "" "Pages to delete (single integer, range or comma-separated list, default: all " "pages)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:74 #: paperwork-shell/src/paperwork_shell/cmd/label.py:68 msgid "Target documents" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:90 #, python-brace-format msgid "Deleting document {doc_id} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:91 #, python-brace-format msgid "Deleting page {page_idx} of document {doc_id} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:97 #, python-format msgid "Delete document %s ?" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:103 #, python-brace-format msgid "Delete page(s) {page_indexes} of document {doc_id} ?" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/show.py:51 msgid "Show the content of a document" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/show.py:81 #: paperwork-shell/src/paperwork_shell/cmd/search.py:93 #, python-format msgid "Document id: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/show.py:87 #: paperwork-shell/src/paperwork_shell/cmd/search.py:98 #, python-format msgid "Document date: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/show.py:99 #, python-format msgid "Page %d" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:41 msgid "Check and fix work directory integrity" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:45 msgid "Don't ask to fix things, just fix them" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:63 msgid "Checking work directory ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:70 msgid "No problem found" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:77 #, python-format msgid "%d problems found:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:92 msgid "- Problem: " msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:96 msgid "- Possible solution: " msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:102 msgid "" "Do you want to fix those problems automatically using the indicated " "solutions ?" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:111 msgid "All fixed !" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:57 msgid "" "Export a document, a page, or a set of pages. Example: paperwork-cli export " "20150303_2314_39 -p 2 -f img_boxes -f grayscale -f jpeg -o ~/tmp/pouet.jpg" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:63 msgid "Document to export" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:67 msgid "" "Pages to export (single integer, range or comma-separated list, default: all " "pages)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:76 msgid "" "Export filters. Specify this option once for each filter to apply (ex: '-f " "grayscale -f jpeg')." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:83 msgid "" "Output file/directory. If not specified, will list the filters that could be " "chained after those already specified." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:119 #, python-format msgid "Unknown filters: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:141 #, python-format msgid "Current filters: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:144 msgid "Next possible filters:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:149 #, python-brace-format msgid "" "'{filter_name}' is an output filter. No other filter can be added after " "'{filter_name}'." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:153 msgid "No possible filters found" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:170 #, python-format msgid "Exporting to %s ... " msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:58 msgid "Commands to manage labels" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:61 msgid "label command" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:72 #: paperwork-shell/src/paperwork_shell/cmd/label.py:80 msgid "Target document" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:73 msgid "Label to add" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:76 msgid "Label color (ex: '#aa22cc')" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:81 msgid "Label to remove" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:85 msgid "Label to delete from *all* documents" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:90 msgid "Loading all labels ... " msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:178 #, python-format msgid "Are you sure you want to delete label '%s' from all documents ?" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:67 msgid "Import file(s)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:72 msgid "Target document for import" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:76 msgid "PDF password" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:80 msgid "Files to import" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:108 #, python-format msgid "Don't know how to import file(s) %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:121 #, python-format msgid "Found many ways to import file(s) %s." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:122 msgid "Please select the way you want:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:140 msgid "Loading labels ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:157 #, python-format msgid "Importing %s ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:166 msgid "Import result:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:167 #, python-format msgid "- Imported files: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:168 #, python-format msgid "- Non-imported files: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:169 #, python-format msgid "- New documents: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:170 #, python-format msgid "- Updated documents: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:95 msgid "Edit page" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:99 msgid "List of image modifiers (comma separated, possible values: {})" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:122 msgid "Modifying document {} page {} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:147 msgid "Generating in high quality and saving ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/search.py:63 msgid "Search keywords in documents" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/search.py:67 msgid "Maximum number of results (default: 50)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/search.py:71 msgid "Search keywords (none means all documents)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/move.py:58 msgid "Move a page" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/move.py:62 msgid "Source document" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/move.py:66 msgid "Page to move" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/move.py:70 msgid "Destination document" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/move.py:74 msgid "Target page number" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:50 msgid "Change a document identifier" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:54 msgid "Document to rename" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:58 msgid "New name for the document" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:43 msgid "Manage additional text attached to documents" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:52 msgid "Get a document additional text" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:57 msgid "Set a document additional text" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:79 #: paperwork-shell/src/paperwork_shell/display/docrendering/extra_text.py:32 msgid "Additional text:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:82 msgid "No additional text" msgstr "" #: paperwork-shell/src/paperwork_shell/main.py:68 msgid "command" msgstr "" #: paperwork-shell/src/paperwork_shell/display/scan.py:86 msgid "Scanning page {} (expected size: {}x{}) ..." msgstr "" #: paperwork-shell/src/paperwork_shell/display/scan.py:127 msgid "Page {} scanned (actual size: {}x{})" msgstr "" #: paperwork-shell/src/paperwork_shell/display/scan.py:135 msgid "End of paper feed" msgstr "" #: paperwork-shell/src/paperwork_shell/display/scan.py:151 msgid "Page {} in document {} created" msgstr "" paperwork-2.1.1/paperwork-shell/l10n/fr.po000066400000000000000000000461061417573700700204070ustar00rootroot00000000000000# French translations for PACKAGE package # Traductions françaises du paquet PACKAGE. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2021-11-29 21:07+0000\n" "Last-Translator: LAZIC Anna <0.0.0.0.0.ffff.255.255.255.255@gmail.com>\n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 4.9\n" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:57 msgid "Synchronize the index(es) with the content of the work directory" msgstr "Synchronise l'index avec le contenu du répertoire de travail" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:68 #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:112 msgid "Synchronizing with work directory ..." msgstr "Synchronisation avec le répertoire de travail en cours…" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:79 #: paperwork-shell/src/paperwork_shell/cmd/reset.py:132 #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:107 #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:118 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:184 msgid "All done !" msgstr "Terminé !" #: paperwork-shell/src/paperwork_shell/cmd/scan.py:51 msgid "Scan pages" msgstr "Scanner des pages" #: paperwork-shell/src/paperwork_shell/cmd/scan.py:55 msgid "Document to which the scanned pages must be added" msgstr "Document auquel les pages scannées doivent être ajoutées" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:55 msgid "Manage scanner configuration" msgstr "Gérer la configuration du scanner" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:58 #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:48 msgid "sub-command" msgstr "sous-commande" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:62 msgid "List all scanners and their possible settings" msgstr "Lister tous les scanners et leurs réglages possibles" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:67 msgid "Show the currently selected scanner and its settings" msgstr "Afficher le scanner actuellement sélectionné et ses réglages" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:72 msgid "Define which scanner and which settings to use" msgstr "Définir quel scanner et quels réglages utiliser" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:75 msgid "Scanner to use" msgstr "Scanner à utiliser" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:80 msgid "" "Default source on the scanner to use (if not specified, one will be selected " "randomly)" msgstr "" "Source à utiliser par défaut sur le scanner (si non-spécifié, une source " "sera sélectionnée au hasard)" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:86 msgid "Default resolution (dpi ; default=300)" msgstr "Résolution par défaut (ppp, par défaut=300)" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:91 msgid "Examining scanner {} ..." msgstr "Examen du scanner {} …" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:133 #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:155 msgid "ID:" msgstr "ID :" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:135 #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:156 msgid "Source:" msgstr "Source :" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:137 msgid "Resolutions:" msgstr "Résolutions :" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:157 msgid "Resolution:" msgstr "Résolution :" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:185 msgid "Source {} not found on device. Using another source" msgstr "" "Source {} non-trouvé sur le périphérique. Une autre source va être utilisée" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:205 msgid "Default source:" msgstr "Source par défaut :" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:219 msgid "Resolution {} not available. Adjusted to {}." msgstr "Résolution {} non-disponible. Ajustée à {}." #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:82 msgid "Version: " msgstr "Version : " #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:84 msgid "Because sorting documents is a machine's job." msgstr "Parce-que trier des documents est un travail de machine." #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:158 msgid "About Paperwork" msgstr "À propos de Paperwork" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:70 msgid "Reset a page to its original content" msgstr "Réinitialiser la page" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:108 msgid "Reseting document {} page {} ..." msgstr "Réinitialisation de {} p{}…" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:112 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:126 msgid "Original:" msgstr "Original :" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:118 msgid "Reseted:" msgstr "Réinitialisé :" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:123 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:175 msgid "Committing ..." msgstr "Enregistrement…" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:131 #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:102 #: paperwork-shell/src/paperwork_shell/cmd/export.py:175 #: paperwork-shell/src/paperwork_shell/cmd/label.py:101 #: paperwork-shell/src/paperwork_shell/cmd/import.py:165 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:183 #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:89 #: paperwork-shell/src/paperwork_shell/display/progress.py:39 msgid "Done" msgstr "Fait" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:50 msgid "OCR document or pages" msgstr "Passer la ROC sur un document ou des pages" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:55 msgid "Document on which OCR must be run" msgstr "Document sur lequel la ROC doit être faite" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:60 msgid "" "Pages to OCR (single integer, range or comma-separated list, default: all " "pages)" msgstr "" "Pages sur lesquelles la ROC doit être passée (un seul entier, une plage, ou " "une liste d'entiers séparés par des virgules, par défaut : toutes les pages)" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:91 #, python-brace-format msgid "Running OCR on document {doc_id} page {page_idx} ..." msgstr "ROC sur {doc_id} page {page_idx}…" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:62 msgid "Delete a document or a page" msgstr "Effacer un document ou une page" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:67 msgid "" "Pages to delete (single integer, range or comma-separated list, default: all " "pages)" msgstr "" "Pages à effacer (un seul entier, une plage ou une liste séparée par des " "virgules, par défaut : toutes les pages)" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:74 #: paperwork-shell/src/paperwork_shell/cmd/label.py:68 msgid "Target documents" msgstr "Documents cibles" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:90 #, python-brace-format msgid "Deleting document {doc_id} ..." msgstr "Suppression du document {doc_id}…" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:91 #, python-brace-format msgid "Deleting page {page_idx} of document {doc_id} ..." msgstr "Suppression de la page {page_idx} du document {doc_id}…" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:97 #, python-format msgid "Delete document %s ?" msgstr "Effacer le document %s ?" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:103 #, python-brace-format msgid "Delete page(s) {page_indexes} of document {doc_id} ?" msgstr "Effacer la/les page(s) {page_indexes} du document {doc_id} ?" #: paperwork-shell/src/paperwork_shell/cmd/show.py:51 msgid "Show the content of a document" msgstr "Affiche le contenu d'un document" #: paperwork-shell/src/paperwork_shell/cmd/show.py:81 #: paperwork-shell/src/paperwork_shell/cmd/search.py:93 #, python-format msgid "Document id: %s" msgstr "Identifiant du document : %s" #: paperwork-shell/src/paperwork_shell/cmd/show.py:87 #: paperwork-shell/src/paperwork_shell/cmd/search.py:98 #, python-format msgid "Document date: %s" msgstr "Date du document : %s" #: paperwork-shell/src/paperwork_shell/cmd/show.py:99 #, python-format msgid "Page %d" msgstr "Page %d" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:41 msgid "Check and fix work directory integrity" msgstr "Vérifier et corriger l'intégrité du répertoire du travail" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:45 msgid "Don't ask to fix things, just fix them" msgstr "Corriger les problèmes sans demander" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:63 msgid "Checking work directory ..." msgstr "Vérification du répertoire de travail …" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:70 msgid "No problem found" msgstr "Pas de problème trouvé" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:77 #, python-format msgid "%d problems found:" msgstr "%d problèmes trouvés :" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:92 msgid "- Problem: " msgstr "- Problème : " #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:96 msgid "- Possible solution: " msgstr "- Solution possible : " #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:102 msgid "" "Do you want to fix those problems automatically using the indicated " "solutions ?" msgstr "" "Voulez-vous corrigez ces problèmes automatiquement en utilisant les " "solutions indiquées ?" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:111 msgid "All fixed !" msgstr "Tous les problèmes ont été corrigés !" #: paperwork-shell/src/paperwork_shell/cmd/export.py:57 msgid "" "Export a document, a page, or a set of pages. Example: paperwork-cli export " "20150303_2314_39 -p 2 -f img_boxes -f grayscale -f jpeg -o ~/tmp/pouet.jpg" msgstr "" "Exporter un document, une page, ou un jeu de pages. Exemple : paperwork-cli " "export 20150303_2314_39 -p 2 -f img_boxes -f grayscale -f jpeg -o ~/tmp/" "pouet.jpg" #: paperwork-shell/src/paperwork_shell/cmd/export.py:63 msgid "Document to export" msgstr "Documents à exporter" #: paperwork-shell/src/paperwork_shell/cmd/export.py:67 msgid "" "Pages to export (single integer, range or comma-separated list, default: all " "pages)" msgstr "" "Pages à exporter (un seul entier, une plage, ou une liste séparée par des " "virgules, par défaut : toutes les pages)" #: paperwork-shell/src/paperwork_shell/cmd/export.py:76 msgid "" "Export filters. Specify this option once for each filter to apply (ex: '-f " "grayscale -f jpeg')." msgstr "" "Filtres d'export. Indiquez cette option une fois pour chaque filtre à " "appliquer (ex : '-f grayscale -f jpeg')." #: paperwork-shell/src/paperwork_shell/cmd/export.py:83 msgid "" "Output file/directory. If not specified, will list the filters that could be " "chained after those already specified." msgstr "" "Fichier/répertoire de sortie. Si non-spécifié, listera les filtres qui " "pourraient être ajoutées après ceux déjà spécifiés." #: paperwork-shell/src/paperwork_shell/cmd/export.py:119 #, python-format msgid "Unknown filters: %s" msgstr "Filtres inconnus : %s" #: paperwork-shell/src/paperwork_shell/cmd/export.py:141 #, python-format msgid "Current filters: %s" msgstr "Filtres actuels : %s" #: paperwork-shell/src/paperwork_shell/cmd/export.py:144 msgid "Next possible filters:" msgstr "Filtres suivants possibles :" #: paperwork-shell/src/paperwork_shell/cmd/export.py:149 #, python-brace-format msgid "" "'{filter_name}' is an output filter. No other filter can be added after " "'{filter_name}'." msgstr "" "'{filter_name}' est un filtre de sortie. Aucun autre filtre ne peut être " "ajouté après '{filter_name}'." #: paperwork-shell/src/paperwork_shell/cmd/export.py:153 msgid "No possible filters found" msgstr "Aucun filtre possible trouvé" #: paperwork-shell/src/paperwork_shell/cmd/export.py:170 #, python-format msgid "Exporting to %s ... " msgstr "Export en cours vers %s… " #: paperwork-shell/src/paperwork_shell/cmd/label.py:58 msgid "Commands to manage labels" msgstr "Commandes pour gérer les étiquettes" #: paperwork-shell/src/paperwork_shell/cmd/label.py:61 msgid "label command" msgstr "Commande" #: paperwork-shell/src/paperwork_shell/cmd/label.py:72 #: paperwork-shell/src/paperwork_shell/cmd/label.py:80 msgid "Target document" msgstr "Document cible" #: paperwork-shell/src/paperwork_shell/cmd/label.py:73 msgid "Label to add" msgstr "Étiquette à ajouter" #: paperwork-shell/src/paperwork_shell/cmd/label.py:76 msgid "Label color (ex: '#aa22cc')" msgstr "Couleur de l'étiquette (ex : '#aa22cc')" #: paperwork-shell/src/paperwork_shell/cmd/label.py:81 msgid "Label to remove" msgstr "Étiquette à retirer" #: paperwork-shell/src/paperwork_shell/cmd/label.py:85 msgid "Label to delete from *all* documents" msgstr "Étiquette à supprimer de *tous* les documents" #: paperwork-shell/src/paperwork_shell/cmd/label.py:90 msgid "Loading all labels ... " msgstr "Chargement de toutes les étiquettes… " #: paperwork-shell/src/paperwork_shell/cmd/label.py:178 #, python-format msgid "Are you sure you want to delete label '%s' from all documents ?" msgstr "" "Êtes-vous sûr de vouloir effacer l'étiquette '%s' de tous les documents ?" #: paperwork-shell/src/paperwork_shell/cmd/import.py:67 msgid "Import file(s)" msgstr "Importer" #: paperwork-shell/src/paperwork_shell/cmd/import.py:72 msgid "Target document for import" msgstr "Document cible pour l'import" #: paperwork-shell/src/paperwork_shell/cmd/import.py:76 msgid "PDF password" msgstr "Mot de passe du PDF" #: paperwork-shell/src/paperwork_shell/cmd/import.py:80 msgid "Files to import" msgstr "Fichiers à importer" #: paperwork-shell/src/paperwork_shell/cmd/import.py:108 #, python-format msgid "Don't know how to import file(s) %s" msgstr "Ne sait pas comment importer le(s) fichier(s) %s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:121 #, python-format msgid "Found many ways to import file(s) %s." msgstr "Plusieurs manières d'importer les fichier(s) %s ont été trouvées." #: paperwork-shell/src/paperwork_shell/cmd/import.py:122 msgid "Please select the way you want:" msgstr "Veuillez sélectionner la méthode voulue :" #: paperwork-shell/src/paperwork_shell/cmd/import.py:140 msgid "Loading labels ..." msgstr "Chargement des étiquettes …" #: paperwork-shell/src/paperwork_shell/cmd/import.py:157 #, python-format msgid "Importing %s ..." msgstr "Import de %s en cours…" #: paperwork-shell/src/paperwork_shell/cmd/import.py:166 msgid "Import result:" msgstr "Résultat de l'import :" #: paperwork-shell/src/paperwork_shell/cmd/import.py:167 #, python-format msgid "- Imported files: %s" msgstr "- Fichiers importés : %s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:168 #, python-format msgid "- Non-imported files: %s" msgstr "- Fichiers non-importés : %s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:169 #, python-format msgid "- New documents: %s" msgstr "- Nouveaux documents : %s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:170 #, python-format msgid "- Updated documents: %s" msgstr "- Documents mis à jour : %s" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:95 msgid "Edit page" msgstr "Éditer une page" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:99 msgid "List of image modifiers (comma separated, possible values: {})" msgstr "" "Liste de modificateurs d'images (séparés par des virgules, valeurs " "possibles : {})" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:122 msgid "Modifying document {} page {} ..." msgstr "Modification de {} p{} ..." #: paperwork-shell/src/paperwork_shell/cmd/edit.py:147 msgid "Generating in high quality and saving ..." msgstr "Enregistrement en haute qualité…" #: paperwork-shell/src/paperwork_shell/cmd/search.py:63 msgid "Search keywords in documents" msgstr "Chercher des mots-clés dans les documents" #: paperwork-shell/src/paperwork_shell/cmd/search.py:67 msgid "Maximum number of results (default: 50)" msgstr "Nombre maximum de résultats (par défaut : 50)" #: paperwork-shell/src/paperwork_shell/cmd/search.py:71 msgid "Search keywords (none means all documents)" msgstr "Chercher des mots-clés (aucun signifie renvoyer tous les documents)" #: paperwork-shell/src/paperwork_shell/cmd/move.py:58 msgid "Move a page" msgstr "Déplacer une page" #: paperwork-shell/src/paperwork_shell/cmd/move.py:62 msgid "Source document" msgstr "Document source" #: paperwork-shell/src/paperwork_shell/cmd/move.py:66 msgid "Page to move" msgstr "Page à déplacer" #: paperwork-shell/src/paperwork_shell/cmd/move.py:70 msgid "Destination document" msgstr "Document destination" #: paperwork-shell/src/paperwork_shell/cmd/move.py:74 msgid "Target page number" msgstr "Numéro de page cible" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:50 msgid "Change a document identifier" msgstr "Changer l'identifiant d'un document" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:54 msgid "Document to rename" msgstr "Document à renommer" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:58 msgid "New name for the document" msgstr "Nouveau nom pour le document" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:43 msgid "Manage additional text attached to documents" msgstr "Gérer les textes additionnels rattachés aux documents" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:52 msgid "Get a document additional text" msgstr "Obtenir le texte additionnel d'un document" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:57 msgid "Set a document additional text" msgstr "Définir le texte additionnel d'un document" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:79 #: paperwork-shell/src/paperwork_shell/display/docrendering/extra_text.py:32 msgid "Additional text:" msgstr "Texte additionnel :" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:82 msgid "No additional text" msgstr "Pas de texte additionnel" #: paperwork-shell/src/paperwork_shell/main.py:68 msgid "command" msgstr "commande" #: paperwork-shell/src/paperwork_shell/display/scan.py:86 msgid "Scanning page {} (expected size: {}x{}) ..." msgstr "Scan de la page {} (taille prévue : {}x{})…" #: paperwork-shell/src/paperwork_shell/display/scan.py:127 msgid "Page {} scanned (actual size: {}x{})" msgstr "Page {} scannée (taille finale : {}x{})" #: paperwork-shell/src/paperwork_shell/display/scan.py:135 msgid "End of paper feed" msgstr "Fin du bac d'alimentation" #: paperwork-shell/src/paperwork_shell/display/scan.py:151 msgid "Page {} in document {} created" msgstr "Page {} créée dans le document {}" #~ msgid "Label to remove on *all* documents" #~ msgstr "Étiquette à retirer sur *tous* les documents" #, python-format #~ msgid "Found many ways to import file(s) %s:" #~ msgstr "Plusieurs façons d'importer le(s) fichier(s) %s ont été trouvées :" #~ msgid "<-- Previous" #~ msgstr "<-- Précédent" #~ msgid "Next -->" #~ msgstr "Suivant -->" #~ msgid "q: quit" #~ msgstr "q : quitter" paperwork-2.1.1/paperwork-shell/l10n/messages.pot000066400000000000000000000341441417573700700217720ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:57 msgid "Synchronize the index(es) with the content of the work directory" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:68 #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:112 msgid "Synchronizing with work directory ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:79 #: paperwork-shell/src/paperwork_shell/cmd/reset.py:132 #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:107 #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:118 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:184 msgid "All done !" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scan.py:51 msgid "Scan pages" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scan.py:55 msgid "Document to which the scanned pages must be added" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:55 msgid "Manage scanner configuration" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:58 #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:48 msgid "sub-command" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:62 msgid "List all scanners and their possible settings" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:67 msgid "Show the currently selected scanner and its settings" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:72 msgid "Define which scanner and which settings to use" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:75 msgid "Scanner to use" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:80 msgid "" "Default source on the scanner to use (if not specified, one will be selected " "randomly)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:86 msgid "Default resolution (dpi ; default=300)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:91 msgid "Examining scanner {} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:133 #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:155 msgid "ID:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:135 #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:156 msgid "Source:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:137 msgid "Resolutions:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:157 msgid "Resolution:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:185 msgid "Source {} not found on device. Using another source" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:205 msgid "Default source:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:219 msgid "Resolution {} not available. Adjusted to {}." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:82 msgid "Version: " msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:84 msgid "Because sorting documents is a machine's job." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:158 msgid "About Paperwork" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:70 msgid "Reset a page to its original content" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:108 msgid "Reseting document {} page {} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:112 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:126 msgid "Original:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:118 msgid "Reseted:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:123 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:175 msgid "Committing ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:131 #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:102 #: paperwork-shell/src/paperwork_shell/cmd/export.py:175 #: paperwork-shell/src/paperwork_shell/cmd/label.py:101 #: paperwork-shell/src/paperwork_shell/cmd/import.py:165 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:183 #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:89 #: paperwork-shell/src/paperwork_shell/display/progress.py:39 msgid "Done" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:50 msgid "OCR document or pages" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:55 msgid "Document on which OCR must be run" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:60 msgid "" "Pages to OCR (single integer, range or comma-separated list, default: all " "pages)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:91 #, python-brace-format msgid "Running OCR on document {doc_id} page {page_idx} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:62 msgid "Delete a document or a page" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:67 msgid "" "Pages to delete (single integer, range or comma-separated list, default: all " "pages)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:74 #: paperwork-shell/src/paperwork_shell/cmd/label.py:68 msgid "Target documents" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:90 #, python-brace-format msgid "Deleting document {doc_id} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:91 #, python-brace-format msgid "Deleting page {page_idx} of document {doc_id} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:97 #, python-format msgid "Delete document %s ?" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:103 #, python-brace-format msgid "Delete page(s) {page_indexes} of document {doc_id} ?" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/show.py:51 msgid "Show the content of a document" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/show.py:81 #: paperwork-shell/src/paperwork_shell/cmd/search.py:93 #, python-format msgid "Document id: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/show.py:87 #: paperwork-shell/src/paperwork_shell/cmd/search.py:98 #, python-format msgid "Document date: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/show.py:99 #, python-format msgid "Page %d" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:41 msgid "Check and fix work directory integrity" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:45 msgid "Don't ask to fix things, just fix them" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:63 msgid "Checking work directory ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:70 msgid "No problem found" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:77 #, python-format msgid "%d problems found:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:92 msgid "- Problem: " msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:96 msgid "- Possible solution: " msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:102 msgid "" "Do you want to fix those problems automatically using the indicated " "solutions ?" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:111 msgid "All fixed !" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:57 msgid "" "Export a document, a page, or a set of pages. Example: paperwork-cli export " "20150303_2314_39 -p 2 -f img_boxes -f grayscale -f jpeg -o ~/tmp/pouet.jpg" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:63 msgid "Document to export" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:67 msgid "" "Pages to export (single integer, range or comma-separated list, default: all " "pages)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:76 msgid "" "Export filters. Specify this option once for each filter to apply (ex: '-f " "grayscale -f jpeg')." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:83 msgid "" "Output file/directory. If not specified, will list the filters that could be " "chained after those already specified." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:119 #, python-format msgid "Unknown filters: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:141 #, python-format msgid "Current filters: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:144 msgid "Next possible filters:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:149 #, python-brace-format msgid "" "'{filter_name}' is an output filter. No other filter can be added after " "'{filter_name}'." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:153 msgid "No possible filters found" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:170 #, python-format msgid "Exporting to %s ... " msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:58 msgid "Commands to manage labels" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:61 msgid "label command" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:72 #: paperwork-shell/src/paperwork_shell/cmd/label.py:80 msgid "Target document" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:73 msgid "Label to add" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:76 msgid "Label color (ex: '#aa22cc')" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:81 msgid "Label to remove" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:85 msgid "Label to delete from *all* documents" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:90 msgid "Loading all labels ... " msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:178 #, python-format msgid "Are you sure you want to delete label '%s' from all documents ?" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:67 msgid "Import file(s)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:72 msgid "Target document for import" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:76 msgid "PDF password" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:80 msgid "Files to import" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:108 #, python-format msgid "Don't know how to import file(s) %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:121 #, python-format msgid "Found many ways to import file(s) %s." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:122 msgid "Please select the way you want:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:140 msgid "Loading labels ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:157 #, python-format msgid "Importing %s ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:166 msgid "Import result:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:167 #, python-format msgid "- Imported files: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:168 #, python-format msgid "- Non-imported files: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:169 #, python-format msgid "- New documents: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:170 #, python-format msgid "- Updated documents: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:95 msgid "Edit page" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:99 msgid "List of image modifiers (comma separated, possible values: {})" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:122 msgid "Modifying document {} page {} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:147 msgid "Generating in high quality and saving ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/search.py:63 msgid "Search keywords in documents" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/search.py:67 msgid "Maximum number of results (default: 50)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/search.py:71 msgid "Search keywords (none means all documents)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/move.py:58 msgid "Move a page" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/move.py:62 msgid "Source document" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/move.py:66 msgid "Page to move" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/move.py:70 msgid "Destination document" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/move.py:74 msgid "Target page number" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:50 msgid "Change a document identifier" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:54 msgid "Document to rename" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:58 msgid "New name for the document" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:43 msgid "Manage additional text attached to documents" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:52 msgid "Get a document additional text" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:57 msgid "Set a document additional text" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:79 #: paperwork-shell/src/paperwork_shell/display/docrendering/extra_text.py:32 msgid "Additional text:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:82 msgid "No additional text" msgstr "" #: paperwork-shell/src/paperwork_shell/main.py:68 msgid "command" msgstr "" #: paperwork-shell/src/paperwork_shell/display/scan.py:86 msgid "Scanning page {} (expected size: {}x{}) ..." msgstr "" #: paperwork-shell/src/paperwork_shell/display/scan.py:127 msgid "Page {} scanned (actual size: {}x{})" msgstr "" #: paperwork-shell/src/paperwork_shell/display/scan.py:135 msgid "End of paper feed" msgstr "" #: paperwork-shell/src/paperwork_shell/display/scan.py:151 msgid "Page {} in document {} created" msgstr "" paperwork-2.1.1/paperwork-shell/l10n/oc.po000066400000000000000000000456601417573700700204050ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2021-11-05 20:32+0000\n" "Last-Translator: Quentin PAGÈS \n" "Language-Team: Occitan \n" "Language: oc\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 4.4\n" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:57 msgid "Synchronize the index(es) with the content of the work directory" msgstr "Sincronizar l’ensenhador amb lo contengut del repertòri de trabalh" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:68 #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:112 msgid "Synchronizing with work directory ..." msgstr "Sincronizacion amb lo repertòri de trabalh en cors…" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:79 #: paperwork-shell/src/paperwork_shell/cmd/reset.py:132 #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:107 #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:118 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:184 msgid "All done !" msgstr "Acabat !" #: paperwork-shell/src/paperwork_shell/cmd/scan.py:51 msgid "Scan pages" msgstr "Numerizar paginas" #: paperwork-shell/src/paperwork_shell/cmd/scan.py:55 msgid "Document to which the scanned pages must be added" msgstr "Document que deu èsser completat amb las paginas numerizadas" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:55 msgid "Manage scanner configuration" msgstr "Gerir la configuracion del numerizador" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:58 #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:48 msgid "sub-command" msgstr "jos-comanda" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:62 msgid "List all scanners and their possible settings" msgstr "Listar totes los numerizadors e lors paramètres possibles" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:67 msgid "Show the currently selected scanner and its settings" msgstr "Mostrar lo numerizador actualament seleccionat e sos paramètres" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:72 msgid "Define which scanner and which settings to use" msgstr "Definir quin numerizador e paramètres utilizar" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:75 msgid "Scanner to use" msgstr "Numerizador d’utilizar" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:80 msgid "" "Default source on the scanner to use (if not specified, one will be selected " "randomly)" msgstr "" "Font d’utilizar per defaut pel numerizador (se pas especificat, una font " "serà causida a l’asard)" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:86 msgid "Default resolution (dpi ; default=300)" msgstr "Resolucion per defaut (ppp, per defaut=300)" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:91 msgid "Examining scanner {} ..." msgstr "Examèn del numerizador {} …" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:133 #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:155 msgid "ID:" msgstr "ID :" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:135 #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:156 msgid "Source:" msgstr "Font :" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:137 msgid "Resolutions:" msgstr "Resolucions :" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:157 msgid "Resolution:" msgstr "Resolucion :" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:185 msgid "Source {} not found on device. Using another source" msgstr "Font {} pas trobada sul periferic. Una autra font serà utilizada" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:205 msgid "Default source:" msgstr "Font per defaut :" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:219 msgid "Resolution {} not available. Adjusted to {}." msgstr "Resolucion {} pas disponibla. Adaptada a {}." #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:82 msgid "Version: " msgstr "Version : " #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:84 msgid "Because sorting documents is a machine's job." msgstr "Perque triar los documents es un trabalh per las maquinas." #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:158 msgid "About Paperwork" msgstr "A prepaus de Paperwork" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:70 msgid "Reset a page to its original content" msgstr "Reïnicializar la pagina" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:108 msgid "Reseting document {} page {} ..." msgstr "Reïnicializacion de {} p{}…" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:112 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:126 msgid "Original:" msgstr "Original :" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:118 msgid "Reseted:" msgstr "Reïnicializat :" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:123 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:175 msgid "Committing ..." msgstr "Enregistrament…" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:131 #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:102 #: paperwork-shell/src/paperwork_shell/cmd/export.py:175 #: paperwork-shell/src/paperwork_shell/cmd/label.py:101 #: paperwork-shell/src/paperwork_shell/cmd/import.py:165 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:183 #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:89 #: paperwork-shell/src/paperwork_shell/display/progress.py:39 msgid "Done" msgstr "Fach" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:50 msgid "OCR document or pages" msgstr "Reconeisser los caractèrs d’un document o de las paginas" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:55 msgid "Document on which OCR must be run" msgstr "Document d’analizar amb la ROC" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:60 msgid "" "Pages to OCR (single integer, range or comma-separated list, default: all " "pages)" msgstr "" "Paginas que la ROC deu èsser passada (un sol entièr, una plaja o una lista " "d'entièrs separats per de virgulas, per defaut : totas las paginas)" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:91 #, python-brace-format msgid "Running OCR on document {doc_id} page {page_idx} ..." msgstr "ROC sus {doc_id} pagina {page_idx}…" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:62 msgid "Delete a document or a page" msgstr "Escafar un document o un pagina" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:67 msgid "" "Pages to delete (single integer, range or comma-separated list, default: all " "pages)" msgstr "" "Paginas de suprimir (un sol entièr, una plaja o una lista separada per de " "virgulas, per defaut : totas las paginas)" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:74 #: paperwork-shell/src/paperwork_shell/cmd/label.py:68 msgid "Target documents" msgstr "Documents ciblas" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:90 #, python-brace-format msgid "Deleting document {doc_id} ..." msgstr "Supression del document {doc_id}…" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:91 #, python-brace-format msgid "Deleting page {page_idx} of document {doc_id} ..." msgstr "Supression de la pagina {page_idx} del document {doc_id}…" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:97 #, python-format msgid "Delete document %s ?" msgstr "Escafar lo document %s ?" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:103 #, python-brace-format msgid "Delete page(s) {page_indexes} of document {doc_id} ?" msgstr "Suprimir la/las pagina(s) {page_indexes} del document {doc_id} ?" #: paperwork-shell/src/paperwork_shell/cmd/show.py:51 msgid "Show the content of a document" msgstr "Mostrar lo contengut d’un document" #: paperwork-shell/src/paperwork_shell/cmd/show.py:81 #: paperwork-shell/src/paperwork_shell/cmd/search.py:93 #, python-format msgid "Document id: %s" msgstr "Identificant del document : %s" #: paperwork-shell/src/paperwork_shell/cmd/show.py:87 #: paperwork-shell/src/paperwork_shell/cmd/search.py:98 #, python-format msgid "Document date: %s" msgstr "Data del document : %s" #: paperwork-shell/src/paperwork_shell/cmd/show.py:99 #, python-format msgid "Page %d" msgstr "Pagina %d" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:41 msgid "Check and fix work directory integrity" msgstr "Verificar e reglar l’integritat del repertòri de trabalh" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:45 msgid "Don't ask to fix things, just fix them" msgstr "Demandetz pas de reparar las causas, reparatz-la vosautres" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:63 msgid "Checking work directory ..." msgstr "Verificacion del repertòri de trabalh..." #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:70 msgid "No problem found" msgstr "Cap de problèma pas trobat" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:77 #, python-format msgid "%d problems found:" msgstr "%d problèmas trobats :" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:92 msgid "- Problem: " msgstr "- Problèma : " #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:96 msgid "- Possible solution: " msgstr "- Solucion possibla : " #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:102 msgid "" "Do you want to fix those problems automatically using the indicated " "solutions ?" msgstr "" "Volètz reglar aqueles problèmas automaticament en utilizant las solucions " "indicadas ?" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:111 msgid "All fixed !" msgstr "Tot reglat !" #: paperwork-shell/src/paperwork_shell/cmd/export.py:57 msgid "" "Export a document, a page, or a set of pages. Example: paperwork-cli export " "20150303_2314_39 -p 2 -f img_boxes -f grayscale -f jpeg -o ~/tmp/pouet.jpg" msgstr "" "Exportar un document, una pagina o un jòc de paginas. Exemple : paperwork-" "cli export 20150303_2314_39 -p 2 -f img_boxes -f grayscale -f jpeg -o ~/tmp/" "pouet.jpg" #: paperwork-shell/src/paperwork_shell/cmd/export.py:63 msgid "Document to export" msgstr "Document d’exportar" #: paperwork-shell/src/paperwork_shell/cmd/export.py:67 msgid "" "Pages to export (single integer, range or comma-separated list, default: all " "pages)" msgstr "" "Paginas d’exportar (un sol entièr, una plaja o una lista separada per de " "virgulas, per defaut : totas las paginas)" #: paperwork-shell/src/paperwork_shell/cmd/export.py:76 msgid "" "Export filters. Specify this option once for each filter to apply (ex: '-f " "grayscale -f jpeg')." msgstr "" "Filtres d’export. Indicatz aquesta opcion un còp per cada filtre d’aplicar " "(ex : '-f grayscale -f jpeg')." #: paperwork-shell/src/paperwork_shell/cmd/export.py:83 msgid "" "Output file/directory. If not specified, will list the filters that could be " "chained after those already specified." msgstr "" "Fichièr/repertòri de sortida. Se pas especificat, listarà los filtres que " "poirián èsster ajustats aprèp los ja especificats." #: paperwork-shell/src/paperwork_shell/cmd/export.py:119 #, python-format msgid "Unknown filters: %s" msgstr "Fichièrs desconeguts : %s" #: paperwork-shell/src/paperwork_shell/cmd/export.py:141 #, python-format msgid "Current filters: %s" msgstr "Filtres actuals : %s" #: paperwork-shell/src/paperwork_shell/cmd/export.py:144 msgid "Next possible filters:" msgstr "Filtres seguents possibles :" #: paperwork-shell/src/paperwork_shell/cmd/export.py:149 #, python-brace-format msgid "" "'{filter_name}' is an output filter. No other filter can be added after " "'{filter_name}'." msgstr "" "« {filter_name} » es un filtre de sortida. Cap d’autre filtre pòt pas èsser " "ajustar aprèp « {filter_name} »." #: paperwork-shell/src/paperwork_shell/cmd/export.py:153 msgid "No possible filters found" msgstr "Cap de filtre pas trobat" #: paperwork-shell/src/paperwork_shell/cmd/export.py:170 #, python-format msgid "Exporting to %s ... " msgstr "Export en cors cap a %s… " #: paperwork-shell/src/paperwork_shell/cmd/label.py:58 msgid "Commands to manage labels" msgstr "Comandas per generar las etiquetas" #: paperwork-shell/src/paperwork_shell/cmd/label.py:61 msgid "label command" msgstr "Comanda" #: paperwork-shell/src/paperwork_shell/cmd/label.py:72 #: paperwork-shell/src/paperwork_shell/cmd/label.py:80 msgid "Target document" msgstr "Document cible" #: paperwork-shell/src/paperwork_shell/cmd/label.py:73 msgid "Label to add" msgstr "Etiqueta d’ajustar" #: paperwork-shell/src/paperwork_shell/cmd/label.py:76 msgid "Label color (ex: '#aa22cc')" msgstr "Color de l’etiqueta (ex : '#aa22cc')" #: paperwork-shell/src/paperwork_shell/cmd/label.py:81 msgid "Label to remove" msgstr "Etiqueta de tirar" #: paperwork-shell/src/paperwork_shell/cmd/label.py:85 msgid "Label to delete from *all* documents" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:90 msgid "Loading all labels ... " msgstr "Cargament de totas las etiquetas… " #: paperwork-shell/src/paperwork_shell/cmd/label.py:178 #, python-format msgid "Are you sure you want to delete label '%s' from all documents ?" msgstr "" "Volètz vertadièrament suprimir l’etiqueta « %s » de totes los documents ?" #: paperwork-shell/src/paperwork_shell/cmd/import.py:67 msgid "Import file(s)" msgstr "Importar" #: paperwork-shell/src/paperwork_shell/cmd/import.py:72 msgid "Target document for import" msgstr "Document cibla per l’import" #: paperwork-shell/src/paperwork_shell/cmd/import.py:76 msgid "PDF password" msgstr "Senhal del PDF" #: paperwork-shell/src/paperwork_shell/cmd/import.py:80 msgid "Files to import" msgstr "Fichièrs d’importar" #: paperwork-shell/src/paperwork_shell/cmd/import.py:108 #, python-format msgid "Don't know how to import file(s) %s" msgstr "Sap pas cossí importar lo(s) fichièr(s) %s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:121 #, python-format msgid "Found many ways to import file(s) %s." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:122 msgid "Please select the way you want:" msgstr "Volgatz causir lo biais que volètz :" #: paperwork-shell/src/paperwork_shell/cmd/import.py:140 msgid "Loading labels ..." msgstr "Cargament de las etiquetas..." #: paperwork-shell/src/paperwork_shell/cmd/import.py:157 #, python-format msgid "Importing %s ..." msgstr "Import de %s en cors…" #: paperwork-shell/src/paperwork_shell/cmd/import.py:166 msgid "Import result:" msgstr "Resultat de l’import :" #: paperwork-shell/src/paperwork_shell/cmd/import.py:167 #, python-format msgid "- Imported files: %s" msgstr "- Fichièrs importats : %s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:168 #, python-format msgid "- Non-imported files: %s" msgstr "- Fichièrs pas-importats : %s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:169 #, python-format msgid "- New documents: %s" msgstr "- Documents novèls : %s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:170 #, python-format msgid "- Updated documents: %s" msgstr "- Document actualizats : %s" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:95 msgid "Edit page" msgstr "Modificar una pagina" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:99 msgid "List of image modifiers (comma separated, possible values: {})" msgstr "" "Lista de modificacion d’imatges (separadas per de virgulas, valors " "possiblas : {})" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:122 msgid "Modifying document {} page {} ..." msgstr "Modificacion de {} p{} ..." #: paperwork-shell/src/paperwork_shell/cmd/edit.py:147 msgid "Generating in high quality and saving ..." msgstr "Enregistrament nauta qualitat…" #: paperwork-shell/src/paperwork_shell/cmd/search.py:63 msgid "Search keywords in documents" msgstr "Cercar de mots clau pels documents" #: paperwork-shell/src/paperwork_shell/cmd/search.py:67 msgid "Maximum number of results (default: 50)" msgstr "Nombre maximum de resultats (per defaut : 50)" #: paperwork-shell/src/paperwork_shell/cmd/search.py:71 msgid "Search keywords (none means all documents)" msgstr "Cercar de mots clau (cap vòl pas dire tornar totes los documents)" #: paperwork-shell/src/paperwork_shell/cmd/move.py:58 msgid "Move a page" msgstr "Desplaçar una pagina" #: paperwork-shell/src/paperwork_shell/cmd/move.py:62 msgid "Source document" msgstr "Document font" #: paperwork-shell/src/paperwork_shell/cmd/move.py:66 msgid "Page to move" msgstr "Pagina de desplaçar" #: paperwork-shell/src/paperwork_shell/cmd/move.py:70 msgid "Destination document" msgstr "Document destinacion" #: paperwork-shell/src/paperwork_shell/cmd/move.py:74 msgid "Target page number" msgstr "Numèro de pagina cibla" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:50 msgid "Change a document identifier" msgstr "Cambiar l’identificant d’un document" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:54 msgid "Document to rename" msgstr "Document de renomenar" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:58 msgid "New name for the document" msgstr "Nom novèl pel document" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:43 msgid "Manage additional text attached to documents" msgstr "Gerir los tèxtes addicionals estacats als documents" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:52 msgid "Get a document additional text" msgstr "Obténer lo tèxt addicional d’un document" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:57 msgid "Set a document additional text" msgstr "Definir lo tèxt addicional d’un document" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:79 #: paperwork-shell/src/paperwork_shell/display/docrendering/extra_text.py:32 msgid "Additional text:" msgstr "Tèxt addicional :" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:82 msgid "No additional text" msgstr "Cap de tèxt addicional" #: paperwork-shell/src/paperwork_shell/main.py:68 msgid "command" msgstr "comanda" #: paperwork-shell/src/paperwork_shell/display/scan.py:86 msgid "Scanning page {} (expected size: {}x{}) ..." msgstr "Numerizacion de la pagina {} (talha prevista : {}x{})…" #: paperwork-shell/src/paperwork_shell/display/scan.py:127 msgid "Page {} scanned (actual size: {}x{})" msgstr "Pagina {} numerizada (talha finala : {}x{})" #: paperwork-shell/src/paperwork_shell/display/scan.py:135 msgid "End of paper feed" msgstr "Fin del cargador de documents" #: paperwork-shell/src/paperwork_shell/display/scan.py:151 msgid "Page {} in document {} created" msgstr "Pagina {} creada dins lo document {}" #~ msgid "Label to remove on *all* documents" #~ msgstr "Etiqueta de tirar sus *totes* los documents" #, python-format #~ msgid "Found many ways to import file(s) %s:" #~ msgstr "" #~ "Mantunas manièras d’importar lo(s) fichi(s) %s son estadas trobadas :" #~ msgid "<-- Previous" #~ msgstr "<-- Precedent" #~ msgid "Next -->" #~ msgstr "Seguent -->" #~ msgid "q: quit" #~ msgstr "q : sortir" paperwork-2.1.1/paperwork-shell/l10n/sv.po000066400000000000000000000411171417573700700204250ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-10-10 16:38+0200\n" "PO-Revision-Date: 2021-01-05 10:31+0000\n" "Last-Translator: Åke Engelbrektson \n" "Language-Team: Swedish \n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.4\n" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:50 msgid "Change a document identifier" msgstr "Ändra en dokumentidentifierare" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:54 msgid "Document to rename" msgstr "Dokument att byta namn på" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:58 msgid "New name for the document" msgstr "Nytt namn på dokumentet" #: paperwork-shell/src/paperwork_shell/cmd/search.py:63 msgid "Search keywords in documents" msgstr "Sök nyckelord i dokument" #: paperwork-shell/src/paperwork_shell/cmd/search.py:67 msgid "Maximum number of results (default: 50)" msgstr "Max antal träffar (standard: 50)" #: paperwork-shell/src/paperwork_shell/cmd/search.py:71 msgid "Search keywords (none means all documents)" msgstr "Sök nyckelord (inget innebär alla dokument)" #: paperwork-shell/src/paperwork_shell/cmd/search.py:92 #: paperwork-shell/src/paperwork_shell/cmd/show.py:81 #, python-format msgid "Document id: %s" msgstr "Dokument-ID: %s" #: paperwork-shell/src/paperwork_shell/cmd/search.py:97 #: paperwork-shell/src/paperwork_shell/cmd/show.py:87 #, python-format msgid "Document date: %s" msgstr "Dokumentdatum: %s" #: paperwork-shell/src/paperwork_shell/cmd/export.py:57 msgid "" "Export a document, a page, or a set of pages. Example: paperwork-cli export " "20150303_2314_39 -p 2 -f img_boxes -f grayscale -f jpeg -o ~/tmp/pouet.jpg" msgstr "" "Exportera ett dokument, en sida eller en uppsättning sidor. Exempel: " "paperwork-cli export 20150303_2314_39 -p 2 -f img_boxes -f grayscale -f jpeg " "-o ~/tmp/pouet.jpg" #: paperwork-shell/src/paperwork_shell/cmd/export.py:63 msgid "Document to export" msgstr "Dokument att exportera" #: paperwork-shell/src/paperwork_shell/cmd/export.py:67 msgid "" "Pages to export (single integer, range or comma-separated list, default: all " "pages)" msgstr "" "Sidor att exportera (enkelt heltal, intervall eller kommaseparerad lista. " "Standard: Alla sidor)" #: paperwork-shell/src/paperwork_shell/cmd/export.py:76 msgid "" "Export filters. Specify this option once for each filter to apply (ex: '-f " "grayscale -f jpeg')." msgstr "" "Exportfilter. Specificera detta alternativ en gång för varje filter som " "skall tillämpas (exempel: '-f grayscale -f jpeg')." #: paperwork-shell/src/paperwork_shell/cmd/export.py:83 msgid "" "Output file/directory. If not specified, will list the filters that could be " "chained after those already specified." msgstr "" "Utdatafil/-mapp. Om ej specificerat, listas de filter som kan kopplas, efter " "de redan specificerade." #: paperwork-shell/src/paperwork_shell/cmd/export.py:119 #, python-format msgid "Unknown filters: %s" msgstr "Okända filter: %s" #: paperwork-shell/src/paperwork_shell/cmd/export.py:141 #, python-format msgid "Current filters: %s" msgstr "Aktuella filter: %s" #: paperwork-shell/src/paperwork_shell/cmd/export.py:144 msgid "Next possible filters:" msgstr "Nästa möjliga filter:" #: paperwork-shell/src/paperwork_shell/cmd/export.py:149 #, python-brace-format msgid "" "'{filter_name}' is an output filter. No other filter can be added after " "'{filter_name}'." msgstr "" "\"{filter_name}\" är ett utdatafilter. Inget annat filter kan läggas till " "efter \"{filter_name}\"." #: paperwork-shell/src/paperwork_shell/cmd/export.py:153 msgid "No possible filters found" msgstr "Inga möjliga filter hittades" #: paperwork-shell/src/paperwork_shell/cmd/export.py:170 #, python-format msgid "Exporting to %s ... " msgstr "Exporterar till %s... " #: paperwork-shell/src/paperwork_shell/cmd/export.py:175 #: paperwork-shell/src/paperwork_shell/cmd/label.py:101 #: paperwork-shell/src/paperwork_shell/cmd/import.py:133 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:183 #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:102 #: paperwork-shell/src/paperwork_shell/cmd/reset.py:131 #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:89 #: paperwork-shell/src/paperwork_shell/display/progress.py:39 msgid "Done" msgstr "Klart" #: paperwork-shell/src/paperwork_shell/cmd/scan.py:51 msgid "Scan pages" msgstr "Skanna sidor" #: paperwork-shell/src/paperwork_shell/cmd/scan.py:55 msgid "Document to which the scanned pages must be added" msgstr "Dokument som de skannade sidorna måste läggas till" #: paperwork-shell/src/paperwork_shell/cmd/label.py:58 msgid "Commands to manage labels" msgstr "Kommando för att hantera etiketter" #: paperwork-shell/src/paperwork_shell/cmd/label.py:61 msgid "label command" msgstr "etikettkommando" #: paperwork-shell/src/paperwork_shell/cmd/label.py:68 #: paperwork-shell/src/paperwork_shell/cmd/delete.py:74 msgid "Target documents" msgstr "Måldokument" #: paperwork-shell/src/paperwork_shell/cmd/label.py:72 #: paperwork-shell/src/paperwork_shell/cmd/label.py:80 msgid "Target document" msgstr "Måldokument" #: paperwork-shell/src/paperwork_shell/cmd/label.py:73 msgid "Label to add" msgstr "Etikett att lägga till" #: paperwork-shell/src/paperwork_shell/cmd/label.py:76 msgid "Label color (ex: '#aa22cc')" msgstr "Etikettfärg (exempel: #aa22cc)" #: paperwork-shell/src/paperwork_shell/cmd/label.py:81 msgid "Label to remove" msgstr "Etikett att ta bort" #: paperwork-shell/src/paperwork_shell/cmd/label.py:85 msgid "Label to remove on *all* documents" msgstr "Etikett att ta bort från *alla* dokument" #: paperwork-shell/src/paperwork_shell/cmd/label.py:90 msgid "Loading all labels ... " msgstr "Läser in alla etiketter... " #: paperwork-shell/src/paperwork_shell/cmd/label.py:178 #, python-format msgid "Are you sure you want to delete label '%s' from all documents ?" msgstr "Vill du verkligen ta bort etiketten \"%s\" från alla dokumen?" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:62 msgid "Delete a document or a page" msgstr "Ta bort dokument eller en sida" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:67 msgid "" "Pages to delete (single integer, range or comma-separated list, default: all " "pages)" msgstr "" "Sidor att ta bort (enkelt heltal, intervall eller kommaseparerad lista. " "Standard: Alla sidor)" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:90 #, python-brace-format msgid "Deleting document {doc_id} ..." msgstr "Tar bort dokument {doc_id}..." #: paperwork-shell/src/paperwork_shell/cmd/delete.py:91 #, python-brace-format msgid "Deleting page {page_idx} of document {doc_id} ..." msgstr "Tar bort sida {page_idx} från dokument {doc_id}..." #: paperwork-shell/src/paperwork_shell/cmd/delete.py:97 #, python-format msgid "Delete document %s ?" msgstr "Vill du ta bort dokument %s?" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:103 #, python-brace-format msgid "Delete page(s) {page_indexes} of document {doc_id} ?" msgstr "Vill du ta bort sida/sidor {page_indexes} från dokument {doc_id}?" #: paperwork-shell/src/paperwork_shell/cmd/import.py:62 msgid "Import file(s)" msgstr "Importera fil(er)" #: paperwork-shell/src/paperwork_shell/cmd/import.py:67 msgid "Target document for import" msgstr "Måldokument för import" #: paperwork-shell/src/paperwork_shell/cmd/import.py:71 msgid "Files to import" msgstr "Filer att importera" #: paperwork-shell/src/paperwork_shell/cmd/import.py:99 #, python-format msgid "Don't know how to import file(s) %s" msgstr "Vet inte hur filen/filerna %s importeras" #: paperwork-shell/src/paperwork_shell/cmd/import.py:112 #, python-format msgid "Found many ways to import file(s) %s:" msgstr "Hittade många sätt att importera filen/filerna %s:" #: paperwork-shell/src/paperwork_shell/cmd/import.py:125 #, python-format msgid "Importing %s ..." msgstr "Importerar %s..." #: paperwork-shell/src/paperwork_shell/cmd/import.py:134 msgid "Import result:" msgstr "Importresultat:" #: paperwork-shell/src/paperwork_shell/cmd/import.py:135 #, python-format msgid "- Imported files: %s" msgstr "- Importerade filer: %s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:136 #, python-format msgid "- Non-imported files: %s" msgstr "- Icke importerade filer: %s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:137 #, python-format msgid "- New documents: %s" msgstr "- Nya dokument: %s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:138 #, python-format msgid "- Updated documents: %s" msgstr "- Uppdaterade dokument: %s" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:95 msgid "Edit page" msgstr "Redigera sida" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:99 msgid "List of image modifiers (comma separated, possible values: {})" msgstr "Lista alla bildmodifierare (kommaseparerade, möjliga värden: {})" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:122 msgid "Modifying document {} page {} ..." msgstr "Ändrar dokument {} sida {}..." #: paperwork-shell/src/paperwork_shell/cmd/edit.py:126 #: paperwork-shell/src/paperwork_shell/cmd/reset.py:112 msgid "Original:" msgstr "Original:" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:147 msgid "Generating in high quality and saving ..." msgstr "Genererar i hög kvalitet och sparar..." #: paperwork-shell/src/paperwork_shell/cmd/edit.py:175 #: paperwork-shell/src/paperwork_shell/cmd/reset.py:123 msgid "Committing ..." msgstr "Tillämpar..." #: paperwork-shell/src/paperwork_shell/cmd/edit.py:184 #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:107 #: paperwork-shell/src/paperwork_shell/cmd/reset.py:132 #: paperwork-shell/src/paperwork_shell/cmd/sync.py:79 msgid "All done !" msgstr "Klart!" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:84 msgid "Version: " msgstr "Version: " #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:87 msgid "Because sorting documents is a machine's job." msgstr "Eftersom sortering av dokument är ett maskinjobb." #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:160 msgid "About Paperwork" msgstr "Om Paperwork" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:211 msgid "<-- Previous" msgstr "<-- Föregående" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:213 msgid "Next -->" msgstr "Nästa -->" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:214 msgid "q: quit" msgstr "q: avsluta" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:50 msgid "OCR document or pages" msgstr "OCR-dokument eller -sidor" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:55 msgid "Document on which OCR must be run" msgstr "Dokument som OCR måste köras på" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:60 msgid "" "Pages to OCR (single integer, range or comma-separated list, default: all " "pages)" msgstr "" "Sidor till OCR (enkelt heltal, intervall eller kommaseparerad lista. " "Standard: Alla sidor)" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:91 #, python-brace-format msgid "Running OCR on document {doc_id} page {page_idx} ..." msgstr "Kör OCR på dokument {doc_id} sida {page_idx}..." #: paperwork-shell/src/paperwork_shell/cmd/reset.py:70 msgid "Reset a page to its original content" msgstr "Återställ en sida till ursprungligt innehåll" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:108 msgid "Reseting document {} page {} ..." msgstr "Återställer dokument {} sida {}..." #: paperwork-shell/src/paperwork_shell/cmd/reset.py:118 msgid "Reseted:" msgstr "Återställt:" #: paperwork-shell/src/paperwork_shell/cmd/move.py:58 msgid "Move a page" msgstr "Flytta en sida" #: paperwork-shell/src/paperwork_shell/cmd/move.py:62 msgid "Source document" msgstr "Källdokument" #: paperwork-shell/src/paperwork_shell/cmd/move.py:66 msgid "Page to move" msgstr "Sida att ta bort" #: paperwork-shell/src/paperwork_shell/cmd/move.py:70 msgid "Destination document" msgstr "Destination" #: paperwork-shell/src/paperwork_shell/cmd/move.py:74 msgid "Target page number" msgstr "Målsidans nummer" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:55 msgid "Manage scanner configuration" msgstr "Hantera skannerkonfiguration" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:58 #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:48 msgid "sub-command" msgstr "underkommando" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:62 msgid "List all scanners and their possible settings" msgstr "Lista alla skannrar och dess eventuella inställningar" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:67 msgid "Show the currently selected scanner and its settings" msgstr "Visa aktuell skanner och dess inställningar" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:72 msgid "Define which scanner and which settings to use" msgstr "Ange vilken skanner och vilka inställningar som skall användas" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:75 msgid "Scanner to use" msgstr "Skanner att använda" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:80 msgid "" "Default source on the scanner to use (if not specified, one will be selected " "randomly)" msgstr "" "Standardkälla att använda på skanner (om inget specificeras kommer en att " "väljas slumpmässigt)" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:86 msgid "Default resolution (dpi ; default=300)" msgstr "Standardupplösning (dpi ; standard=300)" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:91 msgid "Examining scanner {} ..." msgstr "Undersöker skanner {}..." #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:131 #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:153 msgid "ID:" msgstr "ID:" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:133 #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:154 msgid "Source:" msgstr "Källa:" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:135 msgid "Resolutions:" msgstr "Upplösningar:" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:155 msgid "Resolution:" msgstr "Upplösning:" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:183 msgid "Source {} not found on device. Using another source" msgstr "Källa {} inte hittad på enheten. Använder annan källa." #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:203 msgid "Default source:" msgstr "Standardkälla:" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:217 msgid "Resolution {} not available. Adjusted to {}." msgstr "Upplösning {} inte tillgänglig. Justerad till {}." #: paperwork-shell/src/paperwork_shell/cmd/show.py:51 msgid "Show the content of a document" msgstr "Visa innehållet i ett dokument" #: paperwork-shell/src/paperwork_shell/cmd/show.py:99 #, python-format msgid "Page %d" msgstr "Sida %d" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:57 msgid "Synchronize the index(es) with the content of the work directory" msgstr "Synkronisera index med innehållet i arbetsmappen" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:68 msgid "Synchronizing with work directory ..." msgstr "Synkroniserar med arbetsmapp..." #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:43 msgid "Manage additional text attached to documents" msgstr "Hantera ytterligare text bifogad till dokument" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:52 msgid "Get a document additional text" msgstr "Hämta ett dokuments ytterligare text" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:57 msgid "Set a document additional text" msgstr "Ange ytterligare text i ett dokument" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:79 #: paperwork-shell/src/paperwork_shell/display/docrendering/extra_text.py:32 msgid "Additional text:" msgstr "Ytterligare text:" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:82 msgid "No additional text" msgstr "Ingen ytterligare text" #: paperwork-shell/src/paperwork_shell/main.py:67 msgid "command" msgstr "kommando" #: paperwork-shell/src/paperwork_shell/display/scan.py:86 msgid "Scanning page {} (expected size: {}x{}) ..." msgstr "Skannar sida {} (förväntad storlek: {}x{})..." #: paperwork-shell/src/paperwork_shell/display/scan.py:127 msgid "Page {} scanned (actual size: {}x{})" msgstr "Sida {} skannad (egentlig storlek: {}x{})" #: paperwork-shell/src/paperwork_shell/display/scan.py:135 msgid "End of paper feed" msgstr "Slut på pappersmatning" #: paperwork-shell/src/paperwork_shell/display/scan.py:151 msgid "Page {} in document {} created" msgstr "Sida {} i dokument {} skapad" paperwork-2.1.1/paperwork-shell/l10n/uk.po000066400000000000000000000343241417573700700204160ustar00rootroot00000000000000# Ukrainian translations for PACKAGE package. # Copyright (C) 2020 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-11-30 11:53+0100\n" "PO-Revision-Date: 2020-05-03 15:37+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: uk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=ASCII\n" "Content-Transfer-Encoding: 8bit\n" "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);\n" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:57 msgid "Synchronize the index(es) with the content of the work directory" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:68 #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:112 msgid "Synchronizing with work directory ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:79 #: paperwork-shell/src/paperwork_shell/cmd/reset.py:132 #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:107 #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:118 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:184 msgid "All done !" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scan.py:51 msgid "Scan pages" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scan.py:55 msgid "Document to which the scanned pages must be added" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:55 msgid "Manage scanner configuration" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:58 #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:48 msgid "sub-command" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:62 msgid "List all scanners and their possible settings" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:67 msgid "Show the currently selected scanner and its settings" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:72 msgid "Define which scanner and which settings to use" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:75 msgid "Scanner to use" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:80 msgid "" "Default source on the scanner to use (if not specified, one will be selected " "randomly)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:86 msgid "Default resolution (dpi ; default=300)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:91 msgid "Examining scanner {} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:133 #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:155 msgid "ID:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:135 #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:156 msgid "Source:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:137 msgid "Resolutions:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:157 msgid "Resolution:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:185 msgid "Source {} not found on device. Using another source" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:205 msgid "Default source:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:219 msgid "Resolution {} not available. Adjusted to {}." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:82 msgid "Version: " msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:84 msgid "Because sorting documents is a machine's job." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:158 msgid "About Paperwork" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:70 msgid "Reset a page to its original content" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:108 msgid "Reseting document {} page {} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:112 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:126 msgid "Original:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:118 msgid "Reseted:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:123 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:175 msgid "Committing ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:131 #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:102 #: paperwork-shell/src/paperwork_shell/cmd/export.py:175 #: paperwork-shell/src/paperwork_shell/cmd/label.py:101 #: paperwork-shell/src/paperwork_shell/cmd/import.py:165 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:183 #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:89 #: paperwork-shell/src/paperwork_shell/display/progress.py:39 msgid "Done" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:50 msgid "OCR document or pages" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:55 msgid "Document on which OCR must be run" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:60 msgid "" "Pages to OCR (single integer, range or comma-separated list, default: all " "pages)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:91 #, python-brace-format msgid "Running OCR on document {doc_id} page {page_idx} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:62 msgid "Delete a document or a page" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:67 msgid "" "Pages to delete (single integer, range or comma-separated list, default: all " "pages)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:74 #: paperwork-shell/src/paperwork_shell/cmd/label.py:68 msgid "Target documents" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:90 #, python-brace-format msgid "Deleting document {doc_id} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:91 #, python-brace-format msgid "Deleting page {page_idx} of document {doc_id} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:97 #, python-format msgid "Delete document %s ?" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:103 #, python-brace-format msgid "Delete page(s) {page_indexes} of document {doc_id} ?" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/show.py:51 msgid "Show the content of a document" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/show.py:81 #: paperwork-shell/src/paperwork_shell/cmd/search.py:93 #, python-format msgid "Document id: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/show.py:87 #: paperwork-shell/src/paperwork_shell/cmd/search.py:98 #, python-format msgid "Document date: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/show.py:99 #, python-format msgid "Page %d" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:41 msgid "Check and fix work directory integrity" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:45 msgid "Don't ask to fix things, just fix them" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:63 msgid "Checking work directory ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:70 msgid "No problem found" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:77 #, python-format msgid "%d problems found:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:92 msgid "- Problem: " msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:96 msgid "- Possible solution: " msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:102 msgid "" "Do you want to fix those problems automatically using the indicated " "solutions ?" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py:111 msgid "All fixed !" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:57 msgid "" "Export a document, a page, or a set of pages. Example: paperwork-cli export " "20150303_2314_39 -p 2 -f img_boxes -f grayscale -f jpeg -o ~/tmp/pouet.jpg" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:63 msgid "Document to export" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:67 msgid "" "Pages to export (single integer, range or comma-separated list, default: all " "pages)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:76 msgid "" "Export filters. Specify this option once for each filter to apply (ex: '-f " "grayscale -f jpeg')." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:83 msgid "" "Output file/directory. If not specified, will list the filters that could be " "chained after those already specified." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:119 #, python-format msgid "Unknown filters: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:141 #, python-format msgid "Current filters: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:144 msgid "Next possible filters:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:149 #, python-brace-format msgid "" "'{filter_name}' is an output filter. No other filter can be added after " "'{filter_name}'." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:153 msgid "No possible filters found" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:170 #, python-format msgid "Exporting to %s ... " msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:58 msgid "Commands to manage labels" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:61 msgid "label command" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:72 #: paperwork-shell/src/paperwork_shell/cmd/label.py:80 msgid "Target document" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:73 msgid "Label to add" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:76 msgid "Label color (ex: '#aa22cc')" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:81 msgid "Label to remove" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:85 msgid "Label to delete from *all* documents" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:90 msgid "Loading all labels ... " msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/label.py:178 #, python-format msgid "Are you sure you want to delete label '%s' from all documents ?" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:67 msgid "Import file(s)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:72 msgid "Target document for import" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:76 msgid "PDF password" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:80 msgid "Files to import" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:108 #, python-format msgid "Don't know how to import file(s) %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:121 #, python-format msgid "Found many ways to import file(s) %s." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:122 msgid "Please select the way you want:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:140 msgid "Loading labels ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:157 #, python-format msgid "Importing %s ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:166 msgid "Import result:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:167 #, python-format msgid "- Imported files: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:168 #, python-format msgid "- Non-imported files: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:169 #, python-format msgid "- New documents: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/import.py:170 #, python-format msgid "- Updated documents: %s" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:95 msgid "Edit page" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:99 msgid "List of image modifiers (comma separated, possible values: {})" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:122 msgid "Modifying document {} page {} ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:147 msgid "Generating in high quality and saving ..." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/search.py:63 msgid "Search keywords in documents" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/search.py:67 msgid "Maximum number of results (default: 50)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/search.py:71 msgid "Search keywords (none means all documents)" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/move.py:58 msgid "Move a page" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/move.py:62 msgid "Source document" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/move.py:66 msgid "Page to move" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/move.py:70 msgid "Destination document" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/move.py:74 msgid "Target page number" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:50 msgid "Change a document identifier" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:54 msgid "Document to rename" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:58 msgid "New name for the document" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:43 msgid "Manage additional text attached to documents" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:52 msgid "Get a document additional text" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:57 msgid "Set a document additional text" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:79 #: paperwork-shell/src/paperwork_shell/display/docrendering/extra_text.py:32 msgid "Additional text:" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:82 msgid "No additional text" msgstr "" #: paperwork-shell/src/paperwork_shell/main.py:68 msgid "command" msgstr "" #: paperwork-shell/src/paperwork_shell/display/scan.py:86 msgid "Scanning page {} (expected size: {}x{}) ..." msgstr "" #: paperwork-shell/src/paperwork_shell/display/scan.py:127 msgid "Page {} scanned (actual size: {}x{})" msgstr "" #: paperwork-shell/src/paperwork_shell/display/scan.py:135 msgid "End of paper feed" msgstr "" #: paperwork-shell/src/paperwork_shell/display/scan.py:151 msgid "Page {} in document {} created" msgstr "" paperwork-2.1.1/paperwork-shell/l10n/zh_Hans.po000066400000000000000000000374061417573700700213750ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-10-10 16:38+0200\n" "PO-Revision-Date: 2021-02-07 12:49+0000\n" "Last-Translator: 玉堂白鹤 \n" "Language-Team: Chinese (Simplified) \n" "Language: zh_Hans\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Weblate 4.4\n" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:50 msgid "Change a document identifier" msgstr "更改文档标识符" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:54 msgid "Document to rename" msgstr "需要重命名的文档" #: paperwork-shell/src/paperwork_shell/cmd/rename.py:58 msgid "New name for the document" msgstr "新文档名" #: paperwork-shell/src/paperwork_shell/cmd/search.py:63 msgid "Search keywords in documents" msgstr "搜索文档中的关键字" #: paperwork-shell/src/paperwork_shell/cmd/search.py:67 msgid "Maximum number of results (default: 50)" msgstr "最大结果数 (默认值: 50)" #: paperwork-shell/src/paperwork_shell/cmd/search.py:71 msgid "Search keywords (none means all documents)" msgstr "搜索关键字 (none 代表所有文档)" #: paperwork-shell/src/paperwork_shell/cmd/search.py:92 #: paperwork-shell/src/paperwork_shell/cmd/show.py:81 #, python-format msgid "Document id: %s" msgstr "文档 id: %s" #: paperwork-shell/src/paperwork_shell/cmd/search.py:97 #: paperwork-shell/src/paperwork_shell/cmd/show.py:87 #, python-format msgid "Document date: %s" msgstr "文档日期:%s" #: paperwork-shell/src/paperwork_shell/cmd/export.py:57 msgid "" "Export a document, a page, or a set of pages. Example: paperwork-cli export " "20150303_2314_39 -p 2 -f img_boxes -f grayscale -f jpeg -o ~/tmp/pouet.jpg" msgstr "" "导出文档,页面,或一组页面。示例: paperwork-cli export 20150303_2314_39 -p 2 -f img_boxes -f " "grayscale -f jpeg -o ~/tmp/pouet.jpg" #: paperwork-shell/src/paperwork_shell/cmd/export.py:63 msgid "Document to export" msgstr "导出的文档" #: paperwork-shell/src/paperwork_shell/cmd/export.py:67 msgid "" "Pages to export (single integer, range or comma-separated list, default: all " "pages)" msgstr "要导出的页面 (单个整数,范围或者使用逗号分隔的列表,默认为所有页面)" #: paperwork-shell/src/paperwork_shell/cmd/export.py:76 msgid "" "Export filters. Specify this option once for each filter to apply (ex: '-f " "grayscale -f jpeg')." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:83 msgid "" "Output file/directory. If not specified, will list the filters that could be " "chained after those already specified." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/export.py:119 #, python-format msgid "Unknown filters: %s" msgstr "未知过滤器 : %s" #: paperwork-shell/src/paperwork_shell/cmd/export.py:141 #, python-format msgid "Current filters: %s" msgstr "当前过滤器: %s" #: paperwork-shell/src/paperwork_shell/cmd/export.py:144 msgid "Next possible filters:" msgstr "下一个可能的过滤器 :" #: paperwork-shell/src/paperwork_shell/cmd/export.py:149 #, python-brace-format msgid "" "'{filter_name}' is an output filter. No other filter can be added after " "'{filter_name}'." msgstr "'{filter_name}' 是一个输出过滤器。不能在 '{filter_name}' 之后添加其他过滤器。" #: paperwork-shell/src/paperwork_shell/cmd/export.py:153 msgid "No possible filters found" msgstr "找不到可能的过滤器" #: paperwork-shell/src/paperwork_shell/cmd/export.py:170 #, python-format msgid "Exporting to %s ... " msgstr "导出到 %s ... " #: paperwork-shell/src/paperwork_shell/cmd/export.py:175 #: paperwork-shell/src/paperwork_shell/cmd/label.py:101 #: paperwork-shell/src/paperwork_shell/cmd/import.py:133 #: paperwork-shell/src/paperwork_shell/cmd/edit.py:183 #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:102 #: paperwork-shell/src/paperwork_shell/cmd/reset.py:131 #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:89 #: paperwork-shell/src/paperwork_shell/display/progress.py:39 msgid "Done" msgstr "完成" #: paperwork-shell/src/paperwork_shell/cmd/scan.py:51 msgid "Scan pages" msgstr "扫描页面" #: paperwork-shell/src/paperwork_shell/cmd/scan.py:55 msgid "Document to which the scanned pages must be added" msgstr "必须添加扫描页面的文档" #: paperwork-shell/src/paperwork_shell/cmd/label.py:58 msgid "Commands to manage labels" msgstr "标签管理命令" #: paperwork-shell/src/paperwork_shell/cmd/label.py:61 msgid "label command" msgstr "标签命令" #: paperwork-shell/src/paperwork_shell/cmd/label.py:68 #: paperwork-shell/src/paperwork_shell/cmd/delete.py:74 msgid "Target documents" msgstr "目标文档" #: paperwork-shell/src/paperwork_shell/cmd/label.py:72 #: paperwork-shell/src/paperwork_shell/cmd/label.py:80 msgid "Target document" msgstr "目标文档" #: paperwork-shell/src/paperwork_shell/cmd/label.py:73 msgid "Label to add" msgstr "要添加的标签" #: paperwork-shell/src/paperwork_shell/cmd/label.py:76 msgid "Label color (ex: '#aa22cc')" msgstr "标签颜色 (例如 '#aa22cc')" #: paperwork-shell/src/paperwork_shell/cmd/label.py:81 msgid "Label to remove" msgstr "要移除的标签" #: paperwork-shell/src/paperwork_shell/cmd/label.py:85 msgid "Label to remove on *all* documents" msgstr "要在 *所有*文档移除的标签" #: paperwork-shell/src/paperwork_shell/cmd/label.py:90 msgid "Loading all labels ... " msgstr "加载所有标签 ... " #: paperwork-shell/src/paperwork_shell/cmd/label.py:178 #, python-format msgid "Are you sure you want to delete label '%s' from all documents ?" msgstr "确定要从所有文档中删除标签'%s' 吗?" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:62 msgid "Delete a document or a page" msgstr "删除文档或页面" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:67 msgid "" "Pages to delete (single integer, range or comma-separated list, default: all " "pages)" msgstr "要删除的页面 (单个整数、范围或使用逗号分隔的列表,默认值为所有页面)" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:90 #, python-brace-format msgid "Deleting document {doc_id} ..." msgstr "删除文档 {doc_id} ..." #: paperwork-shell/src/paperwork_shell/cmd/delete.py:91 #, python-brace-format msgid "Deleting page {page_idx} of document {doc_id} ..." msgstr "删除文档 {doc_id}中的{page_idx}页面 ..." #: paperwork-shell/src/paperwork_shell/cmd/delete.py:97 #, python-format msgid "Delete document %s ?" msgstr "删除文档 %s ?" #: paperwork-shell/src/paperwork_shell/cmd/delete.py:103 #, python-brace-format msgid "Delete page(s) {page_indexes} of document {doc_id} ?" msgstr "删除文档 {doc_id} 中的 {page_indexes} 页面 ?" #: paperwork-shell/src/paperwork_shell/cmd/import.py:62 msgid "Import file(s)" msgstr "导入文件" #: paperwork-shell/src/paperwork_shell/cmd/import.py:67 msgid "Target document for import" msgstr "要导入的目标文档" #: paperwork-shell/src/paperwork_shell/cmd/import.py:71 msgid "Files to import" msgstr "要导入的文件" #: paperwork-shell/src/paperwork_shell/cmd/import.py:99 #, python-format msgid "Don't know how to import file(s) %s" msgstr "不知道如何导入文件%s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:112 #, python-format msgid "Found many ways to import file(s) %s:" msgstr "找到多种导入文件 %s 的方法:" #: paperwork-shell/src/paperwork_shell/cmd/import.py:125 #, python-format msgid "Importing %s ..." msgstr "正在导入 %s ..." #: paperwork-shell/src/paperwork_shell/cmd/import.py:134 msgid "Import result:" msgstr "导入结果:" #: paperwork-shell/src/paperwork_shell/cmd/import.py:135 #, python-format msgid "- Imported files: %s" msgstr "-导入文件: %s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:136 #, python-format msgid "- Non-imported files: %s" msgstr "- 未导入的文件 : %s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:137 #, python-format msgid "- New documents: %s" msgstr "- 新文档: %s" #: paperwork-shell/src/paperwork_shell/cmd/import.py:138 #, python-format msgid "- Updated documents: %s" msgstr "- 更新文档: %s" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:95 msgid "Edit page" msgstr "编辑页面" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:99 msgid "List of image modifiers (comma separated, possible values: {})" msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:122 msgid "Modifying document {} page {} ..." msgstr "修改文档 {} 页面 {} ..." #: paperwork-shell/src/paperwork_shell/cmd/edit.py:126 #: paperwork-shell/src/paperwork_shell/cmd/reset.py:112 msgid "Original:" msgstr "原件:" #: paperwork-shell/src/paperwork_shell/cmd/edit.py:147 msgid "Generating in high quality and saving ..." msgstr "高质量生成并保存 ..." #: paperwork-shell/src/paperwork_shell/cmd/edit.py:175 #: paperwork-shell/src/paperwork_shell/cmd/reset.py:123 msgid "Committing ..." msgstr "提交中 ..." #: paperwork-shell/src/paperwork_shell/cmd/edit.py:184 #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:107 #: paperwork-shell/src/paperwork_shell/cmd/reset.py:132 #: paperwork-shell/src/paperwork_shell/cmd/sync.py:79 msgid "All done !" msgstr "全部完成 !" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:84 msgid "Version: " msgstr "版本: " #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:87 msgid "Because sorting documents is a machine's job." msgstr "因为整理文件是机器的工作。" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:160 msgid "About Paperwork" msgstr "关于 Paperwork" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:211 msgid "<-- Previous" msgstr "<-- 上一位" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:213 msgid "Next -->" msgstr "下一位 -->" #: paperwork-shell/src/paperwork_shell/cmd/about/__init__.py:214 msgid "q: quit" msgstr "q: 退出" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:50 msgid "OCR document or pages" msgstr "OCR 文档或页面" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:55 msgid "Document on which OCR must be run" msgstr "必须运行 OCR 的文档" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:60 msgid "" "Pages to OCR (single integer, range or comma-separated list, default: all " "pages)" msgstr "OCR 页面 (单个整数、范围或使用逗号分隔的列表,默认值为所有页面)" #: paperwork-shell/src/paperwork_shell/cmd/ocr.py:91 #, python-brace-format msgid "Running OCR on document {doc_id} page {page_idx} ..." msgstr "在文档 {doc_id} 的页面 {page_idx} 上运行 OCR ..." #: paperwork-shell/src/paperwork_shell/cmd/reset.py:70 msgid "Reset a page to its original content" msgstr "将页面重置为其原始内容" #: paperwork-shell/src/paperwork_shell/cmd/reset.py:108 msgid "Reseting document {} page {} ..." msgstr "重置文档 {} 页面 {} ..." #: paperwork-shell/src/paperwork_shell/cmd/reset.py:118 msgid "Reseted:" msgstr "重置:" #: paperwork-shell/src/paperwork_shell/cmd/move.py:58 msgid "Move a page" msgstr "移动页面" #: paperwork-shell/src/paperwork_shell/cmd/move.py:62 msgid "Source document" msgstr "源文档" #: paperwork-shell/src/paperwork_shell/cmd/move.py:66 msgid "Page to move" msgstr "要移动的页面" #: paperwork-shell/src/paperwork_shell/cmd/move.py:70 msgid "Destination document" msgstr "目标文档" #: paperwork-shell/src/paperwork_shell/cmd/move.py:74 msgid "Target page number" msgstr "目标页码" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:55 msgid "Manage scanner configuration" msgstr "管理扫描仪配置" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:58 #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:48 msgid "sub-command" msgstr "子命令" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:62 msgid "List all scanners and their possible settings" msgstr "列出所有扫描仪及其可能的设置" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:67 msgid "Show the currently selected scanner and its settings" msgstr "显示当前选定的扫描仪及其设置" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:72 msgid "Define which scanner and which settings to use" msgstr "明确要使用的扫描仪和设置" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:75 msgid "Scanner to use" msgstr "要使用的扫描仪" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:80 msgid "" "Default source on the scanner to use (if not specified, one will be selected " "randomly)" msgstr "扫描仪上要使用的默认源 (如果未指定,将随机选择一个)" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:86 msgid "Default resolution (dpi ; default=300)" msgstr "默认分辨率 (dpi ; 默认值=300)" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:91 msgid "Examining scanner {} ..." msgstr "检查扫描仪 {} ..." #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:131 #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:153 msgid "ID:" msgstr "ID:" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:133 #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:154 msgid "Source:" msgstr "源:" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:135 msgid "Resolutions:" msgstr "分辨率:" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:155 msgid "Resolution:" msgstr "分辨率:" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:183 msgid "Source {} not found on device. Using another source" msgstr "在设备上找不到源 {}。使用其他源" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:203 msgid "Default source:" msgstr "默认源:" #: paperwork-shell/src/paperwork_shell/cmd/scanner.py:217 msgid "Resolution {} not available. Adjusted to {}." msgstr "" #: paperwork-shell/src/paperwork_shell/cmd/show.py:51 msgid "Show the content of a document" msgstr "显示文档的内容" #: paperwork-shell/src/paperwork_shell/cmd/show.py:99 #, python-format msgid "Page %d" msgstr "页面 %d" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:57 msgid "Synchronize the index(es) with the content of the work directory" msgstr "将索引与工作目录的内容同步" #: paperwork-shell/src/paperwork_shell/cmd/sync.py:68 msgid "Synchronizing with work directory ..." msgstr "与工作目录同步 ..." #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:43 msgid "Manage additional text attached to documents" msgstr "管理附加到文档的其他文本" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:52 msgid "Get a document additional text" msgstr "获取文档附加文本" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:57 msgid "Set a document additional text" msgstr "设置文档附加文本" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:79 #: paperwork-shell/src/paperwork_shell/display/docrendering/extra_text.py:32 msgid "Additional text:" msgstr "附加文本:" #: paperwork-shell/src/paperwork_shell/cmd/extra_text.py:82 msgid "No additional text" msgstr "无附加文本" #: paperwork-shell/src/paperwork_shell/main.py:67 msgid "command" msgstr "命令" #: paperwork-shell/src/paperwork_shell/display/scan.py:86 msgid "Scanning page {} (expected size: {}x{}) ..." msgstr "正在扫描页面 {} (预计大小: {}x{}) ..." #: paperwork-shell/src/paperwork_shell/display/scan.py:127 msgid "Page {} scanned (actual size: {}x{})" msgstr "页面 {} 已扫描 (实际大小: {}x{})" #: paperwork-shell/src/paperwork_shell/display/scan.py:135 msgid "End of paper feed" msgstr "" #: paperwork-shell/src/paperwork_shell/display/scan.py:151 msgid "Page {} in document {} created" msgstr "页面 {} 位于文档 {} 中已被创建" paperwork-2.1.1/paperwork-shell/setup.py000077500000000000000000000056131417573700700204010ustar00rootroot00000000000000#!/usr/bin/env python3 import os import sys import setuptools quiet = '--quiet' in sys.argv or '-q' in sys.argv try: with open("src/paperwork_shell/_version.py", "r") as file_descriptor: version = file_descriptor.read().strip() version = version.split(" ")[2][1:-1] if not quiet: print("Paperwork-shell version: {}".format(version)) if "-" in version: version = version.split("-")[0] except FileNotFoundError: print("ERROR: _version.py file is missing") print("ERROR: Please run 'make version' first") sys.exit(1) install_requires = [ "openpaperwork-core", "paperwork-backend", ] if os.name != "nt": install_requires.append("fabulous") setuptools.setup( name="paperwork-shell", version=version, description="Paperwork's shell interface", long_description="""Paperwork is a GUI to make papers searchable. - paperwork-cli : a interactive shell frontend for Paperwork. - paperwork-json : a non-interactive shell frontend for Paperwork that always return JSON results. """, keywords="documents", url=( "https://gitlab.gnome.org/World/OpenPaperwork/paperwork/tree/master/" "paperwork-shell" ), download_url=( "https://gitlab.gnome.org/World/OpenPaperwork/paperwork/-" "/archive/{}/paperwork-{}.tar.gz".format(version, version) ), classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: End Users/Desktop", ("License :: OSI Approved ::" " GNU General Public License v3 or later (GPLv3+)"), "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", "Topic :: Multimedia :: Graphics :: Capture :: Scanners", "Topic :: Multimedia :: Graphics :: Graphics Conversion", "Topic :: Scientific/Engineering :: Image Recognition", "Topic :: Text Processing :: Filters", "Topic :: Text Processing :: Indexing", ], license="GPLv3+", author="Jerome Flesch", author_email="jflesch@openpaper.work", packages=setuptools.find_packages('src'), include_package_data=True, package_dir={'': 'src'}, entry_points={ 'console_scripts': [ 'paperwork-cli = paperwork_shell.main:cli_main', 'paperwork-json = paperwork_shell.main:json_main', ], }, zip_safe=True, install_requires=install_requires, ) if quiet: sys.exit(0) print("============================================================") print("============================================================") print("|| IMPORTANT ||") print("|| Please run 'paperwork-cli chkdeps' ||") print("|| to find any missing dependency ||") print("============================================================") print("============================================================") paperwork-2.1.1/paperwork-shell/src/000077500000000000000000000000001417573700700174465ustar00rootroot00000000000000paperwork-2.1.1/paperwork-shell/src/paperwork_shell/000077500000000000000000000000001417573700700226475ustar00rootroot00000000000000paperwork-2.1.1/paperwork-shell/src/paperwork_shell/__init__.py000066400000000000000000000001151417573700700247550ustar00rootroot00000000000000import gettext def _(s): return gettext.dgettext('paperwork_shell', s) paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/000077500000000000000000000000001417573700700234125ustar00rootroot00000000000000paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/__init__.py000066400000000000000000000000001417573700700255110ustar00rootroot00000000000000paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/about/000077500000000000000000000000001417573700700245245ustar00rootroot00000000000000paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/about/__init__.py000066400000000000000000000124431417573700700266410ustar00rootroot00000000000000import os import random import subprocess import sys import fabulous.image import fabulous.text import openpaperwork_core from ... import _ # XXX(Jflesch): crappy workaround for an unmaintained library ... fabulous.image.basestring = str VALIDATED_FONTS = """ comic Comic_Sans_MS Comic_Sans_MS_Bold comicbd LiberationMono-Bold LiberationSans-Bold times timesbd FreeMono FreeMonoBold FreeMonoBoldOblique FreeMonoOblique FreeSans FreeSansBold FreeSansBoldOblique FreeSansOblique FreeSerif FreeSerifBold FreeSerifBoldItalic FreeSerifItalic """.strip().split() # to make copy and paste faster COLORS = [ '#0099ff', '#ff4400', '#00bd22', '#bf00ff', ] class Paperwork(object): def __init__(self, core, fonts): self.core = core self.fonts = fonts def show(self, file=sys.stdout): nb_lines = 0 logo = self.core.call_success( "resources_get_file", "paperwork_shell.cmd.about", "logo.png" ) logo = self.core.call_success("fs_unsafe", logo) logo = fabulous.image.Image(logo, width=60) logo = logo.reduce(logo.convert()) for line in logo: print(16 * " ", line, file=file, sep="") nb_lines += 1 font = "FreeSans" if font not in self.fonts: font = self.fonts[0] txt = fabulous.text.Text( "Paperwork", skew=5, color="#abcdef", font=font, fsize=25, shadow=False ) for line in txt: print(line, file=file) nb_lines += 1 print("Paperwork", file=file) print(8 * " ", _('Version: ') + self.core.call_success("app_get_version"), file=file, sep="") print(8 * " ", _("Because sorting documents is a machine's job."), file=file, sep="") nb_lines += 3 return nb_lines class Section(object): def __init__(self, name, authors, fonts): self.name = name self.authors = authors self.fonts = fonts @staticmethod def _group_small_words(words): buf = [] for word in words: buf.append(word) if len(word) > 3: yield " ".join(buf) buf = [] if len(buf) > 0: yield " ".join(buf) def show(self, file=sys.stdout): nb_lines = 0 color = random.choice(COLORS) font = random.choice(self.fonts) words = self.name.split() for word in self._group_small_words(words): txt = fabulous.text.Text( word, skew=0, color=color, font=font, fsize=18, shadow=False ) for line in txt: print(line, file=file) nb_lines += 1 print(self.name, file=file) nb_lines += 1 for author in self.authors: txt = author[1] if author[2] > 0: txt += " ({})".format(author[2]) print(8 * " ", txt, file=file, sep="") nb_lines += 1 return nb_lines class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['shell'] def get_deps(self): return [ { 'interface': 'app', 'defaults': ['paperwork_backend.app'], }, { 'interface': 'authors', 'defaults': ['paperwork_backend.authors'], }, { 'interface': 'fs', 'defaults': ['openpaperwork_core.fs.python'], }, { 'interface': 'resources', 'defaults': ['openpaperwork_core.resources.setuptools'], }, ] def cmd_complete_argparse(self, parser): parser.add_parser('about', help=_("About Paperwork")) def cmd_run(self, args): if args.command != 'about': return None fonts = self._get_available_fonts() sections = {} self.core.call_all("authors_get", sections) sections = [x for x in sections.items()] sections.sort(key=lambda x: x[0].lower()) sections = ( [Paperwork(self.core, fonts)] + [Section(k, v, fonts) for (k, v) in sections if len(v) >= 0] ) pager = os.environ.get('PAGER', 'less') try: pager = subprocess.Popen([pager, '-R'], universal_newlines=True, stdin=subprocess.PIPE, stdout=sys.stdout) except OSError: # Just output regularly, without a pager. output = sys.stdout pager = None else: output = pager.stdin try: for section in sections: section.show(file=output) if section != sections[-1]: for x in range(0, 7): print(file=output) print(file=output) print(file=output) finally: if pager is not None: pager.stdin.close() pager.wait() def _get_available_fonts(self): all_fonts = fabulous.text.get_font_files() all_fonts = {n for n in all_fonts.keys()} out = [] for font in VALIDATED_FONTS: if font not in all_fonts: continue out.append(font) return out paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/about/logo.png000066400000000000000000000030301417573700700261660ustar00rootroot00000000000000PNG  IHDR n<gAMA a cHRMz&u0`:pQ<bKGD#2tIMEEIDATXyPeƟ]Y;YN*=ڤlHy_t"iKhԤU6[YZ|'vQ`hpP P  m?R?| , De~/Wؐ9LG(%IէȼdJ;Tq<^rFۉ,{*ë,&!&可w wb L g%Ӳ.UVB[ʶ MC_PgcC)U3[;9jxeP`JٴUz&w׮ b @pT\M-2|4@w@lu>щ"0ޚz8~.&xwEw}W<̙,99{[QOwQ==~gv]:k^%Q%tEXtdate:create2020-06-27T22:14:27+00:00vP2%tEXtdate:modify2020-06-27T22:14:27+00:00 @>tEXtsvg:comment Created with Inkscape (http://www.inkscape.org/) EGtEXtsvg:title 8#NIENDB`paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/chkworkdir.py000066400000000000000000000071451417573700700261420ustar00rootroot00000000000000try: # Fabulous is available and installed on GNU/Linux systems, but not on # Windows. Still this command can be called on Windows systems using # "paperwork-json" import fabulous import fabulous.color FABULOUS_AVAILABLE = True except (ImportError, ValueError): FABULOUS_AVAILABLE = False import openpaperwork_core import openpaperwork_core.cmd.util from .. import _ class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.interactive = False def get_interfaces(self): return ['shell'] def get_deps(self): return [ { 'interface': 'chkworkdir', 'defaults': [ 'paperwork_backend.chkworkdir.empty_doc', 'paperwork_backend.chkworkdir.label_color', ], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): p = parser.add_parser( 'chkworkdir', help=_("Check and fix work directory integrity") ) p.add_argument( '--yes', '-y', required=False, default=False, action='store_true', help=_("Don't ask to fix things, just fix them") ) @staticmethod def _color(color): assert(FABULOUS_AVAILABLE) color = "#%1X%1X%1X" % ( int(color[0] * 0xF), int(color[1] * 0xF), int(color[2] * 0xF), ) return fabulous.color.bg256(color, " ") + " " def cmd_run(self, args): if args.command != 'chkworkdir': return None if self.interactive: print(_("Checking work directory ...")) problems = [] self.core.call_all("check_work_dir", problems) if len(problems) <= 0: if self.interactive: print(_("No problem found")) return problems if not args.yes: if not self.interactive: return problems print("") print(_("%d problems found:") % len(problems)) for problem in problems: problem_color = ( "" if "problem_color" not in problem else self._color(problem['problem_color']) ) solution_color = ( "" if "solution_color" not in problem else self._color(problem['solution_color']) ) print("[{}]".format(problem['problem'])) print( _("- Problem: ") + problem_color + problem['human_description']['problem'] ) print( _("- Possible solution: ") + solution_color + problem['human_description']['solution'] ) print("") msg = _( "Do you want to fix those problems automatically" " using the indicated solutions ?" ) r = openpaperwork_core.cmd.util.ask_confirmation(msg, default='n') if r != 'y': return problems self.core.call_all("fix_work_dir", problems) if self.interactive: print(_("All fixed !")) print(_("Synchronizing with work directory ...")) self.core.call_all("transaction_sync_all") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") if self.interactive: print(_("All done !")) return problems paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/delete.py000066400000000000000000000112121417573700700252230ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import sys import openpaperwork_core from openpaperwork_core.cmd.util import ask_confirmation from .util import parse_page_list from .. import _ class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.interactive = False def get_interfaces(self): return ['shell'] def get_deps(self): return [ { "interface": "document_storage", "defaults": ['paperwork_backend.model.workdir'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_core.mainloop.asyncio'], }, { "interface": "pages", "defaults": [ 'paperwork_backend.model.hocr', 'paperwork_backend.model.img', 'paperwork_backend.model.thumbnail', ], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): p = parser.add_parser( 'delete', help=_("Delete a document or a page") ) p.add_argument( '--pages', '-p', type=str, required=False, help=_( "Pages to delete" " (single integer, range or comma-separated list," " default: all pages)" ) ) p.add_argument( 'doc_ids', nargs='*', default=[], help=_("Target documents") ) def cmd_run(self, args): if args.command != 'delete': return None doc_ids = args.doc_ids for doc_id in doc_ids: if "/" in doc_id or "\\" in doc_id or ".." in doc_id: print("Invalid doc_id: {}".format(doc_id)) sys.exit(2) pages = parse_page_list(args) del_doc_msg = _("Deleting document {doc_id} ...") del_page_msg = _("Deleting page {page_idx} of document {doc_id} ...") for doc_id in doc_ids: if self.interactive: if pages is None: r = ask_confirmation( _("Delete document %s ?") % str(doc_id), default='n' ) else: r = ask_confirmation( _( "Delete page(s)" " {page_indexes} of document {doc_id} ?".format( page_indexes=str([p + 1 for p in pages]), doc_id=str(doc_id) ) ), default='n' ) if r != 'y': continue if pages is None: if self.interactive: print(del_doc_msg.format(doc_id=doc_id)) self.core.call_all("storage_delete_doc_id", doc_id) else: for page in pages: doc_url = self.core.call_success( "doc_id_to_url", doc_id ) if self.interactive: print(del_page_msg.format( page_idx=(page + 1), doc_id=doc_id) ) self.core.call_all("page_delete_by_url", doc_url, page) self.core.call_success( "transaction_simple", [ # transaction_simple() will automatically replace the change # 'upd' by 'del' for the documents that don't exist anymore ("upd", doc_id) for doc_id in doc_ids ] ) self.core.call_success("mainloop_quit_graceful") self.core.call_success("mainloop") return True paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/edit.py000066400000000000000000000135231417573700700247150ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import logging import shutil import sys import openpaperwork_core import paperwork_backend.pageedit from . import util from .. import _ LOGGER = logging.getLogger(__name__) class NullUI(paperwork_backend.pageedit.AbstractPageEditorUI): pass class CliUI(paperwork_backend.pageedit.AbstractPageEditorUI): def __init__(self, core): super().__init__() self.core = core def show_preview(self, img): terminal_width = shutil.get_terminal_size()[0] - 1 img = self.core.call_success( "img_render", img, terminal_width=terminal_width ) if img is None: return for line in img: print(line) print() class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.interactive = False def get_interfaces(self): return ['shell'] def get_deps(self): return [ # optional dependency # { # "interface": "img_renderer", # "defaults": ["paperwork_shell.display.docrendering.img"], # }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_core.mainloop.asyncio'], }, { "interface": "page_editor", "defaults": ["paperwork_backend.pageedit.pageeditor"], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): # just so we can get the modifier list editor = self.core.call_success("page_editor_get", None, 0, NullUI()) modifiers = editor.get_modifiers() modifiers = [ modifier['id'] for modifier in modifiers if not modifier['need_frame'] ] edit_parser = parser.add_parser('edit', help=_("Edit page")) edit_parser.add_argument( '--modifiers', '-m', type=str, required=True, help=_( "List of image modifiers (comma separated, possible values:" " {})" ).format(modifiers) ) # we need page number(s), but for consistency with other commands, # we require this argument as an option like '--opt' instead of # a positional argument. edit_parser.add_argument('--pages', '-p', type=str, required=True) edit_parser.add_argument('doc_id') def cmd_run(self, args): if args.command != 'edit': return None doc_id = args.doc_id doc_url = self.core.call_success("doc_id_to_url", doc_id) pages = util.parse_page_list(args) modifiers = args.modifiers.split(",") out = [] for page_idx in pages: out.append((doc_id, page_idx)) if self.interactive: print( _("Modifying document {} page {} ...").format( doc_id, page_idx ) ) print(_("Original:")) ui = CliUI(self.core) else: ui = NullUI() page_editor = self.core.call_success( "page_editor_get", doc_url, page_idx, ui ) promise = openpaperwork_core.promise.Promise(self.core) for modifier in modifiers: if self.interactive: promise = promise.then(print, "{}:".format(modifier)) promise = promise.then( page_editor.on_modifier_selected(modifier) ) if promise is not None: if self.interactive: promise = promise.then( sys.stdout.write, _("Generating in high quality and saving ...") + " " ) promise = promise.then( lambda *args, **kwargs: sys.stdout.flush() ) promise = promise.then(page_editor.on_save()) if self.interactive: promise = promise.then(print, "Done") def on_err(exc): LOGGER.error( "Edition of %s p%d failed", doc_id, page_idx, exc_info=exc ) page_editor.on_cancel() raise exc promise.catch(on_err) promise.schedule() self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") if self.interactive: sys.stdout.write(_("Committing ...") + " ") sys.stdout.flush() self.core.call_success("transaction_simple", (("upd", doc_id),)) self.core.call_success("mainloop_quit_graceful") self.core.call_success("mainloop") if self.interactive: print(_("Done")) print(_("All done !")) return out paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/export.py000066400000000000000000000140101417573700700253010ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import sys import openpaperwork_core import openpaperwork_core.promise import paperwork_backend.docexport from . import util from .. import _ class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.interactive = False def get_interfaces(self): return ['shell'] def get_deps(self): return [ { "interface": "fs", "defaults": ["openpaperwork_gtk.fs.gio"], }, { "interface": "export_pipes", "defaults": [ 'paperwork_backend.docexport.img', 'paperwork_backend.docexport.pdf', 'paperwork_backend.docexport.pillowfight', ], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): p = parser.add_parser( 'export', help=_( "Export a document, a page, or a set of pages." " Example:" " paperwork-cli export 20150303_2314_39 -p 2 -f img_boxes" " -f grayscale -f jpeg -o ~/tmp/pouet.jpg" ) ) p.add_argument('doc_id', help=_('Document to export')) p.add_argument( '--pages', '-p', type=str, required=False, help=_( "Pages to export" " (single integer, range or comma-separated list," " default: all pages)" ) ) p.add_argument( '--filters', '-f', nargs=1, action='append', type=str, required=False, help=_( "Export filters. Specify this option once for each filter" " to apply (ex: '-f grayscale -f jpeg')." ) ) p.add_argument( '--out', '-o', type=str, required=False, help=_( "Output file/directory. If not specified, will list" " the filters that could be chained after those already" " specified." ) ) def cmd_run(self, args): if args.command != 'export': return None # argument parsing doc_id = args.doc_id doc_url = self.core.call_success("doc_id_to_url", doc_id) pages = util.parse_page_list(args) filters = args.filters if args.filters is not None else [] out = args.out if pages is None or len(pages) <= 0: input_value = paperwork_backend.docexport.ExportData.build_doc( doc_id, doc_url ) else: input_value = paperwork_backend.docexport.ExportData.build_pages( doc_id, doc_url, pages ) if len(filters) <= 0: output_type = None else: filters_str = [x[0] for x in filters] filters = [ self.core.call_success('export_get_pipe_by_name', f) for f in filters_str ] if None in filters: print(_("Unknown filters: %s") % filters_str) sys.exit(1) output_type = filters[-1].output_type # If no output is provided if out is None or out == "": next_pipes = [] if output_type is not None: self.core.call_all( "export_get_pipes_by_input", next_pipes, output_type ) elif pages is not None and len(pages) > 0: self.core.call_all( "export_get_pipes_by_page", next_pipes, doc_url, pages[0] ) else: self.core.call_all( "export_get_pipes_by_doc_url", next_pipes, doc_url ) next_pipes = [pipe.name for pipe in next_pipes] if self.interactive: print( _("Current filters: %s") % [pipe.name for pipe in filters] ) if len(next_pipes) > 0: print(_("Next possible filters:")) for pipe in next_pipes: print("- " + pipe) elif len(filters) > 0: print(_( "'{filter_name}' is an output filter." " No other filter can be added after '{filter_name}'." ).format(filter_name=filters[-1].name)) else: print(_("No possible filters found")) return next_pipes # else try to export out = self.core.call_success("fs_safe", out) def get_input_value(): return input_value promise = openpaperwork_core.promise.Promise( self.core, get_input_value ) for pipe in filters: promise = promise.then( pipe.get_promise(result='final', target_file_url=out) ) sys.stdout.write(_("Exporting to %s ... ") % out) sys.stdout.flush() self.core.call_one("mainloop_schedule", promise.schedule) self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") sys.stdout.write(_("Done") + "\n") return self.core.call_success("fs_exists", out) is not None paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/extra_text.py000066400000000000000000000056241417573700700261620ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import openpaperwork_core from .. import _ class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.interactive = False def get_interfaces(self): return ['shell'] def get_deps(self): return [ { "interface": "extra_text", "defaults": ['paperwork_backend.model.extra_text'], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): extra_text_parser = parser.add_parser( 'extra_text', help=_( "Manage additional text attached to documents" ) ) subparser = extra_text_parser.add_subparsers( help=_('sub-command'), dest='subcommand', required=True ) parser = subparser.add_parser( 'get', help=_("Get a document additional text") ) parser.add_argument('doc_id') parser = subparser.add_parser( 'set', help=_("Set a document additional text") ) parser.add_argument('doc_id') parser.add_argument('text') def cmd_run(self, args): if args.command != 'extra_text': return None doc_id = args.doc_id doc_url = self.core.call_success("doc_id_to_url", doc_id) if args.subcommand == "get": return self._cmd_get(doc_id, doc_url) elif args.subcommand == "set": return self._cmd_set(doc_id, doc_url, args.text) else: return None def _cmd_get(self, doc_id, doc_url): text = [] self.core.call_success("doc_get_extra_text_by_url", text, doc_url) if self.interactive: if len(text) > 0: print(" " + _("Additional text:")) print("\n".join(text)) else: print(_("No additional text")) return text def _cmd_set(self, doc_id, doc_url, text): text = text.strip() self.core.call_success("doc_set_extra_text_by_url", doc_url, text) if self.interactive: print(_("Done")) return True paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/import.py000066400000000000000000000140531417573700700253010ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import logging import sys import openpaperwork_core import openpaperwork_core.promise import paperwork_backend.docimport from .. import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.interactive = False def get_interfaces(self): return ['shell'] def get_deps(self): return [ { "interface": "fs", "defaults": "openpaperwork_gtk.fs.gio", }, { 'interface': 'doc_labels', 'defaults': ['paperwork_backend.model.labels'], }, { "interface": "import", "defaults": [ 'paperwork_backend.docimport.img', 'paperwork_backend.docimport.pdf', ], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): p = parser.add_parser( 'import', help=_( "Import file(s)" ) ) p.add_argument( '--doc_id', '--doc', '-d', type=str, required=False, help=_("Target document for import"), ) p.add_argument( '--password', type=str, required=False, help=_("PDF password"), ) p.add_argument( 'files', type=str, nargs='*', help=_("Files to import") ) def _file_import_to_dict(self, file_import): return { "imported": list(file_import.imported_files), "ignored": list(file_import.ignored_files), "new_docs": list(file_import.new_doc_ids), "upd_docs": list(file_import.upd_doc_ids), "stats": dict(file_import.stats) } def cmd_run(self, args): if args.command != 'import': return None file_import = paperwork_backend.docimport.FileImport( [self.core.call_success("fs_safe", f) for f in args.files], active_doc_id=args.doc_id ) importers = [] self.core.call_all("get_importer", importers, file_import) choice = 0 if len(importers) <= 0: if self.interactive: print( _("Don't know how to import file(s) %s") % args.files ) LOGGER.warning("Don't know how to import file(s) %s", args.files) return self._file_import_to_dict(file_import) if len(importers) > 1: if not self.interactive: LOGGER.warning( "Found many ways to import file(s) %s. Running in" " non-interactive mode. Cannot request which method" " must be used", args.files ) return self._file_import_to_dict(file_import) print(_("Found many ways to import file(s) %s.") % args.files) print(_("Please select the way you want:")) choice = -1 while choice not in range(0, len(importers)): for (idx, importer) in enumerate(importers): print(" {} - {}".format(idx + 1, importer.get_name())) sys.stdout.write("? ") sys.stdout.flush() choice = int(input()) - 1 importer = importers[choice] del importers if self.interactive: # We must load the labels before importing. Because the label # guesser may want to add labels on documents, and therefore # we need to know their color # TODO(Jflesch): That's slow and overkill. There should be a better # way (maybe storing the labels in ~/.local/share/paperwork2 ?) print(_("Loading labels ...")) promises = [] self.core.call_all("label_load_all", promises) promise = promises[0] for p in promises[1:]: promise = promise.then(p) # use transaction_schedule to make sure that document imports # are not done before we have loaded the labels self.core.call_one("transaction_schedule", promise) data = {} if args.password is not None: data['password'] = args.password promise = importer.get_import_promise(data) if self.interactive: print(_("Importing %s ...") % args.files) self.core.call_success( "mainloop_schedule", self.core.call_success, "transaction_schedule", promise ) self.core.call_all("mainloop_quit_graceful") self.core.call_success("mainloop") if self.interactive: print(_("Done")) print(_("Import result:")) print(_("- Imported files: %s") % file_import.imported_files) print(_("- Non-imported files: %s") % file_import.ignored_files) print(_("- New documents: %s") % file_import.new_doc_ids) print(_("- Updated documents: %s") % file_import.upd_doc_ids) for (k, v) in file_import.stats.items(): print("- {}: {}".format(k, v)) return self._file_import_to_dict(file_import) paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/label.py000066400000000000000000000163571417573700700250570ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import sys import openpaperwork_core from openpaperwork_core.cmd.util import ask_confirmation from .. import _ class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.interactive = False def get_interfaces(self): return ['shell'] def get_deps(self): return [ { "interface": "doc_labels", "defaults": ["paperwork_backend.model.labels"], }, { "interface": "document_storage", "defaults": ["paperwork_backend.model.workdir"], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_core.mainloop.asyncio'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): label_parser = parser.add_parser( 'label', help=_("Commands to manage labels") ) subcmd_parser = label_parser.add_subparsers( help=_("label command"), dest='sub_command', required=True ) subcmd_parser.add_parser('list') p = subcmd_parser.add_parser('show') p.add_argument( 'doc_ids', nargs='*', default=[], help=_("Target documents") ) p = subcmd_parser.add_parser('add') p.add_argument('doc_id', help=_("Target document")) p.add_argument('label_name', help=_("Label to add")) p.add_argument( '--color', '-c', default=None, help=_("Label color (ex: '#aa22cc')"), required=False ) p = subcmd_parser.add_parser('remove') p.add_argument('doc_id', help=_("Target document")) p.add_argument('label_name', help=_("Label to remove")) p = subcmd_parser.add_parser('delete') p.add_argument( 'label_name', help=_("Label to delete from *all* documents") ) def _load_all_labels(self): if self.interactive: sys.stdout.write(_("Loading all labels ... ")) sys.stdout.flush() promises = [] self.core.call_all("label_load_all", promises) promise = promises[0] for p in promises[1:]: promise = promise.then(p) self.core.call_one("mainloop_schedule", promise.schedule) self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") if self.interactive: sys.stdout.write(_("Done") + "\n") def _upd_doc(self, doc_id): self.core.call_success("transaction_simple", (("upd", doc_id),)) self.core.call_success("mainloop_quit_graceful") self.core.call_success("mainloop") def _show(self, doc_ids): out = {} for doc_id in doc_ids: doc_url = self.core.call_success("doc_id_to_url", doc_id) labels = set() self.core.call_all("doc_get_labels_by_url", labels, doc_url) labels = list(labels) labels.sort() out[doc_id] = labels if self.interactive: sys.stdout.write("{}: ".format(doc_id)) self.core.call_all("print_labels", labels, separator=" ") return out def cmd_run(self, args): if args.command != 'label': return None if args.sub_command == 'list': self._load_all_labels() labels = set() self.core.call_all("labels_get_all", labels) labels = list(labels) labels.sort() if self.interactive: print() self.core.call_all("print_labels", labels) return labels elif args.sub_command == 'show': return self._show(args.doc_ids) elif args.sub_command == "add": color = args.color if color is not None: # make sure the color is valid color = self.core.call_success("label_color_to_rgb", color) color = self.core.call_success("label_color_from_rgb", color) self._load_all_labels() doc_url = self.core.call_success("doc_id_to_url", args.doc_id) self.core.call_success( "doc_add_label_by_url", doc_url, args.label_name, color ) self._upd_doc(args.doc_id) return self._show([args.doc_id]) elif args.sub_command == "remove": doc_url = self.core.call_success("doc_id_to_url", args.doc_id) self.core.call_all( "doc_remove_label_by_url", doc_url, args.label_name ) self._upd_doc(args.doc_id) return self._show([args.doc_id]) elif args.sub_command == "delete": label = args.label_name if self.interactive: r = ask_confirmation( _( "Are you sure you want to delete label '%s' from all" " documents ?" ) % label, default='n' ) if r != 'y': sys.exit(1) all_docs = [] self.core.call_all("storage_get_all_docs", all_docs) updated_docs = [] for (doc_id, doc_url) in all_docs: labels = set() self.core.call_all("doc_get_labels_by_url", labels, doc_url) labels = {l for (l, c) in labels} if label not in labels: continue if self.interactive: print("Removing label '{}' from document '{}'".format( label, doc_id )) updated_docs.append(doc_id) self.core.call_all("doc_remove_label_by_url", doc_url, label) if self.interactive: sys.stdout.write("Committing changes in index ... ") sys.stdout.flush() self.core.call_success("transaction_simple", [ ("upd", doc_id) for doc_id in updated_docs ]) self.core.call_success("mainloop_quit_graceful") self.core.call_success("mainloop") if self.interactive: sys.stdout.write("Done\n") return updated_docs if self.interactive: print("Unknown label command: {}".format(args.sub_command)) return None paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/move.py000066400000000000000000000062111417573700700247320ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import openpaperwork_core from .. import _ class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.interactive = False def get_interfaces(self): return ['shell'] def get_deps(self): return [ { "interface": "document_storage", "defaults": ['paperwork_backend.model.workdir'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_core.mainloop.asyncio'], }, { "interface": "pages", "defaults": [ 'paperwork_backend.model.hocr', 'paperwork_backend.model.img', 'paperwork_backend.model.thumbnail', ], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): p = parser.add_parser( 'move', help=_("Move a page") ) p.add_argument( 'source_doc_id', help=_("Source document") ) p.add_argument( 'source_page', type=int, help=_("Page to move") ) p.add_argument( 'dest_doc_id', help=_("Destination document") ) p.add_argument( 'dest_page', type=int, help=_("Target page number") ) def cmd_run(self, args): if args.command != 'move': return None source_doc_id = args.source_doc_id source_page_idx = args.source_page - 1 dest_doc_id = args.dest_doc_id dest_page_idx = args.dest_page - 1 source_doc_url = self.core.call_success("doc_id_to_url", source_doc_id) dest_doc_url = self.core.call_success("doc_id_to_url", dest_doc_id) self.core.call_all( "page_move_by_url", source_doc_url, source_page_idx, dest_doc_url, dest_page_idx ) self.core.call_success("transaction_simple", ( ("upd", source_doc_id), ("upd", dest_doc_id), )) self.core.call_success("mainloop_quit_graceful") self.core.call_success("mainloop") return True paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/ocr.py000066400000000000000000000062461417573700700245570ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import sys import openpaperwork_core from . import util from .. import _ class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.interactive = False def get_interfaces(self): return ['shell'] def get_deps(self): return [ { "interface": "ocr", "defaults": ['paperwork_backend.guesswork.ocr.pyocr'], }, { "interface": "ocr_settings", "defaults": ['paperwork_backend.pyocr'], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): p = parser.add_parser( 'ocr', help=_( "OCR document or pages" ) ) p.add_argument( 'doc_id', type=str, help=_("Document on which OCR must be run") ) p.add_argument( '--pages', '-p', type=str, help=_( "Pages to OCR" " (single integer, range or comma-separated list," " default: all pages)" ) ) def cmd_run(self, args): if args.command != 'ocr': return None if self.core.call_success("ocr_is_enabled") is None: if self.interactive: print("OCR is disabled") return [] doc_id = args.doc_id doc_url = self.core.call_success("doc_id_to_url", doc_id) pages = util.parse_page_list(args) if pages is None: nb_pages = self.core.call_success( "doc_get_nb_pages_by_url", doc_url ) pages = range(0, nb_pages) out = [] for page_idx in pages: if self.interactive: sys.stdout.write( _( "Running OCR on" " document {doc_id} page {page_idx} ...".format( doc_id=doc_id, page_idx=(page_idx + 1) ) ) + " " ) sys.stdout.flush() self.core.call_all("ocr_page_by_url", doc_url, page_idx) if self.interactive: sys.stdout.write(_("Done") + "\n") out.append((doc_id, page_idx)) if self.interactive: print(_("All done !")) return out paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/rename.py000066400000000000000000000054731417573700700252440ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import openpaperwork_core from .. import _ class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.interactive = False def get_interfaces(self): return ['shell'] def get_deps(self): return [ { "interface": "document_storage", "defaults": ['paperwork_backend.model.workdir'], }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_core.mainloop.asyncio'], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): p = parser.add_parser( 'rename', help=_("Change a document identifier") ) p.add_argument( 'source_doc_id', help=_("Document to rename") ) p.add_argument( 'dest_doc_id', help=_("New name for the document") ) def cmd_run(self, args): if args.command != 'rename': return None source_doc_id = args.source_doc_id dest_doc_id = args.dest_doc_id source_doc_url = self.core.call_success("doc_id_to_url", source_doc_id) dest_doc_url = self.core.call_success( "doc_id_to_url", dest_doc_id, existing=False ) if self.interactive: print("Renaming: {} --> {}".format(source_doc_url, dest_doc_url)) self.core.call_all("doc_rename_by_url", source_doc_url, dest_doc_url) self.core.call_success( "transaction_simple", ( ('del', source_doc_id), ('add', dest_doc_id), ) ) self.core.call_success("mainloop_quit_graceful") self.core.call_success("mainloop") if self.interactive: print("{} renamed into {}".format(source_doc_id, dest_doc_id)) return True paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/reset.py000066400000000000000000000101321417573700700251030ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import logging import shutil import sys import openpaperwork_core from . import util from .. import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.interactive = False def get_interfaces(self): return ['shell'] def get_deps(self): return [ # optional dependency # { # "interface": "img_renderer", # "defaults": ["paperwork_shell.display.docrendering.img"], # }, { 'interface': 'mainloop', 'defaults': ['openpaperwork_core.mainloop.asyncio'], }, { "interface": "page_reset", "defaults": ["paperwork_backend.model.img_overlay"], }, { "interface": "pillow", "defaults": [ 'openpaperwork_core.pillow.img', 'paperwork_backend.pillow.pdf', ], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): reset_parser = parser.add_parser( 'reset', help=_("Reset a page to its original content") ) # for safety, we mark the argument --pages as required reset_parser.add_argument('--pages', '-p', type=str, required=True) reset_parser.add_argument('doc_id') def show_page(self, doc_url, page_idx): if not self.interactive: return page_url = self.core.call_success( "page_get_img_url", doc_url, page_idx ) img = self.core.call_success("url_to_pillow", page_url) terminal_width = shutil.get_terminal_size()[0] - 1 img = self.core.call_success( "img_render", img, terminal_width=terminal_width ) if img is None: return for line in img: print(line) print() def cmd_run(self, args): if args.command != 'reset': return None doc_id = args.doc_id doc_url = self.core.call_success("doc_id_to_url", doc_id) pages = util.parse_page_list(args) out = [] for page_idx in pages: out.append((doc_id, page_idx)) if self.interactive: print( _("Reseting document {} page {} ...").format( doc_id, page_idx ) ) print(_("Original:")) self.show_page(doc_url, page_idx) self.core.call_all("page_reset_by_url", doc_url, page_idx) if self.interactive: print(_("Reseted:")) self.show_page(doc_url, page_idx) print("") if self.interactive: sys.stdout.write(_("Committing ...") + " ") sys.stdout.flush() self.core.call_success("transaction_simple", (("upd", doc_id),)) self.core.call_success("mainloop_quit_graceful") self.core.call_success("mainloop") if self.interactive: print(_("Done")) print(_("All done !")) return out paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/scan.py000066400000000000000000000045411417573700700247140ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import openpaperwork_core import openpaperwork_core.promise from .. import _ DEFAULT_RESOLUTION = 300 class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.interactive = False self.out = [] def get_interfaces(self): return ['shell'] def get_deps(self): return [ { "interface": "mainloop", "defaults": ['openpaperwork_gtk.mainloop.glib'], }, { "interface": "scan2doc", "defaults": ["paperwork_backend.docscan.scan2doc"], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): scan_parser = parser.add_parser( 'scan', help=_("Scan pages") ) scan_parser.add_argument( "--doc_id", "-d", help=_( "Document to which the scanned pages must be added" ) ) scan_parser.add_argument("source_id") def cmd_run(self, args): if args.command != 'scan': return None doc_url = None if args.doc_id is not None: doc_url = self.core.call_success("doc_id_to_url", args.doc_id) promise = self.core.call_success( "scan2doc_promise", doc_id=args.doc_id, doc_url=doc_url, source_id=args.source_id ) self.core.call_success("scan_schedule", promise) self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") return True paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/scanner.py000066400000000000000000000210451417573700700254170ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import openpaperwork_core import openpaperwork_core.promise from .. import _ DEFAULT_RESOLUTION = 300 class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.interactive = False self.out = [] def get_interfaces(self): return ['shell'] def get_deps(self): return [ { "interface": "mainloop", "defaults": ["openpaperwork_gtk.mainloop.glib"], }, { "interface": "config", "defaults": ['openpaperwork_core.config'], }, { "interface": "scan", "defaults": ['paperwork_backend.docscan.libinsane'], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): scanner_parser = parser.add_parser( 'scanner', help=_("Manage scanner configuration") ) subparser = scanner_parser.add_subparsers( help=_("sub-command"), dest='subcommand', required=True ) subparser.add_parser( 'list', help=_("List all scanners and their possible settings") ) subparser.add_parser( 'get', help=_( "Show the currently selected scanner and its settings" ) ) set_scanner = subparser.add_parser( 'set', help=_("Define which scanner and which settings to use") ) set_scanner.add_argument( "device_id", help=_("Scanner to use") ) set_scanner.add_argument( "--source", "-s", type=str, required=False, help=_( "Default source on the scanner to use (if not specified," " one will be selected randomly)" ) ) set_scanner.add_argument( "--resolution", "-r", type=int, required=False, help=_("Default resolution (dpi ; default=300)") ) def _get_scanner_info(self, dev, dev_id, dev_name): if self.interactive: print(_("Examining scanner {} ...").format(dev_id)) dev_out = { 'id': dev_id, 'name': dev_name, 'sources': [], } sources = dev.get_sources() for (source_id, source) in sources.items(): source_out = { "id": source_id, "resolutions": source.get_resolutions(), } dev_out['sources'].append(source_out) self.out.append(dev_out) return dev def _get_scanners_info(self, devs): promise = openpaperwork_core.promise.Promise(self.core) for dev in devs: dev_id = dev[0] dev_name = dev[1] promise = promise.then(self.core.call_success( "scan_get_scanner_promise", dev_id )) promise = promise.then(self._get_scanner_info, dev_id, dev_name) promise = promise.then(lambda scanner: scanner.close()) self.core.call_success("scan_schedule", promise) def _list_scanners(self): self.out = [] promise = self.core.call_success("scan_list_scanners_promise") promise = promise.then(self._get_scanners_info) self.core.call_success("scan_schedule", promise) self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") def _print_scanners(self): if self.interactive: print("") for dev in self.out: print(dev['name']) print(" |-- " + _("ID:") + " " + dev['id']) for source in dev['sources']: print(" |-- " + _("Source:") + " " + source['id']) print( " | |-- " + _("Resolutions:") + " " + str(source['resolutions']) ) print("") def _get_scanner(self): out = { 'id': self.core.call_success( "config_get", "scanner_dev_id" ), 'source': self.core.call_success( "config_get", "scanner_source_id" ), 'resolution': self.core.call_success( "config_get", "scanner_resolution" ), } if self.interactive: print(_("ID:") + " " + str(out['id'])) print(_("Source:") + " " + str(out['source'])) print(_("Resolution:") + " " + str(out['resolution'])) return out def _set_scanner(self, args): dev_settings = { 'id': args.device_id, 'source': args.source, 'resolution': ( args.resolution if args.resolution is not None else DEFAULT_RESOLUTION ), } # In any case, we want to make sure the settings provided are valid promise = self.core.call_success( "scan_get_scanner_promise", args.device_id ) def check_source(dev): sources = dev.get_sources() if ( dev_settings['source'] is not None and dev_settings['source'] not in sources ): if self.interactive: print(_( "Source {} not found on device." " Using another source" ).format(dev_settings['source'])) dev_settings['source'] = None if dev_settings['source'] is None: for (source_id, source_obj) in sources.items(): if 'flatbed' in source_id.lower(): source = (source_id, source_obj) break else: source = sources.popitem() dev_settings['source'] = source[0] else: source = ( dev_settings['source'], sources[dev_settings['source']] ) if self.interactive: print(_("Default source:") + " " + dev_settings['source']) return source[1] promise = promise.then(check_source) def check_resolution(source): resolutions = source.get_resolutions() if dev_settings['resolution'] in resolutions: return source resolution = min( resolutions, key=lambda x: abs(x - dev_settings['resolution']) ) if self.interactive: print(_("Resolution {} not available. Adjusted to {}.").format( dev_settings['resolution'], resolution )) dev_settings['resolution'] = resolution return source promise = promise.then(check_resolution) promise = promise.then(lambda source: source.close()) self.core.call_success("scan_schedule", promise) self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") self.core.call_all( "config_put", "scanner_dev_id", dev_settings['id'] ) self.core.call_all( "config_put", "scanner_source_id", dev_settings['source'] ) self.core.call_all( "config_put", "scanner_resolution", dev_settings['resolution'] ) self.core.call_all("config_save") return self._get_scanner() def cmd_run(self, args): if args.command != 'scanner': return None if args.subcommand == 'list': self._list_scanners() self._print_scanners() return self.out elif args.subcommand == 'set': return self._set_scanner(args) elif args.subcommand == 'get': return self._get_scanner() assert() paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/search.py000066400000000000000000000073261417573700700252410ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import logging import shutil import openpaperwork_core from .. import _ LOGGER = logging.getLogger(__name__) class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.interactive = False def get_interfaces(self): return ['shell'] def get_deps(self): return [ # if there are no doc_renderer loaded, nothing it displayed, which # may be fine. # (see paperwork-json) # { # "interface": "doc_renderer", # "defaults": [], # }, { "interface": "document_storage", "defaults": ['paperwork_backend.model.workdir'], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, { "interface": "index", "defaults": ['paperwork_backend.index.shell'], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): p = parser.add_parser( 'search', help=_("Search keywords in documents") ) p.add_argument( '--limit', '-l', type=int, default=50, help=_("Maximum number of results (default: 50)") ) p.add_argument( 'keywords', nargs='*', default=[], help=_("Search keywords (none means all documents)") ) def cmd_run(self, args): if args.command != 'search': return None keywords = " ".join(args.keywords) docs = [] self.core.call_all("index_search", docs, keywords, args.limit) docs.sort(reverse=True) if self.interactive: renderers = [] self.core.call_all("doc_renderer_get", renderers) renderer = renderers[-1] else: renderer = None if self.interactive: for (doc_id, doc_url) in docs: header = _("Document id: %s") % doc_id self.core.call_all("print", header + "\n") doc_date = self.core.call_success("doc_get_date_by_id", doc_id) doc_date = self.core.call_success("i18n_date_short", doc_date) header = _("Document date: %s") % doc_date self.core.call_all("print", header + "\n") if renderer is None: continue if doc_url is None: LOGGER.warning("Failed to get URL of document %s", doc_id) continue lines = renderer.get_preview_output( doc_id, doc_url, shutil.get_terminal_size() ) for line in lines: self.core.call_all("print", line + "\n") self.core.call_all("print", "\n") self.core.call_all("print_flush") return [doc[0] for doc in docs] paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/show.py000066400000000000000000000077521417573700700247570ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import shutil import openpaperwork_core from . import util from .. import _ class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.interactive = False def get_interfaces(self): return ['shell'] def get_deps(self): # if there are no doc_renderer loaded, nothing is displayed, which # may be fine. # (see paperwork-json) return [ { 'interface': 'document_storage', 'defaults': ['paperwork_backend.model.workdir'], }, { 'interface': 'i18n', 'defaults': ['openpaperwork_core.i18n.python'], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): p = parser.add_parser('show', help=_( "Show the content of a document" )) p.add_argument('doc_id') p.add_argument( '--pages', '-p', required=False, help="Pages to show: 1,4 or 1-10 (default: all)" ) def cmd_run(self, args): if args.command != 'show': return None doc_id = args.doc_id doc_url = self.core.call_success("doc_id_to_url", doc_id) if doc_url is None: return False nb_pages = self.core.call_success("doc_get_nb_pages_by_url", doc_url) if nb_pages is None or nb_pages <= 0: return False pages = util.parse_page_list(args) if pages is None: pages = range(0, nb_pages) renderers = [] self.core.call_all("doc_renderer_get", renderers) assert(len(renderers) > 0) renderer = renderers[-1] if self.interactive: header = _("Document id: %s") % doc_id self.core.call_all("print", header + "\n") self.core.call_all("print", "=" * len(header) + "\n") doc_date = self.core.call_success("doc_get_date_by_id", doc_id) doc_date = self.core.call_success("i18n_date_short", doc_date) header = _("Document date: %s") % doc_date self.core.call_all("print", header + "\n") lines = renderer.get_doc_output( doc_id, doc_url, shutil.get_terminal_size() ) for line in lines: self.core.call_all("print", line + "\n") self.core.call_all("print", "\n") for page_nb in pages: self.core.call_all("print", "\n") header = _("Page %d") % (page_nb + 1) self.core.call_all("print", header + "\n") self.core.call_all("print", ("-" * len(header)) + "\n\n") lines = renderer.get_page_output( doc_id, doc_url, page_nb, shutil.get_terminal_size() ) for line in lines: self.core.call_all("print", line + "\n") self.core.call_all("print", "\n") self.core.call_all("print_flush") return { 'document': renderer.get_doc_infos(doc_id, doc_url), 'pages': { page_nb: renderer.get_page_infos(doc_id, doc_url, page_nb) for page_nb in pages }, } paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/sync.py000066400000000000000000000055511417573700700247460ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import collections import openpaperwork_core from .. import _ class Plugin(openpaperwork_core.PluginBase): def __init__(self): super().__init__() self.interactive = True self.changes = collections.defaultdict( # we cannot use sets here because sets are not JSON-serializable lambda: collections.defaultdict(list) ) def get_interfaces(self): return ['shell'] def get_deps(self): return [ { "interface": "syncable", "defaults": [ "paperwork_backend.guesswork.label.sklearn", "paperwork_backend.guesswork.ocr.pyocr", "paperwork_backend.index.whoosh", "paperwork_backend.model.labels", ], }, { 'interface': 'transaction_manager', 'defaults': ['paperwork_backend.sync'], }, ] def cmd_set_interactive(self, interactive): self.interactive = interactive def cmd_complete_argparse(self, parser): parser.add_parser('sync', help=_( "Synchronize the index(es) with the content of the work directory" )) def on_sync(self, name, status, key): self.changes[name][status].append(key) def cmd_run(self, args): if args.command != 'sync': return None if self.interactive: print(_("Synchronizing with work directory ...")) self.changes = collections.defaultdict( # we cannot use sets here because sets are not JSON-serializable lambda: collections.defaultdict(list) ) self.core.call_all("transaction_sync_all") self.core.call_all("mainloop_quit_graceful") self.core.call_one("mainloop") if self.interactive: print(_("All done !")) # ensure order of documents to make testing easier and ensure # behaviour consistency for actions in self.changes.values(): for docs in actions.values(): docs.sort() return dict(self.changes) paperwork-2.1.1/paperwork-shell/src/paperwork_shell/cmd/util.py000066400000000000000000000006521417573700700247440ustar00rootroot00000000000000def parse_page_list(args): if not hasattr(args, 'pages'): return None if args.pages is None or args.pages == "": return None if "-" in args.pages: pages = args.pages.split("-", 1) return range( int(pages[0]) - 1, int(pages[1]) ) else: return [ (int(p) - 1) for p in args.pages.split(",") if int(p) >= 1 ] paperwork-2.1.1/paperwork-shell/src/paperwork_shell/display/000077500000000000000000000000001417573700700243145ustar00rootroot00000000000000paperwork-2.1.1/paperwork-shell/src/paperwork_shell/display/__init__.py000066400000000000000000000000101417573700700264140ustar00rootroot00000000000000# empty paperwork-2.1.1/paperwork-shell/src/paperwork_shell/display/docrendering/000077500000000000000000000000001417573700700267575ustar00rootroot00000000000000paperwork-2.1.1/paperwork-shell/src/paperwork_shell/display/docrendering/__init__.py000066400000000000000000000000001417573700700310560ustar00rootroot00000000000000paperwork-2.1.1/paperwork-shell/src/paperwork_shell/display/docrendering/extra_text.py000066400000000000000000000041051417573700700315200ustar00rootroot00000000000000import openpaperwork_core from ... import _ class ExtraTextRenderer(object): def __init__(self, core): self.core = core self.parent = None def get_preview_output( self, doc_id, doc_url, terminal_size=(80, 25), page_idx=0 ): if self.parent is not None: return self.parent.get_preview_output( doc_id, doc_url, terminal_size, page_idx ) return [] def get_doc_output(self, doc_id, doc_url, terminal_size=(80, 25)): out = [] if self.parent is not None: out = self.parent.get_doc_output( doc_id, doc_url, terminal_size ) extra_text = [] self.core.call_all("doc_get_extra_text_by_url", extra_text, doc_url) extra_text = "\n".join(extra_text).strip().split("\n") if len(extra_text) > 0: extra_text = ["", " " + _("Additional text:")] + extra_text return extra_text + out def get_page_output( self, doc_id, doc_url, page_nb, terminal_size=(80, 25) ): if self.parent is not None: return self.parent.get_page_output( doc_id, doc_url, page_nb, terminal_size ) return [] def get_doc_infos(self, doc_id, doc_url): if self.parent is not None: return self.parent.get_doc_infos(doc_id, doc_url) return {} def get_page_infos(self, doc_id, doc_url, page_nb): if self.parent is not None: return self.parent.get_page_infos(doc_id, doc_url, page_nb) return {} class Plugin(openpaperwork_core.PluginBase): PRIORITY = 11000 def get_interfaces(self): return ['doc_renderer'] def get_deps(self): return [ { 'interface': 'extra_text', 'defaults': ['paperwork_backend.model.extra_text'], }, ] def doc_renderer_get(self, out): r = ExtraTextRenderer(self.core) if len(out) > 0: r.parent = out[-1] out.append(r) paperwork-2.1.1/paperwork-shell/src/paperwork_shell/display/docrendering/img.py000066400000000000000000000103461417573700700301110ustar00rootroot00000000000000import os import tempfile try: import fabulous.image # XXX(Jflesch): crappy workaround for an unmaintained library ... fabulous.image.basestring = str FABULOUS_AVAILABLE = True except (ValueError, ImportError): FABULOUS_AVAILABLE = False import openpaperwork_core class FabulousRenderer(object): def __init__(self, plugin): self.plugin = plugin self.core = plugin.core self.parent = None def get_preview_output( self, doc_id, doc_url, terminal_size=(80, 25), page_idx=0 ): w_split = int(terminal_size[0] / 3) parent = [] if self.parent is not None: parent = self.parent.get_preview_output( doc_id, doc_url, (terminal_size[0] - w_split - 2, terminal_size[1]), page_idx ) thumbnail = self.core.call_success( "thumbnail_get_page", doc_url, page_idx ) if thumbnail is None: thumbnail = [] else: thumbnail = self.plugin.img_render(thumbnail, w_split) if len(parent) < len(thumbnail): parent.extend([""] * (len(thumbnail) - len(parent))) elif len(parent) > len(thumbnail): parent = parent[:len(thumbnail)] out = [ (i + " " + t) for (i, t) in zip(thumbnail, parent) ] return out def get_doc_output(self, doc_id, doc_url, terminal_size=(80, 25)): out = [] if self.parent is not None: out = self.parent.get_doc_output( doc_id, doc_url, terminal_size ) return out def get_page_output( self, doc_id, doc_url, page_nb, terminal_size=(80, 25) ): parent_out = [] if self.parent is not None: parent_out = self.parent.get_page_output( doc_id, doc_url, page_nb, terminal_size ) img_url = self.core.call_success("page_get_img_url", doc_url, page_nb) img = self.core.call_success("url_to_pillow", img_url) img = self.plugin.img_render( img, terminal_width=(terminal_size[0] - 1) ) return [img_url] + list(img) + [""] + parent_out def get_doc_infos(self, doc_id, doc_url): out = {} if self.parent is not None: out = self.parent.get_doc_infos(doc_id, doc_url) return out def get_page_infos(self, doc_id, doc_url, page_nb): out = {} if self.parent is not None: out = self.parent.get_page_infos(doc_id, doc_url, page_nb) out['image'] = self.core.call_success( "page_get_img_url", doc_url, page_nb ) return out class Plugin(openpaperwork_core.PluginBase): PRIORITY = 10000 def get_interfaces(self): return [ 'doc_renderer', 'img_renderer', ] def get_deps(self): return [ { 'interface': 'page_img', 'defaults': [ 'paperwork_backend.model.img', 'paperwork_backend.model.pdf', ], }, { 'interface': 'pillow', 'defaults': [ 'openpaperwork_core.pillow.img', 'paperwork_backend.pillow.pdf', ], }, { 'interface': 'thumbnail', 'defaults': ['paperwork_backend.model.thumbnail'], }, ] def doc_renderer_get(self, out): if not FABULOUS_AVAILABLE: return r = FabulousRenderer(self) if len(out) > 0: r.parent = out[-1] out.append(r) def img_render(self, img, terminal_width=80): if not FABULOUS_AVAILABLE: return with tempfile.NamedTemporaryFile( prefix='paperwork-shell', suffix='.jpeg', delete=False ) as fd: img.save(fd, format="JPEG") img_file = fd.name try: img = fabulous.image.Image(img_file, width=terminal_width) img = img.reduce(img.convert()) finally: os.unlink(img_file) return list(img) paperwork-2.1.1/paperwork-shell/src/paperwork_shell/display/docrendering/labels.py000066400000000000000000000105341417573700700305760ustar00rootroot00000000000000try: import fabulous.color FABULOUS_AVAILABLE = True except (ValueError, ImportError): FABULOUS_AVAILABLE = False import openpaperwork_core def color_labels(core, labels): labels = [ (label, core.call_success("label_color_to_rgb", color)) for (label, color) in labels ] labels = [ (label, ( int(color[0] * 0xFF), int(color[1] * 0xFF), int(color[2] * 0xFF) )) for (label, color) in labels ] for (label, bg_color) in labels: fg_color = core.call_success( "label_get_foreground_color", ( bg_color[0] / 0xFF, bg_color[1] / 0xFF, bg_color[2] / 0xFF, ) ) fg_color = (fg_color[0] * 0xFF, fg_color[1] * 0xFF, fg_color[2] * 0xFF) l_label = len(label) label = fabulous.color.fg256(fg_color, label) label = fabulous.color.bg256(bg_color, label) yield (l_label, str(label)) class LabelsRenderer(object): def __init__(self, core): self.core = core self.parent = None def _get_labels(self, doc_url): labels = set() self.core.call_all("doc_get_labels_by_url", labels, doc_url) return labels def _rearrange_labels(self, labels, terminal_width): out = [] line = "" for (len_label, label) in labels: if len(line) + len_label + 1 >= terminal_width: out.append(line) line = "" line += " " + label if len(line.strip()) > 0: out.append(line) return [line.strip() for line in out] def get_preview_output( self, doc_id, doc_url, terminal_size=(80, 25), page_idx=0 ): out = [] if self.parent is not None: out = self.parent.get_preview_output( doc_id, doc_url, terminal_size, page_idx ) if not FABULOUS_AVAILABLE: return out if page_idx != 0: return out labels = self._get_labels(doc_url) labels = color_labels(self.core, labels) labels = self._rearrange_labels(labels, terminal_size[0]) return labels + out def get_doc_output(self, doc_id, doc_url, terminal_size=(80, 25)): out = [] if self.parent is not None: out = self.parent.get_doc_output( doc_id, doc_url, terminal_size ) if not FABULOUS_AVAILABLE: return out labels = self._get_labels(doc_url) labels = color_labels(self.core, labels) labels = self._rearrange_labels(labels, terminal_size[0]) return labels + out def get_page_output( self, doc_id, doc_url, page_nb, terminal_size=(80, 25) ): if self.parent is not None: return self.parent.get_page_output( doc_id, doc_url, page_nb, terminal_size ) return [] def get_doc_infos(self, doc_id, doc_url): out = {} if self.parent is not None: out = self.parent.get_doc_infos(doc_id, doc_url) out['labels'] = [] for (label, color) in self._get_labels(doc_url): out['labels'].append( { 'label': label, 'color': color, } ) return out def get_page_infos(self, doc_id, doc_url, page_nb): if self.parent is not None: return self.parent.get_page_infos(doc_id, doc_url, page_nb) return {} class Plugin(openpaperwork_core.PluginBase): PRIORITY = 10000 def get_interfaces(self): return ['doc_renderer'] def get_deps(self): return [ { 'interface': 'page_boxes', 'defaults': ['paperwork_backend.model.labels'], }, ] def print_labels(self, labels, separator='\n'): if FABULOUS_AVAILABLE: labels = color_labels(self.core, labels) labels = [label for (l_label, label) in labels] else: labels = [label for (label, color) in labels] labels = separator.join(labels) print(labels) def doc_renderer_get(self, out): r = LabelsRenderer(self.core) if len(out) > 0: r.parent = out[-1] out.append(r) paperwork-2.1.1/paperwork-shell/src/paperwork_shell/display/docrendering/text.py000066400000000000000000000065311417573700700303220ustar00rootroot00000000000000import openpaperwork_core PREVIEW_MAX_LINES = 25 class TextRenderer(object): def __init__(self, core): self.core = core self.parent = None def _get_page_text(self, doc_url, page_nb): out = [] line_boxes = self.core.call_success( "page_get_boxes_by_url", doc_url, page_nb ) if line_boxes is None: return out for line_box in line_boxes: line = line_box.content if line == "": continue out.append(line) return out def _rearrange_lines(self, lines, terminal_width): out = [] for line in lines: new_line = "" iwords = line.split(" ") words = [] for word in iwords: for p in range(0, len(word), terminal_width - 1): words.append(word[p:p + terminal_width - 1]) for word in words: if len(new_line) + len(word) + 1 >= terminal_width: out.append(new_line) new_line = "" new_line += " " + word if len(new_line.strip()) > 0: out.append(new_line) return [line.strip() for line in out] def get_preview_output( self, doc_id, doc_url, terminal_size=(80, 25), page_idx=0 ): if self.parent is None: out = [] else: out = self.parent.get_preview_output( doc_id, doc_url, terminal_size, page_idx ) text = self._get_page_text(doc_url, page_idx) text = self._rearrange_lines(text, terminal_size[0]) text = text[:PREVIEW_MAX_LINES] return out + text def get_doc_output(self, doc_id, doc_url, terminal_size=(80, 25)): if self.parent is None: out = [] else: out = self.parent.get_doc_output(doc_id, doc_url, terminal_size) return out def get_page_output( self, doc_id, doc_url, page_nb, terminal_size=(80, 25) ): if self.parent is None: out = [] else: out = self.parent.get_page_output(doc_id, doc_url, terminal_size) text = self._get_page_text(doc_url, page_nb) text = self._rearrange_lines(text, terminal_size[0]) return out + text def get_doc_infos(self, doc_id, doc_url): out = {} if self.parent is not None: out = self.parent.get_doc_infos(doc_id, doc_url) out["doc_id"] = doc_id return out def get_page_infos(self, doc_id, doc_url, page_nb): out = {} if self.parent is not None: out = self.parent.get_page_infos(doc_id, doc_url, page_nb) out["text"] = self._get_page_text(doc_url, page_nb) return out class Plugin(openpaperwork_core.PluginBase): PRIORITY = 10000 def get_interfaces(self): return ['doc_renderer'] def get_deps(self): return [ { 'interface': 'page_boxes', 'defaults': [ 'paperwork_backend.model.hocr', 'paperwork_backend.model.pdf', ], }, ] def doc_renderer_get(self, out): r = TextRenderer(self.core) if len(out) > 0: r.parent = out[-1] out.append(r) paperwork-2.1.1/paperwork-shell/src/paperwork_shell/display/progress.py000066400000000000000000000064621417573700700265420ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import collections import logging import shutil import sys import threading import time import openpaperwork_core from .. import _ LOGGER = logging.getLogger(__name__) TIME_BETWEEN_PROGRESS = 0.3 def print_progress(upd_type, progress, description=None): eol = "\r" if description is None: description = "" if progress >= 1.0: if description is None: description = _("Done") eol = "\n" str_progress = ( "=" * int(progress * 20) + " " * (20 - int(progress * 20)) ) line = '%3d%% [%s] ' % (progress * 100, str_progress[:20]) line += '[%-20s] %s' % (upd_type[:20], description) term_width = shutil.get_terminal_size((500, 25)).columns line = line[:term_width - 1] sys.stdout.write("\033[K" + line + eol) class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.progresses = collections.OrderedDict() self.thread = None self.lock = threading.RLock() self.interactive = False self.enabled = True def get_interfaces(self): return [ 'progress_listener', ] def _thread(self): while True: time.sleep(TIME_BETWEEN_PROGRESS) with self.lock: if len(self.progresses) <= 0: self.thread = None return if not self.enabled: continue upd_type = next(iter(self.progresses)) (progress, description) = self.progresses[upd_type] print_progress(upd_type, progress, description) def cmd_set_interactive(self, interactive): self.interactive = interactive def shell_show_progress(self, enabled): self.enabled = enabled def on_progress(self, upd_type, progress, description=None): if not self.interactive: return with self.lock: if progress > 1.0: LOGGER.warning( "Invalid progression (%f) for [%s]", progress, upd_type ) progress = 1.0 if progress >= 1.0: if upd_type not in self.progresses: return self.progresses.pop(upd_type) print_progress(upd_type, progress) else: self.progresses[upd_type] = (progress, description) if self.thread is None: self.thread = threading.Thread(target=self._thread) self.thread.daemon = True self.thread.start() paperwork-2.1.1/paperwork-shell/src/paperwork_shell/display/scan.py000066400000000000000000000144331417573700700256170ustar00rootroot00000000000000# Paperwork - Using OCR to grep dead trees the easy way # Copyright (C) 2012-2019 Jerome Flesch # # Paperwork is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Paperwork 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 Paperwork. If not, see . import shutil import PIL import PIL.Image import openpaperwork_core from .. import _ class AppendableImage(object): def __init__(self, width, expected_height): # When scanning, width is reliable. # However height is not reliable, and truncating an image is much # easier then increasing its size. --> we allocate a big image and will # truncate it when needed self.img = PIL.Image.new( "RGB", (width, 10 * expected_height) ) self.width = width self.height = 0 def append(self, chunk): assert(self.img.size[0] == chunk.size[0]) self.img.paste(chunk, (0, self.height)) self.height += chunk.size[1] def get_image(self, start_line=0, end_line=None): if end_line is None: end_line = self.height return self.img.crop((0, start_line, self.img.size[0], end_line)) class Plugin(openpaperwork_core.PluginBase): def __init__(self): self.img = None self.last_line_displayed = 0 # in pixels self.txt_line_height = 0 # in pixels self.terminal_size = (80, 25) self.doc_id = None self.doc_url = None self.doc_renderer = None self.page_hashes = {} def get_deps(self): return [ # if there are no doc_renderer loaded, nothing it displayed, which # may be fine. # # (see paperwork-json) # { # 'interface': 'doc_renderer', # 'defaults': [], # }, { 'interface': 'img_renderer', 'defaults': ['paperwork_shell.display.docrendering.img'], }, { 'interface': 'scan', 'defaults': ['paperwork_backend.docscan.libinsane'], }, ] def on_scan_feed_start(self, scan_id): self.core.call_all("shell_show_progress", False) def on_scan_page_start(self, scan_id, page_nb, scan_params): print() print( _("Scanning page {} (expected size: {}x{}) ...").format( page_nb + 1, scan_params.get_width(), scan_params.get_height() ) ) self.img = AppendableImage( scan_params.get_width(), scan_params.get_height() ) self.last_line_displayed = 0 # We need to figure out how many pixels one line of character will # represent. self.terminal_size = ( # leave one character at the end of each line to make sure the # terminal doesn't add line return shutil.get_terminal_size()[0] - 1, shutil.get_terminal_size()[1] ) ratio = scan_params.get_width() / self.terminal_size[0] # When Fabulous resizes an image to turn it into terminal characters, # it assumes that each characters is 1 pixel wide and 2 pixels high. self.txt_line_height = int(2 * ratio) + 1 def on_scan_chunk(self, scan_id, scan_params, img_chunk): self.img.append(img_chunk) current_usable_line = ( self.img.height - (self.img.height % self.txt_line_height) ) if current_usable_line > self.last_line_displayed: img = self.img.get_image( self.last_line_displayed, current_usable_line ) img = self.core.call_success( "img_render", img, terminal_width=self.terminal_size[0] ) for line in img: print(line) self.last_line_displayed = current_usable_line def on_scan_page_end(self, scan_id, page_nb, img): img_size = img.size print( _("Page {} scanned (actual size: {}x{})").format( page_nb + 1, img_size[0], img_size[1] ) ) def on_scan_feed_end(self, scan_id): self.core.call_all("shell_show_progress", True) print() print(_("End of paper feed")) def on_scan2doc_start(self, scan_id, doc_id, doc_url): self.doc_id = doc_id self.doc_url = doc_url renderers = [] self.core.call_all("doc_renderer_get", renderers) self.doc_renderer = renderers[-1] self.page_hashes = {} def _compute_page_hash(self, doc_url, page_idx): return self.core.call_success( "page_get_hash_by_url", doc_url, page_idx ) def on_scan2doc_page_scanned(self, scan_id, doc_id, doc_url, page_idx): print(_("Page {} in document {} created").format( page_idx, doc_id )) self.page_hashes[page_idx] = self._compute_page_hash( self.doc_url, page_idx ) def on_scan2doc_end(self, scan_id, doc_id, doc_url): self._show_last_page() self.doc_id = None self.doc_url = None self.doc_renderer = None def _show_last_page(self): if self.doc_renderer is None: return for page_idx in self.page_hashes: # we only want to display the page if something has actually # changed page_hash = self._compute_page_hash(self.doc_url, page_idx) if self.page_hashes[page_idx] == page_hash: continue self.page_hashes[page_idx] = page_hash lines = self.doc_renderer.get_preview_output( self.doc_id, self.doc_url, self.terminal_size, page_idx ) print() for line in lines: print(line) print() def on_progress(self, upd_type, progress, description=None): self._show_last_page() paperwork-2.1.1/paperwork-shell/src/paperwork_shell/l10n/000077500000000000000000000000001417573700700234215ustar00rootroot00000000000000paperwork-2.1.1/paperwork-shell/src/paperwork_shell/l10n/__init__.py000066400000000000000000000007311417573700700255330ustar00rootroot00000000000000import openpaperwork_core class Plugin(openpaperwork_core.PluginBase): def get_interfaces(self): return ['l10n_init'] def get_deps(self): return [ { 'interface': 'l10n', 'defaults': ['openpaperwork_core.l10n.python'], }, ] def init(self, core): super().init(core) self.core.call_all( "l10n_load", "paperwork_shell.l10n", "paperwork_shell" ) paperwork-2.1.1/paperwork-shell/src/paperwork_shell/main.py000066400000000000000000000067651417573700700241630ustar00rootroot00000000000000import argparse import json import sys import traceback import openpaperwork_core import paperwork_backend # this import must be non-relative due to cx_freeze running this .py # as an independant Python script from paperwork_shell import _ DEFAULT_SHELL_PLUGINS = paperwork_backend.DEFAULT_PLUGINS + [ 'paperwork_backend.guesswork.cropping.libpillowfight', 'paperwork_shell.cmd.chkworkdir', 'paperwork_shell.cmd.delete', 'paperwork_shell.cmd.edit', 'paperwork_shell.cmd.export', 'paperwork_shell.cmd.extra_text', 'paperwork_shell.cmd.import', 'paperwork_shell.cmd.label', 'paperwork_shell.cmd.move', 'paperwork_shell.cmd.ocr', 'paperwork_shell.cmd.rename', 'paperwork_shell.cmd.reset', 'paperwork_shell.cmd.scan', 'paperwork_shell.cmd.scanner', 'paperwork_shell.cmd.search', 'paperwork_shell.cmd.show', 'paperwork_shell.cmd.sync', 'paperwork_shell.display.docrendering.extra_text', 'paperwork_shell.display.docrendering.img', 'paperwork_shell.display.docrendering.labels', 'paperwork_shell.display.docrendering.text', 'paperwork_shell.l10n', ] DEFAULT_CLI_PLUGINS = DEFAULT_SHELL_PLUGINS + [ "paperwork_shell.cmd.about", 'paperwork_shell.display.progress', "paperwork_shell.display.scan", ] DEFAULT_JSON_PLUGINS = DEFAULT_SHELL_PLUGINS def main_main(in_args, application_name, default_plugins, interactive): # To load the plugins, we need first to load the configuration plugin # to get the list of plugins to load. # The configuration plugin may write traces using logging, so we better # enable and configure the plugin logs.print first. core = openpaperwork_core.Core() # plugin 'uncaught_exceptions' requires a mainloop plugin core.load('openpaperwork_core.mainloop.asyncio') for module_name in paperwork_backend.DEFAULT_CONFIG_PLUGINS: core.load(module_name) core.init() core.call_all("init_logs", application_name, "warning") core.call_all("config_load") core.call_all("config_load_plugins", application_name, default_plugins) parser = argparse.ArgumentParser() cmd_parser = parser.add_subparsers( help=_('command'), dest='command', required=True ) core.call_all("cmd_complete_argparse", cmd_parser) args = parser.parse_args(in_args) core.call_all("cmd_set_interactive", interactive) if interactive: r = core.call_all("cmd_run", args) else: r = core.call_success("cmd_run", args) core.call_all("on_quit") return r def json_main(): try: r = main_main( sys.argv[1:], 'paperwork-json', DEFAULT_JSON_PLUGINS, interactive=False ) print(json.dumps( r, indent=4, separators=(',', ': '), sort_keys=True )) except Exception as exc: stack = traceback.format_exc().splitlines() print(json.dumps( { "status": "error", "exception": str(type(exc)), "args": str(exc.args), "reason": str(exc), "stack": stack, }, indent=4, separators=(',', ': '), sort_keys=True )) sys.exit(2) def cli_main(): main_main( sys.argv[1:], 'paperwork-cli', DEFAULT_CLI_PLUGINS, interactive=True ) if __name__ == "__main__": # Do not remove. Cx_freeze goes throught here if "paperwork-json" in sys.argv[0]: json_main() else: cli_main() paperwork-2.1.1/paperwork-shell/tests/000077500000000000000000000000001417573700700200215ustar00rootroot00000000000000paperwork-2.1.1/paperwork-shell/tests/__init__.py000066400000000000000000000000001417573700700221200ustar00rootroot00000000000000paperwork-2.1.1/paperwork-shell/tests/cmd/000077500000000000000000000000001417573700700205645ustar00rootroot00000000000000paperwork-2.1.1/paperwork-shell/tests/cmd/__init__.py000066400000000000000000000000001417573700700226630ustar00rootroot00000000000000paperwork-2.1.1/paperwork-shell/tests/cmd/test_doc.pdf000066400000000000000000000230121417573700700230610ustar00rootroot00000000000000%PDF-1.5 %äüöß 2 0 obj <> stream xm 0EY 3I2`kpWw>vbQ3%K<%*&ߒ`Ցr}Ji|xD@ !Pέ&UX\|d=pWb}q/#AQ,J%a]V>DhHN )nH&;z7ND endstream endobj 3 0 obj 181 endobj 5 0 obj <> stream xzkxU=UzXl=,$+JQCI+~۴B,ْ`[BciZW] @~=DN3(y\$Db_/7+]!|S,eWɗ^!=r|I/!2AnA[%z L _Q'T1"Qay9</(܆2J L/7-+6r&AI<^ z 3?6KF9 __l)r辌cSv9B¿j@BMjodcگ}ΫvvZ[wM۷mݲq kUk׬.[_:fH_+j*Rɋi2юJ1ZUFD>ʋRwtP?*^\"tDsx s yN0[V ?/~üxWSX6JoFg"h# [-ªJ2[GP֟ہڶͳ )(őEcb*wVJ"-TiT%?&Ns35H!ElegضDsXoo#6Bڹ'O.AT ᣥєH$PdZD.W}=3L7gf dQمq{¢)2 Ѓ{:ŒkE,ȏFM~&Ϝ }[9aOrÑĆ8/y2zD$\b(9J^d+DԎ )$`~tPK|?GyQKcC8\U"gɹL1XZ\͎e92?S왑-))Mf] ǵ7ᔦzfV,];ezZZ)QPe7PXr " u:X] g/# Bisu fvjJ=,w%7p8F kHu]M~(j=ys:^PkkcX}puee*`pX,W;lf kf L 9 h#PaA:Kc@v>i9)@2J%`ۡ Wyٰ`no<ݩl$5':~Ugǁ|΁{[j7pVOc-;X}WYJR:ŘUp]XSX\-aYCeBaVo9m1hr@!)K:FKcmCjL lu6يqh+x=ۭ5o;xGiK}B1ld9*uK lF$h׉v:   [Luwy@ x |?W;G!+>;mv5nJS|[).!"ki HΚa66Y'N8NH:4W,DL7c2Z'fl'odMO^pO_e]y))pn)ggbJwRV;$,@YL"sŤNm6kt:FV;hTr O8'L;!ㄘTN^':u'䄼9'j3TC%B 7,%Y.4N|/)k)uSϿ)_m^f|ϽS<ƉlqaN'. X^Rr\NW(k5fKPXTnݐqC nnx /H\$#X)u5{ 7[UVQλu7pôn6MoIQ6ZV|Il0_ɁQ(4^AZvih~OV4+Yϗz-/|r k7y\H: Y5:^=<p2zazP1(1=+z!Aszp6Mz zHȷmReI^YqB)R$z ] C-h4"Vl iRj԰ЁCmmA,ZmzxPmUs61BV=6kbŸgJb\ǁ9xqp9 8XM.sOs|Gp&?Qp 69XŁ2⋵3Fa`q([T l<`/jiuP!GS y띱*5oS^1uQiCC]gg%XهVnʬՂ`4f,c5BaQk(r6@^q'M:\̹2FkVjUv<0UlvEխ1L\ ,km췑`:n <0`sM͂iO5=<Pyx じz=z򀋒a;X|)/Du76R٘? (!zN`_gpIqZhnbw=R](\ip'aɈQ31zL<;ɧA(8y3Wv+B)V(جt`wnɔN:{o]nx0o7Q7G[wfSdFX0JXHeѻ%fC1Vl$0KV`=p 4x?=mrAZ!^xx>Ni)JXh\?Q~̰38Oy`{r \?oǙlL<.|c==RmyAXe(#u>ݻ<`+ViZ8-bX,e^5WG6ӓɕ5dr"%]wc}΍G"pa`;"lJľClk>܏o>y*y'>DD&AlHX-f 5Ψ0c快Zӹ!stHЯMR+}Ŵ\˟přE=`J%=9ŨW#]͞{TBWŔ{Oq5b7[j=c(l(0qV֊#.8Lp@p,=o ʹX陕ݽ߹ƊL}㍫f?=Ȏ 5̍ƭd/ژ b58XMHlb̒4 7LEi$2GIddf~H_DDPjld4ï*kkxoIT43.lYVAL%srz`\{⩱=hjGz(>*~95TZjVT7\.gKQ> |bx!|*>2S{P4ߛSP<"s"3^?KƆ,rGO&~ _dds4}ect%ptlh?Mxzld2- Y3ь4x&56?1H Xf;Dtղ)at*?6L%PCx|;Ƣcc6MEc趱4:OF'ڦRd-F+htb,qO1G4@|Di<ÉˌV-|81ApԄ'ts&g\t(@Zrxflq>V$+ Z";J(gJKY6Hb_FL>(VJ23M>xj}OQu9HP!ď2&+r_L:H2 J&)+4n4W"Ͱ4Ϛ!eT+6r]I#emܥZ4s5h]/J4Hf[lr6a;CIS_V1 {Ehz-Ц/fu'0BJ"d7 >Aoinmd!~;"~ .^|6݅x9j#Үv%JOmB nWAķMi6IҏyTi8???g0ɱO?\,>}EくC`p₦o{#{{W,5s5sɹ]+鱟2y{l)0>} =y9vǽG>~5^|90w"sy]dpiv\2Ӌw.xߋ7{݋wv ؁}*#wNyNvcw0O8wIʽ d:ѧc4 .,[ dvowo{ҧz&MXmۍ#tb.>vU؝roG&h5 B<_pEg31#}ゑ14f5c=g {:łе"-HO{[$}{NGIS#pC@iLY4L)| DOX4qJS!6B!Bޟ&C"VBtZQGg endstream endobj 6 0 obj 7404 endobj 7 0 obj <> endobj 8 0 obj <> stream x]Mn0t DBH)I$Qi"cV陰ΕՍYtn d,t浢U B[/ Ce1σ͟M[ipGY[YDAQ fl}˟} b%[Q6 \cnQTz-0YlJ/}Rxg;=q!kh΁GOqBGd=D]+<ˈ}2OwΚƄPw|ؔ9ho; endstream endobj 9 0 obj <> endobj 10 0 obj <> endobj 11 0 obj <> endobj 1 0 obj <>/Contents 2 0 R>> endobj 4 0 obj <> endobj 12 0 obj <> endobj 13 0 obj < /Producer /CreationDate(D:20190902205629+02'00')>> endobj xref 0 14 0000000000 65535 f 0000008715 00000 n 0000000019 00000 n 0000000271 00000 n 0000008884 00000 n 0000000291 00000 n 0000007780 00000 n 0000007801 00000 n 0000007996 00000 n 0000008384 00000 n 0000008628 00000 n 0000008660 00000 n 0000008983 00000 n 0000009080 00000 n trailer < ] /DocChecksum /72B44A81E5EEF3D98178B73CE31BA672 >> startxref 9255 %%EOF paperwork-2.1.1/paperwork-shell/tests/cmd/test_img.jpeg000066400000000000000000000142231417573700700232500ustar00rootroot00000000000000JFIFvvCreated with GIMPC        d > !"1A2 #Q8qu$6B%9Rart?JR)JR)JR)QLq_%?ϗ#_._ )3O>CBJ+m w}%}~F=@M>0 ]ET_|W~I -GQ=3Ȫ|71وnMl Mn [ eK#`[XJVZ㍅t݈b5ƉG.֕ 7|ڶ-4(x񦡷!j A5Nc $DC]*ș˾"BI/A뿏}5ؚ:YaK :z~}?e:v]oC8 ~;''6A1>?_{KoA})JR\H{iTb-[c`k>_7io.WYgM T@A/ Ow$*Ó c8 wuu9#ț?%\DaIfL% twN{ L@vx 7]~^r3N&Iӹ#\it]0} LsF'2Cu坪ܘqqbH t3pKSzq:;ͮ8tN(/L-1& )휉v8JN Л~ =FS0) 0U+ɚü%%YX|ټڱt+&_9v!AL_u_Jq59YHߎ/l֧3c+-ٿ=^`~"Tɤk-Rc aP7V XnTx3ހSYӴeE@YS~!A.TMBXsWgx˻:Ju+NIbeGi?*Μ9H)AƯ~Oّ2%r$ kA)JR췃n72C# d]/}9Jm&rh5YVk:q֓֒$5ZEElCtPɐQd9xŶ-nKuCEnEDHS(b} #g2;"cqhE кtIFˑEʐ3Q%NT\*`|%d)bW;sh#LS}.=akz] 쏞2 s*E38 A4c&9&0ƽܢŗmmW%HfibCmC&CEGRv?Lwuúp(E,}"Shw¹:W&pׅ+y\q HlE 9YA:! U~`ŷV.|&LK* J(D@t?}jȘ·vd!\8h M$ݨΓd~:a *W{θhkB f+, =玥I|y#E[,ʡ@J"~ -كq,sm^ؙ z#G>D*nXHES855nsyͯ,Z7rۑ<0 g>NbwY`U׮0JɤRЯLvG ( 'o_l~\e ϖ,L̓pjT$ssLaq/Ɩ q1Oٿk<&2wR)JR)JR)JR)JO-jO#IL.θIMM Rc<"%."a8l\66y E3^2!E¥M'Jy=JmǹMSD6@gى#~qKT w̋AeJo; St==WTwuͅ$TE;h4HYw*(5Db~Gq6Zs{'HjE銉`) őyC9qYR[wqYkX*ɀr.aaR6R_ĕY)UPxʁ2S)LY5NE D5\]svŕ>W}ٹF b<.V,+Bm™5@?i/nJ@ˤTF= ™1CB03y?&Yv™YX0MC;)P1c({~n"HplDfMEt0x-H&0h}{K q"UC~^ Wy7nO8B}זĢG*.e{6ҧQj'̑fN:o.8cxJtJ eDPMuqmݮ\^2-͍s#3HUbGD D!|IH=۪g=l)>j5EQ\<]7}ht;oG&r 2BYS> !H~C.:Vf'0@NQ(@ bba?[ZR_H闭Zi z`B-=K`ѓj9eKL(bR4j3NƳ9.xgΥ‚ ,P1+2M{(&ټ`!T]=c9sW(IIW1g#ڟFPpeHꐆ0z}nW`>Hg[?5]q6bd+Ny,JQ( YSDȨb`zG5eds լSBT;3U.ݓ7t"wwdK 9s;0+:~X ~&E'YeU1"Mn6>ն( M@]j+Cq )o\#ضXHz:2 "hUznxH`~^,cUiUv-ĂJaps(&SJ>C`V0ȜWmwz-eRA"* o)Cj&y[ޖ"vw# ZU= s%8{umX³LO^w&[wԓ2ɼh"XLĢ>cǰf9.>^Faz\HWxL%1.NhC@([g0᭿-f)JTlq`ۆݙC8QQUA;~3 80NvܷGV//H:G)3o@\p>R"{]hS:G)D5]3֍uC s3&(~' 49DʼM:fIn pRR, pl`.eˮ%Z78be}4RY"L1tsvMeRA3p6Gq.F2f$X]Wd aYKQ!<*`(_`n{$l+$ HȻ*f ABҲ1P0L =_T ecd\mn4\vH&)@k"kb=Ɲ{z024jr1xF8(dmw)&l+^V&z%JxH C+lXMZYt Qq &P X^B-糭Cn"QA '/>eiZHlĂ?dX]y\ȱٍ؎qԽ FAy |F˔""mWmǍa[nۯ%C(QҳY3bԌ4EtЈ/FI1Jm٧2pdVIt?>jR(DۤDHB{~+)Jx_qDvOs^?C=WOHv 6U)T%yaV4= ,SϘneeS9?i a/#^lIMx^?OmQTNKxT)DHOLm`: jcsEw ]-_'vyо=6muaނǥ)U"˿e|;_QR'?~~ګ |&]yvR)JR<1f;\~ks[p?:X6-'aRNQUS)2UEE_BX8bԝr̜^]/bGVݸA{6!U-߲ "";N:|ltLdA0Dnf._Vf"=h߸Vc com1R  &kg9QRxE8H|/O?| [7NۯugaoD%[z"};ywM{n[W$Jb ǩJ[ETcq(~@6ƳV^qd h.(Q\tRC} >S+A*|psG(Y JvRiJL}I.%–1̜'کvD[VIѬ!XG;tj}əCaj<Ұ]]s敒Y?A]sC a@"8lc rk] <,AcZH% t̶d(GPrpï)JR)Ze?1V.I%*E+5 Nb&DHR1U1W q)Wpkl֟R៙wpܓ" J:?w2{@)}R6?z׻ E58wvM r,(hh Qut5QfLCrjglF/ mD::gYdDb&Ra Ҕ)JREi3d]}-|Kb jbSe0 !a&*Ø&{.:ڈvRTP:RcGEހ*eJR)JR)JR)JR)JR)JRpaperwork-2.1.1/paperwork-shell/tests/cmd/test_txt.hocr000066400000000000000000000007471417573700700233270ustar00rootroot00000000000000 OCR output

ABC def

paperwork-2.1.1/paperwork-shell/tests/cmd/tests_sync.py000066400000000000000000000105071417573700700233370ustar00rootroot00000000000000import argparse import datetime import os import shutil import tempfile import unittest import openpaperwork_core import openpaperwork_core.fs class TestSync(unittest.TestCase): def setUp(self): self.test_img = "{}/test_img.jpeg".format( os.path.dirname(os.path.abspath(__file__)) ) self.test_hocr = "{}/test_txt.hocr".format( os.path.dirname(os.path.abspath(__file__)) ) self.test_pdf = "{}/test_doc.pdf".format( os.path.dirname(os.path.abspath(__file__)) ) self.tmp_local_dir = tempfile.mkdtemp() self.tmp_work_dir = tempfile.mkdtemp() os.environ['XDG_DATA_HOME'] = self.tmp_local_dir self.core = openpaperwork_core.Core(auto_load_dependencies=True) self.core.load("openpaperwork_core.config.fake") self.core.init() def tearDown(self): self.core.call_all("tests_cleanup") shutil.rmtree(self.tmp_local_dir) shutil.rmtree(self.tmp_work_dir) def test_sync(self): config = self.core.get_by_name("openpaperwork_core.config.fake") config.settings = { "workdir": openpaperwork_core.fs.CommonFsPluginBase.fs_safe( self.tmp_work_dir ), } self.core.load("paperwork_backend.model.img") self.core.load("paperwork_backend.model.hocr") self.core.load("paperwork_backend.model.pdf") self.core.load("paperwork_shell.cmd.sync") self.core.init() parser = argparse.ArgumentParser() cmd_parser = parser.add_subparsers( help='command', dest='command', required=True ) self.core.call_all("cmd_complete_argparse", cmd_parser) args = parser.parse_args(['sync']) self.core.call_all("cmd_set_interactive", False) # start with an empty work directory r = self.core.call_success("cmd_run", args) self.assertEqual(r, {}) # add 2 documents, one PDF and one img+hocr doc_a = os.path.join(self.tmp_work_dir, "20190801_1733_23") os.mkdir(doc_a) shutil.copyfile(self.test_pdf, os.path.join(doc_a, "doc.pdf")) doc_b = os.path.join(self.tmp_work_dir, "20190830_1916_32") os.mkdir(doc_b) shutil.copyfile(self.test_img, os.path.join(doc_b, "paper.1.jpg")) shutil.copyfile(self.test_hocr, os.path.join(doc_b, "paper.1.words")) r = self.core.call_success("cmd_run", args) self.assertEqual(r, { 'whoosh': { 'added': ['20190801_1733_23', '20190830_1916_32'], }, 'ocr': { 'added': ['20190801_1733_23', '20190830_1916_32'], }, 'label_guesser': { 'added': ['20190801_1733_23', '20190830_1916_32'], }, 'doc_tracker': { 'added': ['20190801_1733_23', '20190830_1916_32'], }, }) # modify one document class FakeModule(object): class Plugin(openpaperwork_core.PluginBase): PRIORITY = 9999999999 def fs_get_mtime(s, file_url): if file_url.endswith(".words"): dt = datetime.datetime(year=2038, month=1, day=1) return dt.timestamp() return None self.core._load_module("fake_module", FakeModule()) self.core.init() r = self.core.call_success("cmd_run", args) self.assertEqual(r, { 'whoosh': { 'updated': ['20190830_1916_32'], }, 'ocr': { 'updated': ['20190830_1916_32'], }, 'label_guesser': { 'updated': ['20190830_1916_32'], }, 'doc_tracker': { 'updated': ['20190830_1916_32'], }, }) # delete one document shutil.rmtree(doc_a) r = self.core.call_success("cmd_run", args) self.assertEqual(r, { 'whoosh': { 'deleted': ['20190801_1733_23'], }, 'ocr': { 'deleted': ['20190801_1733_23'], }, 'label_guesser': { 'deleted': ['20190801_1733_23'], }, 'doc_tracker': { 'deleted': ['20190801_1733_23'], }, }) paperwork-2.1.1/pytest.ini000066400000000000000000000000431417573700700155660ustar00rootroot00000000000000[pytest] python_files = tests_*.py paperwork-2.1.1/sub/000077500000000000000000000000001417573700700143315ustar00rootroot00000000000000paperwork-2.1.1/sub/libinsane/000077500000000000000000000000001417573700700162755ustar00rootroot00000000000000paperwork-2.1.1/sub/libpillowfight/000077500000000000000000000000001417573700700173505ustar00rootroot00000000000000paperwork-2.1.1/sub/pyocr/000077500000000000000000000000001417573700700154655ustar00rootroot00000000000000paperwork-2.1.1/tools/000077500000000000000000000000001417573700700147005ustar00rootroot00000000000000paperwork-2.1.1/tools/docgenerator/000077500000000000000000000000001417573700700173545ustar00rootroot00000000000000paperwork-2.1.1/tools/docgenerator/Makefile000066400000000000000000000014751417573700700210230ustar00rootroot00000000000000PYTHON ?= python3 build: build_c build_py install: install_py install_c uninstall: uninstall_py build_py: ${PYTHON} ./setup.py build build_c: doc: check: flake8 src/docgenerator test: install python3 -m unittest discover --verbose -s tests linux_exe: windows_exe: release: release_pypi: clean: rm -rf build dist *.egg-info install_py: ${PYTHON} ./setup.py install ${PIP_ARGS} install_c: uninstall_py: pip3 uninstall -y docgenerator uninstall_c: help: @echo "make build || make build_py" @echo "make check" @echo "make help: display this message" @echo "make install || make install_py" @echo "make uninstall || make uninstall_py" @echo "make release" .PHONY: \ build \ build_c \ build_py \ check \ doc \ exe \ help \ install \ install_c \ install_py \ test \ uninstall \ uninstall_c \ paperwork-2.1.1/tools/docgenerator/setup.py000077500000000000000000000020211417573700700210640ustar00rootroot00000000000000#!/usr/bin/env python3 import sys import setuptools setuptools.setup( name="docgenerator", version="1.0", description="Generate test documents and images", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: End Users/Desktop", ("License :: OSI Approved ::" " GNU General Public License v3 or later (GPLv3+)"), "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", ], license="GPLv3+", author="Jerome Flesch", author_email="jflesch@openpaper.work", packages=setuptools.find_packages('src'), package_dir={'': 'src'}, entry_points={ 'console_scripts': [ 'docgenerator-one = docgenerator.main:main_generate_one', 'docgenerator-workdir = docgenerator.main:main_generate_workdir', ], }, zip_safe=True, install_requires=[ 'openpaperwork-core', 'openpaperwork-gtk', 'paperwork-backend', 'paperwork-shell', ] ) paperwork-2.1.1/tools/docgenerator/src/000077500000000000000000000000001417573700700201435ustar00rootroot00000000000000paperwork-2.1.1/tools/docgenerator/src/docgenerator/000077500000000000000000000000001417573700700226175ustar00rootroot00000000000000paperwork-2.1.1/tools/docgenerator/src/docgenerator/__init__.py000066400000000000000000000000001417573700700247160ustar00rootroot00000000000000paperwork-2.1.1/tools/docgenerator/src/docgenerator/img.py000066400000000000000000000023031417573700700237430ustar00rootroot00000000000000import io import random import cairo from . import words def generate_img( core, paper_size, page_idx, nb_pages, dictionary=None, jpeg=False ): if dictionary is None: dictionary = words.WordDict() paper_size = (int(paper_size[0]) * 4, int(paper_size[1]) * 4) print("Generating image {}...".format(paper_size)) surface = cairo.ImageSurface( cairo.Format.RGB24, paper_size[0], paper_size[1] ) context = cairo.Context(surface) words.draw_words( context, dictionary, paper_size[0], paper_size[1], page_idx, nb_pages, word_height=20 * 4, word_space=10 * 4 ) surface.flush() if jpeg: img = core.call_success("cairo_surface_to_pillow", surface) surface = core.call_success( "pillow_to_surface", img, intermediate='jpeg', quality=80 ) print("Image generated") return surface def generate(core, file_out, paper_size, page_idx=0, nb_pages=1): img = generate_img(core, paper_size, page_idx, nb_pages) img = core.call_success("cairo_surface_to_pillow", img) with core.call_success("fs_open", file_out, 'wb') as fd: img.save(fd, format="JPEG") paperwork-2.1.1/tools/docgenerator/src/docgenerator/main.py000066400000000000000000000140671417573700700241250ustar00rootroot00000000000000import datetime import random import sys import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk # noqa: E402 import openpaperwork_core # noqa: E402 import paperwork_backend # noqa: E402 import paperwork_backend.docimport # noqa: E402 import paperwork_backend.model.workdir # noqa: E402 import paperwork_shell.main # noqa: E402 from . import img # noqa: E402 from . import pdf # noqa: E402 from . import pdf_img # noqa: E402 DOC_GENERATORS = { 'pdf': (pdf.generate, ".pdf", 1), 'img': (img.generate, ".jpeg", 2), 'pdf_img': (pdf_img.generate, ".pdf", 1), } def get_core(): core = openpaperwork_core.Core() for module_name in paperwork_backend.DEFAULT_CONFIG_PLUGINS: core.load(module_name) core.init() core.call_all("init_logs", "docgenerator", 'debug') core.call_all("config_load") core.call_all( "config_load_plugins", "docgenerator", paperwork_shell.main.DEFAULT_CLI_PLUGINS ) return core def get_page_size(): paper_size = Gtk.PaperSize.new("iso_a4") return ( paper_size.get_width(Gtk.Unit.POINTS), paper_size.get_height(Gtk.Unit.POINTS) ) def main_generate_one(): if len(sys.argv) <= 2: print("Usage:") print(" {} ".format(sys.argv[0])) sys.exit(1) core = get_core() paper_size = get_page_size() file_out = core.call_success("fs_safe", sys.argv[2]) DOC_GENERATORS[sys.argv[1]][0](core, file_out, paper_size) def generate_doc_id(core): while True: year = random.randint(1900, 2100) month = random.randint(1, 12) day = random.randint(1, 28) hour = random.randint(0, 23) minute = random.randint(0, 59) second = random.randint(0, 50) dt = datetime.datetime( year=year, month=month, day=day, hour=hour, minute=minute, second=second ) doc_id = dt.strftime(paperwork_backend.model.workdir.DOCNAME_FORMAT) doc_url = core.call_success("doc_id_to_url", doc_id) if doc_url is None: return doc_id nb_pages = core.call_success("doc_get_nb_pages_by_url", doc_url) if nb_pages is None or nb_pages <= 0: return doc_id def main_generate_workdir(): if len(sys.argv) <= 2: print("Usage:") print(" {} ".format(sys.argv[0])) sys.exit(1) generators = ([DOC_GENERATORS['img']] * 5) generators += [DOC_GENERATORS['pdf_img']] generators += ([DOC_GENERATORS['pdf']] * 5) core = get_core() paper_size = get_page_size() work_dir = core.call_success("fs_safe", sys.argv[1]) nb_docs = int(sys.argv[2]) core.call_all("cmd_set_interactive", False) print("Creating {}...".format(work_dir)) core.call_success("fs_mkdir_p", work_dir) print("Switching work directory to {} ...".format(work_dir)) core.call_all("config_put", "workdir", work_dir) promises = [] core.call_all("sync", promises) promise = promises[0] for p in promises[1:]: promise = promise.then(p) core.call_one("mainloop_schedule", promise.schedule) core.call_all("mainloop_quit_graceful") core.call_one("mainloop") for doc_idx in range(0, nb_docs): (generator, file_ext, nb_files) = generators[ doc_idx % len(generators) ] if nb_files > 1: nb_files = int(random.expovariate(1 / 50)) if nb_files <= 0: nb_files = 1 if nb_files > 200: nb_files = 200 tmp_files = [] try: for f in range(0, nb_files): (tmp_file, tmp_fd) = core.call_success( "fs_mktemp", "paperwork-docgenerator", file_ext, on_disk=True ) tmp_fd.close() tmp_files.append(tmp_file) print( "Generating file {}/{} for document {}/{}" " --> {}...".format( f, nb_files, doc_idx, nb_docs, tmp_file ) ) generator(core, tmp_file, paper_size, f, nb_files) file_import = paperwork_backend.docimport.FileImport( tmp_files, active_doc_id=None ) importers = [] core.call_all("get_importer", importers, file_import) print("Importers: {}".format(importers)) assert(len(importers) > 0) importer = importers[0] promise = importer.get_import_promise() print("Importing {} ...".format(tmp_files)) core.call_one("mainloop_schedule", promise.schedule) core.call_all("mainloop_quit_graceful") core.call_one("mainloop") print("Imported doc ids: {}".format(file_import.new_doc_ids)) for source_doc_id in file_import.new_doc_ids: dest_doc_id = generate_doc_id(core) print("Renaming document {} --> {} ...".format( source_doc_id, dest_doc_id )) source_doc_url = core.call_success( "doc_id_to_url", source_doc_id ) dest_doc_url = core.call_success( "doc_id_to_url", dest_doc_id ) core.call_all( "doc_rename_by_url", source_doc_url, dest_doc_url ) transactions = [] core.call_all("doc_transaction_start", transactions, 2) transactions.sort( key=lambda transaction: -transaction.priority ) for transaction in transactions: transaction.del_obj(source_doc_id) for transaction in transactions: transaction.add_obj(dest_doc_id) for transaction in transactions: transaction.commit() finally: print("Deleting {} ...".format(tmp_files)) for f in tmp_files: core.call_success("fs_unlink", f) print() paperwork-2.1.1/tools/docgenerator/src/docgenerator/pdf.py000066400000000000000000000016501417573700700237440ustar00rootroot00000000000000import random import cairo from . import words def generate(core, out_file, paper_size, *args, **kwargs): dictionary = words.WordDict() nb_pages = int(random.expovariate(1 / 500)) if nb_pages <= 0: nb_pages = 1 if nb_pages > 2000: nb_pages = 2000 print("Generating PDF {}...".format(out_file)) with core.call_success("fs_open", out_file, "wb") as fd: surface = cairo.PDFSurface(fd, int(paper_size[0]), int(paper_size[1])) context = cairo.Context(surface) for page_idx in range(0, nb_pages): print("Generating page {}/{}...".format(page_idx, nb_pages)) words.draw_words( context, dictionary, int(paper_size[0]), int(paper_size[1]), page_idx, nb_pages ) context.show_page() surface.flush() surface.finish() print("PDF {} generated".format(out_file)) paperwork-2.1.1/tools/docgenerator/src/docgenerator/pdf_img.py000066400000000000000000000024631417573700700246030ustar00rootroot00000000000000import random import cairo from . import img from . import words def generate(core, out_file, paper_size, *args, **kwargs): dictionary = words.WordDict() nb_pages = int(random.expovariate(1 / 100)) if nb_pages <= 0: nb_pages = 1 if nb_pages > 500: nb_pages = 500 print("Generating PDF with images only {}...".format(out_file)) with core.call_success("fs_open", out_file, "wb") as fd: surface = cairo.PDFSurface(fd, int(paper_size[0]), int(paper_size[1])) context = cairo.Context(surface) for page_idx in range(0, nb_pages): print("Generating page {}/{}...".format(page_idx, nb_pages)) img_surface = img.generate_img( core, paper_size, page_idx, nb_pages, dictionary=dictionary, jpeg=True ) scale_factor = paper_size[0] / img_surface.get_width() context.save() try: context.identity_matrix() context.scale(scale_factor, scale_factor) context.set_source_surface(img_surface) context.paint() finally: context.restore() context.show_page() surface.flush() surface.finish() print("PDF with images only {} generated".format(out_file)) paperwork-2.1.1/tools/docgenerator/src/docgenerator/words.py000066400000000000000000000051401417573700700243270ustar00rootroot00000000000000import codecs import random import gi gi.require_version('Pango', '1.0') gi.require_version('PangoCairo', '1.0') from gi.repository import Pango from gi.repository import PangoCairo WORD_HEIGHT = 20 WORD_SPACE = 10 def draw_words( context, words, width, height, page_idx, nb_pages, word_height=WORD_HEIGHT, word_space=WORD_SPACE ): w = word_space h = word_space context.save() try: context.set_source_rgb(1.0, 1.0, 1.0) context.rectangle(0, 0, width, height) context.fill() finally: context.restore() context.save() try: context.set_source_rgb(0.0, 0.0, 0.0) layout = PangoCairo.create_layout(context) layout.set_text("Page {}/{}".format(page_idx + 1, nb_pages), -1) layout_size = layout.get_size() txt_factor = word_height / layout_size[1] word_h = word_height word_w = layout_size[0] * txt_factor w = (width - word_w) / 2 context.translate(w, h) context.scale(txt_factor * Pango.SCALE, txt_factor * Pango.SCALE) PangoCairo.update_layout(context, layout) PangoCairo.show_layout(context, layout) h += word_h + word_space w = word_space finally: context.restore() while True: word = words.pick_word() layout = PangoCairo.create_layout(context) layout.set_text(word, -1) layout_size = layout.get_size() txt_factor = word_height / layout_size[1] word_h = word_height word_w = layout_size[0] * txt_factor if w + word_w >= width: h += word_h + word_space w = word_space if h >= height: return context.save() try: context.set_source_rgb(0, 0, 0) context.translate(w, h) context.scale(txt_factor * Pango.SCALE, txt_factor * Pango.SCALE) PangoCairo.update_layout(context, layout) PangoCairo.show_layout(context, layout) finally: context.restore() w += word_w + word_space class WordDict(object): DICTIONARY = "/usr/share/dict/words" def __init__(self): print("Loading {} ...".format(self.DICTIONARY)) self.dictionary = [] # length --> words with codecs.open(self.DICTIONARY, 'r', encoding='utf-8') as file_desc: for word in file_desc: word = word.strip() self.dictionary.append(word) print("Dictionnary loaded") def pick_word(self): return self.dictionary[random.randint(0, len(self.dictionary) - 1)] paperwork-2.1.1/tools/get_git_authors.py000077500000000000000000000141011417573700700204410ustar00rootroot00000000000000#!/usr/bin/env python3 import collections import fnmatch import json import os import os.path import re import subprocess import sys CATEGORIES = [ # order of evaluation matters ("doc", "Documentation"), ("openpaperwork-core", "OpenPaperwork Core"), ("openpaperwork-gtk", "OpenPaperwork GTK"), ("paperwork-backend", "Paperwork Backend"), ("paperwork-gtk", "GTK Frontend"), ("paperwork-shell", "CLI Frontend"), ("flatpak", "Flatpak Integration"), (None, "Others"), # default ] REPLACEMENT_RULES = { # Because I'm a dimw** who doesn't always configure his Git correctly. "jflesch": "Jerome Flesch", # Those are translations commits from Weblate. Weblate credits are # downloaded from Weblate manually. "Weblate Admin": None, # Shouldn't happen "Not Committed Yet": None, } EXTRA_IGNORES = [ "sub", ".git", "AUTHORS*", ] REGEX_EMAIL_AUTHOR = re.compile( r"[^(]*\((.+)\s\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} .*" ) def split_path(path): path = os.path.normpath(path) return path.split(os.sep) class IgnoreList(object): def __init__(self, ignore_list): self.ignore_list = EXTRA_IGNORES + ignore_list @staticmethod def find_gitignore(): current_dir = os.path.abspath(__file__) while not os.path.exists(os.path.join(current_dir, ".gitignore")): current_dir = os.path.dirname(current_dir) if current_dir == "/": raise Exception(".gitignore not found") return os.path.join(current_dir, ".gitignore") @staticmethod def load(): gitignore_path = IgnoreList.find_gitignore() sys.stderr.write("Loading {} ... ".format(gitignore_path)) sys.stderr.flush() with open(gitignore_path, 'r') as fd: ignore_list = fd.readlines() ignore_list = [line.strip() for line in ignore_list] ignore_list = [line for line in ignore_list if line != ""] ignore_list = [line.replace("/", "") for line in ignore_list] sys.stderr.write("{} ignores loaded\n".format(len(ignore_list))) return IgnoreList(ignore_list) def match(self, file_path): for pattern in self.ignore_list: if fnmatch.fnmatch(file_path, pattern): return True file_path = split_path(file_path) for pattern in self.ignore_list: for file_path_component in file_path: if fnmatch.fnmatch(file_path_component, pattern): return True return False def walk(directory, ignore_list): for (dirpath, dirnames, file_names) in os.walk(directory): for file_name in file_names: file_path = os.path.join(dirpath, file_name) if ignore_list.match(file_path): continue yield file_path def get_category_name(file_path): file_path = split_path(file_path) for (category_pattern, category_name) in CATEGORIES: if category_pattern is None: return category_name for component in file_path: if component == category_pattern: return category_name assert() def count_lines(line_counts, file_path): output = subprocess.run( ['git', 'blame', file_path], capture_output=True ) if output.returncode != os.EX_OK: sys.stderr.write( "WARNING: git blame {} failed ! (returncode={})".format( file_path, output.returncode ) ) return try: stdout = output.stdout.decode("utf-8") except UnicodeDecodeError: sys.stderr.write( "WARNING: Unicode on {}. Assuming it's a binary file\n".format( file_path ) ) return stdout = [line.strip() for line in stdout.split("\n")] for line in stdout: if line == "": continue author = REGEX_EMAIL_AUTHOR.match(line) if author is None: sys.stderr.write( "WARNING: Failed to find author email in the following line:\n" ) sys.stderr.write(line + "\n") continue author = author[1].strip() author = REPLACEMENT_RULES.get(author, author) if author is None: # replacement rules told us to ignore this one continue line_counts[author] += 1 def dump_json(line_counts): # We want to merge this JSON with the one from Weblate later. So we # imitate the weird JSON output of Weblate here. out = [] for (category_name, authors) in line_counts.items(): category = {category_name: []} out.append(category) category = category[category_name] for (author, line_count) in authors.items(): category.append([ '', # we don't care about emails author, line_count, ]) # sort authors by line count category.sort(key=lambda x: x[2], reverse=True) # Sort categories alphabetically out.sort(key=lambda x: next(iter(x)).lower()) print(json.dumps( out, indent=4, separators=(',', ': '), sort_keys=True )) def main(): if len(sys.argv) < 2 or sys.argv[1] == '-h' or sys.argv[1] == '--help': sys.stderr.write("Syntax:\n") sys.stderr.write(" {} \n".format(sys.argv[0])) return ignore_list = IgnoreList.load() line_counts = collections.defaultdict( lambda: collections.defaultdict(lambda: 0) ) for file_path in walk(sys.argv[1], ignore_list): category = get_category_name(file_path) sys.stderr.write("Examining {} (category={}) ...\n".format( file_path, category )) counts = line_counts[category] count_lines(counts, file_path) for (category, authors) in line_counts.items(): sys.stderr.write(" - {}\n".format(category)) for (k, v) in authors.items(): sys.stderr.write("{}: {}\n".format(k, v)) sys.stderr.write("\n") dump_json(line_counts) if __name__ == "__main__": main() paperwork-2.1.1/tools/l10n_compile.sh000077500000000000000000000023111417573700700175160ustar00rootroot00000000000000#!/bin/bash LANGS="de_DE.UTF-8:de es_ES.UTF-8:es fr_FR.UTF-8:fr oc_FR.UTF-8:oc uk_UA.UTF-8:uk" if [ -z "$1" ] || [ -z "$2" ] || [ "$1" = "-h" ] || [ "$1" = "--help" ] ; then echo "Usage:" echo " $0 " echo echo "Examples:" echo " $0 l10n src/paperwork_gtk/l10n paperwork_gtk" echo echo "You should probably use 'make l10_compile' instead of calling this script directly" exit 1 fi if ! which msgfmt > /dev/null 2>&1 ; then echo "msgfmt is missing" echo "--> sudo apt install gettext" exit 2 fi src_dir="$1" dst_dir="$2" mo_name="$3" mkdir -p "${dst_dir}" touch "${dst_dir}/__init__.py" rm -rf "${dst_dir}/out" for lang in ${LANGS} do long_locale=$(echo $lang | cut -d: -f1) short_locale=$(echo $lang | cut -d: -f2) po_file="${src_dir}/${short_locale}.po" locale_dir="${dst_dir}/out/${short_locale}/LC_MESSAGES" echo "${po_file} --> ${locale_dir}/${mo_name}.mo" mkdir -p "${locale_dir}" touch "${dst_dir}/out/__init__.py" touch "${dst_dir}/out/${short_locale}/__init__.py" touch "${locale_dir}/__init__.py" if ! msgfmt "${po_file}" -o "${locale_dir}/${mo_name}.mo" ; then echo "msgfmt failed ! Unable to update .mo file !" exit 2 fi done paperwork-2.1.1/tools/l10n_extract.sh000077500000000000000000000045561417573700700175550ustar00rootroot00000000000000#!/bin/bash LANGS="de_DE.UTF-8:de es_ES.UTF-8:es fr_FR.UTF-8:fr oc_FR.UTF-8:oc uk_UA.UTF-8:uk" if [ -z "$1" ] || [ -z "$2" ] || [ "$1" = "-h" ] || [ "$1" = "--help" ] ; then echo "Usage:" echo " $0 " echo echo "Examples:" echo " $0 src/paperwork_gtk l10n" echo echo "You should probably use 'make l10_extract' instead of calling this script directly" exit 1 fi if ! which intltool-extract > /dev/null 2>&1 ; then echo "intl-tool-extract is missing" echo "--> sudo apt install intltool" exit 2 fi if ! which xgettext > /dev/null 2>&1 ; then echo "xgettext is missing" echo "--> sudo apt install gettext" exit 2 fi src_dir="$1" dst_dir="$2" src_dir=$(realpath --relative-to=$(pwd) ${src_dir}) while ! [ -d .git ]; do if [ "$(pwd)" == "/" ]; then echo "Failed to find git repository root" echo "Are you in a Git repository ?" exit 3 fi # we must place ourselves at the root of the repository so the file # paths in the .pot and .po are correct for Weblate src_dir="$(basename $(pwd))/${src_dir}" cd .. done src_dir=$(realpath --relative-to=$(pwd) ${src_dir}) mkdir -p "${dst_dir}" echo "Extracting strings from Glade files ..." for glade_file in $(find "${src_dir}" -name \*.glade) ; do # intltool-extract expects a relative path as input echo "${glade_file} --> .glade.h ..." if ! intltool-extract --type=gettext/glade "${glade_file}" > /dev/null; then echo "intltool-extract Failed ! Unable to extract strings to translate from .glade files !" exit 3 fi done rm -f "${dst_dir}/messages.pot" echo "Extracting strings from .py and .h files ..." if ! xgettext -k_ -kN_ --from-code=UTF-8 -o "${dst_dir}/messages.pot" \ $(find "${src_dir}" -name \*.py) \ $(find "${src_dir}" -name \*.glade.h) ; then echo "xgettext failed ! Unable to extract strings to translate !" exit 3 fi rm -f $(find "${src_dir}" -name \*.glade.h) for lang in ${LANGS} do locale=$(echo $lang | cut -d: -f1) po_file="${dst_dir}/$(echo $lang | cut -d: -f2).po" if ! [ -f ${po_file} ] then echo "messages.pot --> ${po_file} (gen)" msginit --no-translator \ -l ${locale} -i "${dst_dir}/messages.pot" \ -o ${po_file} else echo "messages.pot --> ${po_file} (upd)" msgmerge -N -U ${po_file} "${dst_dir}/messages.pot" fi if [ $? -ne 0 ] ; then echo "msginit / msgmerge failed ! Unable to create or update .po file !" exit 3 fi done paperwork-2.1.1/tools/labelgenerator/000077500000000000000000000000001417573700700176665ustar00rootroot00000000000000paperwork-2.1.1/tools/labelgenerator/Makefile000066400000000000000000000015011417573700700213230ustar00rootroot00000000000000PYTHON ?= python3 build: build_c build_py install: install_py install_c uninstall: uninstall_py build_py: ${PYTHON} ./setup.py build build_c: doc: check: flake8 src/labelgenerator test: install python3 -m unittest discover --verbose -s tests linux_exe: windows_exe: release: release_pypi: clean: rm -rf build dist *.egg-info install_py: ${PYTHON} ./setup.py install ${PIP_ARGS} install_c: uninstall_py: pip3 uninstall -y labelgenerator uninstall_c: help: @echo "make build || make build_py" @echo "make check" @echo "make help: display this message" @echo "make install || make install_py" @echo "make uninstall || make uninstall_py" @echo "make release" .PHONY: \ build \ build_c \ build_py \ check \ doc \ exe \ help \ install \ install_c \ install_py \ test \ uninstall \ uninstall_c \ paperwork-2.1.1/tools/labelgenerator/setup.py000077500000000000000000000016621417573700700214100ustar00rootroot00000000000000#!/usr/bin/env python3 import sys import setuptools setuptools.setup( name="labelgenerator", version="1.0", description="Generate test labels", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: End Users/Desktop", ("License :: OSI Approved ::" " GNU General Public License v3 or later (GPLv3+)"), "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", ], license="GPLv3+", author="Jerome Flesch", author_email="jflesch@openpaper.work", packages=setuptools.find_packages('src'), package_dir={'': 'src'}, entry_points={ 'console_scripts': [ 'labelgenerator-workdir = labelgenerator.main:main', ], }, zip_safe=True, install_requires=[ 'openpaperwork-core', 'openpaperwork-gtk', 'paperwork-backend', 'paperwork-shell', ] ) paperwork-2.1.1/tools/labelgenerator/src/000077500000000000000000000000001417573700700204555ustar00rootroot00000000000000paperwork-2.1.1/tools/labelgenerator/src/labelgenerator/000077500000000000000000000000001417573700700234435ustar00rootroot00000000000000paperwork-2.1.1/tools/labelgenerator/src/labelgenerator/__init__.py000066400000000000000000000000001417573700700255420ustar00rootroot00000000000000paperwork-2.1.1/tools/labelgenerator/src/labelgenerator/main.py000066400000000000000000000053021417573700700247410ustar00rootroot00000000000000import random import sys import openpaperwork_core import paperwork_backend import paperwork_shell.main from . import words def get_core(): print("Loading core ...") core = openpaperwork_core.Core() for module_name in paperwork_backend.DEFAULT_CONFIG_PLUGINS: core.load(module_name) core.init() core.call_all("init_logs", "docgenerator", 'debug') core.call_all("config_load") core.call_all( "config_load_plugins", "labelgenerator", paperwork_shell.main.DEFAULT_CLI_PLUGINS ) print("Core loaded") return core def main(): if len(sys.argv) <= 2: print("Usage:") print(" {} ".format(sys.argv[0])) sys.exit(1) work_dir = sys.argv[1] nb_labels = int(sys.argv[2]) dictionary = words.WordDict() core = get_core() print("Generating labels ...") labels = [] for _ in range(0, nb_labels): label = dictionary.pick_word() print(" - {}".format(label)) labels.append(label) print() core.call_all("cmd_set_interactive", False) work_dir = core.call_success("fs_safe", work_dir) print("Switching work directory to {} ...".format(work_dir)) core.call_all("config_put", "workdir", work_dir) promises = [] core.call_all("sync", promises) promise = promises[0] for p in promises[1:]: promise = promise.then(p) core.call_one("mainloop_schedule", promise.schedule) core.call_all("mainloop_quit_graceful") core.call_one("mainloop") docs = [] core.call_all("storage_get_all_docs", docs) docs.sort() print("{} documents found".format(len(docs))) transactions = [] core.call_all("doc_transaction_start", transactions, len(docs)) transactions.sort(key=lambda t: -t.priority) for (doc_id, doc_url) in docs: nb_labels_to_add = random.randint(0, max(1, int(nb_labels / 2))) print("{} <-- {} labels".format(doc_id, nb_labels_to_add)) for _ in range(0, nb_labels_to_add): label = labels[random.randint(0, len(labels) - 1)] print("{} <- {}".format(doc_id, label)) core.call_success("doc_add_label_by_url", doc_url, label) specials = [ ('doc.pdf', '_PDF'), ('paper.1.jpg', '_IMG'), ('paper.1.words', '_HOCR'), ] for (file_name, label) in specials: file_path = core.call_success("fs_join", doc_url, file_name) if core.call_success("fs_exists", file_path) is not None: core.call_success("doc_add_label_by_url", doc_url, label) for t in transactions: t.upd_obj(doc_id) for t in transactions: t.commit() print("All done") paperwork-2.1.1/tools/labelgenerator/src/labelgenerator/words.py000066400000000000000000000010561417573700700251550ustar00rootroot00000000000000import codecs import random class WordDict(object): DICTIONARY = "/usr/share/dict/words" def __init__(self): print("Loading {} ...".format(self.DICTIONARY)) self.dictionary = [] # length --> words with codecs.open(self.DICTIONARY, 'r', encoding='utf-8') as file_desc: for word in file_desc: word = word.strip() self.dictionary.append(word) print("Dictionnary loaded") def pick_word(self): return self.dictionary[random.randint(0, len(self.dictionary) - 1)] paperwork-2.1.1/tools/merge_authors_json.py000077500000000000000000000026631417573700700211610ustar00rootroot00000000000000#!/usr/bin/env python3 import json import sys def load_jsons(file_paths): out = [] for file_path in file_paths: sys.stderr.write("Loading {} ...\n".format(file_path)) with open(file_path, 'r') as fd: content = fd.read() out.append(json.loads(content)) return out def merge_jsons(jsons): sys.stderr.write("Merging ...\n") out = [] for j in jsons: out += j return out def sort_json(out): sys.stderr.write("Sorting ...\n") # keep in mind we are immitating the JSON from Weblate here, and Weblate's # JSON is a bit weird. for category in out: for (category_name, authors) in category.items(): # sort authors by line count authors.sort(key=lambda x: x[2], reverse=True) # Sort categories alphabetically out.sort(key=lambda x: next(iter(x)).lower()) def main(): if len(sys.argv) <= 1 or sys.argv[1] == "-h" or sys.argv[1] == "--help": sys.stderr.write("Syntax:\n") sys.stderr.write( " {} [ [ ...]]\n".format( sys.argv[0] ) ) return jsons = load_jsons(sys.argv[1:]) out = merge_jsons(jsons) sort_json(out) print(json.dumps( out, indent=4, separators=(',', ': '), sort_keys=True )) sys.stderr.write("All done !\n") if __name__ == "__main__": main() paperwork-2.1.1/tools/plugin-grapher/000077500000000000000000000000001417573700700176245ustar00rootroot00000000000000paperwork-2.1.1/tools/plugin-grapher/Makefile000066400000000000000000000015041417573700700212640ustar00rootroot00000000000000PYTHON ?= python3 build: build_c build_py install: install_py install_c uninstall: uninstall_py build_py: ${PYTHON} ./setup.py build build_c: doc: check: flake8 src/paperwork_backend test: install python3 -m unittest discover --verbose -s tests linux_exe: windows_exe: release: release_pypi: clean: rm -rf build dist *.egg-info install_py: ${PYTHON} ./setup.py install ${PIP_ARGS} install_c: uninstall_py: pip3 uninstall -y plugin-grapher uninstall_c: help: @echo "make build || make build_py" @echo "make check" @echo "make help: display this message" @echo "make install || make install_py" @echo "make uninstall || make uninstall_py" @echo "make release" .PHONY: \ build \ build_c \ build_py \ check \ doc \ exe \ help \ install \ install_c \ install_py \ test \ uninstall \ uninstall_c \ paperwork-2.1.1/tools/plugin-grapher/setup.py000077500000000000000000000017421417573700700213450ustar00rootroot00000000000000#!/usr/bin/env python3 import sys import setuptools setuptools.setup( name="plugin-grapher", version="1.0", description="Generate graphic representing the plugins", url=( "https://gitlab.gnome.org/World/OpenPaperwork/paperwork/tree/master/" "plugin-grapher" ), classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: End Users/Desktop", ("License :: OSI Approved ::" " GNU General Public License v3 or later (GPLv3+)"), "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", ], license="GPLv3+", author="Jerome Flesch", author_email="jflesch@openpaper.work", packages=setuptools.find_packages('src'), package_dir={'': 'src'}, entry_points={ 'console_scripts': [ 'plugin-grapher = plugin_grapher.main:main', ], }, zip_safe=True, install_requires=[ "openpaperwork-core", ] ) paperwork-2.1.1/tools/plugin-grapher/src/000077500000000000000000000000001417573700700204135ustar00rootroot00000000000000paperwork-2.1.1/tools/plugin-grapher/src/plugin_grapher/000077500000000000000000000000001417573700700234215ustar00rootroot00000000000000paperwork-2.1.1/tools/plugin-grapher/src/plugin_grapher/__init__.py000066400000000000000000000000001417573700700255200ustar00rootroot00000000000000paperwork-2.1.1/tools/plugin-grapher/src/plugin_grapher/main.py000066400000000000000000000100201417573700700247100ustar00rootroot00000000000000import importlib import sys import openpaperwork_core # skip some interfaces ; too many plugin depends on it, making the graph # unreadable INTERFACES_TO_IGNORE = { "chkdeps", "pages", } def load_plugins(core, arg): (module_name, variable) = arg.rsplit(".", 1) module = importlib.import_module(module_name) plugin_names = getattr(module, variable) for name in plugin_names: print("Loading {} ...".format(name)) core.load(name) g_current_frame = [] g_indent = 0 def print_package(out, plugin_name): global g_current_frame global g_indent pkgs = plugin_name.split(".", 1) plugin_name = pkgs[-1] pkgs = pkgs[:-1] print("Package: {}".format(pkgs)) idx = 0 for (idx, (pkg_a, pkg_b)) in enumerate(zip(pkgs, g_current_frame)): if pkg_a != pkg_b: break else: if len(g_current_frame) == len(pkgs): return plugin_name while len(g_current_frame) > idx: g_indent -= 1 out.write('{}}}\n'.format(" " * g_indent)) g_current_frame = g_current_frame[:-1] while len(pkgs) > idx: out.write('\n{}package "{}" <> {{\n'.format( " " * g_indent, pkgs[idx] )) g_indent += 1 if pkgs[idx] == "openpaperwork_core": out.write('{}class "Core"\n'.format(" " * g_indent)) idx += 1 g_current_frame = pkgs return plugin_name g_interfaces = set() def dump_plugin(out, long_plugin_name, short_plugin_name, plugin): global g_interfaces global g_indent long_plugin_name = long_plugin_name.split(".", 1)[1].replace(".", "-") print("Plugin: {}".format(long_plugin_name)) interfaces = plugin.get_interfaces() for i in INTERFACES_TO_IGNORE: if i in interfaces: interfaces.remove(i) print("Interfaces: {}".format(interfaces)) out.write('{}class "{}"\n'.format(" " * g_indent, long_plugin_name)) if len(interfaces) <= 0: out.write('{}"Core" o-- "{}"\n'.format( " " * g_indent, long_plugin_name )) for interface in interfaces: if interface not in g_interfaces: g_interfaces.add(interface) out.write('{}interface "{}"\n'.format( " " * g_indent, interface )) out.write('{}"Core" o-- "{}"\n'.format( " " * g_indent, interface )) out.write('{}"{}" <|-- "{}"\n'.format( " " * g_indent, interface, long_plugin_name )) # Do not show plugin dependencies. It makes the graph too messy # deps = plugin.get_deps() # print("Dependencies: {}".format(deps)) # for dep in deps: # interface = dep['interface'] # if interface in INTERFACES_TO_IGNORE: # continue # out.write('{}"{}" o-- "{}"\n'.format( # " " * g_indent, long_plugin_name, interface) # ) def main(): core = openpaperwork_core.Core() if len(sys.argv) <= 1: print("Usage: {} [out_file] [plugin list]".format(sys.argv[0])) print( "Example: {}" " out.uml paperwork_shell.main.DEFAULT_CLI_PLUGINS".format( sys.argv[0] ) ) sys.exit(1) for arg in sys.argv[2:]: load_plugins(core, arg) print("{} plugins found".format(len(core.plugins))) package_name = None with open(sys.argv[1], "w") as out: out.write("@startuml\n") out.write("left to right direction\n") for plugin_name in sorted(core.plugins): print("----") plugin = core.plugins[plugin_name] short_plugin_name = print_package(out, plugin_name) short_plugin_name = plugin_name dump_plugin(out, plugin_name, short_plugin_name, plugin) print("----") print_package(out, "") # closes the current frames out.write("@enduml\n") print("====") print("Plantuml command:") print("PLANTUML_LIMIT_SIZE=65536 plantuml {}".format(sys.argv[1])) if __name__ == "__main__": main() paperwork-2.1.1/work.openpaper.Paperwork.json000066400000000000000000000263631417573700700213700ustar00rootroot00000000000000{ "app-id": "work.openpaper.Paperwork", "branch": "master", "runtime": "org.gnome.Platform", "runtime-version": "41", "sdk": "org.gnome.Sdk", "command": "paperwork-gtk", "copy-icon": true, "sdk-extensions": [ "org.freedesktop.Sdk.Extension.openjdk11" ], "finish-args": [ "--share=ipc", "--share=network", "--socket=fallback-x11", "--socket=wayland", "--filesystem=home", "--persist=.python-eggs", "--talk-name=org.freedesktop.Notifications", "--talk-name=org.freedesktop.FileManager1", "--talk-name=org.gtk.vfs.*", "--filesystem=xdg-run/gvfsd", "--own-name=work.openpaper.paperwork", "--env=JAVA_HOME=/app/jre", "--env=LIBO_FLATPAK=1" ], "modules": [ "flatpak/shared-modules/setuptools-59.5.0.json", "flatpak/shared-modules/libreoffice-7.2.3.2.json", "flatpak/shared-modules/scikit-learn-0.24.0.json", "flatpak/shared-modules/sane-backends-1.0.32.json", "flatpak/shared-modules/tesseract-4.1.1.json", { "name": "python-distro", "buildsystem": "simple", "build-commands": ["python3 setup.py install --prefix=/app --root=/"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/ca/e3/78443d739d7efeea86cbbe0216511d29b2f5ca8dbf51a6f2898432738987/distro-1.4.0.tar.gz", "sha256": "362dde65d846d23baee4b5c058c8586f219b5a54be1cf5fc6ff55c4578392f57" } ] }, { "name": "python-Levenshtein", "buildsystem": "simple", "build-commands": ["python3 setup.py install --prefix=/app --root=/"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/42/a9/d1785c85ebf9b7dfacd08938dd028209c34a0ea3b1bcdb895208bd40a67d/python-Levenshtein-0.12.0.tar.gz", "sha256": "033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1" } ] }, { "name": "python-fabulous", "buildsystem": "simple", "build-commands": ["python3 setup.py install --prefix=/app --root=/"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/53/2d/5750798dbb1cd3029c17b6f7456f79948b15f63e4781ffa0be8cf35cfc22/fabulous-0.3.0.tar.gz", "sha256": "54040da01d7ce1e937fc4b61d265e872b007463bea411a3a5762f4d6ee55c312" } ] }, { "name": "python-pillow", "buildsystem": "simple", "build-commands": ["python3 setup.py install --prefix=/app --root=/"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/3c/7e/443be24431324bd34d22dd9d11cc845d995bcd3b500676bcf23142756975/Pillow-5.4.1.tar.gz", "sha256": "5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f" } ], "modules": [ { "name": "python-olefile", "buildsystem": "simple", "build-commands": ["python3 setup.py install --prefix=/app --root=/"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/34/81/e1ac43c6b45b4c5f8d9352396a14144bba52c8fec72a80f425f6a4d653ad/olefile-0.46.zip", "sha256": "133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964" } ] } ] }, { "name": "python-pycountry", "buildsystem": "simple", "build-commands": ["python3 setup.py install --prefix=/app --root=/"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/87/c7/c2c76c3ae4ac79c74c1871ae775ed97b70d475dd90d1e824b1d2fc0cd54f/pycountry-18.12.8.tar.gz", "sha256": "8ec4020b2b15cd410893d573820d42ee12fe50365332e58c0975c953b60a16de" } ] }, { "name": "python-nose", "buildsystem": "simple", "build-commands": ["python3 setup.py install --prefix=/app --root=/"], "sources": [ { "type": "archive", "url": "https://pypi.python.org/packages/58/a5/0dc93c3ec33f4e281849523a5a913fa1eea9a3068acfa754d44d88107a44/nose-1.3.7.tar.gz", "sha256": "f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" } ] }, { "name": "python-pyxdg", "buildsystem": "simple", "build-commands": ["python3 setup.py install --prefix=/app --root=/"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/47/6e/311d5f22e2b76381719b5d0c6e9dc39cd33999adae67db71d7279a6d70f4/pyxdg-0.26.tar.gz", "sha256": "fe2928d3f532ed32b39c32a482b54136fe766d19936afc96c8f00645f9da1a06" } ] }, { "name": "python-pydbus", "buildsystem": "simple", "build-commands": ["python3 setup.py install --prefix=/app --root=/"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/58/56/3e84f2c1f2e39b9ea132460183f123af41e3b9c8befe222a35636baa6a5a/pydbus-0.6.0.tar.gz", "sha256": "4207162eff54223822c185da06c1ba8a34137a9602f3da5a528eedf3f78d0f2c" } ] }, { "name": "python-whoosh", "buildsystem": "simple", "build-commands": ["python3 setup.py install --prefix=/app --root=/"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/25/2b/6beed2107b148edc1321da0d489afc4617b9ed317ef7b72d4993cad9b684/Whoosh-2.7.4.tar.gz", "sha256": "7ca5633dbfa9e0e0fa400d3151a8a0c4bec53bd2ecedc0a67705b17565c31a83" } ] }, { "name": "python-psutil", "buildsystem": "simple", "build-commands": ["python3 setup.py install --prefix=/app --root=/"], "sources": [ { "type": "archive", "url": "https://files.pythonhosted.org/packages/e1/b0/7276de53321c12981717490516b7e612364f2cb372ee8901bd4a66a000d7/psutil-5.8.0.tar.gz", "sha256": "0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6" } ] }, { "name": "poppler-data", "buildsystem": "cmake-ninja", "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-data-0.4.11.tar.gz", "sha256": "2cec05cd1bb03af98a8b06a1e22f6e6e1a65b1e2f3816cb3069bb0874825f08c" } ] }, { "name": "poppler", "buildsystem": "cmake-ninja", "config-opts": [ "-DENABLE_QT5=OFF", "-DENABLE_QT6=OFF", "-DENABLE_BOOST=OFF", "-DENABLE_LIBOPENJPEG:STRING=none" ], "sources": [ { "type": "archive", "url": "https://poppler.freedesktop.org/poppler-21.12.0.tar.xz", "sha256": "acb840c2c1ec07d07e53c57c4b3a1ff3e3ee2d888d44e1e9f2f01aaf16814de7" } ] }, { "name" : "libhandy", "buildsystem" : "meson", "builddir" : true, "config-opts": [ "-Dexamples=false", "-Dtests=false" ], "sources" : [ { "type" : "git", "url" : "https://gitlab.gnome.org/GNOME/libhandy.git", "tag": "1.2.2", "commit": "09f36006b26f41a2bb383b0c853e954c5792cfe1" } ] }, { "name": "libinsane", "buildsystem": "meson", "sources": [ { "type": "git", "url": "https://gitlab.gnome.org/World/OpenPaperwork/libinsane.git", "branch": "master", "disable-shallow-clone": true } ] }, { "name": "python-pyocr", "buildsystem": "simple", "build-options": { "env": { "ENABLE_SETUPTOOLS_SCM": "0" } }, "build-commands": ["python3 ./setup.py install --prefix=/app --root=/"], "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "git", "url": "https://gitlab.gnome.org/World/OpenPaperwork/pyocr.git", "branch": "master", "disable-shallow-clone": true } ] }, { "name": "python-pypillowfight", "make-install-args": ["PIP_ARGS=--prefix=/app --root=/"], "no-autogen": true, "ensure-writable": [ "/lib/python*/site-packages/easy-install.pth", "/lib/python*/site-packages/setuptools.pth" ], "sources": [ { "type": "git", "url": "https://gitlab.gnome.org/World/OpenPaperwork/libpillowfight.git", "branch": "master", "disable-shallow-clone": true }, { "type": "file", "path": "flatpak/pypillowfight-Makefile", "dest-filename": "Makefile" } ] }, { "name": "python-paperwork", "make-install-args": ["PIP_ARGS=--prefix=/app --root=/"], "no-autogen": true, "ensure-writable": ["/lib/python*/site-packages/easy-install.pth","/lib/python*/site-packages/setuptools.pth"], "post-install": ["paperwork-gtk install --icon_base_dir=/app/share/icons --data_base_dir=/app/share"], "sources": [ { "type": "git", "path": ".", "branch": "master", "disable-shallow-clone": true }, { "type": "file", "path": "data.tar.gz" } ] } ] }

]}4~֭3gI=Xݻw1m'%%I\k.pʦ HW5jceE/3TzQ ƢrʘZ^^Lkk׮MG@4Ipm(`}>Y@E@e:#GPn疖(QAÇX[<+w__^SNѓm۶mݺU__bŊd`FdӨ!~; TC @¨@yx"}  ;wF6HJp|gy-e;qℒNMӦMY0]]tVիWv,MxCgT377Þ8bMdggClmll Y91(?#kצ{׿y&44?8p m1 w}٠,&SNA rBXLLLVX+EV”siLP[OgE@(RE*[ < &.{ԩS tY%/cW^q;va:|p)5'$$;wkӦMBQ;J\{}qnݺucǎNKK߅ѱ= 8866=E,֭|خk׮?RJxpyD'<<|D7ᯭ[V!~`p #622C> agg +W^t)¼>}ԱgbLtPСɓw}-5z|tx.`d-j?\"mQI⩤WJ b^ ?ZDN׮]:URE$'{zz:Q߾}6Cyx" _xQPP묬,(էNڻwoIG-aDXSfMx"*tvv6mt{)NJ_~X4.D$. ͝;:uU"<\|V7{lv`kP͗.]Rr&@)fl=z #|ժUhN:w;::=bvTT5(HII{|8}4U>Ń`kk+4A^^ޟ(Ν;X%+_^ɕȢE,--) & իW:thaL  胝L[VTHNN("aÆ1G{J[C{>xNy"ü@LXٳgG5q ?2+ @/WѣnWBgΜٽ{wJ|ɀmɶmnܸQLW_@}Q-% Ìl#&&,MJJUYA !&:NNNJ9b^rww%>>>Lҥ (ك;VZa]w@gM՗.]J)eҤI2I}ttt߾}322"zMMM @G-Ll"1סCydQ0k,V%Lϟ?/r!95㡸pgϞZ 5ĵk\\\$c]*5ׯO:T KGrFHTQ@.udJbƌdgL,ۙ"Nff%Ktuu1()ܿgggK5jpPXXhhhE纺]ܹsDO@{Pb腇7n͛/ _?P:2qgQ\B;FE5W8tP.X@B:@hCܽ{6b7yd`\\\)1:thΜ91c`V< ~ Jj6x¹BX$oqÄHpssP:f͢*ܺ$4haڈ111ǤIX^ep%iXAAm#u;qJzO˖-#ozpuDGGɓ'QQQ^^^+V6mhfvZƍwI__rʕJ9pXcn2@4"Y}v4:xL'O!CK9)XK矈i5B[]v)sҽ{wT,p%6=z 6p@:1Y.*,,u떻KNCc"Z%]ehF|qyPJ)S۷c뾒& =RTAAAydD۶m%>|زeϕoΝ;cƌ:ujffg'E d (-ZD F 8waÆt/X#lll@,loo\V~~~dd$4o~hEVh2^~mhh+}_oo݅NgD5jwޑѣG_4tC=$?~Y#eC?~\eZ53fHS X˗/K1!M~~~`ZZZ`OÆ 믳gRL܈( Cypp+ˋH^^0ax͙3gwʕ)`&. L5C %TaE>tPqѣG̒Daj >| qppp|شiX8T6қ,025}޽{MֵkWUZjsȑ[~=*.\pرm۶azbhڵKLG3CJחV^_'qk!d?@xA+-[ uB777 ePlHW(LW'tQF@k׮>d<&&bSBI Ԯ]rbvԈ#(JѮpAjO_zU6mJg2}M i>QT4_SSwTA׭[%!GիW}],t9ؗLDFFȾYǏիj\(8 9l0/X"d_o }{{{f/$3ҥKKL+WT7nZx1ce޽p%S#55~:.ݻUVE5Y v4j(L; khhF^^^E?^Kȕr…|,qppp|]m޼yZ(4嗎;3fܹPW^ dХ'OlaaإK\",9N6#!!kkZZ硍ٳgFׯdnݿYljVp0(x欭[zNt/ue.2Ui})N> ,=BQT`P16x`5:ʴ.5Cխ[7e*6N:"krpaƒ͘fݻwK;=yr(_ti)<nZ<bgΜ5kȑ#qG<,+ag":tu7ne8q"CQ/ܺu+..FDD@00}ͽOl25A1~yZdp#['z&oڴ&SIʊ[ `ǏW(Bk׮# (@ʕ+|,qppppJobHMMx񢟟ߎ;(̙3mllFeaaammm/ ]ݻ`nA9{UhuԒ;6D<׋1c~%Ht׳gO|]jxfϞMo߾Z.]*޽{@XHּSN0''l68p C ٸq ׂk+W.,,y=///LV%N;;wԼ} ߧD(0ڱRgϞd:th=TZJk߼y$2spppp|6lօ 6}?$w۶m!!!%;hbj*Ng͚E,f膎05wЁ%艤d&ZZZŁH rClٲEwqy~I6m`@Yr( Wth mvŊh: (eH'ӥرcnw-itvʕx&M硡?yCrht%b喖m+6C%7o޽{ [ne%t+r!!594Jٳ\ܢ9*IQ/$o߾mggǾZҔQOvwLB5ʕ+Gh+Ԯ{N†tsP3g~s='6i<###DXpI k^ڵ?}ƍ޽{;3h T"ܹsy=W0>>J|98٨ ;''~>Y=˅akk[R%a<~ٰ2401a:ecԎhʊ޽%KhkkKÇߓ&KKK:gϞ]`s 9-,3EE.(:˗^[jCBBjժ⎮IK'k޼yi[1 #~-eB׏ի~ R?i;zꙛgϞiiiݽ{WXQFĉ|qmi\R__ K` ʕ+L˻wVK@v횒m6oĤE.xx4>`{{{y4mڔ$e۷H,]tiٲYo.t.>岹~>}nܸ!D(_ժUɌde3gD -_c8/~gGaÆY;J" 'SNR .BM|Û WE)h/_۲eˢE JG1  ߽{toVtR'O!њ/el 1Fŋ  Xa(35P{%Q|yG88888䔍BX?#Ȝ.0MaAk;vlcѢEV}]]]v/޽{q׮],/-vj\T&DGG]KRzQ5"..__g``@vn@f i/ݣp 4HHH+=z@aFF=£vjm۶++hNpGwwwVRPP@Fxxȅ[la[nŀ%/~Ss̩\c>O:Pjٲe~~~iiic>mJ' `&i֬f+Ξ=[&'A?,޼yGm߾?ÇO\eM6|آE5j(2O[:4 cccxLLF0ikk0{~L5Θ1C鑃lTJ`j7n@cǎyyy˗+WnĉPA硲bݻq<==7婩*d@' .SyneB٤IJd*TnȐ! ;`Μ9"(~6mJjff;.\PXHS 28C@i 6 D ˕Gaa!4U؎;B/tu''A 8 ]]*qHH)\"󗍸>}9 wNrӨQ#SSSɓvaMT&S ҦΟ?/=*l!))V__LJ(ǶRG.|qpppp)O2HLLtww={-[vܹw^ jjժYfk~kQM~i P㉪Ţ">tPvN J+v'JgTb@gdܮSNBoll,r!; r!ċ/n /WPJUVJGJ;wmaǷ.9Cm3$ܭ;:$_{(ӧOêU^pA 0]p:QǍ8;%hM++hW;w I#^|`+TM^ӴgϞĜ={{xxoz{{sPP͛7AxcO@$*VHb_z%Z ӻwﴴ5j0cǎ᳴@Y-]f߇RDŽ!2ALaY|`W)$ LjȖYʧ>|^f]VaidZ0Yqz RhԨ$JRKU)JdpWREIR;‚驣cooAP)d(SNy4.^" ;wWΞ0a0 gHMeN>#F4jjһ=ߝl")QDq׮]E;qȅwVWRr3g˗Yy޽QSh뱱;f 0 ?K;v 7MOO P0Ç'''SS-ZH *ЁKDD2@CBBlogӧ*dOֆl:?(oݺ5B2^{R͛7~)</^%6{{{a9ಳҥKwo֬5o|ڴi {^^|rfID0TQff& ~U8+@hsiiiŸr+&jΝ;ϟ?h }}}(EIxx_?(qz!V߱cǒm<̙3֭3fLi{۶mYf߾}%י={TTԩSEW^'x7nL:T>""%ã3`7v88888dbҤIX&nxC'k=777Zɻt}fK~*>m*IV[wȠwPF !g?wю;?/t8M"_ƍ_ۭ[7T=ݻw'Nq>|pi#yJ }}}WZ5vXcc9sڵ+::[ ?H{o߾z*82$ESSP /ŽǏgFBQ Lht(iMf(*KoݺŮڽ{wNgϞ@6Ps|pppppHvZmx -Δ˞,Ze:]xj׮.khhWoYh Ƣ C*<`kkK駟fϞ}!C[zʀ; 6''2GYl{eϟ?߿?l)S4SXXxÇ/YdС477gy="]K󃗗l" :,,L^<+Irr2eR98Jnrrr"7f$===DDD3+`gQF߾}.{zzUn)y=J*:u)qT>B88888A_ׯ+:6lpvv޴i>{xx;w%G}`bb"A(];˗/_utt;nQp6m*9X*Y:۶mv*,-"*aL"((6lxUM4]!gϞ]za@'L]ܙ3gےͳT~~~~͚5iM2k.J"CZ*D |Vd9;;;88@ڵ 9rD.ྸ˲eDꄄ@bos΢?IobX#SjwBvM0kFFF+۾V?s Y>@fϞ-mU 7׽{wZnD7Ⱦ%D[[[bŊJ%Cu!Sa۷oS]da'~WӚ5khLCC#33S2 1S0}?~SLg5Q,x gFٰaÔle X.\NCUE9rdVOkժ5pEAQQӦMl``O޽]ˏзo_aмc5cC~~~X#j֬cM۸,n{{{Pxm9}uݻ}tf`333|F9t\K*77v/?>ۭ^|>_bȵ;,()hm۶ީ dD&MpNz>f S%&&AAAE% hv;v,8̙3=z*āYOנA;wd{ A |qpܹi-ǔBIy ݰaѣ۴i#M`XpÇݻWTƍڑZ}]\\(() Pf/P| ȌKtC.]t֭JO,dϟwuu4ikJ6 ;wlnn>w\ Ś(34?|)X>؂ӱ6mJ1T(cO @qcPG P1,LW}zzsA#GΘ1Cab/^PX^2 tT\uɫvȑǏ>tf+++111JʲڑpQa p/ WM8,| ژ<<<2 %f\vfB|qpcb[޽{W7ͅq1cƴkNFχ򐔔ӹ@ݺuYIrr-駟JZ߻wopppppp02Ѿ}{,g"ժUk޼9rҥK/hժU N:A߰aɓ'SRRdFV;( =yK ''''z=ٳ BA58e<ʇKcATj C9j֬)dXxd(N(빀mܸqI&s_^Oܹ333#<..,c߿J(ԞcrU cQ(0@{ܹLJOO1brʓ'O¯CV~ttt =666{0'yyy|KBGOz~ym| 4h+h۶;ؘhX&G500pX;t@QԊ̝; CIo;;;a( hxrr2JI!6M z[n-R |||444ثo߾%_Hiŋ"׮]uta`3I vqy=zIOO$E:4ؔ<ٳG:$$w˖-x譃CVVVQ]2]TkԨ^ MJgÏf%K:6ͷn߾4ͯ^ //8:t( LʸpppppĔ)SL̜9S^gdd\~]h~Q/8s H͛7SRR?9@^BCt Y{ypwwƽrBbccUklll4|-~`ؗ+3PS|ytLcٙoʬs;w@ׅ]aa[ 22Pa\#tRP}qLO<122T iii5ˢSbWG F!ͭN:4tWׯ_GFFnٲeܸq;vv>IfTBCŊq8>88888?`ի@NQݻW dO !pA5wVҿĜap -&㣲2ӣGa;PةS'k)/ahM 0|p lAfyдiHʚ5kŔ޽ ׭[|H-SN:uƍ,t{bPɫv}??_x"yӼxPdذįt*v+++k׮QnjTvmӧcC'q=PWD䋒v O}}}餒E7o*Ba,Jq[|y0*Slْ+x=i֪n߾}_`bb2a `yt+VѮ9SRrUsuuov%~D9+GLϞ=<(b+UN|ߪU+fR0QN: L`ԯ_$/]ttPr"E |9ធ47+ƶt( ~X#Y@y 9< SLCmP`ra`D_%א̐۷9JKKKzaԔ.GX`jD_ɡb'P*7BQثˋwӧO-Z@͝;wJD3gΐQ1;=z4ȃ2nݺK|$15k?Byooo g* Ǐܹr 20BΟ? 1!uSۃKȤ{h\b sssȩ*xxxH?N'ݻ',y&E ...X IrjuƍQQQn_L_Y~~>f'کa!.C ɓ'cC&O21)mRh&oӦ 3g7j`Ҕ?OID!!!:?vrrbnUTٳgLLL%g}J& BKHߡ8ޔ 4l,B~5}Thzm l x\^|j ɢ@dyoNǎDF>|XrU`d1~ӧOHfffJJ۷)Gݻw/77Ttt'OJ9v33v~ /4pgϞ\L]<{m۶,J}Xӓ!J>Ąp;w0<(ކgAfǎ`oo_̖0iP:x600ʻ 9333.. Gb&''K$g={{!KȞM$">oW1=w0A!w/_2ٳWݫ1I(])xdmmmLXpViCPիeb׮]Tr1Zێ/M'@׬Y3gΜ1cP{/z0sL;vGFF;v';r>ԪUk֭=zD,4޽Klقݺuiq?? \hW$}˖-OX/֭i|Za9J4%LeL2A(P*~AÓ@-}Mpp0D O-ׯY=\fK,#iy=7և2 7gQw0A?><<<55ÇO0;w.** ˺i*P ZMVp,]/΂#_xԩSХm۶|3fDء!8nAAAh<##CQFx,&&&0sL*ի\Rr2lj(>z/~o*P혚2VީS'wwwhAE}jJ|O~:+i6C/"-B;vA^r & 7kL>FW=1!*2< I/!3u?ޘ~w|O88888$@Q+X3źQ2$>(<^???WWŋO4\@GGy>}wڨ/@-OnoovZ777pᐐ'OHLBCCImrt%_r2߼yx5?vXJQt"ݻy󦍍 Tn].R| 9Ц%8y@`y\dE:ur;B^^$c…d/LJ*ݺuh߻wHA}ԯ__B+ i0`E0`TZF^޼y<|0hˤIV:|pǂa}esnnnpp0fkJңaÆ-b:;;ӢLeutE\8aaaO|\qpppp1yd(BL}!t)W5ą 4h@I۷osΜ9ZC{( "++KGGQKtt4_((+P0L8|e 7 ?M0A޽KҺHrV7GTv}Z@x5]ժUR"*D 2AUO3!!h۶mOGC׭['%YԩSz988888@T;ܿ٠5LLL͛%*F8sLaq*0mV O3ȄhQzO+V> /5tP4iDnݺE^+Kd@J0aWSФ';v>>>о}{>29#.d/^\dI۶m:tuuuAQ Z8k߾}UQM^o8J>P Kf٘h9͛7}>''~LڧO7իWx=ʡGE@qK.U뢔OGG|[Zj"yCbRRRh91##C';;r]tIN`C"g'ѷt'w6m K8<<<..=Z:?~h Z|7o0N}ڵ---g͚NO2< ӣGxQG&\P^^^C#G>\Ϫ/^?~!.\/233 b P @AeѢE(aÆp;vlѢEZ*V>+cޗ/_)Lm n wQ3g˗D/^qQ?İh!ƀ2ס0E!G  ƌ\j… Mrjfffjjjnn ,ضm[XXXrrrש,۷o:ts\|OHӧ/ަMtsΕ1_jկrueOv~TTT>o#>>& ,+ORy :(eӏ?ZoRv{]R`w'6nXZttt89?&Gr9}F]K߿m۶...CE?22%K|i;un`v|*}Cx ͌IxAlll@@]Ϟ=uCԩSjmi߹s6<[jM3N뗻t钮nÆ U9R("88{`WZ9 '''܀ZGA"4WwsskԨ!\cWw|Dqppppd*,M=;t D˗/g{0}iC_iӦe6mZ_TPlwW|yk21cƂ pW7oi ?MHH T QÇQ+G.^mҤ }8͛rA*S5pBPPv3g{ "g@CCC>!ӯ:MLL,K͛AAA Ӈii{|ֳ?&X*UTv622=lڴL5ja{ne n푒R M>]`QPYa++|mB<+VÉN^_- ={Vsݺu,<޻wƍ,Y2uTӧOgK^~)ܔ)S_LqN_OWdIn ##Q:Z]9uT&L`u}vaJ($r˗/˯ pq`~0U|q!Lnn=<|PYĉz iaS*Lbx 6Iǎ+{6))i,&ORUӣo߾e HQGG޽{C86NsZ4jժ%3loox?5l$-O>g7nNq$GkVkdoݺU3g>}>bm۶1c`j=!vVp}YnnCƍ_W?S}eˊԱVDP:+++Ha}0KDR|Hp2e>~JUp*dz~}[ [nw277ٳ'ӧ ͏ܧO];VːDMS2Iļ@XZZ,s%&&Ξ=K.Ǐ ^ o|G888Aѐ:t(; Vě7o\|r( n  y\]]e###K,9|X]x1.:w\mÍ7%88888 ʠRҥKw e׮]ڵ6mŋ +0 a C[7nH:Rj aΝB NyPW/})w[>>~r&**JF>&9k\2cXA AA `=l;GJѲ?l۷ E{)$$W_n*4K `S W¨\~XKZZBwbŊszA7o,:vX[[[dCzdT.i>~H(Q>#2@ gHtsqq6JIKV@dq$ڵkT8[|p`}>}V\)'O%Srrݺu|Ǎ *ӆi%1h ~ҥ.\aq"y"Kָ7NʇY0ŋUV믿D?˫W?~8~XgV.\P[-E ǎ"e{I 58q"uppZD&۷o/:t ۭ[7>nذ֭+]Y2P<}t9dΝSFDt"lk^/ *W ͚5kРqEVVV@@v~jժE3|:0!N^ڷoÇDWvE%Hwޝqqq"=z4f̘`C:u R6C>~FW _|ۀ1s)J*հaCa٠ʕ+rW?}Hj.Jt|^0Tڕ+WN8JS__gϞ"ԕo޼I}||-ZԮ]e˖/}g2TƝ*(E 47N_Grgxb! 3C^zu>mڴ3[A AUV@$JKK={6fejՄDa׭[RJvvv "Ð~fff3dgddBL" Z<SN ڵ3_/'ە-hݻ0::ZNNN_չ|r7~Gܹ'.}͛WY#AAAvO/2 ifp> ư];uM3fU;Vb]]]===!߬Y3우1F`((WS58mݺuƍ3(-f… n2)ѣqi}uckkk(e]fM<#l6&&fԨQPk֬x0`P¯B*0@@cccy& jP֭[+;UgyExxezE,~Μ9.]O;/7@D\N&5~~R&vܒʘqܹs sZlnذ> >6_~ 9F#,qc^zʜ9 (C~<4W LḸ8̂lw333=z哤ٳg+K1===Rp{?nZQPܰ& "^Z #}}}Kׯ_Yp!C``]rrƏy'R1#(tCv*TshwI)b=$$Y$emm_y{{9cǎ`qǏkuuuqiI,\@f=77G 1+MfǎRV?\=z{L">>d*WjUaׯWĈ>S3QkڪÇWn߾}„ Z" lڴ}˖-: yٲe333W^~}iI8NthbZLx2O{\J%mĶm,--E|r֑3J.mee3K qF5_~rl"@988882-[F)yVVM䨡C$F>|xv[ |-t;w\~APLk֬Yb];;;jC+֥gee 4^ۓ3f0Eɓ'D2Q2N:7y4iaі .Ei@ h D&o݅%e -8&5j+888266SƇ~g;v$@GGK.3fS/OЭQC5kF`7o| ?eSX{EӦM1e| SJ s<5kna^%Asss׭[WfMQhnn~| qѣG Bƍ?)Q"b###I_S96r#j]zȑǏMh"]v ̀ߺu+##ɗ/_9޶mۣGʴ?xJ*se[ 2dMYOY=SQk֬A͛மF* ~֏BQ@aOe v777XJ۾}{;;;HLHtm~I|޽>srr(L/H9[[[b\#1_}W^-7=z`؃}||p%KLׯ!f`j `h̔߼y\a̘1ԩS {IjJ@-X6%\"Æ geeQAS˔)ȼWiBLV@dn10sΤ֩S˖-}5F񹹹=CCC/jnn. 2D G|?!sElkT@vzzxZE9s&hEmڴżbOǏ΃ӨnܸL12h>|޽{Dj6mdl*2u9p ժUnGm1Z7mc^t3fǏҥի5N+ҽo>u'O-w=zŋj&sn޼‚~NJ+z"n3 qFYai׸gϞ 4XK+))J*\R%JEAߦԁ@E?z=5^WY&S=[n-^@` g֭"Ҿ-hE;vď%5d111ׯL{} F+p"D0daR>Q4EbaaA-*WAdRX̶mTRh@bTKÇT{߾}ԇJ֪X\d U<{7oR {!X} mKKC#ȱEL}дھ}VO9&+H tf`MIIի{P `kk2</.a_/^D9s=LB`?s~h8mk##8O>|pR3fPv7l@[iiixQ݃Ϟ= g~srppppۀ7ohWO>*F.]mㆅ qidnLnv=*krժUx=@ … U5(nccM8Qx E_>݊8lj+< h}#FP´ivرzj{{}b=?~֭0nEJېi644@XӧOhU&sM2aK\B%kԨ44(co!]]]LC3gܳgϵk״%1V0AZ)>n߾maa䔟8CY;h{B` @>GQvU_3-ǧtŊ1cL]`fHE˂%:ՃC8::,d 9rD᷇FI])N~ƍƦM0Իu&3& Nڽ{wI:yիW/s{Vq /q.Q$ڵkK?ŋVx|r938Ԃ*?y$"z,Ǿ7AQu((VZbߦM)9}n5664h-E߻w?/?Z=SԨQP͝;wԩ ͛Ϟ=+_-̧0ݻiw(t}!Ȳ0MwƍUf`0."!!A&>FFF0Th#ϟ{#88X$=LLr qdee 2YcImܸ’߿4000 \8;45](!1E hʬDZ$Ο?pG%:^| u6k֬^z,,,00"޽{ɗ/_>}4ڡO:veSSSa S/^wޠ*H? vۘd;3#""HC<&%{8+8$''$ \#66M(?-עM͛/w=(f|x:*Đ NO 9hqi*% `H=zS9?}ꙙIc^$ќf~㍌`_rE]_{ne0ȰE;F?yd*%%29rٳgr()y 'vΜ9{nԨQ˖-x2,: ʔӥM \XMKKCbCxyy)$0w Z'7;lғP2):믿uёr8t׮][e-m_vK7*),X  |H;0Z=njctBOO  &Z qjal[_$}ޕaVF VpOK}QV1h r6y:t@7WWW>,&Nhڴi:!Ǚ3gؐ? aÆZ vIII8;a:aTc`aaagΜIXݹsgKƍ- h1P#iя!t( <.2Ξ=KwRV!@ 5.OHWO:%{)[,}lzTDo߾Ν㓃Hڵk? y䣮*:|ݧ߿?yʁ,_yn􌾾S|yJ&G*3/.EDTtF}ΦT`oo>})wB0-G4P&Zɡ6LHwwwr755eĥ&&&J ,$dʑ#GO>-r`FFFVH(9;;|NNÛ7o& 'FAx@~ ':k*èV`-9-ׯ_HHH>ݥ OȿQ㬧}aPׯYo߾*k(ׯݧ4n vڴi"׵kWM_mܸWM[n۷]R ZrL^ZX`QgΜBا{1Sj2נ0Gဪ;tb.߿O```vvvޘq2,p1CCC۷GJ uvY**_X1ƍr%ӆLOOM>]H!͏?^y(c-TrrxCSXHW:޽{g!? d5۷owE5k[fͳgϴZԯ_ fv>>; 0|G&Nh+E߾};wܾ}VZ5mڴA' L2d?o}q̘1 A ę%'ׯ__p7?E+3-[ |">Oz"GcܹFobb"x=C޽-,, 0!X.\U{5vٲeÃΚ5{QQQ* CBB#""hZr%qh& ƈ ާ88 00t.]*l=A޽{y'^zܸq3xB?!g 77=bccEl_˻C%x%\ӦMAO:20'N3[n'Li&ZM_g{⅟߈#h.?L˖-EJW3/ۥlCԝeaaA)Le?)ăz88hIMa`ëW@A7lagjjjN8j͚5!!!ϟONN~8ۥ<0DrN:xH';;<̙3jqa/SpNZ!|!EPɸ*t")enDDD~` ׇܬYƍ_ 6?hWUVM~TRlI:~g(*UԫWm۶VVV666D['D׮]Y~<:uꠛЦ$ cWLż>~HR4CPPΏ_vQStiͶ¹sp%JlR2 tE W\>^za/&>}tm%,0_}jj .P޽{\ٳS5i҄@n2ܿ^qOKEsss]Glbb"nmQ- s:iii/_!|_ 2zhfMaF{yy ppppp@C>b3l5j&˖PnI&mݺΎaSp ;(3o8-NGFF" q2Ue ?ǵ() ۅga@e:nA@0)WŋPūȽ)9r Vhh}ݺuhį)aQƃ0˔)#jղ/TujsPXvJY f͚YΨ 'i- 9IRJ?sǐ`O>A)n޼yո8AxF\… Tgo߾" XCbw͍x"wUa.D"'&N݇ɥD_ {ݳgOXX˗SSSa=yD:<0>KӦM! (pQAE͛GiKZ~k„ S }uօ~ 9tPu0SǏiz]m#TIY'Onw-]4zԇڰa× IIP<,,w6zBET`ԩFrSbHXs1!!cǎdi3Wv~E pE5p0 6DKj,XTa(E'թSՒVF7===--ҥKoƝcvhqt/yΝ£?22N{(Yʠ~,2 t_))) }˖-iaajZ;99YڵkO@!TNdR 9. U'er888ׯ_JGX?A-#ӓ9 [z͛Q U_<3ەh!CL*W j}DEEkԠ,f-`g(ָ\rP>@ d4hc}ڶmXBY9"Mj<[ mԨD[S܁9*ĦSSSXxE7oބ V ¾m6 ݷoh;z?H a㟧Oxƒ/.MIw^<~DDDjj*Lp'zzzPpB884kիWO0ݻف9-3yd0}J1f77pݸU ^-]]]www $. d֬Y,Q Ƣ&Cg1@\|G!|Hs),^XIl 0]\\J^ =`xS4Yɒ%1:AURH 7:O֭Ϝ9S?@Qp]Z7fhYCS8#GWX0d ` 2e apA݇39)؝;wƷsΥ0.it`{ƯfϞo Ǣ;PT)whZtTӧOKϺu뢛7:rssAl>#nmmSpWwwwˍ1B%!@kԨAY;hc[VV;ʕ+a/`;v ۶mU@ϋ/$8KVR@@stVj>9H-\g-B~~~߿ҥ k1@9s0p mԩ !8-ڽ{W z"wAfpppppО/b3EU.0PKX3 TnnnPsÇ777ԩSǎѣoh+WR{///` q`0$5|j-.eОq3f+F䥀 9쐲e˪t3eB aV6,--e%ʴ.r,10`]]]1Ř(7nܪU@Y+1~߾}m ˟H_~Cy;{,;{<),clPRYR ;w~ɓ'QP^=N/]3"rssx ΆΊK m5b``rƇ$aBunppppp|-dggC;uIр+cm zѣG))) 1xbnnn...?:DFFL{[DD%[ʀdFYnݕuʢ2׵hC2dZ&L rTNN"U(YB[-"ҪI GQW92erPFYXXWkB=5r |ё&~NX $R)JYS 佒?~L1Dkצ4#/b#˗j֬n+l9{ȈbէOcF ?Ɣr$6 ;M[IIIR<GcիW'Kڷo/6o;Y+VTgʕ+{ܿ-[ZZ۴i y8D3DC r|g uaq VI!J*ոqc)S@j߿?!!lajSjճgOP_i}OƓ'Z\C3FLΞ=[\9ZrJnnWnj3)h0?Ο?_|۷Ba8k֬EQ 3ZY9| Ņ $ݻ * +BX_@r cif͚*o:tVqTyԁRB-/eJįHJy[nZLMMA-##J*̷yܸq ]k ,&޶m["Bw7.L\;&O؜,ӧO1\]]L@K*%utt4ibii٥Ko۶ FidddRRңGhwK*UtB~0!c!;;իWp{n:tЩS@Zv?V\IxW:DQ%J*88*O>#kׯ/͛7kԨA1%ƍSNe, 4xb?''ٹ[n 4iի8q4^pJ" ![`Mi/_$OTVS$zyy+Ƅ<-3MW\IKK!`쫨( /^Xr%C`Q]ׯ+w^J([}y9ר}=hdJ-vvvdp؁1cЇL"ʣ*7o<`FO2e_-UpYMD֖|,YB_bӧOluzҥK?~#L]NP>}Lׅ҄Ұa~aFݻ*Szi2_VCz sB4&&&"!CR3^&dxggZy&ET]OOꜜ*85`>|HK/%!!޾O>۷_¢VZPf]R ,m2~/_.J1H ,X%@ff˗=s+V 4ATN*Ka,$a;:99-[%b)9[8"bŊ"l/\1@oiI/>ۻwoVZj+W$*t 7O|)>Tc:++ڵkwލ{7n%߳gOjj?Ç1\p y0,vXCP0-(:D?>D ;fmݛJ=z¯ܠH88 0Aܹل cǎʲe-[ߢ4]xxJ G ȑ#166 *( ?~eL! !*KaBx<"*%V|y[-%l@^w?׊ֻvc6iҤM6")t' ̓P_BŅYn3K20EN<)n˖-A?M6gfX=Z}T&c/a@)sӆ/XxEk׮SKp׽{we^gee%%T ;ظq#NvO!!!ϟ?gϞ}O3ܸqC}4 !PZ5|d98ܿ>+CC)BW_cFRSSD,8P"@ddYՂX^h}M@BBc73%}R cGAAAgҥ e<~6l9ڳP 9,?S˯lq#G?Xy`?}4?~ʪMd%ݻG-^T) iϕxJVN'r,SE Bc.]ȉXXyw駟(?h8'hݤݻw>9?8uꔇǤI+U$ SDR a8A0(GWׅ211{.nlرB<GQ@݅ II˦|) Nőe6p(s2+Cff] h&5~e(<({5X#F]]ݵkײ'OOMM byl`-V؎;BڵբE}a-Z&VVbBΎ)rPz^կ_?vJ1!r,9"5qLC bl}-ԭh/7nl``лw1cƌ=m۶K,˗/Ĭ_R FXcBA׿!>{,:3y0bԪUSSSyw>tWԴiSo`0nt߿{BBZX;J:;;wYJ*o '(d$'';QKiM2ipW&^̔w[nU˽ Zȑ#lvz]K(!^ H)7+ v UDDӧoݺy=EnʕcnaoE___c@ZrPhLO>UR`ԨQ#}{=/ *(SkXŋ你|E!^b:޽Y-idggC=z vNjkk;`KKK-[BZ֬Yl4FckYf8'H:Nnmm=bĈɓ' PR///L'N`" K}Pnݵk* ţYg+2Æ +e;f M ~hȑ Oի*URYDѣG'N}|0)J+l2444iU|!d RZ<_ Tę$#V屗.]tZl͛7Ŋ5kK%/޽{wpj!11qʕwIa$ڵ= rbp۶m* 8Sk³g`Nǧ>|X6nX^A;*K U6Ւ;u[ baytfσA9rDݍ* N qmk׮IaX2'O$/R7I*Ƿ )Vg;wE/_~OBX4Tqqq^:qQ_>g={dD8ϛWhą]rM64MX\ʁC3ꦡWk2,[>bUR2#<<ݵ[_W(hȤZ0BqRi2۩ ѧOtl޼y~V-䝫h\r4vXܰxׯ_N8Fmf̘N:A^+>͝;W+hg;;;ۂwrr󆅅=|֭[f 䙚ʨI&}ԩ_f߾}qk7o%ģ02/=9m۶&ccIe}q(SJ@yQkHwrrr]%==}̙=HE`ߐ,.lɚ7puuBf6 ^)(cgggMXb111ꎨ+WذZ`I }4X DNN/n.\SMF ŋ-$ 1lԢvO{鈈hڴ)]K.ݣ囃iCSN%MѣG}W?~$癔)sssa>IOOO_|9f %e!dѫy&9_ |aRXǎU HrC5PE$aXU]Ξ=K׮]VjGGǀBTtǎRrhZ*~ nڴ5'''xvvv2I|̙3+}ʒoXxyޙ"<<[..u֗;;;朌 ;G tǏDze8P!` Pd:U!=r>(L`nzxx@°-9Ә⫎ ;w2gx*Yrm<>T˗%TPRo0;$4nXPƍGwqqQ75.444dYfP_eCpfb޽{U r1>KEUnyʱf͚]^JŰϖHHH /zldvOF>)HkFFFRRRPP-''Tec_FZ5YĀG@r_vЪwww< T(cFR>6cǎ*h߾=ʜ~bŊSaF2J :|ԩ;vP+q{{{tJI)/^\@n)K.e0$T ^fAbŽ#Gƍk@H?{]bZ*66V5̥ļ  3+++ho"HD0 9[Qtp= cInC<ѣGɒɋG L_o)77WzYW^_QK8QXXXw^l:::ZD;~-$lUAFFF:::lrҗ@ # DĀݺui0sx˖-E2ދ={}ĉ`-T\AAA*WOV<Ν;0#scZZZ,K)н{wBk...ʒf7o7CΝ;WcYy$''ccJ)c@޿,00~x>V9Wc߿YfB^];t"ǏWZ%1͑ _nee]ɒ% 4f1cQ3Bcz FÇ J5J@ "GA=ЪW^ٮAa .]~;q£qŊ>)4ԭ[>}-J-Bek,2jrvvVw# )) ?-TVMo4f32fi88DaUϟGDD`;ل <==4O>/.\jذ!&z)3ԩfׯ߰a4 "y& .o{SJ.LJ* '( yD98888`.]BPٳZ(9[bTn‶Sݻw|E̷ 'O]=}tXٳ^pnn.$+}!|R<*Vؾ}ϙ3g֭ oߖ~i 溲o֬YSOOOo8PKɓ'`/|7#r;wȇ++J &~J*n D|Fx6b1f͚k׮ƍd RkKZNU$nQf͚ CBB ww:899G If(^VV 2ehWHBϨO>q^zM0*}H<+bټď 144T7A=ԩS8HC3f 6jΝSS A9*WL[DOUK~ڵjՒ'mҤ }utB)Z w(P @nC-@).=P܂&@ W{Yhӻʛ7wμ{9#0gw1k,(ƍWE:u:vrӦM'OE {ӧo߾=w܆ /I7rFYVl(Q"ܫZjr%ڵ믳gڶm[u8ξ6l߮];^2Xuf3=x&X́a*fB}zp؜M4;vXFFFoaÆ`r^۷o?I/Vd͚'76swYj>-ݻw!<[j%B'q9r`xٳ/^9ɧO0=<<$Ho>s 0K`̞=[-L 4GL4۪aRP P-YX!4 ;jJHHHW@0q Ym17W>>+Wa^/^\-WR$K q޼y̛q<O&͐!C)5`v0U7u7nܨ_Ȓ9_˖-rJ3pYNDDD` @O7/^yU9s ^`ʪmLU!2W*zߠ?\|yG&X>3g΄MZbsF6qM6PAP8;[l Ύ3@M}ftQEO<F[ݻ7ހ1 ݺukڴ)x+m6hL]6K.?~2+X ٢E ooo Ӡ^y5#%f#` x:.++q݊+ZxR>| .^ 'T?ɓ'͵`m 0vb?G>}d\W{L2:m޽Z>t> Ϟ= 3=zh͚5cVma~wBL˗o֬Y >dI98׼y:vXzuww.]' ϟ5h-yQg׮]rJ'pc˖-:`:tzΝȑ#۷}SN=j(s28eZ7S[AjԩSVW^ >1 ݺuk777LɥKb\@RsH"*eKH|q.\b:$ o,4Pӧ|ā)W_uY?--˗/'N(hNƀ Knݢ&9l$$$$ _ŋ@3o-,(//+W QDF"[IIN>r#Ў; /ܨZj-2G(YNdw:t(7l׮ogUZ++޽JgΜI$Ir`s==,j좖 nZrTW^5ŋ\L``2X%TL...xʊ&AgIT7B4gt M4nݚ?>0/b[m8cm夕SbB ųzΝmf1g o߾nnn&gԩS%RrJ*U CzʕP̎ B1qaaZ0u_-UyHF`(L|D:юv)GD?ydFܺc3gNyfq駱X-\pȑ_n ۶m ݡL>%K!Cok,OmڴO>{s`Q?@o8p+A}fR}/[ C gԿ;g 4̕+Pgʔ ɓ'cM!޽tE>1%`r\~ݪ&MBCXcoϞ={ҥ ${**Tg[sH͛7h\<{LL̚5kpB&M֯_JMYPyJ}ݝ2مD``VVZL޽{}9qQFkޒܓ5eOd^$IT?<44wϟ3,w^n뇳1@}) ?~L2˗/:ҍbŊ:n* 6WP'N(=G*W\z{}F10%KA˖-SQ*.D0y L ZO:e$3eʔ4iRn]reҤI5kĤlíj`X 0 :yd#깅Wzu???#5u놞p!޼ySNdW"~z|܁|5l eB[UẺ,+j[CHbbb-I{mR.]𭯯9q3C[n(C/X4'L`j3g>lBŋ,Y|С… LX͈9rлe˖ >.I$6čN27 7w)fzĘ1cK9wu~Э[Cö~_1_xS4jsŋ9ڞ>}`L1ܧO+WDFF.\oy5L݃ F\^=Pc.JpIaÆϿvZj\N>ݪU+Lm۶Gwuu8qZ.BʝHHo hXv)WDx&浟gϞm]ݷobŊ>|i'O u֕#GBBB"BxϟMnZ! KwUyѰquz݃?v˧^I^L*7cn iCmNk3C\_ MN[-t-y:9p5kQT (P@:Z o+wh`? 3v Uw{Iҥ$H`C+u1qɲ,)D߸qML@ӦM޽ ; 6>92%7Ξ=KrޭKpb"}.[tCZti̙E HKv}///fI4i@@N%K̓#!!!! 0zM*9׭[:&sd ۷O8y~m„ ui=>o RϚ5Qn*/;_v gm՘O>eɒWO)2+HNC1$kҤ 0W\}ү>@Ht:0'2/_a ڼy*UHz/^؀%8 b.]tt47qs[lׯ9C 1o 6mg̘ˑ)_&ۡC*".A1kԨD*Y(Q"tI3"22R$̚5Ixݻwl۶`e% ;A0aB gm*+\ٳWvm_~?عs%KN پ}{hʕ+WT ӧ=x=ؽ*fVq^1`LlxiӦ-n 6lR/GDD( A+u:vɒ% 9s&)^x Q=ZuI>u/8Zjr' ?k=%˖-_>ݣG˗2eJѳ0ΙԩS&O8q"޵ksyf=u~D< k1}pݻ;Lh1<\bŔīVZB1bT!ҝ;wTq3fTh۲eݻosNW\Q֡CZjAϐ!NmP K&jٵ7L 4ɺ⨀n: c'5k4x>a 2ϱNVxR۪TkÇ'NL>;rdJDDD4mڔ^y~U~t3)R.\ !mBCCy𾾾| Ǐ(QOX4T! aÆ,\6铐/^06JSؖq莭`nrUL#p1 㲶2z] u/V5a4bڵk\ѪYf{{v̨K*bȡ`.Uԩծ];m۪hvZUĿ-2a00<<<$IrukbݻĶGjժ7k1$s}c/^lԨCv޽;zʕ+.aOQ /(6x%-UǏΝ?p]֯_jժyM4 ֭͡[lܹsSD„ ?`K.֮/_BB@m)9xo\6p GXb ٳ… T*qo Ҫ1uT&~\P z߶K.1 ]1ZǏ`0C}K::$3fTիW[/_v9Vw܁ [Ą<'Mb3yE-]4H} Xfذa}iD GFFvD(6m /<3gĐ#S_ hYfOL2GUkÕ-)('NnX={6bͭ<Я)<<\9 g&|6m7d9㙝įuߡϟ6۲eK;<{&Mz1rș3g_^[ߪ G:wIF?FEEtkLaq5TYsҿhѢ4Y*]tJl :f, ΁@3L bAիW׈dСCCBB`[hJ+]>}4o޼TRamx:h%"""ƎK@Rzo's}oootLO;35qթS'  vJxh;K3aK/^VXY*sW>} TR&CsQ~}ՏI,ct10,ziUsjt2GDnJHHHI/]]]^~_׏5 lnѢE۶mLJi<XhO>Çu|=zP}K.u֭ X`zZtaÆ :7Zb@2 j^٠t`YQsL;c=w8mɒ%rܹnᰰUV OO߸q\=̙3+w޽5%^xqLR0az)[19q%Ua3fY޽5e˖'>Ŕן111 {Y|96|@`<` 93qy~"]СC^Trx~!44xۏ?ӯu6Þ4RbvD``  瞻HʚY}) >ۅ $faAsf9{Uo04hmt5YE>=? ڵkU_1EthBNxncЪ>Ne+L;vH6-iӦ9|ޝ>}o߾UTQ۲e"""j׮pŊ7{2*V">aÆ g̘}8Й3gwl{)y-$={6~J xh*f7VܸA;n8\H"СCMfcS$H mUn޼ -y۶mK"r BW./8puRv6s0ߓ:֭s $ 9 %K=!22=^7nܠ;|p -&_-:u ܵkV9FrA̚5VZo ,/^o޼C(9r ޽…Ƥ^~ upUV $R HAsAS[%$$$$ ՙAۼ2$?wͪ]ܑĄիaj{,Y^^^Z¥7n,ez9ҙX/y?ZŕW\Aa-o߾e=eP3dȠCO,0:EEE4/R΁!9,G`8tPk:<<v)=xӧcSgO>=tFnV00}"f mYfm4;v`f Lt#  @m󾃁Rٗ#G%QX1__ 6(k[h } yv "](gcLL 8yիW~~~6 z1H`m գWÔ)S[HBBBB]x:y.:UtSJ37g۷ ϝtw[\OL"E֬YSN͟??pSL'o۶M8ڴic2)}>}zI{fV2&QZM<9n?})S& %J~98~R+pƍs:ƳhBnݺݰaC\YfHۧ)|pww7H[n ֣G6իܝ%($Wq-Z;}cٲeB9.\x? {#\`| ?džpmܸ1EqY,Ciĉ%mIwÇ\rڴiZ MԷn:_|租~K?|\{!s/_wWۥKqzxxxśPXk8q@Y[F !+-:|pGs7_~W/沓7aE;#ɓ')[OOOE B7hQ_),8D4cǎ 6;vPRG7o… ޽{ 2kb+Zs]p$s cOBBBQ֭ҷsԨQ1ѯ_N>=Nsvإlٲ1ѣ*^֭ $JhŊr*^0઼j}տ&W{gZg˱cbŊACCC ߿L,Lw\0. 4285|̙^ҰaC~wrIuicԩq $)x"Ѫ\\\Y0z}AG a؆ ImE@p ZUSN^0[ 5UѬY3g\ƍYP"I%K 烯V^ y۷ĉ ߵ捿?0 s9W2di7oX rIHHH8Pd7 N%_&r1˗yիWJ*B;lٲE{hZ/^Q^4^#jժ.\˖-Νn'@ OQ+mܸ R@e%D(ʕ+#p~x̥߼ysgwӧį)KJѣGQYNB'RO*UJ, ֭[sBؤH^:m%$$$$?~ʃ@ 096l%"L*Ub&ɓ'Es:m3gN)}ȵ\rYUEӧ/nܸqҤI}#F dH"&/XwtuuGpVEjea^ő:u7dMĉ[҆ w%](e N *I48z:nb׸ϡװaC-… ͵ھ}{bCNH/7mڤpر:mK|ho޼qd=^z|Roooo%ڵkӏrLJckNU$pN۹sguLp|?0 mTœhٲ%'O6V;7+\J*%'߶F[ܟz!o lyS…q8ܹsqǪUK1nApÜTM: qw~7n =$$tĈPmڴ[2ͭI&;v߿@@:H}ƌڀ'N 6ӯ(0~"EwʯGRtx>>>u~V.hs/.~mİ0}bZKMR*Ucǎ1@fa6B&ҤID6#Sf[labijJbbbYvZ;Q%$9>agرÁaKذhԩS'N]vɅ7xÇ5j373dxΝfFxx6,, *%Jd12NBBBBݻMg|Xk13X{%SSf_VTXM:5.\F;cֆ &ɴ:p ^Ѓ0^NDJ]~]_̦+9$\gV~7 txժU|:IyqNǎR8b,IhѢFѱG$o֬ي+H8%KZ}iԨQ%JP9e;uwϱ /T`jA cзnҹWK$H Yf͑#GX@TPà >h .hUU?q4F +Ut%HKKIҳgOoѢN5k$ 2PgJ|)0>F*~Z̙KsXx/^0-ӉD)?wm]*nb9RN xUӹpG3),3iҤ'VT :}mNd;ILh0lʔ)lƌ^گ_5jXZ%Px<`ڵEb9$$$$ׯiTRc|2~pg`޽tS7i:u ?@۵dɒsg`S qm֬Y,XyP0Zӧ .d?c3rJn:: X@?@+k֬Pvz;sf͚۷okNttt4׋6M &Op Piպe6oތ)EbHGDcCdd$k (;CCCԫS\0S"~Y? J*\7rss7$$ش%Jʸ`5o\ ">~شiӺuV^}ch׋T9s&$j̙E/wv`-f`?1Vw@@ ҥKgqx8pY uL?3gl֭[*T(W\wСcQV-RK,`l(P [l'O(K~8E\X>X2+vbݕm݋x-hs2{w1|:ͫVsF-eEĉ\T)0λw3gNoL9U֥K'_d?XD^+V 6ד潌8M)Uu_Kڛ+ىWRwq/%$c_bp#G e4b(-[TF+'22GSF_yrWwJի:thܸU :4hQ0娓"-*h i&|e7壽\7c ~+ wBnW;uT|y]j6 Q"""6n8f̘f͚,XPD+&MF%AӦM:uő(,7a;V葲uVW׷X{ҤI8\rR\%듿{xx`o۶Bb[DL2$&;wza.^3+!FƍWti$ 'fgqAŋ+>|ȋ@Yd$I8A$$Q(RUISAT15uf+VX'*vҼys4RbE]n?޽;Խ~v;f~߿VZGXŋ娓[laIfVr$hZ4V~mlٲݻW-AɊ#Æ ÑJ*_nݺ"m͚5AOXXÇѽU8pdϟCVSҩ0aB|OOOP"AW|ߴ)Y@3qUPD`xQ6-Z@[@q3wtI:AD!Qc׮]{ʕWO ,@sNwRJ8Il+K@ 0]Kܤ{w6m`Xu;':}{Ŋ0 cazd[_{L) v渟Yѣ2_[6ǵgϞ<aE rUVbk=3gάʐ)!_51+ϘPd}z###+6e\ZVSކ;*W|I T CA{ >WB(!!!!a}z?S, Gd1JvK/aVQo޼YF)1x`): XYf'w5 (~9sDa=z)0M&OߺukeQ7n-Z #0) ϐؖH qiӦTs[:u*ZEEE}]|É`@ԪU sl|1}th*lڴr:uy`hl܊4R,q+đvIpڴi܌]@g͚E}jUJsD[cV,sntAKAׯgI۵k'rwo߾m'OS=kv , fS ԩ؜@~ѣGALGlҿF9rtn[7xyOOO"s[?~d\ߏYvs3gr BJz쩍p҂ ˓v>9s>}9swI kMYnݪAܹ3NT|pܹuօ.|!Ln߾^jX2 yHH,cAݻs) .]b~<([\H2#G\zW\$-hդIIB8Xɂ 7o^Fqa"UyZܼyw\|oaL6J*2ZyR ۷oQF#{2r撨HHHHHO?ƥ;4,@{5 Zҧaa`{1w>#&L0!LtJnnnz?Stt[?|:ԫWյB,<<1hYKSiϟߧO5j·jjذa`>^7Kٲem۶8?lœJҮ 1kg$ Zv-, LMM{7k ^zCݹsŜϟGUZ+>f[L~{=ܜ} C:={ d6mʞ˻ĉ &L6bn@svvϜb_I{U*mЭ`4Y}}}uqqc:*oӧO׭[׳gJ*AYimƦM7 JL/ѣ&[UX>>|@t9\Rc"`.\:(:$ L`XX`0'M$\%Kַo_gG*-ZXhQܹEKZla$=>%*{D|48{:ujԨѵkWpph(;(qA1լYלK3g`k;C , oR޽M'Ot\%߶m[+)ytt4TR I!m2f(Z0ɒmҤ 8Ut8uU5mƎ{AZ<B B]arP:Pa\iӦ==!^<ǟ:uʆ9#8tsw˜ a={6Cta3q#9Am[~l2ӣW^R|)05a=`[xXJJ$ȑ#a;v/_nժEйsg#~S~+V4^zrJ@)SCS;5ӦMA|oǜHBD+\Ӥ+U}}}Մ T+ ܠ=yn%q:䡄gG taժUSlJR"UŞQ4iw[#vE5:fCˈoCCCqoF_@Ǐ-ZԤICݿd xvڑ\hG*cw9%Jd$t#t^xeϞ% Yf1י6bqI L$f(}?;sګWjժֻwSY+**᯿Zb`lٲc[Xڂ\qz왏OfͬJݲeK\Ȑ!rJp/[nm߾=̳  Ĺ;=_>Ԩh(n GVu[rez)gqAXMR25k'!!!!UH,Ysl`@ɳU^d6d~=;-Zݻͭ,gMWn>1cFk֬ |֭Y W VZe͚UiK% '`i۞={˗/a-0 0䌧b~}È₏BĶ ;|GXteڴinN-\~Λ7ODUXʕ+2 i&`p=-z0? $׊+ /eUN"Ъknڴ*]tM,\8B{e5ycǎ)cGA~֫WnȣG`  I2z [40a?`KT4Ei:#^8󈈈2ثqV>|NsB_~1x{eFA%mޑ,CXT~,|w0\ĈvXI&80p g>^^^޸q:tcw͛7c@o߾z0Go>g8cc$IL>΍K2SDԩg̘A d} f$?I|?l۶͢kHÆ 7o޵k!C?>8G֭797^z#7o4n\ro߾+W6mOHVc̻w05k#Xjx4HK\\,+\M~*: ذaJJ~\ϱ;u$j4\U{&h6DZܹ0m2ܬY}-mooo&"O E ުU+(_N-Ǐa!hP̋9sEVT ZM6>>>Ç2eʂ `{g=s ,̙: ~Ei{)(Q"Y+!d| 6je42q~egq̘1$wbRN-^ilN T lk.>c*8p 5R[:ip6V\i.nȈPq$ 7pܩر#y:h>q;ϟ?ڵ+xULXSJJH',\P,Ki`5jhѢa;Z*$cM6Y{;6kf+E^BBB"@7rQ~bmV~L$Iƒy Ø%=nzJYje>|#M7lPt cL,G֊+ߎ'vz㭖,YL2 q ;n\̅*$ -FAT d7gK0dHmܼyS ٳ_HBRXhQ|5k,[0DU&ϟ?OСCb(1p֊j޼8uرwܩb\UEHHc@b̻:v޽iӦAkG Bʕ!ߠ!hX=i =LTX1m$$$$$Ç zY)ٱcGm&,[969C Xp ܾ}c/e )S8 tN?~<=L]&נNj/噔`2|̈}Zdd$L *,8/\вeK V6T̔޽;Žze@,)ƙM690,)I*Lv]c>9k]R{J׾}{:@H_Œ3`Fbnzeb Ç3UҦMK m.b1m fSӃC,X'L>݁/رcbb{yyݸqu+:Ǐ|9t&%$hO:|%!e˖BBv#tX奄\~ktqIEXѤ+ }Kʔ)@}V59sȡ._CLL tzΝ ,"hM5h`ʔ)Nq:tl$Ǐwuu0a:gΜ"(!!!!aŊ46l-+ C.]/QAaNn3**, vmd ҥ܅ˬ ѣ/+V"mڴ8m6&tID 0Tbw+H)Gs&pw,n $X+[l:aDL,H/q``CnjJ,~*'>8&e8^^=g={FPrJ7k.h4F` $MNp U5 *WF\Ξ=+ؘDL4o@ڵ4ѣGJv_P!F^Ú6VŅ)!yX ,\n݂ٳeMRxo6sP5kdF\^ЁO$ ;wn s*V(~qߩ>)Sl'j:W޽,gE2e,͛ra kC:Y&ҥ(wA˗/ģm6k{.]:rJHfXXX``,brΝX+>,*YGԩS򇖐'ڵkdj'O&"0qDo߾֭~8잛P5Gl~&gΜ 6]vׯQ~/s XEo8]DDŋ{Q@ Luh+Vu֠'Q5wf,A0פIQFE>H5Yd +U|˖-8o>"%8bPnɓ9CurӧOg<+x֮Dٍ\ɓ'~mڴяTQQQ2DBB_&?*W)5-8o}hxe(Ae$_9pL2qTig&ݡtfDz*/~80HH(Ç+=vxjb%KKHJJcrUVy{{O6 x %̘1)Pd"իWL ߴi *HxBUTsqADD5q> P ]Yc8e,Xb---%Bt`vnݺE^4}O9C!eN"ّ 2DFѓc=!tLaB(v Z)Jqh隫͛S51 U53WJbb"Խv߭[7o?W+++TreaV]ND5_@zK#ܡ1E`8q_͢{RPDyW #!ȑ#2c`ff) &лvZ~ќk} "Heʔg0 SmϟGI80a` !C/YYYS0i*oTXIړXR,ܾyQIIIB"X /Y}OMwxpŠiҹuQ 9ȅ;066V8ݺuC# ےqEL!(V(!,J>zVٞfCi=tppPz!5i_{dIOO[UXwBX߾}I1>yM8Q沄I=FQJmܹpByח<ׯԺVHǐEc"p][XXHcǎE 0 SbHЍ㧍!m۶B 0ïIJJʰaÔF֨jRfM77|mBKHH/hޏs}{֐hkk+_&IQ]].]ԭ[W\J*-Zݻ3vڥjB#,, qBZZݻwx||7S됐ׯϝ;G͂`5dCzzz*n9wިQlll ###{l2(@M8#?$EEаhucǎ@EC^ĐCihщ'N1hР;)7 D}-@w1112ѯo1Z?~G~DaK!**HUQOL_zEùu=cEׯ_Cx[xk>}9Uh֬&.y2oҤ$]0>hjUŅ^3ZWgE}u[.,CA;hР~%ܹ3vXY=kܸ-ZGVK.'O^nݹsԷzCCRMM1 =,,[0 SZ>886 >___Z9sf۷o!k׮M5oذ!>VITT1-WxK?_zۭ[7LQkkk V%/&3O͛7߼y111T1!Çi)'xj;+kBPڶm&ݻw  p94#ڵk7B|(j֭zB(ʕt2$D*=kΝ8ڨQ#Mrh\K.RPv{C[#*U$|//saÆ4g)qi>CyzzÎqbzaJ8w\NE UVuqqtXaFC >\7 IHΡ XjLMB͍\"{w}~'!%66V.Oǎ---MbJ.ݺuk}JinWM+Te+|rpA3gy1tǏG= !O(2ãNNkҠWB%a/L0 ÔL(>~ڹ]J$֘6l@JS)>WWW~UV![6mL˛6m͛7\ EgƍJfggGFFUIE:҂~\\酤$a=++KUOy0ZZZ5g ֡7nb79dд{Æ qI1!xƍf500;6m4Ȅ8}[X]tAi(N%ٮ8EGa]G .:ujϞ='M+>~}/_6DPn]4˗eٳo߾yZԾx~&Ξ=/0 0LN!!!)E)H4i[Iϟc,o۶-Du4wwwR2:W^,Po/پ!!/0@5j8F;tڕ^^^h(SS<ֽ{ }j}>SL0GFY}\\}t\ \FߋƌCӧO9o$ 0X)p!wvtt /({C6ljoҞ!lڵɔp]hNNܹsi{*T>0 0 S4#5KKˏ|k֬I6VʟGS;y[B x|7u.ת&MDRpڌ]^= I={̟?֯jjȑK. |:<1JСC<i'ԝwQB.]$GǯW miiiccs)uc1{{{[fN%Iw^Ϟ=Yy-S,Zjݺu_*aaBΝiu2e˖U[[MD@2sv a߾}q&Bnݺ(?xp ]4YXC ݻVVVWDDDk !f{ "XwEիW=]"!yv*Rfnn~ py|ϟ?ǥhm GQӃC^i#=>Mm)RO E2e GfhjtU(6:zA-y桄ŋNŋÇ)ED/=ڣG!j CЧP~aaJչ?yy:uB3f|)wI)A!$k||? ZfMׯgϞ-_2Ro׮8{{{CDݻ[ZZ:::RzB-PÇCBB =ppp(];wT;ͥ}-ۿ?PPm۶x4]hh*N4,5әFvF .,p!^^^Tݴi]ԩScƌ`?q:/իW0pɇTڅk?#l߾}„ G8ԭ[Wqşaa)^/^,Q 6(=ʟ,9:y8166ښo٧_q5W{%JY1iW-Sz\ݻKLLDe/zJ @UvSSGIxxm/"B;88n{ ؽ{$]U>1^"y `> 7=1::RRRRJ*UVyTǏƿ׋cy)A;ψ' Ѱa~O`aaH<(M }1qDA.կ_ڋ!YWՅ \/_\.ݾXX\S&dBlj~ YpKώSSS 2`vʕ wڥXTBBѣǍ'˖-Ľ{>yB_]eԩh+"ہKtr^J;VŋO8f!d^z­[VV^f/{lfB6Mwر_ ;JyH)ƼS,8'$$;vlʕ?48$IPDu}}xVFy333777ߏ15\]]A#FIpar ###UY3}j/^]V-J*9::J 5]gϞۻPrRm`?9,9ZV.\@S3f q@¦MP+<5I۷w.;##BKHHv͛7s7QҥK%陙Y4nX>Sd@C 26mjmmZXf{???333WWόbO!?Pu2_~]`+:|6d;wV9?0 0 1;%?b1EVyz'?җ{YÇfffM۶m֭ŋ9srt_ĢaÆh+I:%YikWS(dddC ={vqq9rHI-Z@ ;&GɧגaaaJӧOǔޞK,X dHOOiR+F+W*̀ Gł>lhh*gjja Uر㯏EsM8QP6mph˖-2SBLICͥ|'YYY[UqU0hP337oeaaJ&d=d޹sG@N7n,^ ڍ6]Ddvk]]]\zݺu… JB/>}𲝚[l޼y‹e˖y-OTX]XY\rEܪlRЋ/.Ljs>2mmmr^z;t@ $PlD ժUKez|+VdyXwq͛fԨQ9aa"!aeii) ޴iS$b+Sw,Lչbddd.oݸq{^jۂ Mfgggmm9w} iee%׽qFBR3gv*'!99jB]R0\]]цڵkfjٲ%I't<&<tyZ.|ewwwtg))777Ǎ/^,.رcY3 0 0_5k<83Y@+W˗2o۶۷oV]?!I|9EڱcG==tժUQFӦM7oL0AG^ ""/I9u8r%9sf||苓 7Zwuuի(| +W2mmmsssaa/֭[lI&b5i$ڌ*:-j׮][+Ν;L2tRڭZׯߔ)S-[pܹ[n%%%xBw;U?:;;###MMMeN9{l*U(&WLL !Û7oTYdAeJXf qc24ЅACU~jjjM|@ڵk2%32nLFM=zٳg).7idرB zaa%--ikkmGwrrRzn|||ׯ TǏϟ߽{<}ұ/^uI8elllbbbee|5lٲ`l׮ok (x̙38Tzu=_5qc2 0 0 @  oϘ1Cؓ?:w%>}x2~Ĉz255ȐQX멩 ߿##ABy///Pzzddd .]زe ڹSN233$<<\ӧ#57&0 0 0 N]]]D|===$n߾]e˖?~W\Y*III/^D/0aه_0Ν;'zF*D6 פIwQܹCJm(w2%:tyԩaaaGB(AC WFb?| y޸qcaO>47nUӋmllPEIO~mrr2 b.>ӧOϟ?i&>} 05ٻwo\\\oC6q vs9xSN8eRJAaaa8RZH<~8ۧȟyfW h'ON]tvvv|rll,zq{Μ9rp3*˗'_[[ˋ߁ÇڵkGgΜ)>55ãs@_0 0 0s*@"dx"K~+ggggii:ʕkݺ5[ndkSRRڷoO_-pu~ sss<޽{O<  ߼y}~.}իR 0 0 0(ݻ[ni}"**Jt X/&''S <cT^]دX"D֭[ϟ?ʡ}`` zzzB;Xpss#^!|bܓ?Yf?)SŸaaaaJcƌ˗wqq2d/5ۊaaaaJ&.]ܹ&(([aaaaJ,UVRJ:::zzzD 0 0 0LIց endstream endobj 10 0 obj << /Type /XObject /Subtype /Image /Width 1352 /Height 660 /BitsPerComponent 8 /ColorSpace /DeviceGray /Length 888 /Filter /FlateDecode >> stream x1 g D endstream endobj 17 0 obj << /Length 1194 /Filter /FlateDecode >> stream xڍVKo6WV K^ lnmDJr( 5ȏ݇[m#i6H*#TEy PG2Kl|3${->b`5gwoORɫJ[MTuwBketbEHNka;E~#TD(3Q@"kK8s@_Ua#)Gǣ< j<!c_x8:' bu8޷H}8uǙY6/Mi(kDi Rr m3U eܸ KM̟q(C#~fe!Eo:N$iPPjDv)e]{!xcGʵ íE$Uq aO̚<֧MFHT$Y.ȕ%JR;&1Lf JJ&ǡcj~tm`` G`Z>(릿2jT~R }) 36^cP}a^J6 ޓK;j_F<>z_1H*Alѱ+(xs̙ʓZCw 0s嗓ay-+p)סHU endstream endobj 11 0 obj << /Type /XObject /Subtype /Image /Width 1024 /Height 300 /BitsPerComponent 8 /ColorSpace /DeviceRGB /SMask 19 0 R /Length 282755 /Filter /FlateDecode >> stream x읋TչsV(@BBH!!!!d'LH {mգ[R}=xAZ5d&9ڗ$H>ٳgYy&ՕY(x+z_ڳ KD|6u/ }PQQQQ](((Q(%{?!tz!4?ѹx<.qF穨.4O {?&#RiV𜱁Á@Ec]n^9t,5$!EuɕFLEEEEE i~? ]{!*˥+"V > @~"> D .QQQQQ]w1!AhvAhЧ>\ R(eB4jG@ BBρ(S]Qn?/ |jDԛ[ sY}E1p\(p?*****%D; @yp gA*ru๐ )r' B|ˁ(S]Qn?/ ?wQ*s BpPXu&TT9RO[\5-gmü[,-lvxk럮|TTTTTT9x%*>xkyM^wMwl`%)SQQQQQ-Hj 4-Jh{I)WEk p3m>v]_g] Ol4YnLZx>h8t΅,Vk)Ex FUU?Saq>5v_x={^ެ/6q!ӛFXi?Zr,+<\F>ơex]ׂ ,;N[uy ^X.}l6rs@n?V']Ub|\ٲ~1hGW_G?ZFb-8XY?98lBp>_dh?hXO . G[6麺0Pa7| lvrVME'Uz?ycę9a#n{7]wC ׬NgCyF^GpΔ.5 -g~~suf-15C+}|,/V-Rp߽R|O&_[[zz V7EqACIvhl@ _v<\t90UƻwrzӰ1ϥKx_\H,6~ruy(X˻oxCX}nuJ`%dt>= >ކ |נqɲ1#~ZA<>r}X^|ytowyPV_(SQQQ}+߯q'Ⳳr?[&o(>s3̼_j{8MxM3?8Xq7n\OJlhe? oǐ0f ^[޼+7XaˀX`MaɊͪxwޗx@$p6޾y,dJ=VR^x?X 4;< no  0d28.ˇhx_ąZX SJ_3&@?P0t\c!G7xΓ[򀃢p^ːflnT=6 oy @`y`~qeܸ^q#nQ[8H~}wCq.W\?'R  Rskxf? mqɯ+5{Yna `Xz޵xe,6 BviMߐ{zXvt>Z^`ei[梓.!X7* ~"RͿ`A!rf j>J2j_ea!,o7`׃M OF44p﹯`VE?>=׃?l9 [z?v~^w^ Gԛ_6i3*?ZN`Pȥ{6͇?#Y[EY: CTTK_p}<Ux:~d+SQQQQQB@?^Eh"$.Y=S@K@àY{@x/9PQXCF#_Qb?Eg.@: u>vXtցK[A3VpT?9qkݝotwNu Iqx?****+K `!.GNG/pk9A@ g>#&}E}+LTTTTT5,Y%D 8/+Y 0Z.OTTTTT,$'HR}ʜTW n?#@|TXŤOEEEE]s٤ХS#RQ]vC]Rhꪗ)QQ]V-.(QQQN%sHEEz'hN******)}QQQ(>+PQQ].N4>uRQQQQQQQ}C']+ `TT(SQQQOEE3߽y-m^kiAj~Y^x卖xZWf{vmӌ} Zƍ} ~2xm"~ظVlUZBp@zY++h3L܂'MM(DWjE˯aЖ(@b _|Upb+ r͸VEkk,rk;z z+zmkUZ^״Iߒ-oTTn?.Kxp,,t>a||J)户r} / ZaE, R94< JSsx6܈f1;:}/ʫ:+}4v9yX)(eK,)i3oՍWXsK^,#3ЊXvad%dt \euҟ0<,Rg<1(4'UpmMRn7%܇< 5dg]!CS]+wMR"tN;M^"~K\t\?UnY+U \[4l}D{__Us#tۭ3b@|f^T}XCi%3adLP>C=5͊q*5Qt'YW'y6M!V}ɍS2 Sפ˛՘5X% KoVO}ɠK} $Ṣ!ш3iiK͕8_ rٛiOJ˥2KeL&d/5kxbj E+#`KD42&tf~Z%Mzi~vp5K +w5O9uGRhdr[^3mLPH,h6ԛ%u])A|9N~+7> N/-?A?xz1J;Ob&Qg~u3I/,1lV?2[f2ׇT A4,&M9nXP[ a>२|{_ 0 i\Dܓ\ /ok~&qjOX󆄟]Tq#3ۤ @t &,E$J?sS9v53A:LN%C@ .$jN _e_(ypn<ҧȘ3Y%9 2 FsḌ|DW#LƜw=W`'+E/Gi շ^/GAo} ,4kTq5< tD-ທGg5 8'UőN g] 10WzKN2kUiDw*WUVQ,3'C> }Ў-iA8 u~sNc׃.BCM} ɟd$3p^k8 q3+.cVl#֞ ii~/9GI5& ܱ~B>23$MboLn>M h>5{=Ϫ`ٖD!O8 f҅bΒۗO?~"LH5Fy=ദևX fub~S/_!wD_٩="*Øz, {D`@Y*1PC"2ٔlSɟP椻k?c4s;x]c)"X=-~L_Ӿa 2[^Z>_ ÿ:HӔ%0&zrDiLK+6mՌU& oRmhD yCHt4k럯Y4Ogy# zuAC?m|pp6K9 ߝ4&kyoN&@: k{Զvc/jsmL}l*o3k痒|A t>gbQV ,L i_Qܵ#3'aC#`UiMI,Ej0T,%2TX?KK_%RA 珓Q2q?aFi@W%aK_ЍXarPDMsӆ4LQPZ+)8<4SLO?:$GBO0+${XK-#pͅLD"{F1j\65 6۟N ]Ĝqg&F Ni]p(=B`xs۶=N[R1eӆ ~"$)3'^|^l}.& ɞPi^H :J *GS˔ҕQ %dr_NP;!uqM\~9US+K^7g#9lQ2뻽@DahY:Oid3 hrg &:_ .(۰ a-S}-SK;OgO ΐ_(;uYZHfϮ2xZhsFMZD q(Y$Zԥ^ȃ am4;r?Oa_xL?Y+A"]WQ?n9_AT(}8U\32pZ¾BFKA `MmZ ]wCy%$Uh4==*+3 9 Me 7XiTf2JDjMM#;id$S8dؤXgIm֕ax|Q.2X [$ ʒKƊZ7 NSڈL's\\V`74;2j\a< aٲG)SQ}TGɫ_xղhPob?9F/A2֏+ iUr)InA ?'0K'8)P-n#+I,*8GWq#dSPxS҉aNk77oɣmDJҲ= %m%JiSX5Bc+VeeL~xɵ?W)'vO , Q>ډ'd&{!$YG?yYVoz?yw9_W|.W]w923+NE53WLsY^ CBIb_ɾ6t}/fPSu'&/t!i"GR9!AGhz*c!J')N@#>Oq1On[UC NR](LHuCഀ4aWEJVAF<%?&¿  Gׇ᫸ |ߣp!z_q%=…<\>=ȇ}r_`%$Bˡ x\8<&pI.tBs\1&xi+C}G M裣hp <g@L>>Qpp}\ﻏ æ\plqt mɇiCo'#`%WE*'6Mbk"=TCO ͢3Llgyv.CG.,.Miq nwGb8v1Iq$KG{p څD NfRj N3 "OOaeQ.n$O65%.fd,h}F\6>i{fII^'@s??_5U =e*Od(.$3_=_3,1? Lʎ} Q.uAW H͢EYP<+N Ň چI q>,8QB 5o"&zٸ!_$pWѕeC;y?KN*[j\0q / Ya d\y*;羮]K4_ϟa"L<@552O8||gߕwġ:.f, > EXB{={as}jv8q#:mΨ7^1pvFqXqv;;|6'1}A!{f]#;vqaXjChj|Vgp9xy|p>3d\=V#u[8qu-@Q{Oc`w˰{lvs5촆+;B됭WKw5-:'o;{;o;53`992׿P^?[iԾǷhn/,:,k 0:v x@U?QH.n֣5 fއ.׌SzMh|&wgg Lv\s~ЈOeB /ltfǞ=z\I8V:=1*LDI$h T 0?"h?\ ? ၗ ~e:-CpakJ@5#1429H>"F>A!b674lc$C6 -E3A_Scljy/]4ey| 4 ? OdI4^Vb?M݄b)r^u/\d_qdFa6^, *M+Y 'My_1tg ƼT`aZ0)a?h23ǭq:H22㣨̥BS,z>R7.VjNT?J +T.E9̌|zؙ'|y;˳xh2% )f&?0s_w k쫽+1hсú;\7%{ջl,îۆ`[l;ٸytS0^KРwۆ]B-:hyG<μƒ{[\ysj:\?_Z|ͫ,ÕQ<&%hJ-S;Rq>讼4F E09$'k)Ԗ)#S4"R&q2CoGj)iX* -GG͐RrJ"<9>pqߺ˷?cT  0{U7 td7ui+zasW^1{{`U3RTqsu Z0V15y9tV%Q35m-WLp^ˆaiW588/0?`W\sO0SW`dASk?-hhx1! _Plc+gJT2hdDף)8>:(=L! zK 88$ޠ)ѱ0rL) p&YRQR5 yˑAAD1 4m8 ݪVUKjҲ2 xzboX԰A%I?cیTRg kXd&)F`WS`cE79o#h\0`VrZA,#Ǣ8-R3<7tld`BĔHi'R[#p>ъL]h b1A1V;"LNe/N:(pg)Gx2JZV3AԁRD ċ&H W#2b0׽؀_u/S{_5bI5gSqGb>gFb_%?LsųM52՗KJ֒l17ðncW?rSQ;z{1Z`Xxp P]: =uwOn5c ǫkG{Uq܀=ܽ^]3ji/GUͭ;E[uEGb^mP|Ȳ;F7c Mkm-(l 8biY1ڱkl͝BwT2޴'ix~x}cv._ nc{ :#w E]i+>>\wc?[X#&x1YCz he8@< h=:#Tʰ4_Þ']yl^`z:Vdyvg(FH`Q -NYed*+P33cb(938z_2 Db6l3(*: 'E{3oEG=٬h,LD88#@Ɔ g4y"SIT [@İi9G?o,&G_H}7#e {HbRxI!QU\TM.,ifgfhd8=^З7#OEqAi%G_;TUGІHO)Ui2u2]/vr?Y*EOLl1u 9_u/?Kct+rE ,#LLo (Fn)̴ϣˑox0~ba/mX ׼ N7<9t5( H?yz83{Ij?Dl}}}h¡y/EJӹQq]hWC{=Ԕ~/Rn|zrXleKkc' N]p7hMx1,QP`_2_<͞=*roXzM/GcЈ$yY7x;f"hcQ%g5r2͊]<^ xT(bY,Ž)g3ӊ؆WR sJ G1G$9EJ(=9EE89X.3-'s12JOa_Mc ńW"W ro:+aN O4g9;G8FIBo3d ?nS +钂UiQa+kWFbJR6G+^spzX-rt5\B~(X[bE|0/mo+CzoGhY&L^^8yf/OE%,-~~I `40g"F 5@~lZs*LJW6_hO~=왈8q`,{fݧ?^{e~!M~zrpA0p. 䛞4ح+CοD@l罨]3C=p}\Fx[hxVƓaZy9CM2D<[0~$h-'vdtB76;vֻkFl¢DEX[7=]u5#5Ցpg|п0Ҵ;&jnmib֦*ݷg@Mhͮ%_|bh{-̠2M-#֖ȁ=.Ȟ6@Qػ'lsw |sc}#>|;."^Q{{UcMѨ{SDI}.Sn/akOSz;7]۫F k![?b|XgPޖ[+ýC>w` }NHvif Q VFOA"3 ϖ"dž ?uŔ1["uĢӸ$ryrbLŒO|֞A@ISXNh/@2(!3c2Q#0:B8 G>8dp[Π 0 ^"8$g5,Ktt)dpDfRʤLe/mURr>pUQ5Db:ffMP%lhXR&W;UlS4l >nS+7OgΧc{ǹ-AfixwHa`@ hzfd7#fiƳPH#jPEjhh:56?+:@PR.4IE%Um") &E,&ԋD(U".T ǰU +x&dagp[_?ORFC?a?lzJ@` ?Y23bW mt./Y݌ׯסG2I3eeĸ.\1aO_p7=ǵ=`\&L<,J31e/=Z` d  cBQ+] 5 Bͷpt8Wf<@gO^րd}Ä#r77o~i[[[pm)h(<1Cfg~YXrp_cmn;apiXD>kUAOxQ[oUтc0/~7A_{{bTr?yc79ohަ#{{J*{;pw۴hU\6z`ҷiջAW/c]qWWè;q y`E|4Y;]xS`URXg2~-gJxk?~9̅^ ؞#0G#gbôDq(`xDl oQ $]GbTuDf$D\+awhJHHqr14 XK Cr30&RF >񎌳G2dCS)i `E_?sE "H$sPiV4SQ1bͤ\]hP#T'S 0QV#C'##:2(1SVrxtV "nXdxՀJ(@IP%'e0IR 8tO'R(@Œ<F9FR䃙g _؞ݧPS;.UרW}t~8$U/F1anx=t=-| @J|/G%9+=8;(ǘhs| k;w( wҔ>rʡ; 6Kջ|ݱM%coMCm .߆Xټ׷jњX E'߿aQO+w+<_zsuo`l޾8^[`GS >mdiW[ LN1gJ 殝%'{n5?ɘwL18})H{Swxߌž%q$$.ϊEQv܊)H0Z٬\'+suY%ā  7hċ"(!SPHJ5 :b/g Q\_]a!=bHl0O_od)Oj ir\@5k5A@_*? J&Ӭpq~翎i9!쁢!=CTM \Ҫ䯏AqfS'K._RWGєDI3{֤d`R LHd2pROqdXQu! 1 q\ӑQ#蒂U> ?(?,Vq9Z PjS}x5Uqo)}_j_O/x9vÓw.Yi25/"^tpO /Ϗ2@ >ece?_  02S ?Ke<DXn .8\ (e` GpJE ?^ud`'by$Etb ƚކDYI:,k &BӶjh'j[ vT_JTNe^1QTvdcMM-7ׇ J?uہ3 '5[cbk[_(,=:h-:jTTMNO'[EMP}ݺjø3ou2^?2+puyJ+9MvׁP<U<6–­Ǫ+#y[ n嗟ptn9G>tyۀE x[h:0hl^ ox=HE~pišֿ%){19=b0̘ H]|9eoO4 ?\Kц`)y9?x dOl`-h$ZY.:ߠ)_`@R>'{ϋ/͚*Ӿ2w bVLm"NG5t6Z+ĶE@3Sp_4qσ%.UpPZCu/T\jY*.& JhDID`W^Q^.YџѢw誎ؠḆx>S5;b6懒0Voﵐ\vM}S%K4 YO}ls8"vk d=ľ301g`NI 7 sP<#N-$:r"?3A5QEH{ P6F$.TUŌc9_ fpdP"W#W&F r1;3D 㩠^ZTd.̈*i㰎131M &n-\#{j],}*Z E~9Epbit[6p貴`_2j @9*ūUcY͌(DEkq+\?=h1 D“Ŕ@#} |kYIW +pj PL*=Jl}gBvA2,p)1~/1lkAA"K&ya:iXNՊ6dq4;U\Ul}&OѶce}gjfs-myc1uwgEޱ֦HXPD5yMNz}+xnxiXVylqk9Ժi}{c1tZm%ێTVFĮxSV+.iu->nkvW K*wy++]u3KƻGwd+zPK )(*Y;y@>`7oCw x?..0Z9ht'p.uϛYp̮zz}%H\}eX.>y @&LTf圊F'a193);qb%\OI†RH;T TLO`UPay|A8$K$( $u$g(se"W2{WៜJ]T_ gQW˗K%W=?YÊ܋=Ϝ5Zn#3޷Y( pOg7dY)kmټĊ 64qnc.(|~aY@ݑ6+*b'/{MʼnPO*6xVhib=wl4j c7(zo}Z]k voQI0hؿ|@p] [갣QX`ΑÞ@󞾊޲1ճfi=qRgS}Q9hط|#.Boxd|yi]OA5tc5<)'!w;I% vZ` W/M3/o!EȠ)ზS[Lk@ 0XnZLª꓈t Ś!3xmJ˜ fsR*"'Mr,,#U ᤂ$BvS$j5#y8)!Oi..(lii RW7s9Fp)`$O.yO&YxVn -OvC5H F4&Yލ(D:‡ቈq6:%rPϨlRmi !d,QMWN5QG"WO7+MH^^f ,AU%P93 dDuwژW/j_{$o^_;;JJ 1i |5 փOt2)5A<$ ߯d],8tY:yA.m=2?{ou Nt]oW]*:Lډv;~Vl)dIE @  Ab_.@ɖH%kDHIES9^\,t.{ D06A 9 Գfe-ּj֪䦍UW, mϽUg_x#jm_Yb]6<سonyyтMʗ~ jmъm%E[UVsQh7rzE >#)ia2RN*ϟb:RՙDN /yĨ(/K f O0S]D189|㜔 _B$5roEwJMx@}W~-f#DD[?&%vm?MMʪ’Y/$b~qI vg?]s|awAY/KO<'VArD`dS?;i|g.EI.-/51{Z0Wi y1#ӜG,>+ULH ޴ĵI %g1+#=Gp\!XHΌH1z2<yb'H{FAt(90EqA !/WTޘ /o_|Jq)?lJ}|k IOB~.R?ӽtS/:G_'k.{ UlVl4ZuluѦkn֛sz3hj0i6#FַѩorL&Icq[h4kVSgqUFSפ[|f`ÖƠhjpMn1جsa}FsCSjH8hXOa$ ӂq4D,Qe܀ jcVto >KU{_I!{\|oP;G|]/Oh0 L}o=n_X|wZ K4)|+Bv3+Тb`*nbIV3=e 3, d6NaU+̈"M@\2SG LMvK^r3yB:ibKЁR GX76Ñi#] \`#㱩ʣ/]x 3d ^{TO醃JG{{CIW~I7%=ځGΆc2e6-,O(?{ PK̒d). +7POR WS~=)~}ªWm@WC⨣ց6I(IаCs54bH5_'Nr{12 Px}¥&- DĂ8z ԤD9{#PM9Ok*o<>_!p; qȫA|G/:q45V3~RXID_ Ţ51k}SG-q_mV1+Q+>M2&(YYzA 1t>Ί?}`w>;Em=~|Ls..H57 )x8ɬ<| ,h= %L#ElII_nȑ+x,\_0f? [6vtK   ({cMd0ϐ?  L$PdCщG3/P_x?HYwュ᎞^;|T1HAio8G_5[QNi}/v@H>_ '=/u1[}VO (1*ڬZ@Q+uҥ|'KG=eUU~qWsvտ~yjw~7|k?xbƓלW87OOgw,9!3i{.םAzϙ6S SBҏL#r)D<|EC !ȓ2Ol:Χ 4gx.q#!6D9LO.G4ʷ)y AN Le r#!O|7F = ' D梃?X$Tןg|d}ёn7zn(};ro[Ϯ†x_9|v8@ѬBz&5)N)g%*@=wS`gZ=]W.+6.:̈ĝR ShI{ ޔ=I͙n4ALhbbb* @G(Hb1:)}-lq"_"/7yrY._r&L˜J*{oitxC t-|rܻ돿OWl8@xl\L؟S%`BtۙBb5 uI^OlzJ 2&yf{mmVmS.I F 3?@вΥ3>z{Ϸ*eQTku˪(*Mn=EEɧ^x,bkZU<?z5<_{0.u2jmjiЭX}}ޫ־ڠ 彔\媒ȳ+,)&.ש_ْA=ӀeH DNjqjQ"c.fDb Inkp2 +֒|UU; ^<Ԏ^?4O,t(۞(=lnL*t7!2J3$$i>y)Dy1eDIiFYRf#$ $ >yp҂yE>VUfGgW\K];R{?g}f[ Wf,r;55 )Gњ?>U%! Gfs{ nXco*ơoWl8(W[t)?X"e@RDMNJOF%#&w z/gel=/u6ƛɱ_p&篜!!uHFz ktgD[DŽ(s>[Q9 ,?Oy.]y/DE|orG6銾q[OyѢޯV].?7?c-`a'Xd^t1np#k"lB6ʌ6PqdE@M9D7ZfৈU4m4lQ(Wl 1%vVJ_=x?/,%AjÿyjǺk >`KןxtS>rO՝d﹭\{4%sg,KTDfE!K׬XLd v( 81@6R}X\I'I7$\L翐J[9値l/C1>=| #(# ?7]Uuaɖ3]9~^_[ԥМ仿O#/x<:5P_LiC@:nKd@ JJI;,ެtQ E{B Y(ÇާŠ/9~(~AbK>Ь$NA"% c/qNT eipjT'Ie\gK k?!G{gCIweM׾ƣ5[Ƌ+N|{9k$3U@ .ȴ() 25 l "y-!8I zoi0S??!7Rf>I:$cH/:Fqg{iH8uI1G-+M g**ZmfT׬O*W'WŤYT6o3[ˊje>ҷ:%[U~ğ [/)6lJC[z !ǃDhKDnl'T/m^@g,h!no@Yb̈́ OL~JXI^b]p>HHl:Y<Iw~ac?Uζyx=6t*1iۘ/_.e/ sk?/5SHb KY x/>i.j_,qGs씜 ٨>Րm,}9 i 9%YpT| Ь+[`IYPT^֭kZGȵĐZȑ͓N.' dm-#r^͛$xIXEtJK2|R&. >vvd*A_I1b&gX4b%@~Nf6!'M(L˞۟C|-d9sv)TƝ.f"-痘n#QJ 2͞oI6e[ ٜl=[%e\W / }s/2l@oۖ*md|6q 4Xzi1tg3\g%K*-=CXX*(2ság oBԿ ^r xd{o!>)JpE.dc`ȅ%n?;[_vR v9R6_|B"K4q6rgHC\gm+"'>%*.>%\sQp\ܕNqif+d@Τs8.å}BzD\~͜A¥[ ɝ214(SZhx]{';PX)?2 ߴr919l=Wx9wa?sgw3]H5wvœAk4~moKdiOxHi gl'ae1.3 /]XBP=\q\E2ɂONRВ&@^H)ԑiqR +}edx{Ӷ]"WyLɴΛJ?+3t|lO@Z`斤-$ɏm1EVQJ=H.JJ&s."^Vz' .WHSa*(?s\3J.ˋEg 2{JonZ66l_~-1!!0\?> C =:IBɀ(?Y.嫇3ʪm;+eϷ~zێIbo>غgWlϯl׵b1wL虰+Ewödz}oxGC f\'l>O[nSai 6EFbޱHN y%Q}8l\ 6 $ҡĚifj*1v7SNP; Nͅ\ Zc'rMKd '|q6xZo3mW>QlW5~A4aK&x|{idf{WY.+!m oxTߪq~TezWک6i~WfyWn~{wFci?N+}w]A{~وpMy |h@$xW?z'b7 ~ِӉ$ڑ,C~̏߫=Tu=>?l:M"$!棁 +m{͖:~]AyAMeomy޴I8?f5 EG1ja]GmPw8 014p"Da#cᠩ/>SN_{h3dL)65--fUfij0 MZW1 6LДq H]׏`ۮ+'S;`0\_tڳ@eJ|drnPMa\@Y@8#4/ۭD s8N{b6/:7[9?d]{n^^;Q7\+VUzKe'dʁ GKdT,SWtTU[d=+*Pi*zj%+}EJx)񔅘bS%1 lfR€K;4 kWϕ֟QzZ k&`j5ɀU&ŗ^qZ:\YY>UQvV٫R(+(Q:UQY-jl<֦MTu,WquIсm+>Y` yw%% |TWVklPyUY\ֽ~ZͅӝS\ -%q-,[[:`| rru\v MSGWR|p^Vykځ‚} a'^YvX8qThV:j KJbE| xCJa#V.jО-w7h^+A-2]=vmޣӝ3/a8 gu gxX0pk]_̗z:퀬(=BqAd՝ۍ>4K cF]P՞D;Z6JߎY B UV v? N;|kSOPc:zvwn/zW'U{ cѩ gV͡|Xp4CAp`sҷC?;ΐg׮݉*YWiIGBL7hϖuTSx1zBDqʎ= {ʶFלF+_Wׇfu2熢8eKoC׫~ RS/s uBDW++@TlHɒzR?rY._|ϥ3xeD}Wb=l<~x?,{/d̙{o#EK^{8*{;l ռokZ+8zgm 4 {}nvS@osۢMWMA$|'PW 7w{k>"nq rIΝ :/U8M#Vlk: Xz@jo3/~3=fO@/?H_f7o:/l VNJ_ x:P;X|& {װM}4l~My[nFL'՟+ *w"~gc1sO$: qmDg1ДZ݀9q=ZS}\&֟׊.9X1# `))o0^<^Zڡo8[^~ 57mXvU}sچyvcB4ķvBEMoqo>8ub/P?ٔZϭW6uaa>~u]򺳥_X/Қ%GK*{Uy`jG+9O qβF"|c e `wخlk0;y@ $h2~hxv-0ijtA=Ӡ0`80X0^weCxfE.6 KQwj |(lr^788>jcs-[#t0Yq:8nHS%v؉CB4:r 6ؼp#BHx}uO \i54],HPhO D5D۬Wf6m1py+,3f1_BCS_ q: vI ǂ4px"ZAu>XPlWzwPoySj5G7VW~jOz&2z7:h<sJÐ|/ "`;xW=mj/E":%| Ls#II0RZ.Gs0`aUfoН%y=Z[{;#'A͵lpZmQкw}'׮ w*F0JTup8  0k=8J}a Me'D'"9rY.*cb+~>7XۯVw_3d::ڸ?q^X=aSwP/t0P/T;yD"wވ~_CoΠO/W 6 TR5 *?'.DMk9P ;BONpH{^6\(v}7cb矿\jw:|~W]{T{ {|u{y}- 5E\"ACo|,j NE7"5Žqو<0h +k=amW;9AQ07t#Si~c:cvxnхu̡`pE3;޶@nfرV { l j agG<6 !@& +H@#$&h*[*_3[(LZ-*x?#~[wze0JA n 4.[1fe!"[ZB 'f@ # 8^ Xt1ȯXtυBqœa\ 87  M Le`g42 OGS(`eT,\V7QF#fh(Z0O4YB_nJw_릏CWt򶻸At0ldm!ψpqN ۼwH6 tm5 [A׭;7pXkns,ٯv ܶ6OqR*Z"-;41S]ۭ+%\z {f3, 2z`@QqF5<#khH=ɖQ<;\Ecx|: G"1jlw=F#1y6v3݌1=#qw(@|wZ48s\?]GZ9`q &iS& 3vUh){TT#fDz*jqmm\bVZRFlH I>#߸$Es/GsWg|{\b$=2ɯGK#\pt:Wt&MGӀ:LirGt G-omFg#Hbr%'"<2C\8>(Q@H^r3q2ϳ'{k^'$Qj4*DgDݷ^tbbmk8EfT}&jOi7PY=<8)C8>u]?qz̀bsRiiG2f\A0A7h_h"Ju*!j2ȁ3MVBls<8n#L$i8J`0F%d]n>zwDiYH):<;a'nMĐ9O!bsM@0nMpb[ev#8&xR[-#tNyuJu`/L93%jO-)9T(v˪ ïjB9(w‰'ĺ vUU|՞OYYv#3Dx%5~**܏2őJw('ըP|7aJէ՞--=[XEzM?NXi.ŕcC Luv֩p >U/:=QG=PߏZht PQ#u@o\`IpUY^{6.*VU{UMO|L]E[̂7WVUQYFFу>PQ~x E^O}\ 54Zu5 e/Nw pYࡠcZi'xLUh.>ӝA}b p KVѩR@ 7ma4$[l͋!׃@M2Aܳ8'#B6xďf`EG߻,d1Fђ1:~ ,عt,ϒS??P1Ĕ 8f奇 vWedG1Ңb?0N~r%E++:M!I<*Yn޸oML8g鑘Q~#1۸BރSԹYWZpԍ3g6$,\"1O-DӃ.ćȲPpKXaxu[ܺ-~n$".f~$%ɉeArKi6)k{1.낽0i.,`Uyy'^httUE'#KfE" W|b2O: ר\T) |yLL8;cF}@#_ |NUil.슙hM2$jٱsP2sphLL՝;`)'JK:j=YWׇ-[:1Wso)еtc':I89  8XT&;Fu'vbv絘qFя8#s»ݣ&̦ݞ1ol<tw]հnz&K삶c]l˸e1jn̒#/ĸYF5[IP.~~\N L ":Pevy"~% sE ,w`X,øHEbӸ3xn:h"lqb“~v۵&U:>[ذ4(X^n͖a_]w3|DD:!?\Pdߣ."_QpSxh:Pn9q-t5q mx681u 5Wy mC a~l<Og`#1рTByi 8HJĝD)q8|1.RK*$g/Squ.qͨmúG~WAljb[ӳ8aW< 8!eY/] [-dX$2 RvDb?U/Xe|U\ʜr}CIO ߭E۸Ǯ|v}Zqڂ.61❖7\O\r\kSAoo{bh!ۭd#v((>mTF 'E,-NZ7Jx{ț s?‰Đ/2EU/.I@J˥S \z W)jlL~  o;#dy6H7&BWU32À:{+u[=@ec6vq{HL`cFbޏEg++!]D8r1bVeOQVdm-hwqdʳ" `@ECl]?8@Є``|vN5$~@U{hE(b 8Ϣ[i!q^o;7Ǖ[=wZ\c[.(@)2FzMz)nN4jc0Rp6(ߌQi@Ż`\޳&“j6_7/MC~b2 k? ONis~\3D=Ɍb ̚Qyg+ȜwO t;'I|Bq̕%fs,/rJz D<'2 >m,x+{?P&{@s9P#Я]kqI# Uczx]nYSv8<%. !B&|j暬kFuƳ4WڋjR7TZ_(;\,(;QPq\>\nT xK7k{L}UBMYGQ}wꎿ?oW"xFY\`F,g.4 LV1T]ҎZ yqW8 +iϪT Wˎ5}ejM?NP%p< z۫՞Q15Qq gӞWVQ/roտ7#t+'*Ep^uep:?G.4*S8 pLNx8 d&n- ݃D`.q%~1 O>{:&%$lAA8\vcmqINM!֌2½BSC$[>Y\*)9Do͖l9C1^ a g6ntUU'Z}js>|6ozu(ҏb[|E#痴f0ʎu`TyѨQlh8K֡J: AybKeݍ Je8(wɪThE 8X tʊ zٲCx.0BzUNƉc>ST0Z8,+aah dW_ܔ{ssܬJ3F/UT1/j4%J՝½-FTl[YٹSUEhlОF42S\Lּ$du-YFC?Qtii9cDBy~ou:r|$"`Ejs?ۧҞ[#YvҮyb^صakxciSϽOW${~큗6v+8ݫ>]ٕ|'d6&ڧ ,dixt %4h} ]&|]9t.--0rd ]L0 M@6 ;. =/yJ! +`vI@ǦlnEƬ*dp, ɹdrr1?E3AP㾰Zް+bX{CqG_u1t_G#-烎s`804 .[Q!<0 G3:,LFPx8Bd Q̀LJkE1^_qu+[Q;= ;tzskTU( +*>˺i y˺'~}mMg)FzIl!XT~eK,b _͗XuKs107Aef*Ww^wi'Z+-7ɢ"_`"x1LoٮDD PqvY)jޏS0ς\~!L~Vd%^z -^=1T~=OsOzˤqyL; $1h*k +;L*DaTpm%DCPgR"n㌏FBI5JOQ3#[iyuw懈!ѸG_SK0 ̒CXecʓuU*̀,d,4naU|>Ngqw,Ztak|[lS:eiR Vѳ) +FUIeLg!B#ڸ_.嫂Qg*6RbLNgت"qJѳ-pWhxE2Wퟎ0#9)& "^JɕzHd.Ząۊ>2' ' Ф?t?l ޣvax#wnbEb7Un anҙ%K4 jp{n`#)Dg|c#8pDa6 aR87Mڜu%Z8LPv6ݷ[ܣeEky_ly6g=g|*2x a§(XkV"?TE򟒞4L0 ٮaL[Z0:#H/xX|8t6( "BC.ʢ')4FF%k H;tD|E3kh# iiEucFmQ_YS8?gҌ⋖񗜙OaMגV" ;IBUafןGC57tjP+XB}&%u>fs(*L):${0ʨp `4 5hp"ljUkZmͦKs\1e_qtLdऋ <$+9mܨkg~S.oQ6|ǁgRB=KjzɊ)i@="ip8l՟yR$abDkH"Iz WK(X^q66hT>GghO]$ԋϢbRg ާV1/$[t|rDg3B:-[ØI|I=!>NrOaJJl{_">431ZW}OHԴZF851^Ae!)g&vR]}tӲrY._1i9Gy 2G<}+a T3їf(T Nt^ o!h0DX,WB!BbnQ$k!\v4Ju9Q֜}gIt 1!^8J@矚$jPc&,4c_5Vs˻3G՗1$vAAe6],ڼ$TUAm;č_{`TA$v$U8T%W&bTE&; nڦCMxIAE ;)' ʪf"Z38?;N^]iߗ)(织:9h^BiӧRHWjkO=Q --dPJvw^wēK$Zwtaq2dn\Lhۄ?G+)-jƙ-WBU0OS)0D>⊛HWKdfpc"WU|@+>+/?ӝݴnZ} eŇXYI\\ʕe }$W@cj*4ZUZځ6W#vS%uk>Al?FS0)__nBуDŽދnO&J]Uej^t,>mI񿼻!?dUG++j YW`'IOdx?a}ImNcA^ Q4՞.(Q\_ˏvc?'=64mX1U\(b?W[XU>019TRr^Z)YuBٳӒ.X~ޮr@`ya>9Vw#"Rz곲ÀQf99֯ fstJ#,owØ0/P^k7Rw뽋l*.д{yWiBǾ"~̷~\:VK{)=I>K٤a1IlpEkcnMm$.5BVGn`jaJ}1 !QvA|NucsvfflFG)i:.p#"߉o.AiC .G`k= nY\|ՀL6m͈e!8LNDU}70M3uGɎDMuwI`NvVyqh EVT8 *b̟ *cyaD+{4ZI]=,yn0*T;d+榫jEfPoTT]W?TX&;||Fu&\YliX9͔W CFpEuFwJ֛W|tOe y_ Z")]OpR5*O’2/L<$iZE)oz n9[F|rT 8)nD75!lًn3DV=N[8'0GpEy?aJ3TTOfJ)4{B +JBR QZD.L$Gd*h]YV`ehvfE(Cx"[OIOuSlbTDh2`q9Ӹ 1GCT6NB,)54476fcWA;0MMFHnkH}sY-ӒcO:PR3%_IL@*PwkE#ҹݷϨ/aUWuݮ D '(Q-,x_S؁ߢMLhQ2!Pzӭ9]֢()ItzIi/_kK= "YQQUW ^V9]g}ީc?ƫD1ΒEC99UKh>ΐ{e,3!خi :d f4|81%e.Hz&I*,@W}l'}"nqp6f"͐KI*BGdW&ܮq7"-Y,,a"G:*vT`pjjdwc4prJ04g2yOhzVw-/_LPؑ^LRĜmtcw̱Z߷>+mHiʴ&O$ `}qϱZ?Z]}>%zqӦ/JQ/>TB˿y?TU4S^o?-_E=$:FYuۆЗ0}zFXS]6{MKV0ƣBM'mQwyx!oy ugi6CW3Uv铧t+úl|OgS6pJnjgl0[{nU-<5Ʀpɛ'C TNIZ(~Qa:dLDsn -^o$d4WP!dɼH/ <>25.25=xqaO˼x 8xؤb/cDReXu2}|Xoi˲۬&p_9 H{8t rI>(qw #qh(B :!ΏlVԫd SBeQ^]f赠GӮPrq@Et)oxRQDuą **xZ'顆ީaVٯ7#;|@oE`_oKz ϩ?G3#i cW*88xmyA}|s/^3q{n1_ƢKWvv!1'^x9? RR9WEWZ@\ҚL 0l 0"3!(F2F _ݣU3Ni!{5џy~w-v$tCWv{KgwQa|lp:Gci`>Zoz}d0xC*2Ҭ{`Nd49#DcHn]aP.;]fˠt'h"ޛ$m` (^?0g4cdT,=pz81Bq "i2r> n;Ui#b'v6u㾅Q#7Kd꾅@R)͐.DVмnsg )6})cG_1? _LMkzuVeRZD~xv ɛۆ>w85l"PB6-\ʼS$" F DiۨQͬpb2 i It u2HQSyxK'xζOJe4w!TA"DEtO6ߜXg B{9 3fFA&74]V(PTt&YZ;l쒩ْL@j>l6g}|n7L)(=ʍLij&6\+yLdzL/࿍?s}/7DK^4^NuwQ /R&MFIz}/7*0s<1N˚[S JsjyKޫyXWK,2/j4@UNie 70^SKx$ @}t1eQvB2E٬l 6WkP(xeaKຼ l4sb!fc7bI@;\(G:+u4O\3Q^F?^tɻiR~'V〶 饔d?1F/=hG>_%A>N$#w* m# 'ս꫅(Z9ΖLgF`6Xlʑ'.˚ךO馧M[Gq9RY&+8`Ʉ2IySP B9̴fuviRT>USa&pN?MORG*-S"D<\:&>x]ӺH(1yz<ڑ,L9#~0vr/c%*9k,E]:AI0@.bÑ~η~tr*(u:]wv9%MaNׅgAY|:K}> 6Hrt-|<\|gfbm2BK/c'&Zj;,4Cxq$^'Zsz%lNE bz'q >xX}6bL235f!tI Xy %ظt d#F69Q!b ^iQ|=n$$k4) '7(6?LTWaGױfJiAuYHgl|uMjaW׊U/Ǫ/v=}Er.-IewN}lQs@ħt%yشk:Y[ ]Gsx(bu20P~yUdg;_2;ø1VT4"ڤDD q*JVz?VMpr3Cd;ΗEb>BQr/NRM3Tȫ:2eL;bkܒ?~PYԥœ:w%_۟uѪW{\NgOT۾^A^**#}D-H @+"x:qU)Άb 9Z,Ϣ/eFz S*0 6Yns7)Aifz$R*\/)ϝ4{;iٓc‡m3|eubD_߸]:2C A_%&'&`1(ZzJ]<@D-;)I5l;r =]Pn]I0 Bw h_ 26kiʬxnqC*cR 0$hZ WUޏF܎HbUAsӺt$KRDӜa"КUwV.n{VcoFw(% r33?4(ܜ gO|Ӹ= ?eD34R~ۉzZ"A~y 7(X#jϲm<)~n Ў񢦖r*hy.eN(h"JN2(m45$ud 4p~d0'{[kh?uȆ~_ӥUPtg,|?PT-zsY@nr grXBxOz{/%]JK'xɲIAAgX㰍h ;91k'tX#XG6j[,0Qh=qn!@p.7Ra*u07炦ֲ p;%A*^wd]C?7=-;L~ z}/ BYe1C}i7؆pNgl&a vt< &YN|i7SkxF:7hUJN{l‘8'\GcfSjb ޶'F:\GitoRb"5$U$cD,p^ð-2DrܘAئ;ō7pNKyW %}fM'fp3N#HtXTy6I*tha=OhD]DMcqÀ(|%͚Yߨ[uN_Q7b''{g1Jida4?0Cq|3f0A$Rv0 j,baJl#6r.B`ޠKӠ6(i&2#ZS1w*r{9c!""vCov'>UaS#av.P ɉzT>u4q DֱOY;'_;~yMY݇r. ~3Ѭ.{P?恏 9k:ɺz8Xn盇.[guU? m6ʤIL̬Oےt>tfF& K7G^o"zܷ*"wb?&CҼc}~@tu*]$KT{k3dYBTn;^QZs7juVi¥H`˺"#H$yF&A0c@taLB qD(}ۇq ~QLȫEEY6\p%)x8H4A HII$'H/E"-YDgکΐI44?eEf]DAQ./(sw4N샜x{z'QDޠ~**hѧ*Z@`XpH7V sq;ǹ6T /WV6Qnkh׸7pmDqnfB@pT <Y_oKniIR6RfkV$$ ޣJy~*-}dgY12SHR65OlyZP~_)T !j@PV]imB[͹t+@ۣ9p BuT}uq.qNDNyz?QP{ui0N(-w;q#HD]@f4iGH|K:z-c  $DfwiG_ͣV;c:u__#<z#G>[B?f 5N.o 8{pfg# [~m\\|Ve&%$ibfh)H$6bK к3hC$AQ#I+7x\N{eNH%!_v1*@2hc.c^& l߯m彘gT۫O֮!f[9 +u+-75 YWa6eNuE؇*AZ3I)" dU%e-Ը*ńH9V}ƴja"4hi-ڇe"#_hh V\4)BQï-7 Z=Qqz~U2Bp">]|lA>TEh%M-tŎ@ #8)6e~5? {g'=$L $W 4IJhVRRx(5 M '7>5$(TS4vY ZzCUxpD4yE#HD>1(Xtj缆razϦC+&xĿ&S-K;]͇ }I=5,S<3A#!,ܺsHݓ\y-)͇#i>wRLKLIE:C$']Ke*l ]=p/~ WƦ0m.x~Js̵|Hyf{eEz^%O,%^C 1DEsC.e6R/'<[U=l/J GXnv^*^3gC$KKJ4PWc2RUqOubg=!=r ,=_՞n&k5tb_,1^u\{~.';Be;poKQî\1cN`,T_j:vQ ާԇ34NHF+**t2,( VÈCS0 mcsyys v=q?!" ;c?5?2 H9|cj}Ds2E[F>&?]Y4F*Hjbe~|.-+&ӂ;̀ aVgOo*CW\0тÇ>2MڂμGNÊ^u/HXe1Cr2NAh^R҃s/i5*' -Ci-yjJ<0O+\W]~{rź_y(<裲?هQEw1s✞d.6P@yxۜsl'KWJM:B|RJaqr񬳅W!N\H~(DTǘhEEG| Hó~%MJ{)yjy;هNPM-retP ?P(U&iBQ}klhb3eaF3l)q%*]Pbv#P HҦb8l!5!`NYKn% HQ"[Y'^i&NYIM]s"ΉwsAgx5*!ż'̈h]H55Fg)N'^%vb}֜jlYm'C#s>E+Iۙ;q)zݟ&{TM߃؅"7Jv}.S۳:S.I6OT۫f/5FiFY>TCBNm$^g#6H)B ?@x̆#wПs XtJU%:U#碑E/u&ҹLxK0GD Χ6)* +wpz c^ϔ6u;1e ]wO #I鎋Q8z!1­ẑ6~q"_fra.,.6L A7a1 CmCzL \-s/vI{]~Nz{Rϧ)}ᯨظδç)zxZox@KOH?sx0+ )y>eKCӯ%e3ʟy=4r';u3?TӼN^BS&klZ}TWy⍫)sX> I! 63#$OB,Ȣmk+Vxj?{ N쒼Y[2OO=eDvH)R~uO; B5Ap-SJA"J+q&:S"_U/?],}c@UU8!E#Hd̛Vme%@iDyJ'${q}|`(4P&h+Pw݀WV{`EPuWڌL_#h R.)mǑ%$ʿ ww%%=@ n*.u?qOqo|p?4QJSZ<͋~ɎtO}Gf)&ǙvO2w >_~H^Z(YOqiG兆!C%8Dhua<iëTR88W{@?I7K5˒>OB[XrN:ӁM ,l@5ׄI؈ -3my"MM@zUUl@pYK"[R)Q]nOi?n rXՑdKBN1v:{tҠxiŇF=q@x!>2(-PWU=fbQ:4]ǎ؆@pFW|=" tjT^,*fކ6RPga ?o0. 7|,}F쓅1{ OXʆ::礒#GY5<=HbG+!G+.IhG.#ѥphQ*aV "P^.%FҧEǵP9Cʸ=IQhn2^ yw~#khεy=;g~3_}W=sX}|.ǿ{?x3cB&f5Ѻ Gx~Al& x@B-n1:o<0_sHD7Dl@?qK>$:NC`[00ǦYOA=#Wu?\~[U%q s{ٖxS{][7fuyBAC1%L: t[h&12 NvS2L<| iY9EÅg;iPe+N2@#{2r WαFfc'wf,ؾpK# ? n ?Uuw~no}>u_>)/!d/-]{RP1pEwo,8h/|3.hD//8} 7Ee3&0 Ýl:mw4J\:{\22Le="/#`wW}ju 2z VV.B`{8zL&ڂp{H{8_ݮquj<+RuTϪB\4jxUAUodu``Qj.LBucHh6w ?|6}G]Nh4F. eGS, BuyNe.w6Dq=%l x#1"*BMaJ:?[S"zOWe(ᴏ䝸\!GܓԺ^ySeCH-l4WijS*(ȉ;A¤C EdeUW*v1Nn,=@>-J4;MN)(.)"2%i372D#puPQt'N\`l=f^eP ,8Ӂz :* OA~#Ԅ1Kб{'Eց@ F7b"̔>Uem:} N_X7>R\|1O4vMYJzbP}CI7YOVFZub8rGDIMbcbl5i'p#<0Ie]Hd@-^eRuj" */%~Kb-PJ/jW(𿨇 9M bj#HZ*T?t{"Ui5"#*DIG"ޔXBpXQP=̃z]w~eRʻDJ)ID4@[||cg ;nj2Qd+* QUZbEC#&ܤө/TlFWZ:lJ!xquɹ`5ip-AtLcG$na֐i%JM^ ,d3Yid@җf!j 2ϕ\ t|{|{; n5Ւ:$)c֔!쳶`u=m7dM(xT<D~,¨'vزi_7K,-堚+/8E+@ 3]z ]k*YSfPVzyea(^&%g)ƏF`O-'IڄӼ"Bnh)tJdRB XpQPxe$X6m:O&@4ؒ /"0F"tˆfdIo"BWD ozʦJ 7p@«KMmȂ9'eOwW뉼+ FGp~Nil%0M8I^oWdBsr.z=Sː,9rGءh^XxUEa"X,z}# ౳aC⺪˜S6 (?&}8^ʸ6@?61cW:F9E]2rC浦e_qkjq.$Qͦ\ʩg\>ZsFm#d1FǕe_İ%jY@,QE6D oZh$hPbH4?(ӶxN[KO>lX(j\MJYOO=\UJOn ͼ+%Q4E{>+߫cBdQ#x@>4O`Ȯ\l"ېP,-v50Ϡ.Ѳ of0Π~b,ƈ I*(``m8L$|@x/ 18Q[%A~)[d*(Z d2}&T=ȄHŔeZDbiJH G^"qHg, a+Ħ~xUvdny4Dx퀬h}D@z 4lË^ ũXe"p+ZkGI:>NXW'IZۀ7F~KxE(Y&=_qh쟪BJ6vFs1:߹}JKʚ?EBM1Ҵqs.띔 _CTǑF[řLĢ!!ʑ{(^)_Ѯ,e^DUl/(ly‚<%c@N9獦>}𡏬6gïo =G&@Ԉ1>h">_(S #G>FcAcq!Cɍ#?N4oʦu08\c9>J P hw;i .JNʥN]n ysl)j%h$<W2ONdHPf";P]iՔM펂D*e]6E+C&x "#HǞtϤ(xJekEnKYRB?S^o9Q{N x0,C>]љ8YӘz G$>3>z7\. aʙJ$QJ3^ɷE%ma^f4zfh$=5QW Σ@m揰lmž65I N>tI&bVtɵXks@ajZ#e*P_BZEW{ԘƋ"Wg׫^y3=|4薵.+*^̨򮠑3^>% Gby'UPI#:xhNƗvJ}(v4 v2 VZXo4%>cvJeRSZc;мj#ؤCvQl&BUsN߈74K-Fb5zJ?SV7l3 ]Ca2Dx""MUxI`!YKŷEgG]:hw$5Xjɛ'd(D;Pʄp*n>eX./LMW7*I ?w2Tg,e] z%=I{g]'x>/2K ~=e|l-O@%57 o\ʲ,]3%[L4Rؐ:)Z5L &ePSWyq6? -[`\˿NYXЮ+8lCcX/nw X< enXmc%%6neje|yG{ h`ۼuVsɅe)μ"Ƒe{%8b%v73ħ3[c~ʽpag56w@Ȁv`vZ%|U;"xjT $0$YwȾ1~pbfo[QPUDQBvɴ`$SZGC|5hA$5ᖞ$'VK={¤ĖP/ BR𹶖(DPfz#IQR[UEuAȑQĪlax|U ^dߏ&#q_FaZ5W ]n-$Xu3'nm5 Fsz@MApOf#*T13NnDo4kVq!t20X_Eb5=qu55;l<?jEh)zmw#ߪv:lÇ}hcK1q GQBGa6GYPZڒ)8|};_1'Uc3eQЊq:J1ULw֓P0e6/C5b{_2oMwJb0gu鲜u> vS!TgDmH5/]#qt_='q2#K^ޥcu9i]!j{:_ ifX!R_lVb11І$3e | ^KA d^CDoKl!pxy6ӳ7 1w[U%48?Sf"pEW_䋓4860a2vЎ?_?_|L;wWܣWs.FI5ːn3ozOMwt:XȽ$+ˊ@,(=(<,.: :^E8 c) o{=w_Gu:<c6,,>ǎl['γnGaPpyvPzJ2 n*j$rq} 3V#@uI\PM-S IIdD>I]xh][3Atss%)A:Ɓ2MkLKFx&m*ebPhF͜|iڗaSL qFc7[ pw!/rK;8UY <[ѻczS&9KR1R ͢B3e@}co,@`>9b2zl&'A+! peII/5x bvv"v;F*"J̔0jKJݱ @őA*%Ӵ6SH*0u{qJ>v o!:娘 )6q[*4Hq<8< *L}o-Vq-Y=lz9d<"{Gik'+Qװ!5l#{Gp*JYݥ(#@wp͉5ri4'_=5Ulsɫ?K})˧Z2HKJw#beV2SɝJ($"@C`&&HeR7 ;0RE{Qsr"*Ex RW9v3I^89Yc">ǹQFn8VӉ MnW-?t(2*%9;ˬlthHw9!EZm_il՚!9gXkCh?)@LR9)[Lw;e?%2_whu 6H{휶2 <GNz{-_e~J GfpҨgkь,N<1S+-ںǹ91f]EUj*:PTԩu[78Mt/3}]Ef!^ykD"_p`y)Fs:G?dUm L|e uVk D.ʻ:+_hlT7 ٦e3u ˅OpBaaN-_Y&KS 9=.xPBք^]t @D/ed|1Zq7-.Uebd7F5ZЇnIDZG%㸢<~VGxj 92Qm h֧&*d*f*Mʶ<>ܤ)"Z{Z,4R]FTÑhBu:spY&[ :E …]#b **"26W.y7-shJv밧tBdjR ֶ6B[g*$ZiH*% yiƃQ)gfPꙐomLDê(tzܖx?ApTϖ piit `v-C{'g,Ɓ$=\>T3+ƩP-A&_y]檕RG:ѱ txPL>\`נcGiѿ^?BQE+/O)O2Se }u7‰0/Pʩpڿ#z] @jWEG= P )/2eE"fLbN2B?K6Nl%+8㫪H+B4eH8O0muL:csɴ:6nVІqwsr|@1Lc`F"Gtje}7>$bt&s_gcnN{Buױ46=13D#Ǻ}IZo]+b`Uo{1kб3V56ᡄ da "^--Qlǹ-,_WZz 8JD/ 5nt{p'kp?{R!Pe]_èQڵ))+u AHh-$Hm"÷B>)ۛ;`7v"垤'ғ &Q.il⦦ r78}[&ӡzCS&үڄid QDwp^Bytɜ5]W"ƾЀ+>.0 JH]R*uAQ_\DsЖ}iGwISt gI 1ڬ*үV]nd-GфQqÇ?CmGZb1U%F'7?Z51A@ tOeZQ{&wJ KMf܎P6zTQSҮ<Q̀BN^o.1/wNo(OlIGu/920P_7nB^TOp}g0X(kK73 ;YgV-x2~Yc<ݗw1tE݀A촏`͹ތع!gyV ;bD`{?ȺzBz1eŽ'v2wJI5"SR6 y4R[(|F()fMLB`5)s"#>( 7&޴Zmِ<5|ӖlҺ%\FLK+ۋũ q#we'xw)}" }歷 sKAfSܲh> %бc3C%?]CLlK>3zi& nL~4Gs"2T f@MA*m?ML:2h-!ŐN9`VQUW0x{ `07k}Gs;=|A-(rsZ۫-'1Z15 e:B%{:dus"NnvRnNM㯽sZ{\0izG r:duj>{7u.+hNg8oM8+jUR}$z /TCzrt /`gɶaIE.L4hyil\i%3?ceуxqWWwVJJ% c7F#dp_"HJY90`L2+3UTj(;ŝ7+).RUs}1();{K ŋ{NPhxBKkj~H?uD#Y$͏[7S"JY۷ßM"Oz?ߨRz5iMra /bUOA+g%%W=pȁ_}^ ak#ICY@ZDAc(a)PUWdey{S . Ȑ1!ls+nLN4f^-4(iXM rIw4i6-.J7p ٚZiB߇ ׌3_p . VIDa\2I%ŗ>efO<@YaLI}D#K,ne%J3ɩԉ 6zozp1 z-\0ļIi{q;7P'o>U?-XG;:v˯oYYHD``Ab)욃WWֻZIZhlJUE ?p^;5ibYo83zX^q:}zw2iStv duz$ ,"k ,nr4ʎ2‘5i&Tn@|%{D~^=EcBp1:=5ķ'n-S ‡Zƫ}s>YYu٦7p])L(ZC=x̹؉Ć\<vgA.:}Fw;Y!3~([P3J9퉄]jhCJ9TFG`/(EwؖPpI*0Pܨ386A:K~dZNˆGV~^aFsɝ 1vuԠٶrۋbЖONpu鐊cV=%n{fNΗ8Xbxkۢ`NWUWk; -c6 AH+KNq_5Ӯ-_⩎>Q6XdD}!!_g^֞=in~dl3)E YAQ-dF)JKBBZ-~TE,J #옪AD%W(8yyvRN)\x^Tt r&i6`ì*_?dZGnj|HI"AY_s6ҒACE?FxI9}fàl@=l:.R]?R<(’j60YAf'=?m<5G% J¡Ei-wlI𕪿$6|$fyaW]?k _"k=k/˼x7,[|˂*x%UzT]U,hG:ۧjl[RLk|Ѯt*|!oצpBPqnzUiEe7 .ɝ,M.$Mw 7O,&7RL9>1j U,Rl pRu#Y{<tgr]oJL[Ke n/N׋~-M[I_]EO/pU>PcلE.]miyU/-?AqH3%!'{K3pEWEk+(BW$Y ,3Ko6\Uej >ki@4XXbTNtfdH 8?Uw  H̶Yg'5!e'~*\Wf/V^i,/e'>s9^9~;F E.^/`D.ƣ1f3n-v##V.T-˽JebD`x c##菵uwܮDb]@‘eQBqޫ[%O^Ufxcl)~/G} @Nl#M|өLDj]-%]xh{l7Eo#u1_`5=iJDb@(eJkZnfy31T]6}qC.4P(Đ6ϗ]LV:?[Tx ;<,iPx ׆yM+ZI8*K[r~ҥ`H3cd`JٓdhRx"} 0D/%چz^s>aZ=bM죬 Vs8145=FҺr=T%]p?ZJR}m {RFvVPp9ufuݙ{w&T_$ELQ4Yb;~: j<{t`nWClp İ{na1uᲊ²22V,*s{DN K^9YwEo=OI*-iЏ(U^Ț֒h k[ol ?rrν9J5k:שtu*VD+R䢰JN#6޽[gK FrѮWƁVF+"M׆۞g-'=t8g3kںf,pTl,i"&ۍ@do/[jXZ#YB(+*vsC=40_\-WS\1s.X3.:gN˽XX1〞=p@u6ЮD MsJk@ꃶw0yeЀzI{joHd]CO ]NXt:6(/&z3r ;c\44E TG>k] ,Uzfb!It%g4Wym4 4|+^/Liy!!SA^ߓf/^u BEy;9:F6T񳎔^.,Й٭$ҫ%%W1!, F VU-̓?p'IrPN> Qנ/XLQ0>oYGM'>#Ki,&^XPy_03/#lO"xEEu(ʝXS8N+L žveaBRܪU1hYY<< JaGp87x'1B^Ybm̰-̻h4( f[T5f7,+:QG ɧFV ht%Y{cT p=<>Ep0K'6D=c.e! i@QPeaWJ XYw)W)o>"*fuT=wܲ?}ԕ~54~TPA;yW0~t|XEGj*I&v6S>dduh@}xMDYm[QL8p2ċk}"%/5Ֆ% )+?sxL˱k(A7W::UkAN.ť^$v:ST?Kݯ4_01o0T@Vo9zpE}@zO$v=}睟`k0Ͽ?"`[۶O A6{ Nkݓmako6FT  &o+wY(rgf?DD=K]ƂBQ/IYTd0yE$7I'WWkj6!M08wWcYj4WJyZ56W i^+{pS\|1EAZBO^yU VA'?QJ[oulE\eg=4‹8!̲N8=V]Ct=F}?Z Cy7phγ:Β{Jz7"xlUď( tzz5XDžS4఍t~lzU.&t62O#~Q,vf\XUds6rB4BKƆ0-[ϖz\!xg}oVn7whJRJ ą{KQ W͙ ׽Ig W KեM$__i6F1JTG#y_afw80б<109^s,O^O /k 1rU@aKI %֠3-J^V[)aT=*IwUXtݒ %x^:QyD5|FfC(k~aGnrR߁!ǘ_HHU8bhWT{:\|w ($-l7-s_ )o`\rz:ڶEi<^To"kzT(ta"k4n6'ݱui2ZK%1x IЁ>u_ 5{ۿ)GzaP @W*'|셒L ;I=K+G'Ϥ%3Wiz 7aG1i>& D8Pc#_ڞ3Il"F`/'-Dij~ TϪ>p^ сZ5T[{C!g53, ձ& P+G.l,~kKB/KK'sxWYmE ?CujJJqTv@,ke-6x a4TV[cEEpZ߭D#+=o$]y/NXVz\A~i/(`SiN[QW\|zOsRpe8 WJ.+s/ iOҧ> Ce{q{A=>u&VJsM-+ {jG_Е_۬PUbVTO}a8O~̦a\r$F~T_އ75WX@ 7>]ϘFţ /⑉-ݩS?%μy37K<;@ū | tyIjׁbYYc .zUxI+ZIe"ދG#E=#s:N:Rzb+ΑF^Cij5[bwS 0 ˑS rc.%vТ͋=GDHJ[wU3zS#ӽx|#%rOxi]c>20tc"+W@Eg?E a6Bqtϥz &A ^`'p>nNyyFkq|5}_nyY &3qF;Vӳm8׫D#k|6M_%:ڎ_uy2ؒ|+.H͏Z۞by@yOl9,d瞨`(J!]tP$VY bY#AgmJPT; 1 hLg^UEdM2wC.T-9UZْCRhՠ}BC dfۛQrn$z?dF}9D%b+7[M ?1^{g~%uW |]:; X;U{v٬sԋ}RQW5,56>B+K:R i \3Ot.y7"zt3h8;Xh\+2m]o!Z7H顁 jWM8b%яX'Ti6 *B]%% £ %!q" (X/%1WSȱ0_Gb3'suuwN_ZbYhj")ějq9htT/Qhŗ 3hQ-~5}2Nfqz L_2W;OYg=Up23uee"T4_gqHH 8\ e,-;KW"_}|`dub혪M3\8bL89LaRdY|TU-:+2BK]U"#,[uo68'0_]MV- ydXQC旨`/홅^'JJq3c Yr_GFeH )yg~7WpE<6YeH=^h~һcF&h t$67DG->.k so^X~a\Bc~qO_p1;PHN%1gLp5LUelP^4Lc٧Y!#m2O8үcFNZ>fMi}BT깁!Q.Lt㛏1T3~MC8XM5r]̄׸x!o;n7ٍB 8;XZ)RYIȱCfD]ߊG_>L?o&VtԽ6 (Nu_ ľuE[?V?LqE{*Ak|aa/%8fGRwA;+}C}т Vۨˍ? iQEx-O~֏mvyy_lTYRrPq_9Z3*oBfKKNY{z;̚&7:OH$9u\a?rQ>zM}0 &sd_ojRYw?&qn 49Aln~DK15 ^#*[$֡^f(@Z,jjv+;vqiu>zf.6&.+wp:~Z!॓o.ߵRI^KQDx2nyvu4.uX%tsFkyX :r[V;`m𨵫iT24_LYP "A4ȸvLi'1P{z񢞵YǘBqhI$z3Y{[AqiJbJR/{xF"pgӬE}NR9$½z֙e Nb5UI$hqG] Q%i:oY|6m1 =G+;v wb4g[@PNj6!x+N?F$|bu89 6e3{#YƸ ,YTU-h<sv)=A!11x!Q÷->-^D=Ek@hXY57=`EPIn:Nv9Éĭo^f}6y_uWI`E>UZz ̄N`!*('YX 2j#=+*9YCK1n aa< ̆~߬< \qϸ$Y_ ^v? OKK{l1_s1czueZ۟ʄÜ(|;P< җ( %uY?ow=`6Fo-A`ċ'N a]=.-ߖ=-dJK5xM•MY>YK?mJU0t9>CKSBCZ2V#"Ax19\@B]M҅l3$4 RA#oxJ#sNLz.9HEELҵcRR1TaCniZCo>Oҩf]ewLlfVLe‚>ljFs2*0` /1#8&#U+hҼцb\O-cJ.S&eEW .%kn*Ucr} ',̿XVvT] 4C?X-oy?ڎ_ /!e7xޣb*GŴ+i.N'rXN /%ͮ,^ɮudvދҩɚ[Ƹ YU)׺L\LHwdK2&AZWY_#bT3s݁ș&HW: _Mg垤 gbᜰ2H4M.Y(/[\L8EdkВNEDpMYNlex|=ə[mK8""x1yr5UQ~7ٟ4 /j|aR_Ήht>uO099Z\ ?"&D\cqx䧉߈"DlH*eiƆe=(\C>h =-$Xw7{_d> Z/m07AbfTeIe1p:.P  /&hhp2&̳޲`S)DҩYWd%km{mvo,%xAuy0 6{B$/x$j}S>)hM\!AfV#R&ҝE[е+C؉c4?8@qO:l\ j^onyI-kMH+MEm\n_.ϵr4kd^V"ձ YJ e\<`K/GaJ&7,жkgiYv>ϛ+GU Jd(6(;%ՍDקnm~,*CeWÐX-ЪYPL o58D,drf%g^\5d'C˔/TPO*[r&*MCmpXul1N;)fk:{RCKf<&Nd_"u$(䏛y$pDra \4s= \-ǪBn'B'̖aq@(mڵxpۣvψK*ϳ(0șo.Qj{S{d+tׅP)PA2 ojaeI6.hryrA@ka}s4f̆ag!EK^JOY+G0פw{O}UH`'z a'=vvLQiN]kK/jMA46uVα"J"Lt6)|HY"b)4z:-8YyǷO++Wo 眦d8.[,=jjzoinz`-38I#u+Fܠ?I X2O~Y訤2鵒'˯ٰ޳?~Gh'OL2hhSbyٵ@`b6B&8_-LfaYJP%o33J WS!ؾ# w9licQ5*{l=B󮯿d2Z@GAq#1>آ.S],6"T!ܷ'P\|@+z8ǫT}1vDHb"̥v(< "[oC,;Q q]jao 1)jzhаof{ ~]ϛiUSDܕ–TnZ8w*E9+XDNujH7lo2V$¯7]IqdFガ>68Fu%%Wpf. Y6V\xgO6MƁܓp 5&ci岲5vt))`o*4w5Ғ+<>eC+4i(¹2Qd"vU _{OfSD -x:(Ǩk,t+jѨYFobeov:wsdhAikG8&ʿM m=po$B#v`!^ ̉9?0/R71}sG$EV(pNK!("7G; 0YU\^ԃbHB2 H_jbQUL%, _oW$dMC9'Е_Ǩx_C LF*F0 Ck8 AE̤00y0彈m I-Ϛ2K 0ș}8KL+Xc9xD"ϜW>de6R< ʢ/h?^LKcN<?_gEqhОdArLu5-d5!؄b^Z-rLRWkO&b%ϐZRMw+?/ Gh;!ߝfk FzGxiBj1$%w\^R]Y1TA8F{0+d" }?T1io_#AFc#`֢m o'ּ108bifa_ẙ'/fXl-\Pbcìͣry8K&7apxY_|Q0fj ;CVQΝ*^MT8D3ͷ/RJ6xRF>IE$ۙ-l#[aY횦;L}sYWv Ӳ]os:ySmn/@?KwCcɩI#H0Mj[&u`z!Z3Y hS EPWy_u!@G"qUTƤ.\ !ܳy=Y|?8mCD+#I)*MYB+ CUw'N:xj`Y5a?n,Z^j2A@h)@g)_w؁n̓x@Ue .d07s }3Q\@ "f+1ۂ!ha@&j=%n `.m -V\|"߰ZN>o\&ˆS*㽕u}M6Gu"Xa j9& z֨]˫ Tb̸!_gDHyC}#z UNQP0$V &G"ZCJUN"eU(ԛmȹwLR xW{Ĵ{'>!߼_U`޲f2;=2`+(Ik_ /٬ced!7=_i":MfhDƀ{s^,Fc ɐqpDF]H }ӑFR^ߣd54DkDBZ+Z|me.sǍ E1ZjC32jlWXg~#?MxD~K'Q&5l=F?&q+$|BevM4pe6:F'D2ZGh;~B}t . {SR;|=d[[ݶUt^V{۳Ʃ uKt܎  g :`!҅HhRxbef1 `sBr`M$̞+$~Gcd4_"$;O%Lx .z<%_9 LʜۿCD$I1֙9njlmy"C?;gG1~K NHpco3HSJKH\u,-O륊ȒL_Hr])0'-GZ'S}6T)!>oj~ĊS54'3[ӊΧ<:^ikӶm&Rg&l9][ Xh> :\H]mM JsR.^$naH&ߦV ei->RxiN:~>$scQ:g5Ǝfz:fr MvAK_a_!=c㸽'N|&28,/)+){W$'~ʖ8Wi4(\bfR:`2ntn4:?؂;[ZrUW~rIeD!*$GmK@ec <7}ߐɴj@EEoQEqnYFO p'=T7?Ge;ZZM 1;^]1+1zc`yY|cœ&?I3q~ÇC+">N};?q)/Ypzp*MՏ;}`FzG6dc-)ŽXN[Q Ȅ#=\ۨģxxX"<'z}qt^ޗdKΥL,L*sKJ.b!GOGB2J>ʢpF&2IϒPvګ,==MiGv3dһmϚ47o5oH&߱GT$nHȿ ?Fۭ-O[[[ĭ6bl\MgԖ\&: iWNi\%Rν2ktZ`h˪},3y)LYeAv*m,MZ-&deۉv8/zXX~+KSm zWBB;ɀPǶ63-v4*.i"hB>Ӳjن, U&$v5Q#ڒV|_ekQ."*ϔ% eW-5;GWVwzش<,Yd펎]+5lTAA8f7Y \]Ԗ("hJ4~eCR/?$ jG,9S~&YKK[^(_on&\ P"XM3/帝|pmb$Sǎ>rNqQjq%«ZF|a4V!dD4Ip|vb6='姤T9HB 1 ]a'qK`:3ĞDn9NVVz]a2ޱuϽ_b!q`x3{ uR껯U1d%-TЗs],VM\GӪVp3ќdD, İˎrLjA@#+gR"?&S4Y,<`F)[ǷBWV! 0IA;Ol7:S^L ժȴRжe7raJ=hlxq _I:^H7E(B7׈&ptnZlKc~Tv ќVEn͏qI-$1'^zX ˜Ab09u:n7T\GQoFjO%jnR8TAAC‚U+B O_h#L%a.H'.4TWpwŝ ЏV:{J;}˒+ &wvhJY'v;ݽ @.VP}:o4㛘l g|?ClSKjƼbj>(SBl0TTa%=@j[97;/ȸvߥK 9\ӇlOV{&HZi#cAt ?1[Z!fG'!lɟᜀxk#>?C] od7_^+o58fEm.!eQ$3wTF2/i<׎Ѹ,޻VTɌ ]k@,d`2F[Inrz#/ -jkoo)Hފ'EM_t p:% Cŏ]Tql鸈G18^Nh_{]\lD7#gݝFC7dMI~ 1R PbLR R]4.KTT`Ź&Od*P {H06JF%;!_A99_sgd^ziPxQwBH~{:SRr q"T7abek:u2()wh!\9tN}*]{ܳxO朋Ep<?/L'>Ր?DOhI h1ů jjD646?yg" HY'DmJi0 .[x!ƚL.QA +Q^WSsUYx5 ~;~9ڎ_ ,=Q>*+`"h,nƽ/1ꒇck $DeX8O1 v%Ώe $!HU͍K_t笾힑J(K8ӧt2Ba_Wpڼ :u 7K%qWBxK^[[ifKØOo=20cPk/>yGW6sIByPr2P7D>8~&Fl ؉4F߾"Lan>nҒvwtSeO=DŽ ghs;94i O FWpfV[3sw0{= /\SD8;75=P2yx>5psf'ꈋoCKF a?tLJ4bs;0ly*5ƙް&זnp5U-fjrJ>[ <%1ɴ>fzh' ̼mOHy}'oi}v qx 7߯K\Ϛvdc?/q- |:aE)qa =7C=R^!MxtYQ:%ks&)%a^ns4)H2KO'+]z%^ l<}h06cj|JM7k(ƣgjD>p||h[7=>m/P?~AǿOY yю\~B ?}rTVI5x.sB' qW/DE .*hh!K8m0a>gw~4 e긌h^i1574lÀY\@u 9ssZ&"\MqQ i1":]"Up[&m+ .FTcAiZ/*(QY q247? i C΃vFpdܮN(9YO=H$kN.RX\mcӣT@)YUBGVe1R/Is C+YWpJd!İ)),2}g1FQ,J} LL 8mGۯ]/) X?Zb䧿Oyz*KM$$nc0U` VU|chЫ'-\@Ɲ-<*#ZZ俔QOw&IC&i!f4T kbWP\@4$RJGb![/"\ORi8[$Wgdn(HLLFcQxNo+XQf 3EUL[uPh%ߌ67onjj~Lw/@SRw~uK3bb6ҺBǕp>p dlJS&NF("&xl T&'*Hު!YI,74>FGh% nQB5 "4 FnAUWFp[QJ*E*_GYtRTƵy<6X$E58C%@_QBh"ckU%S%Ljީh_7!vQgUޭo^p&31t+Khz}>9_F+ADRHo"ӵx?HD ő#Щ>ŇPc̓H;1;?]I:vá\!Bo}ސÖVho @$ .r/3pfgb{g̬/| h١ o7Ή"3o>X݂CGpX[l*V1z^kf/Ou%ZJ\iRB"zAo6昋 g`?2& Lš+4-:>F/D L#:j)ѧx)lQ|s)nJ}z{8 ,T켾g2-쾘y*@]iYYT| @ӚaPiV=&./D^mʊg555U[[5Xjxcc[]YQ++6p,p,n8бズ؈riyzEz4 Ǵ>EWUl뀣b7W+eyxd4O!33>3ݞkӯSmw \5{n#V|!,-p*PåbX.qh!Z zQWW+3O&u乩i'oeqjzzXQ&SϚnUSQoK`Xt^rn=H{QDF:]|=Jt`= ˀ1zВ8P{QC`h)3^0T[A^B3RK~d/-cv?5 kCzHf@[oM=vnj2.25}.4`4D1Rwh饫 s{Iy4|x&52:c;#\Ex2?_w72mH` V`JB++ '{:X$YCFm - u(ڪZVQQ:穡-N'#VU)$;}"LTS[MB\iC GhTU[Q" Xo`7i3S9ީlMeXt#Yo#7ĮaSM٤˫'cv-O᳆vW HXW/b2Y4VFK)vlӞ<EGRy{)<Dsd2SQhYU1V~^þa5)RqEj#dc҆ "j6#%7bW;|SN4>olιλ}8'F04g6{xfa" ;"0E^K u{Nr=LO5 ^^XCjݲʧfӘ5k2@]f9^ק+F)|~x(} m+;ƫk7 k&`@f^]>Q?!uud:`-H)aeXpHv9ӨQSn#Syfnwp1$NIGv۰VGQ9p[-Li#\<6F"pyjyjyc3\Br5k ~YQ9<2RENDb蕑^ޙ7yxN $'V7`?No~!vDb9дQyatU .yHrQ7v-Lqs/$դH,2DHȼR򾢪,3AYC4. b=wB&MH! l:-g B!9I9Ԙ7Kedm:^n )w;]JcN~+nϸ]/ŷ\ukO9{|(`\F 7=YΗ~lݩRw8$-p1ޠ?w~j|u 4_Ҳugw=s53K}/w+_/v|UQ Ћ>30VdTrȂΏ^ ޲hE'߃!jnW䤯Ԣ 9&eWLI_]&MFiyFIخ%猤Q#d=>aalajrʫK*Vc}|FI(OaNz8s b]ݦ7S[%!O[ u[Fc?"4.0 *pj) ėK%Oqf-xpHt3d#E^8`'Μ.KmihlGX|xqp0yl, -³Ps!6E@]F$J;n Icwj ͖a`% ?0oI/ [;ǂ<͸<[蝄VV 0Hz960Im66>o`=N}H\p(b}[H$sHa)٥Ht)47ҘeoWV GCL5NL*VXll?ФDXVWYlFq;pnp qB2+hAVȂ)Rpd}ݎ ŔFVrD0?-G^בdE"_qYy?$(izS6L%kUVs " qN\26ZL#bʌ65zhF`D^!҂FQ2)38Yŷo/W0DbUTw3\>*`9E>3ZA:S5nKUS'j+O}F& Ch0\!j2KpX.-""gP¡El_0[wƾ7ޜ/[G5h%bd a~yZ릁$a%]TK=K%0p[RὄdO%I~B cyRe(INH%Q9ZNe=4XiIX U'i?JRyhd(\OrI&Khr dtcik"/:F e a':!sMOT ׁ-GFlN ыa5~!Slq{&WI9;fuhc~i>i#{ag9 *{z(P4 &p,U)IXvvxU:UoI{s x+-P`YhI}%iu4RYJ*JigM(VD6HDIQNVsz$vM"bb1Tz$VT cu&X|M!Ns\y@k7 !Z̠Ue^Ǟ.|Fs2}Hv(N))ScP]Bc!?PD+̣Qz}T^n^{P̈́{h8qb5yHlr:' 4wUw+gzUqhtӠ*FaA=|QiՂ3L:vVS6~Ցԩ:ԓ=;ƅncE] R,-vX\$P^#'@ORsHU9|*FV\hfRD6D T -(/ ` 9KȨVDwI,rN-d&#W*՞ąG\{% "QZ@rW!F L73NYpҾ ݉#bX^N-A^R%P`mEŁҊ,*'t/^bv/*k׌xMbv9eE`b@G,(א"za')i{%2R y[YF7PHS>#,HvZvŰ|)oJsU2#{8+eLT[LhG"ݮb)3>:0uu558!CS xS4[Jl:OXL {? -O `:Ort-x@$^"N߀aqG8C% Q:ce?l# @~ <3|1uNt@TV 읓O lQxӔ42b) -xf8:l.פ2BPper?ŴqNUVN/u_b*9:Ga.YqjKt5;5T]5s&:Mw'E$8(4Y:aCfu^khzr;D~HX!QAH5u4zPR5DMܭtR x0Em3xf14L3`"h2R)4)IDc&(!:ni B$*Y,BR=UV$JsL?ZFANRVvO59NY"hb>yA3KBY){,hcI3dv&|+%p좑%Yrt! %p0.2_1 Cju;&@D1ޣL )_ߡCқ= ~߬2\IB?e QeHw"ĕ߶Qg` `d$4ZgpI;[ͤ?/0<{h_sƗW_{[o?yܵä>$zJ\)r E! 2m;Tx15vӟRmSq2"Y O{C6B{\ l(!Fa =m`{iXpɘùWVQ\DJC1>hn$-^VLJD*yuGw~kO\ t4.aZ5q{/u6NC =4H=p0\vH^n2H9^#X$#4Tki;00(q $ 'K/+Py .ɻ:p1(N[N =y,g$fjeWJHG1  cqvI&YSX{V]d5%bI]6+my>Z[)ph8@œ#y0EUc^M4Eږe#H]}&LgB&x4HPZZ: ,~US# S] Z ˄*,-97(K# 04 d⤠ѴhlvaDnsFvaoFv윪#K~-o[];scf3yfKo0]u''32-Yt 5rdZDaJ E_YO,AWFݧQ,Z _~/ ~W<Γ) %gv{\ lΈ.Zlâp#NJkV05m4]k\Pp-hoj;-s4\p#H J|:<1/IZXagӪwi}Nr|A_OW zcXB"b4њ%9â±%Ž&R(^YЈq7Ra,{$DPp0|3,qNVaq~s"\_Gn' ǔ9t=s]ٙ7]p! q)MfwBien .DŽƄ'IȀ"IF92Mo'R?<3?iQXZn™P`+gfVpzUe7h2 /\3VpF puf͆ApMC>W'_g?w <2\dݢ 83&C$!p "2%Xq wЯ56QBuzC6666=+lnމU#ԥZ~wRW:"ggZyAqUOhS|}NJEj Sb^!rjV@lCJnIr7: C!v"5$ϤF=oّi K?Dc(n =@vbT`^u!Z+"/,؂&x %! k؟MBK2 Ke>`2pA9O/qq>nb0 A~#FH&sƛ>C ttb? hi\XihK}swdzj"٭c6ۨ2bu;&@R0+OVSTJeJ cöx4L$V*|u9ǁ3/𱚌mBNM8FÀ& vؠ#[3X$-Kopv~ [R0QbOe {2W}##(WH㮘(+]MWRU:J%ݼxۜcwl+젹ţtٵ[_rpb{̓ ˊC 9 n9'}\uF0P^x%8Hϋ$k3Tr8ygGċp$\?`SHdhhY|Dc+QE s GV>ÕWa{$.㜀^0s1Pz^nSXMp1c"@,lz귳q.8ݹ>ep2:>p8 ~[ >bNIzG~'XL* r6q":3VRD@tSuh)1k㩺=Ğe3%4 "Xlf^^9ht4/%Fu*76P=]DHh<}<\ 4p9)OX!?]Hx.7M+zNwO0BtSV˰5 ``v'Xpv.$KC;\ D"r-v8[C`L-rF}0e2ʧ4H{T]m'vSS . :`ꆐG>! ]!]oVDڹVXnjƩ'T+LKkBu4z\/܎8Wj0;pBo,G/< 4kt9]~+Pl3 g0sC=/bp'YK\&0$Ԗ%o\hpwH9\(x,1 L1–NDAkqp/Y( ,ڝB n#'8ゴاi碟dT\ÒY}lΆ`sWTM1vN E~qp!B9T_B3F#Kvu챓d$u*'M*US^ +) \9AJ a*(:$*zԆO47]YF04ǁ\^Q7DijJO3UViր}\JfpC<}QҬ&xqID-#š). g]azDZZbˇt'bMòTpB.+{@\J7H õ! ՒGPb}s\50;#G_{$$]W IYlN4~%9>b4+-륥+$&kf /&|Yb5~,=/Xa3=OM  ɼFJh]b#7_KW˔c 8^埄5d_UZGl'σW$N= !>OY‰ +KNb L"qQAJ X+ђ*^m]/?.ˏ_.Wآ?6?mkK;m`%`ƞmR哚S5<5 lCBԆRn^o0m±¹CK(iO1Al `RVT>1r*~FUUXAS]eONH"_z"nHGxZqPWʪIGvRDe  Z􈉪n ,M蝢JCEKpm'Nc$R")r/ĩO>*DTAf,1 }IiɓbO¡EblVDHÁnPI[{e4㬉L6Bz"zGM7yCQN{ %1V1H_t Qoc6oX]^s,o4L i}s<F, =ܧR155 Bzq6J ?d5ގ%\9EgbXٟ=v4 Z@wBib4x3L+fNaDAhV-/B5UM `lhd75݂{3,Boeg$NGY@l6SgfԭT ')mR.tw [uK}Yt``1ĔJz aVdz Ѓ򊵒P@pRU AqRU3]z\Sw lb?*բ%yX8b:8R8O$v%o&? ݰݠ젽_-e/_GFCqma72n! 'Ęlx 2NcofξIcSVweRz'ձZÔ{@EpE[K~VPp2cFc?6L~x6-4| R2ĈJ}27?UU.?4VÌ\]4Ɓ,*sfFi) 848T7-IJ<²JyfOePS~]Hu4h4ci{EEp GKh\xQWcfsɯ_zS%X[mT~TT4~r^TRR}PjL ~'}<ѥ+@Je\n{ CG~%Z'XRu4)FYnuV{a=Ci0[uӠ--}K]Cywt\]Aweź:]R"mH2$ΟsVM!1\3S)LAZ"XJp؎昊p؂K zsn! tA}uC#q } z]OVMy#24wϝBx|LDL@tԞlIp 6/!kKnr<|CU;%$邼7a)?+Z=4W~n'*L;wdkFgeހv<ޙ@p.+>j' GzKϞ%