pax_global_header00006660000000000000000000000064133261565110014515gustar00rootroot0000000000000052 comment=3e509e6bdd2c07ac715cfc27946f86123744a46e pyinsane-2.0.13/000077500000000000000000000000001332615651100134265ustar00rootroot00000000000000pyinsane-2.0.13/.gitignore000066400000000000000000000004101332615651100154110ustar00rootroot00000000000000*.py[co] venv*/ # Packages *.egg*/ *.egg-info _version.py dist build eggs parts bin var sdist develop-eggs .installed.cfg # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox #Translations *.mo #Mr Developer .mr.developer.cfg *~ .tox/ pyinsane-2.0.13/AUTHORS000066400000000000000000000003461332615651100145010ustar00rootroot00000000000000 259 Jerome Flesch 7 Ross Vandegrift 1 Russ Garrett 1 Timothy Cyrus 1 kingmoonracer pyinsane-2.0.13/ChangeLog000066400000000000000000000134211332615651100152010ustar00rootroot000000000000002018-07-25 : 2.0.13: - WIA: Support of Canon MG3051: Fix getting data from the drivers (a Python byte buffer was reused instead of being duplicated) - WIA: Support of Epson Perfection scanners: Handle resolution range correctly (3 values provided instead of 2) - Sane: Support of Epson DS-310: Do not try to set a value on an option that can have only one value. Sane driver may return an error. - WIA: Support of Epson WorkForce ES-300W: * call to seek(0) wasn't handled properly * scan must be done on root WIAItem 2018-03-05 : 2.0.12: - Build method has been changed: Use now "make install" instead of "python3 ./setup.py install" - WIA: Add traces (from the C++ code, but using the Python logging module) - WIA: Fix behavior regarding feeders (Brother MFC-7360N for instance) 2018-02-22 : 2.0.11: - Add pyinsane2.__version__ - Sane: Normalize behavior between single and multi scan: Single scan must raise a StopIteration after the first EOFError - Sane/daemon: remove trace that reduced badly the performances - Build: Add Makefile (if working directly on the Git version, please use now 'make install' instead of 'python3 ./setup.py install') 2017-07-11 : 2.0.10: - Now works with 'setup.py develop' (thanks to Matthieu Coudron) - WIA: Some drivers (Lexmark for instance) returns WIA_ERROR_PAPER_EMPTY when calling transfer->Download() instead of returning a shorted image (like HP) - MacOSX + Sane: disable dedicated process workaround (doesn't work) - WIA: Minor optimisation (Use collections.deque() instead of list.pop()) - Sane/exit(): Exit gracefully 2016-12-06 : 2.0.9: - Sane: Add workaround for scanners Samsung M288x: Option 'source' is actually called 'doc-source' 2016-12-04 : 2.0.8: - WIA: Set various options to default values to avoid problems when scanning - WIA: Fix ADF support: clearly tell the driver we intend to scan pages one by one (required for support of Brother MFC-7360N) - WIA: Workaround: Prioritize source options over scanner options (required for support of Brother MFC-7360N) - WIA: Fix behavior of PyinsaneImageStream::Write() (required for support of Brother MFC-7360N) - Sane/Add workaround for Lexmark MFP: the option "resolution" is actually called "scan-resolution" 2016-11-22 : v2.0.7: - set_scanner_opt(): Fix the list of exceptions to catch when actually setting the option - Windows/WIA: Fix: If scanning only one page, at the end of page, don't request the next page - Windows/WIA: Make setting options more resilient to crappy drivers - Windows/WIA: Add __str__() on most objects (useful for diagnostic) 2016-11-19 : v2.0.6: - Windows/WIA: Fix ADF support - Windows/WIA: Fix support of some flatbed scanners 2016-11-18 : v2.0.5: - Windows/WIA: Also look for network scanners 2016-11-18 : v2.0.4: - Released not done correctly. No changes with 2.0.3 2016-11-10 : v2.0.3: - Windows/WIA: Fix build - set_scanner_opt(): Fix exception raised when trying to set an option that is not active 2016-09-22 : v2.0.1 --> v2.0.2: - Add MANIFEST.in (.cpp and .h files were not included in package ...) 2016-09-22 : v2.0.0 --> v2.0.1: - pyinsane2.set_scanner_opt(): Check the option is active before trying setting it - Change slightly the output str(scanner_device) (remove useless "Scanner: ") 2016-09-20 : v1.4.0 --> v2.0.0 - API has been redesigned so it can handle backends other than libsane (rename SaneException into PyinsaneException, etc) - Add support for Windows Image Acquisition API (v2) - pyinsane2.init() + pyinsane2.exit() added - pyinsane2.sane.abstract_th removed 2016-07-27 : v1.3.8 --> 1.4.0 - Add a workaround for Sane driver crashes and memory corruptions ('abstract_proc') - Python 3 support: Constraints must be str() objects, not bytes() - OS X support - Rawapi: Fix: When Sane is not available, 'import pyinsane.rawapi' should actually work, but pyinsane.rawapi.is_sane_available() should return False - RawApi: Fix mistake preventing authentication - Methods '__del__' removed on Pyinsane's objects 2014-08-10 : v1.3.7 --> v1.3.8 - Fix scan from feeder (multiscan) 2014-07-09 : v1.3.6 --> v1.3.7 - Fix: On some scanner, the option 'source' is not active and shouldn't be used (prevented scans) 2014-05-01 : v1.3.5 --> v1.3.6 - Keep the behavior of scanner.options[].value always consistent: if an option is not active, we should always raise an exception - Fix Python 3 support - Improve tests 2014-03-05 : v1.3.4 --> v1.3.5 - Fix Mode=LineArt support 2013-11-05 : v1.3.3 --> v1.3.4: - Fix Python 3 support (again): fix imports 2013-10-12 : v1.3.2 --> v1.3.3: - Abstract: Fix python 3 support 2013-09-25 : v1.3.1 --> v1.3.2: - Fix ADF support: - Request the next page(s) correctly (the call to sane_start() was missing) - Make sure each new page raises an exception EOFError, even the last one - Fix image spliting 2013-09-22 : v1.3.0 --> v1.3.1: - Add a workaround to prevent crash when multithreaded programs stop : never call sane_exit(). 2013-09-08 : v1.2.2 --> v1.3.0: - Changes made to the abstract API (previous functions are still available but deprecated) - Add functions and info to get the scanned image as the scan goes 2013-08-31 : v1.2.1 --> v1.2.2: - Abstract: Fix scanner options handling 2013-08-19 : v1.2.0 --> v1.2.1: - Fix exception raising in abstract_th 2013-08-18 : v1.1.0 --> v1.2.0: - Python 3.x support 2013-07-06 : v1.0.3 --> v1.1.0: - API cleanup : make some classes of rawapi available directly from the abstract modules - Fix feeder support on scanners where the source is called 'Automatic Document Feeder' instead of 'ADF' 2013-06-30 : v1.0.2 --> v1.0.3: - rawpi: SaneEnum: Add method __eq__ to make value comparisons easier - list_all.py / scan.py : Take into account some scanner specificities 2013-05-19 : v1.0.1 --> v1.0.2: - Switch from PIL to Pillow 2013-05-05 : v1.0.0 --> v1.0.1: - Fix "Lineart" / "Binary" mode support pyinsane-2.0.13/LICENSE000066400000000000000000001045131332615651100144370ustar00rootroot00000000000000 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 . pyinsane-2.0.13/MANIFEST.in000066400000000000000000000002701332615651100151630ustar00rootroot00000000000000recursive-include pyinsane2 *.py *.glade *.xml *.css *.cpp *.h *.txt recursive-include tests *.py include AUTHORS include ChangeLog include README.md include setup.cfg include LICENSE pyinsane-2.0.13/Makefile000066400000000000000000000030441332615651100150670ustar00rootroot00000000000000VERSION_FILE = pyinsane2/_version.py PYTHON ?= python3 build: build_c build_py install: install_py install_c uninstall: uninstall_py build_py: ${VERSION_FILE} ${PYTHON} ./setup.py build build_c: ${VERSION_FILE}: echo -n "version = \"" >| $@ echo -n $(shell git describe --always) >> $@ echo "\"" >> $@ version: ${VERSION_FILE} doc: install_py (cd doc && make html) cp doc/index.html doc/build/index.html check: flake8 # pydocstyle pyinsane2 test: ${VERSION_FILE} 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 -v xx ChangeLog | grep ${RELEASE} @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: uninstall_py: pip3 uninstall -y pyinsane2 uninstall_c: help: @echo "make build || make build_py" @echo "make check" @echo "make doc" @echo "make help: display this message" @echo "make install || make install_py" @echo "make release" @echo "make test" @echo "make uninstall || make uninstall_py" .PHONY: \ build \ build_c \ build_py \ check \ doc \ linux_exe \ windows_exe \ help \ install \ install_c \ install_py \ release \ test \ uninstall \ uninstall_c \ version pyinsane-2.0.13/README.md000066400000000000000000000214161332615651100147110ustar00rootroot00000000000000# PyInsane 2 ## Description Python library to access and use image scanners. Support for: - [Sane](http://www.sane-project.org/) (Scanners on GNU/Linux, *BSD, MacOSX, etc) - WIA 2 (Windows Image Acquisition ; Scanners on Microsoft Windows >= Vista) It supports: - Flatbed - Automatic Document Feeder - While scanning, can provide chunks of the image for on-the-fly preview (see [Paperwork](https://github.com/openpaperwork/paperwork/) for instance) - Python 2.7 (GNU/Linux only) - Python 3.x (GNU/Linux and Windows) Not tested but should work too: - Handheld image scanners ## Dependencies On all platforms: - [Pillow](https://github.com/python-imaging/Pillow#readme) (if the abstraction layer is used) Platform specific: - GNU/Linux, *BSD, MacOSX, etc: [libsane](http://www.sane-project.org/) ## Supported scanners In theory, all scanners supported by Sane or WIA should work. In practice, each driver tends to have [its own quirks](https://github.com/openpaperwork/paperwork/issues/533#issuecomment-262777789). Pyinsane [tries to include most of the workarounds you may need](/src/pyinsane2/__init__.py#L33). [There is a list of scanners known to work (or to have worked at some point)](doc/scanners.md). ## Installation ```sh # recommanded to get the latest stable version sudo pip3 install pyinsane2 ``` or ```sh # for the development version git clone https://github.com/openpaperwork/pyinsane.git cd pyinsane sudo make install # will run 'python3 ./setup.py install' ``` Installation on GNU/Linux should work out-of-the-box. Installation on Windows will require Python, Visual C++ and WinDDK (see below for details). ## Tests ```sh make check # check style + static analysis make test # run tests ``` Tests require at least one scanner with a flatbed and an ADF (Automatic Document Feeder). If possible, they should be run with at least 2 scanners connected. The first that appear in "scanimage -L" must be the one with the ADF. For reference, my current setup is: - HP Officejet 4620 (Flatbed + ADF) - HP Deskjet 2050 J510 series (Flatbed) On GNU/Linux, you can simply enable the Sane backend 'test'. ## Usage ### Scanner detection ```py import pyinsane2 pyinsane2.init() try: devices = pyinsane2.get_devices() assert(len(devices) > 0) device = devices[0] print("I'm going to use the following scanner: %s" % (str(device))) scanner_id = device.name finally: pyinsane2.exit() ``` or if you already know its name/id: ```py import pyinsane2 pyinsane2.init() try: device = pyinsane2.Scanner(name="somethingsomething") print("I'm going to use the following scanner: %s" % (str(device))) finally: pyinsane2.exit() ``` ### Simple scan ```py import pyinsane2 pyinsane2.init() try: devices = pyinsane2.get_devices() assert(len(devices) > 0) device = devices[0] print("I'm going to use the following scanner: %s" % (str(device))) pyinsane2.set_scanner_opt(device, 'resolution', [300]) # Beware: Some scanners have "Lineart" or "Gray" as default mode # better set the mode everytime pyinsane2.set_scanner_opt(device, 'mode', ['Color']) # Beware: by default, some scanners only scan part of the area # they could scan. pyinsane2.maximize_scan_area(device) scan_session = device.scan(multiple=False) try: while True: scan_session.scan.read() except EOFError: pass image = scan_session.images[-1] finally: pyinsane2.exit() ``` See examples/scan.py for a more complete example. ### Multiple scans using an automatic document feeder (ADF) ```py import pyinsane2 pyinsane2.init() try: devices = pyinsane2.get_devices() assert(len(devices) > 0) device = devices[0] print("I'm going to use the following scanner: %s" % (str(device))) try: pyinsane2.set_scanner_opt(device, 'source', ['ADF', 'Feeder']) except PyinsaneException: print("No document feeder found") return # Beware: Some scanners have "Lineart" or "Gray" as default mode # better set the mode everytime pyinsane2.set_scanner_opt(device, 'mode', ['Color']) # Beware: by default, some scanners only scan part of the area # they could scan. pyinsane2.maximize_scan_area(device) scan_session = device.scan(multiple=True) try: while True: try: scan_session.scan.read() except EOFError: print ("Got a page ! (current number of pages read: %d)" % (len(scan_session.images))) except StopIteration: print("Document feeder is now empty. Got %d pages" % len(scan_session.images)) for idx in range(0, len(scan_session.images)): image = scan_session.images[idx] finally: pyinsane2.exit() ``` ### Scanner's options The options available depends on the backend and on the specific driver used. The WIA implementation emulates common Sane options ('tl-x', 'br-x', 'tl-y', 'br-y', 'color', 'mode', 'source'). So you should use Sane options by default. See [the Sane documentation](http://www.sane-project.org/html/doc014.html) for the most common options. Beware options casing can change between WIA and Sane implementation ! You should use ```pyinsane2.set_scanner_opt()``` whenever possible. You can access the option values with: ```py device.options['option_name'].value ``` You can set the option values with: ```py device.options['option_name'].value = new_value # or use the helper: pyinsane2.set_scanner_opt( device, 'option_name', ['possible_new_value_1', 'possible_new_value_2'] ) ``` You can get the constraint (accepted values) with: ```py device.options['option_name'].constraint ``` Constraints are usually: * None : unknown constraints / no constraint * tuple : ```(min_value, max_value)``` * list : possible values (Ex: ```['Flatbed', 'Feeder']``` or ```[75, 150, 300]```) ### Note regarding the Sane implementation When using the Sane API as is, some issues with some Sane drivers can become obvious in complex programs (uninitialized memory bytes, segfault, etc). You can get corrupted images or even crash your program. This module works around issues like the following one by using a dedicated process for scanning:
corrupted scan --> scan fine
(see [this comment for details](https://github.com/openpaperwork/paperwork/issues/486#issuecomment-233925642)) When ```pyinsane2.init()``` is called, it will create 2 Unix pipes (FIFO) in your temporary directory and a dedicated process. To avoid forking other file descriptors from your program, you should initialize pyinsane2 as soon as possible. Building requires nothing except Python. Libsane is loaded dynamically using ```ctypes```. ### Note regarding the WIA 2 implementation #### Build Build requires: * Either Python 3.4 + Windows SDK 7.1 (Visual C++ 2010) + Windows DDK (aka WDK) * Or Python 3.5 + Visual C++ 2016 + Windows DDK (aka WDK) (included in Visual Studio 2016) (see [the Python wiki for more information](https://wiki.python.org/moin/WindowsCompilers)) You must define the following environment values before calling ```python setup.py install```: - WINDDK_INCLUDE_DIR (default value: c:\winddk\7600.16385.1\inc\atl71) - WINDDK_LIB_DIR (default value: c:\winddk\7600.16385.1\lib\ATL\amd64) #### Usage WIA provides one WiaItem2 by possible source (Flatbed, ADF, etc). And each of these items has its own properties. To make the model consistent with Sane, all the properties have been merged in the same list. When you update a property specific to sources, it is updated on all the WIAItem2. The update is considered successful if it worked at least on one. Some properties are emulated to make the API behavior consistent with the Sane implementation. The WIA implementation emulates common Sane options ('tl-x', 'br-x', 'tl-y', 'br-y', 'color', 'mode', 'source'). If your program must be cross-platform, you are strongly advised to use these emulated options instead of the WIA ones ('xpos', 'ypos', 'xextent', 'yextent', etc). ### Other examples The folder 'examples' contains more detailed examples. For instance, examples/scan.py shows how to get pieces of a scan as it goes. To run one of these scripts, run: python -m examples.[script] [args] For instance python -m examples.scan toto.png ## Contact * [Mailing-list](https://github.com/openpaperwork/paperwork/wiki/Contact#mailing-list) * [Bug tracker](https://github.com/openpaperwork/pyinsane/issues/) ## Application that uses Pyinsane * [Paperwork](https://github.com/openpaperwork/paperwork#readme) If you know of any other applications that use Pyinsane, please [tell us](https://github.com/openpaperwork/paperwork/wiki/Contact#mailing-list) :-) ## Licence GPL v3 2012-2016(c) Jerome Flesch () pyinsane-2.0.13/doc/000077500000000000000000000000001332615651100141735ustar00rootroot00000000000000pyinsane-2.0.13/doc/Makefile000066400000000000000000000011471332615651100156360ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = python3 -m sphinx SPHINXPROJ = Pyinsane SOURCEDIR = source 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) pyinsane-2.0.13/doc/index.html000066400000000000000000000005061332615651100161710ustar00rootroot00000000000000 Pyinsane documentation pyinsane-2.0.13/doc/sane_driver_corrupted_mem.png000066400000000000000000001716231332615651100221410ustar00rootroot00000000000000PNG  IHDRYsBIT|dtEXtSoftwaregnome-screenshot> IDATxw|\ՙ{"H;cpCB15~l M!MBH $@Xdw,d7R!%bl0bui4ctGidI>{9gny{W_VUnoPUՎD"e D"dt=L$x"@_w׀SPxZRu}H$ rwA"H$'-K[K[.SUuk/746q:+d0 H$ DaO!H$0d2irjKDմjp4rQ"H$D"3A, TcL&rwL"H$D"$ 7TUQkD"H$DR0PT뺌!H$ 2 mW4N|wIi5;X8,y*j^-2zD"K6ֆ'팮s hUUQOpS___K%e 7k,9vl/T7>躎ܝH$sD"f ( ~?Ӌ6H`Ѵгҙ/&h,Vvgz%N9\~|#D" ɐJjb&^i</p5;|2sYĤ?3m[QdpnCE)s>2R8K$#,dyONu}9UD"H$I HD"1 QKyg~Nz+R8K$doVU~z"H$s )%O§kuhZU^+7|g~Nz)R8K$D"]'46F&b੨l$:tw乭[tH:|>{zFμg&wi{EQ8կvzzV{{7n~Y9mjjbuCsYiw:),ɏwfqхpJKs3cw8`cٶE?E{g7/ނ(+Kx@__?o|#.؟h`6~ q!}1~#B :\{|~R8/[>fPφ8kz#ux~UW7Os<¶rw礦Zl/fciΥ^=-[2KC;Vo߁;N=(ؘOEQVVU\׿o,jl,>k.a8կ}#4\|<_x5Ww}\pyX8t~ 3뛰=ʍkX]w݇㯫5kH$dw*~۷rinmEdm&H8<0p^ pm/Y?w#_z m2_֒l @;  ~𸵣iO.Ϟ=l޼d2gƧs}^zŨjvnR|`G{}n~EQWy>@4;ncgroD"grq5W?0>"xur٥5wgo4| [6f!_Ƨn080Νjgxh` @*t1QUSCum-{v'L&l465p:Kگ: AQތg.jjNeXgmtGwd|]؇ϫ=wVaH9p w7;h&S8NwrgSQY~KS4䉿TWUq=?/|{5xͼv~yǩr w,Y vx-|fZ(*+*džIˆxOr!||;<<TUUp: /g7utvu񷧞3pхwGQn,n|^JEE7rYE.={# x-[},k8~[gҥ|#`_? 5b1)K`:蕝; nyǎ <i!ơC47iS̓ 媑[\N~1~wmK/ᩧ,sѦ;ţ?Mee%3N~ye~l2y>s}~M_ய)O~# `ժUٛ0mI@1 @ ٰ~lw|0vׯp5~Ɇxa6n|qֆ|K_uk_o˖pƚ9e ~65꜍x^*kn>/{8u)pլ= Ӧukx='?%N#}o5;ݙ7hFkk+яs`|#Ǖ={ sqŋ7n}쉿=G~>_%y\|ٻxM.48(/%@Q<ۢy%b௫3y-(޾cǸU5QwOgmXOUnd|kK o xDƆz: mEݹk7==8?qϽɨ 㱈x,\L&3+2s&ܢi">ŭUW©N?d2 .kB}nm-ͬ:^|i;=9O ;ٳw/g8u)\ַ 0Lќa$e֭[-g<444r֖Vz~MWur@:ЁcG~8`̞ܕyrYPUWG P:aźXQ.#c ѓߡP &S8IN8Ǹmoy o-\rE! [њB[[;u2jLJDǡCt:#?ν?)kמ ? 9*dbz:K]]-v<\v)Cwk|pӍ7{?ۍi$34+y1p_H#INt\%_<<۱p짬Xi^d ?OށwIYsG1D7y0 NY8_(s8 EQ0 cƖ8a=nllŗ!i٦CmnSx<~C~/g}r^l%/Xx=y2nkV+ à!4Mgll^|\188cOb߿'u7iF}λos6rkfsG<8kN?֯㍯_yv줣׭nq;zhDR|W_no} '||{g\]]-{Mϧ}|%\ @OO?x\r[/< MMhjZ︓;8{~^޳ aoj8ȟ}z{I$\v477׿M738&i\`TC(B"`uqߏj1&3.^ɗt u goo;|ί|pw>n߿r/_Kwp3a7iM`oשrL7g;\PQ>z[ʺYH$'>3APp@_ߤ<E8~`?kw զ^3iɮ0PT˹ܿT*54eBZ%DR!H$'u"D"H$DZ%\_ӽ^zԅ$sl>sWY"H$D")iqH$&BQ,yd"Kyg~Nz+,H$D"8K$iXk|r"3\|EZ%D"H$gDi^6_a]4혎4>&rZfr%N\~}.-I42@ub(cp:%rjr2k,9vlKfR>HD"Dd2'mMp8f8._K%ݙ^cɱSg{!3>H,HLϼ}K$||knZ݅cFjH$D"H$% D"H$DRX@"J"H$D gD2o+D"\a+H }%H$r4D"H$XZD");K"H$BFՐH$D"HJ@ gD"H$pH$D"HJ@ gD"H$,vFFEDR, .J]H$dRP8FFJDu` ųD"H$E((c(M;}H$eD4D"pNحDRfT H$ɜ('D"H$ɜF:0K$D"H$% D"H$DRUC"H$D")iqH$D"HJY"Hí7kܗN}~'WD""c͚zB!^zi;&$hlf·_6?^s52_f'D"33Χ:}죸\.sy_7ڵkZu^~e`pٹkϴ˼Mobtuwϸ؞DRN7k溷^߿xq߶ Gx3׬x?}'D"LK8۷z?W]u%WM Oc۷ogϞ=X,|>ׯfO8rK=g~~~6j.ٴM7 ;D"HJcF+Evٳg/cccٳ~n^zi;N7 Ӯ;NH$D"m6g]DRF>>P'>Q'kD"H*]5JGwg;~Yt<꛴ 7j~z56_4]9EUhoor1OhooOP8̳>kZŧ_fX,?Nh{htS")3B4 }2F"H$1#g޾EQ0 cmhFeEz*0{9(~{4UK.!/3g% QYYiX][6l=TR݊`Z#`ۉD['7E=2(S?N2D44M+ww$dAN-]ẄjB!rk׮ڷkK/^^|E m/H" IӢE@VXya$߳w/A8+nAWWpޞD2ߘY`٤hH$bݘ13ZK۹7o|W]uYn۷og޽fv Tx=_E! M+ڮ‘PU I$bkDaf3_y0טo;6nȺuG>BpOQDŽB!E(B$B1'R0HC!,|q*LxҹjaN00 ?YL.B~v2 tH$BEEO:&LR[[k|rb1n73::JMM xT*n' bZq\h Jzl6s$&gFQ57|X&5" }}1}477~ycccSW*g`>\$I:::ظq#>,Cx<RhX,@3sMUwOt{|c4d DQ2L*Y,t('?L&3bbEp{M &iaJ4t:fc׮],Yp8L2$Ll2^~ebtuuq9}vD"XVzzzXx1 /l2t]K,b̶:Dee%2<쳜y晜yh({)ĔmMUw!Nt{|PsNV+p*<~hTUUU2#rtvvn:Keeyl2$Jgg' H8N8MMM SSSS"9^k>::rsNN'vQձm6V\I fFWWUU!( H$B("R]]m_[lᢋ.0ؽ{7ո\.صkTWW344i޽{D"Auu5DJ,Cul6`MӨ3044D&pݍjA"  a Noo/@6(xߏ0苮d`w>*bZmE{H 0q B|>vImm-@g)yjdp\8pMee%dX,F]]P*3=MeG9 rtZ#pçhllr-Hfp,F1&ݯ(M,Ol.]ܿbӟi9% tRzzzhjj"N*t:DUU, D]q:$I,u&1C(/t: p84P(8nN<GQ\.tڴaP:2i%k,4.!d5>D|EX96 ô> ^"@Qv}Hb6#Iq߉Pu8h4B+E"0eBL&322BKK 5EvWWٳWUB!ioog5y ]]](Buu5;v젭 ݎfD"Qq^c2_|]v$JQQQbl2***ػw/+V  o>V\iN~?V0v`]{8U'"!+?O`7Y["!\Ώ?pWܩcuAUUb6!Az-Ze߾}e^ à bPUUE$tDLqJd2x<tR3CΝ;袋L&uuuR)Y(L+??hO $KyDu]7$CKq^-KxEt5 X,}Ylq„D"D8rذn]l}/|iq\klk>ْ#_)&.d?S^iӬS.fY"HP]L V($BMP1tِ&G+/ax<A gD"9G _Gp:H$d́)HN ʼn-d.d= _ŬK)H$G{ 'sk)UhvjɎ/}>#-䤦iR96 ;R}CRK$LJRP7dǔX )%Ia8팎(qO^/T Fި H$0 N#n g tR3|a8N(dKoo/555D"l6vH$b'b ?d/5Q5, ^ G'"l!0`VW"M % `C&13{<|D"3O<7cll63ɚ1F2W`HӤi3̦%LbPUT*egXHR汊LRt:ͤ,NT*E$1CWkL}ZVfܕ)%I;}hjjGVQn{{{X,)8X,6oҥKln2H$صkr9)qEXAtuuaab1Z[[DTb066ffiZhFEECCC(¡CX|9 `BWnH$p\f?^/GhIGn@ė!Dn0iH&`?sϱxbhoo'NAkk+@LD0c3G"(300f34770@W NG[[ݻwl2݋墥EQꢡ97+箜 GݍcGC"A"])Gtbـ#UV$NDR8DXuD.a-DYTħuZVӂ(LX[UX,0)Ea9&dRc3iVH$"D")"6x&0wD$$7nxkHLHɭC4F}gC$aJ,,xt Hந(wWN8jሻ2~5Mn?N_ ~Hr뙌dvv:-)KpLD"96ZZ%NEvOyL~R(Ⱦ7 r*s߾ #DpX,+*g_2}d"ɉBbY El_JYɉc&%ǟÆJOJ$I4ɉQ5U"%{Z"98P"H$ecŴ-VP,[9Ub[^炠 i|14L6#"(b.h Bib.H$bFEP s_*B4h.r γu_H$->!H,H$1U(İ.Vv:BoX^sOEԄP(D,h4n`0H8:tJ͐###022TTTDQ3@*"JMkX,iS4d1k7Ÿ<7#ϪD"HNa0<O}L !bK$'1d̬/>mhɌۇܿjӦrwaFXǧf>k HO#2+JvOOX4P(DMN_sߙ.K$@CBMi[0D"8(~Eb0텬ݩB喃ò5YRZHL<,(k62~r,s1!'s 8Hq1ǴuV v~z @o֭ /2^.Q_.[ ;3 bXjA3lݺ]74(%׷~:P&ʶn?YNN\" `o"@43^(2|v3(#20w0OUj=cBb6>~3DԘ*Q!WBioDsh69L'`(YABV1κ)H EQЏ+Jq*H&t]5N)5M3oe<rq\$ &~l8Nx<>AZ˥"FT :!UXx;Fq8f\_Zአ*J$ɘ_,0KL5BPygW$%wfj6YAקiCdž5HQvBd8V:}@V%CѧqD",J!sG%6+ƍbEbJY43V,3f`M8vvu]jQr (RxO`ed2zzz7Ν;FzD0DK,36 7xejdbcY`͆a\.SXCVl ǃ(vDԗ'0 b6-;8qCDٮXJUu!_G1qE8:A}}}Ѻ.93uuug0avIHIQlG/ȃq_f@A]U ##tx1 zq ŧqG=S×H$Rʗum)vb;WSX;W` %vHjN%Z b~8tSb̋{XFb芕gz 6d1-iﵨf(-#35 TVUӽYOJ7攅w[$I h4EQ3q$1BXE5PrFnL&C vDLgJj2<`\vM+!7@n10p:(rTsEt"m-Z !-X@WLKPy)Oe: e2ѝ_|=c¬bXuaհz*$MPU6`(Nv) ҙ#'TOoY+nd0 6qj(J L(HNn;Zs9z]lQu$\>rQ,&</&'t:(NrױX C$tqEQ̙1+"c BMR"BI<7ϯ,rp:@Jww9x<As&!-Zd``X,Fss3@ `FIFd0 >M{\nb99=uӗ \Ե4s`':Gj[CZH3*(9N+4'ь/$Js`.x<7LFQT<H$ pTZ\J*-4*T 1܅}bqq "H(@w\ " A.Ds09ׂ-.x̹/́l,_,L?˖-#288h&8|0+gG`V^/ի"J˰lXVZZZlzfd2i***Xr%Hi0^L&c~ӉA4UUUx<~?---fi>tD"A:tRWW \e8렍} EɊb%l>7U>tF4B(h! bYdTU!He]9viгb[Q4ONHX$D")h4J$\t(_"Flz)krAyǎ,_!N.]J4d2K.P(ҥKQ>VX޽{Mo#a~?0661mmma~?===\.t]'p!l6###Ϻx<>౺\vQߏjvH `Ŋ|>sVee%dd29áCXhMXx1tt:mZ.bB^ aud2j% hSBEQ\.UUUft:Mmm-t:fOsYgONp hoo_$K/rJdݎƍٶmW&088Ț5kxTUU 4n۴\.; Y˷f0|*peIn40L!{<4FtS%i IDAT*TdRTo>ST8gΦ?рa(hjf?ڤ'T3PH7p(` 6|QD"H$S`.'i}¶j"B\Ñh.j)SB> rӡ n.M!Xdqa\p%\b lii1( TUU=nvEJBi3庽/_l (MD1z)J6 \gC/<ɘa E1"f/ E($xKq *VTKccJ A44|H#`azX5r~H"H$RJXh[Ɏ-v|~J$v򓸈mn5Ph\\(|a~S###TVV5pġbp[|GeBmp1O힛u2D H$bƈPHDcN2_ dBHT 񙜗88R:kV#q]EBeDDd24}BK[dI1-4.& t@t:M"0cZ,DC*&zD¨ժԢ6VUD2A?Ouddv:fZ0߿Ckk+a"bas(UUUf$ UU|rzA3T[SS|>3C}8&?1 LX̍$؅ փ .k%[䪀##B,R sb1պ9M#l;~EE[޹y8Ģh4DQ3yn7㐋D: 5I$ٓkCQh,5DQ #{dZ3)|>}攄~b D"G,?TxPx z!钻J|D"*RYYafX09NRfh4(.b$I^/6Ç ˆq:B!3~p$1fv56!rnhL&C"0Kx^kkjj2{CCd2jkkkS[[ˁXt)TN/^L04Cv3D\cc#PiFt*n|//X,f}r} -,N]]E5U/d .ʈ( gC o:Yu-@MˎbTMEޠ^]7W!k<%ɉD La%SBX%wE}5EXiX,m>R"܅["&oW^=a_n /_>EL>!RVZe+b:BB~ؓYrm 4MrMtR9Z躎G4&L Y[[kff1ITTTD]2G T*E86B `bAT!OLG",ϜّRֵY#LB(Kf1)%cNT|> b$}2^I"H$ ^ʟbE^)!m6JL )(ʈ r]U`/өsHZq݌oll ׋n73i8>Z E@ 53:FFFaxxJ  {, }߽RU]]} M@&VRʼn?P8Y pYc\̒078"" I'twoUus{}]TשKw'N}{S< *lB!ąj [սU]ѵ V,q[ЕJ.rү \mm(=J؝R7$ZS:l&\p"%o*V/D\0Ylt#]>+*l&ߋt;B5'- !~sC^'"aro\MyXLܺsq!B#^+ J߭Jq2vp>}:y~CCCp sssLMMqykx'hA2ɲZE9sN<nqк+&:I#H[`I]e/`]tv~[ F1L w籴@OJT& EkEѣqV[kxXd_9T:!Ԭlm?[V;1WkWfR\bsA|A:KKKII \ZyTZӋ%vW.ƭ[k) IRDoool6 uf!1>>l}rZFPHAVW\AZ?ST::066~.se۷oghhk-OB @__F9P*^+ am v9uԩIf3hOlYJJX$"0dyyZ 57Z0/T|yjJ" ҀMKJBt_tWh2}rGz`z.uvlkӵc Q^MtL&CPHvFQDR JAkiiZ}}}I)JEUWo7FB7J$qW?\&s4G7ML)JXk V%~cX,&w ˖!tW,;4okMMt=}O]9Lr-uqo6EM:r%Ok'x69Y)EdZo q1(ڋ:*nc 47d2>J\R07?OPb=n;[)غZ ͍-JLNNyR$YZZb``IFGGy'ؾ}; OP`jj*2.//STI޽'|m۶O!###Qxٽ{wpQq<FFF~ݻ<) BAmxGرcLNNK__=I@t)w]r\R?>00lrYѮTayy9uor\ܾ8՗A ҃pϷۯQY>|/]sM^hOgΜP(p1vT[&Vqa :ݻ9vXeᳳdYΜ9{f) `a׮]LLLP,;2bell0>> ǏOE/ꫯfnn'NeE###|䪫ԩSA ?}4W\qd2d؎10'YMT2^y^r% ʟ {l5pV*X/1?PY\Xرw}Sa6 . ccm 5(td=R*w%K.c~);*tK"߷/^RGѹ08%p,mSSSz,|rŅ-ZPXcM2j5Yi.XW*O}vLMM}vN8 7ܐt$CjZ3 7|3'NkMnH27x#f$#(*rZF>0jӧٷo_>}:!Ns``,gp2̢ LNNRX^^<|ߧ^'[(O1%Z 5@F۫Iי#ͽ^.3  GexxGG>gll>~* r,ǵbaac Rrw]1\yxǑ#GZsȑuu]?<IIkcq4sss\ve\UV4=_=رcj5/$'N$EW^ye}󌍍1ڵ+Y~N* J).rѾ.ͺC*T~JUtB綻>!xS8/gΟy!3Bꘅm]]LB<# 푄B<=ί^!B B! B!؄o:x !BnZN"KAbLDUv>-էZrudu}-gtMm7&ZJ}\lO˔n"B!b=]01nQk5^@ڦ+3cWƇQZ%LJL&|c+  IDATK܃1_ws1|wo>;0 =wȍ7޵adxpϽwAٽ[f!B2Ɯ{gl(TU||"Q){>Yck@pw}|g]O/} {g?ύsc~o},..+Kg5P~СͿB!Mj ei0ruJ5Ɯc) cLkR(JB)p`ltA|oVqPXf}/:< Z??Jn{<7lyf:}9t;Y!♲9񉓷+2TFeas!)k-C{ւQRXnA.l}Ze,//g6485W_G;r~]R129961qG2j}nt;wn阫Qy{rzr>qlx!BKɦ畊Κ*|fn~y,i4DQDT" rAFRY-g [l6˯z+nY|M7:ϭsj k~?/~ыx-qnB!M`9fnO ZaaLȄIF`LV(dufF,o[9q˿\>f>/t<^R0{<%|]沒-.g6ovV\>oy8~g_q _͇? !Bqp֊LFh2)V>2?V9%fK,--0V+E=U <Fr.A7?3?K_~=Y>dB!-6,ըa@vtqy>\ nCqeWr%2f#\.bkٵM+m;XC{- [72<̝~7n9B\lO>nc'}5`;^Ow ML[o>7:ύYDbr^_s>ơÇ}]{-?;WX!5p6Ƣexx(>[ ؕԵ" ߞ5C0*j˥=8hS@|aXZKV2[QFS፷o%_>A>UW^I\ޯ~W[UOkx_0_>(\ynt;+O?7;ృy. 8|0wx-B!3}|nn嵯EZ!a".|fKK}Ud"  m;pUɿJ:[|X$*o-'jfgg%?]} !>ǎ`,O!Dw?J]flgZP便mTf^{q[guiC0ZQJW!B!6oS6b%h Nr'鋈*9 ߞ[ͱRFb-{9QzkpBB!9{l;U VךEm)D G83˙yg4g(T4rvw3lkEJl6 "X|G{z2-ۄB!fs^ZkjwȈhNZ_j%5'N}>(*!gl/L&ö!=1B-9'meB!s[ <@БEfan">fAdQ &/ B)ȢBkda,DƩbk-&m=s 59P!B<7t u{>?S}=Nlɗ*4a1k( &`LD}4Zǭ.ҳ֠5&"鰡<)ou_hD !B-8{8(,NN8}vPݵ3 L 6+vv ׎..Y㦧%r0 ˳k8g!Bqau8[0AsDTJrZrezS쫑4!Y#L{r/PQJBxDv{(_7pZR,m(|ߧZ`."B!*]PkRgbs-45q pꉃt PB:.jtjݲ)-SJ5B!t85!4M|'ϳg"Vs0O=iMH-B!x6^ 93Z#>ŞGOɩ)4Lȭj KɹcNnb}Cvr gC!BqVr,CÃ̰}35=,//Q,s2Z+,e-hbJ{(VPc$-B!.BibIѠX,jj144yd|\.n5:\d-`he!XPZz(Y:kB!8jĭ,###d2(<<ϣlRTZc>ͮ[\.qAbsqIsBB!Ė[bvv ֠N e]ZE9CIB!x8(:UVEc}*}*q~9LB!Xm8ĵQ*%EXQ:BVHg wfZ!BԤf_s Ɂtm1.>g!B5[OW~aUo x6`3B Ðe+u՞VXcxvMI+Qr1 l"j^ێMXIB!؂-ΪF*ܺ9AY%T򘭲֒x Hp׀#nXdeEB!؂-]u;4 nn(Esz= AQ `aa% AEa C6oeT9 !Brg|a ^h(XMXsG8u3sl^/]8+:>mwhF)~$lB![ٶ?Ak<)z3, _,Q8Ag*}QB!؊5Κe*JaY#[(`-Y89Esvm4 }5dy0jlu3\f:JB!x8RD< ҘgU{|QE\![.]+^NZn]T9?X)E&)|C<^(՞xG!BN5(0L>Ž^B88ș3Ԇz2ԃ|E$YMZ0СE:jFNQZIT:A(B!BlV9RUՀgYbp_?Z oj j2o' -|(9LUVI5Yd2|'"zz9}4|'?OM!B.LB!ؼbq`6FΘO@2v}&B!ąpQ$UnA +ncL ppw[zB!3U#%bq9XE!B~O@!BK9gO6P!BKߖj%`xV`JVx:V֬Y ͖Fk+ѽߏR hB!bs8 9s=r,ZBߏ)$@6 }n%lNOS$.B!ֻjXPZ17?\.@Di5mcc,.,bX闬" ̾S*J8k t獵$uvd1-B!dˁ ٳu3bzz0 1Qɠfyyk-ʥ6\?ȵiEQ ڣ0L-!Bsl8uzEr,Sfj @T" 0dyyZ5DA댭5(QoPr,//sG>V PhVZ#B!Sଔ"2[VA+.xuBc#=i(aМ&RVfϞEZ+fff(Jٳ(@)E]9 B!bnzY$ֶcx>;6(jލWkӚQd|ô3߀V}to?tl+B!bkٽvs.\,ٷy/Ժ]uPL&㹥B!BS!B!E",B!#B! B!Q|m>SqQo!BY]5^_1V ZAd,&@ŭ)~qznsJҽ;9!P)t>V T|-S۽-֞}pk J" L]!B39b,FfExZSVўGk#:J9BbVfL\>O4ah4d|j}xqG+.B!D7]gX,333r9|GiVy>zР'y^{JP-X6>[)IPThZIV5 Z,r%2qVy` %B!ۺiJ)2 \l6c}jJ$F!lZ䬉|@nc a244i|ϧRbOT9⌸wଔko|#߷/q/?y~}|fvvc?ܿ~;??1^*^0:27-> gϳ{.;xpcny=qЖ_O!BݺAchh(p0j;m8Cl,ZǏΫ"޹sgG01}Ϊ]ZŞw>}jdǛ_wF|ZW]%n:Ə|3@įZ0CP17z?~M;ŖB!xT5,;c 1(ZӾ_>B0&nGQ~}Qa9Ka+n/~17WuO풊aMLtѣ  Aj>uky^2ӔRoĩS|/򜎹\>oyGMLw׾O>^!Raଵ"|JAw37MLzqqQ:߇ҚL&*k+:}L?v+_J=u4vK۾};Nضgn;gҗ{2s-F9~8X !B\J6 8tCG8ѣ9y4F5 ?jUk <<?̉6YV>"+mCCÌmy1FgrX Jc+Lا~jT*ۭ>?rS mJ{c?6Sɶ\s;:2[9f7_^nzի:sĉM=^!RaW O{84[-b[gf/BLO7yv/dE2QQ׹_-p)~F^s Zu]bbbc[:jM7?v0w|9|H<]h6A17z>1>?kxkم|B!q.BR{*G&.  0IG`4 YJL&`{qkTI-OT+?}&y!ozR#|;jSr̉G##v+2ahЍ"vLƧ&II$1+e.+]j(Qb$Y!B<κ=A0"r=ԞfEu|! qqr+Xlvskڋq9.Y}˺B!bugU*@Ҏv#_Sڽr{r`Y0R( Jor_}6Mv5[ھ~1wbYqB!Bu (V&A&PcPZoT$ԱZۑ Nk:D%xڂ[p(B!f8Pk q@kҸJwu Xpv]KJ99hoJB!8Pcj%`՞G=\|?{V /axwcccf!B!DZ(7:1B.W`||'BNkCf%VqĽB!u8Dch4T*U|}8^fgg1d>d24{Mnt'>7}h 2r[!BlQZc}jYPcjjB}{0veʟ^?f;8Eo֝N-V"ftWk3WB!XSR @h@RAk,ZFNo_/r95:͝o֊(X\lu9"1d2[ΚPwmSB!جuK5/SY^֚ d2+RpgYleNEPB1dY`ue.I:ڵ+$O-4ƴoXXX$ˑdÐ(2,,,P.W6 q NF<B!ج +}Iy؎͛冞~}/4 ̙%$$fc,D@ ( گKUZ% Z"?ŭT*J-"گR-Pp&dI&~9g朙3B|?<}_}o.>"͑fpIvIݍzTnX&RA0Vtlʑr(;!r TzVcf#$.jbA='ܳ""""f0o޼k d׹m=1T2+G~ ff;"""""´B @1A K$&'r/ٖ4">jg9iyN*;<ƪXCDDDD:7m4`4 fSLTaLN^0ٻ: ^=by1'骙H=54D);7ugW-""""΍CMviq`-'TmgglMsݻI)sT*zg|>O3a<EDDDDu{X"""";gF;εid6e-T@i({ ,^ ARp9HqIbgS; Y\mNH,XQ*ِ9{{.\̦E\+<^j1P_yϵ!`☌(fA3vl~[`P-f؞.?o!On^$XʥZ<`msE`8HYʹ{7kUK)TaM~vNcJͭ쬵 CL̴I99蠥KUms_3`ض]vbѲeA@yʣ,8|%a.C%q>ƄIgvDQ1Tzʇ󀱖#8(& IN5+4^7Ŏs"<o t+WPkg!4'=ڦ2\YDDDD)K5`Rn剣`z0ckի cq2ڹ=]sW_6 M@εQ  6Q&ޓ2a=z] P&d Y~,o2{k3q30վpcvg"""""Ϭ}>>U%'nVcC441mO}W V[%m; \nF-Y[9_]j=h_VtfwpvQ((DQ\pttnbHI:c`(dY(yMnSgm """"ҩ}^a!c;22sb(\=CC;\.(n(Kds9JCCC1Т|ÏvNuܮ$k[_k=ϥx0Lq޼aHEMCQsy==}M\Rg >+h!Tbjxrxt h_n<^~k'oy$wt됬r y88]vw{M⡐,""""{>9PDDDD@}'y6m8dp1` q2Uo [>t{ XZikIB;x,C'O:V3l7k*n a}i/P*(A#bq>ͯ+c4B\ T%T|w$;E1bʳEDDD68ZMg҃%뽔|I"qHRR)ݕ:xB*7n$c;g R.$>y9zvEDDDd&frw244D.)Ktww㜣rsQ.\ŝj$y |7LR?@Eb0 1w)Wv.@""""ҙYd5S==brKs㝧R0ܞJHض)/_NXR!###d2=#i{;wrЭu&ezdÆcc nR)]]],]|u/y $&'-^E10CkzNSQ<=\7lŹl6LB1vq3w0javVw """"7QpnSp띛ՋyyQHgOi þZat5dzjI^~CnlI/{A`&ʤR뿼~_<~)8/ٯi HB!"2f_?ӎHj/9-cpѷDDDDd?5ƛ|cH?qam9$ C h{\}}oGWTRgEA@-^g{J{XkfEADDDDDd*SN4PT&MګZ8 `V1Mx3qLRizZ}.""""ҩZxn5NkǢ(6UcL}$ĵje2kZjc T[/ȁmpnܑjGqG$jZDMkR4T*9ja|⳵{=e ȥOďwh8r[__Svn>|?w=U{ő,ֻص?eXEsY۹kn坯y_9l_glxrwv{,ӏ;O# hEDDDgkm˰Y;koƝf\S}s㽜sAPom{j!=c B=x֨A@:x[kIR[>֓9e|ƻ8tI%W>'r99nn:ǭ_>5_'֭_mr-g^z#OhJs>v?^}(}l;Rcy׉b7={.qrٷ)U]:GYDDDut:x h~!qNk!kqL[[?"ϭaHyҎt:]/ۘ[XwI䨥|nsCڿ'dRxs˿ (~|ś8r|ztCvO=f;GKv`5/gM׼oΫ%sgi9;+Vf|ωy˞gq;w>/?մaKp9On:>qFg}'= ^oCLJ14Rb|=8lxj7L{ȁmpi׉cO}==f^3Ygoq=u5{{<;GJcWz`k@_w_eu^{!v Okf G,?93/i'y&l{~Wh?wK<{\X}䒖Ssw'*yh\_-&oyX=L]ޮ4]ilޮ4]ryˉ=Bo;May| x+89s?Tl5'kۭd~+y yK'˳q]ZW?*w=O$""yk8?kMp?_X>SJsף8Sf-M(WAMM{@2ͥ4GZrQ{^L1ߴ{޻aͼu/ӏ!wǶ#^GRe< IDATeܞ,Wa^鿈<˗Ӳg$< cwy=}[Yd*~-dR -8:ʭsVx-<뉈H{f;~=45gqܿqU{_IDDi,"OZxӈ<Ed-p""l,"{읯y??m۪NDD@YDf'F*L'9,u>~*GYDf}ox!oya :\ȳ_NDf9.Xwڑ'#DDD8ȬQ(EM9Pg'gq^yGn-XC5?eۮ>~Bؽ_z \p}i>↓vћO ]}"""O?gحwmt7~8?{9 >nţ㉈Uz9PD:v7O}|gvEDDDD:,""""g(8t@YDDDD """""Pp适HEDDDD:,""""g(8t@YDDDD """""Pp适HEDDDD:,""""g(8t@YDDDD """""Pp适HEDDDD:,""""ҁp_?x3־`Kشm^v<EdFN9zw2}ݙzsj^q$~p}"""{J5Dc޽vRhnߝ >gDDD>gHw;i ە~LDD䙡,"9sM;ͫ[]dhě?q#R9 gQEDD 8HGN9zYχ.ïވp霵/Xy3"""{vE#z~ ^^8}wpм<_z˦FDD@,"ThɦϙT@&Lt'w7˾8xq?/'vNyȁLYD:?>-Ǘ?JWeg)9@us/^q$\o}M?X?>c'""iYD:kM_;>C<3Ec?}#oso;7=O&""TCDfwl%w#Xe,['wl߿W;"","36 ExajyWt:$"R]]]XcpGǀ'Oa@:t:EOc,:oqg?o?k{_>|a 4}NY}≼3k^|17|k~a{ܵ^0?/ۮL5||㍬\zh{N=ӹ+s|G;;9Ll -;p>)oւbO^;r],[ԭ#oG%E8DQ1+Q}Qt<^G4?BȝwM?Gu~w sw']SO;^_&JE8:tkח\g%9""""jcqUK%{G\?v'jisDQDŔe2b9wDQsI.˔^J n+lذ9/]TK*jћٸq# Nvs]^"xࡇf|ωr}|n믹|׊h8'}?c$m Ͻw(۶m#VU(y2l8pS(r L'=ў_w}-u׵?⎻ūW?ꪦN{ů~ŶZYsG󎷿w=M;3g;vkO9~{˛ȳA$9Nӓ LrB!}vC*ﯿJqc9dx0=9- r,A E>'c ¤bHRa]px&5]H2`fp=;VZ֮c9嚋⋹fz{qeTRL{+G? O<w_cIg@ݝlATCoQ5Z @:&K'/VO:6:h|k? }S^#Dj]~/3d?tCd|zyn;Z3Ϥso;On}z|Csٜym8ﱁ%PZ;_tap6Iۼ|z|/3W}af3f)&uOoi]ODD͸Q>εPZ +eKaVґ}-֚9)yA# 6 m}e( j|Rw!0#{ľ C*R)SDqD*fҥؽܓYDDDDmpxp9 E``bOYJ*\)bdU̎!JbJ z8}U^f~|PH'8ZOD#99p+'qC MCVfŔ#_rt$1\g;[YDDDDfmpm|{clBa@>#(cgXkvFIg u$ݽȁMj_0tЛt0pBٛgOxOX#ۇXy,ߏ۽;MҹIRKXA8ZKOO@` x]2{L5c' PyqyO|o[xa;(YZ>PS2uE]A+R1AL@@zEHS뺤+wc4=ֳ{x'׭,""""{[J2*CyG9 .G]0|ػdX 06 Á_an(qs2Le?ԹvބsvƘ5VUytmp(WGl[C>ʱo據hg!zOa*əH-&L=6{bRL J i~WZDDDD3m')(C̙3Bcfk QTX,AuI# jP\cl@WWnzj}M^LY#ɰl2Mik1ATwu=v>ă7-t{_:`!cttt:Me* x@}tw2hpDG@jͦ3Esp6SccG&\ͦP#L&M&c``!JNIwc|P""""ҙis弰>ĘdG7\E䘵0Hy ;ۗT0ʕ+$BwtP}0w8e4A9;8Ȍ$qN\ܹ?fȕb)䲐58aÞLaЃWVM[D!""""{ߌXWĵ쒝5]cFXMJHfUCDDDDfbF;ξ6>;e,RMq66Xb=Ж'v]=ı&&?7^ةZvEDDDs탳x#[w0c-_A{rs ,<|a.6i]WpQ.k-tz<<#㣶:E""""wqI0 <X_Gbٵ)ȅYLo7*xL`06HI:b Q|ŋ%2o[ zE'9_sV϶;^hLs gb`aٹm<]s{ BNp:CZS!@^Ktsk:cL>zbձCDDDD @C7Mל{d8|Av 9G'/ ւqu4˜)Y=EDDDdokU{c-GRxOl; '/WĴ]5&2hWDDDD~7jk}n},^ZpPxpvalb`2- .x^`1ZS}[p&w"""""33RK%c(e0L^&B|>(t8 P,d2DQDWW7 OɝP=>-ڵ(l8P({y1ܹ("Jq}bH6%"vT*Ouhǹ |ms|ܹ14t;/Z[̵{zq}}$> ޛ Ѭ,""""3J5A@'{H&?rEDDD_gv?oY!""""Oۅ׽U*1mX6---""""}EDDDDqy6k&!% ;XC C0nqpTU_3ɀJ2]Ds$X*FU֟L0göm{9Bq2|t&SoNY /AϬELNDDDDdoqpvc-T*JZJTÜX*Ὧ3DQT*MoooT*[°ћwgDDDDD68'mjL2XdɒE@sz wP,0 I \ak]`Ѵ J6󫣵k9l5^r;j6T-""""{Z&rv!WG 6\ 4O-6n漇@u""""ҙ2r)ڷy<4S?65=]tK~3X̃ǫHGptH;_gxƤks|YP03 """";o8'Lήɉ3mk}Lg\~!""""Mqsךg¾zt@YDDDD """""hq~y/[DDDDڙUMo>r0 qޞ6/ 逾yym8Xb)UI:GGǷ|=䂠~TDDDDDd*m$8IBrLEXk# KFW[k!jݳ' û !NRtJ8)JAܹsAjTBqelܴ=K/x{ض};x|kۮNW_ME> Z(~h`?rm .RjJfyi_cXT:CWWyzzCJ2@\fpA0\*2{RLJDZ,@u;O;bΤ"V-g-gmÃ?̢4O<}\}ы/憯};{￟v5]իe53iO~o+VCM{矞~:w\~IyG;;9Llc wI>8;1AhޓxsV&醑djØgT"Ge"әGK׬s5G>?)ysQGq?]|0y'O:{/?T%e'-l%"strskK.Y9Pt=ıǹ$q5]\ Ԇعz7}ܞ\L;wT8%#(E`7Cs' n+lذ9/]TK*jћٸq# Nvs]^"xࡇf|ωr}|n믹|׊hq6&il ثLZv&; Fض};Lk JRD>RdqE 12, vyyk_Kww7vu-?ORX,6hѢIןv-<(=p5'JӼsfz Œ9Qox_j>~##kz6]G]5w/}5<~bFofJ?aX 4JBBHwEЁiߛ̙֭K.iK%;D"bm-T pکr㷿w:qF.|زu+_WguωvUy-[d'+O;\yk( A` {SVAt4!b0X0::JEaH6!"& +|wwIz͜v-/]zk}wOIk 7_t)瞦5V\ɪ+77f7uru<{R,5<̢zpؼy3K,zI0{vfxxw2V9t ;ҩ}}rYbqau2%eXc۶mqҏ [>IYdI4Q iǾ 7^|M75{ۺu ,X/0 9yj>?⎻ūW?ꪦN{ů~ŶZYsG󎷿w=M;3g;vkO9~{˛ȳ;ΆkT$l` vy5Q =0bN)ON[Ƙ:o'{ۺn-N#;ӸEDDD,40p,|(Ә_Z:G>'T}LˉqwO1>1O=q܏X7cuyݯT,""""2˳ΆZ<6Ǿy64 &ĚT#MyjR)dx X\\Сý(9v3CwFkǺ,rJmΣwÉǏq,5,=Tn!-7,Jv{Oy rk!qI>~ pĉLLN`q1lB`֭F@Bf9gdYF$}-|]gZn+pQ vsIg,E:(HӔ8b_? y3!j7]\\СT" FzNbll<v$)Q.X߄KJ%nw^\Ldi0>0JiԎNDDDD7xs  /,)Qv\*ST* AoжZ-#20(%%n.6mΒe6tK߸@D}EDDDd*IS8Ȳ 3Ii^բy؀8NyODDDDDY{[l兗>g9IJ{N(  z%ODDDDDlqƌ,M IjOsNgyXB% ضr EDDDdtvX:8m&X539o',""""Oug[ kOdށ@z2)=8q1N#0g#7p9Xki `B4{gNЮ&ԷOSuج똻%6u EDDD50pvB <| mǎ:3Ŗ-SX9vQ^ܭX!oLaxjJ`bb,K9y$9v$ޏuރ6Ȑg5o0}+#'Xxt#[H|^r!8!{:!c9J=`g] >1oLcԠYDDDDdT8`FcAt2Cbv>}@)f0lr!NJEq!o9a#:~ɋ䉈պ<1ز4!l63M%ia}whox3݋9+2餘%ˬ1ZuW`݌ 5Ƨ4O@9娈}1;#V-ϭSj󡈈F.xt7{;NHݦ>V'R8{ω'FsDah6T*ҴC6Vx.eh""""tZK5nvMRc7GRdYJբ^PI<˼@^g YdlEtd||r)pwedYlbGI:nboWMf099x^Ai-ݴP''&VCι 3EㄩI=,f*;OS*WwdY0rӡL opkKc]gyrZ80j9GVݻf`YFzff'۷E[m1|#sv`ja-""""#8>σWORa޽nY9H8&bs`ĒEux~jEDDDdx83#,"pؿf5ò7 ,3шEDDDik9>mIzwn~O>+8lO8[򿈈Ȱ@{\{mt0~2z-jEDDDdx}"""""ge?"""""g4 A8 aY믻tA5""""2ȪzkXlwbdb$?cO3pnu?hB=3*a`dr<=DD,1s78h4t:,I8@(D@#NsFɓD#NgFQɓ'IrL"",l1[qQyd֝hf8gqqJQEQ6#"#(bL)> p1j6E8=YQ*򩀶v;C>Sx/yYws`7[kfIP8x >?Gcnn">OS8l3q$IDATi$NHHIS!P՘d|llÌ)Ͽ2>̷o]zk_W[n7ܰK/z#xӏ}|Kmoc^`|[7~cϼBwq}+=sYvg{>\} |/~;T*1EDDDf҅6gqL$dYFv煉YE1)HJ1YὧZl4pQTT/,pa[>3Cm^ w>;f^r|={^m6*:r {^peyσ=뮽x#7rQ/{?k+_ɝwş|io{5i3C;gf/Go믧i6#MEDDDdkq:&IRZe vVš0,$S)jX1I2Y!NbJ/;[| 011SK%lpVE։k#a~~k6~~]ΙeJΛUW^'?inxօrǝw2w_|8q[>e|ᮻW|ſ""""gu3`dgf ȼǙ,?nȼ'⸑g RryzoFt/vDŽ +kx\y_U_pe4Mn.@'s^w޹WYZ4{OZ ~OEDDDL7T5Ȳ@(:ftc=EFnp{!hdY#j'@ZodYF'#ަDQxX,J*\v̓>̎ݱ};\|1_ۿZ^糸ȏ{gT-svc_s%/^Nh-gW3Pry9r8woopQʥWk_[v|K/2ϽR^<3l69~;gfz3C8s7_DDDlNIZcO)IȼTJR.wye +9p>c=]Uٱ}{7ww&fnw5ŔJbasњuyI__W{50};z{㘗\~9o_~97}sssǗʟ9ȑG{Z뒋/~7o|L(We}wlvvvY ."""=ZFNڡhw:D. b.I:0gq{0$jB$ jUx?|puJ(|Tk5ȲzeF٤0w8/2B˂MO}3wrA:x]{-tjsWя}lٽϽ593Í7n1 o#t6|F˿(oq _<""""gu8{B q^F,oms5@\V+˄n!غsBq y)7x[Kw []p~6/~33|[=9o~s5J&'yWګ}/|yW]뮻7Mll=; 믻W5̷R8~?f+[[!Dg+Aey6g"=s{./@_ć?CyLy2muŶxW z8y0qǓ<޻ku 'F93I2TSSsi!rDX8t#?(wo,alf+N{wZNdYJfi$)qy7 (""""rQ`|3 "Y '<|a\ZmZ>3EM x,QڨuGlA&FY lDDDDih́|d <1f~~joN<IH8}LVNgf5lvBi-""""g7 yw( {a TUw*m'i0miW45F\NFHZ/..pa,1>^g]Kz9þ} w>w7щńpʵ*cc1!=9(Jڵ}>0rzVo !E'8xjV m8bϞ=$IIYgyR Xd=0MM`R]Љ]ORᓈv[P!"+2!\>LZ,|yȲZO\p0y@_>+h'ۺmHLe&;Q 3Ȋ~B~c)J1 ˈvimN)r8wX0˚=+y gb@/""""gxOر#G$ }|כȱcT+U=BÜ=o6H !\$,# `L7@˿T+f+^J-DDDDdj{$avv6C 6$wj"gsB3΍"Z+EґzNb>Nyh$I q< 86+2 EDDDdxjtȕJޱ@\.MSͫ^szV*Y\\$N(nessv9’"""""j}єȵ˃Y۰#@\!M;* jE49G\\*33 #37^02P,""""C8y6]?i<{],uJ98ٻwoyi;=\^>des`EDDDi@3Y\X 2q\@)TkT*pA+NG#MSJm۶=fV֮.^0 EDDDdkgefF=fthxm+6y|(GV kݮ!larPuK5R(>wg~oq.M=&"""""g FnCd I~xoo(Iv8V[]^?uK;6mIȦ`H+:w2c':ʅ5'2| 65dd>c$IBxɲ Όo2 @Qz3ncQ͌K/+5,mx[yDpK!6 uK\r%]V^ҔA{BzxHq%\׃K/DKz8vm8p% uNDDtݳa,Z^0J8ɯh @DVY62 ^#Q]s~/ZsX_fɁY1d">{'F\KD*?sz_DDDljD䬡YDDN׍IENDB`pyinsane-2.0.13/doc/sane_proc_workaround.png000066400000000000000000006361231332615651100211400ustar00rootroot00000000000000PNG  IHDRhh`WlbKGD pHYs  tIME0$`< IDATxwTޙ6-.{Y ؍&&DDMQ/{7|#&[( (Uz/,efg1Y]aBa{|sG^PX$eRRRiI((((޵#FPPPPP6BUU_Po t~A~ JMAAWEBiZ)RT$4}W 8)%%eTGGBH)U)((*֯*nCJI(" Ӫz^\ѡJIAAAAAAAA$nnlL@@QPPPPPPPP8ŀm+>zbϮ]Oq70 <+((~'!:x ~s[{1y吝ccGg>їE«P`LRJx/ t}dKXO[ Ƕ}/ˊ+((([F/U _}qY`22PuOqeEpBA) 1eA@uD,SU+((((lݸ;q*4E(c@y%9 U>Tչ" aPSUEsc#PC rY E _{%6`AUu5/j͚eNnn.YY?p6 >%>Mk6ӇÅ6k??~<߸Z/_3gvd999Bc1CbJK\iFQP6nDVV1X>gf{A&nw9yQ[[i>ǐ!,NϜ3%Kk_7***}i1o465پzOneI< yHSSw?Naa!o;vrJ'O:vuwޙgi᥀aHV^8ͣ\M455xɻڳ~sHf6ywhkkg) eΜ; }Djj*W\~Uhmm_W^?/'s9[o{PS][恵NSR4dZ NvN -[ˣ@ c$taTi?8;=oq={~ϣ<spjJ 5rddfw^ǓllgϦߟ'yXr?.j~/Ya#/xK%;;ѣF_z cz4RSYj5bJCfF L/)!d՚5ijj0 #FΫS<˃ib/ښZZEQ:n>^Κ=& R]]pQۛ!˸)%'MtсaF85tv-!,}}93HNNV^zek9cG{ ҥK/)qOmM W]y_җc箝C73R4t( xE$%'mf:q.¾}Oo5lv ߽̝;P0ȋ/c{lamq 7DuuQ+Zj*+g@^Iݞu}z/--O'|)ቿDZGߥq7V\[?cz42.B~xcݱ׮W_%;;#z9hJ,}=m2yg̙͆yBp=wsҰb^-8g<222իe;z|wC2f|H);v,;V^oh -5 Q#G2d+WBJɺ yWqfz4>^Ot>|B]|J˜9[FFFn޲2Ә4{:A zD:ZHIMP0ԧA{e_y9 'O//~/^r1'7v yy;HMMFccS/;v?)Ǝʯ~^}ku),2F~1mTHOOh[[[ۥ!Cx7, zU%"ǚMG{|`Ϯ$sU'ϘilӸ9T1 5'iFFswss3/ <| ̎sk_s嫗}^Z@[[_tB~C68--$C$''/-ں^GTWV/=$47o>r @JIEU30/$%'HJJbQdge=456a)cyU>R8ݥe5 IE6?,>K{$۱eK5n\#1n7?ȈǸǷ*G^n.6RSS_!aCعkr̓m?K.0׳ϱ+W1<^lt] `;w1j4gLT[Zʇ+Vp̙]lܴTUUK FT5WsҰai?|~=L0>roj,3z[CJJ T222B=bB30⫶0RyԊ?x<:Yauh ÿ~]ws̓#_gdggW->x_BUUƎ`Eo/ZMZ?W_? ECߢWG׹;xشi>˾%.W/ / jxo2z~yVFEKGmx<N>Es_c=G~ŏ~|+kVax>Zz [o3yn=zeyN0]pv;{,iW8@jj*sgϦ&-6VRJu78uLfΘή{cv^>^sN=~%55,Y|~Ϗp 3fLWt?µW_ũdI+Y]&MȌi|ٺ};6mt_%ӦqhPg.FHCq9?e˖qsscÚ?~G:v X+sN?bg:x }>kO'?/m;vϰu|ؽg/,^HgsYgRXX|=8!(--Cgݻz=1i½Mtرq'kŃ =G㏑]8XQO}]}],}}6lDrJjxgPb%%v ˮo}oHl_9z٘2q|zs=L9SW*((P]YsfpZ#cv#[UUUG\;n="i~:V}xá^?uvv~@$JVPPPP8,UPPP8"PPPPPPPPP8`Uv̙w*4GUwP +((З!㼎TeRN((XAA!4:VPTDRN((XAA]1 8(T ]XI}U}<.v(XAAZ[dHܒSRh1VaŧccGMroRHVBQ}$I~nFUUU禆cO[ Ƕ}/ˊ+(( v}9 +:z}\ EN(x1B_VPPPXAAAAAH :IW+(,*R3NxRPPP(/-EY  " +((((((((((зIt!!_?2Ta(((((!4 MS"0hjhP$XAAAA$mmxt] MikiQXAAAA$`$W g*!T(((((((((wP +(((((((((P& 'pB@AA'p/Aԩׂ \4Pw/l0O_RV\IG%.]qe\_dM]K2e$nηYy -ݸq/2UUq%3anMwPWWwD۸q#;v񐕕EII >kc~X‰{>qqܹZf233cdy6u*3OͷVB^/,ZDRRmmL:OAߧ{=1#"PL)ȶY´MJ4M5 iHZ$IT% إt@J""!@iH),}.}h>biwwe+3\nDhwxybJF:,(%Cۇ(cE/Dn7nE”w#^ M-ZX2 -RҥnDҌ.\kp=E u D_H @bHoc~`MZ8Riz."_JTvXړ5iai{b%b wfuy_Wh*4=$#jH%R=\F`Ӑ22h F(!v̓?| pss3YYYRZZ^_HԩS9U ر};7l‡ y۷m6|>FͷޢK;vdP={u+(iii娿OAAߞÐC2NDiWNaܦ:eGLZ7tXRhzL8w/D! Fߺ*G3ѝ]}'Q:n%îp%#m"AHےpDm\g )DI)"ʱD8\-0-[e6jc E$j"A]ۿ-Fɴ N'}; xÆ̞}:\y~?:_rűcnܸ;w2d&M֭[ë #=;ϲ*mnn\ojlr^hoGӴnVFG} zo3!%A#̪OXExoDqE֡WMjm˜~\-NiqȦ:[AaQʬu~07jh3ЉhfRczcD-'1-hv̔ժ:|)b/1x|i:.Im45!˺\+e"(6סp-)mXTDlJfbݐ~Ѽ |۸[3nB`'b*v4*G - F;F8y 0)5K -!4,N+1]7i)a[[Hv`0W_c̙L6n ^.bJJaK{iG޽#G2~x>5 IDAT)%?'|pߧp<+zy ?m '%g?:FdDQiyL sIN4ɘp h7OH[v2fپ3\}0=47El@CȈ=<&XEį A" +I.I [=آFRo ȊC:D4D'_HWhqXQlj,^k;ab-]2N@Me.Ap6(eV=Z<݅X%֟ä.n٪'\ ۖazV8uy!FDVBXXw.,?wLU͋#Vօ{tL#bޤDl$/4R|?0}z {73GTo~G#k2jԨXQ3ƅ/wRn+"v \]]fxWpOneA[yGhllTUUjjL̙3IJJ[b L駝2>\n8SP p !"DK$ k7bd5KN'[7RNLGBh(#fVΩ.c|7j+c'cCE_îV'Ks[m9}y{êɇpͷ}4s2C(LG$= ڡY (- z=@EGa]Em͊[{^7/Fԅ f"")zfjd6[=E0-.w:Xâ:ƇSjSz1xqMviZύΙRK233[8p e}=((B3yvɽiHyP<V+ ]@32OR: }7YZT1Ynm; u-[mx¦&4Ywp-Wo@ ®% 9y; 2kI31L:'- 7Wz'6,݋o_I5iG@sAϋNYk߰.LҦ ۂ}#ʏFJ7|{{b6e0+}pݻ6 G]'Vg2dZ {9[A[;öX=.J7hB82"䜮L G{:,mYثk-5VQF~~ /䶯,6HF*Tܶw8}v#.D̬9 U5{)͈+jk8D]P |0,vҴ27stWuw JUm-mQij*W–vlX_ Б^MAAAC$JNW`2mݎRX&_+~L@,fbO[X2wVUuun&r#e=l="]X=l!Ch k2iS ͬJ؞}a60PB0_;:g?P(Է]%ї CZyiac Vk%nM]Bf[F\化V C\DvLk* -]H ~W ar&Lp,vCwLAAGt7-┓' 8[V;<}0+@أ9;V{.U5VuW_}k Y"v.+!;qv/n’mDB2i";JYNDpʳ{pW) p] Jʻü/Jj R[ZcWuOK P@L&1Êݴpc\<'byaawwîڷelg+(((D. djn|F?+m[堞&scnPsfwm"H>7a~p'Z1`#@8Zm?5;(U9*ZBWȗ| (1.9◈L-2#JLi~a_ۘnw"`%vUomǮ H`.`.ۨ*հP<X!G'a-Xۊ"ac.4ӎԯ9ĊÈKFC.!]0ՅgpBjcZ[ٸb"2N,*KBğ5 #vٮIisƐ( Gɯ]a2fXyn}fi7Tnmiqϻg+2VK_VZ==ʞ-Yѝ;˾cm %5݄p aDTKBlk= k tTm͋s ðF@tYDFaD惏 a-#/DeoWŝQe{8~swuZZX1DL`mx<'ަP#x=zPì]1'xt*7mJ` C  =h^!茹 Io$ وi0"tUͪfK[HS,"qb_viVE;7V3y2pg1C1)VE\`kvx.fw4ܑ3&^mHC>S|BzD5$ TF BB#}4 Y$E6FY ͧh`@舼 $û]g=40$=oÀ!xD,4:CݵE 鋶iKj/'"Fc:j$ϴ#dD莏~$o, Ї 6t]W$XA!~?i$''|654գi~ Kpwd?W:*5 `0kxufvAz4D gێP__Okk()%eTԢi:i Pqs*&쨛"$,ѡ ͅ<ٷF =@4и]c|. o4z]xpmdu0{fHSe`g;p(aak=.7H{IDmXπ\|^VaF|E]]y99l߶ŋP{L^-7mfU+-Ûc@lII!/wP$]gYv-5467s=aǮ 4|?֬eظiׯ'?7$/mmm]OEE ޳A5Xr%ޏT@=XȨUMu3YJ,>6cvf=8j"mȣibW>;8: ;PP8xHqrfj#H"ՅkA"N5=w˳>˥_"Vb֬Yؿ;w%c媕?''\uz);FEgtgz0oLPSаyu9-=+s6YNYVM QU%Lv|q.x̐ -p|iڼ0q#fV{t-^VK %u[wkihn£i,Y~)۷템|LV\ɜft{ttM3|~0~(>\2 ***6e*~zk GYy9M%1}1 *]I ƌé3Os__4:;;yg̚92Ǝ'LZt]d~qW@rmRJ<  k.N?m˖}iSy7xwRRSRfƍ3<ˠA0 U8si<0~8ƌs9O7/ o=f"mʹcnmm]:!'C #K(wN@[!Nk{;Pʊ :c2rp :\:xW)/+#)90#33|ƍCА,xn"Ԝ.0BBr[ [a#yq߿]MI~HLdJ[.i2a$NZ;X$Tͪ}!}Aaj%沴 i!㚋z|G-d" r{cغq#㟸먩'#3Ҳ2Zc͚5x5++ sd/@'>cԩ,\GAuAtCLϞ} 465ݻ>$Ǝ /HVvii$%'گٙ,ZstRf͜IzzztJ0IFz:ECص}߰AC Iy >$ZڑAQQ1˖->4jjذa#ӧ 𭷘0v޲}ROUM Ǐ'DI&|mDZdG ;)-蟈@C!Cp ':vQ&Pʰ=E kl۱P(4oHJJ ୅oQSWGkk |;ߦFGG@MA:~0,q+rs%&3L 7lVu,ЌFajlmw16T*(=mGר;ŗ~9g^xݻv3tH!bɒw1B!rsͦ-[@SS#au6BEE;e&S[߀Fˀ\8PYIP=6l`+fɒ%z{EojjFACs 1t(?XA("P]W޽{hnnO}C;~ -͜zL~?-쵴ٴq#ÆQ8XA]]gNuuY465R^^Na`&L@16Jnq!H'75]BZ#k G!+(((F|6_def l5\ͤvξ%k֬a޼:t(-K_kVK'k.v: +&95#Fg~?蟓æ͛b͜yCX5ьjNY(7Ni6d'hDӜvQjkd&Yn&fW̞blt_s…a;93fbp|>|\o&_">""AנUV#gYʞ{@jJ sABp"**HNIfڔ)Sٲi3 7|^/f INN3f0 ?MQTTL bSRfTW"A l> HOO6ZZui~SW[Ljt?@ÇESs3g̝ Bp**+2x0O?sDJj*SMŐCeKؖz@΀$8MB/%A8 mS,RQ~ .쑱_:gTӪB_@yi)C{4)4pH{b2ifBN'ǃittt4j$))u ,Csk;-Man:]h;ai &ڃu$&QYtۈ(NGf+pZ"G# 9VBkaG6m8ntGP]ٸxuF u~OLt [q3}"_%x C|:RIakPdC0iP$]Dǣ`GBё@ «=>x:>-A'A P(DC!.="ͧǞ E8A A|A JyM c-puH)s=BE y4ʡ!RV_-#cKV IDAT @<84SZ"~YG[KMM OhA4t#NCz1 IjZ2 2f/-`Dx1K{/Ϫ kq"\#Yݧö_DTs2fV~Q[I~uU9ޣ!  :hFItOad'ҧ!ÇZZC(S-$$ (D(d=_E!)^3$ɣ 4x<GݺuIzw`#r>@42 4 !4by{g(j4- ZC!I[0DbHNj֜v5\ڱ3Hm>Raxr@3xKĶpb_r1JXeLh:]qFCS{vf;lڵѣF -[1j$H 63&Fd`Ն '33Y1!W>pEf6_όgMP8>F06G Ȥ AA>Z\ g3¾;xu7ngd0mT0D-)%&hniAYe{ۀc=vH@a禬ʄ[\AGdmsA[T Y8ݒ޺PBg*lOE3)[wwI #duhhssYMOvf&o&B|L6;vPYYIjZ*+gA3r(r23ՑykBπ9zɬ^1͍rg˨?TQ#:lB+̞= V\Emu 9RRSyw?n,y:;YC֬Y_#G%dgI'~I3<s3I0)Ѕ[1\ٓ[Zx"ٟtVN~x%^Ff]lv(,*߾]q%- )-y{\8ND[ k&v[oPA0CV{pFhDsCJ|wd|Ӎܽ`(! ]c)knYQ<4Mt!ؿ?C a5446r!N9e&< yyh?𣕼ʫh~qIPEZKk+ ͐!hÇ9,]f4]pg!&/3V|Q#Yc̞͖[صs'|󤤤pg찹')xA$hfsIP[칝aWm|)c \)E}Y'wYz=f!t=~ĕȟ5kX~Fd$؈Ց̈gLBKs;y0k֬DHJ镔L3߷w^Z[Z8>uY@CعYviHJ,M6¹]/%wbʤIRijiob2SZJcc#cF@֭ >}:'D$̙ xE$'藞NVV5:BNNIII5w~斈ܟLmS1u K,!ttQ^V7ad1xt~:;g/S8x998TWOsC#EC)߿ŋ3r(GJG[MgW^N*p!mVlAbu`&K6%M:Zj&NOv5߰ 7.aXҴ-JǮYisC8CyǏ)EI]!QP8▭[Z\G1r$^>Z̙3YRfz*y‹>)'ϗDTgg5qa~iilٺÇ3zhvSO>]Gi~B0b(`#GZF $g),{} I(:_|\ƌ_ϧͣsyǼyxEqA.".8|^|E}u4sie6םj`*K@6@Hȉ6{(q7٢',Q֝k_0B=wlmvZaXV"0ՀiSÓ# j"dHuW_W2bu+U+`a%qWfرR[[ad܄KKM@gȠs0 jcomm%-5&RShh<,F{nƍ1 ?/ : &r.4ofo";;VpeV:;"I%/ =bʄ1g}GFF|^n.IIx`֭dذb,^ˆosw`#pWrZn#i&$:&(g4k.2 93]fl&pfr%떷W[DѬ BR2jH^ iioGHػw/'LcÆ ?NkײsF?&͜Kc DSs\p9Ts޹^+_·~DcckWfq4571 7#s`R~ZΚ3=,uuu|^֭_ϸq:/`R233z|G1 '`{0h`S4t(5ۋ㸱$5f#R(ix?Gb[ٻ ECZ0L\.~d+K/{nff8u4R t"aKWwՠAwOCC?~?~WT.Ϟ9<3\Zg^P*kw&'Yv Ks9o,7^0M]sBuy.>s={W]ի<9o\牏~ BFl~Կu :>t$gX:/CZh])3@Vߖ"o!'iǔׯӟΐ6oVY$#}%.To0sue(0 犦'?mѬ8r L0K okۭ~[6h_ 7mn9GL%)`iBI%xŐP!pT*ttt ,W*t8R!g2!K8n uL/֯PM. _]u%ipB?۱(J-|{pr/S@(iа=P4pnXr&5xd=}umV+YdA<0-8ZVx: vL.e(lCN'"ְg?@xNkKzw-9ӓh{d6E *ij+XaQռ0myAקhH*{Zq(3oSynq)fߏ ˷oC@9 AaP% W ?ҳtꁇWl08N tRu W>\fdkחh_y[H*tSJQ0M^9t_;-0*RH˥BQ*KcSR[m=kh Ж"skۭ)t*CYU%N{;=`k9,³JrN,9S!)L,ۡyuX{㺘{F:r iJAͲSBGޤυuA]`.b; WyU [aӰ]G\WQ,G.u <醐m7|GR:G2RD"/9Xؿ=M~7OLcz81gXS,ʦ?d|\jV$ <~EBK&of># XH%cZ]r@ö={@ˢ!V|mWQ)s,` qL,eV'3BRlj./TdZub 4lvpL3YZZ* eˢqL)i8.8--a)u uߵAV^ayJ9IVL3ǶYZ\Bv8fre) ,Q!|]_OJx4:vLO&l\a<_!wܔG(%;9Jgw4\*QQޝfFAP֖@[zwKDF2̋/TCgG׭gkػ{JvU^}5|kyoə3gy7~:wa;8E%.w}5|#se ANJ _7$RTyd#(zkN>8X 3Mp?uf68_B uPJ79LֈX,jt}.r"8Ú.D q2\C5I|Y@\I) +r&c׿6I Ѵ&"NѢO!yֲwM3F={p.]k> {}T+oq޽[Iqvɫ '0 c1=5ؽg7;!{gϟgppy.]ѣx`>rW?s=Gaq\~KW.S,at #wRRqjo&R**s/c7npECwWje dO߉-$t1>:(Ŷ-Ʋ,q;fnn1ɢ]js#6UL/ \EUYm4rB0uW٤G.}NuSYsooq7H "4!75x`D!kE  0|O8w8שWk W]{.uˢX3qcGs6hX}=[}{ C=W^F;933srOސ;~{r(<~<#X4e7 N<]wmuϙ3g 9.ąIiMoGg0DXK.Ȟ'#~"sB%~њ'.JL|aHcq] <Ĩpmk/3m3nO'FWP!VrP\ǚ=w7`'2Ӭ7'*,s\q/15=8]…T2V[m3AdHRWjf_477}ڵǎq5.^(}LOOclabbѠӷ;z(Ce:#`ee)%eOXQKI#wȏ'c2t?* :&*Jb+u]m kbb}$iw"5:xkưnQY 9YIB/olױm{gxgؽ{7׮]el|kWqy7$'zj"q ۶mCG~ 6rw'>hܵu+R|?GGGGg295MTf.` J_~~ E>z{{Qr2ڵky'?Vv= Z9fZcÆ!T,1::O<믿΅ )خ"(ea&Wo gOr>bc IDATq?m [쳪}A[[S p[R`0pp BT [Fua BҫFV$XV~zzz˳Cg``|>wsBxBR[n- ӋEczFK]\Z;v>fa<Ȟ={صc+Ν?< xy#ڵkܸ~֬[KqK\b֭,,,أRʕ|ӟ7ꕫlߵ5k>6ƽ{ev "+++Ct^U 0ƁHSZZ2L)y)2 $}ޛFkA0AY>ɠ}H#bh> ]+NmDPWNqY:EVN#G EP򑏰ylQ9;ˎ;شq#7nݻwq9*djz# ϵabrg6ofݺu}}u]l۾e>)lݴbm;RB\b]wvd߰~{pl) Y^yKk3O[%Dv C26> ڂmcJ]i0u46@ҽfbw[Ƞh"8pnܸ.Muwa:l;;\gy3A! Aee>hmn5~-P=;d .Wh㸎g\(VL#E`hR lvr9lr9URTR.|JA 9?RY*pM|$EV`;/M )Y "|!P$:c]IDlBĤ's3ޝU=w k( wT.F` B` ]E =G)@ QթR۶=J߳nOU@ OχkK ڭ~ۻm/'H!ɫJ>r/yF&`cpUz ##:?zY27f(_#Gزy?۷m#{Q9$oCCCKNZxxc'8q7mGg˦M(UVxoa3\1ېB0s&?}gJ%{?ǚ5JEP45;NG( nLF ȲMぃer9}6fMrޫ*|nJI 5]<"-QqS8K?:hQ@GZ2!2@K48xsss9}!ۜ;[p^~_FVgZgMs{XZX_w7go2:zťEŋH!yBrΜ920ɓ'ɓ^q R #,/-q)edxyNzAĚ5kp_4M<|BrK9 R!) yy3g4130 #UPЪ[PP-XEʛ6 yR.w)?9<9zk׮#^ım9)=U-[reP{066e[r =ٳB`Y6v$g9;=K`=KG{C~rL o.sr9cǎcJOȈ#hws=Ν;Je$bl|Cș3gٴqRLS+ReRM@mCi4SE$I![ bۚ1e鄷x?U)^)"]Fy2/+ܣٿG9vz|rgp9R09z=5śoF w {9a捛׮o|z)<c޽5QaOs |D("6*Uh$m71оCfڸyl}n-tGvzJyZK. C9y$Zs9pO022k8 owW^} 6CJrcK2iP(djb^~Iy… xKT*,--W{zzÇ#=sPʙ:\ti!Y,1MO.! iVk+T)YZZ )% ttw=*m9::)JazL[Ţ">y{Covkv=!/SGQ.ի &'&8wW\aiyM6;O>ɓ'yceyyj8+6lX*7ndnn7v]N<[p\#G~d=7@bl|'OQY3M._ñeeppEΝ;$37o~z>ʕ|"W^e|z!8նNIAN{X-8.Yvۗr]}naY:{=?Gyk_E>\zWGwyN:͛07;KRennr^FO&'%os}m]9 <'NK/|'OvZLipЫ=5ٳKOO?s:} ,}^lejm[6SSlv㌍s޽|_`ppԧx!z{z?`hp(8ڤJV  O-O}&+l#$U (; ze00bSn6ÈN?}[Fwd)4h e^S99zG{f][t*㸼=sצLMϰRYaΝ|5j*Gc|4Νc TV*lk++*z;sIxwص{7o?ϳi&zi>cY;9},RT*U~֮bai l޲ǎѨyg>rs~>ix?gN,Tذ;|.ֻs|w?ŁCb;6>^l7JfQF:xory7[KN&eMV+UڠC&`PX&[1o:i<7*u~QH+M渊nvőٸi#?KKڽ 7b=أ ޽{Yf }e{g``RΎ.֬YCOo/r#GT.-[tttpF֯}/wsG35;~AΜ=Úu`5kظi==ݡVX4:t~z "]Dqaǎ,,-g$9,pdWYef"UV%S203sg;C٠-ZwwBfn~۶tuua6iҨשtuvb6Zή.VV8<)<ܳə^gGG'=[*RJb ZoL{7m6oBÎxI %ѝ"`b CVXC>-U:8"49SD6kNI@%g)o-Ϣ帡 LՍaR`]h g.a@^JjKєZF /= *!K^JߖIQ4urFt zw elk~)) jRpgTQJrMWz8pl5/+I*9U8;PdMeƵ]gGQT"۞U,|+ C9oY}9C)gа]RJ5[yZh8`w*`9 * 2~?6!NHex tk(" ɐ7v}Uʩ!K(Bpy]ҠW͋AsU'4'g1NEo:>rHjU'! !,-'mrBV^JI'DJ`Van.`B&3&AbUTl[̍@{޹Fm<MU${ ~BZiء jyBxr_YxZ2ccPpk9xU͟ iw@})~u8.C#@\\Kz[30miȰ{?y:}sZ-nnX/ I-T,-]ߞ̰4!NZKho`eC߉yƭzRgփ}L5},2iBF3AO>&LƏS*j=5P! s]97$)DC)W\|LHËY &H:'b,^Zbs:E D7G`ό&J hr"3KB#j+jrؿ #v5|kWDEϾp)?T2Ϡce9'Z}OEsW%U\>֣u۳Տ%  "e<$*BFLʖ`\(~O? f\YēWV ߑ|%C`VoBC32O(NkVѓ!o" @frZ@4; TsQך od<7鰓쾗[mԚc/_2z "nv> >@L2(ސ{ӴI4&9ҩb$Ht"ΪfF X嬪Rxeɿio 4*wr7zhޥBsUЏYRb\h"FV?O:\](:҅jJS׏MӰCaȖ/|-nl':(vc[jIJp􈚫DTH?:(٪FzX7{3aDh2}܂ &B߹@jO\َ[}]Hh'bz {)Cs*B+XeDzs_H-^I!Q?hvkv&oez!nzjכ#U#exB26Th 蘃MdD*S]59/22",$:q,z!45tlJ3@ьo5@P0jͦsY,fV/-ѵT-%8,wVXナ5l}[faXEJp3Ad>Vש&_0pD-f*9BH. 6ivkv-n2 o:~X1B`^PB+$DQ-u]dcuoʹ/'#ZCdTרldZLટSou {BܑBea%CJU Q ;z!c5M:dd''bnxwҐ\(m&3~Y^4C*5 AeM䲿h(C8K&"QlDu_6ڦ 'A|ڄ}([wϙ4Ԟޭ(}ݽŘAfW;lvkvhZRkN0h1"N IDAT0 e4,f'K3̪ǖhꗫ3<+}dصKMv3)\+5Dϣ.5qzM5}6H^G!dh6gV#@%[tdj|or46aO3rfD˩?vsߪGfқrE h$ W?bĹ߫GG9 _g[yڀJh;w[ p`YBF'qDU\zu5HeB2&䯭ڭ~&JDӇ³ER.·r5FP 8˜ I6h.Mo5@cl>뤚>$#%2$B+Dc*l#8U=.?z8,pcl2DQMޚ}*c 4lW>{[5/  b9FY'Vkx|El`s#i}]L(Dž1n8pU&{z=kC@շ͙iP(QJa;VHgwvt`{`*w\c+nu++܆n?x J( H%#'XjZKofOa!v*Wp6"4ņHŖ~ &H^MԪcj|j%r"N2c 3 0,-ȴyJ:ƮjV?yߓEc9ә&W*֟tj*i-8$mhTboH&{vXTz/qBeݹ$={H2Y 'gyxœ>yԅuU pT!-X\\BHILu!QKXZ28 B):U E9zVLjb8vnvkMcgL>[ yZ٠mz\YH7>El&@&KR, Dd{c@s&f?fe"fތ} ^U2~QnG_nVE# d1I 12v.mu+Eѹ.lun #Y: 0/@PO&2b5Ad!pI}m_vfoWJa+Ue۶-t͸uyqx\0XYYΎNl[C=֣^op5eiX4p'RҖ%[-͇clJJ-`\2jz&f/|fkЃ=zlu[SnhH[a(8Śbe,m(`/A:"9E,t l7+mt ~MtɊhjL&2Hgy|*7j63/rN׻KW[I,IV?+ \HOŐi7 ʤ!kBn WIPW >~\} U')E|NNNR!! A]!(ǶT*vh[ض{Bhm;TU;)T*FOZm[wg}- -ۭE ^PT B1*L.M[XI7(A\fhY."`Nbf~k(V8H{m,e`ǧMW%U"$Cfv50ct"nʤ53w๓lu5 G },?;0LV~9aHBzẔ~֭[iJ>z)B00 3͵\n[ oѠ.TLbx*KbH,a$@Jj; 8m5ҬN3kxaYE\L,KsƲw3竺jlINLO`#l\}ߕje-7-MF"~! m&8BtM~"RQ Ӕ1`۶郔aᘗM0\%gzn;H{y!Q@aa&moۆ4|(6Yţ73$⺐ϙH8HiRz-%8CM IN4,|D &g 'KyHtXâ]YϘ{HKy`.3͘ḫ;cџf x`˂VHU)funnQL| TCQ+bSMtIdNӠY\*ymK0[8 A&-7n WeTaIlvqUر_ĸx,I9Ar"'~LJSV%tO\ena˲$7I\P(077OWWw)M[6Fbа8M\frjzΖ6,lRk02֯R\M0|*.R,˫=2]]]%gnӍa¶ۨTW*tww3 E,ۦs/"`Tk,03=COoG29=\Y^^u\::;-㣣lݺrG1Ij[HeZfj7JЊ&wݱB=4!BbBȨPJhoA(vkvtzފN/ SXH3k쀋u ^rU "ƒȚ VBK͇͈eVM`1*%Ej2 +5!Vё4E4WTUxGš5֬H&bzuBZE"Bh"D%9Qey5To xi.\@ސ˾T@ϟ}0B?J)V+W.qE`OF99x!jvXl#c@uIh,+Tj,nVS!-}{<$~NZمDkR5;cdF3ۇkE*6y,fSiT&n\|.,gΜ׾E$\N!{zXTbܻ֭wp(C''2ݸ4| yOP*y7{N:::(KJݍaH҅ ::( yLLLvZlܹsKE>Oi`;7gfxids7ohΑ#޵˗.H$ZUmHhn P17P-O? [M2ƌqܶڭg5}UU*Ŏ㵺K>?'WOI!*0YLĿzaA3eJEk|jp4[&&nF>S1lr J148͛x#J,ҐtjT*+X7-z /nd9ofZ޳~xwxcZc+Xps~|6 [g@Tѣlݲ[6SV|SJ*j|K_33عKW;q^Ξ;KVO<$Fm^vlN#H kr~OUMjV9nY21!3kbx~aذyڰy3~_hvk_v6nrGWM-2Rj7CHQW'b5.Q| Q 6-{oitYsBLK 2uBeAjrYD6uw%泞uk~o:U߹P"Zt]Mtג7 nqqlbEngx[mͼ֥>Ai\?V1[1gl%bN SCԯo1ǀraiiuPXfF*Ej&BՐ3$ƂkUr,lG3r8>] ͔ہf8B̩w.LS``;.ߤ,+:3n9sYI솋8M '9[yvk`^F`qUlť&9fvlUJr#u@oCb~=K4iPbf!g9ᰈ-!MI\a,L)lhXh$ tc/n6ӯ_ɟ 4_- tZ,` #U Hf3 LOCpW7fg0Ww ~>'xz6 8$8oaffej.fЬգa /8066.F^߿ᮻ /67Q#bzj'O¶ 3SS~@ű111I0 @+Y"d(n,8_bhKi;-u"Z(WX# 9/)#+I^hE+Z^_,p! ,\T6FhNJ@:d\66a*FK-aD˩zA^L s)fqHk$I.zi$)62 O5 팿"r\jݲuO' #@ؖQ$Q0 8gl[OÕW\u6a 7ӰK%7pΝ qх]_,n݆ݻw\]ȩpczz=T{>tc׃a|l ==/_ Ǣ>\.cnng3O>۷cq9T$?z0r&'ʞ=^z;.6!sN"r)_q-`̭Oi&YZM~:8&X^-+m.AWEmM4I(+Z~oP ets WӸv2>1jJ̶hќ) i2Wfb(ylAT ] 8t~'vz+?cǎo&<ߌ"*WxKqٵ [^ k_W_WE_o/Pϡҍv\l;!nM >>ضQ/GZ/|!LLݽxA+|f6 / %~ (CzB"%\Y59F&Ư¾&=1ߘD0SPڮ eF:eY8z(j90f䔱br>'$sBzH:G 38zJer)YV 10.2 PbEx0(Zъ&M2p &ts!(MUb(蹣!(CY 8qQ[1K0*VCJ5V@&(7cs_!x戴4H+iE;J+07OI,'A $Hײ~~Όwbh`8'e@-%.lZ@ T>#Sw918\~Q`.-D ,_:ٹJAKWj#XjZM\ua4<AE9b9[ Mw7 5@Q}w]} FiX5*Kʟ%CϚFB6‘\c?2eLLNRhK,AT:#'ȃJZn`bb0<<ӧOT*,Xᡡ8fVzdLt y[#?|TU<3—cǏcOd˿7܉O~x{9n&{ߋ˖/`fvvܼiSy-7pWcK8o|ⓟxBU+W~ns3ߓr#A$|F; I8vSe[NrWӳ8v$N^{$;YX֙ R){<=jCVwU4 0۳e m˫_" ۇⳟ֭]ցsIK.D(Jknq-v?^ٳ­[q->fyMw]?s#زu+q)n oߎ'z K.EwOn sQ.wm؁xFG1<rng?{hE[&B& !<=ePlDcs]<߶4r_UAQ $rTJD5))YnЛ%Z8k4]4dÙ2tI݄UyJ0#WT-#ogEtM!#N)dl889}% J~\ /[={a Ǝ+.Ã>{w݇ /܆9gbuxp5נ061k_*6o9K,x;l6Յ9ި06>Fm۶KԩShqIAfcpYVYh7? Lhωs6˿y'jZ[nW_^s_{-~7/6aٲe:f7xߍvxqх3;G?[ъEpFq@a\u OhʼQN[$&/R<3O "=`f (gZ(lF7@QZ]OcxPyj|c/o. Ts dh!|B:5:dR 2c+(<\hChÒ>Y0Tr`Mʹq\HVkCoo/, 7 f J |qzʥJ%MO``b,^<e- xf/'> 4[-\o\O<_RM6lq>p?\cò-\Ɲ*P7!TJhy O %(0# rhr)1:xOQ.'WzT+?m~5kpU ,+;@uPnPrX׋*R)sAP]FD_U8y?s-[rW^~9Z}zS`wyyvz >OO믽oV|@WE{]4XXfeEIhg x0l㧄q#~}G4|TXO&.0!3F.Wx~YhykY-kj?W `Z4#ig k*b+P ,Hj>gc76=48 ׅF7X@`}d*TԖ%~x)t}E'59dhRV^5A 4c6, h~KTƏ1G };(<O$fZ(ymLsBRe.p2`39'kDP,D.Iu{1_oR ϛc6: rV KQ܇8GF|{Z{;߉/ /6_ ;v)߸s'|}=y_R#VZ^|q^lwqB195eǏcʕ*)=€"f /-ǡ#0dj㓾P"ѩi4-/e1LN ,]܏Zm[ ~OS CAb$[Kfۘf2!h@bvniqC\ ^f8~dTlc6>dA'̅L|r96dP) ])\Dũ!{J3c1\# )O2Re b;ϊ\L9GQ:Γ}/ vsp%ApX$3\!ÁeP"`jjN8}zcu\ׅd!\ G_É'1==#%ܧHJXVaxx9V,_%K14 +WBww\:aΞ;k;._o28m"3=믻.z\|1zyyviG7ݤvZh?/gKgY*ۄݻߡdErcccWɱ?#8ݻg-|{0rCbnf '_{ ߿ ߹[$";)2m\EY-F[ YJ+\ws3He\[\?<}A .TQU:;7-]:8NȉXEҖ c!lğr"e6C?pCujZ討{?] c,C }&x[_ 5Ee1<y':qfx7[wO>W~p߈M7~̳*yJWWPVӃJvϸ-o{.,_ y׻pW$gFDzhatkw݅Kqo?ַb|l ?(eLLL˗cp]}}Xb= Z !8TE ]sX$KrmIivndy_5̈uc߾WxvI*\u:K~ ?f-ah;ZENs$7 ei qa 쌋o$`tĘfRJ2pڇq_4;| 0&0A/Z_GKHɟ_u/FoOyQ[/7jsǸIJa/|Ay7}s}ۇGn GqA|7~SL-PbNvC۲XN4b +18WvmŸOB% //߁>ܷk3AӼWeb0H|/a޴Zm<+w wepam u/P7oKA哅SC#r6eD ADPv49=4gJFM;7{n֋^R@]u6 A FCt,R0MV|,k?6J!Y$_34ã6*#@L=kZvXv-e?m,+5OW@W;'`ypPS:@^7iY ^'UǴ=t:0qY_Z5#Dpt,| /c8r~䓘‡?A|q1߆G_{+lذr3؏ݏ8~8>+E.g-0RIcR(J?!b>hQ0lT:S,/9=Ç] ,Y<-Y꬏ s/&"an<"cS&YIN3y@y,bmRu'@21~YS)V _[Hf8|0~C-ȹ(3p!Vz- 1FGRN #2ܗrɂX-5_mĢhk61;nreH).zw^ڳo{۰y|{a[6>#(E]/q[jGI^=ݽx;-±NrTX'ɭk X KRAQSm_a<"kyzlظ{_ݏ޾@[ȩU,h#T= $tv,!y4R;Tkv7B="O?#B@Asxqyܻ6"GͰ?B]>1 z\"8w?^~!2[o!"u;v jjs*chpH%hi<8&`}u[]. l""49p5Gbhxޟ}'+[n@ș O~ O__x+'TbHS'! nD ;'T B`5F'-'E &KJnQ&2;e3I0p;Stcxe[X"KMfkݐFt@Ȩ2̓cF6t% J58K$C^`{6caO@Pw|cShBc|.4$ANt{iY6z{P*H;Ѷm0uXVz(ZъVXIHFluʰp7+x}-p߯e$=O3\Ӈi|.M+.,iiy< d LJ1>bV*Dcrm8-&`C2>_`Й -/bw) "`ZNo IDAT'm[cX=pAQI69 1e)=^3YT=C6!`v}7SC{ 0w0zV !#/ڞ2}pc>qX\$ w?ɵY.hE+ڏoc,n4W8E=a^+[)φa,^ ,H\@XbT el `VH˒vUF*T!$9S e+\Y>X,t64& HQ  mi]FšˊYh@Inf1ug9f/L[Tj%VY|L^ ۘ\v0_0JmLZ1] y1oHu)U"'Y0 "Slqn"uK|4r2 4څ*/  0LMbzrU(5z0:fm8hI/*JV EC#F:[Lr hE+O`-%,KU@EwS_AOw, [*z mQkd$AeBX[yAeuVdNMO["sQ\6,ʘrviь30Ԧ0z$6n;GgQ&\]=E8fp?z]o-Cmр%!Ths}Ŷx,'Y,8%bs"h1f"vԧ"JXDIP4`$Ns d5%t*%C bK"f1HrzPG ~:C7/ۙ9 ŘFE9ڌ4޾ӑ)AN l^*^,Ap,^nB{h,(AMai -4ܹSBTR4/shE+OV|Jv yhILX$kՆGfL) {2TߓT!\ռV # `a(H1&r O @*Xy ԕ'ҠG'Y Ӳ61m-#Ir%461oW5oTrM(:0~ҍҘ3 -(sx"w?uhO\`d d FgH*e,ݲD !47t!GE9"rE):gYW WPY?#Cs!ʗi>ɥ%[8&2*˫.@9BDb4&Z)7;Pv!Wӽ4YJ"Gqubeo̻zq\xoȐ*0Di]/C sAi^n+ Wja !3O)4yrQOAXr;cĊ0ai.p!# %u Ŀ>p;@ ҃3;g4;I0徧FSbF+"`w $vڃmLϠiwO|#\7dԒ?I{q\ R}"$xGFJBN|%LH#OLH.ћ`"k!Xm#yZ:Bt3!ǵI),guǎCрm†uq@0) ߾38i.9//ZъV JZR;\ȅMTSRAd1cˣsY]l$R z0̺dj($ݍkB0±%NeoM9B26+2%>T@{^g .%H Z݋4)>y #JW! @CH3H^'!: L [f*3|B@GNXu-ρHr K0>>UV¶lDeYT,^޸YIt f'L(T<~)ZъV% o,5eHBbL~aY@De)EVeE Ea1쐦B^;R'"mR%D 7Bd%`ǜR)fTоBbnl%$٦eș3Yi'te}d434AJs'Le;Xp52oVСW.IY9Q M-$=UJ60A `1Fzjf M000fK²,8r+X3.x*pϓ-:ː#`.lFъVē%IqL?tc-d\Mjr)i;hfRs bI"JMvH<Ia55-֤P 0#\u".!`~`P .Abq[QB3,7*h!YYg2Th H|XDQV.WϦ) ߕvټMָh-uO[`׉ʱ!a"8 p+^V21igzXԯ( 71ݍhv\$#}gѨBPG—H`<3e}q.bȝ/}8JmHuo:΄ tvH)f.1HpSnw)YLeFT3t"LF5lec<#C(ajcbAi t mBJ{o!~P’r)b"C D*hL@Ҏ(HQq#)cS&@ax}Kj-sr,HST4 ct0Ja]cGYqijR펵"ÈMMal8x(N8^wy`1O=ŋcjz >|&\CgCB C Š+jrMׅc۰ hrch `AxdXv-VX 8r;wOi/!XX<@|ߔx!A.A @r(<GERATBلp%^ L!VJhE+F|SCD[ݗ ,, EԹʹ} ]~kj 5-ĭ`k߄wXKGнvELgL<)ڼԾQu؋:ӷ ]GVng}} mآS\;+m<p[-^_Wʕ+QTPTo6mތmQctjK,ƩS#<`={jJضay[3q k'e(V\ǎcՊxP.x=U`zfk֬la5g)Ղ=93_>;"ApP 9xf\Q0K ؅(ضmkjhFv'wE+ZԢn(# 7nY+@R2}hJ:VG&' -2Ld#1M0K=Ig_@m⛯Ei&XUX#2Lgd0:h7iwA@v~ yoWd~VsIs(ߏR?_Z[>h4c=Xv-:[ؼa=A>}NDߏիVbjr8FGFܳϢZ 3SSxWsj~8x'^;FQ*yJoC G 4]X(BC#ό.CΣ䵑)o?w ˒7k&-h!Ш7QTpM7gXd#ky'?F/_pضEؿ? nzӛ0;;g}~a6{x`&a Y}6 uf:o>tV}!0`qEB^X< }NهFT&jQ.Zъ 1C1x ʢB!Jb(X6si̖5:Өgm~.@75ѻp cApz`7JMw?%X̆e{SXCD"XH$5WŝUk~U d;"j"Eڰ= F23 WǴDrm" MOM7 ]{-(f^؋k.Fm|c~]ol`I̾m'*Kc#  7iєlp,2G(s53 -e#؇4pmZ5]0flpIh$kuqJ<,Ӝ[~s*Ve=YT sPy5Bt-5]4MbP0o擓`Ul:HKj>łES@ ԣϡ~2#7gPY1 >`s3s(UpVPx v? C ;}e` 4Hx #J"k2P;%rP$F®BJ%HB4H6+c\7G!MYK!LFw6e直L+o2 5:1KG(+eBo=D_΍ !#7H;T9B_Ȝ " d8y8YB eC8˨2#iq oZ^ŢcK%338vXԱj xp H}-R[E峢hE+S`E|0cBl^0sFے,m PX iGoY[/Gl|%гy-Z{c(-uz;oFeròNO<3hrto[{%V,Ak`Uz dULd6XCd/a[E$$S0uծ"r5CcL$ sJ/J,5YE1`Xl53eզiP8O [N8]P;B@RJQR3R36ShdcɿgpB1|`qR}BZ hEh-g)í7KۼI 4\EJKcnU;$۬`ĐuA>ely$Fp=>1uVL|1`aT ?+@B7]XaU9RzoSbc~y78l.wz{d&bP.ݰC}s58YL^X O3R^h(Y̜:y;QƬĊ wYMߙ ;+I*Qưh6a <#o\11 ZdET4Mp98x& )W7J{Mfv|Mz}3Y? ޛF[r\eߎ<ӝkTU*,[؍ܶ=1 VXnyx4n0mɖd*R{ǒGyιU%YO%Uսo}R#g_5LICn؆IR`9fJ%ZH 3 3IYcweDGc4Fc4FEP}p`k["U~(O\G;sV~ha5SB"O}}xB5&9c_G ~с< &e`*ZE ivBXظ|xGѽj&OےS`g &1Nʻ<朅٣2Z{.2ZePy}o}2X#*7>oVs37H Ϸd |Lحf+]԰JG_z˵Ym?SYɚKQQd.<8ap]fT+aRe~G1ew~"hO2@"p$ LD 'JM55n4?. 2OfopG,hhdn#@JW*iPt"wu.j:%a55v' ы@= $=7B=,~l{[ذ%a5GNgoBBaY5@C:,OC}n trzO?܏phr i2ˀ.|.\9ָ׃lz=뾚99۳* L ]cu%]ywK`Od2Dsxt]2vx9[r_w (9%* a%sZ.T"⌡b46c8{0e-ּ"ipc 09d6b+w@c'nFJDHkea@@.; +=A>ovjA8y[?0Z!h7-6PU5-*sK U؛&G:\6Eqp%2ȧ.URdJ%$Wol' QD^FuŬ%7*~ ip4pθ1eqf=`6oJǘ7cYjt |̀{[RɦwL%sU iPM@g> bJge_Fg{ި(C]7'SɑFhhF3OӥT֜2TҮL,)Ip\'i 7姞C샞[8Ssaw !ٔXcq>,˝Pw6 I"410yY$P)Vʥgax'@px N/kh5I OŁǟD{iGyΖ XDHaܓњʾCQ!{=ځkg89({id W_UakYHMIa52gw^D\K|u2 Kmj.Cfg P,3FJPvfU%W ^UA,r|69/zAt(\-=e9?pCy,/Aߔ9J DfeEz°E!E8N066nsXn-VVVgw_ ı̚EhheCieSݺuڹ&YJ lE/Pc=.I氰aķ:̴!6m@srs=;wuQ+l/fc'o_DoivASO/E|8o12ֽ,6[зyx˷1y.`I!C " $6{=hXoƶmk @ 4\u>V}K؍[wCpO.yA*fpOH`چL}ejdm{o U>m.li25uN`nMplr)Kl'9E f{%$*}V4SK1gQ{YIJBq.'*L~0kV~HoZz*~z@"!R6 LO H?S//4R Ea#Q0 [ auUF( Hk(fȆ CpvB}^PAW^ĖuF2qz+Vo؅z H)p'7ۇfۃ>D1t#X.C`iC螴͙6MC<:'o=O6O^sv"iݲFmXa4C܆KNGcf (Bi$.h_{~h1#zI^y8ַu\sι(gD}d)j48CsK (}6k| 0h4%Yمњk3H!t#0:-2 r]`DFrfP( HX|ID< PK[6 rLu*©1$-#Lr_hOabZ$Y'fIU`sAB Au#,ZBY]vt7D][wO=t#8AK1}zDkf(Ac ݶ+' 0ryVd%~Y,0#^B`!3 B"UY\mJY_IYjw}s6^Z9ښ 3PxsNx =urPV4֯;Q08Pmq<^8sM*n;Җ i4Fc4FcnV)^j7r|Y 6vCCLCm\w_;!Z!)(, *VB5`H@o& pٺG|u@ :vh!wl@<fB++]5@ z/jaxym{†uA$Ѽt[F[@k`=XXe7נ˰A~0J1C0if/YWJ)SZ"ɶ- z˵\z%`גcDUhd \-8?Y yMok]9(X]-c찚guIKgz{f_~}ݱa5*`'1;(!̒\8~YЕ&%e~YT j#kU^hx4Fc4^˃-`KN VnjJ ަ~?"8‘CO睁5b % IDAT@"1ab4my SVw`' ϣ>z ^"YHh%@Kt#4'iDx:/=$Ha9rSw}VPIv{[t,^3rLuh_~'1G$&ʡE5^ל涎22"o|suNn6h쭿%2l}u(w-R%aU.a}dINHgޖpG 3Wo*.YcZͲU"A5V11jafHI%KխT_WΝ P R ːX'ѕ>$m휬t]0? u DR#,9z\Oa͈\I0$ p)MrVB S8Qij9*B`U +֦vJ 'RE15(,*.L_4^@Z D])1$4a)!phܼ_q@1ޮ5=ӷ"x|/{ܡ9E4]߁5G`axcz 'Et0b@APpGN:nOf!_|?:ȩ1KA )+`O:S~aCkfhdb.F|^XXz ZWq=kdk]5߬7<ݐJT? e׎C'7V{.{8wSYvS2*T!Agg ffFncFFi'mE6JA؞ `Q,EѨ6nz ^XDNyi C{[\?i8y^gc@0$h:r6j:|f$4<݇DChO`Ն>$l]Z!IBK2s 90gz9BiC$a4ރX~~?s%uho[oj@ M#ɩÞ묗 oʽ|y2Z+4C'~Į]'bzjJ{@h^VQq9 DϐH` jΫdE2\Tθh*+%j#spkuh"g]}[T*n)D `cgJ&o1"j*س5j@PJۖ%w (#`f4F2zFZklٺ zhAتwz|7uytOlR8<~f?80Zp*VڵSΙ;}X_Ck Vy?N>Im˭ ww<#_&v`O+9 8`3vK%lYupC9zPe+cf\}Uxqyceec##(^>%K}RIn.kC;7T} q VM ^ ϔ%v^>FR!N^# +AGs{ azz [6o6uKǪB`Ӧذa#0@Fr! PSV&11$ӚIX,SXN3|)Xzd&/8:BPaNA@!Hl-]>{` yOYT`Z:Ú] 9CAFz<8F񿶠q[aM@d`v2@Z3211V+Ŗ;s|ey.q (͘XXM?tiI$Đa(IrX6ใH ,{1~&*H 9[n vWBZy~wi 2=B@9e؆k>SŃd1n'tv9-Qlk{.plSDZa̰Rn6* é; N.;]妷2X7ŋ^-5ZzܝȊ<2ݬiώHl|\Gc4F{wP*(oQCYfuG X5BcS_3Br |ŖۃkB !Mړ`iݳ[E2(/COLKM5ށD $ E@+VVU*>Xdˀ,|U3c%@SRІ)(r۾5cZ %̵~k<e˯"!w"Sc+ZwկL~f!ebf1TYb7ʒtʵMd\j>ԎLU޳ҳitTM6`6uu;QܻeWÂJ)Y*)@{*VShcE㘢Ѻ?1(Lq5wOl9H² @1&vF,}FL]| ұ Zk,=h,sN\BLSlڀFan/헠xރiL{ÅhwR(X+ QnV~M-LoYa?8akM78Rz`QbYv ^_muRʫ$Ͻe2uqmu5,}6>|g?w' k, <|=U|K~M3+ JMc$u楦Gi1,Y@FU"a/)&TZ3[^۰ܾG}5nE0qv4@斍03J^.W8oC_p &❘0%vTCu jUQi9Jع>_~/qtLՎbDk|\Tu,ZWjd3au|w# |^e,\3 **j(kZ$┄0HsRRMl^KR+mfaAn 0 ,:/`w`o_o_!$ ]nv$3k1c { ­kJ`A~龈0 +ٻ-zj :4lxehvB%BT- ʵ4߳냫uB55[O 5;Z:>EqE qby5+GkW a?|fίG)Š_M46n~9 ˮ U'ΐSEP?7c3$/+>hA%\ lyNNٌ?m'&K$J17B^vpwoE@/`{}ܣ{ܹ{ &8u`f@BK Zȝ`_xj?蔝Wעy h17H2dk3̞ٝucҪD{~ hX]2aGSg ku3$3~s$;g.YI__#/ϯ> aUpcq%N`ij 9tic0H c1U@Bc`9U2@UsSgMeӆ@qfY~C7yg ަ vu)[{9q,!w韍6M[W[cV)U+.@ 뮽 7݉?fLCX;8=0/ 3 Ɨ2ށޗ|fL|Gy9hL+ԚrQ{OD,hG]#+Yu%ʘhrPgֽ(؍B(UsDEs,*VPwƺ)(oTs9VM_Tq%Jkkʟ54BPbb(mP9HG.!Y ՃǾTv 9gǿ~a a@u;"CC|! r\od:Wa hh+`D\E2cX- _9+Y @k4O :D,Mxͬټc5/< C|2h3QghuaݘCZD3xϛќ6ЁւlalAvR?_d6\`7pU٨õ ~='Uތa֨s-5*]Kͬ^mːddB46޾uబaJu,;C_G.lx.'sVchG~ 堨 뎪L3mGhhӿlV= b#w[݅p2R6qżXR)1qݛ0swއ.8V`hoY?N,? a XyYлAPWcsѾRLu:d`0HTq\8Bؖ8}k/Ikٲ Fr:?]H6+L^FPu.-OO}t ˲|zj^*s> AT8'ӁNw[v:g~@WB~`\<Ȭ1:/>p)X:?((?_(g~g肎_顔r͊uh p5<%o$.>m] i|0q-^ BD8|Xc7_&o~ S!1#-yZK砄Q/d4낈J|Mz?ɨa†Yяjr~ _ͥjѧlNQgY”Y)+:_cpýYf^ϸ|s tuYsd7wr,Ib}ͧ=z綥K\o=~KqA庮:e_۸L6{.쵴u@D5=ֵW/a~s۵ R Gb7#q;N@mWc-abtbրp dW*[8Qs5!gAK\łi Ʊ©˭tP\1FIvN2gi-XauVf޺9QTV]qCW\AT=[YOENB5cqƜ*JUD%H-Dmߞ,/iM&rqPϽH^n 8AyQL]pJ32^CQvTOLU"GZ)U֔,Ұ#3T+v#>Ra b]RU%NRP^\ J cYhfr,8gotZ $9dgqcs7B?s;0ќ F퍅@?Z" 5†,K]u.M&[}CO.еl\;Zd ZU}sg= Y8τMs̈́*%( 9ڄ:9YTknHFYSۯa-7 o"g >/#e0e{}e^ѠJ,8˺.r8)(?}ixC!Q p2Y% ^B "DQH)u:rd0DBER t=a52A]_XݐRTpVma/naO.2 DJc|Z?6t3aCnn!;S~fz-4$E4aP9Fef=$/gKGa>#G󑌍㒃_6]wK^j-\6vE[ PJhh˸*fu42j7 >ˤ[ IDAT|@;A-{(4*_`U"om D]X7\#1Ma^iټK k.36o߀f Mg`8/8a(Acr\[O]}N.8b'*ʧ%/GBTJ%(_5J2Wt|uZQm*p2a5o9@%u,k]M]sKPu=[=;|ۯpͺBW] **7sUވD8͗kdKN N%4yCqs fxc`f" C4M4 <^hI=aJh40==)DZڀV$1I`Æ 8th ci4 ކCk;;;|_9v^p~N WA3_ ?񎳱m8n/~܎֌mc ~]a[xŅu<`I.oҌ|`%JFj4^uR}k2jwyi2g)tXyݹh>3a9'}H:aIaФfדfr>gfƞ̦LQY{{O킒Arϭ_~/kmf~GU37kW&-oAfjiD*>i'0$fMstq$!۵_ řXaw*ՁsQ:{qDua4By|l㍂6wgtcx٬(:u̾= 6ˬμ%(a fcmk]`+bs4AZKi_]PUq}kc[}oА^]N*31RV~*JTNẒ̲-*o\ lJ'џTaÆ B!QBBT|O#~,4p]FtDy؛swh ㅬx/.;saۏǚ.=m>W濟]G_nO]mFtZPt$uz GU;(e/)-\|i:{!-XNg-Or*@}UF0QJ_CMu0b3l}-!TR ݣ V$l]0cfkqڰIN Pv :8+v4E"Ъ6?*X[oqpԺxv(y~,;󆊌*ڂ0KUdP Uf:Xj7k~>v;'y&b؛;%5u0RvZ̴PZ9 :֍ ;6M)$I{$OH3P~ y| 2\tF\;gqN|鮧4cqS/q>yl^3^5cx<^Mxù۰؍3WBha |Wq'ދ]sF rjےs"`$c[V%[Ƿaت욬1I(uSHuJ Vm1854G[sY1 ~@v}0iC\҃oȘWZ0gc:Z۬k\s?k * . 6Dq-='1LM:V4%In1DQN8w:lܸBH靖2aqyt80.7לqӽo^?{_̌7% +1w~}n={v %~}OpYcMz&}{S6x~[+.1rQU/n7vu[ک\}DA!BohvW-ـŶT)Tݸ9_Le]CKu{>xS68й*UqSQe3ڔ?|* qn :Bcv5Wh `>Ad{.ˏ{Jq`P)dʷ F>{|6LC~pY}82hCrvWJAI`aa̦"c],9"–͛clذ\2Qx)?ƃOu\×?^4]qĉ·5%=pkVjW1gY~FW`E?}G۟oy7~W)eڣ2W~|2e+PƵ,mcrEo>[n}'y@SRsTVm[*P.{dz=3`p+N݀s~*$Y(ҀY-حd`Dۦ=MC:T0TqM3aWS fJLrhb;?QzUgj1)]#_s/rFuXA{\<T{n}w>V#[Vyvmy7j΃}1S]M]|4br)3ﶹxW pc <YHOHhBk0 011v )R$"`bR B;_t-[!̯=qfa 96~`̩N-H\%ݡ#+L5q۟e/?߸/^[.ngxK?|)~;jY7rӸupC򟝴y O0?BV7v\yVs*ZׯY>RJnHu/WWpO`peXda\ui[X^`ҸJ=TQ;,`3l *˩XJ{7V(?=<Wߵi{emGөvx_ӕFgy"Fmڣ&a:{lMLgŠv\G0~.gv" ̎O*i"`}@H!1)-E :"!f"A;hXRP+="t:hT^&sY@cXgWCxNǖgpY[>/߄_\w1|W]O|Nuܺn3o^<&;)DA߿_g~3M;wz|W?}cAPZjv=oO]M_K5wfWgЏ9{z?1t4 @рiguz**7 E+k|׬_/fvdVb EJML$īs Ipm~!h/zL2hr}~+cw󕯻zCEUegĸiPڔ c\- "t!֯__P{TK5] 2~oƿ~͘4p%]4|돦RrŸg{yزv7kFcqw۵޻͏yh{!~?t=s]'<ԡZ| 3~ыTԚ>|N;aw@0\p2n sE2j9ESR`R^aSIY-0[򩎕%2+jUb_vLI9ϊxj2%6-f Ldz.9UzS+ ģ,`$wZf/fG=o5siK&5)gԃ\~V4E_qadd}~)h>{~_Zk2.t M10w?׭A0"$;fiRHP..RrA01'A_[O#'h–˅ e@BD+]cb7IGHzSق96"YZ(3vɂYjWh =qZ 37#-)֖@!0uךst7iMdzDÈ+:)J`yaj.YD4%lHqAf Z)3oca: 6;bs[ameI c0gȌL8YD((9AI$PZlh#"IМ F$qA H),* )9@ $J} v J0N eR'0>ZC+^/à1%IRC sLI 4{c$I AHH! $AhZ3h^J+q4">R<^f;`a-BHS^Fh!QUJaVEQFffsTJ( h4JYCNߘ0;d>G Oj۸ibp丽c@>@Bn>3Sf=O=6F8 CJNؖFأ1Jwp֎cGJA `8SU0Ѓ9l s;[TȀDFʮE s-\$iLA) LSDq#Itn'%AJ Aqӆ0ci6D b-DA7X6Acm~^ O i*t#YITЩlʸYNE=F@") ;LY[ @DD A$ CqϵV`͈@)(B!x*3*NC H)sgl>4!NЋv{$ d0J q4J&l@ @ tWV$1VPӫlFfKKh42 HIq 2I) "z!!@glD#`aV5^$ ̳֜#kh+Au$R@3G:d:w뮵FŐa0 $ `E#Ra@2"Fu0 l9)4a7!A`_AH֐D6, ΐr"e(9;iݱ(59JH閌J"]V+gI"b2eT.MzV%,dkT ^J={"0\Q)Y855QݧEs`VA֜ɲ62\dg)ݟftj21VȷϢ $ص~ {DuY {>BX%,=R8Noa(A"J:GdJZΟcsoΆ7fF3i5Ќ&I^u:PQ(N0yB:xN^y\9v]|"5 z^~p]z]_x|Wǧ@G/"OxBDB2D&b #T7' -}<ES$1` e-WY9Lm\|Y8,8䢝X5 ,̹XH[M}ޓpgPU@ JosQHnכ@(nʽ(kR Fc0(PPf8#i1LX +VBǞ $RRto,qF&\Cr,,Dߧ;<Ōiס?:@lRjҍJ`$4&-ks$+~D̠ۡxd/,A*&Nª СhDKg'AhhYƟ(ʠ2C&ȯjW2㜔CSlsZb" |tF\{ԝYӢXw=I B/~O#ZEN" KEttvԁ9jDض8v$ R=8gcGQ*PсiTgn;U9oy[Vwmėo<[xi:ǃci9Ai4\ E SN mXM1EIvRx*8cA6I, PG,]MPh`!? j 40%i籱Ӿ+Ppy\BU+ʈn< RXdZa| -(u}4Ҙ_nfZ9pLAWPP)ܖ>S; IV,ɾgp2?+v[uKhDC:2jah.9$p)S-$: pFKgvB{G;~uaU(s,(1 g[~oK3EENOs1#(vҖoNw9T1g |~F) J,i̘N\$(9omy)s̒PDަ$sU SmbjŒ,c2`41cȜEm;8d~5@?y( ]~sme):ߦ>Yc}Wҝyh1`_(\<u{JT5t ]9AS 29U@0ǿ9DD[c^؅GWc.l:w)nb ;_%g.@\eYTpJHNJΧg.LL}(ZM[LE7Ј:V)yQ#ٳ$ls! e"e~9>b)lcfކAD*Yfbx_Taʟd, ,A05Hs1r$kfRDNiK\jL $k&'-UC:`~A,w#1V-EհP 7i5.24B $GN{=rHkq^z|@#oyk;7s ^XĵT,݉T\QS2տ'ݕ"HgPILֻ|I&Pai2 BMe2Y7$D,ͭt_4Мd ,%3bL/7 c27o(;坎X 2LC<ŻIdp\"!Fu R1cOA/MMBT Qz66K@MDx>EZZZ «yprؗTa>E]׍D7Nx{kH. [gv &^\Gzڋ/\_>&.H,bfU@6RP'p*9&Pf|e6b]"@暊9Ii V}#H*Bp?'Hl)"!Tݚ uuz>yP%cX3cĄS$V 3iPƘKbb8jq.R(dqo2 R]Y8Khjj6袄H!ڻ`]$)e|s У`Eɀzo^ Ǥ,myF ʘ^s ̠!@R O{rPP_z (k|'!#F"(xxx S338v|--%p.*QVhzT(Xp,θIs󖷼"Jb3i[j^p:#(`5m {ÑcGQjiyXf ~|j*J--hkmwn1+Vĩ|c $WII2S,,!Khl)$,u1Kf0.jZ, MФ xӏ̀EX֌`EJcHA`g>*4Y~TWJPZYZx y<4v8?XD.qꕉᦦ,%z@<3*٘Fqߥi6#3 ~ZZZP(P,8c( _iKgV-xЄhiiarr2`@?X`rr1,YgK.AWlNڑy[43ݮ8 Y!9?瞇s9===(ϔ~ g?) tvw(  K%ajzxxQ8<4/Oc^Gք>6JIH˺7 2ƚZU%l)}v-鹕, ɬ!m^>ͦdfސthf6/3ͱOtF2 `qV͟k"mu54"I֏IlsooӫśaACխQMDhimERJ%TU`ff1JE̟!^` ߗ7.f NE"QLD(88uk¶ J%tw@BP@Gw7p7xWRJc κ,]T $$ ,"f0HHH{<Trhf a9z*h8^<ϋBZAG-oy[L0z{zq Xv-Jn{IJKQ.`ltJyBr3Wޒ.c)/Æ7ѳ;y? qY#wB˙(!ʻKdڠܷ,)G'XM)6h>OB˯,q>F榎K~vGKNɅMALj87Hz'bV`y4xLVu}g\mjU Y!Pq/mddKDKk+J>omp,=?Qc2" K9y>Wq)!Mέ%4?4 b(SP(& rЗ,ВW^@fVc{L^SL(M{:NوԕlHPŲ|ҥZ,rAaEhqsήnRC5YAKg)IŌ DⅆR/ AUyүIhfK22oZ___D9K -hok^DP5ɀ\,oyۯZ[ i2"T8J0yxbڂx"C²ŋPzֶ{`-p ? YW!`я_/:C%@e*!sX  SRm~=hulM6aɢE{0~%5`!M&9VDƥ§,@I:h,^y/XCLLSRЖE *3YySS $ИԐB 䛒"A t!幛KuSqԴ ?Q]J mTi LoZ* ]ON͛'$K?ˁ1Jx25&?sfyfm+PN-oy{mpxh. {= .wu7~ÖW^c=Ã`z-G6/ 'N`֭8~|OM(9all CƛV+8rC'00x'O`ص- 9v NEV]mMT705> c;A84x/ByW`bj ˖-ü$}YX鑙ug;(nmfM{FF5ǧxT흶(HLNisc362݈Ok:ĀĠΌkk))XH IDAT\M#E;{eDb["HN#wi7 -c>g{\ىQضBl {]]]ss †ݤjrfH@`c`prFOa|b;/oyn܈e+K/ah).X}SޝwOO-Ka7n]xo>~3m{{ P*o>}xnZ,X[^yCصkVX cccضm~/Ax~p׿<8000#GꫯAWW硧;v!8+/qó?ޞxq.\{-ZZ[ Ox(6{Wxg)a #8N9 U Hͯ:Pr&$iI0}"4gg# Vҭ)ʻ%i9MPFCPޅ:r"An$Sx 9uC5P@"c[oAOǀlz|fChzٳ;I`S _8#8D FqҿIyP`~|M&'066qLOOEu`#oy[{]e[[z*ƪy+V{=8sߏɩ)Tu,ZgXp!z-s SSӨWkxᅰ- zMѼt=m^===.0ڎs/K-.[%Kb֭]0NaǛoMWaW\cE8Ɲ>h;:.v5`Y6,ZXli$RՅ5:^J9~gؖk˰dBXg^kVam{N{ \p`0Nڍ\<g[pgy}m<書cC]l ,Xwx_ZZ[095qp!elٺ ` J--xga[p166 !&n&w}(JM)199^lܸ ˆ+W^uVZ "4/_[ D@gW8pW^qȚsP=RYqL r7(Z)Yj&5(3; I(YuOxs2aK+Vs_yc Z=` @pcF)"OLoag]1;*iU B}[QGr6E@-'G?~}7+Oq/֢&sEm,o$022 8Étwwq (gP.W0Zsl Q,1o^?*J  -h[\Ekkmۨjhk)arjN˲n`Dh)8bfY jmꅊiq(fU ,"@H0u!|'`T]xHɻYd:CTY9Z37;4k 94) tԵ{񱔠zX+=GXL٤,=B8zGЩK8nve Zscv(, =5O}ӧeo*X~ ` B@;9ѣ1=:q,@ \; ܨ[_=σ?{-?G088{q1|[[_} 'u8Cp}a{q]wc!/=u E}݋a|ݻSOadd+MԊj=b3 _6s.U3%T@< X/*8# k8wT9 F`J cPµ`A &T|U:M&-~Xs{ 5"0T?~~:Sb6Ff56:wJcϝ&`7DNmkԐ#y^FA&gIhL%.eL~>X* +lg<0Ou#5da$|R RQ N OeIqޏy ׃N𴴆DyM5Ԝ9.x1?,oyǓ).3b2ֆCA0ԧ091 1y-lٺrw/.ڸ?~a%mP-`H{{'V* 0ڎϟ'p`b| [ncq b8l3.B\ pcXd _WjU,\}}}Ǻup m Ýf] utY0XRøfq-YMII5kYy)MXO,2e7IP SH-O }8kS<[^ύ֤tZ<2YUUΐ%<>Yk> N#w|3w aS2]3MlVw5(ӿo*6Ca6 ({wpCg8rd=+= Xߏ Y Oxj&n>ǏСC8qb'p833C-os!iзй$#o Nk^1z:ڰj 8ySX`qqB vDP]_|PTP)CZ]W]yz}8|0<Å;x[G{{^ڲm"9˶0x>Oc鲥}97t3J8xm`& iR*pF,LcA4B87{#tp" (xT\OˤqؤvDXIP!WzJjDA4xRzMLdؔeA we0sA.5Yƈ14z (fKRFӗag H9D03#(c, mPMU[{y`6)Bp%1 ShP0α~zr@\8ҘdPMvW/Ѕ[zI,3lU*2{=[r%{"Ҕ橉el~iB"+jN7(&P[ݢM7q ܄Nm*!jב:CBkAĤ ƙ䀧C)n҅ i2h D$ ݘTQ:C`D?===cǎ^s,Y$zLNCC$Caew{d~tr*T]bű`AbN֞<8/Uyz {4@5X-eTHNDZmzS**:Qh-ژP,`3PP,Xt|j]кUsP Jmñ-=V}p K(V;mQnd#,ja{hRܻ rṬ;bOhֱ$) @0'Q(O0bm^4}5IM$)QazXm/;ZQ|J{2Ĝ2FMyr0?4~!LJ᳟wf"`0B3t]oJU#ʍ:1_rժʂy"P ly[N`!pW |6,۴; N]8hmGZ|-\$V5z3˾^p`\]= 6FF PVՁK} 냉Vk^J1L͗5}@uK!K \ pjIky?uUf@Cj5cNӹD ;`FsCCTXSc'i}nU?C(ǝ잦6fhlh'A(ŋ4Z7D'AU D\uۆvx*N &(̠("?]JhR #a!(I]#O4lKm/^]KGض0>]]S8rP,!G JD(9/mقÇpyaUسo?m{Oܽw|fe+O'?~3_ca?C {M7ߌ/Bl~QpO|O<8&&kťmı!*IN(`byJK/f֑F4C[WJ@F鰟H;O>4LNH #bҽʃoԲ*  p OSKWO10fCl\lX,,jldY ?,6~, Xpȣ4A򖷼;}x?p~u7~{}LVv'r89:'|^z)n0W16: ,W_s N<oļ9>q͸pÅ(W*8spm\0P^r 1֮]EK?~?bEa`~;֜qn-?*m{lbR51NрDt iQ9ꮣO}˳$.R"}81EM+V rbo25bn)CFL)2yd">] w4!"2ړ@qUKܧ fJ VD?7zHjVL3?+YRnwD, X.#8zc04Rӝ1-o:t3&ua3,L\cq`l|$ GVūoG?"<6 K,s>!ؖScŖm?P, d0rtwcBog'f5bptزu+F[k+-!1͂X~Â_Se>3NnsRt"d< cX/ 9q$b_ dpf: UFdf D3!igrtSjXLAIܱH y0J< e" !g-|E 'σ AMȄȓ@ap="ns$;&"zO3lX93?ERMleV7 ,P=\Wȹi󖷼Vķz&d-nBgk",}NQX C5G@wW7Z% ?6x*]0S.cjz_AOWpYg<5\.\`y⋰g.7ވ}s]LNNvg9pp~ñ8FG19SƇ<˖-8ZT<%T+CLv*]: M1mV%~Z&ԀGxR]o4F9@*iqSjaPL.Dи?Qew mه+:Th)r8D㏝#1NJv{wK6GklFkAke1:Vx, BkFcim[ApmBɱq/P-mm? ڊND$Gk(8-YEDh-8(<j t7 F}n"pu[h4u z{{:qcm-/ 7܈o LNNVk_g)b^x[1]~~CسgnAp@WW.\Ai] ,l$^|e z o !Jl׶ j,Øgph` {PT`qCpϿ=ix7}mv w{/j*S=G|xŗPp8&'';#Ǟ|  Iz{m2$TثRSAnYx_+rZ6MOO BxFgg'* ,z8J\Ekkh_DʖL7s˕.{ NEM` IDAT{`9W߁bi)$@/5a]1Sى+B'Pt5` `5ynB֬[R W_{-lۆmq럏5kjx@]6lOz+X,\'hɪZX4e[M54CB!-d㹩Jc#YřZxsʞ~]YFg\ZH˕Kc\QːYVCf:Y g2H2/R#H~^dG? cXAA@|_>];wbbr$XW_{6mBsm'? nv=c5lRӟOg:vG{ǗK/axuT*LMOömaxoG6 P(⟿Xb~K_O=UVcղeغm.Z<=7vm~v78>Zq}maX|9^xEؖaJ%<_3yg_r3݋ B3,D* F_rnj*~ pq2PGN |vqۯ=sP 4T*FKK cBV w\(PTQjiAZ(@H[V,T~ +tb(vv|7d~d:=Mߖu25ߚ7P]p]m̐1>T,pnaddK.Egg'Q,# .ukqh`_p.8095 0̓lԪJX xp0<zhz~Maqc 3:HHrsaP([n7_6zBBILzii盷vhx=Gg`4J{wl _fo>|#LeO,r`1*Qj8$S6uX` ,4ˆTRd/r^D2@7{Gtkmb2ѕ%OjCW7i@/C*0/q̘K6HcY<SZ9;H2xfCu8î;k.|7c޼y+T, kO<.((gy.\..~~ݘQZ~NP(MSOoDPVxoS828[ndžO/~K<Xh!ZJp=Ǟ~c7܀'׋)0Z4z{{Q#! :LNMCWW [vu]r^xB@OO2<[6H5 ́ayi4!AIi`9ցج Z!^ԉ<4FgC(1[[!-ok< .Zއ`痣][N[e;W=LC;Rձ))LڶI>Q͹e) ;%fFlK2O\ >J:%_2Lrpr۳[Yj3s ʐYWWU(M ?yOSbG#ŭj(hkoCk'P0>1 AG˱1>:n"ku0`?u۲QUQP,ڊzjOqLNN ^,fсz݅V)sK OapL0c?ΊS$|@D('FpojEYuӛ—o:݁ t(/NUwKT/x41 ZbV41!pP"d>6 q3PwlњC_MF) xAS0>XNIMa( 8 NԂ&I',k௜+1Bs)fLڃ:Csi٥@ೡae;ܲ獡"V'󇒊M;%5x#խ44mL$g12b3$r>/'2hy[~stOkBmoOab.R ,X@OO~s,њ\li=+OECxBFn.nx1F  u=I2(2e֑ZTC-̖X3˚r:6X"HQe(2zS9DƲ^0jzod# ƢAL6h(n X銙`6i 7fb#+` >M3yG72&AZPŋY,ͭ!H eZ=~EX'-~,Hy^ I9ob i}`rlM5X*b["~[=4~u ]ֹp"zfoXW﯈"'}/(IR1Y "P̅.qz)4 P8}k>R8RAq, <_G(yՀ]obV̔sNI(Em[ 'y)R D֢Dv pEڂ(Q"&fj_l\4C'f9,>j"_QwZ$3(S'.fgـ&.N{@*MUX@Uj67Ą!ms}O^ nM2z -g5Sd`CB"qenf*(^?Bl |-cwҤe $pRC5( iLT<WX1"TY4i20˧O`EpdTUQAvJe&rgkmiTu Z-ie^Y!qށfV" ,󖷼 $@ ]x 2i;LI X$xb* O̼kc@و3@㬾_JVS.`I #Iej )̙*rӪӃJ+tL,f`OF`*#.aq !45"DUL;=4nE Xk#J^#ɷwDKr,]819|bJ%r3r333 "ç}[w100׭1ޞ_0Z-'V#󖷼Ō~{H4(eRۤ<IדTcsc.O&M0i *R`vdJ )an& ?Sn9ֺ7|ⵙ>1O9z:H43N@4&Qݝ0ryxIŎGwxOY{iGfpI2Xl)σ$WI8mPpFGG@* @gg'j:F{{zͤ@%\. bZ{iJ.zۗyd!˄9\r_ 0TG 88 ~8O.1CH 8ʂ"5U d,%ۘ0ȶ6 Afp00SLH<婘z~A[ƍ8ҰK8BMNJ(H *a(Z],8 (]8  @MP- xB-18p8 z/T=׃ ^gAx'Z>q'NߺَZVZHVV0];Ei<&s&<9C"33ѐ3c#1}-v)<}9s>twwCnq== A{xˮ\s}Ω**IJKI>!$A~H ׇrq*OQ~ >` %!- 黪tդ*՞f5cs>Wk<:g9ך7F1*D~+V`۶X~=fff0 Iq(K\=bowk];חu(6n^-s=`Yh!P&[K?Y7`7LsWSQy=iD[MKzQ[<#O311,J~mZ([8ٹ[bCX+c'| lSNuz!ބ[[o_i~83q&\qxހV` /9%X<ۈ~gy&݃MNjN<۶?kK,ŋO>k>߻.<ԓ8csN='oglذGn| wq'~^7ccaժU8qkvf,gm)04kk69e;%ǞQ$ 6ZV*EHkMzz XXz}GÃ>~lsa28p'cʕXPp qGcźu`m^WY;]ںֵqԀ`zO .Bԗނ\ 40䀅T 4?_#O)m)}U.El) %?֊[/b4T KA:,lp -%;9nlllǒ[?h(\OI੧W_ Òŋg˗aDZxݻa`08q=ٹ9t*_泟i}aӓOa 6\qW1Ě~moÅ/9>/ ;8S]u{p8{c3ٷ9lٶ ؁eKO͛a<10*O[wT}gjuWc2!7܀9# tܟ˧z?aIskm\r px9IđvpiZۣ߮ukb u^n+Wrp1@-wΔQZ>EJW :.*t#ybY1 6!HLjIFE*Y(ж)RHX9y9FBm=RcdBPl3Q% 9W+d|fDUPHwG8C0=7/}r` p}[n8+,q՘_z ]=;;/[nAQ8xj8뜳qa၇MvZmoe]{;v<˗N=ދ7o5]c9w֮= z /Z+^+V裎ƒ<8u oxF3?095Ho nz_?{~oLJUEGVO{AEm/tv/\ ֵ`ȣZ~sb/px:Vjʘ185.@®Tio]rQOY,5V_Y:z*>66=0Z& p)6&S`hX\ =a-f `(~hksBQ^G }_1<vܕkrOi.oQ(9LLL^eYJOL0V Jܩtp8Ē ̖L61p,&(jKg١Tb@a0lI( (G JB0V',9K.X=>a<[=E(; IY15c澠f7 Hׂn Q6o[ݟJ AYk]Z"FnbLn]ZJBGiE|*s,*a&d7EW,S1acc9# ZurjUs9$X +;;E"`vjM7TYEQ`z01f0{% c1, 'Ʒ,a=w A=΃a>;pQ7AIw0))#T"<>w75) z _[ ֵ80E,i^+poQk3,~^NdPS{6dB&b94 X~b$E*csq>fCڃQSYgR+F6ʹN 6 C:ݏQ쳦qUz1H_ĪZ-PBQ0A8agKtf>k\)&z4v6"837Ѥ{Ҍ>ҺXzwn!Qukj3Xu,b {,e3m ^ "f Ϙ4b,3anH {El]ߪ4[a4 ET^p /ӎZ)`6@ #_wB\?KH  T[ WWH s8yqZwQO@s*Uɸ ZU9Jn)6Cgi1hSq䚵 2Q>#HeyfpI.'~deCJA00oWRN/UUQf`PN ֵ([}uid>L?r:'d/CvͩΒ-lhz0틮(l^/FǬ܎vIHMi@)KuS-m|t=*U : r)y t7Q5cj tQ,dB[,IK!heG2A63i :1k[ֵ@ǟ9BT)A1fت@4߱  \s7%ȉoUriieu#tQ K[3 +\X'䕺paOI!@8^flv0ɮ-z6HU0M2l&F?nfI!g!X.;ph20T7_|_ /edQP>ouմ0`R-α;Aͱi4O+,7a,cЕ[X?P0 ܵuo!Lc+OYJYX;#t| <(-/_\'eMYصLsB#RZt&sоM I:K. \]+O%L43i%xg *J7Qʇɀ(̈́n͠ƈ+c`wHү9m֔{#̷5 nBa MЅPtǓP삽EzԟEֵ 66&ZIqWzUUҴ46|\I7R  >c1)r׵UQ^[֯$92CZd'J-J+,HhsdmfFcs\j~cSc2Bo ePR6@ U I8Ղ6J\ /g2y_PZ9ZF5{K-s;ߓIZy&#qRňH<,s>~0Fx޽D3zJkZpʲlrBhVs86)C/~I6kTNk];16qoUK§UھhH,f[6eh6ǃ~Jit( 5)"kZ2_2ݒOތ6gAmTxhjMQ˹ſKB&viBec[ B~fݯZ2$JLBrri8=0戁D3U˱QNAWinӌZg[ϥc'Ͻ L2F+,&/u0(Q5 *E%^9.Ziփ@\';);!uk- R 8s*`ߕe)XR_-|6AlP%F1ZHM1b h^}[3C׌3gs}k#%0qRJY|;6 (<+qe duds\ФRkqe.{ R@8f29rƖ3Yz<@9U=g~=i*!Mx^%KF<0SYXܳ9|orrR| /~߀kF]Z~0[Xs.o¢ҳ;o^g%&{ ̔Ey*^-ާZ@6ZaWQQVd4c$,|!zT; q፣|փ_&ۧ4p< z\f93E3Ͽ8/>At?w=p÷?ԧϊ |ӛ%]yqڵG$݋C9?}ûNxXx1nv|ӟO>|>wqq~KN=s[{tC~Y™B۰m38cP z݇{{8cqCcnn/<8l n{+VO>vZz=l8N֞=^ufTCFS5ir fk-xeQ"g3a)D I2k~Ws-سg78{KX6 V[ƞ7qo.w  `0}p%,oߎm۶aǎxguVl߾{}0;;`:~7>\u՘K׽ox/"9ZW\x!j8pYg__AV?1cg 󦷾eYλ^jM~ssso;8v_4؁_Ї~\w]~LX+Hpb󖭸 _D;}K/#>җ{&{={=wߍo\uv?_y%DϢϯHԗiJ6URΦZYB΁ME/B}1_蕪Xb%^tMz7ogU\FbV=3FIJM]Wo$`3jt3TU=E/2t*ZAic; 6/4g8-LD 0[茿@fn;sԛsrqO:ͫ@PtSk+gYvbbds5m1~}k <33i`nn)=99ŋcXt),Y%K`Xt)&''Qż v+B{97\oGg/gqˉ y7 xߎ~spw>Q9ۿ>G}q8Fs|˛ߌ'qGW]a_S-)nǶleXj k1Q|Wcҥ8樣pEn O159N{ &''z*p㠃’%K@ Ys(42X^L2zq4{ |QE.Hc&o·^qo߁+zh]Duy] uǪL.+CY,# xsU(*I֦ʪ oѳ5qʕVP6j~'\r8>.X"fıI)ЋwX=>箞>G{ i ^J0 Wzj~-²e˰h"LNNbjj SSSz Œ%KtR,Z~#n~EQ`bb {λž={߲e VX{غuugfp]waʕpֵ6߹knҗWW|k"]Sk>XXhXu?z%_)]ڂh1f1㠪/i,'z`bb>pϽbݑG6}kAan0k^>8smQ ?0ٟ /^z)^~]C V}]lٌi|[q O;oG| ~ݻòaxO>$1_۶W| wN` @bK8X2V5*1%*]w`غuKVb51I,geFl-?(ފ#Ӏ$Ji)eF';J,`ɂ' 2]Qm{w}:G@QOMѤ1L"}7*9}ߐ!sf$p}@& G&c\ER, bq=;N0˾#˲lge}.X,g#nlYg^55|>3<~N=oɟSO?o{^Ro`嘙Iۇk&%G==f&&&>\ze9|rW,^'kZ~`_kCGXj~U¦,> q`|;Qa0,qa't"/]{xы^^ú{_mX~;00iME\ē;kwjmdުf#2g8ºuGxv܉GO6 y5*y.]M*_ZQeTU1X9Ĵ8Rm{ez\#ݼE:E2!dމes: 7>;#i /FQ@iǺ&+fQ) 5(If s8?DO_wZerGoø?qz!x,}{ƍ830qwR@·%ٝ;Ŀ}cxjf|ng>뿎'za_җ^j|3U~_W! =իʋ^!s f5a03$(>.y1P:GݬC/80LN?8m{j7y B\x楳֧T,0 pR1#dAwvs<= 3^QҕN,Ze^@`vX8׳( 0(& ȡ+@%ʕ+z ̕6EZux"a9^QgY&Љ\pds ~h" шf, !Vmfkv2*2j$#*tX8֫30f`+9>#xjOmSJ-ػ0{{[-gw= "l޲t.\^o~[ yMc[w{}49hǕWqx{ӀSk333عk֮Y`x'qGtk? 0߆78Q[aP/ds5#97p 9aPN+kivV5 ~6;TZ ¤a2*{i|ؖZ_MC@y>>7pV߱K ٵOJcQe358C5^`ng{:&a?z$Ep "Mgf NYi6R.kV:Ya 7x0™Zgty JWH@Sgs?\9|A²e˒ kNz=\pRرy'^v-[ Fzb3τHF9om؀s?|Ιkߺ\rEwׯk]Ga1w`fX-Hlٿ]U H.1Y[ ^:20e9&і3 ๑M,`sXdSb8k\ZĤ1MpZ5?fN}u}}nkI\`ɝY9U2W/y (ڔ Z pcgvc&JOFH>^|EÔ2c)08cצe~F\:iLbQ^3<ťFl719r.`iDo;t7wqby 4`Q-V%quC+'>͛7?Xf qi "rm☗~G>'x?~}w!rix%3g⻹c]GqeҥKBuQ??<裸qy3_tk?MTB7ldv \cvm`Cg7ʼn*o;﨩6 @5HeL72`&P`T˴7 [{>җ&?^:ۀͪ;n%>?jVJi?vOkQMb2skGΣI505a_WRS9ώo~\B34/5iL-mvuĐY p=tm|;/C}{ߋ%K{~Wm6k_/+ox#>O]o;֮Ym'?)&ΩeK}I'|ߎ+V~7w7?ϗ_>ćw ?GŇ>aw$f,QZ?7d@f+>0 ^2/1'h{\[mF:YZƌN|a5hM)0&UI/N HLu y/(kS' AI GsN~Oa|(D ~knCQwnTϙ$}$\rVFC`eg]{Df\ }$ y^T鼖 }s l a֭_O֯|ϼm(p>ѡu=^%Y,Ts#\2T\'ۈ$50]-6(l .+V"3t|k0IpzՁ:te IDATCP.bR_mA~-u56E|EHy~ϢQ"̢ol7nXUL$ (,":ϖEу1EtְtҶx”`j޷3AK3oa٘?yqsO=w-Ȼߢk]Zk^šr 4D"T@ ,lL 1[[hfZcFriU{>/VdF]I-))֟1ڵM:mu|c3qo2h_q>jq97"G39}n6GCIkz}{/߸ (NRݑ+-Z, 7ۥ@tk];HYliR6d߱{Ҷl>KͰ1 D7u-XVB9?4G1!% <(Wc \ZJ2dj %jnJkwsƉIC͖=)mwk]ځ [cӰpۛze grlnƌ{nn-ZQ`{`%k;Iux\T~-5 "3zEl^vAIRښ8ύr<@܍yN9T@n#w\nzt&O]ЌnOcTh\77wd̻b7%g-I򕉁voA ukt4~cP n{^Rj;C H I mXt QU@Jq#A&5er[tqtY7Ŝ?rmsk[j:|6f$)Jb< oO2ovi*O '|w#WPtѸ7Z{.^ D] !Җќ cRS 4'H0-W(PKZ׺v#`Es-a5`"[\ˇٖ\VZ+u`8t44Uf-f@b F HJUF0{-sfMKw}tRqgxRJT৲w67,J`y K]4RYam4Y^ȊgݨtQp$rٹ|ZHa).zG`^j!fG1) 8X`\`9ڤ36,0RG^WVb;2H}gI̮c`^t6)?ӊ?mn*fu.z- c`n Vvͭ^ 4F![^ZΟ$C13#&,(90k;o`kb. p׺ֵ5zuq44OU[oaK zc$cPJܢpm.kC\MjҾk;iK+ 0"NK̷ZCzi 3E?QĐ_-pQѳn.5Hd WC2xC $ <Q@j?RTŀx6Q~G4?^5NmhsKj~Nąy<. ֵ-ՋD:H6PHS/l񬬶p9̥ڜB58 ZPֲOF ߜKPίy&H,[Xi%:w@s 2,BJ}Pm2?l$1cSYg8ZVHj;^%E GlJT4uQZ ̘1Hrιơ .ɦ_"V[V lUZa$\$8JOUi1P 2Iz9\$cdR4I ;k]%{g=c;Ld$(YU 8`uBՠCVB"HzFzlj_JI80ZL4 <Ŏ9J ƓKt,'Q[q~VjZ3=XD%:[j$$1H]&HFFe}8s45ZZ "X/dukv3r4q I ԀiE&سPNB c>fa`ML'XU%)zjf+QP@m*yq71qss9E%k#OY4 E 4ӓP8'6`:xCޏia4 h\5%%9#v?`}ZzL_S 3nUmTRVyبWJqcjW(Pö#{(i6vAĎZ׺ֵIdYnTMS ,fM9Ysi,ök}2նɉ:l\^0N?s :uk][h@DK.1` sC)VZ ELW~1$%u%1u# Wki pv}UR2i X0k 1]SvLy_x沒!#f"q SJKh$J(.sr|L$ 쮜\$uip5!zIU5;d$Rr.H>F;BEߣà5vD0Yuby"G~MQ2]Z׺]jcZXtW,kY1,S@tl/o`zKҐ"!%5eYELS#.´ RtPEUق2E9Oડoi1o@Kд4sjZ1jZSCH̵# Q@Fn@Q ]J.PR|'e(j=pJ+Aswk]B4JJc}ZEmhKOXclALжc_ J.y#$8AI&yE$ $$רQȹeyEYUUOreΩ8y>eF'f#2ՌN'lI`)j^o,ŠgCs'WiK5 ?NG7IB@ zq<̼<+vEp]Z׺@Vk9b)9cY`˜^RjbT$ݶLh*Cs`_RMPXaWe0ɋ(*\dϤWv€An&H66翔3eŎH:l3z)n5F/33sXd1da "LNL kȉPN9>OZ[16谘YlݾDU+W+Yck"53zї$zSH[hmF ŁO0ZlU;ܵukϡ5l+}r@6I uRT.&~u1֮].Z^KT8E+$[b<-1O~># m sTzTRFCBrLO*lp*6n܄M( =,qͷƉ<) %1qfC,p7`ff?az0Ģ>&fK~Ŏ}{]ŚU@y `6Ú*--+ba~EZSIvjWEG|W]Z׺p8l,\!P ``FCPXS['xuhC7x*cQ2H,@jut,Yq:E_WzcV9ʲUEҌBd^mhNEG)Sf8>lXv9?*͍x>h6Sc0+гv܉+W`OaŊxk.a!rpLӾ0vKX_lm;GKm`]`dT$dk1-[FxpD 0Պd)4y榦M;/*(7;;;wqwGžk.;w`rr7]wј\uGe}O== \-6Eaq5 ./q79ݻw㑇8x:o~` ^=pCȅ`!~Xh-uܻCkƾv4(SIjE~Z׺ֵj2֨,^`pM[1C C*kvn` ` [S,f.O>-Z;CQyzqeZm}],m3f.b{ Uzw,GSg:B`S]Z׺@؛a_e&*R&`oI2^ ڠ^[ f)16N]D 3-1La-qW%j) i>i& 2Zv˲ 47.{TLyHzA:ϔtSSx+_Yҁ%^0}K,^}睋ae˖ᒗÏtbs׬y&fK*2pr98fl)|hɃx3q us6s94~1#.:ܵuk&(+ xC h9nB3'vP9]Dr7_I@8g{sy JepFZ׺v3qAXeDWX3`$iDfdF3Poz8O+=ǙxRdFm1\ʖk |`=,k lY^pmضNc9)@EJlC ;<1<` ӊ&SE&e3 tR bAinnz+f#S6nWm@M<j9ct%ʲp׺ֵ-4L12ܛaN{91#c9iW͍(

1bYOߧB*HFKg=mfRdg3Wzm}snp"S~.bRP g$?Ѐ dD/f Rd E;S{`8T=5WGR2} 5zj9r r wk]ZӳU R?%1u'}))Abp(yX_HAtmR%P:|¹ ;%P C|},{$ڂyH:.V ͺ$;δkMh7C80hUZnXa YfkKD Jjw(s/,wk]ZdRoNl\,f*,oZldeZ%]wCNFmv1[r UvT'ETrxe|5ApYa,)> IHm 'HiTj64bSM~N8$2)J2dj>-݈ݤ/k,vN"VX= kf u9]Z:[iZ^BVƅ3 \'Q>rE j)/^ A]9`ɱ9W`fܻT#ŊL]ﳲsk19ڜm5}#ϑt`^WQ=eNN7A*m0ō~\/ [[slm(΂mAvZx8V={@rGptAԝϐY&=CWB`-[c*6ɳ2eYqۆ"r'Tz""`NWlԵ}IT`IMڡOPzwNy$?5 |㓛 #f:]_i\]Z׺7閭HidN6T™JiIu6Jd}QU.~ )!QTIG0\P8`'0q\6 `M~T,K4c6/8%C, DdI. .+*M48d qϭͩ- :&/1$sifCm%Rܧ=lycV8.CsQr#pwmMB̃1z[)ap?AH2jv< ,&:uk][hu gQRi#6 Y a,psdbU6Hu~92jyvmܳaWMA{a~/y~<'4 G0Զks/#@7z>iFar(xKawYtw!Je$SfiV>-$^lFP ŨQ:WE"uk]ckHκ40FiRPͱ\m[ i4('R($p|ZaR\nPSY_ba1g;nX K8:NLvmۉq+B.)4#.ή Lv>σ?8i]| oA3}X ESXF%@W(M]۹9}ˏɌpljg1[\ 1$¥Cb>;=1 (Z׺ֵn29hLlɢi@9jjL,8!A֠P*l#whsi#< %0=o1{a=|dwߋEc7D^?_&ǡ(dJ!%J1W[={Cbo1B4 ˆ9[;g("t4aҴ f L:$@LNI6S> iO"X5 uqPD 0zx"㧅4\>u`4&;G,mHIbM%Jr3cֵum ps_ :ȏ-YRԦl /VE4uh@/L>5ࠃNGES)!A10{:L}웙܅p_%/8 V&360wY1܌`dPTqӽuj 5zn\-GQ u0IkG-N&wzTǨ +iDQ]xGhKvLjrcY1!f2wG`k}D tvd :ܵuk~0 ZRDfk[t%7IZms2:HV&lD.2 Vn2`'xp XnxIkXPKbjY xvy BV>݀P łA; w<9͕B,aNt[kV`+mz15XH h:OYYR\,q|yt{Dl“q| ; rabkwTYhZ>Oj3"EZ׺ֵfHaf961 9Mcma)p (Qht:49^mRPTNnN,}q ,?ފe'SA>yUP$ԕul( 9vPvT[c1_7&S#6_ xx)q^0s/׈ i sW@,pJ|"5as'jӋ"5&hR +ryʅ5T ֵuc-)2k eѓVՖ.dzrdJ\|Q@M*,9H9q`嵐+E  !Wo,4J*߅plaa.pX`= ~ o ~Q&1<|3ZHjE֘`"˕Nr<'1LRqܣ#P^eLslf oK@KAl Hv9E*L!*s^Fǵ! 7Ь`FatǓP[BabCObؓ|7&9`U/9߮ꕘSn{=abfl,{ 199ʪ*Vűy)3ȶ#gco @^a*1Z3ȥ[DTVepx=3Xieb6YBZ޻}k l~c%^$ӝBξLWWsQ>CqsJ1לFKuQn]0sfܫqa;ܵuk S&T[95ΰˎ!)X\< 8PU42b$uC倫d ,ދޮ=vpU3(30㰻`-Ê3O֯߀+n,v흁yq8(l}ƌ\S9[X:q3/ۘ@.;3|gRYhf R#/ȓ &0{l&o #i!/ȫX*{34c BeFИ(=w#_Hmsһ-uk]"-SDbI0T‘8g^m]λP^nckec17,;KZ׺v@x٢9]+&$:R~˳ŮI\"XKMA4'fwo܆#av!+`扭XqƋ0j9lð$,>E0Ym{'s]{Xq(fv+-a恍xz; ({3yJ{ bQ%`M b4y#X6 QL6⾝+m|眚!f66҂mrL#y,1 yĩD[ zMLhqiOg-ͳn &\Ǭmpg fgT=p]Z׺e XJ6X"癸r[ӆɨ4 BKfD?؁8Vg gPџ:tXvXo]cշܵ$v?V]r65̮X|K5RK,ZK^Lz,9xfK8؉zM!ǯG?c @#ns,w?JNoWIlqF,QPkdnC(pCc\tsyrm~}?۸EgQL|jM;[m`. ,XP]E$9QQUTEFx/4K{xIJ|YUwۭnIݭ!ZHڐ<`Fx/ccc  ! $!о/WzU#22-H}r;9!-?'EM`pYzԣ8cE_`?3K>kE$Y,k_ ȭ|Kh_m.{{ !۶[w7;1\'ˏaqSHvH_CǬl53XQ9yr;hHHoɳ{۰c/9=نci3S1Z|tMK gu"N;f`IS\G+A>cZ}~_\SLsD_R{~UܺKkU?QގT нC!/v!2Qzԣx\/e˟O #brejmiQx̐ټ[=V[ 4/؛= i'N= aہٟDF e.Hٌl0{0wiH]C' ᎂ>IW1P#g?e4D_E2dkV`-Gkjڎ:|8# Y}%]w.kb9(sRXp@"o:P(:n n;Y(8Evms VsBq d}oT[*3!;Š ~z?*YĮ{ܴҜwYD~3blt]vR4).G) $; G=Ȳ̾Zv(/*Ъ+-FTV.'@ 4g3%]=>{gл.`>r>X{ D"좳o ZM(g {TODG$:߻iqfL 0~gsEB .>ep& ";ڃ_$-EhW)Q?(j1P6`Yʰ`l*=k߹ /-,Vs!Ke"Ys 9p)Qzԣ/a|%5'ab ΍ Y g/y $ R>0F-$t!@[Hy~{%Ū_$*@R&*;j F.3zw> m'۞Eէ`h2^VW  &Z<#DzSRoso= ï:+~-9(4e!z,e7+豣Dh6,WsHG71 UD%6Oc㵚Ot¼q F(Վ;3J'7v AUzl U0rya>o5ڪeֺ`4Pmڢc>൏p \q_ua{J僥8ڐϼG=^ңTu2k˥qDPiL0v@e2*YŚu;AIA02fy8j%] ?H@<? 91 EN@ xqAs9fv}F7A @3 40z8W <_} w=ѫߍלl킄U2&>;F})lhȀ [f ؓUݟ%΋~|PQ:Xo_g#KB:$v&ܦQ*_ 10!2$_~r%8?~A"^?Tp;?jzԣe櫢 IDAT_WYj `eɂrHGn2+H bʵ)|2k*!]Xjj2(Gn-ʠú~+j~{AL|Y0qvROD1sHmrHBM슷bݟ&&P"f"ikNËn 6ޖK@ #yijwAjoA%vP)#7U~Ǫ~Ù*FbMn>{CG8벣|o Au{fXA':B87ϗ +c[9VEFt"Ǖ1Yl6U@PfdӨsWoKvEB԰zhB^WG=Q:TQQc>^5ӈ)%@mo7Eشg9 c_fB*ͬr ?PDB:FwvtގHMcgc LmcHh5^ތƁY}bMa&4Uy]0ݭ=y8R H{=8} jvCg?#U Y/iReSy:hsrm ˬ^FHp1n`Ut´. T KլE%W? mQz2;v$myɀ f "~*ߔlh7d lhHp>Q(3 $I,gY;< ?QOT FNv)qfV/!эve qLx5RƢ(,g_])SL/.T578ڵ "Bo?Ov ~Xj۠ \v-By`z>$p=QzQ~#O_*]pJMDUޭ. 5;e!I$2܆l>POZ>:H4X˴.322n3<ГP*ȓ7a#xcȞ|C'1Xc֠w֝E {1VM!%?udm+_,`z,zvV_r6F %AG!Ywge`"(i͡ŖRq`O%BenYk &b׍'Pw GۤUv10yO1Qewr40*Nz)ܽvMYhWޱNnp_p#~vNݺVhpH)ߕGr֭MXdYjzԣxA@'̏7 53 ʰ Ȧ[wbGm@:#Q \n:fimWd;1ѷ]姝6?%; vCk@ #Gp;!лq4&0t4#нn$-,|8 Yu|ia Я){ >`& gml$P=T)kp"}*k`_8&*U01@1\>!- oU޾ bWri%,WYrqbT6 ,r\[rXr3ˈFJ^\"q1糡thi 8Y_ko|Eg$f baL|8eHHB2i'HO3sL(96 v{ }睎׾#H!iI+xaݯboÞ*Xqh 7iWRpeOjLWK>\P]0~S{emA"``|i ;0؇1ϳ )|ɃߐݳluDXYp'd=y\Z,m$B>Uړkڰ 8X" 2+gq& * ޷eF̕>8W=9W5gbiɀm4l6^8O?̱֣K3BU 5L֊z4'2K\JfeCsbmn~<~Xse= '^ N?/,_H3{)F,ebH$8` h!qfLQ7e*vh)Fs g|w>Gadr}}w|j7zk0ZZ\kW"`];r `m.v1}QHڇ_7܉ƱGb`̓vSd$@4az9΃M߁嗖`B,X.&L^eYczrQ("r NrQɵ?3DZ`P"_4TE(;B""E*#sRK& gK][f[Vzr[$'G=uK|3ULFϥ쎒nXYX h<5:~=V\vWL"vz )D^l@g4M@(ձGaLGBS;p7cqڛ0̳H"} Xη&8͊z#V{= 5 "P6f~;pzs]4/ڂ] 2b]83Q "fk9U.Pe1qZcL5.}]q?`v0I Au#euC }bźpW b}Aضs p0\:v.OG=q8F+{'G*;ƴ/w2$|z4صX~مY@mDRYy;:w܏o~N(*,= {߹͹ݏރ5A[6cgb ȣČ8shL3?g=c!]~~,u?1rfL{:0feZJQP ~Aܬ`sPqͩ' 6s=XP,`Rn]mfL@Z8`#,bFQg?;bAGxa ؉6;MP`<|^~8Hĭ=VTęDm#P٠{k t=䤠@Fn\GrD3oގE7GiIrtD;? /_{0yND^0r46ҵ1vi=nv_Zfab #}@ l@h(^wz=yXK1r JB?-Ug*sm%5qIJ~v[ ˢC+U_0Fr!<ڀRSɄq/xb1S~/[mxX>T5ʽ)e۴rM^"X`F2%3U4Ώw.LօT~,K$I'T_l5!R/h 4P=oUQ=ˬgd2Ta(vG=^P8g%lܦۘTq;Ym'YK)فdSN[_N3#DAB;D)DY$,Aߗ(F Ř8u=hA{^_|.Ə[^ !f~ThZ8em\gmFb[C.Ի+8YK6z8X$:ҔCB?P&0FcC-"FBPfQ} 0H0a*`ژ̴f hq. ꛣ4}8q/wGFWp,עxmjq [4TuŀSnƃX?$U.XXX@E2$-v\)_ E.=8$h4~nnFFi)%ziv2IbJ$It^zԣ8,ߔ3\jrHDg J>0^z݅~1l:0tt CҤEERdlCL}m=i!AyB&;0uFCObsסVcϿ As_h/1Iv@)6j$R \k'TZfꪀjP$7eaw6m8bf⫎y6 `>ةd_,@e(nWEέ VWE,C.JU8gP5O&&)8M$DR@B{B$`f$IL|P(&B޽(^;$i@& FK sRzԣ;rȳZz[[W;HODO0Fd~7<ȹvCrZ Z2@LUУK^²^jlXH!}7f 11SCce@ҰC A>F0E˂jj@M%C4']c.&@ †3.]t8{҂0[x&3yVhq?777W!Y.QE_qLptEp9dwK'Wל#uSb(xbAjI)/ Rt$MW'-B+UņYlUS Fd4w^,4O9J;rm Bkf}:. _.dyTׄj/Z{҈`aڠ1CHt]03dNCXڝσffȄe,C66~it]LMMazz k#; ^{g?6T~nʕĶۣ?nR ޲gxgp{ރ?Їkسw/7{g?)suۼK#>Ii__ğ}+@}iϿkpMxߏ^M7bqqU1N%Q?PrU&e,е7c|4Nf&ߓBb 8퀩Jr(Wd%tpĩ'.d7`ۗn‘o=OGfX|i>a1q&MvR(I:u(&;$qc}}cGbڻjnʛϦ' K#K_!C5\:P&Yރ*2_y6xvw<D-gy j?7L\0!n`HJw@sya1)R) d{P6ws*F!+^`xh##PJaxx0t= zi f8/uOlYL7 ih`ddJ)LNN"21h6D@qtceG?9\~+qÚiof}6~O} 7}'<x!8xk=333+|#w>o\s 6n؀G}to:ηmWu|'jLU;"'рD#oɊ`A,UXu̷ ;$L5,D)l Kܸ#GC. 9 ;n'`B%~)q/z 50u^ N$7r\V-%PT0~E4,9㪣6U]78!`uk}H7=0h l j0S |.P:.͂m[Q;uxjHmd-0!r\[ EHES= Zd Xf>v8FDҌ$ sp:j)oxBd#M!$kly3?<\w-o|c޲E}x©7Oϋwߍs9K󩼿 >%5]+Qe ?mcg't?{{r. b[-)#SZ`:U{*.ǹG"{;kJz̐ ij/NnH)[{gSOE?sy\`gm>OczwWZN=G?Zxgbaa?(>CD#֮?Qw1u5Ǐ0=HTQr&" zV8tۂ޷@S0qz*U[TS Y=n7شyKtc IDATIOR$n`1sm_}#eh5̀ eG-[0TT쀗8q̤b>YQ)&klAK: mek~[o ;虍eL>޻zNV$;ЀKĬcr,/J,L G(wj?byq0_-OZP:&UzG޿3|7'8,S`|%_@US^r\w޾գJ 6_GZ=&>43z1rw׃9 @Mi4BoщEL@g :w>_&h1O7UH3N7q?WEYFWQ[F61>u)ݸe({G,phH\ڗ2liy0%b* FgD͂\Yp:4uU_,qgVUpDŶخwzVa8ơy2WT;Th4ˈ<;h4=ۋhZRNnXvO)Eaa]@cٲexW}lghcoGGFJ_ q͵z fP|Wص ~Ng~; 65眃K/jtU'\E #{U0Υ==s*[-oc'0y&t9A. 9z-X!M;׃hvi di Ԕ..bw`kCgc`tP v3 PJa.Y7if˓l[2ob%k `sFC"dʟ(RkGN0u* 0>+^ F[ZU:$WɄb1ԏS=]x۱e ]r>\*ֶ,?\bQm{q+#7chVWV{t~%!p>B7nLCcs1Ej{n4M4 !MoxR&X|Jzclt py[7Xv-Şzׯ}mcƍشq#oxnOO;xo"`t`Xn]GʙlJ10]ڮ܅H(1kV@z3MScCݽZ1j21RS}߻ScM@,@BB$wN}(U cf ,e ? btt~WTPATZi *j*h[vYY`]M/V k[,orł[>*1{+ [AബXqX6 *fl6\X 9XDa{~'F077,f Y8̉ A)FJ @#22 ̷ؽk7,XjN}B[pGq(tf2A'e" ՗i_+_W\UG'H=\A{k=W?q+^'|p={ ٦z*~1Uƛn=ſ}G=~x`/" ցu71=ëca_u[0_o+m"9XHd?V`Y;p͘[N²&6ٔP= UA"Tf*ecME1ݤ {G&D (<~ǷfkU4ڭSO-,|Vue˴X `RY` 2c?K|v}(Rʌn򾜩yxg=xl]<>@;Z-t=$R"IRQ^2hBD6V4K9P! ƐevIn[=O}4m|{pw{ZZ!.|{iEfz*WcOblL_N^77غ߾{8UH?6x[Q6*TN~m\5v KV^;y :_ WˎZɣ֢_2o &cObyA*(P@{?[X{&&Ī7f"#@"ݐ5vWWnLpLOY@%M,oX6o0^Esωp~%)g.1ipŊFBs@/Q[E4\J٥zԠE0U$b`Ubi&.ȱߨ:'etK[ ָWl{r`9::X>-':a}dډVͼ$Ȩȋ^d?n}I'KH -./rwnWcc曗w^~9&'w ~׻/~qo:G}w֭[m<5Ǐ$șD XV57aW P Z ^}|uzH V~<ӏG{^z7Id*2"I߼52qY~k0rND D&2{XU%c5MI:S X,A*4bn۶ -?|zY*PyT(s<=y.p^OJ'Upv-Ջ.4>'Dy!kٱEna|}Ln5[߭ :oթ;&kXhIo|acw80hGug~M{_"3XBJ4kB++f)r=-ѝ2#㔛M\|:yI}?=6T.b]>J&,2}00M 6Vbdj*RS{ $DBHSoN7c31~vO825cV]٘0Duܮ`[[M3`A12/E>YH({62C뤌ѱQ9VMJ?kY|Pn d `B܂V?/kT<0`¹oc!d۲}cmg~p9}wM7 HI}[5'K:/$bN>"S&2Z$;X6 5`vnm$ 92n=(_W'%ʿ?W+֣'fdIHW;4D/dT6zO؀.D^ k1zz=_&A`W`30j2\se˒ Ov6eǜ%+@}ڈRgaQ+Im°39O0a0!6؅ϟɝ-cV=dL1qZ1ӳc'S0."Pq\e|ǥX`K%\T;Kg۸m^W*I] 1RYxsa %?J/=O˿,xʶd.Yt{J:ޛ3|Y:iޘO0yhƣ=gwq^t5&oweʁV.5B,)Am-S 6ݵnEbf:TNO }\0 aNU?6MbEDS {-Y~1qyd Z+6!7~ )sn!I&!ۅu)Wi7׽ЫX9KK>2UTbnByL"C[ աp!F&PG a]U{/wm<}Ec~3V;bBs{c'ccObx|:SSP\ Z\mbrr4&Gh4@=0ƶmېez%6>>5ӖbdB.BX홾qD={ףxXړ~uwp&9%VNt$Ŝb5@H0}X +ӪJ[: ^hnJ s Z+ 0T4a(mŌMcca V4 ,1?YId 4Xk֭;*76:\ԙ9رCCCv<شiDlp=Q-efl#G3˨/ Q)E^BMOc(XYIʑ~AduuRB21O("%в覾ITO\ ,-²P! vX $[/P3,j9E󋑟Hi(/e#R"mvJy37"6}y9BSR:{rvY _ 2f atUʽg]aXK)ݠ22fNsH@BbScg&5#nPSJDNH2Dkf>fe$KEq 3Tq7wh}i98N*( vA mSų@J,ʅJK U_Ϲ,F%8\XMbS?PĺxNS$ @z%:|;ކzaEXs Bd!$il*jq?;wNPޔªHѩo=QLöZ,TaoÌlGsi@m,z3D74Cƌ^Du|M(ZS+չ@)HIX}q'=D )g>i-+J_Fr9| i~"Mwc2Y1+L,jYYG ~Kd)uI39RBHfHt  T$!4 l/_Bhɦ* ܍R䱷B N*3Iʙc8r *ĂE IDATtSSTHޕ0Қ˛r8{`~ L\l;*ÜPL6착IޯHld y[D kP/RB/9T3D{ÂtK>r{Lesf9&D|r !y7P2b}1Lx Eahɩ䘅"Y/hQȳ \i bZQ^{=!B\ׅ"*JL q2 ~XLм A5 /O+4U+I"{,!̓˱qhȲTK9BL3ᅄ$8X sw)O8ynCy4:F>n _W"bEgaW"D&yxNvéuge:fwHmrIF~ kQqG=I lZp&rދYy2 QHÜCm@=JZoBhYTD1d>%7aTRLV4U19xUH$!gٝaTuCE%Nڐ U0kF#d.͛piRW#J*/ s4x1eV`2g39[XAG 1f\rweBȭHd@[h OSi0+&{(gv9 eBJ4,ky^ѸȅLsn^FQ]w QXY"HtpIb7Q p$ P=sܴi[7lƗU}(Z@~Q;RV{#d~AM(uHTj3Qݧ7PuRʓZQz#N(*=s#eʠlo?@rgo.oUE$13K]$ ,6้(ngEJbf,6A(k'M@F+3E $I 91saw0L Yhwi*dDB{fxpd?ehv-scl-E}+ ٯ 5/XawSYvQN. ߏ6cx5#ׯ/JLBi٬! e= UzG9a$)rľm<-U5 7@4͟Fݤ%pc6ד,GËPe aB"@@8ӱ %9nJL"x,r7\PT9O2=9X';*g,{͹횱s% ,J'lQ a J/VX뽐dx7gĖw?o뾰F꜏`gK*>L*/L ]v º{imJX4rҙ,c,\1;:jkig4`>c}b=y./,v<:&&&$ SS8x FN˗cUh$ 2U=!c,zԣ/AO.Ks~`>SC?ŘvLWffTLץǸi^Ź,ب(2aRȡጱXܯHk.{.Dl8oAESJm |ƌ&8 0 (ҽV4C 6X,7 r^ji>OՃ%dzjGO Ɓ` hs܇5PrDCU=[K),pϾP`~$4vڅ+Wbbb `xx7n,iLBZ‹m5rs%!%leޙl<+G=bDzWvde֒Ex)imAw4 6=69D=o> 8ۆ\ -YNs%4n@=gݹ ΂޽TZl\H"yuy {`ܒ lms\qs\C bUw+8$䯳e= =x>0T\w*Y9BV.&†_7 <>-wʄX\\&''DzenrJ +ݭHN%ZҴŎ̍,C$Eg#ނڶfdd<`UNu#\=QX e'mUOfuc?RחQ}a>qV"65<6J kc@~rֺ98[Y,1 yP,hT!V58RIJf8*A! 1.2Ifi o0oXN~s\GPMK6ZC]w{pƇ>6h\yzT쉃{2XW^0[l=펄1® GF|߶<0տUPv=ctЋ (8{/ 6/V:JhC)@ex`m]2Yl6Kh4| }/RV-ӓ0<< fy4MHaffXfTV }irmH^OG=B f11KH^Pm/%MzI<&IpAfMT&-4TD`eQ`3|*iD])D?6oҤn/®4":[ "clq!l_MnfasQevaVE@@d߼2krVQ4jh# %.9R@XV`ʧ0$JP%2"14ŦJ1.K.jD*9|%%A(}]**ZMt= 5122v{iVVkӫGחeDRK Q0 C@}1wu>8:2.u=Vd 3nUޤmWa܃hY\Zgb`'PmecpI['eǀJPTmj;!k^,E( Cߜ?2ZG^eX P0efyq!#[_HYw)1fշ BA=/}e$Lw -c|Mo^!4ڒg֨$JdKnK$#L,`fiâ^fhǢ= ~1lZ%lYRIRus9G_y-]t99DFFo-GF%U@Z0vAl G u[VRƪF\7y0yTVC@Νl#sVYW+3Tb{tg: -TH;d#e{@'Z4 yA#ܼw_Sgd.&Jie`;P1y~ Z\M/y1!s]}~00kh)-p;S-mV6窤h½_CzFcqa1a/jnuQùs$ hX~]CLK{S%:>૪~[m]pI)< w[e?r)J]j2\+t{K6ZexI ͢Emr.~z(m5Rg3Ǧ*vH?oӿWp(em9>Պ9kq/Sڏh`1u sU&IǏ@]Ʃυո9 ^{᳿Ƿ>6iX>SY7 o8Xr5el0<$Ƚss3_l!=ʙˊ(R/, Fnu[BϦ<Q`]^&s\lyc!(t\WIm^;}w?e(hE,htU;BA :$ 3 bTrkb|-RO=ZOۼb]%?u!'X|>? >mWD{}?u+]) Œϔ۬(0  UזW "k֬q"b`Xw :Y]\훖ce ]ۺY@ <a N+)\Aam9;7f\NXBj 06`LEsu*܏"*VBf],[Zh!F? pK6_<g3}Iϸ<kVR5Byhs#+nU%֕ڒ~Uӈ-ws{]E7C#Pl-X]ImiKpp^y.?3.tP##Tf-870O1l n&ffA' q&k9cѮLƍ(j7խn[}f5'ntNc9)&ͦ4]{"?;Yu O5 +mgKm<1E۞{ $YY( M!Jh8b?7peL.$kʕq,uLp&@?`#V䚢HB)VWҊ ˾7hS]HQu'] p'rxQ,C)Ƴ-S[06cM7_v\̟_3T.1cSWi&io߰#A˨ҝCF*ֈ9.Yk 8~40+ ꂶխnֵs TMꦘ-jV{wQ Ja48*ɁwY:bv(,JYkE탇0~N%. 4\zznjR%F_xkp1׿ngKdPf﫞稊.sQ(<9{PHƕ}B+/kf59#B[W{ȂJ~~s.8`jx8u}~@o_hHHX^+gPZay R$I4UHӌ8wWɶnu[ܹcS39,K)b-*CUdd 5agEhk*ԁE~q-K#R0 FX~J`x[@2E_hOUZfyqF(qXx .}30=FФQ$P<40)Yf2h "wd9?HG.y׾׬( ]Z:25~ ϏKa^yIǕDqWQ8Xz0V 2Y[iU6>|ȷ^.S)A^? Q$ LVm'|E*3*aYN<%BXyɺc<{Lh$,[ӗSaB]0WbxN7URj[*fnեuT1{0EPn*=wEw %b^ ]s(p'AzLJO~kn_+Ԋ+^LQ=%4T @!9w珟D<ΝCCZ&- HBf=Q\W@ԭnu+A 8^g3,`Vu GAfK]9z̙C  Th˲w>mg[ δj(7cI.T .\KeAJ)z˰ G)_cec4͂߰/Ks}!)AK,8؁hEmuV͞ߨ*uYQl&.kfQ`@('g9H{}4vT RԀh+UBa)p@e+ DRi5=sgXN:3bٴ k6͡Bh̎a+w WEdvNMGԅYnHDţENZV9lVCRdq7B;a.åB 1 ,Kqaeg36۰1 C CY$ Y!|_)$njx1B,XE%x h x]cI+qZ(<^8@WH^{!U.H|" ji Gs`&~Uc-tor=ja#F_ƛ 6,!"cG`+}˘\ϜԦ l527! eA;b{,MS>|N%LNM!I,--AkM6czzD^`5V`GoR8n IDAT*C\͒1C|.hLq(X.OTr*@ KYL4q~ e.LVඒ]7Yup9`pGN06ZQZwK x9̡/D 8Т5cWU^'5Ms %w"">NvXRAtbR\waaTyD ZrZշknڊ*u,ڻnh˄9̪Wm#> ([gqYl޾љRņ=wZP dU0:J)=kh5CزJkٝ˒]l9duJCVUbAĊ \#ȁe56E5%[,:NRhq^-]$LSBCbRJ{.  DfPFfs!@2,tfvP:"s}0`Yq#UX0 JetwJO)9AZ݃3\t!;RI|';KnPcU= ̋@4 dDk6?<@±S@'-hMC 4D$ *=34X^^ѣG$ (6m{ זHSUjhAvEpu[?Qh[jf0nOt;MK)LK ~y:6O1$\tUk6wgDU&Feg6y)@v$;J|L/M˰3)$P`vv?} B*~X2)sl6qW[,C>iJE يPKUwVҷr^)F{rA)Vd1$z,HSf?Nk աeY9 CQ^v$hdZiKU0PN\b-YiY4E~rXVcO@@losJXڷ1(㻑iF#|}a1\ch 7FU-tqu5yDT HC:BDr禮 ZK2g.r>v !~spJ*[UYDE,<HKx2Tc۠rUJHȢ0"(tkhΏ w,VmUl4Pݢ?WocR ּ"AGSlvbao1jjMLYb/Q*>d9Prg ($@"j4)-eA*>'! CIҴǚ)W(Ykxy.&ge[$Ĵ,%_Kri@v;~$5^h9afT{`θ"~o>X Vw+fq ]@)(TKQz:w @j =P0 Vy< k>" #*#QV F8!*%vr?5a~8-T Νˬ'i(Y(Q` -HrLZs 3) ~! - IMQ95U8QA5F;%Ўv( 9v 8Q)㇭mj;%Ì%4(4mT8T~:>೚|nI {G鳑Db\{j]*kBNoK \9ar~vH &]}cKxp0׺խn\o :Ȋz.6c!%6`1,ZZ;7-춭iKpE4YVcͳAlq[>h+-Inl `Wӂ$r`%Wk `)QVj!(B:p)\ s*9 AF•ea~=;o)QZNPJ|P _x}MB NL4c>4MY"ǡBl6)(*eZ-$IqmPI@ {׭nu˂fEr?W#L \ĩ d* $W5UO,ur P= sQ`oizte R"r Qa߯ۇG{۷ϝǡömWSٳؽ{74AGǀ_Gk.lXO? *9zس{7>/ݻ& 2N?-7aطo4M6,JYT÷ aB |#-Z+KX.X g博 pصu+2Eoab&XJ~9W>s{AEQT)$33h1nz?={'>q4 + 8صk/ UƍvqY8v Rd[=~b)p!8v 7lm݆}_߇-[C pCx pu0^>I۰Z+BمE:u W]y~lڴ ?0:vhjÇ3hwXv-Ξ.ફv܅GF= 7[V\N`M7g}S=wݍÇԩSXf뮻ΝCƾ}a維 (oOUWm rUl4|7ܗ%8u)g͚uYdx׭ېmu~>R QazzlNQnu[xn4ۼ ?uҢǣUs6w!'e)؊w@|Y-,O5Qs^oJk‚R81M75nڻƙgOS/֯_~,,d?6䉓]nH;qc-X~;nG+ pyLݻ}7x衇+EelٲhD]cf,lӧq-7$v9~Aw1;,,2Z3q vs`֭<{Yq Q~)%?<Ν;6aa~fQa]Xvq޽Щ 茍cݺuxۿx&'&0'A+A܈nz黾 .^o|}HRz[qq]J>ApϷ܃g}oO&zI:O?iH)195~pͷ`;ʦinw c>`ڵx}ȉعc'fb0H7>R*-?`*{u W_y%Tkڹr䦍%cVoI HSk q՞ \}^OR|^qHR`lSO{W]u5 J(qk^ͯ 6nĶ[>(r?XXXF~Gh/p|v̚Y4}& 8 M)8y$^y5'i|[DŇv5&OY3 Ff*x+1@݆@LEBiٗQ譤 ݫb|e^)bEgSl}pU.<Fu[ ]0okGW *֟#sMs [[H2z [bbqPdas뻝l.o 8ss 35O{a> 0h4XHRX2p'dO,S\R9K/ƦPwіTl/<$/8OM^^"M5d߭- k0HH X W*|/IK)! tc1Јbn&$Ȕ+2)%4lTa)јtBY)`)M"4Uk^>;PUl<pQUѾ !gzĦխnusYa-23VbfeCۈ68cb(ej(~,?2 _9R|ᛟ cxx7;'@Hc_%_#8}ƚ qY\>w30l55fFa@܈i6ן49Nӈ!D3q(;N(hdm:&ڍcf{%R~{o7b1qN#.?)G0AF#cqFFqlI Iv5c8/.:Elˢ5ۉwxTK?Lun(nu[^.v&f.;X0j[lR3p)6@ʊ)+ Ӆm6tL6%M}o06p+rC}@}7%.+8L^^![33׽qyk3Bihw:=w !b]O h`|| O<$]q>N?o/^C V )>ڝ ~Jᓟ,nzt8{,_rha໾M_X>y4[MD2ۑ8y$&&'#?e߿G<>OaӦMկC > j:8~(oۆ /bjj'OĞks='N݇SwEœU@v. 3(1 kWApWDV(200H hh~:h;bN6 e05UwVՍm+o"BlNzڬ,s r[qV o;mɞ8ݾ,,9Ɣ^7LlZ-9b F 6`ZeTIzK7@N} ^sk?ছo#<կ|?MCwq_ct ŇFwqkfgOnތ©ӧW!9s6"@jX{ GȨ>|[lN nڼ ׯ_\W@p<Ν?E4fwSpWlF| O05=H|?\{-@waxN=#CӸjN$*ukߐNy2ffʙl`~yKgM/#kZCpe ӧSXXXE>sOS1seo6"DD[ъã(ۮp;nu[^@ l;d7%!].\TsaNW{ ʂLQs9_J ; †BP4s}'<`;7Ŷ5qK}v1=9I|$mۆ;whcӶxj Ć9p"SN;駟:$Μ9W򕘚scfvRJܹk֮EÑcGm6_qnncjjOgE1zN:_g.pxg155ݻSO?(رc;N= ǎ@CFY 8daH_wށ'Obai W\ <N<-[ˏ<`jj 'u]'|QK}.>}~<z>")qS}ߨiy)D{G߅ Xi; {( .rW88  A "{=hs3$ɒ&VZ}1Vl*er9"Q}.+ lYRڄI@Zby>{ ?=<#j+wbOEw A MKP OO7Pa Xn-&&&[pItmY?oF׃ٍ|[1==8q$n?Sm9z V E/U7ߌőÇ1q.y3z=BH4M},--bzzATa܈(`]ر}&Dqɉq9{qcrr?Eh!yV_>rl6T%{@ \uظe ⸁٩ ;y ٽ.vؼe+^{۫7RJ㎵8sBk`"xUflN^U",*ܧ IDATV~7I ^$Lsߕزe#3I.\2T"c4Mt]t: lD  zI 13;Mss$2( NUq#Q/Tnu# Ji.!xяl_잇cyղLK{cc]0zݱYB.=Ŵqui;pbWmcù igXŦi]c^¯;nr|"W2iMm[ zxEd`lNrWl˒m[$X~"df[lTaޛt24q(N')t֭-[& ơ4n)&&W]345.M陌d$)֮ݒ㘘GhqF#MƮmRJgj chS,8IcJS`Yt)!DF5 -,'Y NKi8q H`0HQ;/$FC *F7hH@P>RpBmF2(Wŗ(qY\xvZH˹v`ɉ i$I)lXJk4qz@D$x4s& ֮]S:˅u"u˾9-:ٓ θ-"s+&|pm.WՠdƘ#c]fSnm^˲Wmh[ &ʐ:JwBuioWg%O ν^/MsfڶB`6@LRMJ^[Tkx)l$-6Jٱf9Qyj^4Pw~ Y[Qx)~ -^i" F,̾,}g$I(mf$ %AV*dL`&E+$\n< 1sMLFJ ܠK]n7~&-vW;B L]jcWdٙ@6VeхJ,:M33`)Juns<3gNcǎX^^^(":6*h5Lu[fi.ц .wRM`YXHcesX@ϰ~e~& 6 }p;sVX4 |\;.EZlfMsJMBc1HR+:6[Ki^Wo)aӋH Aމp#ؠj΀dY?Z7N~).Q8+/6v v3iʨZ;wa/Ms}S AM l!ad +%H0l_9c]7+;Q5#f@  & KֽِUyz`<=_P X8sFN֖e(@S]nKDzYw;? 7A`/7^P x> :H0-!Eci4J F[4tmQ-tśa ;ʒzLS[fS)pתB./ ryVe;3lzGE;z4[Uֿ>ଗ]vO*0: Vˏ9V9.vᔗJiSSMaQ(/QֺP8 nx/ZK'>rJsŊUh289߸l`tF8p Sq(ߺխn?O!/h+4v}ǯ HL&(;K $H\p-)M+\_w 0sr.1H/`Ojsc 2cu8birfo<M30n4T89Rn^2\qW(FG-o-G߻C!N/Ы@,5s.PV%MT_ }#>^Vn&U``d[g9ՅR<4ڋR nXLM-eVmUam-1L~Q2}U2%Fsb@m4Op1@lh[h`'>417_8#Հ>Z2rHeBBj=/ƻiyVQ;(fl]:;V?12%CNJb`#A&`+m/bDu[bU,1>@Yv/.iqzV&`t'J)K}*(JfSkY(CPW7mc;,qFt-rXf#_pAl K~!HAZgO۽ExG(tU }-l{*g9bvB%IJ;0vE|Z%( tHW#\V׆$9ko[Mhnu˧ن)SWZ0fObX'/TUO嵸 6<#/s7W5Sar?N:4}WM0X#5 ; ş;I.(NB=W ~Îܹ+.BTiBS^([Vmaw0 mB1Uۮu9]PE䐨Y/+V(+%ۈ^1\6``m|? k<6cY@WHjxZtP lWerqgñԶKZH UD)\]T-89hN 20LmFXY›4UǍG-FhNq]+yܠYU_q=0f?%gϟU-V [,T%p|.dqt. 39ixAQ V&@X93bΧú:p}+)0y+Mjf1Ee&<DWXY%(k.[cBY担f/7]vk;l_,8.j>'E^xe3ecaN\?WZ䚻);asGBoD864b} aֽn4fۿx9!='_4}!)4??'ċ?CqC)(J&n^D]Xu(?bL|1JRр"N/V۶ě 9v\F|ȘKIe|]؜ JANQLZPS:DY(&pKct 84AE߹6y2[vnWVf5瑒[Ma\]M]6[ҲJSiFC \pL& Ll``_B*f C55̫c k7(tbmC_م@hu P(XwBǫ SLZ2>$SSӈ(,k,?J)p<")j6r4 4M H))z(š5k FZCu[\&0h>1LT4U5e\G' $`mR6ɮbwUew]@JR8flpDTA*Sof!FM[i`cgon&}LEUF5 IxiU=2s.дǼ E)0[Jg0j VaP͢+m,&Ai02ttUX.BHܹs8Ydcu*Pխn>T#IjJ60ߵ~B^W..V%W[㮤(CaGZž ʯҹҷXl+XP^} *Mfd`B c5Ӿl^9*T<-i V[Q <'14X-fjqiF5/=>g_ٟ8`xG~$xkvƯ/c''>__Ɩ͛C>nق_z{ɏ} я?ϣnhn/5t C,uSP{i lMQZs-5t4ͷ )%^gVyl9HU-܆9ϥYEQ2nt&}֖^l)v+D\u %,5}T.?8䞳Dlfut30G;$!KVv/0c5y &WX}car} RXR8`g] p]Qִqv09ݺ51?c0@qhQ-*Z6+@E%IF8(t2@B";^)m(:g??qC;[ncukn?wp;߉_y{o~'pMo" |}C$a/2;ށ$I*ϩz|?wc0`ΝX^^T^`3eً"LNSʘ|AN"+p\ç/T q "KHĕnZqK uX˰t#gh DZye ru-VThE捣a,6eS"MP8A8ۻӡwRiFR+=1[UT1(n$n>gz`H@HhKh4[RS1z>>;(SSXv )=8(h4j51Dzd@* "ڝS3he ?uɸ^j_%~w~ o}k{~ _߷O>6͑Ϛ&xk~o{[?C|o=؟ ލ wq.^/o߼W's{Uߚ[ݾ%:Т0R +x +ofbGa}B֦! YpD0^xJC&<%_} Wjgm2S.U6('XO|bc mb&1nb0>jO!#$'Wиj8Lj;CCJF7(^CrdnF]u%X5c75EvW#RŮ9'1VSP2<+ B2s5ZcVDj .u*g Ui)ߒ*UG^ڗ֔J Z+ i*I$ IJbQZE?>C߹箻T>r(Ç1a {O}*xL۰'\9AM{;O~|q^WoIp 7E(5Ǡj! S*N &=a_ps(ϩΩ[kE%cNN{"w~ 㫽M.e<bK8,!Qx!M#mq7/*<d9lB^nnwظqx#)wy{`U$ ԃv]. Er7"}ӝ'O×tN]ȿ~pM݇JZ IDAT߭QU^מ}{H/qT njO⮎pHȷQc;bS|!} >ЎyrTy|:O?q-{AX8~#@'C*9dcH;Ae}g0Y) ^ n/'˪@t@.\, "-2sCLtZ2CX؎Ä25w7ҟw]k9v^.Eq1Q(q.Fћ ,//#%$I1ڭ6Dc5䙽pjEC߾۾ ϿKgg{=w݅?}=2ICߺu+o9Ʈ;kN|%wino{;Jsrmyy.^ƹcǰe˖Xm0L]XiJ`h@,% c^]<\FJUTDELʂJsqTMOvtAu,H 0r˴ML ghb"w#-,-%Atd$\@O_vPfvBR|.SOϾdWiB\qC{(qxTN2wMh_wyR2%;H0+VVv!yG =pB)A"C_ŋYg)ـJ鴑* %Hq~mKӧOڽcذ~=tE5j6e˖\H ($/_ٟ$ۏ[+8;X~~9|k_Ýw܁|I?oŗq c{Ŀ Jj{A_J۷@]Z22 V547ݼSvFzp,U%4GHYauõ`, NEBJH*w]W59ʳ{-vYRWJiwY84 ~L!Ylm 5*ͱٔʃ[!WIM~S:;zxˆ)NH69ֿrpk?B)L8 pbN7m_LXdpbbR $ #(d~MڬWSP* l44 ^t:5MR3vQ4ER p[&G/ѣ8r(~W}J)ދ#}787=x>g{^`0w̳oWFTu{I4Ss&Of۶B3"(hX@)Rif%vbelʃ'|Gp2/f!uʹuFM$ ztWB5AŞrkRHh^)l3_~[tnϛa8"fwmDJ&'W!|Ų૘BBT: Fq%Dy^6`r_1h_M#`F.a5 ozK5Bʄ)P~WvvuW)b%),_U\t$@ɨHeww`D"iZTJ'p]w/~Kw򷯸Z?$I߿ -oƹ96Ƀ3?sxކ{׻?385KͧkJWU7a13eY--&`4apnkX^, #*6c*]^& AtQL0#p1\zw?C^gTZzzQ6!K*Dqܯ^x(fq4gx[e+p,3-'4Pcܜlɶ"PyaQFJ?Jҁ]T`PSFuacFoX,U "aleg+vFB"cnArw,T l 9 p5a~$? d{~ UDfQ+|uUasBˈ xa RhFsTQ$EF\խnu[:@ʝ &|I$^NTJcry R U !/V7_غ - a#K]p]R U҉m-^?8qT>g6?|KǶ4,+I=lwK^S;lւU5lg6uyF&(洹)p>Øa߫fȨ8qP|FY˒1BH4`4F5@A\Rڲɠz14M $if|h4m|knu[.eBRv̢ 6qcx_hfU uB;sVQ,eLk,E/zES Y\v{Vy/U,Þ_#v9t'@2n髕dkt;ͧ OyE;lB?8yyA3Q=.] WGZCB#Jh@i% b';=,=s3PQ뢸+o/jMԥZh_Z G)sA\!d V=ϳ֮,*+*$c)R_w,[a)Joh:.p ]5LF^4"gO+ }ZHͨs ͖3Mqᵠ5B% ʰɜŲs{Di) [&s3x .Zߛm9u_ǙHV+t i&Q"2ˉ4-[T\ehtwgm;` LMgHʇQ8Թ0;5* Pb9`&bs`hx"NjQDpp| )eVjUI8z 4Dڇ@+gJՔBLf%\MY!T:>^*<_լ\ϮG7,;&\&K9@-U@,n^1D ƎA K+Z9yݾWg]Mcp!RM{#a[ee6&3(l1Hɍ`MZwlDͶ)fpؔ,XR9RΔs!sXֽpK;2f˩4A5k5%L$p.[70O+b3sغs'aJ@}F4 }m)|@$~Z [lIt^9}%L^D_0 144eak.AbV7 rgf:P (Xu1YHP&l`.JuR.tyd {*c ݲV Ĕ33 (,Mu ̀8B]z z~3^sn ``V H#F^*Lk8a#EdK,UjUsz0}T^NCːQךc+$(s! WW3Z*1G3\3gJa f#d?* Hp)UP̪ ,}p3e혶K旚9nY8sIlR8 HH+1kEaZ{J'09s-A7a篻XpԻj.rm9e d/Ba΅$#ʺ`,I4p[{ {p$&C"y*$ox>bʨs6ԝMS\9T@VE1WNrUZ(6Իw(2HEx'Xu1׭)kg%Ef`fxJ 4J[ص+X9,7#$к]3Ju]$!ǴG5\+MX5P14H1A%'#5`)lR U&-r̕]¦Xi/}Gϥ&fD^%^P4±ʍ^6S:e,N>"8yDk;C`<4Lzզ9pޫ%Z"6Ӣ=$V̰)#q#L(ZS y(K`Q-"YŔ:K7[CU[v(iE|oK}}6 EӟIwEa H(J\ZFNKv~(p+u)6;F:37Z$Y# ڊ=@>z>13wL`|h]%-DQ+~ <asy"|ͯaΝR{q$ dlRޣ,IȪjU0E5vm^Qe)]Kt*jB7åJuʹ'2~VJ3ZDofp&++n4QP0tPnk% u<#aZij$k,fvTi\,'VĊ8Fj|lj'A1::!:֭[ 6oY R\`\UZ>|h!b0kLgAӸ@-)%̯JzPRi@_#jv2sv8=nO{ ~ FP=0oiYv>(X`ZVGI24HQJQ]/AɁQ zQhvfQ\8 ieom˅_&gW'2Gޫ 𘱻 e @{[ת޵/Q`F/\`nlR}<56m܄kS0>#177jc2͢^o`Ϟy<ڢ[k* g}Vkۘ^\TjUgmg.c*8 AjL 5ϸl[U/SE݋-*rtJ=;>ӹQ6 */)ÉTzqȮYdt)0}+v&eLF.+uhѼ:V(J3dZ #H ^ࢤ` `ʟ-3CY^]ښTO/ώJREOL o~8vsC8-\i!nUqlyr/ Kǐ`h4=ȪUj/ 2ɞ$E4/42EbEzXVbJG ͮDClB!U3LlTt'7K3{ U{ݴsfPBh7x*dHK}`L:rENX:vtx?'ǐM3(c+dcR{Tk-՚\_Z Hζz\ZǥfdPU/$*#n`X7Q guԀ6NKDYrwYAHDH'?,&ڵ{y'l/f_7s5br*ꍾi6@P,bt:A }}}}UZzgSΝK>G`.fͬђ_h<د[Rh gJb{p2I;nH{!34@\j̮`YF27Gb:Mpb UFm oj%!6 W=o*UCK^DҩGjNrLTGҮ[fYĮY:w5s``.&JZZVl9jbT\hVK*!XXXm0΁8M^WjUSQ WjIeuVŶp^V2 5ĩO{1'ǏRSx衇q]їk'b0=G_=?q'>8"bQI VC mwꣅZ FLd& #f#`!QUQ)X]M]wUEA}'Y J#=4bT kivCfH6h(t&#Ro)ACyzOWZrUՔ tsSۘ|u zz*xe >/\#*#$Ly]v,XIj5ٳKKpz4_Z1H9!H]V}dBasܪ,*b:jyu.[\?S_տï< xMl߱  -R o-[0;wxGi./n|.q=wQ"._^~g_| w]Z vqc֭j8v$?G}Gōxǰ}6t" G:v2Om;^s)fWBh@*nijPYpd{L;2vdzB@/t9*j~x_غxUָS{,4re6MO*GylGiܣ4^: &pe\zq4n-j_1vkxi/OqU>#uc}/HoɠZժl4 # uk6KsuidԹ pQ"pptzuLnVfsw^y.2?U:tf>3033N Hׯc֭瞻o`dt"q7077s Z{8{,0chnw0cI= X` n rd`aN9 ۩Pwt[W6{m,0+Q}ZR) H9K뾟P^u^WqzM:SC2u1Xt~/] \5!dWlGV9Bb|l,'U+$G&7/jضuseRQ,\uZ8PUj=Y*'jj!\enptṊ Klܸ ?o;`ǏCjh5[cW_}['a߾Gh4066N,/?Gw\F/^b$8}i6mڄelٺAeW/} /\6oތHHpm1yu0A -I[j"@LIm+;˗͸vnL-L | 8U:>ra3AHn¨/ks. M?Ž"@ TV$RJHȞSu)GT 8SwBCj5VEºwV!cČhwYDՃ\kQs/LJ}3DFvnۊB!Dc >Ė-ys+[@O|Жw3-[ضs;v܎{Q mؔ[Js9 aVc,Mtř47\90▬r-=w (FRfuJ/e8F1z߮GZL9PfyI]*Hq%WRD[7qnEyʡι~9`zU%!&1.HB,{ E]i=p8^e5#L>iG$@v,JI9ZEmR[J!Zg ׭30]7g(R0U(4]mUjU{ؼ0A QlQ ;oo l Ը(K:(ݙEYZ6],wؕ 5^ɮS %yd~KOB.^Kl,Q*}N[Ŋփpc6op9byy??;w" #\| jF>n݊'EýޛVЪͦلKV^[LɃ.Mg(l8=1e[I%SN:C[ \a 8y@Ȁ0HXo7`md֖7yrBHxoۍ;&01;wl=;q]عs;8q|pYb]w׿!S\^م^K+2PU \IŲjH=TҍqN(!YHA+#>9W)rS瘱"Mw R*:hW&֥4D0Fh1:@jU d+d2V*Ee e"T*ئ DQ^1+@}MmtK;@.r7J UL-sg' `aiy lob˖8LLMU/XZaF{S¦<҇w07_S6z`F~LHVs$ܤ$&,HC8Wqw 8æ=1y L"'UjUs-j.3͔mIیgitKmҋt3f FZT(j!BJv"D7!KFc82V#$V'HXiwfk͎j'y T2(Ə6(7@HIrE@=qفIdlC \2xOc}7oKn۶l( K0 BK7`ddxG?Ƶ)?CDqpRʞMA\&>&\̹CF`b{݉EU~m )5 s猡 yh9vNJQz]0Q Ҽ0<0Ҿ';/!%8chaqjsp | ]D#{-ld*_{*3i/{]gLɔL/8 ld9>_ZЬgVN9p~%t7@_|l}B NcXY]E?+Xmߨ#bohuBHMW84]LLv:HMHT5)ac z^H'ܗicIRLπ*pP!m ?Q! \ғD&/bWG\z1}3F7oDڄWcGv!d@\߱3*%lE"S'JR V9'5ѿ3~bYuL uLj1|حH<t" =Y)FG e$1ca,y}O%vj|t$ @=Hth;[UpQ $X |uC b}!$0N\|GG"^~~| ЊAhGejS`a rPPVCl s@R!=+'O?'pH?oG SO=E\EqwO=/}}u9Wxꩧq6o l6q]w;=,`# FF055wz G MvBcqq 1ss' uq )16? lݼ##G6n܈p#=F?q}nw/NlG+`uuKO>O}SA$$6߀X\|_4i+I- B^ @Cnԝ2R`i5Kr!$2V!bxHL0 H@k__ ك~6oތ0q!0UgP[D IDATiA*jU:fEN'wjOp.:f9]$LףL̔_ҁ*ޯH ڝXp 4j}X^\c &Dj;<?98ƛo .` #63_'! ?H:m;l߶gɫhGjc7hG!9"3ťEvڍs 4lقv8xӳsã?+.`8wFGFbia -݋? 3mKq202INEA`aLA86k"@܄Ôd 5i^dStZWH`Ԋ I|OqY۷6Ǚ3g000 ?s @^nܘÍ%Z-\|سgs^]]KXm5?8v0$c8{loG8شqvcxwpϽ'> r}_Xg{AE8y|GDX\\@2_s.@F?~ͷ™sgh#" $[ 8s4ȾG'?IKĥ@ӾlF,~)ХB j'{(\z2aI Cb-HL$LN"G2bȫ XddY婁ͥ+oNHUjf˃`ALnH18gcA81mP5%Uj]UŅGO} 7qqQ }q=vwy6nXXX0$Z 7_,l݂͛7|/>񏃱}O™SO~a+Wm޳^[_¥Kqhqg} BJl߾v j#t!h6cguxǏ??nc "ƳCP kB!rg*mGW.i, AOQiR]R`^MtaԖ3M, R)j0ΰƾ}+x0~ ?Z q|عsnق۷ިceb){n7e c׮]p<|Æ 088{.#x0;7}~ذa=pa˯} 7n`hh6mj3Xn333xæu|߸ђtPSaR|y&aLA!2J=T.ɃI(M)'vݯK@3yb7FDmG"N^dzv  cdw%+jɳgD@'5H& @Zreh e1ѕysWQL* GrW|O p1LڕoժV_3uRu)իS" h 3WA g7Zd@X 1r٥(J@n9I{=/YC}`0B(N@ O5j}!d Zpc Hr``a tT,aϓ7ű,t\y}{B&<q H@ye_*dg<] ˜D: , Oܡʔe3*ƴBM)M68Pb7l'K$^"m'iJ1I< qZFyz0B-Hi3oxb!I=|_+JvkAP\o Qj'Lw|8(]J8"^EdX4cd 0+ ɮV;xgZDj@[&c㯎Fm׋ƅx~o!fb=Z)r{V9e{MPݢlu0/D7 гf(JvmRJY`^%"2ݏc~UY\_k@( S/"cg2dQj\qBMLUYW1b,Yd6FZ[t$0ImW9ˡ^ X\XOǗ~8s {ԙ3r >wpםw[O?w`#[zB|sZ w~{ %>Oŗ_Mp!  ?s _֍\ /`( ?}x#ѣG1?9|A\$lPe=sLqܼF/ՠUJ^Av,*)T^AZwNv[PvqV"Rs ι#Ɏ؊PnwnUj&TtQڲh.@ۣ̮ ySg{]LuY/)Uד8Zh)ɸ@,.Wu19ϑN˽qCH* 9g2eir**P ; dk6X[S7;{q-o}2wn㇒+(6Ϙ1^= R 裃ow9?%#7|0(>azz1<29^8;zz)lܸ 'Nɫy\1x?O1ׯ<ΰZ Nc(py <7 _7n|>aq~я~_{8w<6l܀{?rOjb&R"s;ԱY7 ;N+kKS;BHI뙰S; _Z/LJ=Wm\r/4&''qmqILNNʕ8s ܹst_K.\zgϞիWq㤎PWFUZm?-zn0QdlPk+3HZEK]Ks]3N&7{%q@&Kb5EOsCwC$g?||-U듚Iij]w g=ޒ{ǎ; Ħ㘙E;1豣F^uy`czz:7TɎ_|x ['ajj 8: QxWh1:v9ŗ(ܙ#\П={F$ÿ<t:(\]2 fYZm a4TD̀ܽjUڇ+żR7eLPF-"Ţbj 6Wzdx΀J@dS˥x_*&%S.p}$?{Fk~o~0sEЌ"KBΌuJKcHjB)nS1(gt 4 UD'NCP8/cTz&VVW%lGp._GőG}v\rZ ;{V 7nDIAɡÇ}ׯ/a\FEX^\8D,'QvN.>wc < oqet:moKKX^ZƯ}cصk.OMx)ʼSJ&[wgditYhI M"\aA0#_U&/yR3g˶;gͰ1 :o^Z0n=`^j0s5HLkKMȚHѧ {,"/,RG$ @_A*u]]RjEVxE"Bɟz: ]Hϊkl,1Kn^{0oظn~d{.p<ض Wufȁ9Vذq~!l6#os33xϰsv #G*Np>X__gydsxc``z/~acG111F2(rvv GA"s,ߍZ!$6 p;y3BA[9|'dO}20C OJjUn `=#bT"8""s2 u[ XcH{sr{{M;8ΙzxVI;d5pHgRXu@\3qBJ\pCCXn,)czQaFVZhugn}+Jz#'SڲiUKj&ڱTf{= SN;tX4mx^<ԭQLfJC%2f` 9$*i,0[6#,-ZV> }T3:^Ղ!E)Zkaڭ ^ ~>D*M. DP8Nlu|yʒ1X@_ZJMJƥ]pigߓ,ܽ6 *1)40>~~\MHHq/P$ iUS$T5O+.ۮyQ/]CRwTjUڇ-gRہP*V H]HYDNw(R2 -HeL9^a4{gΠj*w)fT)v@ٿ̘pSKy4V[HT>F`>j RhJ@QD! K89`A@-j35Ϗ}W/v~v4h\yq%J0% 8# hjUZ~`uє`6K`rF\P`̪F.5/:gRxVU\7t(cJ]j%A]6ޏG066>Q!J z~W=s1חKdre\Ě8bƙj^Ujݑ7(Ci,`ndgD1+B[HR:uV Lu7 o=W0;̒\>Cx Bbdt6S̟DW1A4S@ZvY_3Yʠ1L˯N=vqߥe^@˪Q;&`RG`)+r t)%.Gz]CV%y Dz3wMWAZ35$_3B Nv !b0[gBKh;`C "H֤aE1cPTиjUpTk۟2h&ڿzTN9x`vy^F㈅֭4iצ,%Waee%!@/8أ8ƥE0122M6!B%n5YT9K=7%GjUڇ[ٜP- Cm`?uJ$[ ˘@L穝*% f^yos;S1{ηUJ"2Ɲ&>Yd'pR]D 1X*)é* soO12sT?dHL Fl۹&g,i0 9mp|w m?6df͒IwkPY웴RF2[StP8k+En_ XMYl׺rҀ`FT> F)sI+c0^=au'֔b1];ˬ<*0!q~It_HWn=q8`mPOBzi͑a16Lw-%FGG0;;;wjK˨ձ"$qw~$֫V}Xp:J̜P1,㚋p`u/*WLm;#vbϰ͕YBBa'399d2O}_̺WȪ ixt&\+b*SSV9V u PUfk* sd ])7.aur!SF}s˼jp3I_d;L?Uf?:K.4uUMwfP|ƕ3kwԳ_SMk]q|;z 7DZ*.8XRD7O^BseMN+69b] )USVC+fئ/ﵽ/4hSp0-S,HXcAcy^qժV tsi,  BئB0`MRLwS.X BI t=Qfh 39w `Pm~ mCǔ5+Oe򠎋h DPT@'3Ҹ.gS=-]7[pt;FEb3k]q}F}~i0Kcoij#ޢN nݻG9GϲV3;ސlԺAh?k[TVr7e=EP]ES/H<3@ʆ۝־Ĉ",S Ɗr񓘞9L2"U.l04X-S-fӫHS(o'3MfUE2҈.$RZE  N,JM9l:ieiޠcҺYi՛P$Ly,3_Zf!0;1Ji}oޗ2w|.r˵z<}j+L*̈́RL/c13 *(*Aw Po RqZoJ)ԌkuSP?n%~_` 0;b:UEpUZtd[}kegY~kڝ>ݤ @A ,/H4c2$0*8BIAM13d!X8R#@.1kw˹Z;?Ow'$SFk|kNCHm?!V*0Jg<ʵi+hb^`R̥bp!o sx~m 1#UXYa%8q:񡅣VR 90fvZW(V95Y{0]4v,MomνԾI ǵMBmɧ*9I]C=j*Zkۅqs+ܽq ]}ZY?=$)>]'&@pZBJz졀e"R"٤OCe@} Ifb 5 /BFX H,~OxK.Av;°"[$tsۉ:j&@>9)#9,=yz<)fs1"bE5 ϾB1iv{p]@MѯDXuC}Րd7\q'nAP v npޒNԏ в3M nd{dD3x t#jfYn \8)#4@ j~"(!8( bv˻ULXۯ 6',qyl,$ (,xQ{fVWCbڵPOtTgF ,2J-:(5I%&(rM4Bi |ۄy?3G[ ;"0<_=ME@u:;Ifh!d5m ^py(߯4*kW3ɵ ~֦Bimlu}.w1rqW ®xmC*l[0՞zӈ*@lS+U_J@P"U*ov ֠)o lhFr jfeމW׆6pm[ɬ=Mvrȍ{j_DuLklQ `MN]+W@&TRV) WL{O{=ovdriAbW07ϝj.hOC׌Fc*n_Lշ Drmk44m SY3>0u4Mt9gލq^x!|_['W"U*C0*{XsM, D5(]m*CkXߘz#6IWIӖ-1^b#Z-yHmoxOB۠]CQ5&يg0yĴXNQV3SE43jmaOfhСsԦdܬx1PqMl';=w_í)3I} z ଙ~Mgm}N~ycah8GA dyKKKغuYv 0wN^y6FI1ۛz.r55汰^,o ON ~ 7~X]]ſ_Wf|w;WVu]\x+-ފ_ӟ{p=ѣ_v8vD>|3{{Ͻu淾߻B <{P`~8-qpv $9u'U], kʆ,r$6_=hƈ[$)Vf*Z:O{9Re=Eo%MbksLײV8"L!z!2nlc[̳lF|\w:t;^Ӯ&7m Dh[hBm]2_&BOh }>2kO4x Om啷5S`eO\<+;w +ZC7v2*JHӤ=!ťYu)*эo k>^qe⪫?c_|% p7e\px>Pc]wҗwsWU˿+7,٩>s|?pI[Jl <p@P((P+M~J TzPťȾVFɷm(:T׭kֱ|g~ҪDZ8ys]c!1Ba 1ݷMOdόrU W/mjg}{VLT"XnE }MTZcXHw\: oyw-땳o{#p;iYlf2`~ؾW2vf^%A e0;WznC!2ffk,˱^;Wʙax!"BI~w:/;7K  kعsW]~9{tv\v6666.?n\u־ Xp7)2IeiU*?xO;m"*T.Z±6W(smɱ4Kfђ=ClfXL)7BTZyvAD}ݹȍ. ROLe:*Uk^8U.l4.7Xg R^3UؚHюF+ca;=U$]XgPF+Y:.H~!6R^\;í -Q;\Y=ڶ#ع=Q~9p6K *0㡩f~^gZkiqa4Lll`c0Dn++;:wO*-[ƫ7-䢋02|փ껙\y5z>aSw~>(>|Bh8p`&/ŏ5M7 <`+8\LeDmEp ~#z4fKj#Ա3-*61nv寭(41vTCrF0;ʌ8uǵ'ƽp]=bQV~ʁW_;}ib&mqE+h`{;%4q)޹ l%nGȶM?1m?~1DD&˞^g1 vC`SU `<~< H[)T4rݘnvS$IcǎIi^I3H!MUC ?Wc~n; ^~xe| c("zwƷmkۇs߻43?S qWdw3`0cǰse&py¬ heQBqNԕX5i$߮bff6Ǟ t)E٭FTrNjI0Ѐhzy+eUФa6F>.Zj`¹Extrab7XjohNpAu+u ;G5ky㈭c*Hp;Օh4BH*7M5H[iv҅}j4R1rޗ7tE!uu-bo.2~zr=r8VWWK/FA51!gV@<>!kx,vlPŁK;]޽IR,{Fc2ZnUd€i?şxXپ"MSK[ߟ|I|.~;`aa7l?#wO4'2 /ބ_y,ew3E\W_Z={XZ x&#ԉ(Dxvۦ \j alVPrlk-׀Є~V*mhk`pI{mueHڮ9oˎ ĿynR_Zջ5l#jތa~aIDATXg:"^Pns(vqJMk 5$ l ۝MۚO!';4َ'&7☫SN㏥b$AGlnxDUvΔ3/e)BXZZk ݗ23שvS$O1;3$M[r<"և`x#Gŗ\fH#|~7ߏGǛ_.K\~9n?{ _Εpm<"n<OIɟn0 - Uj XTKٖ;5Wبh?{* h$?`ۭ]kR39psXnrm߈#D9"JϒxT)$  Gy6kjIsQ{ e u-Ѧr}YkѮ_rwxe?/o gwx \Yҗ#_ 0?/N+KKxUWW]Uw?>鉟9{~{M\uxuzb%xv `.вSsB,w5y1HXfpK;(EA̜QvfնEzFlᔇja7CY>Q1IEd'~Kq,؟0& Kn;˥i[S,[7L59C@UjQk EezziyyĊB6l5V3}+k=`k)v6CvGJ8*oUijM]3&fL m0#˲BD=U{={oXfHԛ|.(p$Qn0MNpP0.lu飙V,Mi6|?2 !)/U-Tֺ̀2uAR!.qj'~lKަT}+;~Uy wC?m xU(Cn ٞUj~Џ^mo6ho{lNBMPs榁 MR#*Esmy'—iM0D;mŎ#{r7k*Ta4,dr E,ſ?f򇉦#rGP WWzȲ̚= b %l0's_C`fn0̀m+*F3 Kٍ<.01.o58p42+,,a]F0F#+N _xы_-еbQYh4D39Uq<` l0).ȟ 5Цg;1r]L '[c#>~6*S~K}nӐѱ 0C]]8@LY Y}nuż9Q*}(N\7}"IOՙ$>)gM\8b*6J!Ϲ&f^u,HbΣurJs 6ʹxTXe| 6|*]Ý8ikVΆ3i0 LgX]SxRѾNT]m#V )ѐeyb6sWc"Cą;W sTRS`> ,}Ç1c+ ֱ՘ݾ3@w#fi Jf3.vY߸i@ L( ΈDر5X\\Z٢0޽{tE'GG׾ eֺ$ 9 i1~Yn%EvrqDňrm(Էƃi]2I%&ų4}Vx\t] B$ʙ4k\UW*\bw{i EC7x<`M,9P7Q1&)='esqspМ#IY3 Y iQRJyy?12yJT(e^6(IC9,Ckp8Pf͍[.K7A'XJu>T "*Z%J"V8&ŵX.f]޿lǧZן br]Ju"d٨Np}BA]$QN*< @!ˬu-eד@&D[5Zs2rUc4@SK WcT4E<yCvZ?*v23E<83߹㄰y; FF:f OcU^K|}󼸘;l99ʁY1+N_xn&xMr04A\72z[{1ҎmY^DN@^vj#^ .+DݰxBwA̲t=sBmB @M~IAU~Us>sXj (?PuEKKi=Cמavkm9{@ !hTlHeBU1j<| NYkC٘yr_{Ӭ T@ @ @F]mC(&I]5K"I{INF&6斦D @ " h44BҴSTVըZ1DE% @IY&Z@ @p pͲ @~]ݳLj3#s]~ȹ @ 6P vHQemY^ƑǠih3nŶ'!olt%AWJ x#q% ,@ ؃E;p!t:,.-'پ,լ(R(٬:(X, *^׺ bWr@ p8*>$'c2666팭$No9Yc8 Ip%/f*m\4.;DoE@ DlwtgϞ2"hnREM[ʊ3 k NĆ Up8e$<]%mpŅ RK 2uN$hMb@ [p)e X>loA" % sys.argv[0]) print("") print("Options:") print(" -s : Generate intermediate images (may generate a lot of" " images !)") sys.exit(1) for arg in args[:]: if arg == "-s": steps = True args.remove(arg) output_file = args[0] print("Output file: %s" % output_file) print("Looking for scanners ...") devices = pyinsane2.get_devices() if (len(devices) <= 0): print("No scanner detected !") sys.exit(1) print("Devices detected:") print("- " + "\n- ".join([str(d) for d in devices])) print("") device = devices[0] print("Will use: %s" % str(device)) print("") # For the possible resolutions, look at # device.options['resolution'].constraint # It will either be: # - None: unknown # - a tuple: (min resolution, max resolution) # - a list: [75, 150, 300, 600, 1200, ...] pyinsane2.set_scanner_opt(device, 'source', ['Auto', 'FlatBed']) pyinsane2.set_scanner_opt(device, 'resolution', [300]) try: pyinsane2.maximize_scan_area(device) except Exception as exc: print("Failed to maximize scan area: {}".format(exc)) # Beware: Some scanner have "Lineart" or "Gray" as default mode pyinsane2.set_scanner_opt(device, 'mode', ['Color']) print("") print("Scanning ... ") scan_session = device.scan(multiple=False) if steps and scan_session.scan.expected_size[1] < 0: print("Warning: requested step by step scan images, but" " scanner didn't report the expected number of lines" " in the final image --> can't do") print("Step by step scan images won't be recorded") steps = False if steps: last_line = 0 expected_size = scan_session.scan.expected_size img = Image.new("RGB", expected_size, "#ff00ff") sp = output_file.split(".") steps_filename = (".".join(sp[:-1]), sp[-1]) try: i = -1 while True: i += 1 i %= len(PROGRESSION_INDICATOR) sys.stdout.write("\b%s" % PROGRESSION_INDICATOR[i]) sys.stdout.flush() scan_session.scan.read() if steps: next_line = scan_session.scan.available_lines[1] if (next_line > last_line + 100): subimg = scan_session.scan.get_image(last_line, next_line) img.paste(subimg, (0, last_line)) img.save("%s-%05d.%s" % (steps_filename[0], last_line, steps_filename[1]), "JPEG") last_line = next_line except EOFError: pass print("\b ") print("Writing output file ...") img = scan_session.images[0] img.save(output_file, "JPEG") print("Done") if __name__ == "__main__": pyinsane2.init() try: main() finally: pyinsane2.exit() pyinsane-2.0.13/examples/scan_adf.py000077500000000000000000000030761332615651100173650ustar00rootroot00000000000000#!/usr/bin/env python3 import os import sys import pyinsane2 def main(args): dstdir = args[0] devices = pyinsane2.get_devices() assert(len(devices) > 0) device = devices[0] print("Will use the scanner [%s](%s)" % (str(device), device.name)) pyinsane2.set_scanner_opt(device, "source", ["ADF", "Feeder"]) # Beware: Some scanner have "Lineart" or "Gray" as default mode pyinsane2.set_scanner_opt(device, "mode", ["Color"]) pyinsane2.set_scanner_opt(device, "resolution", [300]) pyinsane2.maximize_scan_area(device) # Note: If there is no page in the feeder, the behavior of device.scan() # is not guaranteed : It may raise StopIteration() immediately # or it may raise it when scan.read() is called try: scan_session = device.scan(multiple=True) print("Scanning ...") while True: try: scan_session.scan.read() except EOFError: print("Got page %d" % (len(scan_session.images))) img = scan_session.images[-1] imgpath = os.path.join(dstdir, "%d.jpg" % (len(scan_session.images))) img.save(imgpath) except StopIteration: print("Got %d pages" % len(scan_session.images)) if __name__ == "__main__": args = sys.argv[1:] if len(args) <= 0 or args[0][0] == '-': print("Usage:") print(" %s " % sys.argv[0]) sys.exit(1) pyinsane2.init() try: main(args) finally: pyinsane2.exit() pyinsane-2.0.13/pyinsane2/000077500000000000000000000000001332615651100153365ustar00rootroot00000000000000pyinsane-2.0.13/pyinsane2/__init__.py000066400000000000000000000131061332615651100174500ustar00rootroot00000000000000import logging import os import sys from .util import PyinsaneException SINGLE_THREAD = bool(int(os.getenv("PYINSANE_SINGLE_THREAD", 0))) if os.name == "nt": from .wia.abstract import * # noqa elif sys.platform == "darwin" or SINGLE_THREAD: # The dedicated process appear to crash on MacOSX. Don't know why. from .sane.abstract import * # noqa else: from .sane.abstract_proc import * # noqa logger = logging.getLogger(__name__) __all__ = [ 'init', 'exit', 'Scanner', 'ScannerOption', 'PyinsaneException', 'get_devices', 'set_scanner_opt', 'maximize_scan_area', '__version__', ] __version__ = "2.0.10" def normalize_value(value): if isinstance(value, str): return value.lower() return value def set_scanner_opt(scanner, opt, values): """ Utility function to set most common values easily. Examples: set_scanner_opt(scanner, "source", ["flatbed"]) set_scanner_opt(scanner, "source", ["ADF", "feeder"]) """ assert(values is not None and values != []) if opt not in scanner.options: # check it's not just a casing problem for key in scanner.options.keys(): if opt.lower() == key.lower(): opt = key break # otherwise just keep going, it will raise a KeyError anyway if not scanner.options[opt].capabilities.is_active(): logger.error( "Unable to set scanner option [{}]:" " Option is not active".format(opt) ) # this may not be a problem. For instance, 'source' is not active # on all scanners, and there is no point in raising an exception # when trying to set it to 'FlatBed' or 'Auto' return last_exc = None for value in values: # See if we can normalize it first if isinstance(scanner.options[opt].constraint, list): found = False for possible in scanner.options[opt].constraint: if normalize_value(value) == normalize_value(possible): value = possible found = True break if not found: # no direct match. See if we have an indirect one # for instance, 'feeder' in 'Automatic Document Feeder' for possible in scanner.options[opt].constraint: if (isinstance(possible, str) and normalize_value(value) in normalize_value(possible)): logger.info( "Value for [{}] changed from [{}] to [{}]".format( opt, value, possible ) ) value = possible found = True break # beware: don't select a source that is not in the constraint, # with some drivers (Brother DCP-8025D for instance), # it may segfault. if not found: last_exc = PyinsaneException( "Invalid value [{}] for option [{}]." " Valid values are [{}]".format( value, opt, scanner.options[opt].constraint ) ) continue if len(scanner.options[opt].constraint) <= 1: # XXX(Jflesch): Epson DS-310 # Do not try to set a value when there is only one value # possible. Sane will return SANE_STATUS_INVAL. return # Then try to set it try: scanner.options[opt].value = value logger.info("[{}] set to [{}]".format(opt, value)) return except (KeyError, PyinsaneException) as exc: logger.info("Failed to set [{}] to [{}]: [{}]".format( opt, str(value), str(exc)) ) last_exc = exc logger.warning("Failed to set [{}] to [{}]: [{}]".format( opt, values, last_exc) ) raise last_exc def set_scan_area_pos(options, opt_name, select_value_func, missing_options): if opt_name not in options: if missing_options: missing_options.append(opt_name) else: if not options[opt_name].capabilities.is_active(): logger.warning( "Unable to set scanner option [{}]:" " Option is not active".format(opt_name) ) return constraint = options[opt_name].constraint if isinstance(constraint, tuple): value = select_value_func(constraint[0], constraint[1]) elif isinstance(constraint, list): value = select_value_func(constraint) options[opt_name].value = value def maximize_scan_area(scanner): """ Utility function to make sure the scanner scan the biggest area possible. Must be called *after* setting the resolution. """ opts = scanner.options missing_opts = [] set_scan_area_pos(opts, "tl-x", min, missing_opts) set_scan_area_pos(opts, "tl-y", min, missing_opts) set_scan_area_pos(opts, "br-x", max, missing_opts) set_scan_area_pos(opts, "br-y", max, missing_opts) set_scan_area_pos(opts, "page-height", max, None) set_scan_area_pos(opts, "page-width", max, None) if missing_opts: logger.warning( "Failed to maximize the scan area. Missing options: {}".format( ", ".join(missing_opts) ) ) def get_version(): from . import _version return _version.version __version__ = get_version() pyinsane-2.0.13/pyinsane2/sane/000077500000000000000000000000001332615651100162645ustar00rootroot00000000000000pyinsane-2.0.13/pyinsane2/sane/__init__.py000066400000000000000000000000061332615651100203710ustar00rootroot00000000000000# nop pyinsane-2.0.13/pyinsane2/sane/abstract.py000066400000000000000000000353641332615651100204540ustar00rootroot00000000000000import sys from PIL import Image from . import rawapi from .. import util # import basic elements directly, so the caller # doesn't have to import rawapi if they need them. from .rawapi import SaneCapabilities from .rawapi import SaneConstraint from .rawapi import SaneConstraintType from .rawapi import SaneException from .rawapi import SaneStatus from .rawapi import SaneUnit from .rawapi import SaneValueType from .rawapi import sane_init, sane_exit __all__ = [ 'SaneCapabilities', 'SaneConstraint', 'SaneConstraintType', 'SaneException', 'SaneStatus', 'SaneValueType', 'SaneUnit', 'init', 'exit', 'Scanner', 'ScannerOption', 'get_devices', ] # We use huge buffers to spend the maximum amount of time in non-Python code SANE_READ_BUFSIZE = 512 * 1024 # XXX(Jflesch): Never open more than one handle at the same time. # Some Sane backends don't support it. For instance, I have 2 HP scanners, and # if I try to access both from the same process, I get I/O errors. sane_dev_handle = ("", None) def init(): sane_init() def exit(): sane_exit() class ScannerOption(object): idx = 0 name = "" title = "" desc = "" val_type = SaneValueType(SaneValueType.INT) unit = SaneUnit(SaneUnit.NONE) size = 4 capabilities = SaneCapabilities(SaneCapabilities.NONE) constraint_type = SaneConstraintType(SaneConstraintType.NONE) constraint = None def __init__(self, scanner, idx): self.__scanner = scanner self.idx = idx @staticmethod def build_from_rawapi(scanner, opt_idx, opt_raw): opt = ScannerOption(scanner, opt_idx) opt.name = opt_raw.name if opt.name is not None and hasattr(opt.name, "decode"): opt.name = opt.name.decode('utf-8') opt.title = opt_raw.title if opt.title is not None and hasattr(opt.title, "decode"): opt.title = opt.title.decode('utf-8') opt.desc = opt_raw.desc if opt.desc is not None and hasattr(opt.desc, "decode"): opt.desc = opt.desc.decode('utf-8') # TODO : multi-line opt.val_type = SaneValueType(opt_raw.type) opt.unit = SaneUnit(opt_raw.unit) opt.size = opt_raw.size opt.capabilities = SaneCapabilities(opt_raw.cap) opt.constraint_type = SaneConstraintType(opt_raw.constraint_type) opt.constraint = opt.constraint_type.get_pyobj_constraint( opt_raw.constraint) return opt def _get_value(self): self.__scanner._open() val = rawapi.sane_get_option_value(sane_dev_handle[1], self.idx) if not self.capabilities.is_active(): # XXX(Jflesch): if the option is not active, some backends still # return a value, some don't and return an error instead. # To avoid mistakes in user programs, we make the behavior here # consistent and always raise an exception. raise SaneException("Option '%s' is not active" % self.name) if hasattr(val, 'decode'): val = val.decode("utf-8") return val def _set_value(self, new_value): self.__scanner._open() rawapi.sane_set_option_value(sane_dev_handle[1], self.idx, new_value) value = property(_get_value, _set_value) class ImgUtil(object): COLOR_BYTES = { 1: { # we expanded the bits to bytes on-the-fly "L": 1, "RGB": 3, }, 8: { "L": 1, "RGB": 3, }, 16: { "L": 2, "RGB": 6 } } @staticmethod def unpack_1_to_8(whole_raw_packed, pixels_per_line, bytes_per_line): # Each color is on one bit. We unpack immediately so each color # is on one byte. # We must take care of one thing : the last byte of each line # contains unused bits. They must be dropped whole_raw_unpacked = b"" if sys.version_info < (3, ): positive_bit = chr(0x00) negative_bit = chr(0xFF) else: positive_bit = bytes([0x00]) negative_bit = bytes([0xFF]) for chunk in range(0, len(whole_raw_packed), bytes_per_line): raw_packed = whole_raw_packed[:bytes_per_line] whole_raw_packed = whole_raw_packed[bytes_per_line:] raw_unpacked = b"" for byte in raw_packed: if type(byte) == str: byte = ord(byte) for bit in range(7, -1, -1): if ((byte & (1 << bit)) > 0): raw_unpacked += positive_bit else: raw_unpacked += negative_bit assert(len(raw_packed) * 8 == len(raw_unpacked)) raw_unpacked = raw_unpacked[:pixels_per_line] whole_raw_unpacked += raw_unpacked return whole_raw_unpacked @staticmethod def raw_to_img(raw, parameters): mode = rawapi.SaneFrame(parameters.format).get_pil_format() # color_bytes = ImgUtil.COLOR_BYTES[parameters.depth][mode] width = parameters.pixels_per_line height = (len(raw) / parameters.bytes_per_line) if parameters.depth == 1: raw = ImgUtil.unpack_1_to_8(raw, width, parameters.bytes_per_line) return Image.frombuffer(mode, (int(width), int(height)), raw, "raw", mode, 0, 1) class Scan(object): def __init__(self, scanner): self.scanner = scanner self.__session = None self.__raw_lines = [] self.__img_finished = False def _set_session(self, session): self.__session = session def _init(self): self.scanner._open() rawapi.sane_start(sane_dev_handle[1]) try: self.parameters = \ rawapi.sane_get_parameters(sane_dev_handle[1]) except Exception: rawapi.sane_cancel(sane_dev_handle[1]) raise def read(self): if self.__img_finished: # start a new one self.__raw_lines = [] self.__img_finished = False try: read = rawapi.sane_read(sane_dev_handle[1], SANE_READ_BUFSIZE) except EOFError: line_size = self.parameters.bytes_per_line for line in self.__raw_lines: if len(line) != line_size: print(("Pyinsane: Warning: Unexpected line size: %d" " instead of %d") % (len(line), line_size)) raw = (b'').join(self.__raw_lines) # don't do purge the lines here. wait for the next call to read() # because, in the meantime, the caller might use get_image() self.__img_finished = True self.__session.images.append(ImgUtil.raw_to_img( raw, self.parameters)) raise # cut what we just read, line by line line_size = self.parameters.bytes_per_line if (len(self.__raw_lines) > 0): cut = line_size - len(self.__raw_lines[-1]) self.__raw_lines[-1] += read[:cut] read = read[cut:] range_func = range if sys.version_info.major < 3: range_func = xrange # noqa (non-valid in Python 3) for _ in range_func(0, len(read), line_size): self.__raw_lines.append(read[:line_size]) read = read[line_size:] if len(read) > 0: self.__raw_lines.append(read) def _get_available_lines(self): line_size = self.parameters.bytes_per_line r = len(self.__raw_lines) if (r > 0 and len(self.__raw_lines[-1]) < line_size): r -= 1 return (0, r) available_lines = property(_get_available_lines) def _get_expected_size(self): """ Returns the expected size of the image (tuple: (w, h)). Note that (afaik) Sane makes it mandatory for the driver to indicates the length of the lines. However, it is not mandatory for the driver to indicates the expected number of lines (for instance, hand-held scanners can't know it before the end of the scan). In that case, the expected height of the image will -1 here. """ width = self.parameters.pixels_per_line height = self.parameters.lines return (width, height) expected_size = property(_get_expected_size) def get_image(self, start_line=0, end_line=-1): if end_line < 0: end_line = len(self.__raw_lines) assert(end_line >= start_line) lines = self.__raw_lines[start_line:end_line] lines = b"".join(lines) return ImgUtil.raw_to_img(lines, self.parameters) def _cancel(self): rawapi.sane_cancel(sane_dev_handle[1]) class SingleScan(Scan): def __init__(self, scanner): Scan.__init__(self, scanner) self.is_scanning = True self._init() def read(self): if not self.is_scanning: raise StopIteration() try: Scan.read(self) except (EOFError, StopIteration): self._cancel() self.is_scanning = False raise def cancel(self): if self.is_scanning: self._cancel() self.is_scanning = False class MultipleScan(Scan): def __init__(self, scanner): Scan.__init__(self, scanner) self.is_scanning = False self.is_finished = False self.must_request_next_frame = False self._init() def read(self): if self.is_finished: raise StopIteration() if not self.is_scanning: self.is_scanning = True self.must_request_next_frame = False if self.must_request_next_frame: try: rawapi.sane_start(sane_dev_handle[1]) except StopIteration: self._cancel() self.is_finished = True self.is_scanning = False raise self.must_request_next_frame = False try: Scan.read(self) except EOFError: self.must_request_next_frame = True raise except StopIteration: self._cancel() self.is_finished = True self.is_scanning = False # signal the last page first raise EOFError() def cancel(self): if self.is_scanning: self._cancel() self.is_finished = True self.is_scanning = False class ScanSession(object): def __init__(self, scan): self.images = [] self.scan = scan self.scan._set_session(self) def read(self): """ Deprecated. Use scan_session.scan.read() """ return self.scan.read() def get_nb_img(self): """ Deprecated. Use len(scan_session.images) directly """ return len(self.images) def get_img(self, idx=0): """ Deprecated. Use scan_session.images[idx] directly """ return self.images[idx] class Scanner(object): def __init__(self, name, vendor="Unknown", model="Unknown", dev_type="Unknown"): if hasattr(name, "decode"): name = name.decode('utf-8') if hasattr(vendor, "decode"): vendor = vendor.decode('utf-8') if hasattr(model, "decode"): model = model.decode('utf-8') if hasattr(dev_type, "decode"): dev_type = dev_type.decode('utf-8') self.name = name self.vendor = vendor self.model = model self.dev_type = dev_type self.__options = None # { "name" : ScannerOption } @staticmethod def build_from_rawapi(sane_device): return Scanner(sane_device.name, sane_device.vendor, sane_device.model, sane_device.type) def _open(self): global sane_dev_handle (devid, handle) = sane_dev_handle if devid == self.name: return self._force_close() sane_init() handle = rawapi.sane_open(self.name) sane_dev_handle = (self.name, handle) def _force_close(self): global sane_dev_handle (devid, handle) = sane_dev_handle if handle is None: return rawapi.sane_close(handle) sane_exit() sane_dev_handle = ("", None) def __load_options(self): if self.__options is not None: return self._open() nb_options = rawapi.sane_get_option_value(sane_dev_handle[1], 0) self.__options = {} for opt_idx in range(1, nb_options): opt_desc = rawapi.sane_get_option_descriptor( sane_dev_handle[1], opt_idx) if not SaneValueType(opt_desc.type).can_getset_opt(): continue opt = ScannerOption.build_from_rawapi(self, opt_idx, opt_desc) self.__options[opt.name] = opt # WORKAROUND(Jflesch): # Lexmark MFP CX510de: option 'resolution' has been mistakenly named # 'scan-resolution' if ('scan-resolution' in self.__options and 'resolution' not in self.__options): self.__options['resolution'] = util.AliasOption( 'resolution', ['scan-resolution'], self.__options ) # WORKAROUND(Jflesch): # Samsung M288x: option 'source' is actually called 'doc-source' if ('doc-source' in self.__options and 'source' not in self.__options): self.__options['source'] = util.AliasOption( 'source', ['doc-source'], self.__options ) def _get_options(self): self.__load_options() return self.__options options = property(_get_options) def scan(self, multiple=False): if (not ('source' in self.options and self.options['source'].capabilities.is_active())): value = "" else: value = self.options['source'].value if hasattr(value, 'decode'): value = value.decode('utf-8') if (not multiple or ("adf" not in value.lower() and "feeder" not in value.lower())): # XXX(Jflesch): We cannot use MultipleScan() with something # else than an ADF. If we try, we will never get # SANE_STATUS_NO_DOCS from sane_start()/sane_read() and we will # loop forever scan = SingleScan(self) else: scan = MultipleScan(self) return ScanSession(scan) def __str__(self): return ("Scanner '%s' (%s, %s, %s)" % (self.name, self.vendor, self.model, self.dev_type)) def get_devices(local_only=False): sane_init() try: return [Scanner.build_from_rawapi(device) for device in rawapi.sane_get_devices(local_only)] finally: sane_exit() pyinsane-2.0.13/pyinsane2/sane/abstract_proc.py000066400000000000000000000161761332615651100214770ustar00rootroot00000000000000import logging import os import pickle import shutil import struct import sys import tempfile import PIL.Image # import basic elements directly, so the caller # doesn't have to import rawapi if they need them. from . import abstract from .rawapi import SaneCapabilities from .rawapi import SaneConstraint from .rawapi import SaneConstraintType from .rawapi import SaneException from .rawapi import SaneStatus from .rawapi import SaneUnit from .rawapi import SaneValueType __all__ = [ 'SaneCapabilities', 'SaneConstraint', 'SaneConstraintType', 'SaneException', 'SaneStatus', 'SaneValueType', 'SaneUnit', 'init', 'exit', 'Scanner', 'ScannerOption', 'get_devices', ] logger = logging.getLogger(__name__) length_size = None fifo_s2c = None fifo_c2s = None pipe_path_c2s = None pipe_path_s2c = None pipe_dirpath = None def remote_do(command, *args, **kwargs): global length_size global fifo_s2c global fifo_c2s global pipe_path_c2s global pipe_path_s2c cmd = { 'command': command, 'args': args, 'kwargs': kwargs, } cmd = pickle.dumps(cmd) length = struct.pack("i", len(cmd)) os.write(fifo_c2s, length) os.write(fifo_c2s, cmd) length = os.read(fifo_s2c, length_size) length = struct.unpack("i", length)[0] result = os.read(fifo_s2c, length) assert(len(result) == length) result = pickle.loads(result) if 'exception' in result: exc_class = eval(result['exception']) raise exc_class(*result['exception_args']) if command == 'exit': os.close(fifo_c2s) os.close(fifo_s2c) os.unlink(pipe_path_c2s) os.unlink(pipe_path_s2c) shutil.rmtree(pipe_dirpath) return return result['out'] def init(): global length_size global fifo_s2c global fifo_c2s global pipe_path_c2s global pipe_path_s2c global pipe_dirpath start_daemon = os.getenv('PYINSANE_DAEMON', '1') start_daemon = True if int(start_daemon) > 0 else False if not start_daemon: return logger.info("Starting Pyinsane subprocess") pipe_dirpath = tempfile.mkdtemp(prefix="pyinsane_") pipe_path_c2s = os.path.join(pipe_dirpath, "pipe_c2s") os.mkfifo(pipe_path_c2s) pipe_path_s2c = os.path.join(pipe_dirpath, "pipe_s2c") os.mkfifo(pipe_path_s2c) logger.info("Pyinsane pipes: {} | {}".format(pipe_path_c2s, pipe_path_s2c)) if os.fork() == 0: # prevent the daemon from starting itself (due to the way # imports behave) os.putenv('PYINSANE_DAEMON', '0') os.execlp( sys.executable, sys.executable, "-m", "pyinsane2.sane.daemon", pipe_dirpath, pipe_path_c2s, pipe_path_s2c ) length_size = len(struct.pack("i", 0)) fifo_c2s = os.open(pipe_path_c2s, os.O_WRONLY) fifo_s2c = os.open(pipe_path_s2c, os.O_RDONLY) logger.info("Connected to Pyinsane subprocess") def exit(): remote_do('exit') class ScannerOption(object): _abstract_opt = None _scanner_name = None idx = 0 name = "" title = "" desc = "" val_type = SaneValueType(SaneValueType.INT) unit = SaneUnit(SaneUnit.NONE) size = 4 capabilities = SaneCapabilities(SaneCapabilities.NONE) constraint_type = SaneConstraintType(SaneConstraintType.NONE) constraint = None def __init__(self, scanner, idx): self.idx = idx self._abstract_opt = abstract.ScannerOption(scanner._abstract_dev, idx) @staticmethod def build_from_abstract(scanner, abstract_opt): opt = ScannerOption(scanner, abstract_opt.idx) opt._scanner_name = scanner.name opt._abstract_opt = abstract_opt opt.name = abstract_opt.name opt.title = abstract_opt.title opt.desc = abstract_opt.desc opt.val_type = abstract_opt.val_type opt.unit = abstract_opt.unit opt.size = abstract_opt.size opt.capabilities = abstract_opt.capabilities opt.constraint_type = abstract_opt.constraint_type opt.constraint = abstract_opt.constraint return opt def _get_value(self): return remote_do('get_option_value', self._scanner_name, self.name) def _set_value(self, new_value): remote_do('set_option_value', self._scanner_name, self.name, new_value) value = property(_get_value, _set_value) class Scan(object): def __init__(self, scanner_name): self._scanner_name = scanner_name def read(self): return remote_do('scan_read', self._scanner_name) def _get_available_lines(self): return remote_do('scan_get_available_lines', self._scanner_name) available_lines = property(_get_available_lines) def _get_expected_size(self): return remote_do('scan_get_expected_size', self._scanner_name) expected_size = property(_get_expected_size) def get_image(self, start_line=0, end_line=-1): img = remote_do( 'scan_get_image', self._scanner_name, start_line, end_line ) return PIL.Image.frombytes(*img) def cancel(self): return remote_do('scan_cancel', self._scanner_name) class ScanSession(object): def __init__(self, scanner, multiple=False): self._scanner = scanner.name self._remote_session = remote_do('scan', scanner.name, multiple) self.scan = Scan(scanner.name) def __get_imgs(self): imgs = remote_do('get_images', self._scanner) imgs = [PIL.Image.frombytes(*img) for img in imgs] return imgs images = property(__get_imgs) def read(self): """ Deprecated """ self.scan.read() def get_nb_img(self): """ Deprecated """ return len(self.images) def get_img(self, idx=0): """ Deprecated """ return self.images[idx] class Scanner(object): def __init__(self, name=None, vendor="Unknown", model="Unknown", dev_type="Unknown", abstract_dev=None): if abstract_dev is None: abstract_dev = abstract.Scanner(name) else: vendor = abstract_dev.vendor model = abstract_dev.model dev_type = abstract_dev.dev_type self._abstract_dev = abstract_dev self.name = name self.nice_name = name # for WIA compatibility self.vendor = vendor self.model = model self.dev_type = dev_type @staticmethod def build_from_abstract(abstract_dev): return Scanner(abstract_dev.name, abstract_dev=abstract_dev) def _get_options(self): options = remote_do("get_options", self.name) return { x.name: ScannerOption.build_from_abstract(self, x) for x in options.values() } options = property(_get_options) def scan(self, multiple=False): return ScanSession(self, multiple) def __str__(self): return ("'%s' (%s, %s, %s)" % (self.name, self.vendor, self.model, self.dev_type)) def get_devices(local_only=False): return [ Scanner.build_from_abstract(x) for x in remote_do('get_devices', local_only) ] pyinsane-2.0.13/pyinsane2/sane/daemon.py000066400000000000000000000104521332615651100201030ustar00rootroot00000000000000import logging import os import pickle import struct import sys import pyinsane2.sane.abstract as pyinsane logger = logging.getLogger("Pyinsane_daemon") device_cache = {} scan_sessions = {} def get_devices(local_only): global device_cache devices = pyinsane.get_devices(local_only) device_cache = {} for device in devices: device_cache[device.name] = device return devices def get_device(scanner_name): global device_cache if scanner_name in device_cache: return device_cache[scanner_name] scanner = pyinsane.Scanner(scanner_name) device_cache[scanner_name] = scanner return scanner def get_options(scanner_name): return get_device(scanner_name).options def get_option_value(scanner_name, option_name): return get_device(scanner_name).options[option_name].value def set_option_value(scanner_name, option_name, option_value): get_device(scanner_name).options[option_name].value = option_value def make_scan_session(scanner_name, multiple=False): global scan_sessions scan_session = get_device(scanner_name).scan(multiple) scan_sessions[scanner_name] = scan_session return scan_session def get_images(scanner_name): global scan_sessions imgs = scan_sessions[scanner_name].images imgs = [(img.mode, img.size, img.tobytes()) for img in imgs] return imgs def scan_read(scanner_name): global scan_sessions return scan_sessions[scanner_name].scan.read() def get_available_lines(scanner_name): global scan_sessions return scan_sessions[scanner_name].scan.available_lines def get_expected_size(scanner_name): global scan_sessions return scan_sessions[scanner_name].scan.expected_size def get_image(scanner_name, start_line, end_line): global scan_sessions img = scan_sessions[scanner_name].scan.get_image(start_line, end_line) return (img.mode, img.size, img.tobytes()) def cancel(scanner_name): global scan_sessions return scan_sessions[scanner_name].scan.cancel() def exit(): pass COMMANDS = { "get_devices": get_devices, "get_options": get_options, "get_option_value": get_option_value, "set_option_value": set_option_value, "scan": make_scan_session, "get_images": get_images, "scan_read": scan_read, "scan_get_available_lines": get_available_lines, "scan_get_expected_size": get_expected_size, "scan_get_image": get_image, "scan_cancel": cancel, "exit": exit, } def main_loop(fifo_dir, fifo_filepaths): global COMMANDS pyinsane.init() length_size = len(struct.pack("i", 0)) fifo_c2s = os.open(fifo_filepaths[0], os.O_RDONLY) fifo_s2c = os.open(fifo_filepaths[1], os.O_WRONLY) try: logger.info("Ready") while True: length = os.read(fifo_c2s, length_size) if length == b'': break length = struct.unpack("i", length)[0] cmd = os.read(fifo_c2s, length) if cmd == b'': break assert(len(cmd) == length) cmd = pickle.loads(cmd) logger.debug("> {}".format(cmd['command'])) f = COMMANDS[cmd['command']] result = {} try: result['out'] = f(*cmd['args'], **cmd['kwargs']) except BaseException as exc: if (not isinstance(exc, EOFError) and not isinstance(exc, StopIteration)): logger.warning("Exception", exc_info=exc) result['exception'] = str(exc.__class__.__name__) result['exception_args'] = exc.args logger.debug("< {}".format(result)) result = pickle.dumps(result) length = len(result) length = struct.pack("i", length) os.write(fifo_s2c, length) os.write(fifo_s2c, result) if cmd['command'] == 'exit': break finally: os.close(fifo_s2c) os.close(fifo_c2s) logger.info("Daemon stopped") if __name__ == "__main__": formatter = logging.Formatter( '%(levelname)-6s %(name)-10s %(message)s' ) log = logging.getLogger() handler = logging.StreamHandler() handler.setFormatter(formatter) log.addHandler(handler) log.setLevel(logging.INFO) main_loop(sys.argv[1], sys.argv[2:4]) pyinsane-2.0.13/pyinsane2/sane/rawapi.py000066400000000000000000000467651332615651100201430ustar00rootroot00000000000000import ctypes import functools from .. import util __all__ = [ 'SaneCapabilities', 'SaneConstraint', 'SaneConstraintType', 'SaneDevice', 'SaneException', 'SaneFrame', 'SaneInfo', 'SaneOptionDescriptor', 'SaneParameters', 'SaneRange', 'SaneStatus', 'SaneUnit', 'SaneValueType', 'SaneVersion', 'is_sane_available', 'sane_init', 'sane_exit', 'sane_get_devices', 'sane_open', 'sane_close', 'sane_get_option_descriptor', 'sane_get_option_value', 'sane_set_option_value', 'sane_set_option_auto', 'sane_get_parameters', 'sane_start', 'sane_read', 'sane_cancel', 'sane_set_io_mode', 'sane_get_select_fd', 'sane_strstatus', ] @functools.total_ordering class SaneEnum(object): VALUE_TO_STR = {} def __init__(self, value): self.__value = value def __int__(self): return self.__value def __eq__(self, other): if isinstance(other, int): return self.__value == other return self.__value == other.__value def __lt__(self, other): if isinstance(other, int): return self.__value < other return self.__value < other.__value def __str__(self): if self.__value not in self.VALUE_TO_STR: txt = "Unknown value (%d)" % (self.__value) else: txt = "%s (%d)" % (self.VALUE_TO_STR[self.__value], self.__value) return "%s : %s" % (type(self), txt) class SaneFlags(object): FLAG_TO_STR = {} def __init__(self, flags=0): self.__flags = flags def __int__(self): return self.__flags def __add__(self, new_flag): return (self.__flags | new_flag) def __sub__(self, old_flag): return (self.flags & ~(old_flag)) def __contains__(self, flag): return ((self.__flags & flag) == flag) def __hex__(self): return hex(self.__flags) def __cmp__(self, other): return cmp(self.__flags, other.__flags) # noqa def __str__(self): txt = "%s :" % (type(self)) txt += "[" for flag in self.FLAG_TO_STR: if flag in self: txt += " %s," % (self.FLAG_TO_STR[flag]) txt += "]" return txt class SaneVersion(object): SANE_CURRENT_MAJOR = 1 SANE_CURRENT_MINOR = 0 def __init__(self, major, minor, build=0): self.major = major self.minor = minor self.build = build def is_current(self): return ((self.major == self.SANE_CURRENT_MAJOR) and (self.minor == self.SANE_CURRENT_MINOR)) def __str__(self): return "Sane version: %d.%d.%d" % (self.major, self.minor, self.build) class SaneStatus(SaneEnum): GOOD = 0 UNSUPPORTED = 1 CANCELLED = 2 DEVICE_BUSY = 3 INVAL = 4 EOF = 5 JAMMED = 6 NO_DOCS = 7 COVER_OPEN = 8 IO_ERROR = 9 NO_MEM = 10 ACCESS_DENIED = 11 WARMING_UP = 12 HW_LOCKED = 13 VALUE_TO_STR = { GOOD: "No error", UNSUPPORTED: "Operation is not supported", CANCELLED: "Operation was cancelled", DEVICE_BUSY: "Device is busy. Try again later", INVAL: "Data is invalid", EOF: "End-of-file : no more data available", JAMMED: "Document feeder jammed", NO_DOCS: "Document feeder out of documents", COVER_OPEN: "Scanner cover is open", IO_ERROR: "Error during device I/O", NO_MEM: "Out of memory", ACCESS_DENIED: "Access to resource has been denied", WARMING_UP: "Lamp is not ready yet. Try again later", HW_LOCKED: "Scanner mechanism locked for transport", } class SaneException(util.PyinsaneException): def __init__(self, status): Exception.__init__(self, str(status)) self.status = status class SaneValueType(SaneEnum): BOOL = 0 INT = 1 FIXED = 2 STRING = 3 BUTTON = 4 GROUP = 5 VALUE_TO_STR = { BOOL: "Boolean", INT: "Integer", FIXED: "Fixed", STRING: "String", BUTTON: "Button", GROUP: "Group", } VALUE_TO_CLASS = { BOOL: ctypes.c_int, INT: ctypes.c_int, FIXED: ctypes.c_int, STRING: ctypes.c_buffer, } def can_getset_opt(self): return int(self) in self.VALUE_TO_CLASS def buf_to_pyobj(self, buf): cl = self.VALUE_TO_CLASS[int(self)] if cl == ctypes.c_buffer: return buf.value return cl.from_buffer(buf).value def get_ctype_obj(self, pyobj): cl = self.VALUE_TO_CLASS[int(self)] return cl(pyobj) class SaneUnit(SaneEnum): NONE = 0 PIXEL = 1 BIT = 2 MM = 3 DPI = 4 PERCENT = 5 MICROSECOND = 6 VALUE_TO_STR = { NONE: "None", PIXEL: "Pixel", BIT: "Bit", MM: "Mm", DPI: "Dpi", PERCENT: "Percent", MICROSECOND: "Microsecond", } class SaneDevice(ctypes.Structure): _fields_ = [ ("name", ctypes.c_char_p), ("vendor", ctypes.c_char_p), ("model", ctypes.c_char_p), ("type", ctypes.c_char_p), ] def __str__(self): return ("Device: %s (%s, %s, %d)" % (self.name, self.vendor, self.model, self.type)) class SaneCapabilities(SaneFlags): NONE = 0 SOFT_SELECT = (1 << 0) HARD_SELECT = (1 << 1) SOFT_DETECT = (1 << 2) EMULATED = (1 << 3) AUTOMATIC = (1 << 4) INACTIVE = (1 << 5) ADVANCED = (1 << 6) FLAG_TO_STR = { SOFT_SELECT: "Soft_select", HARD_SELECT: "Hard_select", SOFT_DETECT: "Soft_detect", EMULATED: "Emulated", AUTOMATIC: "Automatic", INACTIVE: "Inactive", ADVANCED: "Advanced", } def is_active(self): return self.INACTIVE not in self def is_settable(self): return self.SOFT_SELECT in self class SaneInfo(SaneFlags): EMPTY = 0 INEXACT = (1 << 0) RELOAD_OPTIONS = (1 << 1) RELOAD_PARAMS = (1 << 2) FLAG_TO_STR = { INEXACT: "Inexact", RELOAD_OPTIONS: "Reload_options", RELOAD_PARAMS: "Reload_params", } class SaneConstraintType(SaneEnum): NONE = 0 RANGE = 1 WORD_LIST = 2 STRING_LIST = 3 VALUE_TO_STR = { NONE: "None", RANGE: "Range", WORD_LIST: "Word list", STRING_LIST: "String list", } @staticmethod def __constraint_none_to_pyobj(sane_constraint): return None @staticmethod def __constraint_range_to_pyobj(sane_constraint): return (sane_constraint.range.contents.min, sane_constraint.range.contents.max, sane_constraint.range.contents.quant) @staticmethod def __constraint_word_list_to_pyobj(sane_constraint): list_lng = sane_constraint.word_list[0] return sane_constraint.word_list[1:list_lng+1] @staticmethod def __constraint_string_list_to_pyobj(sane_constraint): string_list = [] idx = 0 while sane_constraint.string_list[idx]: string = sane_constraint.string_list[idx] if hasattr(string, 'decode'): string = string.decode("utf-8") string_list.append(string) idx += 1 return string_list def get_pyobj_constraint(self, sane_constraint): pyobj = { self.NONE: self.__constraint_none_to_pyobj, self.RANGE: self.__constraint_range_to_pyobj, self.WORD_LIST: self.__constraint_word_list_to_pyobj, self.STRING_LIST: self.__constraint_string_list_to_pyobj, }[int(self)](sane_constraint) return pyobj class SaneRange(ctypes.Structure): _fields_ = [ ("min", ctypes.c_int), ("max", ctypes.c_int), ("quant", ctypes.c_int), ] def __str__(self): return "Range: %d-%d (%d)" % (self.min, self.max, self.quant) class SaneConstraint(ctypes.Union): _fields_ = [ ("string_list", ctypes.POINTER(ctypes.c_char_p)), ("word_list", ctypes.POINTER(ctypes.c_int)), ("range", ctypes.POINTER(SaneRange)) ] class SaneOptionDescriptor(ctypes.Structure): _fields_ = [ ("name", ctypes.c_char_p), ("title", ctypes.c_char_p), # Is actually multi-line ! -> you have to get # a pointer on the pointer and use ptr[0], ptr[1], etc to get all the # lines ("desc", ctypes.c_char_p), ("type", ctypes.c_int), # SaneValueType ("unit", ctypes.c_int), # SaneUnit ("size", ctypes.c_int), ("cap", ctypes.c_int), # SaneCapabilities ("constraint_type", ctypes.c_int), # SaneConstraintType ("constraint", SaneConstraint), ] class SaneAction(SaneEnum): GET_VALUE = 0 SET_VALUE = 1 SET_AUTO = 3 VALUE_TO_STR = { GET_VALUE: "Get value", SET_VALUE: "Set value", SET_AUTO: "Set auto", } class SaneFrame(SaneEnum): GRAY = 0 RGB = 1 RED = 2 GREEN = 3 BLUE = 4 VALUE_TO_STR = { GRAY: "Gray", RGB: "RGB", RED: "Red", GREEN: "Green", BLUE: "Blue", } VALUE_TO_PIL_FORMAT = { GRAY: "L", RGB: "RGB", RED: "L", GREEN: "L", BLUE: "L", } def get_pil_format(self): return self.VALUE_TO_PIL_FORMAT[int(self)] class SaneParameters(ctypes.Structure): _fields_ = [ ("format", ctypes.c_int), # SaneFrame ("last_frame", ctypes.c_int), # boolean ("bytes_per_line", ctypes.c_int), ("pixels_per_line", ctypes.c_int), ("lines", ctypes.c_int), ("depth", ctypes.c_int), ] def dummy_auth_callback(sane_ressource_str): return ( "anonymous", # login "" # password ) class AuthCallbackWrapper(object): MAX_USERNAME_LEN = 128 MAX_PASSWORD_LEN = 128 def __init__(self, auth_callback): self.__auth_callback = auth_callback def wrapper(self, ressource_ptr, login_ptr, passwd_ptr): (login, password) = self.__auth_callback(ressource_ptr.value) # TODO(Jflesch): Make sure the following works ctypes.memmove(login_ptr, ctypes.c_char_p(login), min(len(login)+1, self.MAX_USERNAME_LEN)) ctypes.memmove(passwd_ptr, ctypes.c_char_p(password), min(len(password)+1, self.MAX_USERNAME_LEN)) sane_is_init = 0 sane_version = None sane_available = False for libname in ["libsane.so.1", "libsane.1.dylib"]: try: SANE_LIB = ctypes.cdll.LoadLibrary(libname) sane_available = True break except OSError: pass if sane_available: AUTH_CALLBACK_DEF = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p) SANE_LIB.sane_init.argtypes = [ ctypes.POINTER(ctypes.c_int), AUTH_CALLBACK_DEF ] SANE_LIB.sane_init.restype = ctypes.c_int SANE_LIB.sane_exit.argtypes = [] SANE_LIB.sane_exit.restype = None SANE_LIB.sane_get_devices.argtypes = [ ctypes.POINTER(ctypes.POINTER(ctypes.POINTER(SaneDevice))), ctypes.c_int ] SANE_LIB.sane_get_devices.restype = ctypes.c_int SANE_LIB.sane_open.argtypes = [ ctypes.c_char_p, ctypes.POINTER(ctypes.c_void_p), ] SANE_LIB.sane_open.restype = ctypes.c_int SANE_LIB.sane_close.argtypes = [ctypes.c_void_p] SANE_LIB.sane_close.restype = None SANE_LIB.sane_get_option_descriptor.argtypes = [ ctypes.c_void_p, ctypes.c_int ] SANE_LIB.sane_get_option_descriptor.restype = \ ctypes.POINTER(SaneOptionDescriptor) SANE_LIB.sane_control_option.argtypes = [ ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.POINTER(ctypes.c_int) ] SANE_LIB.sane_control_option.restype = ctypes.c_int SANE_LIB.sane_get_parameters.argtypes = [ ctypes.c_void_p, ctypes.POINTER(SaneParameters), ] SANE_LIB.sane_get_parameters.restype = ctypes.c_int SANE_LIB.sane_start.argtypes = [ctypes.c_void_p] SANE_LIB.sane_start.restype = ctypes.c_int SANE_LIB.sane_read.argtypes = [ ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, ctypes.POINTER(ctypes.c_int) ] SANE_LIB.sane_read.restype = ctypes.c_int SANE_LIB.sane_cancel.argtypes = [ctypes.c_void_p] SANE_LIB.sane_cancel.restype = None SANE_LIB.sane_set_io_mode.argtypes = [ctypes.c_void_p, ctypes.c_int] SANE_LIB.sane_set_io_mode.restype = ctypes.c_int SANE_LIB.sane_get_select_fd.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_int)] SANE_LIB.sane_get_select_fd.restype = ctypes.c_int def is_sane_available(): global sane_available return sane_available def sane_init(auth_callback=dummy_auth_callback): global sane_available, sane_is_init, sane_version assert(sane_available) sane_is_init += 1 if sane_is_init > 1: return sane_version version_code = ctypes.c_int() wrap_func = AuthCallbackWrapper(auth_callback).wrapper auth_callback = AUTH_CALLBACK_DEF(wrap_func) status = SANE_LIB.sane_init(ctypes.pointer(version_code), auth_callback) if status != SaneStatus.GOOD: raise SaneException(SaneStatus(status)) version_code = version_code.value major = (version_code >> 24) & 0xFF minor = (version_code >> 16) & 0xFF build = (version_code >> 0) & 0xFFFF sane_version = SaneVersion(major, minor, build) return sane_version def sane_exit(): global sane_available, sane_is_init assert(sane_available) # TODO(Jflesch): This is a workaround # In a multithreaded environment, for some unknown reason, # calling sane_exit() will work but the program will crash # when stopping. So we simply never call sane_exit() ... # sane_is_init -= 1 # if sane_is_init <= 0: # SANE_LIB.sane_exit() def sane_get_devices(local_only=False): global sane_available assert(sane_available) devices_ptr = ctypes.POINTER(ctypes.POINTER(SaneDevice))() status = SANE_LIB.sane_get_devices(ctypes.pointer(devices_ptr), ctypes.c_int(local_only)) if status != SaneStatus.GOOD: raise SaneException(SaneStatus(status)) devices = [] i = 0 while devices_ptr[i]: devices.append(devices_ptr[i].contents) i += 1 return devices def sane_open(dev_name): global sane_available assert(sane_available) if isinstance(dev_name, str): dev_name = dev_name.encode('utf-8') handle_ptr = ctypes.c_void_p() status = SANE_LIB.sane_open(ctypes.c_char_p(dev_name), ctypes.pointer(handle_ptr)) if status != SaneStatus.GOOD: raise SaneException(SaneStatus(status)) return handle_ptr def sane_close(handle): global sane_available assert(sane_available) SANE_LIB.sane_close(handle) def sane_get_option_descriptor(handle, option_idx): global sane_available assert(sane_available) opt_desc_ptr = SANE_LIB.sane_get_option_descriptor( handle, ctypes.c_int(option_idx)) if not opt_desc_ptr: raise SaneException(SaneStatus(SaneStatus.INVAL)) return opt_desc_ptr.contents def sane_get_option_value(handle, option_idx): global sane_available assert(sane_available) # we need the descriptor first in order to allocate a buffer of the correct # size, then cast it to the correct type opt_desc = sane_get_option_descriptor(handle, option_idx) buf = ctypes.c_buffer(max(4, opt_desc.size)) info = ctypes.c_int() status = SANE_LIB.sane_control_option(handle, ctypes.c_int(option_idx), SaneAction.GET_VALUE, ctypes.pointer(buf), ctypes.pointer(info)) if status != SaneStatus.GOOD: raise SaneException(SaneStatus(status)) return SaneValueType(opt_desc.type).buf_to_pyobj(buf) def sane_set_option_value(handle, option_idx, new_value): global sane_available assert(sane_available) if isinstance(new_value, str): new_value = new_value.encode('utf-8') # we need the descriptor first in order to allocate a buffer of the correct # size, then cast it to the correct type opt_desc = sane_get_option_descriptor(handle, option_idx) value = SaneValueType(opt_desc.type).get_ctype_obj(new_value) info = ctypes.c_int() status = SANE_LIB.sane_control_option(handle, ctypes.c_int(option_idx), SaneAction.SET_VALUE, ctypes.pointer(value), ctypes.pointer(info)) if status != SaneStatus.GOOD: raise SaneException(SaneStatus(status)) return SaneInfo(info.value) def sane_set_option_auto(handle, option_idx): global sane_available assert(sane_available) info = ctypes.c_int() status = SANE_LIB.sane_control_option(handle, ctypes.c_int(option_idx), SaneAction.SET_AUTO, ctypes.c_void_p(), ctypes.pointer(info)) if status != SaneStatus.GOOD: raise SaneException(SaneStatus(status)) return SaneInfo(info.value) def sane_get_parameters(handle): global sane_available assert(sane_available) parameters = SaneParameters() status = SANE_LIB.sane_get_parameters(handle, ctypes.pointer(parameters)) if status != SaneStatus.GOOD: raise SaneException(SaneStatus(status)) return parameters def sane_start(handle): global sane_available assert(sane_available) status = SANE_LIB.sane_start(handle) if status == SaneStatus.NO_DOCS: raise StopIteration() if status != SaneStatus.GOOD: raise SaneException(SaneStatus(status)) def sane_read(handle, nb_bytes): global sane_available assert(sane_available) buf = ctypes.c_buffer(nb_bytes) length = ctypes.c_int() status = SANE_LIB.sane_read(handle, ctypes.pointer(buf), len(buf), ctypes.pointer(length)) if status == SaneStatus.NO_DOCS: raise StopIteration() elif status == SaneStatus.EOF: raise EOFError() elif status != SaneStatus.GOOD: raise SaneException(SaneStatus(status)) return buf[:length.value] def sane_cancel(handle): global sane_available assert(sane_available) SANE_LIB.sane_cancel(handle) def sane_set_io_mode(handle, non_blocking=False): global sane_available assert(sane_available) status = SANE_LIB.sane_set_io_mode(handle, ctypes.c_int(int(non_blocking))) if status != SaneStatus.GOOD: raise SaneException(SaneStatus(status)) def sane_get_select_fd(handle): global sane_available assert(sane_available) fd = ctypes.c_int() status = SANE_LIB.sane_get_select_fd(handle, ctypes.pointer(fd)) if status != SaneStatus.GOOD: raise SaneException(SaneStatus(status)) return fd.value def sane_strstatus(status): global sane_available assert(sane_available) if status in SaneStatus.__meanings: return SaneStatus.__meanings[status] return "Unknown error code: %d" % (status) pyinsane-2.0.13/pyinsane2/util.py000066400000000000000000000044661332615651100166770ustar00rootroot00000000000000import logging logger = logging.getLogger(__name__) __all__ = [ 'AliasOption', 'PyinsaneException', ] class PyinsaneException(Exception): def __init__(self, status): Exception.__init__(self, str(status)) self.status = status class AliasOption(object): def __init__(self, name, alias_for, options): self.__dict__['alias_for'] = alias_for self.__dict__['_options'] = [ options[opt_name] for opt_name in alias_for ] self.__dict__['name'] = name def __getattr__(self, attr): if '_options' not in self.__dict__: raise AttributeError() return getattr(self.__dict__['_options'][0], attr) def __setattr__(self, attr, new_value): last_exc = None for opt in self.__dict__['_options']: try: setattr(opt, attr, new_value) except Exception as exc: # keep trying to set the options for consistency # (some driver may return an error while accepting the value # ...) logger.exception("Failed to set option {}: {}".format( self.__dict__['name'], exc )) last_exc = exc if last_exc: raise last_exc def __str__(self): return ("Option [{}] (alias for {})".format( self.__dict__['name'], self.__dict__['alias_for'] )) class ResolutionOption(object): def __init__(self, actual_opt): self.__dict__['_opt'] = actual_opt constraint = actual_opt.constraint if isinstance(constraint, tuple): if len(constraint) > 3 and constraint[1] < constraint[2]: # constraint is (min, interval, max) # constraint must be (min, max, interval) constraint = ( constraint[0], constraint[2], constraint[1] ) self.__dict__['constraint'] = constraint def __getattr__(self, attr): if attr == 'constraint': return self.__dict__['constraint'] return getattr(self.__dict__['_opt'], attr) def __setattr__(self, attr, new_value): setattr(self.__dict__['_opt'], attr, new_value) def __str__(self): return str(self.__dict__['_opt']) pyinsane-2.0.13/pyinsane2/wia/000077500000000000000000000000001332615651100161165ustar00rootroot00000000000000pyinsane-2.0.13/pyinsane2/wia/__init__.py000066400000000000000000000000061332615651100202230ustar00rootroot00000000000000# nop pyinsane-2.0.13/pyinsane2/wia/abstract.py000066400000000000000000000502231332615651100202750ustar00rootroot00000000000000import io import logging import PIL.Image import PIL.ImageFile from . import rawapi from .. import util from .rawapi import WIAException __all__ = [ 'init', 'exit', 'Scanner', 'ScannerOption', 'WIAException', 'get_devices', ] logger = logging.getLogger(__name__) def init(): rawapi.init() def exit(): rawapi.exit() class Scan(object): # WORKAROUND(Jflesch)> When using the ADF with HP drivers, even if there is # no paper in the ADF # The driver will write about 56 bytes in the stream (BMP headers ?) # --> We ignore BMP too small MIN_BYTES = 1024 def __init__(self, session, source, multiple=False): self._session = session self.source = source self._data = b"" self.is_valid = False self.is_complete = False self._img_size = None logger.info("Starting scan") self.scan = rawapi.start_scan(self.source) self.multiple = multiple def read(self): # will raise EOFError at the end of each page # will raise StopIteration when all the pages are done if self.is_complete: if self.multiple: return self._session._next().read() else: logger.debug("Page is complete + single scan requested" " --> StopIteration") raise StopIteration() try: buf = self.scan.read() self._data += buf except EOFError: # Jflesch> Some drivers (Brother for instance) keep telling us # there is still something to scan yet when there is not. # They send some data (image headers ?) and only then they # realize there is nothing left to scan ... logger.info("End of page") self.is_complete = True if len(self._data) >= self.MIN_BYTES: try: self._session._add_image(self._get_current_image()) self.is_valid = True except Exception as exc: logger.warning( "Got %d bytes, but exception while decoding image." " Assuming no more page are available", len(self._data), exc_info=exc ) logger.info("End of scan session") self._session._cancel_current() raise StopIteration() raise else: # --> Too small. Scrap the crap from the drivers and switch # back to the last valid data obtained (last page scanned) logger.warning("Got data, but too small for a BMP" " --> StopIteration") self._session._cancel_current() raise StopIteration() def _get_current_image(self): stream = io.BytesIO(self._data) # We get the image as a truncated bitmap. # ('rawrgb' is not supported by all drivers ...) # BMP headers are annoying. PIL.ImageFile.LOAD_TRUNCATED_IMAGES = True try: img = PIL.Image.open(stream) img.load() finally: PIL.ImageFile.LOAD_TRUNCATED_IMAGES = False self._img_size = img.size return img def _get_available_lines(self): if self._img_size is None: try: self._get_current_image() except Exception as exc: logger.warning("Exception while getting current image", exc_info=exc) # assumes we just got truncated headers for now return (0, 0) # estimated line_size = self._img_size[0] * 3 # rgb data = len(self._data) - 1024 # - headers return (0, int(data / line_size)) available_lines = property(_get_available_lines) def _get_expected_size(self): if self._img_size: return self._img_size options = self._session.scanner.options return ( int(options['xextent'].value), int(options['yextent'].value) ) expected_size = property(_get_expected_size) def get_image(self, start_line=0, end_line=-1): img = self._get_current_image() img_size = img.size if start_line > 0: img = img.crop((0, start_line, img_size[0], img_size[1])) if end_line >= 0: end_line -= start_line img = img.crop((0, 0, img_size[0], end_line)) return img def cancel(self): # TODO raise NotImplementedError() def __str__(self): return ("Scan instance for session {}".format(self._session)) class ScanSession(object): def __init__(self, scanner, srcid, multiple): self.scanner = scanner self.multiple = multiple self.source = scanner.srcs[srcid] self.images = [] self.previous = None self.scan = Scan(self, self.source, self.multiple) def _add_image(self, img): self.images.append(img) def _next(self): self.scan = Scan(self, self.source, self.multiple) return self.scan def _cancel_current(self): if self.previous is None: return self.scan = self.previous class ScannerCapabilities(object): def __init__(self, opt): self.opt = opt def is_active(self): return True def is_settable(self): return self.opt.accessright == "rw" def __str__(self): return ("Access: {}".format(self.opt.accessright)) class ScannerOption(object): idx = -1 title = "" desc = "" val_type = None # TODO unit = None # TODO size = 4 capabilities = None constraint_type = None # TODO constraint = None def __init__(self, scanner, objsrc, name, value, possible_values, accessright, constraint): self.objsrc = objsrc self.scanner = scanner self.name = name self._value = value if constraint: self.constraint = constraint else: self.constraint = possible_values self.accessright = accessright self.capabilities = ScannerCapabilities(self) def _get_value(self): return self._value def _set_value(self, new_value): if self.accessright != 'rw': raise rawapi.WIAException("Property {} is read-only".format( self.name )) has_success = False exc = None for obj in self.objsrc: try: rawapi.set_property(obj, self.name, new_value) has_success = True except rawapi.WIAException as _exc: logger.warning("Exception while setting {}: {}".format( self.name, _exc )) logger.exception(_exc) exc = _exc self._value = new_value try: self.scanner.reload_options() except Exception as exc: logger.warning("Failed to reload option after updating value", exc_info=exc) if not has_success: raise exc value = property(_get_value, _set_value) def __str__(self): return ("Option [{}] (basic)".format(self.name)) def __eq__(self, other): return (self.name == other.name and self.constraint == other.constraint) class SourceOption(ScannerOption): idx = -1 title = "" desc = "" val_type = None # TODO unit = None # TODO size = 4 capabilities = None constraint_type = None # TODO constraint = None accessright = "rw" def __init__(self, sources): self.name = "source" self.sources = sources self.constraint = [srcid for (srcid, src) in sources] self._value = self.constraint[0] self.capabilities = ScannerCapabilities(self) def _get_value(self): return self._value def _set_value(self, new_value): if new_value not in self.constraint: raise WIAException("Invalid source: {}".format(new_value)) self._value = new_value value = property(_get_value, _set_value) def __str__(self): return ("Option [{}] (source)".format(self.name)) def __eq__(self, other): return isinstance(other, SourceOption) class ModeOption(object): idx = -1 title = "" desc = "" val_type = None # TODO unit = None # TODO size = 4 capabilities = None constraint_type = None # TODO constraint = None def __init__(self, scanner): self.name = "mode" self.scanner = scanner self.constraint = ["Color", "Gray", "BW"] self.capabilities = ScannerCapabilities(self) self.scanner = scanner def _get_value(self): opts = self.scanner.options if opts['bits_per_channel'].value == 1: return 'BW' if opts['channels_per_pixel'].value == 1: return 'Gray' return 'Color' def _set_value(self, new_value): opts = self.scanner.options if new_value == "BW": opts['depth'].value = 1 self.scanner.reload_options() return if new_value == "Gray": opts['depth'].value = 8 self.scanner.reload_options() return if new_value == "Color": opts['depth'].value = 24 self.scanner.reload_options() return raise WIAException("Unknown value '{}' for option 'mode'".format( new_value )) value = property(_get_value, _set_value) def _get_accessright(self): opts = self.scanner.options return opts['depth'].accessright accessright = property(_get_accessright) def __str__(self): return ("Option [{}] (mode)".format(self.name)) def get_pos_constraint(options, opt_min, opt_max, opt_res): if (opt_min not in options or opt_max not in options or opt_res not in options): return None vmax = options[opt_max].value / 1000 # thousandths of inch vres = options[opt_res].value return (0, int(vmax * vres)) class PosOption(object): idx = -1 title = "" desc = "" val_type = None # TODO unit = None # TODO size = 4 capabilities = None constraint_type = None # TODO constraint = None def __init__(self, scanner, name, base_name, options, opt_min, opt_max, opt_res): self.name = name self.base_name = base_name self.capabilities = ScannerCapabilities(self) self.scanner = scanner self._options = options self.constraint = get_pos_constraint( options, opt_min, opt_max, opt_res ) def _get_value(self): return self._options[self.base_name + 'pos'].value def _set_value(self, new_value): diff = self._options[self.base_name + 'pos'].value - self.value extent = self._options[self.base_name + 'extent'].value self._options[self.base_name + 'pos'].value = new_value self._options[self.base_name + 'extent'].value = extent - diff value = property(_get_value, _set_value) def _get_accessright(self): rw = True if self._options[self.base_name + 'pos'].accessright != 'rw': rw = False elif self._options[self.base_name + 'extent'].accessright != 'rw': rw = False return "rw" if rw else "ro" accessright = property(_get_accessright) def __str__(self): return ("Option [{}] (position for [{}])".format( self.name, self.base_name )) class ExtendOption(object): idx = -1 title = "" desc = "" val_type = None # TODO unit = None # TODO size = 4 capabilities = None constraint_type = None # TODO constraint = None def __init__(self, scanner, name, base_name, options, opt_min, opt_max, opt_res): self.name = name self.base_name = base_name self.capabilities = ScannerCapabilities(self) self.scanner = scanner self._options = options self.constraint = get_pos_constraint( options, opt_min, opt_max, opt_res ) def _get_value(self): return (self._options[self.base_name + 'extent'].value + self._options[self.base_name + 'pos'].value) def _set_value(self, new_value): new_value -= self._options[self.base_name + 'pos'].value self._options[self.base_name + 'extent'].value = new_value value = property(_get_value, _set_value) def _get_accessright(self): rw = True if self._options[self.base_name + 'pos'].accessright != 'rw': rw = False elif self._options[self.base_name + 'extent'].accessright != 'rw': rw = False return "rw" if rw else "ro" accessright = property(_get_accessright) def __str__(self): return ("Option [{}] (extent for [{}])".format( self.name, self.base_name )) class Scanner(object): def __init__(self, name): self._dev = rawapi.open(name) self._srcs_list = rawapi.get_sources(self._dev) self.srcs = {"any": self._dev} for (srcid, src) in self._srcs_list: self.srcs[srcid] = src self.options = {} self.reload_options() self.name = name self.nice_name = self.options['dev_name'].value self.vendor = self.options['vend_desc'].value self.model = self.options['dev_desc'].value self.dev_type = self.options['dev_type'].value for (opt, val) in [ ('current_intent', 'image_type_color,maximize_quality'), ('format', 'bmp'), ('preferred_format', 'bmp'), ('page_size', 'a4'), ('depth', 24), ]: if opt not in self.options: continue try: self.options[opt].value = val logger.warning("Option '{}' preset to '{}' on [{}]".format( opt, val, self )) except Exception as exc: logger.warning("Failed to pre-set option '{}' on [{}]".format( opt, self ), exc_info=exc) @staticmethod def _convert_prop_list_to_dict(props): out = {} for (propname, propvalue, accessright, possible_values) in props: out[propname] = { 'value': propvalue, 'accessright': accessright, 'possible_values': possible_values } return out @staticmethod def _merge_constraints(props, constraints): for (propname, constraint) in constraints: if propname not in props: logger.warning( "Constraint found on property [{}] but property " "not found".format(propname) ) continue if isinstance(constraint, list): constraint.sort() props[propname]['constraint'] = constraint def reload_options(self): original = self.options self.options = {} dev_properties = self._convert_prop_list_to_dict( rawapi.get_properties(self._dev) ) dev_constraints = rawapi.get_constraints(self._dev) self._merge_constraints(dev_properties, dev_constraints) src_properties = {} for (srcid, src) in self._srcs_list: src_properties[srcid] = self._convert_prop_list_to_dict( rawapi.get_properties(src) ) src_constraints = rawapi.get_constraints(src) self._merge_constraints(src_properties[srcid], src_constraints) for (opt_name, opt_infos) in dev_properties.items(): self.options[opt_name] = ScannerOption( self, [self._dev], opt_name, opt_infos['value'], opt_infos['possible_values'], opt_infos['accessright'], opt_infos['constraint'] if 'constraint' in opt_infos else None ) # generate list of options from all the sources, and try to apply them # on all the sources for (srcid, opts) in src_properties.items(): for (opt_name, opt_infos) in opts.items(): opt = ScannerOption( self, self.srcs.values(), opt_name, opt_infos['value'], opt_infos['possible_values'], opt_infos['accessright'], opt_infos['constraint'] if 'constraint' in opt_infos else None ) if opt_name in self.options: if self.options[opt_name] != opt: logger.warning("Got multiple time the option [{}]," " but they are not identical".format( opt_name)) self.options[opt_name] = opt # aliases to match Sane if "xpos" in self.options.keys() and "xextent" in self.options.keys(): self.options['tl-x'] = PosOption( self, "tl-x", "x", self.options, "min_horizontal_size", "max_horizontal_size", "xres" ) self.options['br-x'] = ExtendOption( self, "br-x", "x", self.options, "min_horizontal_size", "max_horizontal_size", "xres" ) if "ypos" in self.options.keys() and "yextent" in self.options.keys(): self.options['tl-y'] = PosOption( self, "tl-y", "y", self.options, "min_vertical_size", "max_vertical_size", "yres" ) self.options['br-y'] = ExtendOption( self, "br-y", "y", self.options, "min_vertical_size", "max_vertical_size", "yres" ) res_alias_for = [] if "xres" in self.options.keys(): res_alias_for.append("xres") if "yres" in self.options.keys(): res_alias_for.append("yres") if res_alias_for != []: self.options['resolution'] = util.ResolutionOption( util.AliasOption( "resolution", res_alias_for, self.options ) ) if 'source' in original: self.options['source'] = original['source'] elif len(self._srcs_list) > 0: self.options['source'] = SourceOption(self._srcs_list) else: # Epson WorkForce ES-300W self.options['source'] = SourceOption([("any", self._dev)]) if 'mode' in original: self.options['mode'] = original['mode'] else: self.options['mode'] = ModeOption(self) def scan(self, multiple=False): if (not ('source' in self.options and self.options['source'].capabilities.is_active())): value = "" else: value = self.options['source'].value if hasattr(value, 'decode'): value = value.decode('utf-8') if "adf" not in value.lower() and "feeder" not in value.lower(): # XXX(Jflesch): If we try to scan multiple pages # from a feeder, we never get WIA_ERROR_PAPER_EMPTY # and loop forever multiple = False if 'pages' in self.options: try: # Even with an ADF, Pyinsane actually request one page # after the other. # This is not orthodox at all, but still, it has proven to be # the most reliable way. self.options['pages'].value = 1 except Exception as exc: logger.error("Failed to set options [pages]", exc_info=exc) return ScanSession(self, self.options['source'].value, multiple) def __str__(self): return ("'%s' (%s, %s, %s)" % (self.nice_name, self.vendor, self.model, self.dev_type)) def get_devices(local_only=False): devs = rawapi.get_devices() out = [] for dev in devs: try: scanner = Scanner(dev[0]) out.append(scanner) except Exception as exc: logger.warning("Failed to access scanner {} : {}".format(dev, exc)) logger.exception(exc) return out pyinsane-2.0.13/pyinsane2/wia/properties.cpp000066400000000000000000000771421332615651100210310ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include "properties.h" #include "trace.h" #include "util.h" static const struct wia_prop_int g_possible_connect_status[] = { { WIA_DEVICE_NOT_CONNECTED, "not_connected" }, { WIA_DEVICE_CONNECTED, "connected" }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_access_rights[] = { { WIA_ITEM_READ, "read" }, { WIA_ITEM_WRITE, "write" }, { WIA_ITEM_CAN_BE_DELETED, "can_be_deleted" }, { WIA_ITEM_RD, "read_can_be_deleted" }, { WIA_ITEM_RWD, "read_write_can_be_deleted" }, { -1, NULL, } }; static const struct wia_prop_int g_possible_compression[] = { { WIA_COMPRESSION_NONE, "none", }, { 100 /* WIA_COMPRESSION_AUTO ; Visual C++ 2010 doesn't know this value */, "auto", }, { WIA_COMPRESSION_BI_RLE4, "bi_rle4", }, { WIA_COMPRESSION_BI_RLE8, "bi_rle8", }, { WIA_COMPRESSION_G3, "g3", }, { WIA_COMPRESSION_G4, "g4", }, { WIA_COMPRESSION_JPEG, "jpeg", }, { WIA_COMPRESSION_JBIG, "jbig", }, { WIA_COMPRESSION_JPEG2K, "jpeg2k", }, { WIA_COMPRESSION_PNG, "png", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_datatype[] = { { 100 /* WIA_DATA_AUTO ; Visual C++ 2010 doesn't know this value */, "auto", }, { WIA_DATA_COLOR, "color", }, { WIA_DATA_COLOR_DITHER, "color_dither", }, { WIA_DATA_COLOR_THRESHOLD, "color_threshold", }, { WIA_DATA_DITHER, "dither", }, { WIA_DATA_GRAYSCALE, "grayscale", }, { WIA_DATA_THRESHOLD, "threshold", }, { WIA_DATA_RAW_BGR, "raw_bgr", }, { WIA_DATA_RAW_CMY, "raw_cmy", }, { WIA_DATA_RAW_CMYK, "raw_cmyk", }, { WIA_DATA_RAW_RGB, "raw_rgb", }, { WIA_DATA_RAW_YUV, "raw_yuv", }, { WIA_DATA_RAW_YUVK, "raw_yuvk", }, { -1, NULL, }, }; static const struct wia_prop_clsid g_possible_format[] = { { WiaImgFmt_BMP, "bmp", }, { WiaImgFmt_CIFF, "ciff", }, { WiaImgFmt_EXIF, "exif", }, { WiaImgFmt_FLASHPIX, "flashpix", }, { WiaImgFmt_GIF, "gif", }, { WiaImgFmt_ICO, "ico", }, { WiaImgFmt_JBIG, "jbig", }, { WiaImgFmt_JPEG, "jpeg", }, { WiaImgFmt_JPEG2K, "jpeg2k", }, { WiaImgFmt_JPEG2KX, "jpeg2kx", }, { WiaImgFmt_MEMORYBMP, "memorybmp", }, { WiaImgFmt_PDFA, "pdfa", }, { WiaImgFmt_PHOTOCD, "photocd", }, { WiaImgFmt_PICT, "pict", }, { WiaImgFmt_PNG, "png", }, { WiaImgFmt_RAW, "raw", }, { WiaImgFmt_RAWRGB, "rawrgb", }, { WiaImgFmt_TIFF, "tiff", }, { NULL, NULL, }, }; static const struct wia_prop_clsid g_possible_item_category[] = { { WIA_CATEGORY_ROOT, "root", }, { WIA_CATEGORY_FLATBED, "flatbed", }, { WIA_CATEGORY_FEEDER, "feeder", }, { WIA_CATEGORY_FEEDER_FRONT, "feeder_front", }, { WIA_CATEGORY_FEEDER_BACK, "feeder_back", }, { WIA_CATEGORY_FILM, "film", }, { WIA_CATEGORY_FOLDER, "folder", }, { WIA_CATEGORY_FINISHED_FILE, "finished_file", }, { NULL, NULL, }, }; static const struct wia_prop_int g_possible_item_flags[] = { { WiaItemTypeAnalyze, "analyze", }, { WiaItemTypeAudio, "audio", }, { WiaItemTypeBurst, "burst", }, { WiaItemTypeDeleted, "deleted", }, { WiaItemTypeDocument, "document", }, { WiaItemTypeDevice, "device", }, { WiaItemTypeDisconnected, "disconnected", }, { WiaItemTypeFile, "file", }, { WiaItemTypeFolder, "folder", }, { WiaItemTypeFree, "free", }, { WiaItemTypeGenerated, "generated", }, { WiaItemTypeHasAttachments, "has_attachments", }, { WiaItemTypeHPanorama, "hpanorama", }, { WiaItemTypeImage, "image", }, { WiaItemTypeProgrammableDataSource, "programmable_data_source", }, { WiaItemTypeRoot, "root", }, { WiaItemTypeStorage, "storage", }, { WiaItemTypeTransfer, "transfer", }, // WiaItemTypeTwainCapabilityPassThrough, // Jflesch> Doesn't exist ? { WiaItemTypeVideo, "video", }, { WiaItemTypeVPanorama, "vpanorama", }, { -1, NULL }, }; static const struct wia_prop_int g_possible_planar[] = { { WIA_PACKED_PIXEL, "pixel" }, { WIA_PLANAR, "planar" }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_suppress_property_page[] = { { WIA_PROPPAGE_CAMERA_ITEM_GENERAL, "camera_item_general", }, { WIA_PROPPAGE_SCANNER_ITEM_GENERAL, "scanner_item_general", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_tymed[] = { { TYMED_CALLBACK, "callback", }, { TYMED_MULTIPAGE_CALLBACK, "multipage_callback", }, { TYMED_FILE, "file", }, { TYMED_MULTIPAGE_FILE, "multipage_file", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_dev_type[] = { { StiDeviceTypeDefault, "default", }, { StiDeviceTypeScanner, "scanner", }, { StiDeviceTypeDigitalCamera, "digital_camera", }, { StiDeviceTypeStreamingVideo, "streaming_video", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_hw_config[] = { { 1, "generic", }, // Generic WDM device { 2, "scsi", }, // SCSI device { 4, "usb", }, // USB device { 8, "serial", }, // Serial device { 16, "parallel", }, // Parallel device { -1, NULL, }, }; static const struct wia_prop_int g_possible_document_handling_capabilities[] = { { AUTO_SOURCE, "auto_source", }, { ADVANCED_DUP, "dup", }, { DETECT_FILM_TPA, "detect_film_tpa", }, { DETECT_STOR, "detect_stor", }, { FILM_TPA, "film_tpa", }, { STOR, "stor", }, { DETECT_FEED, "detect_feed", }, { DETECT_FLAT, "detect_flat", }, { DETECT_SCAN, "detect_scan", }, { DUP, "dup", }, { FEED, "feed", }, { FLAT, "flat", }, { DETECT_DUP, "detect_dup", }, { DETECT_DUP_AVAIL, "detect_dup_avail", }, { DETECT_FEED_AVAIL, "detect_feed_avail", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_document_handling_select[] = { { FEEDER, "feeder", }, { FLATBED, "flatbed", }, { DUPLEX, "duplex", }, { AUTO_ADVANCE, "auto_advance", }, { FRONT_FIRST, "front_first", }, { BACK_FIRST, "back_first", }, { FRONT_ONLY, "front_only", }, { BACK_ONLY, "back_only", }, { NEXT_PAGE, "next_page", }, { PREFEED, "prefeed", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_document_handling_status[] = { { FEED_READY, "feed_ready", }, { FLAT_READY, "flat_ready", }, { DUP_READY, "dup_ready", }, { FLAT_COVER_UP, "flat_cover_up", }, { PATH_COVER_UP, "path_cover_up", }, { PAPER_JAM, "paper_jam", }, { FILM_TPA_READY, "film_tpa_ready", }, { STORAGE_READY, "storage_ready", }, { STORAGE_FULL, "storage_full", }, { MULTIPLE_FEED, "multiple_feed", }, { DEVICE_ATTENTION, "device_attention", }, { LAMP_ERR, "lamp_err", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_horizontal_bed_registration[] = { { LEFT_JUSTIFIED, "left_justified", }, { CENTERED, "centered", }, { RIGHT_JUSTIFIED, "right_justified", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_orientation[] = { { LANDSCAPE, "landscape", }, { PORTRAIT, "portrait", }, { ROT180, "rot180", }, { ROT270, "rot270", }, { -1, NULL }, }; static const struct wia_prop_int g_possible_page_size[] = { { WIA_PAGE_A4, "a4", }, { WIA_PAGE_CUSTOM, "custom", }, // see WIA_DPS_PAGE_HEIGHT and WIA_DPS_PAGE_WIDTH { WIA_PAGE_LETTER, "letter", }, { WIA_PAGE_USLEGAL, "uslegal", }, { WIA_PAGE_USLETTER, "usletter", }, { WIA_PAGE_USLEDGER, "usledger", }, { WIA_PAGE_USSTATEMENT, "usstatement", }, { WIA_PAGE_BUSINESSCARD, "businesscard", }, { WIA_PAGE_ISO_A0, "iso_a0", }, { WIA_PAGE_ISO_A1, "iso_a1", }, { WIA_PAGE_ISO_A2, "iso_a2", }, { WIA_PAGE_ISO_A3, "iso_a3", }, { WIA_PAGE_ISO_A4, "iso_a4", }, { WIA_PAGE_ISO_A5, "iso_a5", }, { WIA_PAGE_ISO_A6, "iso_a6", }, { WIA_PAGE_ISO_A7, "iso_a7", }, { WIA_PAGE_ISO_A8, "iso_a8", }, { WIA_PAGE_ISO_A9, "iso_a9", }, { WIA_PAGE_ISO_A10, "iso_a10", }, { WIA_PAGE_ISO_B0, "iso_b0", }, { WIA_PAGE_ISO_B1, "iso_b1", }, { WIA_PAGE_ISO_B2, "iso_b2", }, { WIA_PAGE_ISO_B3, "iso_b3", }, { WIA_PAGE_ISO_B4, "iso_b4", }, { WIA_PAGE_ISO_B5, "iso_b5", }, { WIA_PAGE_ISO_B6, "iso_b6", }, { WIA_PAGE_ISO_B7, "iso_b7", }, { WIA_PAGE_ISO_B8, "iso_b8", }, { WIA_PAGE_ISO_B9, "iso_b9", }, { WIA_PAGE_ISO_B10, "iso_b10", }, { WIA_PAGE_ISO_C0, "iso_c0", }, { WIA_PAGE_ISO_C1, "iso_c1", }, { WIA_PAGE_ISO_C2, "iso_c2", }, { WIA_PAGE_ISO_C3, "iso_c3", }, { WIA_PAGE_ISO_C4, "iso_c4", }, { WIA_PAGE_ISO_C5, "iso_c5", }, { WIA_PAGE_ISO_C6, "iso_c6", }, { WIA_PAGE_ISO_C7, "iso_c7", }, { WIA_PAGE_ISO_C8, "iso_c8", }, { WIA_PAGE_ISO_C9, "iso_c9", }, { WIA_PAGE_ISO_C10, "iso_c10", }, { WIA_PAGE_JIS_B0, "jis_b0", }, { WIA_PAGE_JIS_B1, "jis_b1", }, { WIA_PAGE_JIS_B2, "jis_b2", }, { WIA_PAGE_JIS_B3, "jis_b3", }, { WIA_PAGE_JIS_B4, "jis_b4", }, { WIA_PAGE_JIS_B5, "jis_b5", }, { WIA_PAGE_JIS_B6, "jis_b6", }, { WIA_PAGE_JIS_B7, "jis_b7", }, { WIA_PAGE_JIS_B8, "jis_b8", }, { WIA_PAGE_JIS_B9, "jis_b9", }, { WIA_PAGE_JIS_B10, "jis_b10", }, { WIA_PAGE_JIS_2A, "jis_2a", }, { WIA_PAGE_JIS_4A, "jis_4a", }, { WIA_PAGE_DIN_2B, "din_2b", }, { WIA_PAGE_DIN_4B, "din_4b", }, { WIA_PAGE_AUTO, "auto", }, { WIA_PAGE_CUSTOM_BASE, "custom_base", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_preview[] = { { WIA_FINAL_SCAN, "final_scan", }, { WIA_PREVIEW_SCAN, "preview_scan", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_sheet_feeder_registration[] = { { LEFT_JUSTIFIED, "left_justified", }, { CENTERED, "centered", }, { RIGHT_JUSTIFIED, "right_justified", }, { -1, NULL }, }; static const struct wia_prop_int g_possible_show_preview_control[] = { { WIA_SHOW_PREVIEW_CONTROL, "show_preview_control", }, { WIA_DONT_SHOW_PREVIEW_CONTROL, "dont_show_preview_control", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_vertical_bed_registration[] = { { TOP_JUSTIFIED, "top_justified", }, { CENTERED, "centered", }, { BOTTOM_JUSTIFIED, "bottom_justified", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_auto_deskew[] = { { WIA_AUTO_DESKEW_ON, "deskew_on", }, { WIA_AUTO_DESKEW_OFF, "deskew_off", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_intent[] = { { WIA_INTENT_NONE, "none", }, { WIA_INTENT_IMAGE_TYPE_COLOR, "image_type_color", }, { WIA_INTENT_IMAGE_TYPE_GRAYSCALE, "image_type_grayscale", }, { WIA_INTENT_IMAGE_TYPE_TEXT, "image_type_text", }, { WIA_INTENT_IMAGE_TYPE_MASK, "image_type_mask", }, { WIA_INTENT_MINIMIZE_SIZE, "minimize_size", }, { WIA_INTENT_MAXIMIZE_QUALITY, "maximize_quality", }, { WIA_INTENT_SIZE_MASK, "size_mask", }, { WIA_INTENT_BEST_PREVIEW, "best_preview", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_film_scan_mode[] = { { WIA_FILM_COLOR_SLIDE, "color_slide", }, { WIA_FILM_COLOR_NEGATIVE, "color_negative", }, { WIA_FILM_BW_NEGATIVE, "bw_negative", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_lamp[] = { { WIA_LAMP_ON, "on", }, { WIA_LAMP_OFF, "off", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_photometric_interp[] = { { WIA_PHOTO_WHITE_0, "white_0", }, // WHITE is 0, and BLACK is 1 { WIA_PHOTO_WHITE_1, "white_1", }, // WHITE is 1, and BLACK is 0 { -1, NULL, }, }; static const struct wia_prop_int g_possible_preview_type[] = { { WIA_ADVANCED_PREVIEW, "advanced", }, { WIA_BASIC_PREVIEW, "basic", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_rotation[] = { { PORTRAIT, "portrait", }, { LANDSCAPE, "landscape", }, { ROT180, "rot180", }, { ROT270, "rot270", }, { -1, NULL, }, }; static const struct wia_prop_int g_possible_segmentation[] = { { WIA_USE_SEGMENTATION_FILTER, "true", }, { WIA_DONT_USE_SEGMENTATION_FILTER, "false", }, { -1, NULL, }, }; static PyObject *get_possible_values_int(const struct wia_property*); static PyObject *get_possible_values_clsid(const struct wia_property*); static PyObject *get_possible_values_none(const struct wia_property*); static const struct wia_property _g_wia_all_properties[] = { { WIA_DPA_CONNECT_STATUS, VT_I4, "connect_status", 0, g_possible_connect_status, get_possible_values_int, }, { WIA_DPA_DEVICE_TIME, VT_VECTOR | VT_UI2, "device_time", 0, NULL, get_possible_values_none, }, { WIA_DPA_FIRMWARE_VERSION, VT_BSTR, "firmware_version", 0, NULL, get_possible_values_none, }, { WIA_IPA_ACCESS_RIGHTS, VT_I4, "access_rights", 1, g_possible_access_rights, get_possible_values_int, }, { WIA_IPA_BITS_PER_CHANNEL, VT_I4, "bits_per_channel", 0, NULL, get_possible_values_none, }, { WIA_IPA_BUFFER_SIZE, VT_I4, "buffer_size", 0, NULL, get_possible_values_none, }, { WIA_IPA_BYTES_PER_LINE, VT_I4, "bytes_per_line", 0, NULL, get_possible_values_none, }, { WIA_IPA_CHANNELS_PER_PIXEL, VT_I4, "channels_per_pixel", 0, NULL, get_possible_values_none, }, { WIA_IPA_COLOR_PROFILE, VT_I4, "color_profile", 0, NULL, get_possible_values_none, }, { WIA_IPA_COMPRESSION, VT_I4, "compression", 1, g_possible_compression, get_possible_values_int, }, { WIA_IPA_DATATYPE, VT_I4, "datatype", 1, g_possible_datatype, get_possible_values_int, }, { WIA_IPA_DEPTH, VT_I4, "depth", 1, NULL, get_possible_values_none, }, { WIA_IPA_FILENAME_EXTENSION, VT_BSTR, "filename_extension", 0, NULL, get_possible_values_none, }, { WIA_IPA_FORMAT, VT_CLSID, "format", 1, g_possible_format, get_possible_values_clsid, }, { WIA_IPA_FULL_ITEM_NAME, VT_BSTR, "full_item_name", 0, NULL, get_possible_values_none, }, { WIA_IPA_GAMMA_CURVES, VT_I4, "gamma_curves", 0, NULL, get_possible_values_none, }, { WIA_IPA_ICM_PROFILE_NAME, VT_BSTR, "icm_profile_name", 0, NULL, get_possible_values_none, }, { WIA_IPA_ITEM_CATEGORY, VT_CLSID, "item_category", 0, g_possible_item_category, get_possible_values_clsid, }, { WIA_IPA_ITEM_FLAGS, VT_I4, "item_flags", 0, g_possible_item_flags, get_possible_values_int, }, { WIA_IPA_ITEM_NAME, VT_BSTR, "item_name", 0, NULL, get_possible_values_none, }, { WIA_IPA_ITEM_SIZE, VT_I4, "item_size", 0, NULL, get_possible_values_none, }, { WIA_IPA_ITEM_TIME, VT_UI2 | VT_VECTOR, "item_time", 1, NULL, get_possible_values_none, }, { WIA_IPA_ITEMS_STORED, VT_I4, "items_stored", 1, NULL, get_possible_values_none, }, { WIA_IPA_MIN_BUFFER_SIZE, VT_I4, "buffer_size", 0, NULL, get_possible_values_none, }, { WIA_IPA_NUMBER_OF_LINES, VT_I4, "number_of_lines", 0, NULL, get_possible_values_none, }, { WIA_IPA_PIXELS_PER_LINE, VT_I4, "pixels_per_line", 0, NULL, get_possible_values_none, }, { WIA_IPA_PLANAR, VT_I4, "planar", 1, g_possible_planar, get_possible_values_int, }, { WIA_IPA_PREFERRED_FORMAT, VT_CLSID, "preferred_format", 0, g_possible_format, get_possible_values_clsid, }, { WIA_IPA_PROP_STREAM_COMPAT_ID, VT_CLSID, "prop_stream_compat_id", 0, NULL, get_possible_values_clsid, }, { WIA_IPA_RAW_BITS_PER_CHANNEL, VT_UI1 | VT_VECTOR, "raw_bits_per_channel", 0, NULL, get_possible_values_none, }, { WIA_IPA_REGION_TYPE, VT_I4, "region_type", 0, NULL, get_possible_values_none, }, { WIA_IPA_SUPPRESS_PROPERTY_PAGE, VT_I4, "suppress_property_page", 0, g_possible_suppress_property_page, get_possible_values_int, }, { WIA_IPA_TYMED, VT_I4, "tymed", 1, g_possible_tymed, get_possible_values_int, }, { WIA_IPA_UPLOAD_ITEM_SIZE, VT_I4, "upload_item_size", 1, NULL, get_possible_values_none, }, { WIA_DIP_DEV_ID, VT_BSTR, "dev_id", 0, NULL, get_possible_values_none, }, { WIA_DIP_VEND_DESC, VT_BSTR, "vend_desc", 0, NULL, get_possible_values_none, }, { WIA_DIP_DEV_DESC, VT_BSTR, "dev_desc", 0, NULL, get_possible_values_none, }, { WIA_DIP_DEV_TYPE, VT_I4, "dev_type", 0, g_possible_dev_type, get_possible_values_int, }, { WIA_DIP_PORT_NAME, VT_BSTR, "port_name", 0, NULL, get_possible_values_none, }, { WIA_DIP_DEV_NAME, VT_BSTR, "dev_name", 0, NULL, get_possible_values_none, }, { WIA_DIP_SERVER_NAME, VT_BSTR, "server_name", 0, NULL, get_possible_values_none, }, { WIA_DIP_REMOTE_DEV_ID, VT_BSTR, "remote_dev_id", 0, NULL, get_possible_values_none, }, { WIA_DIP_UI_CLSID, VT_BSTR, "ui_clsid", 0, NULL, get_possible_values_none, }, { WIA_DIP_HW_CONFIG, VT_I4, "hw_config", 0, g_possible_hw_config, get_possible_values_int, }, { WIA_DIP_BAUDRATE, VT_BSTR, "baudrate", 0, NULL, get_possible_values_none, }, { WIA_DIP_STI_GEN_CAPABILITIES, VT_I4, "sti_gen_capabilities", 0, NULL, get_possible_values_none, }, { WIA_DIP_WIA_VERSION, VT_BSTR, "wia_version", 0, NULL, get_possible_values_none, }, { WIA_DIP_DRIVER_VERSION, VT_BSTR, "driver_version", 0, NULL, get_possible_values_none, }, { WIA_DIP_PNP_ID, VT_BSTR, "pnp_id", 0, NULL, get_possible_values_none, }, { WIA_DIP_STI_DRIVER_VERSION, VT_BSTR, "sti_driver_version", 0, NULL, get_possible_values_none, }, { WIA_DPS_DEVICE_ID, VT_BSTR, "device_id", 0, NULL, get_possible_values_none, }, { WIA_DPS_DOCUMENT_HANDLING_CAPABILITIES, VT_I4, "document_handling_capabilities", 0, g_possible_document_handling_capabilities, get_possible_values_int, }, { WIA_DPS_DOCUMENT_HANDLING_SELECT, VT_I4, "document_handling_select", 1, g_possible_document_handling_select, get_possible_values_int, }, { WIA_DPS_DOCUMENT_HANDLING_STATUS, VT_I4, "document_handling_status", 0, g_possible_document_handling_status, get_possible_values_int, }, { WIA_DPS_ENDORSER_CHARACTERS, VT_BSTR, "endorser_characters", 0, NULL, get_possible_values_none, }, { WIA_DPS_ENDORSER_STRING, VT_BSTR, "endorser_string", 1, NULL, get_possible_values_none, }, { WIA_DPS_GLOBAL_IDENTITY, VT_BSTR, "global_identity", 0, NULL, get_possible_values_none, }, { WIA_DPS_HORIZONTAL_BED_REGISTRATION, VT_I4, "horizontal_bed_registration", 0, g_possible_horizontal_bed_registration, get_possible_values_int, }, { WIA_DPS_HORIZONTAL_BED_SIZE, VT_I4, "horizontal_bed_size", 0, NULL, get_possible_values_none, }, { WIA_DPS_HORIZONTAL_SHEET_FEED_SIZE, VT_I4, "horizontal_sheet_feed_size", 0, NULL, get_possible_values_none, }, { WIA_DPS_MAX_SCAN_TIME, VT_I4, "max_scan_time", 0, NULL, get_possible_values_none, }, { WIA_DPS_MIN_HORIZONTAL_SHEET_FEED_SIZE, VT_I4, "min_horizontal_sheet_feed_size", 0, NULL, get_possible_values_none, }, { WIA_DPS_MIN_VERTICAL_SHEET_FEED_SIZE, VT_I4, "min_vertical_sheet_feed_size", 0, NULL, get_possible_values_none, }, { WIA_DPS_OPTICAL_XRES, VT_I4, "optical_xres", 0, NULL, get_possible_values_none, }, { WIA_DPS_OPTICAL_YRES, VT_I4, "optical_yres", 0, NULL, get_possible_values_none, }, // TODO(JFlesch): Visual C++ says WIA_DPS_ORIENTATION doesn't exist ?! //{ // WIA_DPS_ORIENTATION, VT_I4, "orientation", 1, g_possible_orientation, get_possible_values_int, //}, { WIA_DPS_PAD_COLOR, VT_UI1 | VT_VECTOR, "pad_color", 0, NULL, get_possible_values_none, }, { WIA_DPS_PAGE_HEIGHT, VT_I4, "page_height", 0, NULL, get_possible_values_none, }, { WIA_DPS_PAGE_SIZE, VT_I4, "page_size", 1, g_possible_page_size, get_possible_values_int, }, { WIA_DPS_PAGE_WIDTH, VT_I4, "page_width", 0, NULL, get_possible_values_none, }, { WIA_DPS_PAGES, VT_I4, "pages", 1, NULL, get_possible_values_none, }, { WIA_DPS_PLATEN_COLOR, VT_UI1 | VT_VECTOR, "platen_color", 0, NULL, get_possible_values_none, }, { WIA_DPS_PREVIEW, VT_I4, "preview", 1, g_possible_preview, get_possible_values_int, }, { WIA_DPS_SCAN_AHEAD_PAGES, VT_I4, "scan_ahead_pages", 1, NULL, get_possible_values_none, }, { WIA_DPS_SCAN_AVAILABLE_ITEM, VT_I4, "scan_available_item", 1, NULL, get_possible_values_none, }, { WIA_DPS_SERVICE_ID, VT_BSTR, "service_id", 0, NULL, get_possible_values_none, }, { WIA_DPS_SHEET_FEEDER_REGISTRATION, VT_I4, "sheet_feeder_registration", 0, g_possible_sheet_feeder_registration, get_possible_values_int, }, { WIA_DPS_SHOW_PREVIEW_CONTROL, VT_I4, "show_preview_control", 0, g_possible_show_preview_control, get_possible_values_int, }, { WIA_DPS_USER_NAME, VT_BSTR, "user_name", 0, NULL, get_possible_values_none, }, { WIA_DPS_VERTICAL_BED_REGISTRATION, VT_I4, "vertical_bed_registration", 0, g_possible_vertical_bed_registration, get_possible_values_int, }, { WIA_DPS_VERTICAL_BED_SIZE, VT_I4, "vertical_bed_size", 0, NULL, get_possible_values_none, }, { WIA_DPS_VERTICAL_SHEET_FEED_SIZE, VT_I4, "vertical_sheet_feed_size", 0, NULL, get_possible_values_none, }, { WIA_IPS_AUTO_DESKEW, VT_I4, "auto_deskew", 1, g_possible_auto_deskew, get_possible_values_int, }, { WIA_IPS_BRIGHTNESS, VT_I4, "brightness", 1, NULL, get_possible_values_none, }, { WIA_IPS_CONTRAST, VT_I4, "contrast", 1, NULL, get_possible_values_none, }, { WIA_IPS_CUR_INTENT, VT_I4, "current_intent", 1, g_possible_intent, get_possible_values_int, }, { WIA_IPS_DESKEW_X, VT_I4, "deskew_x", 1, NULL, get_possible_values_none, }, { WIA_IPS_DESKEW_Y, VT_I4, "deskew_y", 1, NULL, get_possible_values_none, }, { WIA_IPS_DOCUMENT_HANDLING_SELECT, VT_I4, "document_handling_select", 1, g_possible_document_handling_select, get_possible_values_int, }, { WIA_IPS_FILM_NODE_NAME, VT_BSTR, "film_node_name", 0, NULL, get_possible_values_none, }, { WIA_IPS_FILM_SCAN_MODE, VT_I4, "file_scan_mode", 1, g_possible_film_scan_mode, get_possible_values_int, }, { WIA_IPS_INVERT, VT_I4, "invert", 0, NULL, get_possible_values_none, }, { WIA_IPA_ITEMS_STORED, VT_I4, "items_stored", 0, NULL, get_possible_values_none, }, { WIA_IPS_LAMP, VT_I4, "lamp", 1, g_possible_lamp, get_possible_values_int, }, { WIA_IPS_LAMP_AUTO_OFF, VT_UI4, "lamp_auto_off", 1, NULL, get_possible_values_none, }, { WIA_IPS_MAX_HORIZONTAL_SIZE, VT_I4, "max_horizontal_size", 0, NULL, get_possible_values_none, }, { WIA_IPS_MAX_VERTICAL_SIZE, VT_I4, "max_vertical_size", 0, NULL, get_possible_values_none, }, { WIA_IPS_MIN_HORIZONTAL_SIZE, VT_I4, "min_horizontal_size", 0, NULL, get_possible_values_none, }, { WIA_IPS_MIN_VERTICAL_SIZE, VT_I4, "min_vertical_size", 0, NULL, get_possible_values_none, }, { WIA_IPS_MIRROR, VT_I4, "mirror", 0, NULL, get_possible_values_none, }, { WIA_IPS_OPTICAL_XRES, VT_I4, "optical_xres", 0, NULL, get_possible_values_none, }, { WIA_IPS_OPTICAL_YRES, VT_I4, "optical_yres", 0, NULL, get_possible_values_none, }, { WIA_IPS_ORIENTATION, VT_I4, "orientation", 1, g_possible_orientation, get_possible_values_int, }, { WIA_IPS_PAGE_SIZE, VT_I4, "page_size", 1, g_possible_page_size, get_possible_values_int, }, { WIA_IPS_PAGE_HEIGHT, VT_I4, "page_height", 0, NULL, get_possible_values_none, }, { WIA_IPS_PAGE_WIDTH, VT_I4, "page_width", 0, NULL, get_possible_values_none, }, { WIA_IPS_PAGES, VT_I4, "pages", 1, NULL, get_possible_values_none, }, { WIA_IPS_PHOTOMETRIC_INTERP, VT_I4, "photometric_interp", 1, g_possible_photometric_interp, get_possible_values_int, }, { WIA_IPS_PREVIEW, VT_I4, "preview", 1, g_possible_preview, get_possible_values_int, }, { WIA_IPS_PREVIEW_TYPE, VT_I4, "preview_type", 0, g_possible_preview_type, get_possible_values_int, }, { WIA_IPS_ROTATION, VT_I4, "rotation", 1, g_possible_rotation, get_possible_values_int, }, { WIA_IPS_SEGMENTATION, VT_I4, "segmentation", 0, g_possible_segmentation, get_possible_values_int, }, { WIA_IPS_SHEET_FEEDER_REGISTRATION, VT_I4, "sheet_feeder_registration", 0, g_possible_sheet_feeder_registration, get_possible_values_int, }, { WIA_IPS_SHOW_PREVIEW_CONTROL, VT_I4, "show_preview_control", 0, g_possible_show_preview_control, get_possible_values_int, }, { WIA_IPS_SUPPORTS_CHILD_ITEM_CREATION, VT_I4, "supportes_child_item_creation", 0, NULL, get_possible_values_none, }, { WIA_IPS_THRESHOLD, VT_I4, "threshold", 1, NULL, get_possible_values_none, }, { WIA_IPS_TRANSFER_CAPABILITIES, VT_I4, "transfer_capabilities", 0, NULL, get_possible_values_none, }, { WIA_IPA_UPLOAD_ITEM_SIZE, VT_I4, "upload_item_size", 1, NULL, get_possible_values_none, }, { WIA_IPS_WARM_UP_TIME, VT_I4, "warm_up_time", 0, NULL, get_possible_values_none, }, { WIA_IPS_XEXTENT, VT_I4, "xextent", 1, NULL, get_possible_values_none, }, { WIA_IPS_XPOS, VT_I4, "xpos", 1, NULL, get_possible_values_none, }, { WIA_IPS_XRES, VT_I4, "xres", 1, NULL, get_possible_values_none, }, { WIA_IPS_XSCALING, VT_I4, "xscaling", 1, NULL, get_possible_values_none, }, { WIA_IPS_YEXTENT, VT_I4, "yextent", 1, NULL, get_possible_values_none, }, { WIA_IPS_YPOS, VT_I4, "ypos", 1, NULL, get_possible_values_none, }, { WIA_IPS_YRES, VT_I4, "yres", 1, NULL, get_possible_values_none, }, { WIA_IPS_YSCALING, VT_I4, "yscaling", 1, NULL, get_possible_values_none, }, { 0 }, }; const struct wia_property *g_wia_all_properties = _g_wia_all_properties; static PyObject *get_possible_values_int(const struct wia_property *propspec) { struct wia_prop_int *values = (struct wia_prop_int *)propspec->possible_values; PyObject *list; PyObject *value; list = PyList_New(0); while(values->name != NULL) { value = PyUnicode_FromString(values->name); PyList_Append(list, value); values++; } return list; } static PyObject *get_possible_values_clsid(const struct wia_property *propspec) { struct wia_prop_clsid *values = (struct wia_prop_clsid *)propspec->possible_values; PyObject *list; PyObject *value; list = PyList_New(0); while(values->name != NULL) { value = PyUnicode_FromString(values->name); PyList_Append(list, value); values++; } return list; } static PyObject *get_possible_values_none(const struct wia_property*) { Py_RETURN_NONE; } PyObject *int_to_pyobject(const struct wia_property *property, long value) { const struct wia_prop_int *values; int i; PyObject *out; if (property->possible_values == NULL) return PyLong_FromLong(value); values = (const struct wia_prop_int *)property->possible_values; for (i = 0 ; values[i].name != NULL ; i++) { if (values[i].value == value) return PyUnicode_FromString(values[i].name); } char str[256]; str[0] = '\0'; str[sizeof(str)-1] = '\0'; for (i = 0 ; values[i].name != NULL ; i++) { if (value & values[i].value) { if (str[0] == '\0') strncpy_s(str, values[i].name, sizeof(str) - 1); else { strncat_s(str, ",", sizeof(str) - 1); strncat_s(str, values[i].name, sizeof(str) - 1); } } } if (str[0] != '\0') { out = PyUnicode_FromString(str); return out; } return PyLong_FromLong(value); } PyObject *clsid_to_pyobject(const struct wia_property *property, CLSID value) { const struct wia_prop_clsid *values; int i; assert(property->possible_values != NULL); values = (const struct wia_prop_clsid *)property->possible_values; for (i = 0 ; NULL != values[i].name ; i++) { if (values[i].value == value) return PyUnicode_FromString(values[i].name); } wia_log(WIA_WARNING, "Got unknown clsid from driver (property=%s)", property->name); return NULL; } PyObject *int_vector_to_pyobject_list(const CAL *values) { PyObject *out = PyList_New(values->cElems); PyObject *val; ULONG i; for (i = 0 ; i < values->cElems ; i++) { val = PyLong_FromLong(values->pElems[i]); PyList_SetItem(out, i, val); } return out; } PyObject *int_vector_to_pyobject_tuple(const CAL *values) { PyObject *out; PyObject *val; int i; if (values->cElems < 2) { wia_log(WIA_WARNING, "Got a range with not enough elements ! (%d)", values->cElems); return NULL; } out = PyTuple_New(values->cElems); for (i = 0 ; i < values->cElems ; i++) { val = PyLong_FromLong(values->pElems[i]); PyTuple_SetItem(out, i, val); } return out; } PyObject *str_vector_to_pyobject(const CABSTR *values) { PyObject *out = PyList_New(values->cElems); PyObject *val; ULONG i; for (i = 0 ; i < values->cElems ; i++) { //val = PyLong_FromLong(values->pElems[i]); val = PyUnicode_FromWideChar(SysAllocString(values->pElems[i]), SysStringLen(values->pElems[i])); PyList_SetItem(out, i, val); } return out; } int pyobject_to_int(const struct wia_property *property_spec, PyObject *pyvalue, int fail_value) { char str[256]; char *pstr, *nstr; int has_match; int val; int i; const struct wia_prop_int *str2int; if (PyLong_Check(pyvalue)) return PyLong_AsLong(pyvalue); str2int = (const struct wia_prop_int *)property_spec->possible_values; if (PyUnicode_Check(pyvalue) && str2int != NULL) { // parse string strncpy_s(str, PyUnicode_AsUTF8(pyvalue), sizeof(str)); val = 0; has_match = 0; pstr = str; while(pstr) { nstr = strchr(pstr, ','); if (nstr) { nstr[0] = '\0'; nstr++; } for (i = 0 ; str2int[i].name != NULL ; i++) { if (strcmp(pstr, str2int[i].name) == 0) { val |= str2int[i].value; has_match = 1; break; } } pstr = nstr; } if (has_match) { return val; } } wia_log(WIA_WARNING, "set_property(%s): Failed to parse value", property_spec->name); return fail_value; } int pyobject_to_clsid(const struct wia_property *property_spec, PyObject *pyvalue, CLSID **out) { const struct wia_prop_clsid *str2clsid; int i; const char *value; if (!PyUnicode_Check(pyvalue)) { wia_log(WIA_WARNING, "set_property(%s): Invalid type for clsid property", property_spec->name); return 0; } str2clsid = (const struct wia_prop_clsid *)property_spec->possible_values; assert(str2clsid != NULL); value = PyUnicode_AsUTF8(pyvalue); for (i = 0 ; str2clsid[i].name != NULL ; i++) { if (strcmp(value, str2clsid[i].name) == 0) { *out = (CLSID *)&str2clsid[i].value; return 1; } } wia_log(WIA_WARNING, "set_property(%s): Invalid value for clsid property", property_spec->name); return 0; } pyinsane-2.0.13/pyinsane2/wia/properties.h000066400000000000000000000021141332615651100204610ustar00rootroot00000000000000#ifndef __PYINSANE_WIA_PROPERTIES_H #define __PYINSANE_WIA_PROPERTIES_H #include #include struct wia_prop_int { int value; const char *name; }; struct wia_prop_clsid { CLSID value; const char *name; }; struct wia_property { PROPID id; VARTYPE vartype; const char *name; // NULL == end of list int rw; const void *possible_values; // points to a (struct wia_prop_*) ; see vartype PyObject *(*get_possible_values)(const struct wia_property*); }; extern const struct wia_property *g_wia_all_properties; PyObject *int_to_pyobject(const struct wia_property *property, long value); PyObject *clsid_to_pyobject(const struct wia_property *property, CLSID value); PyObject *int_vector_to_pyobject_list(const CAL *values); PyObject *int_vector_to_pyobject_tuple(const CAL *values); PyObject *str_vector_to_pyobject(const CABSTR *values); int pyobject_to_int(const struct wia_property *property_spec, PyObject *pyvalue, int fail_value); int pyobject_to_clsid(const struct wia_property *property_spec, PyObject *pyvalue, CLSID **out); #endifpyinsane-2.0.13/pyinsane2/wia/rawapi.cpp000066400000000000000000000661541332615651100201210ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include #include #include "properties.h" #include "trace.h" #include "transfer.h" #include "util.h" #define WIA_PYCAPSULE_DEV_NAME "WIA device" #define WIA_PYCAPSULE_SRC_NAME "WIA source" #define WIA_PYCAPSULE_SCAN_NAME "WIA scan" struct wia_device { IWiaDevMgr2 *dev_manager; IWiaItem2 *device; }; enum wia_src_type { WIA_SRC_AUTO = 0, WIA_SRC_FLATBED, WIA_SRC_FEEDER, }; struct wia_source { wia_src_type type; struct wia_device *dev; IWiaItem2 *source; }; static PyObject *init(PyObject *, PyObject* args) { HRESULT hr; if (!PyArg_ParseTuple(args, "")) { return NULL; } if (!PyEval_ThreadsInitialized()) { wia_log(WIA_WARNING, "Python thread not yet initialized ?!"); PyEval_InitThreads(); } wia_log(WIA_INFO, "WIA->init()"); hr = CoInitialize(NULL); if (FAILED(hr)) { wia_log(WIA_WARNING, "CoInitialize() failed !"); wia_log_hresult(WIA_WARNING, hr); Py_RETURN_NONE; } Py_RETURN_NONE; } static HRESULT get_device_basic_infos(IWiaPropertyStorage *properties, PyObject **out_tuple) { PyObject *devid, *devname; PROPSPEC input[3] = {0}; PROPVARIANT output[3] = {0}; HRESULT hr; *out_tuple = NULL; input[0].ulKind = PRSPEC_PROPID; input[0].propid = WIA_DIP_DEV_ID; input[1].ulKind = PRSPEC_PROPID; input[1].propid = WIA_DIP_DEV_NAME; input[2].ulKind = PRSPEC_PROPID; input[2].propid = WIA_DIP_DEV_TYPE; hr = properties->ReadMultiple(3 /* nb_properties */, input, output); if (FAILED(hr)) { wia_log(WIA_WARNING, "WiaPropertyStorage->ReadMultiple() failed"); wia_log_hresult(WIA_WARNING, hr); return hr; } assert(output[0].vt == VT_BSTR); assert(output[1].vt == VT_BSTR); assert(output[2].vt == VT_I4); if (GET_STIDEVICE_TYPE(output[2].lVal) != StiDeviceTypeScanner) { CW2A dev_id_str(output[0].bstrVal); CW2A dev_name_str(output[0].bstrVal); wia_log(WIA_INFO, "[%s, %s] --> not a scanner --> ignored", (char *)dev_id_str, (char *)dev_name_str); *out_tuple = NULL; return S_OK; } devid = PyUnicode_FromWideChar(output[0].bstrVal, -1); devname = PyUnicode_FromWideChar(output[1].bstrVal, -1); *out_tuple = PyTuple_Pack(2, devid, devname); FreePropVariantArray(2, output); return S_OK; } static PyObject *get_devices(PyObject *, PyObject* args) { HRESULT hr; CComPtr wia_dev_manager; CComPtr wia_dev_info_enum; unsigned long nb_devices; int actual_nb_devices = 0; PyObject *dev_infos; PyObject *all_devs; if (!PyArg_ParseTuple(args, "")) { return NULL; } wia_log(WIA_INFO, "WIA->get_devices()"); // Create a connection to the local WIA device manager hr = wia_dev_manager.CoCreateInstance(CLSID_WiaDevMgr2); if (FAILED(hr)) { wia_log(WIA_WARNING, "CoCreateInstance failed"); wia_log_hresult(WIA_WARNING, hr); Py_RETURN_NONE; } hr = wia_dev_manager->EnumDeviceInfo(WIA_DEVINFO_ENUM_ALL, &wia_dev_info_enum); if (FAILED(hr)) { wia_log(WIA_WARNING, "WiaDevMgr->EnumDeviceInfo() failed"); wia_log_hresult(WIA_WARNING, hr); Py_RETURN_NONE; } // Get the numeber of WIA devices hr = wia_dev_info_enum->GetCount(&nb_devices); if (FAILED(hr)) { wia_log(WIA_WARNING, "GetCount() failed !"); wia_log_hresult(WIA_WARNING, hr); Py_RETURN_NONE; } all_devs = PyList_New(0); while (hr == S_OK) { IWiaPropertyStorage *properties = NULL; hr = wia_dev_info_enum->Next(1, &properties, NULL); if (hr != S_OK || properties == NULL) break; hr = get_device_basic_infos(properties, &dev_infos); if (FAILED(hr)) { break; } if (dev_infos == NULL) { // not a scanner continue; } actual_nb_devices++; properties->Release(); PyList_Append(all_devs, dev_infos); } wia_log(WIA_DEBUG, "%d devices found", actual_nb_devices); // wia_dev_info_enum->Release(); // TODO(Jflesch) ? return all_devs; } static void free_device(PyObject *device) { struct wia_device *wia_dev; wia_dev = (struct wia_device *)PyCapsule_GetPointer(device, WIA_PYCAPSULE_DEV_NAME); // TODO free(wia_dev); } static PyObject *open_device(PyObject *, PyObject *args) { char *devid; CComPtr wia_dev_manager; struct wia_device *dev; BSTR bstr_devid; HRESULT hr; USES_CONVERSION; if (!PyArg_ParseTuple(args, "s", &devid)) { return NULL; } wia_log(WIA_INFO, "Opening device(%s)", devid); hr = wia_dev_manager.CoCreateInstance(CLSID_WiaDevMgr2); if (FAILED(hr)) { wia_log(WIA_WARNING, "CoCreateInstance failed"); wia_log_hresult(WIA_WARNING, hr); Py_RETURN_NONE; } dev = (struct wia_device *)calloc(1, sizeof(struct wia_device)); dev->dev_manager = wia_dev_manager; bstr_devid = SysAllocString(A2W(devid)); hr = wia_dev_manager->CreateDevice(0, bstr_devid, &dev->device); if (FAILED(hr)) { wia_log(WIA_WARNING, "WiaDevMgr->CreateDevice() failed"); wia_log_hresult(WIA_WARNING, hr); free(dev); Py_RETURN_NONE; } wia_log(WIA_INFO, "Device(%s) opened", devid); return PyCapsule_New(dev, WIA_PYCAPSULE_DEV_NAME, free_device); } static void free_source(PyObject *source) { struct wia_source *wia_src; wia_src = (struct wia_source *)PyCapsule_GetPointer(source, WIA_PYCAPSULE_DEV_NAME); // TODO free(wia_src); } static PyObject *get_sources(PyObject *, PyObject *args) { struct wia_device *dev; IEnumWiaItem2 *enum_item; IWiaItem2 *child; PyObject *source_name; PyObject *capsule; PyObject *tuple; PyObject *all_sources; int nb_sources = 0; struct wia_source *source; PROPSPEC input[2] = {0}; PROPVARIANT output[2] = {0}; HRESULT hr; input[0].ulKind = PRSPEC_PROPID; input[0].propid = WIA_IPA_FULL_ITEM_NAME; input[1].ulKind = PRSPEC_PROPID; input[1].propid = WIA_IPA_ITEM_CATEGORY; wia_log(WIA_INFO, "Getting sources"); if (!PyArg_ParseTuple(args, "O", &capsule)) { wia_log(WIA_WARNING, "get_sources(): Invalid args"); return NULL; } if (!PyCapsule_CheckExact(capsule)) { wia_log(WIA_WARNING, "get_sources(): invalid argument type (not a pycapsule)"); Py_RETURN_NONE; } if ((dev = (struct wia_device *)PyCapsule_GetPointer(capsule, WIA_PYCAPSULE_DEV_NAME)) == NULL) { wia_log(WIA_WARNING, "get_sources(): invalid argument type"); Py_RETURN_NONE; } all_sources = PyList_New(0); hr = dev->device->EnumChildItems(NULL, &enum_item); while(hr == S_OK) { hr = enum_item->Next(1, &child, NULL); if (hr != S_OK) { wia_log(WIA_WARNING, "get_sources(): enum_item->next() failed"); wia_log_hresult(WIA_WARNING, hr); continue; } CComQIPtr child_properties(child); source = (struct wia_source *)calloc(2, sizeof(struct wia_source)); source->dev = dev; source->source = child; hr = child_properties->ReadMultiple(2 /* nb_properties */, input, output); if (FAILED(hr)) { wia_log(WIA_WARNING, "WiaPropertyStorage->ReadMultiple() failed"); wia_log_hresult(WIA_WARNING, hr); child->Release(); continue; } assert(output[0].vt == VT_BSTR); assert(output[1].vt == VT_CLSID); CW2A source_name_str(output[0].bstrVal); if (*output[1].puuid == WIA_CATEGORY_FINISHED_FILE || *output[1].puuid == WIA_CATEGORY_FOLDER || *output[1].puuid == WIA_CATEGORY_ROOT) { wia_log(WIA_WARNING, "Ignoring unmanaged source type for [%s]", (char *)source_name_str); free(source); continue; } else if (*output[1].puuid == WIA_CATEGORY_AUTO) { source->type = WIA_SRC_AUTO; wia_log(WIA_INFO, "Got source [%s] (auto)", (char *)source_name_str); } else if (*output[1].puuid == WIA_CATEGORY_FEEDER || *output[1].puuid == WIA_CATEGORY_FEEDER_BACK || *output[1].puuid == WIA_CATEGORY_FEEDER_FRONT) { source->type = WIA_SRC_FEEDER; wia_log(WIA_INFO, "Got source [%s] (feeder)", (char *)source_name_str); } else { source->type = WIA_SRC_FLATBED; wia_log(WIA_INFO, "Got source [%s] (assuming flatbed)", (char *)source_name_str); } source_name = PyUnicode_FromWideChar(output[0].bstrVal, -1); capsule = PyCapsule_New(source, WIA_PYCAPSULE_SRC_NAME, free_source); tuple = PyTuple_Pack(2, source_name, capsule); PyList_Append(all_sources, tuple); nb_sources++; Py_DECREF(tuple); } wia_log(WIA_INFO, "%d sources found", nb_sources); return all_sources; } static IWiaItem2 *capsule2item(PyObject *capsule) { struct wia_device *wia_dev; struct wia_source *wia_src; if (!PyCapsule_CheckExact(capsule)) { wia_log(WIA_WARNING, "invalid argument type (not a pycapsule)"); return NULL; } if (strcmp(PyCapsule_GetName(capsule), WIA_PYCAPSULE_DEV_NAME) == 0) { wia_dev = (struct wia_device *)PyCapsule_GetPointer(capsule, WIA_PYCAPSULE_DEV_NAME); if (wia_dev != NULL) return wia_dev->device; } if (strcmp(PyCapsule_GetName(capsule), WIA_PYCAPSULE_SRC_NAME) == 0) { wia_src = (struct wia_source *)PyCapsule_GetPointer(capsule, WIA_PYCAPSULE_SRC_NAME); if (wia_src != NULL) return wia_src->source; } wia_log(WIA_WARNING, "Invalid argument type (not a known pycapsule type)"); return NULL; } static PyObject *get_properties(PyObject *, PyObject *args) { PyObject *capsule; IWiaItem2 *item; PROPSPEC *input; PROPVARIANT *output; int i; int nb_properties; HRESULT hr; PyObject *all_props; PyObject *propname; PyObject *propvalue; PyObject *prop; PyObject *ro; PyObject *rw; PyObject *access_right; PyObject *possible_values; if (!PyArg_ParseTuple(args, "O", &capsule)) { wia_log(WIA_WARNING, "get_sources(): Invalid args"); return NULL; } wia_log(WIA_INFO, "Getting Properties"); item = capsule2item(capsule); if (item == NULL) { Py_RETURN_NONE; } for (nb_properties = 0 ; g_wia_all_properties[nb_properties].name != NULL ; nb_properties++) { } input = (PROPSPEC *)calloc(nb_properties, sizeof(PROPSPEC)); output = (PROPVARIANT *)calloc(nb_properties, sizeof(PROPVARIANT)); for (i = 0 ; i < nb_properties ; i++) { input[i].ulKind = PRSPEC_PROPID; input[i].propid = g_wia_all_properties[i].id; } CComQIPtr properties(item); hr = properties->ReadMultiple(nb_properties, input, output); if (FAILED(hr)) { wia_log(WIA_WARNING, "WiaPropertyStorage->ReadMultiple() failed"); wia_log_hresult(WIA_WARNING, hr); free(input); free(output); Py_RETURN_NONE; } free(input); ro = PyUnicode_FromString("ro"); rw = PyUnicode_FromString("rw"); all_props = PyList_New(0); for (i = 0 ; i < nb_properties ; i++) { if (output[i].vt == 0) continue; if (output[i].vt != g_wia_all_properties[i].vartype) { wia_log(WIA_WARNING, "A property has a type different from the one expected"); continue; } switch(output[i].vt) { case VT_I4: propvalue = int_to_pyobject(&g_wia_all_properties[i], output[i].lVal); break; case VT_UI4: propvalue = int_to_pyobject(&g_wia_all_properties[i], output[i].ulVal); break; case VT_VECTOR | VT_UI2: // TODO continue; case VT_UI1 | VT_VECTOR: // TODO continue; case VT_BSTR: propvalue = PyUnicode_FromWideChar(output[i].bstrVal, -1); break; case VT_CLSID: propvalue = clsid_to_pyobject(&g_wia_all_properties[i], *output[i].puuid); break; default: wia_log(WIA_WARNING, "Unknown var type"); assert(0); continue; } if (propvalue == NULL) continue; wia_log(WIA_DEBUG, "Got property [%s]", g_wia_all_properties[i].name); propname = PyUnicode_FromString(g_wia_all_properties[i].name); access_right = (g_wia_all_properties[i].rw ? rw : ro); Py_INCREF(access_right); // PyTuple_Pack steals the ref possible_values = g_wia_all_properties[i].get_possible_values(&g_wia_all_properties[i]); prop = PyTuple_Pack(4, propname, propvalue, access_right, possible_values); PyList_Append(all_props, prop); Py_DECREF(prop); } wia_log(WIA_DEBUG, "Got all properties"); free(output); return all_props; } static PyObject *get_constraints(PyObject *, PyObject *args) { PyObject *capsule; IWiaItem2 *item; PROPSPEC *input; ULONG *prop_attributes; PROPVARIANT *output; int i; int nb_properties; HRESULT hr; PyObject *all_constraints; PyObject *constraint; PyObject *propname; PyObject *prop; if (!PyArg_ParseTuple(args, "O", &capsule)) { wia_log(WIA_WARNING, "get_sources(): Invalid args"); return NULL; } wia_log(WIA_INFO, "Getting constraints"); item = capsule2item(capsule); if (item == NULL) { Py_RETURN_NONE; } for (nb_properties = 0 ; g_wia_all_properties[nb_properties].name != NULL ; nb_properties++) { } input = (PROPSPEC *)calloc(nb_properties, sizeof(PROPSPEC)); prop_attributes = (ULONG *)calloc(nb_properties, sizeof(ULONG)); output = (PROPVARIANT *)calloc(nb_properties, sizeof(PROPVARIANT)); for (i = 0 ; i < nb_properties ; i++) { input[i].ulKind = PRSPEC_PROPID; input[i].propid = g_wia_all_properties[i].id; } CComQIPtr properties(item); hr = properties->GetPropertyAttributes(nb_properties, input, prop_attributes, output); if (FAILED(hr)) { wia_log(WIA_WARNING, "WiaPropertyStorage->GetPropertyAttribute() failed. Will use defaults"); wia_log_hresult(WIA_WARNING, hr); free(input); free(prop_attributes); free(output); Py_RETURN_NONE; } free(input); all_constraints = PyList_New(0); for (i = 0 ; i < nb_properties ; i++) { if (output[i].vt == 0) { continue; } constraint = NULL; switch(output[i].vt) { case VT_I4: constraint = int_to_pyobject(&g_wia_all_properties[i], output[i].lVal); break; case VT_UI4: constraint = int_to_pyobject(&g_wia_all_properties[i], output[i].ulVal); break; case VT_VECTOR | VT_UI4: /* FALLTHROUGH */ case VT_VECTOR | VT_I4: if (prop_attributes[i] & WIA_PROP_RANGE) constraint = int_vector_to_pyobject_tuple(&output[i].cal); else constraint = int_vector_to_pyobject_list(&output[i].cal); break; case VT_VECTOR | VT_UI2: wia_log(WIA_WARNING, "Got VECTOR|UI2 as constraint. Not supported yet"); // TODO continue; case VT_UI1 | VT_VECTOR: wia_log(WIA_WARNING, "Got VECTOR|UI1 as constraint. Not supported yet"); // TODO continue; case VT_BSTR: constraint = PyUnicode_FromWideChar( SysAllocString(output[i].bstrVal), SysStringLen(output[i].bstrVal) ); break; case VT_BSTR | VT_VECTOR: constraint = str_vector_to_pyobject(&output[i].cabstr); break; case VT_CLSID: constraint = clsid_to_pyobject(&g_wia_all_properties[i], *output[i].puuid); break; default: wia_log(WIA_WARNING, "Unknown var type for constraint"); continue; } if (constraint == NULL) { wia_log(WIA_WARNING, "Failed to parse constraint of [%s]", g_wia_all_properties[i].name); continue; } propname = PyUnicode_FromString(g_wia_all_properties[i].name); prop = PyTuple_Pack(2, propname, constraint); PyList_Append(all_constraints, prop); Py_DECREF(prop); } wia_log(WIA_DEBUG, "Got all constraints"); free(prop_attributes); free(output); return all_constraints; } static int _set_property(IWiaItem2 *item, const struct wia_property *property_spec, PyObject *pyvalue) { HRESULT hr; PROPSPEC propspec; PROPVARIANT propvalue; propspec.ulKind = PRSPEC_PROPID; propspec.propid = property_spec->id; PropVariantInit(&propvalue); propvalue.vt = property_spec->vartype; switch(property_spec->vartype) { case VT_I4: propvalue.lVal = pyobject_to_int(property_spec, pyvalue, -1); if (propvalue.lVal == -1) { wia_log(WIA_WARNING, "Setting property [%s]: pyobject_to_int() failed", property_spec->name); return 0; } break; case VT_UI4: propvalue.vt = VT_I4; propvalue.lVal = pyobject_to_int(property_spec, pyvalue, -1); if (propvalue.lVal == -1) { wia_log(WIA_WARNING, "Setting property [%s]: pyobject_to_int() failed", property_spec->name); return 0; } break; case VT_VECTOR | VT_UI2: case VT_UI1 | VT_VECTOR: wia_log(WIA_WARNING, "Setting property [%s]: Vector not supported yet", property_spec->name); // TODO return 0; case VT_BSTR: wia_log(WIA_WARNING, "Setting property [%s]: String not supported yet", property_spec->name); // TODO return 0; case VT_CLSID: if (!pyobject_to_clsid(property_spec, pyvalue, &propvalue.puuid)) { wia_log(WIA_WARNING, "Setting property [%s]: pyobject_to_clsid() failed", property_spec->name); return 0; } break; default: wia_log(WIA_WARNING, "Setting property [%s]: Unknown var type", property_spec->name); assert(0); return 0; } CComQIPtr properties(item); hr = properties->WriteMultiple(1, &propspec, &propvalue, WIA_IPA_FIRST); if (FAILED(hr)) { wia_log(WIA_WARNING, "Setting property [%s]: properties->WriteMultiple() failed", property_spec->name); wia_log_hresult(WIA_WARNING, hr); return 0; } return 1; } static PyObject *set_property(PyObject *, PyObject *args) { PyObject *capsule; PyObject *py_propname; PyObject *py_propvalue; IWiaItem2 *item; const char *propname; int i; if (!PyArg_ParseTuple(args, "OOO", &capsule, &py_propname, &py_propvalue)) { wia_log(WIA_WARNING, "get_sources(): Invalid args"); return NULL; } item = capsule2item(capsule); if (item == NULL) Py_RETURN_FALSE; propname = PyUnicode_AsUTF8(py_propname); wia_log(WIA_INFO, "Setting value of property [%s]", propname); for (i = 0 ; g_wia_all_properties[i].name != NULL ; i++) { if (strcmp(g_wia_all_properties[i].name, propname) != 0) continue; if (!_set_property(item, &g_wia_all_properties[i], py_propvalue)) { Py_RETURN_FALSE; } Py_RETURN_TRUE; } wia_log(WIA_WARNING, "Pyinsame: set_property(): Property not found"); Py_RETURN_FALSE; } struct wia_scan { struct wia_source *src; IWiaTransfer *transfer; PyinsaneWiaTransferCallback *callbacks; PyinsaneImageStream *current_stream; }; static void end_scan(PyObject *capsule) { struct wia_scan *scan; wia_log(WIA_DEBUG, "End of scan"); scan = (struct wia_scan *)PyCapsule_GetPointer(capsule, WIA_PYCAPSULE_SCAN_NAME); if (scan == NULL) return; scan->transfer->Release(); delete scan->callbacks; free(scan); } struct download { HANDLE mutex; // because it seems that the callbacks can be called from many threads ! Py_buffer buffer; PyObject *get_data_cb; PyObject *end_of_page_cb; PyObject *end_of_scan_cb; PyThreadState *_save; }; static void get_data_wrapper(const void *data, int nb_bytes, void *cb_data) { struct download *download = (struct download *)cb_data; const char *cdata = (const char *)data; // because Visual C++ says "unknown size" for (void) elements. int nb; PyObject *arglist; PyObject *res; wia_log(WIA_DEBUG, "get_data_wrapper()"); WaitForSingleObject(download->mutex, 0); PyEval_RestoreThread(download->_save); wia_log_set_pythread_state(download->mutex, NULL); for ( ; nb_bytes > 0 ; nb_bytes -= nb, cdata += nb) { nb = (int)WIA_MIN(nb_bytes, download->buffer.len); memcpy(download->buffer.buf, cdata, nb); arglist = Py_BuildValue("(i)", nb); res = PyEval_CallObject(download->get_data_cb, arglist); if (res == NULL) { wia_log(WIA_WARNING, "Got exception from callback download->get_data_cb !"); } Py_XDECREF(res); } wia_log_set_pythread_state(download->mutex, &download->_save); download->_save = PyEval_SaveThread(); ReleaseMutex(download->mutex); } static void end_of_page_wrapper(void *cb_data) { struct download *download = (struct download *)cb_data; PyObject *res; wia_log(WIA_DEBUG, "end_of_page_wrapper()"); WaitForSingleObject(download->mutex, 0); PyEval_RestoreThread(download->_save); wia_log_set_pythread_state(download->mutex, NULL); res = PyEval_CallObject(download->end_of_page_cb, NULL); if (res == NULL) { wia_log(WIA_WARNING, "Got exception from callback download->end_of_page_cb !"); } Py_XDECREF(res); wia_log_set_pythread_state(download->mutex, &download->_save); download->_save = PyEval_SaveThread(); ReleaseMutex(download->mutex); } static void end_of_scan_wrapper(void *cb_data) { struct download *download = (struct download *)cb_data; PyObject *res; wia_log(WIA_DEBUG, "end_of_scan_wrapper()"); WaitForSingleObject(download->mutex, 0); PyEval_RestoreThread(download->_save); wia_log_set_pythread_state(download->mutex, NULL); res = PyEval_CallObject(download->end_of_scan_cb, NULL); if (res == NULL) { wia_log(WIA_WARNING, "Got exception from callback download->end_of_scan_cb !"); } Py_XDECREF(res); wia_log_set_pythread_state(download->mutex, &download->_save); download->_save = PyEval_SaveThread(); ReleaseMutex(download->mutex); } static PyObject *download(PyObject *, PyObject *args) { PyObject *capsule; struct download dl_data = { 0 }; struct wia_scan scan; HRESULT hr; IWiaItem2 *src; if (!PyArg_ParseTuple(args, "OOOOy*", &capsule, &dl_data.get_data_cb, &dl_data.end_of_page_cb, &dl_data.end_of_scan_cb, &dl_data.buffer )) { wia_log(WIA_WARNING, "download(): Invalid args"); Py_RETURN_FALSE; } wia_log(WIA_DEBUG, "download()"); /* With most scanners, scan is run on a source. * With Epson Workforce ES-300W, it is run on the device itself. */ src = capsule2item(capsule); if (src == NULL) { wia_log(WIA_WARNING, "wrong param type. Expected a scan source"); Py_RETURN_FALSE; } if (!PyCallable_Check(dl_data.get_data_cb) || !PyCallable_Check(dl_data.end_of_page_cb) || !PyCallable_Check(dl_data.end_of_scan_cb)) { wia_log(WIA_WARNING, "download(): wrong param type. Expected callback(s)"); Py_RETURN_FALSE; } dl_data.mutex = CreateMutex(NULL, FALSE, NULL); hr = src->QueryInterface(IID_IWiaTransfer, (void**)&scan.transfer); if (FAILED(hr)) { wia_log(WIA_WARNING, "source->QueryInterface(WiaTransfer) failed"); wia_log_hresult(WIA_WARNING, hr); Py_RETURN_FALSE; } scan.callbacks = new PyinsaneWiaTransferCallback( get_data_wrapper, end_of_page_wrapper, end_of_scan_wrapper, &dl_data ); WaitForSingleObject(dl_data.mutex, 0); dl_data._save = PyEval_SaveThread(); wia_log_set_pythread_state(dl_data.mutex, &dl_data._save); ReleaseMutex(dl_data.mutex); hr = scan.transfer->Download(0, scan.callbacks); if (hr == WIA_ERROR_PAPER_EMPTY) { end_of_scan_wrapper((void *)&dl_data); WaitForSingleObject(dl_data.mutex, 0); wia_log_set_pythread_state(NULL, NULL); PyEval_RestoreThread(dl_data._save); ReleaseMutex(dl_data.mutex); wia_log(WIA_INFO, "No more paper"); Py_RETURN_NONE; } else if (FAILED(hr)) { WaitForSingleObject(dl_data.mutex, 0); wia_log_set_pythread_state(NULL, NULL); PyEval_RestoreThread(dl_data._save); ReleaseMutex(dl_data.mutex); wia_log(WIA_WARNING, "source->transfer->Download() failed"); wia_log_hresult(WIA_WARNING, hr); scan.transfer->Release(); Py_RETURN_FALSE; } else { WaitForSingleObject(dl_data.mutex, 0); wia_log_set_pythread_state(NULL, NULL); PyEval_RestoreThread(dl_data._save); ReleaseMutex(dl_data.mutex); } // TODO(Jflesch): destroy mutex ? Py_RETURN_TRUE; } static PyObject *exit(PyObject *, PyObject* args) { if (!PyArg_ParseTuple(args, "")) { return NULL; } wia_log(WIA_DEBUG, "exit()"); CoUninitialize(); Py_RETURN_NONE; } static PyMethodDef rawapi_methods[] = { {"download", download, METH_VARARGS, NULL}, {"exit", exit, METH_VARARGS, NULL}, {"get_constraints", get_constraints, METH_VARARGS, NULL}, {"get_devices", get_devices, METH_VARARGS, NULL}, {"get_properties", get_properties, METH_VARARGS, NULL}, {"get_sources", get_sources, METH_VARARGS, NULL}, {"init", init, METH_VARARGS, NULL}, {"open", open_device, METH_VARARGS, NULL}, {"register_logger", register_logger, METH_VARARGS, NULL}, {"set_property", set_property, METH_VARARGS, NULL}, {NULL, NULL, 0, NULL}, }; #if PY_VERSION_HEX < 0x03000000 PyMODINIT_FUNC init_rawapi(void) { Py_InitModule("_rawapi", rawapi_methods); } #else static struct PyModuleDef rawapi_module = { PyModuleDef_HEAD_INIT, "_rawapi", NULL /* doc */, -1, rawapi_methods, }; PyMODINIT_FUNC PyInit__rawapi(void) { return PyModule_Create(&rawapi_module); } #endif pyinsane-2.0.13/pyinsane2/wia/rawapi.py000066400000000000000000000135601332615651100177600ustar00rootroot00000000000000from collections import deque import logging import os import queue import threading from . import _rawapi from .. import util logger = logging.getLogger(__name__) _rawapi.register_logger(0, logger) SINGLE_THREAD = bool(int(os.getenv("PYINSANE_SINGLE_THREAD", 0))) class WIAException(util.PyinsaneException): def __init__(self, msg): super(WIAException, self).__init__("WIA: {}".format(msg)) class WiaAction(object): def __init__(self, func, **kwargs): self.func = func self.kwargs = kwargs self.result = None self.exception = None self.__sem = threading.Semaphore(0) def start(self): if SINGLE_THREAD: return self.do() global wia_thread global wia_action_queue if wia_thread is None or not wia_thread.is_alive(): raise WIAException("WIA thread died unexpectedly !") wia_action_queue.put(self) def wait(self): ret = self.start() if SINGLE_THREAD: return ret self.__sem.acquire() if self.exception is not None: raise self.exception return self.result def do(self): if SINGLE_THREAD: return self.func(**self.kwargs) try: self.result = self.func(**self.kwargs) except Exception as exc: if (not isinstance(exc, EOFError) and not isinstance(exc, StopIteration)): logger.error("Unexpected exception", exc_info=exc) self.exception = exc self.__sem.release() class WiaWorker(threading.Thread): def run(self): global wia_action_queue while True: try: action = wia_action_queue.get(block=True, timeout=1) action.do() except queue.Empty: if not parent_thread.is_alive(): return if not SINGLE_THREAD: parent_thread = threading.current_thread() wia_action_queue = queue.Queue() wia_thread = WiaWorker() wia_thread.start() def _init(): _rawapi.init() def init(): return WiaAction(_init).wait() def _open(devid): out = _rawapi.open(devid) if out is None: raise WIAException("Failed to open {}".format(devid)) return out def open(devid): return WiaAction(_open, devid=devid).wait() def _get_devices(): devices = _rawapi.get_devices() if devices is None: raise WIAException("Failed to get device list") return devices def get_devices(): return WiaAction(_get_devices).wait() def _get_sources(dev): sources = _rawapi.get_sources(dev) if sources is None: raise WIAException("Failed to get sources") return sources def get_sources(dev): return WiaAction(_get_sources, dev=dev).wait() def _get_properties(dev_or_src): properties = _rawapi.get_properties(dev_or_src) if properties is None: raise WIAException("Failed to get scanner properties") return properties def get_properties(dev_or_src): return WiaAction(_get_properties, dev_or_src=dev_or_src).wait() def _get_constraints(dev_or_src): constraints = _rawapi.get_constraints(dev_or_src) if constraints is None: raise WIAException("Failed to get properties constraints") return constraints def get_constraints(dev_or_src): return WiaAction(_get_constraints, dev_or_src=dev_or_src).wait() def _set_property(dev_or_src, propname, propvalue): ret = _rawapi.set_property(dev_or_src, propname, propvalue) if not ret: raise WIAException("Failed to set scanner properties") def set_property(dev_or_src, propname, propvalue): return WiaAction(_set_property, dev_or_src=dev_or_src, propname=propname, propvalue=propvalue).wait() class WiaCallbacks(object): def __init__(self): super(WiaCallbacks, self).__init__() self.received = deque() self.condition = threading.Condition() self.buffer = 512000 * b"\0" def get_data_cb(self, nb_bytes): self.condition.acquire() try: logger.debug("Got {} bytes from driver".format(nb_bytes)) # the only way I've found to make sure the buffer is # always duplicated data = bytes(list(self.buffer[:nb_bytes])) self.received.append(data) self.condition.notify_all() finally: self.condition.release() def end_of_page_cb(self): self.condition.acquire() try: logger.debug("End of page from driver") self.received.append(EOFError()) self.condition.notify_all() finally: self.condition.release() def end_of_scan_cb(self): self.condition.acquire() try: logger.debug("End of scan from driver") self.received.append(StopIteration()) self.condition.notify_all() finally: self.condition.release() def read(self): self.condition.acquire() try: if len(self.received) <= 0: self.condition.wait() popped = self.received.popleft() if isinstance(popped, Exception): raise popped return popped finally: self.condition.release() def _start_scan(src, out): ret = _rawapi.download( src, out.get_data_cb, out.end_of_page_cb, out.end_of_scan_cb, out.buffer, ) if ret is None: # Brother MFC-7360N logger.debug("download() returned PAPER_EMPTY --> StopIteration") raise StopIteration() # WORKAROUND(Jflesch): Samsung SCX-3400: after successfully # scanning the page, download() returns an error ... # Let's just ignore it. return ret def start_scan(src): out = WiaCallbacks() WiaAction(_start_scan, src=src, out=out).start() # don't wait return out def exit(): _rawapi.exit() pyinsane-2.0.13/pyinsane2/wia/trace.cpp000066400000000000000000000056731332615651100177330ustar00rootroot00000000000000#include #include #include #include "trace.h" #include "util.h" #ifdef __cplusplus extern "C" { #endif static volatile HANDLE g_mutex = NULL; static volatile PyThreadState **g_thread_state = NULL; static PyObject *g_log_obj = NULL; static PyObject *g_levels[WIA_MAX_LEVEL + 1] = { 0 }; // pre-allocated strings static PyObject *g_default_msg = NULL; static int g_min_level = WIA_DEBUG; static char g_log_buffer[1024]; static char g_log_buffer2[1024]; void _wia_log(enum wia_log_level lvl, const char *file, int line, LPCSTR fmt, ...) { PyObject *res; PyObject *level; PyObject *msg; va_list args; va_start(args, fmt); if (g_log_obj == NULL || lvl < g_min_level) { return; } memset(g_log_buffer, 0, sizeof(g_log_buffer)); vsnprintf_s(g_log_buffer, _countof(g_log_buffer), _TRUNCATE, fmt, args); g_log_buffer[WIA_COUNT_OF(g_log_buffer) - 1] = '\0'; _snprintf_s(g_log_buffer2, sizeof(g_log_buffer2), _TRUNCATE, "%s(L%d): %s", file, line, g_log_buffer); g_log_buffer2[WIA_COUNT_OF(g_log_buffer2) - 1] = '\0'; if (g_mutex) { WaitForSingleObject(g_mutex, 0); } if (g_thread_state) { PyEval_RestoreThread(*((PyThreadState**)g_thread_state)); } msg = PyUnicode_FromString(g_log_buffer2); level = g_levels[lvl]; res = PyObject_CallMethodObjArgs( g_log_obj, level, msg != NULL ? msg : g_default_msg, NULL ); if (res != NULL) { Py_DECREF(res); } if (msg != NULL) { Py_DECREF(msg); } if (g_thread_state) { *g_thread_state = PyEval_SaveThread(); } if (g_mutex) { ReleaseMutex(g_mutex); } } void _wia_log_hresult(enum wia_log_level lvl, const char *file, int line, HRESULT hr) { const char *msg = NULL; LPCTSTR errMsg; _com_error err(hr); errMsg = err.ErrorMessage(); msg = CT2CA(errMsg); _wia_log(lvl, file, line, "HResult error code 0x%lX: %s", hr, msg); } PyObject *register_logger(PyObject *, PyObject *args) { PyObject *log_obj = NULL; int min_log_level = WIA_DEBUG; if (g_levels[0] == NULL) { g_levels[WIA_DEBUG] = PyUnicode_FromString("debug"); g_levels[WIA_INFO] = PyUnicode_FromString("info"); g_levels[WIA_WARNING] = PyUnicode_FromString("warning"); g_levels[WIA_ERROR] = PyUnicode_FromString("error"); g_default_msg = PyUnicode_FromString("NULL NULL NULL !!"); } if (!PyArg_ParseTuple(args, "iO", &min_log_level, &log_obj)) { return NULL; } if (g_log_obj != NULL) { Py_DECREF(g_log_obj); g_log_obj = NULL; } g_min_level = min_log_level; if (log_obj != NULL) { Py_INCREF(log_obj); g_log_obj = log_obj; } Py_RETURN_NONE; } void wia_log_set_pythread_state(HANDLE mutex, PyThreadState **thread_state) { g_mutex = mutex; g_thread_state = (volatile PyThreadState**)thread_state; } #ifdef __cplusplus } #endif pyinsane-2.0.13/pyinsane2/wia/trace.h000066400000000000000000000020711332615651100173650ustar00rootroot00000000000000#ifndef __PYINSANE_WIA_TRACE_H #define __PYINSANE_WIA_TRACE_H /* #define ENABLE_STDIO */ #ifdef ENABLE_STDIO #include #endif #include #include #include #ifdef __cplusplus extern "C" { #endif enum wia_log_level { WIA_DEBUG, WIA_INFO, WIA_WARNING, WIA_ERROR, WIA_MAX_LEVEL = WIA_ERROR, }; #ifdef ENABLE_STDIO #define wia_log(lvl, fmt, ...) do { \ fprintf(stderr, fmt, __VA_ARGS__); \ fprintf(stderr, "\n"); \ _wia_log(lvl, __FILE__, __LINE__, fmt, __VA_ARGS__); \ } while(0) #else #define wia_log(lvl, fmt, ...) _wia_log(lvl, __FILE__, __LINE__, fmt, __VA_ARGS__); #endif #define wia_log_hresult(lvl, hr) _wia_log_hresult(lvl, __FILE__, __LINE__, hr) void _wia_log(enum wia_log_level lvl, const char *file, int line, LPCSTR fmt, ...); void _wia_log_hresult(enum wia_log_level lvl, const char *file, int line, HRESULT hr); PyObject *register_logger(PyObject *, PyObject *args); void wia_log_set_pythread_state(HANDLE mutex, PyThreadState **thread_state); #ifdef __cplusplus } #endif #endif pyinsane-2.0.13/pyinsane2/wia/transfer.cpp000066400000000000000000000146611332615651100204560ustar00rootroot00000000000000#include #include #include #include #include #include "trace.h" #include "transfer.h" #include "util.h" #define TRACE() do { \ wia_log(WIA_DEBUG, "%s(): L%d", __FUNCTION__, __LINE__); \ } while(0) PyinsaneImageStream::PyinsaneImageStream(data_cb getData, void *cbData) : mGetData(getData), mCbData(cbData), mRefCount(1), mWritten(0) { TRACE(); } PyinsaneImageStream::~PyinsaneImageStream() { TRACE(); } HRESULT STDMETHODCALLTYPE PyinsaneImageStream::Clone(IStream **) { wia_log(WIA_WARNING, "IStream::Clone() not implemented but called !"); return E_NOTIMPL; } HRESULT STDMETHODCALLTYPE PyinsaneImageStream::Commit(DWORD) { TRACE(); return S_OK; } HRESULT STDMETHODCALLTYPE PyinsaneImageStream::CopyTo(IStream*, ULARGE_INTEGER, ULARGE_INTEGER*, ULARGE_INTEGER*) { wia_log(WIA_WARNING, "IStream::CopyTo() not implemented but called !"); return E_NOTIMPL; } HRESULT STDMETHODCALLTYPE PyinsaneImageStream::LockRegion(ULARGE_INTEGER, ULARGE_INTEGER, DWORD) { wia_log(WIA_WARNING, "IStream::LockRegion() not implemented but called !"); return E_NOTIMPL; } HRESULT STDMETHODCALLTYPE PyinsaneImageStream::UnlockRegion(ULARGE_INTEGER, ULARGE_INTEGER, DWORD) { wia_log(WIA_WARNING, "IStream::UnlockRegion() not implemented but called !"); return E_NOTIMPL; } HRESULT STDMETHODCALLTYPE PyinsaneImageStream::Read(void *, ULONG, ULONG *) { wia_log(WIA_WARNING, "IStream::Read() not implemented but called !"); return E_NOTIMPL; } HRESULT STDMETHODCALLTYPE PyinsaneImageStream::Write(void const* pv, ULONG cb, ULONG* pcbWritten) { if (cb == 0) { // Brother MFC-7360N .... *pcbWritten = 0; return S_OK; } wia_log(WIA_DEBUG, "%s(): L%d: %lu bytes", __FUNCTION__, __LINE__, cb); \ mGetData(pv, cb, mCbData); TRACE(); mWritten += cb; *pcbWritten = cb; return S_OK; } HRESULT STDMETHODCALLTYPE PyinsaneImageStream::Revert() { wia_log(WIA_WARNING, "IStream::Revert() not implemented but called !"); return E_NOTIMPL; } HRESULT STDMETHODCALLTYPE PyinsaneImageStream::Seek( LARGE_INTEGER dlibMove, DWORD dwOrigin, ULARGE_INTEGER *plibNewPosition ) { TRACE(); if (dwOrigin == STREAM_SEEK_END && dlibMove.QuadPart == 0) { TRACE(); plibNewPosition->QuadPart = mWritten; return S_OK; } else if (mWritten == 0 && dwOrigin == STREAM_SEEK_SET && dlibMove.QuadPart == 0) { // Epson WorkForce ES-300W TRACE(); plibNewPosition->QuadPart = 0; return S_OK; } wia_log(WIA_WARNING, "IStream::Seek(%lld, %u) not implemented but called !", dlibMove.QuadPart, dwOrigin); return E_NOTIMPL; } HRESULT STDMETHODCALLTYPE PyinsaneImageStream::SetSize(ULARGE_INTEGER newSize) { wia_log(WIA_WARNING, "IStream::SetSize(%llu) not implemented but called !", newSize); return E_NOTIMPL; } HRESULT STDMETHODCALLTYPE PyinsaneImageStream::Stat(STATSTG *pstatstg, DWORD) { SYSTEMTIME systemTime; FILETIME fileTime; TRACE(); GetSystemTime(&systemTime); SystemTimeToFileTime(&systemTime, &fileTime); memset(pstatstg, 0, sizeof(STATSTG)); pstatstg->type = STGTY_STREAM; pstatstg->mtime = fileTime; pstatstg->atime = fileTime; pstatstg->grfLocksSupported = LOCK_EXCLUSIVE; pstatstg->cbSize.QuadPart = mWritten; pstatstg->clsid = CLSID_NULL; return S_OK; } HRESULT STDMETHODCALLTYPE PyinsaneImageStream::QueryInterface(REFIID riid, void **ppvObject) { assert(NULL != ppvObject); TRACE(); if (IsEqualIID(riid, IID_IUnknown)) { *ppvObject = static_cast(this); } else if (IsEqualIID(riid, IID_IStream)) { *ppvObject = static_cast(this); } else { *ppvObject = NULL; wia_log(WIA_WARNING, "Stream::QueryInterface(): Unknown interface requested"); return E_NOINTERFACE; } TRACE(); // Increment the reference count before we return the interface reinterpret_cast(*ppvObject)->AddRef(); return S_OK; } ULONG STDMETHODCALLTYPE PyinsaneImageStream::AddRef() { TRACE(); mRefCount++; return mRefCount; } ULONG STDMETHODCALLTYPE PyinsaneImageStream::Release() { TRACE(); mRefCount--; if (mRefCount == 0) { TRACE(); delete this; } return mRefCount; } PyinsaneWiaTransferCallback::PyinsaneWiaTransferCallback( data_cb getData, end_of_page_cb eop, end_of_scan_cb eos, void *cbData ) : mGetData(getData), mEop(eop), mEos(eos), mCbData(cbData), mRefCount(1) { TRACE(); } PyinsaneWiaTransferCallback::~PyinsaneWiaTransferCallback() { TRACE(); } HRESULT PyinsaneWiaTransferCallback::GetNextStream( LONG, BSTR, BSTR, IStream **ppDestination) { #if 0 return SHCreateStreamOnFileEx(L"C:\\pouet.bmp", STGM_READWRITE | STGM_CREATE, FILE_ATTRIBUTE_NORMAL, TRUE, NULL, ppDestination); #else TRACE(); *ppDestination = new PyinsaneImageStream(mGetData, mCbData); return S_OK; #endif } HRESULT PyinsaneWiaTransferCallback::TransferCallback(LONG, WiaTransferParams *params) { wia_log(WIA_DEBUG, "%s(): L%d: %ld, %ld%%, %lluB, 0x%X", __FUNCTION__, __LINE__, params->lMessage, params->lPercentComplete, params->ulTransferredBytes, params->hrErrorStatus); if (params->lMessage == WIA_TRANSFER_MSG_END_OF_TRANSFER) { TRACE(); mEop(mCbData); // mark the current page as finished } return S_OK; } HRESULT STDMETHODCALLTYPE PyinsaneWiaTransferCallback::QueryInterface(REFIID riid, void **ppvObject) { assert(NULL != ppvObject); TRACE(); if (IsEqualIID(riid, IID_IUnknown)) { TRACE(); *ppvObject = static_cast(this); } else if (IsEqualIID( riid, IID_IWiaTransferCallback )) { TRACE(); *ppvObject = static_cast(this); } else { TRACE(); *ppvObject = NULL; return E_NOINTERFACE; } TRACE(); // Increment the reference count before we return the interface reinterpret_cast(*ppvObject)->AddRef(); return S_OK; } ULONG STDMETHODCALLTYPE PyinsaneWiaTransferCallback::AddRef() { TRACE(); mRefCount++; return mRefCount; } ULONG STDMETHODCALLTYPE PyinsaneWiaTransferCallback::Release() { TRACE(); mRefCount--; if (mRefCount == 0) { TRACE(); delete this; } return mRefCount; } pyinsane-2.0.13/pyinsane2/wia/transfer.h000066400000000000000000000045621332615651100201220ustar00rootroot00000000000000#ifndef __PYINSANE_WIA_TRANSFER_H #define __PYINSANE_WIA_TRANSFER_H #include #include #include #include typedef void (*data_cb)(const void *data, int nb_bytes, void *cb_data); typedef void (*end_of_page_cb)(void *cb_data); typedef void (*end_of_scan_cb)(void *cb_data); class PyinsaneImageStream : public IStream { public: PyinsaneImageStream(data_cb getData, void *cbData); ~PyinsaneImageStream(); virtual HRESULT STDMETHODCALLTYPE Clone(IStream **); virtual HRESULT STDMETHODCALLTYPE Commit(DWORD); virtual HRESULT STDMETHODCALLTYPE CopyTo(IStream*, ULARGE_INTEGER, ULARGE_INTEGER*, ULARGE_INTEGER*); virtual HRESULT STDMETHODCALLTYPE LockRegion(ULARGE_INTEGER, ULARGE_INTEGER, DWORD); virtual HRESULT STDMETHODCALLTYPE Read(void* pv, ULONG cb, ULONG* pcbRead); virtual HRESULT STDMETHODCALLTYPE Revert(); virtual HRESULT STDMETHODCALLTYPE Seek(LARGE_INTEGER liDistanceToMove, DWORD dwOrigin, ULARGE_INTEGER* lpNewFilePointer); virtual HRESULT STDMETHODCALLTYPE SetSize(ULARGE_INTEGER); virtual HRESULT STDMETHODCALLTYPE Stat(STATSTG* pStatstg, DWORD grfStatFlag); virtual HRESULT STDMETHODCALLTYPE UnlockRegion(ULARGE_INTEGER, ULARGE_INTEGER, DWORD); virtual HRESULT STDMETHODCALLTYPE Write(void const* pv, ULONG cb, ULONG* pcbWritten); virtual HRESULT STDMETHODCALLTYPE QueryInterface(const IID &,void **); virtual ULONG STDMETHODCALLTYPE AddRef(); virtual ULONG STDMETHODCALLTYPE Release(); private: unsigned long long mWritten; int mRefCount; data_cb mGetData; void *mCbData; }; class PyinsaneWiaTransferCallback : public IWiaTransferCallback { public: PyinsaneWiaTransferCallback(data_cb getData, end_of_page_cb eop, end_of_scan_cb eos, void *cbData); ~PyinsaneWiaTransferCallback(); // interface methods virtual HRESULT STDMETHODCALLTYPE GetNextStream(LONG lFlags, BSTR bstrItemName, BSTR bstrFullItemName, IStream **ppDestination); virtual HRESULT STDMETHODCALLTYPE TransferCallback(LONG lFlags, WiaTransferParams *pWiaTransferParams); virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject); virtual ULONG STDMETHODCALLTYPE AddRef(); virtual ULONG STDMETHODCALLTYPE Release(); private: data_cb mGetData; end_of_page_cb mEop; end_of_scan_cb mEos; void *mCbData; int mRefCount; }; #endif pyinsane-2.0.13/pyinsane2/wia/util.h000066400000000000000000000002511332615651100172420ustar00rootroot00000000000000#ifndef __PYINSANE_WIA_UTIL_H #define __PYINSANE_WIA_UTIL_H #define WIA_COUNT_OF(x) (sizeof(x) / sizeof(x[0])) #define WIA_MIN(x, y) (((x) < (y)) ? (x) : (y)) #endif pyinsane-2.0.13/setup.cfg000066400000000000000000000000301332615651100152400ustar00rootroot00000000000000[nosetests] verbosity=2 pyinsane-2.0.13/setup.py000077500000000000000000000063021332615651100151440ustar00rootroot00000000000000#!/usr/bin/env python import os import platform import sys from setuptools import Extension from setuptools import setup, find_packages if platform.architecture()[0] == '32bit': DEFAULT_ATL_WINDDK_LIB_DIR = "c:\\winddk\\7600.16385.1\\lib\\ATL\\amd64" else: DEFAULT_ATL_WINDDK_LIB_DIR = "c:\\winddk\\7600.16385.1\\lib\\ATL\\i386" try: with open("pyinsane2/_version.py", "r") as file_descriptor: version = file_descriptor.read().strip() version = version.split(" ")[2][1:-1] print("Pyinsane version: {}".format(version)) if "-" in version: version = version.split("-")[0] except EnvironmentError: print("ERROR: _version.py file is missing") print("ERROR: Please run 'make version' first") sys.exit(1) if os.name == "nt": extensions = [ Extension( 'pyinsane2.wia._rawapi', [ 'pyinsane2/wia/properties.cpp', 'pyinsane2/wia/rawapi.cpp', 'pyinsane2/wia/trace.cpp', 'pyinsane2/wia/transfer.cpp', ], include_dirs=[ # Yeah, I know. os.getenv( "WINDDK_INCLUDE_DIR", "c:\\winddk\\7600.16385.1\\inc\\atl71" ), ], library_dirs=[ # Yeah, I know. os.getenv("WINDDK_LIB_DIR", DEFAULT_ATL_WINDDK_LIB_DIR), ], libraries=[ "ole32", "wiaguid", ], # extra_compile_args=['/W4'], undef_macros=['NDEBUG'], ), ] else: extensions = [] setup( name="pyinsane2", version=version, description=( "Python library to access and use image scanners (Linux/Windows/etc)" ), long_description=( "Python library to access and use image scanners (devices)." " Works on GNU/Linux (Sane), *BSD (Sane), Windows >= Vista (WIA 2)," " MacOSX (Sane), etc." ), keywords="sane scanner", url="https://github.com/openpaperwork/pyinsane", download_url=( "https://github.com/openpaperwork/pyinsane/archive/" "{}.zip".format(version) ), classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later" " (GPLv3+)", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Topic :: Multimedia :: Graphics :: Capture :: Scanners", "Topic :: Software Development :: Libraries :: Python Modules", ], license="GPLv3+", author="Jerome Flesch", author_email="jflesch@openpaper.work", packages=find_packages(exclude=['examples', 'tests']), data_files=[], scripts=[], install_requires=[ "Pillow", ], ext_modules=extensions, zip_safe=(os.name != "nt"), setup_requires=['nose>=1.0'], ) pyinsane-2.0.13/tests/000077500000000000000000000000001332615651100145705ustar00rootroot00000000000000pyinsane-2.0.13/tests/__init__.py000066400000000000000000000000061332615651100166750ustar00rootroot00000000000000# nop pyinsane-2.0.13/tests/tests_abstract.py000066400000000000000000000205001332615651100201640ustar00rootroot00000000000000import os import unittest import pyinsane2 def get_devices(): '''Return devices, perhaps after creating a test device.''' devices = pyinsane2.get_devices() if len(devices) == 0: # if there are no devices found, create a virtual device. # see sane-test(5) and /etc/sane.d/test.conf pyinsane2.Scanner("test").scan() devices = pyinsane2.get_devices() return devices class TestSaneGetDevices(unittest.TestCase): module = None def setUp(self): pyinsane2.init() def test_get_devices(self): devices = get_devices() self.assertTrue(len(devices) > 0) def tearDown(self): pyinsane2.exit() class TestSaneOptions(unittest.TestCase): module = None def setUp(self): pyinsane2.init() self.devices = get_devices() self.assertTrue(len(self.devices) > 0) def test_get_option(self): for dev in self.devices: for (k, v) in dev.options.items(): if v.capabilities.is_active(): self.assertNotEqual(v.value, None) else: self.assertRaises( pyinsane2.PyinsaneException, lambda: v.value ) def test_set_option(self): for dev in self.devices: dev.options['mode'].value = "Gray" val = dev.options['mode'].value self.assertEqual(val, "Gray") def __set_opt(self, dev, opt_name, opt_val): dev.options[opt_name].value = opt_val def test_set_inexisting_option(self): for dev in self.devices: self.assertRaises(KeyError, self.__set_opt, dev, 'xyz', "Gray") def test_set_invalid_value(self): for dev in self.devices: self.assertRaises( pyinsane2.PyinsaneException, self.__set_opt, dev, 'mode', "XYZ" ) def test_set_inactive_option(self): for dev in self.devices: noncolor = [ x for x in dev.options["mode"].constraint if x != "Color" ] if len(noncolor) == 0: self.skipTest("scanner does not support required option") if "three-pass" not in dev.options: self.skipTest("scanner does not support option 'three-pass'") dev.options["mode"].value = noncolor[0] # three-pass mode is only active in color mode self.assertRaises( pyinsane2.PyinsaneException, self.__set_opt, dev, 'three-pass', 1 ) def tearDown(self): for dev in self.devices: del(dev) del(self.devices) pyinsane2.exit() class TestSaneScan(unittest.TestCase): module = None def setUp(self): pyinsane2.init() devices = get_devices() self.assertTrue(len(devices) > 0) self.dev = devices[0] def test_simple_scan_lineart(self): try: self.dev.options['mode'].value = "Lineart" except pyinsane2.PyinsaneException: self.dev.options['mode'].value = "Gray" self.dev.options['depth'].value = 1 scan_session = self.dev.scan(multiple=False) try: assert(scan_session.scan is not None) while True: scan_session.scan.read() except EOFError: pass img = scan_session.images[0] self.assertNotEqual(img, None) def test_simple_scan_gray(self): try: self.dev.options['mode'].value = "Gray" except pyinsane2.PyinsaneException: self.skipTest("scanner does not support required option") scan_session = self.dev.scan(multiple=False) try: while True: scan_session.scan.read() except EOFError: pass img = scan_session.images[0] self.assertNotEqual(img, None) def test_simple_scan_color(self): try: self.dev.options['mode'].value = "Color" except pyinsane2.PyinsaneException: self.skipTest("scanner does not support required option") self.dev.options['resolution'].value = 300 scan_session = self.dev.scan(multiple=False) try: last_line = 0 while True: scan_session.scan.read() nb_lines = scan_session.scan.available_lines[1] if nb_lines > last_line + 100: # just making sure it doesn't raise exceptions scan_session.scan.get_image(end_line=nb_lines) last_line = nb_lines except EOFError: pass img = scan_session.images[0] self.assertNotEqual(img, None) def test_multi_scan_on_flatbed(self): try: self.dev.options['source'].value = "Flatbed" self.dev.options['mode'].value = "Color" except pyinsane2.PyinsaneException: self.skipTest("scanner does not support required option") scan_session = self.dev.scan(multiple=True) try: while True: scan_session.scan.read() except EOFError: pass self.assertEqual(len(scan_session.images), 1) self.assertNotEqual(scan_session.images[0], None) def test_multi_scan_on_adf(self): adf_found = False pages = 0 if "ADF" in self.dev.options['source'].constraint: self.dev.options['source'].value = "ADF" adf_found = True else: for srcname in self.dev.options['source'].constraint: # sane-test uses 'Automatic Document Feeder' instead of ADF # WIA uses 'feeder' if "feeder" in srcname.lower(): self.dev.options['source'].value = srcname if os.name != "nt": pages = 10 # sane-test scans give us 10 pages adf_found = True self.assertTrue(adf_found) self.dev.options['mode'].value = "Color" if not adf_found: self.skipTest("scanner does not support required option") return scan_session = self.dev.scan(multiple=True) try: while True: try: scan_session.scan.read() except EOFError: pass except StopIteration: pass self.assertEqual(len(scan_session.images), pages) def test_expected_size(self): try: self.dev.options['source'].value = "Flatbed" self.dev.options['mode'].value = "Color" except pyinsane2.PyinsaneException: self.skipTest("scanner does not support required option") scan_session = self.dev.scan(multiple=False) scan_size = scan_session.scan.expected_size self.assertTrue(scan_size[0] > 100) self.assertTrue(scan_size[1] > 100) scan_session.scan.cancel() def test_get_progressive_scan(self): try: self.dev.options['source'].value = "Flatbed" self.dev.options['mode'].value = "Color" except pyinsane2.PyinsaneException: self.skipTest("scanner does not support required option") scan_session = self.dev.scan(multiple=False) last_line = 0 expected_size = scan_session.scan.expected_size try: while True: scan_session.scan.read() self.assertEqual(scan_session.scan.available_lines[0], 0) current_line = scan_session.scan.available_lines[1] self.assertTrue(last_line <= current_line) if current_line >= last_line + 100: img = scan_session.scan.get_image(0, current_line) self.assertEqual(img.size[0], expected_size[0]) self.assertEqual(img.size[1], current_line) last_line = current_line except EOFError: pass self.assertTrue(last_line <= current_line) self.assertEqual(current_line, expected_size[1]) last_line = current_line img = scan_session.scan.get_image(0, current_line) self.assertEqual(img.size[0], expected_size[0]) self.assertEqual(img.size[1], current_line) img = scan_session.images[0] self.assertNotEqual(img, None) def tearDown(self): del(self.dev) pyinsane2.exit() pyinsane-2.0.13/tests/tests_saneapi.py000066400000000000000000000146471332615651100200200ustar00rootroot00000000000000import os import unittest if os.name != "nt": from pyinsane2.sane import rawapi def get_devices(): '''Return SANE devices, perhaps after creating a test device.''' devices = rawapi.sane_get_devices() if len(devices) == 0: # if there are no devices found, create a virtual device. # see sane-test(5) and /etc/sane.d/test.conf rawapi.sane_close(rawapi.sane_open("test")) devices = rawapi.sane_get_devices() return devices class TestSaneInit(unittest.TestCase): def setUp(self): pass @unittest.skipIf(os.name == "nt", "sane only") def test_init(self): version = rawapi.sane_init() self.assertTrue(version.is_current()) rawapi.sane_exit() def tearDown(self): pass class TestSaneGetDevices(unittest.TestCase): def setUp(self): rawapi.sane_init() @unittest.skipIf(os.name == "nt", "sane only") def test_get_devices(self): devices = get_devices() self.assertTrue(len(devices) > 0) def tearDown(self): rawapi.sane_exit() class TestSaneOpen(unittest.TestCase): def setUp(self): rawapi.sane_init() devices = get_devices() self.assertTrue(len(devices) > 0) self.dev_name = devices[0].name @unittest.skipIf(os.name == "nt", "sane only") def test_open_invalid(self): self.assertRaises(rawapi.SaneException, rawapi.sane_open, "whatever") @unittest.skipIf(os.name == "nt", "sane only") def test_open_valid(self): dev_handle = rawapi.sane_open(self.dev_name) rawapi.sane_close(dev_handle) def tearDown(self): rawapi.sane_exit() class TestSaneGetOptionDescriptor(unittest.TestCase): def setUp(self): rawapi.sane_init() devices = get_devices() self.assertTrue(len(devices) > 0) dev_name = devices[0].name self.dev_handle = rawapi.sane_open(dev_name) @unittest.skipIf(os.name == "nt", "sane only") def test_get_option_descriptor_0(self): opt_desc = rawapi.sane_get_option_descriptor(self.dev_handle, 0) # XXX(Jflesch): The name may vary: sometimes it's empty, sometimes it's # "option-cnt" # self.assertEqual(opt_desc.name, "") self.assertEqual(opt_desc.title, b"Number of options") self.assertEqual(opt_desc.type, rawapi.SaneValueType.INT) self.assertEqual(opt_desc.unit, rawapi.SaneUnit.NONE) self.assertEqual(opt_desc.size, 4) self.assertEqual(opt_desc.cap, rawapi.SaneCapabilities.SOFT_DETECT) self.assertEqual(opt_desc.constraint_type, rawapi.SaneConstraintType.NONE) @unittest.skipIf(os.name == "nt", "sane only") def test_get_option_descriptor_out_of_bounds(self): # XXX(Jflesch): Sane's documentation says get_option_descriptor() # should return NULL if the index value is invalid. It seems the actual # implementation prefers to segfault. # self.assertRaises(rawapi.SaneException, # rawapi.sane_get_option_descriptor, self.dev_handle, # 999999) pass def tearDown(self): rawapi.sane_close(self.dev_handle) rawapi.sane_exit() class TestSaneControlOption(unittest.TestCase): def setUp(self): rawapi.sane_init() devices = get_devices() self.assertTrue(len(devices) > 0) dev_name = devices[0].name self.dev_handle = rawapi.sane_open(dev_name) self.nb_options = rawapi.sane_get_option_value(self.dev_handle, 0) @unittest.skipIf(os.name == "nt", "sane only") def test_get_option_value(self): for opt_idx in range(0, self.nb_options): desc = rawapi.sane_get_option_descriptor(self.dev_handle, opt_idx) if not rawapi.SaneValueType(desc.type).can_getset_opt(): continue if desc.cap | rawapi.SaneCapabilities.INACTIVE == desc.cap: continue val = rawapi.sane_get_option_value(self.dev_handle, opt_idx) self.assertNotEqual(val, None) @unittest.skipIf(os.name == "nt", "sane only") def test_set_option_value(self): for opt_idx in range(0, self.nb_options): desc = rawapi.sane_get_option_descriptor(self.dev_handle, opt_idx) if (desc.name != "mode" or not rawapi.SaneValueType(desc.type).can_getset_opt()): continue info = rawapi.sane_set_option_value(self.dev_handle, opt_idx, "Gray") self.assertFalse(rawapi.SaneInfo.INEXACT in info) val = rawapi.sane_get_option_value(self.dev_handle, opt_idx) self.assertEqual(val, "Gray") @unittest.skipIf(os.name == "nt", "sane only") def test_set_option_auto(self): # TODO(Jflesch) pass def tearDown(self): rawapi.sane_close(self.dev_handle) rawapi.sane_exit() class TestSaneScan(unittest.TestCase): def setUp(self): rawapi.sane_init() devices = get_devices() self.assertTrue(len(devices) > 0) dev_name = devices[0].name self.dev_handle = rawapi.sane_open(dev_name) @unittest.skipIf(os.name == "nt", "sane only") def test_simple_scan(self): # XXX(Jflesch): set_io_mode() always return SANE_STATUS_UNSUPPORTED # with my scanner # rawapi.sane_set_io_mode(self.dev_handle, non_blocking=False) try: rawapi.sane_start(self.dev_handle) except StopIteration: self.skipTest("cannot scan, no document loaded") # XXX(Jflesch): get_select_fd() always return SANE_STATUS_UNSUPPORTED # with my scanner # fd = rawapi.sane_get_select_fd(self.dev_handle) # self.assertTrue(fd > 0) try: while True: buf = rawapi.sane_read(self.dev_handle, 128*1024) self.assertTrue(len(buf) > 0) except EOFError: pass rawapi.sane_cancel(self.dev_handle) @unittest.skipIf(os.name == "nt", "sane only") def test_cancelled_scan(self): try: rawapi.sane_start(self.dev_handle) except StopIteration: self.skipTest("cannot scan, no document loaded") buf = rawapi.sane_read(self.dev_handle, 128*1024) self.assertTrue(len(buf) > 0) rawapi.sane_cancel(self.dev_handle) def tearDown(self): rawapi.sane_close(self.dev_handle) rawapi.sane_exit() pyinsane-2.0.13/tests/tests_wiaapi.py000066400000000000000000000146731332615651100176510ustar00rootroot00000000000000import os import unittest if os.name == "nt": from pyinsane2.wia import rawapi class TestInit(unittest.TestCase): def setUp(self): pass @unittest.skipIf(os.name != "nt", "Windows only") def test_init(self): rawapi.init() rawapi.exit() def tearDown(self): pass class TestGetDevices(unittest.TestCase): def setUp(self): if os.name == "nt": rawapi.init() @unittest.skipIf(os.name != "nt", "Windows only") def test_get_devices(self): devices = rawapi.get_devices() self.assertTrue(len(devices) > 0) def tearDown(self): if os.name == "nt": rawapi.exit() class TestOpenDevice(unittest.TestCase): def setUp(self): if os.name == "nt": rawapi.init() @unittest.skipIf(os.name != "nt", "Windows only") def test_open_device(self): devices = rawapi.get_devices() self.assertTrue(len(devices) > 0) devid = devices[0][0] dev = rawapi.open(devid) self.assertNotEqual(dev, None) del dev @unittest.skipIf(os.name != "nt", "Windows only") def test_invalid_open_device(self): self.assertRaises(rawapi.WIAException, rawapi.open, "randomcraphere") def tearDown(self): if os.name == "nt": rawapi.exit() class TestGetSources(unittest.TestCase): def setUp(self): if os.name != "nt": return rawapi.init() @unittest.skipIf(os.name != "nt", "Windows only") def test_get_sources(self): devices = rawapi.get_devices() self.assertTrue(len(devices) > 0) devid = devices[0][0] dev = rawapi.open(devid) self.assertNotEqual(dev, None) sources = rawapi.get_sources(dev) self.assertTrue(len(sources) > 0) @unittest.skipIf(os.name != "nt", "Windows only") def test_invalid_get_sources(self): self.assertRaises( rawapi.WIAException, rawapi.get_sources, "randomcrappyobject" ) def tearDown(self): if os.name != "nt": return rawapi.exit() class TestGetProperties(unittest.TestCase): def setUp(self): if os.name != "nt": return rawapi.init() devices = rawapi.get_devices() devid = devices[0][0] self.dev = rawapi.open(devid) self.sources = rawapi.get_sources(self.dev) self.assertTrue(len(self.sources) > 0) @unittest.skipIf(os.name != "nt", "Windows only") def test_get_dev_properties(self): props = rawapi.get_properties(self.dev) self.assertTrue(len(props) > 0) @unittest.skipIf(os.name != "nt", "Windows only") def test_get_src_properties(self): props = rawapi.get_properties(self.sources[0][1]) self.assertTrue(len(props) > 0) @unittest.skipIf(os.name != "nt", "Windows only") def test_invalid_get_properties(self): self.assertRaises(rawapi.WIAException, rawapi.get_properties, "crappy_obj") def tearDown(self): if os.name != "nt": return rawapi.exit() class TestSetProperty(unittest.TestCase): def setUp(self): if os.name != "nt": return rawapi.init() devices = rawapi.get_devices() devid = devices[0][0] self.dev = rawapi.open(devid) self.sources = rawapi.get_sources(self.dev) self.assertTrue(len(self.sources) > 0) @unittest.skipIf(os.name != "nt", "Windows only") def test_set_src_property_depth(self): rawapi.set_property(self.sources[0][1], "depth", 8) props = rawapi.get_properties(self.sources[0][1]) self.assertTrue(len(props) > 0) for (propname, propvalue, accessright, _) in props: self.assertTrue(accessright == "ro" or accessright == "rw") if (propname == "depth"): self.assertEqual(propvalue, 8) self.assertEqual(accessright, "rw") rawapi.set_property(self.sources[0][1], "depth", 24) props = rawapi.get_properties(self.sources[0][1]) self.assertTrue(len(props) > 0) for (propname, propvalue, _a, _b) in props: if (propname == "depth"): self.assertEqual(propvalue, 24) @unittest.skipIf(os.name != "nt", "Windows only") def test_set_src_property_preview(self): rawapi.set_property(self.sources[0][1], "preview", "preview_scan") props = rawapi.get_properties(self.sources[0][1]) self.assertTrue(len(props) > 0) for (propname, propvalue, accessright, _) in props: self.assertTrue(accessright == "ro" or accessright == "rw") if (propname == "preview"): self.assertEqual(propvalue, "preview_scan") self.assertEqual(accessright, "rw") rawapi.set_property(self.sources[0][1], "preview", "final_scan") props = rawapi.get_properties(self.sources[0][1]) self.assertTrue(len(props) > 0) for (propname, propvalue, _a, _b) in props: if (propname == "preview"): self.assertEqual(propvalue, "final_scan") @unittest.skipIf(os.name != "nt", "Windows only") def test_invalid_set_src_property_depth(self): self.assertRaises(rawapi.WIAException, rawapi.set_property, "crapobj", "depth", 8) self.assertRaises(rawapi.WIAException, rawapi.set_property, self.sources[0][1], "crapproperty", 8) self.assertRaises(rawapi.WIAException, rawapi.set_property, self.sources[0][1], "depth", "crapvalue") def tearDown(self): if os.name != "nt": return rawapi.exit() class TestScan(unittest.TestCase): def setUp(self): if os.name != "nt": return rawapi.init() devices = rawapi.get_devices() devid = devices[0][0] self.dev = rawapi.open(devid) self.sources = rawapi.get_sources(self.dev) self.assertTrue(len(self.sources) > 0) @unittest.skipIf(os.name != "nt", "Windows only") def test_scan(self): scan = rawapi.start_scan(self.sources[0][1]) try: while True: buf = scan.read() self.assertTrue(len(buf) > 0) except EOFError: pass # no more than one page expected self.assertRaises(StopIteration, scan.read) def tearDown(self): if os.name != "nt": return rawapi.exit() pyinsane-2.0.13/tox.ini000066400000000000000000000002421332615651100147370ustar00rootroot00000000000000[tox] envlist=py27,py3 [testenv] deps= Pillow nose commands=nosetests [flake8] exclude = .tox, build, dist, venv*, *.egg*, .git