pax_global_header00006660000000000000000000000064131551327460014521gustar00rootroot0000000000000052 comment=920e598c1ec6eb9a48dde32282d10adc9cc3b011 pdf-tools-0.80/000077500000000000000000000000001315513274600133575ustar00rootroot00000000000000pdf-tools-0.80/.gitignore000066400000000000000000000000601315513274600153430ustar00rootroot00000000000000aux/ dist/ .cask/ pdf-tools-*.tar pdf-tools-*/ pdf-tools-0.80/.travis.yml000066400000000000000000000024731315513274600154760ustar00rootroot00000000000000language: cpp env: - EMACS=emacs24 - EMACS=emacs-snapshot matrix: fast_finish: true allow_failures: - env: EMACS=emacs-snapshot compiler: - gcc install: - if [ "$EMACS" = "emacs24" ]; then sudo add-apt-repository -y ppa:cassou/emacs && sudo apt-get update -qq && sudo apt-get install -qq emacs24 emacs24-el; fi - if [ "$EMACS" = "emacs-snapshot" ]; then sudo add-apt-repository -y ppa:ubuntu-elisp/ppa && sudo apt-get update -qq && sudo apt-get install -qq emacs-snapshot; fi - sudo apt-get install -qq make automake autoconf libpng-dev libz-dev - sudo apt-get install libcairo2-dev libglib2.0-dev - POPPLER=poppler-0.28.1 - wget http://poppler.freedesktop.org/$POPPLER.tar.xz - unxz $POPPLER.tar.xz - tar xf $POPPLER.tar - pushd $POPPLER - ./configure --disable-gtk-doc-html --disable-xpdf-headers --disable-poppler-qt4 --disable-poppler-qt5 --disable-poppler-cpp --disable-utils --enable-xpdf-headers && make -j4 && sudo make install - popd - curl -fsSL https://raw.githubusercontent.com/cask/cask/master/go | python - export PATH=/home/travis/.cask/bin:$PATH - export LD_LIBRARY_PATH=/usr/local/lib script: - make test pdf-tools-0.80/COPYING000066400000000000000000001045131315513274600144160ustar00rootroot00000000000000 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 . pdf-tools-0.80/COPYING.SYNCTEX000066400000000000000000000023771315513274600155570ustar00rootroot00000000000000License: -------- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE Except as contained in this notice, the name of the copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from the copyright holder. pdf-tools-0.80/Cask000066400000000000000000000003341315513274600141630ustar00rootroot00000000000000(source gnu) (source melpa) (package-file "lisp/pdf-tools.el") (files "lisp/*.el" "README" "server/epdfinfo") (development (depends-on "emacs" "24.3") (depends-on "let-alist") (depends-on "tablist")) pdf-tools-0.80/Makefile000066400000000000000000000057161315513274600150300ustar00rootroot00000000000000EMACS ?= emacs # Handle the mess when inside Emacs. unexport INSIDE_EMACS #cask not like this. ifeq ($(EMACS), t) EMACS = emacs endif EMACS_VERSION = $(shell $(EMACS) -Q --batch --eval '(princ emacs-version)') EFLAGS = -Q --batch # Note: If you change this, also change it in lisp/pdf-tools.el and # server/configure.ac . PACKAGE_VERSION = 0.80 PKGFILE_CONTENT = (define-package "pdf-tools" "$(PACKAGE_VERSION)" \ "Support library for PDF documents." \ (quote ((emacs "24.3") (let-alist "1.0.4") \ (tablist "0.70"))) \ :keywords \ (quote ("files" "multimedia"))) PACKAGE_NAME = pdf-tools-$(PACKAGE_VERSION) PACKAGE_DIR = $(PACKAGE_NAME) .PHONY: all clean distclean package bytecompile test check melpa cask-install all: package clean: rm -rf -- $(PACKAGE_DIR) rm -f -- $(PACKAGE_NAME).tar rm -f -- lisp/*.elc ! [ -f server/Makefile ] || $(MAKE) -C server clean distclean: clean rm -rf .cask ! [ -f server/Makefile ] || $(MAKE) -C server distclean package: $(PACKAGE_NAME).tar $(PACKAGE_NAME).tar: server/epdfinfo lisp/*.el mkdir -p '$(PACKAGE_DIR)' cp lisp/*.el README server/epdfinfo '$(PACKAGE_DIR)' echo '$(PKGFILE_CONTENT)' > '$(PACKAGE_DIR)/pdf-tools-pkg.el' tar cf '$(PACKAGE_NAME).tar' '$(PACKAGE_DIR)' melpa-package: $(MAKE) distclean mkdir -p '$(PACKAGE_DIR)/build' cp lisp/*.el README '$(PACKAGE_DIR)' cp -r Makefile server '$(PACKAGE_DIR)/build' echo '$(PKGFILE_CONTENT)' > '$(PACKAGE_DIR)/pdf-tools-pkg.el' tar cf '$(PACKAGE_NAME).tar' '$(PACKAGE_DIR)' install-package: package $(EMACS) $(EFLAGS) --eval \ "(progn (package-initialize) \ (package-install-file \ \"$(PACKAGE_NAME).tar\"))" server/epdfinfo: server/Makefile $(MAKE) -C server server/Makefile: server/configure cd server && ./configure -q server/configure: server/configure.ac cd server && ./autogen.sh bytecompile: cask-install cask exec $(EMACS) $(EFLAGS) -L $(PWD)/lisp -f batch-byte-compile lisp/*.el test: all cask-install cask exec $(EMACS) $(EFLAGS) -l test/run-tests.el $(PACKAGE_NAME).tar cask-install: .cask/$(EMACS_VERSION) .cask/$(EMACS_VERSION): cask install check: bytecompile test print-version: @[ -n '$(PACKAGE_VERSION)' ] && echo '$(PACKAGE_VERSION)' install-server-deps: sudo apt-get install gcc g++ make automake autoconf \ libpng-dev libz-dev libpoppler-glib-dev -sudo apt-get install libpoppler-private-dev -sudo apt-get install gtklp melpa-build: server/epdfinfo -cp -p server/epdfinfo .. @if [ "$(shell uname -o)" = "Msys" ]; then \ for f in $(shell ldd server/epdfinfo | awk '/mingw/ {print $$3}'); do \ cp $$f ..; \ done; \ fi $(MAKE) distclean @if [ -x ../epdfinfo ]; then \ echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~"; \ echo "Server successfully build. "; \ echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~"; \ else \ echo "Server not build, maybe due to missing dependencies (See README)."; \ echo "Required: gcc g++ make automake autoconf libpng-dev libz-dev libpoppler-glib-dev libpoppler-private-dev"; \ false; \ fi pdf-tools-0.80/NEWS000066400000000000000000000114471315513274600140650ustar00rootroot00000000000000-*- org -*- * Version 0.80 ** Tablist package The files tablist.el and tablist-filter.el are no longer part of pdf-tools, but continue to live on in the tablist package, on which pdf-tools now depends on. ** View *** Encrypted files When encountering an encrypted file, query for a password and attempt to decrypt it. *** Backward sync from isearch In isearch, press M-s s to visit the source of the current match. *** Disable unicode in mode-line New variable pdf-view-use-unicode-lighter which allows for disabling the use of unicode in the mode-line. * Version 0.70 ** View *** Register integration The keys m and ' now set resp. jump to a register corresponding to a position in the PDF. Also '' is handled special: It jumps to the position before the last register-jump. *** Export parts of a page as an image ** Info *** Interface changes The return value of many pdf-info-* functions was changed in order to prefer alists over other data-structures (indexed lists, trees). ** Virtual PDF A virtual PDF is a collection of pages (or parts thereof) of arbitrary documents, which appear to the rest of pdf-tools as one big PDF, though they are always read-only. * Version 0.60 ** Regexp support You may now search for perl-compatible regular expressions (PCRE) in PDF documents, both via Isearch and Occur. If that scares you, customize the variable pdf-occur-prefer-string-search. ** Occur *** Asynchronous search Searching is performed asynchronously in a private server instance, i.e. it doesn't block neither ordinary editing nor pdf-view-mode. *** Moccur Added the ability to search multiple documents in one occur buffer. ** Isearch *** Occur Integration M-s o now starts occur, while keeping the isearch session, like it is in text-buffers. *** Word search M-s w now does a word search, which will also find hyphenated words (as determined by pdf-isearch-hyphenation-character), though not at page boundaries. ** View *** Navigate by pagelabels M-g l may be used to jump to a page by label, i.e. it's displayed number. *** Rendering Added the ability to display the page as it would be printed (e.g. w/o annotations) and to apply a color filter (pdf-view-printer-minor-mode resp. pdf-view-midnight-minor-mode). ** Outline New option `pdf-outline-display-labels', determining whether to display labels instead of plain page-numbers. * Version 0.50 ** PDF Tools is now available on MELPA. ** SyncTeX *** File name handling SyncTeX is pretty picky about source filenames. So instead of trying various filenames and hoping for best, we find it by directly inspecting the database. *** Heuristic backward search Backward searching now tries to find the exact position in the LaTeX buffer. This may be disabled by setting pdf-sync-backward-use-heuristic to nil. *** Renamed most variables/functions/commands. The old ones are still there but declared obsolete. ** Compatible with Emacs 24.3 ** Integrate with bookmark.el ** Compiling on OSX PDF Tools should now compile on OSX, though it is unsupported. ** MELPA Try to handle an update via MELPA by package.el by shutting down the server, recompiling and restarting it. This may be deactivated by setting pdf-tools-handle-upgrades to nil. ** Auto slicing A new minor mode which will automatically slice the page according to it's bounding box. * Version 0.40 I basically reimplemented the whole thing. (Not really, but a lot has changed.) ** Displaying PDF Files. Rendering is now done almost completely in libpoppler (no convert anymore), while PNG images are kept in memory and files are solely used as a means of exchange between Emacs and epdfinfo. In essence, display should be much faster. *** New Major Mode pdf-view Hacking up doc-view.el to support a server-based ,,rendering engine'' would have been to awkward. So a new major-mode was needed : pdf-view-mode . Both are very similar regarding user-interface. Some differences are: + Setting the width to `fit-width', `fit-height' or `fit-page' keeps up with window-size changes. + The values of the slice are relative, i.e. independent of the image-size. *** PNG Image Cache Image data is cached, in order to keep the time it needs to display a page low. Some pages are pre-loaded for the same reason, while idling. The number of cached images per buffer may be customized using `pdf-cache-image-limit'. ** Annotations *** New supported types Provided epdfinfo was build with a recent version of libpoppler, you may now create and modify the following markup annotation types: highlight, squiggly, underline and strike-out. ** Various *** You may now select extended regions with C-mouse-1. *** Numerous other changes pdf-tools-0.80/README000077700000000000000000000000001315513274600157002README.orgustar00rootroot00000000000000pdf-tools-0.80/README.org000066400000000000000000000373551315513274600150420ustar00rootroot00000000000000#+TITLE: PDF Tools README #+AUTHOR: Andreas Politz #+EMAIL: politza@fh-trier.de [[http://melpa.org/#/pdf-tools][http://melpa.org/packages/pdf-tools-badge.svg]] [[https://travis-ci.org/politza/pdf-tools.svg?branch%3Dmaster][https://travis-ci.org/politza/pdf-tools.svg?branch=master]] ** About this package PDF Tools is, among other things, a replacement of DocView for PDF files. The key difference is, that pages are not pre-rendered by e.g. ghostscript and stored in the file-system, but rather created on-demand and stored in memory. This rendering is performed by a special library named, for whatever reason, poppler, running inside a server program. This program is called ~epdfinfo~ and its job is it to successively read requests from Emacs and produce the proper results, i.e. the PNG image of a PDF page. Actually, displaying PDF files is just one part of PDF Tools. Since poppler can provide us with all kinds of information about a document and is also able to modify it, there is a lot more we can do with it. [[http://www.dailymotion.com/video/x2bc1is_pdf-tools-tourdeforce_tech?forcedQuality%3Dhd720][Watch]] Please read also about [[#known-problems][known problems.]] ** Features + View :: View PDF documents in a buffer with DocView-like bindings. + Isearch :: Interactively search PDF documents like any other buffer, either for a string or a PCRE. + Occur :: List lines matching a string or regexp in one or more PDF documents. + Follow :: Click on highlighted links, moving to some part of a different page, some external file, a website or any other URI. Links may also be followed by keyboard commands. + Annotations :: Display and list text and markup annotations (like underline), edit their contents and attributes (e.g. color), move them around, delete them or create new ones and then save the modifications back to the PDF file. + Attachments :: Save files attached to the PDF-file or list them in a dired buffer. + Outline :: Use imenu or a special buffer to examine and navigate the PDF's outline. + SyncTeX :: Jump from a position on a page directly to the TeX source and vice versa. + Virtual :: Use a collection of documents as if it where one, big single PDF. + Misc :: - Display PDF's metadata. - Mark a region and kill the text from the PDF. - Keep track of visited pages via a history. - Apply a color filter for reading in low light conditions. ** Installation The package may be installed via melpa and it will try to build the server part when it is activated the first time. Though the next section regarding build-prerequisites is still relevant, the rest of the installation instructions assume a build from within a git repository. (The melpa package has a different directory structure.) *** Server Prerequisites You'll need GNU Emacs \ge 24.3 and some form of a GNU/Linux OS. Other operating systems are currently not supported (patches welcome). The following instructions assume a Debian-based system. (The prerequisites may be installed automatically on this kind of systems, see [[#compilation][Compilation]] .) First make sure a suitable build-system is installed. We need at least a C/C++ compiler (both ~gcc~ and ~g++~), ~make~, ~automake~ and ~autoconf~. Next we need to install a few libraries PDF Tools depends on, some of which are probably already on your system. #+begin_src sh $ sudo aptitude install libpng-dev zlib1g-dev $ sudo aptitude install libpoppler-glib-dev $ sudo aptitude install libpoppler-private-dev #+end_src On some older Ubuntu systems, the final command will possibly give an error. This should be no problem, since in some versions this package was contained in the main package ~libpoppler-dev~. Also note, that ~zlib1g-dev~ was for a long time called ~libz-dev~, which it still may be on your system. Debian wheezy comes with libpoppler version 0.18, which is pretty old. The minimally required version is 0.16, but some features of PDF Tools depend on a more recent version of this library. See the following table for what they are and what version they require. | You want to ... | Required version | |-------------------------------------------+------------------| | ... create and modify text annotations. | \ge 0.19.4 | | ... search case-sensitive. | \ge 0.22 | | ... create and modify markup annotations. | \ge 0.26 | |-------------------------------------------+------------------| In case you decide to install libpoppler from source, make sure to run it's configure script with the ~--enable-xpdf-headers~ option. Finally there is one feature (following links of a PDF document by plain keystrokes) which requires imagemagick's convert utility. This requirement is optional and you may install it like so: #+begin_src sh $ sudo aptitude install imagemagick #+end_src **** Compiling on OS X Although OS X is not officially supported, it has been reported to have been successfully compiled. You will need to install poppler which you can get with homebrew via #+BEGIN_SRC sh $ brew install poppler automake #+END_SRC You will also have to help ~pkg-config~ find some libraries by setting ~PKG_CONFIG_PATH~, e.g. #+BEGIN_SRC sh $ export PKG_CONFIG_PATH=/usr/local/Cellar/zlib/1.2.8/lib/pkgconfig:/usr/local/lib/pkgconfig:/opt/X11/lib/pkgconfig #+END_SRC or likewise within Emacs using `setenv`. After that, compilation should proceed as normal. **** Compiling on FreeBSD Although not officially suppported, it has been reported that pdf-tools work well on FreeBSD. Install the dependencies with #+BEGIN_SRC sh $ pkg install autotools gmake poppler-glib #+END_SRC If you choose not to install from melpa, you must substitute ~gmake~ for ~make~ in the instructions below. **** Compiling on Centos It is possible to compile pdf-tools on Centos. Install poppler the dependencies with: #+BEGIN_SRC sh $ yum install poppler-devel poppler-glib-devel #+END_SRC **** Compiling on Fedora #+BEGIN_SRC sh $ sudo dnf install make automake autoconf gcc gcc-c++ ImageMagick libpng-devel zlib-devel poppler-glib-devel #+END_SRC **** Compiling on Windows PDF Tools can be built and used on Windows using the MSYS2 compiler. This will work with native (not cygwin) Windows builds of emacs. This includes the standard binaries provided by the GNU project, those available as MSYS2 packages and numerous third-party binaries. It has been tested with emacs 25.1. Instructions are provided under [[Compilation and installation on Windows]], below. **** Compiling on Cygwin On [[https://www.cygwin.com/][Cygwin]] the following dependencies are needed. - libpoppler-devel (*Lib* category) - libpoppler-glib-devel (*Lib* category) - libpng-devel (*Devel* category) - make (*Devel* category) - gcc-core (*Devel* category) - gcc-g++ (*Devel* category) - autoconf (*Devel* category) - automake (*Devel* category) - perl (*Perl* category) - emacs-w32 (*Editors* category) *** Compilation :PROPERTIES: :CUSTOM_ID: compilation :END: Now it's time to compile the source. #+begin_src sh $ cd /path/to/pdf-tools $ make install-server-deps # optional $ make -s #+end_src The ~make install-server-deps~ command will try to install all necessary programs and libraries to build the package, though it'll only work, if ~sudo~ and ~apt-get~ are available. This should compile the source code and create a Emacs Lisp Package in the root directory of the project. The configure script also tells you at the very end, which features, depending on the libpoppler version, will be available. These commands should give no error, otherwise you are in trouble. **** Compilation and installation on Windows If using the GNU binaries for Windows, support for PNG and zlib must first be installed by copying the appropriate dlls into emacs' ~bin/~ directory. Most third-party binaries come with this already done. First, install [[http://www.msys2.org/][install MSYS2]] and update the package database and core packages using the instructions provided. Then, to compile PDF tools itself: 1. Open msys2 shell 2. Update and install dependencies, skipping any you already have #+BEGIN_SRC sh pacman -Syu pacman -S base-devel pacman -S mingw-w64-x86_64-toolchain pacman -S mingw-w64-x86_64-zlib pacman -S mingw-w64-x86_64-libpng pacman -S mingw-w64-x86_64-poppler pacman -S mingw-w64-x86_64-imagemagick #+END_SRC 3. Install PDF tools in Emacs, but do not try to compile the server. Instead, get a separate copy of the source somewhere else. #+BEGIN_SRC sh git clone https://github.com/politza/pdf-tools #+END_SRC 4. Open mingw64 shell 5. Compile pdf-tools #+BEGIN_SRC sh cd pdf-tools/build make -s #+END_SRC 6. This should produce a file ~server/epdfinfo.exe~. Copy this file into the ~pdf-tools/~ installation directory in your Emacs. 7. Start Emacs and activate the package. #+BEGIN_SRC M-x pdf-tools-install RET #+END_SRC 8. Test. #+BEGIN_SRC M-x pdf-info-check-epdfinfo RET #+END_SRC If this is successful, ~(pdf-tools-install)~ can be added to Emacs' config. Note that libraries from other GNU utilities, such as Git for Windows, may interfere with those needed by PDF Tools. ~pdf-info-check-epdinfo~ will succeed, but errors occur when trying to view a PDF file. This can be fixed by ensuring that the MSYS libraries are always preferred in emacs: #+BEGIN_SRC emacs-lisp (setenv "PATH" (concat "C:\\msys64\\mingw64\\bin;" (getenv "PATH"))) #+END_SRC *** ELisp Prerequisites This package depends on the following Elisp packages, which should be installed before installing the Pdf Tools package. | Package | Required version | |-----------+----------------------------------| | [[https://elpa.gnu.org/packages/let-alist.html][let-alist]] | >= 1.0.4 (comes with Emacs 25.2) | | [[http://melpa.org/#/tablist][tablist]] | >= 0.70 | |-----------+----------------------------------| *** Installing If ~make~ produced the ELP file ~pdf-tools-${VERSION}.tar~ you are fine. This package contains all the necessary files for Emacs and may be installed by either using #+begin_src sh $ make install-package #+end_src or executing the Emacs command #+begin_src elisp M-x package-install-file RET pdf-tools-${VERSION}.tar RET #+end_src To complete the installation process, you need to activate the package by putting #+begin_src elisp (pdf-tools-install) #+end_src somewhere in your ~.emacs~. Next you probably want to take a look at the various features of what you've just installed. The following two commands might be of help for doing so. #+begin_src elisp M-x pdf-tools-help RET M-x pdf-tools-customize RET #+end_src *** Updating Some day you might want to update this package via ~git pull~ and then reinstall it. Sometimes this may fail, especially if Lisp-Macros are involved and the version hasn't changed. To avoid this kind of problems, you should delete the old package via ~list-packages~, restart Emacs and then reinstall the package. This also applies when updating via package and melpa. ** Known problems :PROPERTIES: :CUSTOM_ID: known-problems :END: *** linum-mode PDF Tools does not work well together with ~linum-mode~ and activating it in a ~pdf-view-mode~, e.g. via ~global-linum-mode~, might make Emacs choke. *** auto-revert Autorevert works by polling the file-system every ~auto-revert-interval~ seconds, optionally combined with some event-based reverting via [[https://www.gnu.org/software/emacs/manual/html_node/elisp/File-Notifications.html][file notification]]. But this currently does not work reliably, such that Emacs may revert the PDF-buffer while the corresponding file is still being written to (e.g. by LaTeX), leading to a potential error. With a recent [[https://www.gnu.org/software/auctex/][auctex]] installation, you might want to put the following somewhere in your dotemacs, which will revert the PDF-buffer *after* the TeX compilation has finished. #+BEGIN_SRC emacs-lisp (add-hook 'TeX-after-compilation-finished-functions #'TeX-revert-document-buffer) #+END_SRC ** Some keybindings | Navigation | | |--------------------------------------------+-----------------------| | Scroll Up / Down by page-full | ~space~ / ~backspace~ | | Scroll Up / Down by line | ~C-n~ / ~C-b~ | | Scroll Right / Left | ~C-f~ / ~C-b~ | | Top of Page / Bottom of Page | ~<~ / ~>~ | | Next Page / Previous Page | ~n~ / ~p~ | | First Page / Last Page | ~M-<~ / ~M->~ | | Incremental Search Forward / Backward | ~C-s~ / ~C-r~ | | Occur (list all lines containing a phrase) | ~M-s o~ | | Jump to Occur Line | ~RETURN~ | | Pick a Link and Jump | ~F~ | | Incremental Search in Links | ~f~ | | History Back / Forwards | ~B~ / ~F~ | | Display Outline | ~o~ | | Jump to Section from Outline | ~RETURN~ | | Jump to Page | ~M-g g~ | | Display | | |------------------------------------------+-----------------| | Zoom in / Zoom out | ~+~ / ~-~ | | Fit Height / Fit Width / Fit Page | ~H~ / ~W~ / ~P~ | | Trim margins (set slice to bounding box) | ~s b~ | | Reset margins | ~s r~ | | Reset Zoom | 0 | | Annotations | | |-------------------------------+-------------------------------------------------| | List Annotations | ~C-c C-a l~ | | Jump to Annotations from List | ~SPACE~ | | Mark Annotation for Deletion | ~d~ | | Delete Marked Annotations | ~x~ | | Unmark Annotations | ~u~ | | Close Annotation List | ~q~ | | Add and edit annotations | via Mouse selection and left-click context menu | | Syncing with Auctex | | |----------------------------------+-------------| | jump to PDF location from source | ~C-c C-g~ | | jump source location from PDF | ~C-mouse-1~ | | Miscellaneous | | |-----------------------------------------------+-----------| | Refresh File (e.g., after recompiling source) | ~g~ | | Print File | ~C-c C-p~ | # Local Variables: # mode: org # End: pdf-tools-0.80/TODO000066400000000000000000000020501315513274600140440ustar00rootroot00000000000000-*- org -*- * pdf-isearch ** Allow for entering multi-byte characters with some input-methods. The PDF buffer is in uni-byte mode prohibiting the user from inserting multi-byte characters in the minibuffer with some input-methods, while editing the search string. * PDF Forms Recent poppler versions have some support for editing forms. * pdf-annot ** Updating the list buffer is too slow + Update it incrementally. + Possibly skip the update if the buffer is not visible. ** Make highlighting customizable * epdfinfo ** Maybe split the code up in several files. * pdf-view ** Provide some kind of multi-page view ** Make persistent scrolling relative Currently the scrolling is kept when changing the image's size (in pdf-view-display-image), which is actually not so desirable, since it is absolute. This results e.g. in the image popping out of the window, when it is shrunken. * pdf-info ** Add a report/debug command, displaying a list of open files and other information. ** Use alists for results instead of positional lists. pdf-tools-0.80/lisp/000077500000000000000000000000001315513274600143265ustar00rootroot00000000000000pdf-tools-0.80/lisp/.gitignore000066400000000000000000000000251315513274600163130ustar00rootroot00000000000000*.elc .dir-locals.el pdf-tools-0.80/lisp/pdf-annot.el000066400000000000000000001677651315513274600165640ustar00rootroot00000000000000;;; pdf-annot.el --- Annotation support for PDF files. -*- lexical-binding: t -*- ;; Copyright (C) 2013, 2014 Andreas Politz ;; Author: Andreas Politz ;; Keywords: ;; 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 . ;;; Commentary: ;; (require 'pdf-view) (require 'pdf-info) (require 'pdf-cache) (require 'pdf-misc) (require 'facemenu) ;; list-colors-duplicates (require 'faces) ;; color-values (require 'org) ;; org-create-formula-image (require 'tablist) (require 'cl-lib) ;; * ================================================================== * ;; * Customizations ;; * ================================================================== * (defgroup pdf-annot nil "Annotation support for PDF documents." :group 'pdf-tools) (defcustom pdf-annot-activate-handler-functions nil "A list of functions to activate a annotation. The functions on this hook will be called when some annotation is activated, usually by a mouse-click. Each one is called with the annotation as a single argument and it should return a non-nil value if it has `handled' it. If no such function exists, the default handler `pdf-annot-default-handler' will be called. This hook is meant to allow for custom annotations. FIXME: Implement and describe basic org example." :group 'pdf-annot :type 'hook) (defcustom pdf-annot-default-text-annotation-properties `((icon . "Note") (color . "#ff0000") (label . ,user-full-name) (popup-is-open . nil)) "Alist of initial properties for new text annotations. FIXME: Describe. Restrict keys and values." :group 'pdf-annot) (defcustom pdf-annot-default-markup-annotation-properties `((label . ,user-full-name) (popup-is-open . nil)) "Alist of initial properties for new markup annotations. FIXME: Describe. Restrict keys and values." :group 'pdf-annot) (defcustom pdf-annot-print-annotation-functions '(pdf-annot-print-annotation-latex-maybe) "A alist of functions for printing annotations, e.g. for the tooltip. The functions receive the annotation as single argument and should return either a string or nil. The first string returned will be used. If all of them return nil, the default function `pdf-annot-print-annotation-default' is used." :group 'pdf-annot :type 'hook) (defcustom pdf-annot-latex-string-predicate (lambda (str) (and str (string-match "\\`[[:space:]\n]*[$\\]" str))) "A predicate for recognizing LaTeX fragments. It receives a string and should return non-nil, if string is a LaTeX fragment." :group 'pdf-annot :type 'function) (defcustom pdf-annot-latex-header (concat org-format-latex-header "\n\\setlength{\\textwidth}{12cm}") "Header used when latex compiling annotations. The default value is `org-format-latex-header' + \ \"\\n\\\\setlength{\\\\textwidth}{12cm}\"." :group 'pdf-annot :type 'string) (defcustom pdf-annot-tweak-tooltips t "Whether this package should tweak some settings regarding tooltips. If this variable has a non-nil value, `x-gtk-use-system-tooltips' is set to nil if appropriate, in order to display text properties; `tooltip-hide-delay' is set to infinity, in order to not being annoyed while reading the annotations." :group 'pdf-annot) (defcustom pdf-annot-activate-created-annotations nil "Whether to activate (i.e. edit) created annotations." :group 'pdf-annot :type 'boolean) (defcustom pdf-annot-attachment-display-buffer-action nil "The display action used when displaying attachments." :group 'pdf-annot :type display-buffer--action-custom-type) (defconst pdf-annot-annotation-types '(3d caret circle file free-text highlight ink line link movie poly-line polygon popup printer-mark screen sound square squiggly stamp strike-out text trap-net underline unknown watermark widget) "Complete list of annotation types.") (defcustom pdf-annot-list-listed-types (if (pdf-info-markup-annotations-p) (list 'text 'file 'squiggly 'highlight 'underline 'strike-out) (list 'text 'file)) "A list of annotation types displayed in the list buffer." :group 'pdf-annot :type `(set ,@(mapcar (lambda (type) (list 'const type)) pdf-annot-annotation-types))) ;; * ================================================================== * ;; * Variables and Macros ;; * ================================================================== * (defvar pdf-annot-color-history nil "A list of recently used colors for annotations.") (defvar-local pdf-annot-modified-functions nil "Functions to call, when an annotation was modified. A function on this hook should accept one argument: A CLOSURE containing inserted, changed and deleted annotations. It may access theses annotations by calling CLOSURE with one of these arguments: `:inserted' The list of recently added annotations. `:deleted' The list of recently deleted annotations. `:changed' The list of recently changed annotations. `t' The union of recently added, deleted or changed annotations. `nil' Just returns nil. Any other argument signals an error.") (defconst pdf-annot-text-annotation-size '(24 . 24) "The Size of text and file annotations in PDF points. These values are hard-coded in poppler. And while the size of these annotations may be changed, i.e. the edges property, it has no effect on the rendering.") (defconst pdf-annot-markup-annotation-types '(text link free-text line square circle polygon poly-line highlight underline squiggly strike-out stamp caret ink file sound) "List of defined markup annotation types.") (defconst pdf-annot-standard-text-icons '("Note" "Comment" "Key" "Help" "NewParagraph" "Paragraph" "Insert" "Cross" "Circle") "A list of standard icon properties for text annotations.") (defvar pdf-annot-inhibit-modification-hooks nil "Non-nil, if running `pdf-annot-modified-functions' should be inhibited after some annotation has changed.") (defvar-local pdf-annot-delayed-modified-annotations nil "A plist of not yet propagated modifications. It contains three entries :change, :delete and :insert. Each one having a list of annotations as value.") (defvar-local pdf-annot--attachment-file-alist nil "Alist mapping attachment ids to unique relative filenames.") (defmacro pdf-annot-with-atomic-modifications (&rest body) "Execute BODY joining multiple modifications. The effect is, that `pdf-annot-modified-functions' will be called only once at the end of BODY. BODY should not modify annotations in a different then the current buffer, because that won't run the hooks properly." (declare (indent 0) (debug t)) `(unwind-protect (save-current-buffer (let ((pdf-annot-inhibit-modification-hooks t)) (progn ,@body))) (pdf-annot-run-modified-hooks))) ;; * ================================================================== * ;; * Minor mode ;; * ================================================================== * (defcustom pdf-annot-minor-mode-map-prefix (kbd "C-c C-a") "The prefix to use for `pdf-annot-minor-mode-map'. Setting this after the package was loaded has no effect." :group 'pdf-annot :type 'key-sequence) (defvar pdf-annot-minor-mode-map (let ((kmap (make-sparse-keymap)) (smap (make-sparse-keymap))) (define-key kmap pdf-annot-minor-mode-map-prefix smap) (define-key smap "l" 'pdf-annot-list-annotations) ;; (define-key smap "d" 'pdf-annot-toggle-display-annotations) (define-key smap "a" 'pdf-annot-attachment-dired) (when (pdf-info-writable-annotations-p) (define-key smap "D" 'pdf-annot-delete) (define-key smap "t" 'pdf-annot-add-text-annotation) (when (pdf-info-markup-annotations-p) (define-key smap "m" 'pdf-annot-add-markup-annotation) (define-key smap "s" 'pdf-annot-add-squiggly-markup-annotation) (define-key smap "u" 'pdf-annot-add-underline-markup-annotation) (define-key smap "o" 'pdf-annot-add-strikeout-markup-annotation) (define-key smap "h" 'pdf-annot-add-highlight-markup-annotation))) kmap) "Keymap used for `pdf-annot-minor-mode'.") (defvar savehist-minibuffer-history-variables) ;;;###autoload (define-minor-mode pdf-annot-minor-mode "Support for PDF Annotations. \\{pdf-annot-minor-mode-map}" nil nil nil (cond (pdf-annot-minor-mode (when pdf-annot-tweak-tooltips (when (boundp 'x-gtk-use-system-tooltips) (setq x-gtk-use-system-tooltips nil)) (setq tooltip-hide-delay 3600)) (pdf-view-add-hotspot-function 'pdf-annot-hotspot-function 9) (add-hook 'pdf-info-close-document-hook 'pdf-annot-attachment-delete-base-directory nil t) (when (featurep 'savehist) (add-to-list 'savehist-minibuffer-history-variables 'pdf-annot-color-history))) (t (pdf-view-remove-hotspot-function 'pdf-annot-hotspot-function) (remove-hook 'pdf-info-close-document-hook 'pdf-annot-attachment-delete-base-directory t))) (pdf-view-redisplay t)) (defun pdf-annot-create-context-menu (a) "Create a appropriate context menu for annotation A." (let ((menu (make-sparse-keymap))) ;; (when (and (bound-and-true-p pdf-misc-menu-bar-minor-mode) ;; (bound-and-true-p pdf-misc-install-popup-menu)) ;; (set-keymap-parent menu ;; (lookup-key pdf-misc-menu-bar-minor-mode-map ;; [menu-bar pdf-tools])) ;; (define-key menu [sep-99] menu-bar-separator)) (when (pdf-info-writable-annotations-p) (define-key menu [delete-annotation] `(menu-item "Delete annotation" ,(lambda () (interactive) (pdf-annot-delete a) (message "Annotation deleted")) :help "Delete this annotation."))) (define-key menu [goto-annotation] `(menu-item "List annotation" ,(lambda () (interactive) (pdf-annot-show-annotation a t) (pdf-annot-list-annotations) (pdf-annot-list-goto-annotation a)) :help "Find this annotation in the list buffer.")) (when (pdf-annot-text-annotation-p a) (define-key menu [change-text-icon] `(menu-item "Change icon" ,(pdf-annot-create-icon-submenu a) :help "Change the appearance of this annotation."))) (define-key menu [change-color] `(menu-item "Change color" ,(pdf-annot-create-color-submenu a) :help "Change the appearance of this annotation.")) (define-key menu [activate-annotation] `(menu-item "Activate" ,(lambda () (interactive) (pdf-annot-activate-annotation a)) :help "Activate this annotation.")) menu)) (defun pdf-annot-create-color-submenu (a) (let ((menu (make-sparse-keymap))) (define-key menu [color-chooser] `(menu-item "Choose ..." ,(lambda () (interactive) (list-colors-display nil "*Choose annotation color*" ;; list-colors-print does not like closures. (let ((callback (make-symbol "xcallback"))) (fset callback (lambda (color) (pdf-annot-put a 'color color) (setq pdf-annot-color-history (cons color (remove color pdf-annot-color-history))) (quit-window t))) (list 'function callback)))))) (dolist (color (butlast (reverse pdf-annot-color-history) (max 0 (- (length pdf-annot-color-history) 12)))) (define-key menu (vector (intern (format "color-%s" color))) `(menu-item ,color ,(lambda nil (interactive) (pdf-annot-put a 'color color))))) menu)) (defun pdf-annot-create-icon-submenu (a) (let ((menu (make-sparse-keymap))) (dolist (icon (reverse pdf-annot-standard-text-icons)) (define-key menu (vector (intern (format "icon-%s" icon))) `(menu-item ,icon ,(lambda nil (interactive) (pdf-annot-put a 'icon icon))))) menu)) ;; * ================================================================== * ;; * Annotation Basics ;; * ================================================================== * (defun pdf-annot-create (alist &optional buffer) "Create a annotation from ALIST in BUFFER. ALIST should be a property list as returned by `pdf-cache-getannots'. BUFFER should be the buffer of the corresponding PDF document. It defaults to the current buffer." (cons `(buffer . ,(or buffer (current-buffer))) alist)) (defun pdf-annot-getannots (&optional pages types buffer) "Return a list of annotations on PAGES of TYPES in BUFFER. See `pdf-info-normalize-pages' for valid values of PAGES. TYPES may be a symbol or list of symbols denoting annotation types. PAGES defaults to all pages, TYPES to all types and BUFFER to the current buffer." (pdf-util-assert-pdf-buffer buffer) (unless buffer (setq buffer (current-buffer))) (unless (listp types) (setq types (list types))) (with-current-buffer buffer (let (result) (dolist (a (pdf-info-getannots pages)) (when (or (null types) (memq (pdf-annot-get a 'type) types)) (push (pdf-annot-create a) result))) result))) (defun pdf-annot-getannot (id &optional buffer) (pdf-annot-create (pdf-info-getannot id buffer) buffer)) (defun pdf-annot-get (a property &optional default) "Get annotation A's value of PROPERTY. Return DEFAULT, if value is nil." (or (cdr (assq property a)) default)) (defun pdf-annot-put (a property value) "Set annotation A's PROPERTY to VALUE. Unless VALUE is `equal' to the current value, sets A's buffer's modified flag and runs the hook `pdf-annot-modified-functions'. Signals an error, if PROPERTY is not modifiable. Returns the modified annotation." (declare (indent 2)) (unless (equal value (pdf-annot-get a property)) (unless (pdf-annot-property-modifiable-p a property) (error "Property `%s' is read-only for this annotation" property)) (with-current-buffer (pdf-annot-get-buffer a) (setq a (pdf-annot-create (pdf-info-editannot (pdf-annot-get-id a) `((,property . ,value))))) (set-buffer-modified-p t) (pdf-annot-run-modified-hooks :change a))) a) (defun pdf-annot-run-modified-hooks (&optional operation &rest annotations) "Run `pdf-annot-modified-functions' using OPERATION on ANNOTATIONS. OPERATION should be one of nil, :change, :insert or :delete. If nil, annotations should be empty. Redisplay modified pages. If `pdf-annot-inhibit-modification-hooks' in non-nil, this just saves ANNOTATIONS and does not call the hooks until later, when the variable is nil and this function is called again." (unless (memq operation '(nil :insert :change :delete)) (error "Invalid operation: %s" operation)) (when (and (null operation) annotations) (error "Missing operation argument")) (when operation (let ((list (plist-get pdf-annot-delayed-modified-annotations operation))) (dolist (a annotations) (cl-pushnew a list :test 'pdf-annot-equal)) (setq pdf-annot-delayed-modified-annotations (plist-put pdf-annot-delayed-modified-annotations operation list)))) (unless pdf-annot-inhibit-modification-hooks (let* ((changed (plist-get pdf-annot-delayed-modified-annotations :change)) (inserted (mapcar (lambda (a) (or (car (cl-member a changed :test 'pdf-annot-equal)) a)) (plist-get pdf-annot-delayed-modified-annotations :insert))) (deleted (plist-get pdf-annot-delayed-modified-annotations :delete)) (union (cl-union (cl-union changed inserted :test 'pdf-annot-equal) deleted :test 'pdf-annot-equal)) (closure (lambda (arg) (cl-ecase arg (:inserted (copy-sequence inserted)) (:changed (copy-sequence changed)) (:deleted (copy-sequence deleted)) (t (copy-sequence union)) (nil nil)))) (pages (mapcar (lambda (a) (pdf-annot-get a 'page)) union))) (when union (unwind-protect (run-hook-with-args 'pdf-annot-modified-functions closure) (setq pdf-annot-delayed-modified-annotations nil) (apply 'pdf-view-redisplay-pages pages)))))) (defun pdf-annot-equal (a1 a2) "Return non-nil, if annotations A1 and A2 are equal. Two annotations are equal, if they belong to the same buffer and have identical id properties." (and (eq (pdf-annot-get-buffer a1) (pdf-annot-get-buffer a2)) (eq (pdf-annot-get-id a1) (pdf-annot-get-id a2)))) (defun pdf-annot-get-buffer (a) "Return annotation A's buffer." (pdf-annot-get a 'buffer)) (defun pdf-annot-get-id (a) "Return id property of annotation A." (pdf-annot-get a 'id)) (defun pdf-annot-get-type (a) "Return type property of annotation A." (pdf-annot-get a 'type)) (defun pdf-annot-get-display-edges (a) "Return a list of EDGES used for display for annotation A. This returns a list of \(LEFT TOP RIGHT BOT\) demarking the rectangles of the page where A is rendered." (or (pdf-annot-get a 'markup-edges) (list (pdf-annot-get a 'edges)))) (defun pdf-annot-delete (a) "Delete annotation A. Sets A's buffer's modified flag and runs the hook `pdf-annot-modified-functions'. This function always returns nil." (interactive (list (pdf-annot-read-annotation "Click on the annotation you wish to delete"))) (with-current-buffer (pdf-annot-get-buffer a) (pdf-info-delannot (pdf-annot-get-id a)) (set-buffer-modified-p t) (pdf-annot-run-modified-hooks :delete a)) (when (called-interactively-p 'any) (message "Annotation deleted")) nil) (defun pdf-annot-text-annotation-p (a) (eq 'text (pdf-annot-get a 'type))) (defun pdf-annot-markup-annotation-p (a) (not (null (memq (pdf-annot-get a 'type) pdf-annot-markup-annotation-types)))) (defun pdf-annot-property-modifiable-p (a property) (or (memq property '(edges color flags contents)) (and (pdf-annot-markup-annotation-p a) (memq property '(label opacity popup popup-is-open))) (and (pdf-annot-text-annotation-p a) (memq property '(icon is-open))))) (defun pdf-annot-activate-annotation (a) (or (run-hook-with-args-until-success 'pdf-annot-activate-handler-functions a) (pdf-annot-default-activate-handler a))) (defun pdf-annot-default-activate-handler (a) (cond ((pdf-annot-has-attachment-p a) (pdf-annot-pop-to-attachment a)) (t (pdf-annot-edit-contents a)))) ;; * ================================================================== * ;; * Handling attachments ;; * ================================================================== * (defun pdf-annot-has-attachment-p (a) "Return non-nil if annotation A's has data attached." (eq 'file (pdf-annot-get a 'type))) (defun pdf-annot-get-attachment (a &optional do-save) "Retrieve annotation A's attachment. The DO-SAVE argument is given to `pdf-info-getattachment-from-annot', which see." (unless (pdf-annot-has-attachment-p a) (error "Annotation has no data attached: %s" a)) (pdf-info-getattachment-from-annot (pdf-annot-get-id a) do-save (pdf-annot-get-buffer a))) (defun pdf-annot-attachment-base-directory () "Return the base directory for saving attachments." (let ((dir (pdf-util-expand-file-name "attachments"))) (unless (file-exists-p dir) (make-directory dir)) dir)) (defun pdf-annot-attachment-delete-base-directory () "Delete all saved attachment files of the current buffer." (setq pdf-annot--attachment-file-alist nil) (delete-directory (pdf-annot-attachment-base-directory) t)) (defun pdf-annot-attachment-unique-filename (attachment) "Return a unique absolute filename for ATTACHMENT." (let* ((filename (or (cdr (assq 'filename attachment)) "attachment")) (id (cdr (assq 'id attachment))) (unique (or (cdr (assoc id pdf-annot--attachment-file-alist)) (let* ((sans-ext (expand-file-name (concat (file-name-as-directory ".") (file-name-sans-extension filename)) (pdf-annot-attachment-base-directory))) (ext (file-name-extension filename)) (newname (concat sans-ext "." ext)) (i 0)) (while (rassoc newname pdf-annot--attachment-file-alist) (setq newname (format "%s-%d.%s" sans-ext (cl-incf i) ext))) (push (cons id newname) pdf-annot--attachment-file-alist) newname))) (directory (file-name-directory unique))) (unless (file-exists-p directory) (make-directory directory t)) unique)) (defun pdf-annot-attachment-save (attachment &optional regenerate-p) "Save ATTACHMENT's data to a unique filename and return it's name. If REGENERATE-P is non-nil, copy attachment's file even if the copy already exists. Signal an error, if ATTACHMENT has no, or a non-existing, `file' property, i.e. it was retrieved with an unset do-save argument. See `pdf-info-getattachments'" (let ((datafile (cdr (assq 'file attachment)))) (unless (and datafile (file-exists-p datafile)) (error "Attachment's file property is invalid")) (let* ((filename (pdf-annot-attachment-unique-filename attachment))) (when (or regenerate-p (not (file-exists-p filename))) (copy-file datafile filename nil nil t t)) filename))) (defun pdf-annot-find-attachment-noselect (a) "Find annotation A's attachment in a buffer, without selecting it. Signals an error, if A has no data attached." (let ((attachment (pdf-annot-get-attachment a t))) (unwind-protect (find-file-noselect (pdf-annot-attachment-save attachment)) (let ((tmpfile (cdr (assq 'file attachment)))) (when (and tmpfile (file-exists-p tmpfile)) (delete-file tmpfile)))))) (defun pdf-annot-attachment-dired (&optional regenerate-p) "List all attachments in a dired buffer. If REGENERATE-P is non-nil, create attachment's files even if they already exist. Interactively REGENERATE-P is non-nil if a prefix argument was given. Return the dired buffer." (interactive (list current-prefix-arg)) (let ((attachments (pdf-info-getattachments t))) (unwind-protect (progn (dolist (a (pdf-annot-getannots nil 'file)) (push (pdf-annot-get-attachment a t) attachments )) (dolist (att attachments) (pdf-annot-attachment-save att regenerate-p)) (unless attachments (error "Document has no data attached")) (dired (pdf-annot-attachment-base-directory))) (dolist (att attachments) (let ((tmpfile (cdr (assq 'file att)))) (when (and tmpfile (file-exists-p tmpfile)) (delete-file tmpfile))))))) (defun pdf-annot-display-attachment (a &optional display-action select-window-p) "Display file annotation A's data in a buffer. DISPLAY-ACTION should be a valid `display-buffer' action. If nil, `pdf-annot-attachment-display-buffer-action' is used. Select the window, if SELECT-WINDOW-P is non-nil. Return the window attachment is displayed in." (interactive (list (pdf-annot-read-annotation "Select a file annotation by clicking on it"))) (let* ((buffer (pdf-annot-find-attachment-noselect a)) (window (display-buffer buffer (or display-action pdf-annot-attachment-display-buffer-action)))) (when select-window-p (select-window window)) window)) (defun pdf-annot-pop-to-attachment (a) "Display annotation A's attachment in a window and select it." (interactive (list (pdf-annot-read-annotation "Select a file annotation by clicking on it"))) (pdf-annot-display-attachment a nil t)) ;; * ================================================================== * ;; * Interfacing with the display ;; * ================================================================== * (defun pdf-annot-image-position (a &optional image-size) "Return the position of annotation A in image coordinates. IMAGE-SIZE should be a cons \(WIDTH . HEIGHT\) and defaults to the page-image of the selected window." (unless image-size (pdf-util-assert-pdf-window) (setq image-size (pdf-view-image-size))) (let ((e (pdf-util-scale (pdf-annot-get a 'edges) image-size))) (pdf-util-with-edges (e) `(,e-left . ,e-top)))) (defun pdf-annot-image-set-position (a x y &optional image-size) "Set annotation A's position to X,Y in image coordinates. See `pdf-annot-image-position' for IMAGE-SIZE." (unless image-size (pdf-util-assert-pdf-window) (setq image-size (pdf-view-image-size))) (let* ((edges (pdf-annot-get a 'edges)) (x (/ x (float (car image-size)))) (y (/ y (float (cdr image-size))))) (pdf-util-with-edges (edges) (let* ((w edges-width) (h edges-height) (x (max 0 (min x (- 1 w)))) (y (max 0 (min y (- 1 h))))) (pdf-annot-put a 'edges (list x y -1 -1)))))) (defun pdf-annot-image-size (a &optional image-size) "Return the size of annotation A in image coordinates. Returns \(WIDTH . HEIGHT\). See `pdf-annot-image-position' for IMAGE-SIZE." (unless image-size (pdf-util-assert-pdf-window) (setq image-size (pdf-view-image-size))) (let ((edges (pdf-util-scale (pdf-annot-get a 'edges) image-size))) (pdf-util-with-edges (edges) (cons edges-width edges-height)))) (defun pdf-annot-image-set-size (a &optional width height image-size) "Set annotation A's size in image to WIDTH and/or HEIGHT. See `pdf-annot-image-position' for IMAGE-SIZE." (unless image-size (pdf-util-assert-pdf-window) (setq image-size (pdf-view-image-size))) (let* ((edges (pdf-annot-get a 'edges)) (w (and width (/ width (float (car image-size))))) (h (and height (/ height (float (cdr image-size)))))) (pdf-util-with-edges (edges) (pdf-annot-put a 'edges (list edges-left edges-top (if w (+ edges-left w) edges-right) (if h (+ edges-top h) edges-bot)))))) (defun pdf-annot-at-position (pos) "Return annotation at POS in the selected window. POS should be an absolute image position as a cons \(X . Y\). Alternatively POS may also be an event position, in which case `posn-window' and `posn-object-x-y' is used to find the image position. Return nil, if no annotation was found." (let (window) (when (posnp pos) (setq window (posn-window pos) pos (posn-object-x-y pos))) (save-selected-window (when window (select-window window)) (let* ((annots (pdf-annot-getannots (pdf-view-current-page))) (size (pdf-view-image-size)) (rx (/ (car pos) (float (car size)))) (ry (/ (cdr pos) (float (cdr size)))) (rpos (cons rx ry))) (or (cl-some (lambda (a) (and (cl-some (lambda (e) (pdf-util-edges-inside-p e rpos)) (pdf-annot-get-display-edges a)) a)) annots) (error "No annotation at this position")))))) (defun pdf-annot-mouse-move (event &optional annot) "Start moving an annotation at EVENT's position. EVENT should be a mouse event originating the request and is used as a reference point. ANNOT is the annotation to operate on and defaults to the annotation at EVENT's start position. This function does not return until the operation is completed, i.e. a non mouse-movement event is read." (interactive "@e") (pdf-util-assert-pdf-window (posn-window (event-start event))) (select-window (posn-window (event-start event))) (let* ((mpos (posn-object-x-y (event-start event))) (a (or annot (pdf-annot-at-position mpos)))) (unless a (error "No annotation at this position: %s" mpos)) (let* ((apos (pdf-annot-image-position a)) (offset (cons (- (car mpos) (car apos)) (- (cdr mpos) (cdr apos)))) (window (selected-window)) make-pointer-invisible) (when (pdf-util-track-mouse-dragging (ev 0.1) (when (and (eq window (posn-window (event-start ev))) (eq 'image (car-safe (posn-object (event-start ev))))) (let ((pdf-view-inhibit-hotspots t) (pdf-annot-inhibit-modification-hooks t) (pdf-cache-image-inihibit t) (xy (posn-object-x-y (event-start ev)))) (pdf-annot-image-set-position a (- (car xy) (car offset)) (- (cdr xy) (cdr offset))) (pdf-view-redisplay)))) (pdf-annot-run-modified-hooks))) nil)) (defun pdf-annot-hotspot-function (page size) "Create image hotspots for page PAGE of size SIZE." (apply 'nconc (mapcar (lambda (a) (unless (eq (pdf-annot-get a 'type) 'link) (pdf-annot-create-hotspots a size))) (pdf-annot-getannots page)))) (defun pdf-annot-create-hotspots (a size) "Return a list of image hotspots for annotation A." (let ((id (pdf-annot-get-id a)) (edges (pdf-util-scale (pdf-annot-get-display-edges a) size 'round)) (moveable-p (memq (pdf-annot-get a 'type) '(file text))) hotspots) (dolist (e edges) (pdf-util-with-edges (e) (push `((rect . ((,e-left . ,e-top) . (,e-right . ,e-bot))) ,id (pointer hand help-echo ,(pdf-annot-print-annotation a))) hotspots))) (pdf-annot-create-hotspot-binding id moveable-p a) hotspots)) ;; FIXME: Define a keymap as a template for this. Much cleaner. (defun pdf-annot-create-hotspot-binding (id moveable-p annotation) ;; Activating (local-set-key (vector id 'mouse-1) (lambda () (interactive) (pdf-annot-activate-annotation annotation))) ;; Move (when moveable-p (local-set-key (vector id 'down-mouse-1) (lambda (ev) (interactive "@e") (pdf-annot-mouse-move ev annotation)))) ;; Context Menu (local-set-key (vector id 'down-mouse-3) (lambda () (interactive "@") (popup-menu (pdf-annot-create-context-menu annotation)))) ;; Everything else (local-set-key (vector id t) 'pdf-util-image-map-mouse-event-proxy)) (defun pdf-annot-show-annotation (a &optional highlight-p window) "Make annotation A visible. Turn to A's page in WINDOW, and scroll it if necessary. If HIGHLIGHT-P is non-nil, visually distinguish annotation A from other annotations." (save-selected-window (when window (select-window window)) (pdf-util-assert-pdf-window) (let ((page (pdf-annot-get a 'page)) (size (pdf-view-image-size))) (unless (= page (pdf-view-current-page)) (pdf-view-goto-page page)) (let ((edges (pdf-annot-get-display-edges a))) (when highlight-p (pdf-view-display-image (pdf-view-create-image (pdf-cache-renderpage-highlight page (car size) `("white" "steel blue" 0.35 ,@edges)) :map (pdf-view-apply-hotspot-functions window page size)))) (pdf-util-scroll-to-edges (pdf-util-scale-relative-to-pixel (car edges))))))) (defun pdf-annot-read-annotation (&optional prompt) "Let the user choose a annotation a mouse click using PROMPT." (pdf-annot-at-position (pdf-util-read-image-position (or prompt "Choose a annotation by clicking on it")))) ;; * ================================================================== * ;; * Creating annotations ;; * ================================================================== * (defun pdf-annot-add-annotation (type edges &optional property-alist page) "Creates and adds a new annotation of type TYPE to the document. TYPE determines the kind of annotation to add and maybe one of `text', `squiggly', `underline', `strike-out' or `highlight'. EDGES determines where the annotation will appear on the page. If type is `text', this should be a single list of \(LEFT TOP RIGHT BOT\). Though, in this case only LEFT and TOP are used, since the size of text annotations is fixed. Otherwise EDGES may be a list of such elements. All values should be image relative coordinates, i.e. in the range \[0;1\]. PROPERTY-ALIST is a list of annotation properties, which will be put on the created annotation. PAGE determines the page of the annotation. It defaults to the page currently displayed in the selected window. Signal an error, if PROPERTY-ALIST contains non-modifiable properties or PAGE is nil and the selected window does not display a PDF document or creating annotations of type TYPE is not supported. Set buffers modified flag and calls `pdf-annot-activate-annotation' if `pdf-annot-activate-created-annotations' is non-nil. Return the new annotation." (unless (memq type (pdf-info-creatable-annotation-types)) (error "Unsupported annotation type: %s" type)) (unless page (pdf-util-assert-pdf-window) (setq page (pdf-view-current-page))) (unless (consp (car-safe edges)) (setq edges (list edges))) (when (and (eq type 'text) (> (length edges) 1)) (error "Edges argument should be a single edge-list for text annotations")) (let* ((a (apply 'pdf-info-addannot page (if (eq type 'text) (car edges) '(0 0 0 0)) type nil (if (not (eq type 'text)) edges))) (id (pdf-annot-get-id a))) (when property-alist (condition-case err (setq a (pdf-info-editannot id property-alist)) (error (pdf-info-delannot id) (signal (car err) (cdr err))))) (setq a (pdf-annot-create a)) (set-buffer-modified-p t) (pdf-annot-run-modified-hooks :insert a) (when pdf-annot-activate-created-annotations (pdf-annot-activate-annotation a)) a)) (defun pdf-annot-add-text-annotation (pos &optional icon property-alist) "Add a new text annotation at POS in the selected window. POS should be a image position object or a cons \(X . Y\), both being image coordinates. ICON determines how the annotation is displayed and should be listed in `pdf-annot-standard-text-icons'. Any other value is ok as well, but will render the annotation invisible. Adjust X and Y accordingly, if the position would render the annotation off-page. Merge ICON as a icon property with PROPERTY-ALIST and `pdf-annot-default-text-annotation-properties' and apply the result to the created annotation. See also `pdf-annot-add-annotation'. Return the new annotation." (interactive (let* ((posn (pdf-util-read-image-position "Click where a new text annotation should be added ...")) (window (posn-window posn))) (select-window window) (list posn))) (pdf-util-assert-pdf-window) (when (posnp pos) (setq pos (posn-object-x-y pos))) (let ((isize (pdf-view-image-size)) (x (car pos)) (y (cdr pos))) (unless (and (>= x 0) (< x (car isize))) (signal 'args-out-of-range (list pos))) (unless (and (>= y 0) (< y (cdr isize))) (signal 'args-out-of-range (list pos))) (let ((size (pdf-util-scale-points-to-pixel pdf-annot-text-annotation-size 'round))) (setcar size (min (car size) (car isize))) (setcdr size (min (cdr size) (cdr isize))) (cl-decf x (max 0 (- (+ x (car size)) (car isize)))) (cl-decf y (max 0 (- (+ y (cdr size)) (cdr isize)))) (pdf-annot-add-annotation 'text (pdf-util-scale-pixel-to-relative (list x y -1 -1)) (pdf-annot-merge-alists (and icon `((icon . ,icon))) property-alist pdf-annot-default-text-annotation-properties `((color . ,(car pdf-annot-color-history)))))))) (defun pdf-annot-mouse-add-text-annotation (ev) (interactive "@e") (pdf-annot-add-text-annotation (if (eq (car-safe ev) 'menu-bar) (let (echo-keystrokes) (message nil) (pdf-util-read-image-position "Click where a new text annotation should be added ...")) (event-start ev)))) (defun pdf-annot-add-markup-annotation (list-of-edges type &optional color property-alist) "Add a new markup annotation in the selected window. LIST-OF-EDGES determines the marked up area and should be a list of \(LEFT TOP RIGHT BOT\), each value a relative coordinate. TYPE should be one of `squiggly', `underline', `strike-out' or `highlight'. Merge COLOR as a color property with PROPERTY-ALIST and `pdf-annot-default-markup-annotation-properties' and apply the result to the created annotation. See also `pdf-annot-add-annotation'. Return the new annotation." (interactive (list (pdf-view-active-region t) (let ((type (completing-read "Markup type (default highlight): " '("squiggly" "highlight" "underline" "strike-out") nil t))) (if (equal type "") 'highlight (intern type))) (pdf-annot-read-color))) (pdf-util-assert-pdf-window) (pdf-annot-add-annotation type list-of-edges (pdf-annot-merge-alists (and color `((color . ,color))) property-alist pdf-annot-default-markup-annotation-properties (when pdf-annot-color-history `((color . ,(car pdf-annot-color-history)))) '((color . "#ffff00"))) (pdf-view-current-page))) (defun pdf-annot-add-squiggly-markup-annotation (list-of-edges &optional color property-alist) "Add a new squiggly annotation in the selected window. See also `pdf-annot-add-markup-annotation'." (interactive (list (pdf-view-active-region t))) (pdf-annot-add-markup-annotation list-of-edges 'squiggly color property-alist)) (defun pdf-annot-add-underline-markup-annotation (list-of-edges &optional color property-alist) "Add a new underline annotation in the selected window. See also `pdf-annot-add-markup-annotation'." (interactive (list (pdf-view-active-region t))) (pdf-annot-add-markup-annotation list-of-edges 'underline color property-alist)) (defun pdf-annot-add-strikeout-markup-annotation (list-of-edges &optional color property-alist) "Add a new strike-out annotation in the selected window. See also `pdf-annot-add-markup-annotation'." (interactive (list (pdf-view-active-region t))) (pdf-annot-add-markup-annotation list-of-edges 'strike-out color property-alist)) (defun pdf-annot-add-highlight-markup-annotation (list-of-edges &optional color property-alist) "Add a new highlight annotation in the selected window. See also `pdf-annot-add-markup-annotation'." (interactive (list (pdf-view-active-region t))) (pdf-annot-add-markup-annotation list-of-edges 'highlight color property-alist)) (defun pdf-annot-read-color (&optional prompt) "Read and return a color using PROMPT. Offer `pdf-annot-color-history' as default values." (let* ((defaults (append (delq nil (list (cdr (assq 'color pdf-annot-default-markup-annotation-properties)) (cdr (assq 'color pdf-annot-default-text-annotation-properties)))) pdf-annot-color-history)) (prompt (format "%s%s: " (or prompt "Color") (if defaults (format " (default %s)" (car defaults)) ""))) (current-completing-read-function completing-read-function) (completing-read-function (lambda (prompt collection &optional predicate require-match initial-input _hist _def inherit-input-method) (funcall current-completing-read-function prompt collection predicate require-match initial-input 'pdf-annot-color-history defaults inherit-input-method)))) (read-color prompt))) (defun pdf-annot-merge-alists (&rest alists) "Merge ALISTS into a single one. Suppresses successive duplicate entries of keys after the first occurrence in ALISTS." (let (merged) (dolist (elt (apply 'append alists)) (unless (assq (car elt) merged) (push elt merged))) (nreverse merged))) ;; * ================================================================== * ;; * Displaying annotation contents ;; * ================================================================== * (defun pdf-annot-print-property (a property) "Pretty print annotation A's property PROPERTY." (let ((value (pdf-annot-get a property))) (cl-case property (color (propertize (or value "") 'face (and value `(:background ,value)))) ((created modified) (let ((date value)) (if (null date) "No date" (current-time-string date)))) ;; print verbatim (subject (or value "No subject")) (opacity (let ((opacity (or value 1.0))) (format "%d%%" (round (* 100 opacity))))) (t (format "%s" (or value "")))))) (defun pdf-annot-print-annotation (a) "Pretty print annotation A." (or (run-hook-with-args-until-success 'pdf-annot-print-annotation-functions a) (pdf-annot-print-annotation-default a))) (defun pdf-annot-print-annotation-default (a) "Default pretty printer for annotation A. The result consists of a header (as printed with `pdf-annot-print-annotation-header') a newline and A's contents property." (concat (pdf-annot-print-annotation-header a) "\n" (pdf-annot-get a 'contents))) (defun pdf-annot-print-annotation-header (a) "Emit a suitable header string for annotation A." (let ((header (cond ((eq 'file (pdf-annot-get a 'type)) (let ((att (pdf-annot-get-attachment a))) (format "File attachment `%s' of %s" (or (cdr (assq 'filename att)) "unnamed") (if (cdr (assq 'size att)) (format "size %s" (file-size-human-readable (cdr (assq 'size att)))) "unknown size")))) (t (format "%s" (mapconcat 'identity (mapcar (lambda (property) (pdf-annot-print-property a property)) `(subject label modified)) ";")))))) (setq header (propertize header 'face 'header-line 'intangible t 'read-only t)) ;; This `trick' makes the face apply in a tooltip. (propertize header 'display header))) (defun pdf-annot-print-annotation-latex-maybe (a) "Maybe print annotation A's content as a LaTeX fragment. See `pdf-annot-latex-string-predicate'." (when (and (functionp pdf-annot-latex-string-predicate) (funcall pdf-annot-latex-string-predicate (pdf-annot-get a 'contents))) (pdf-annot-print-annotation-latex a))) (defun pdf-annot-print-annotation-latex (a) "Print annotation A's content as a LaTeX fragment. This compiles A's contents as a LaTeX fragment and puts the resulting image as a display property on the contents, prefixed by a header." (let (tempfile) (unwind-protect (with-current-buffer (pdf-annot-get-buffer a) (let* ((page (pdf-annot-get a 'page)) (header (pdf-annot-print-annotation-header a)) (contents (pdf-annot-get a 'contents)) (hash (sxhash (format "pdf-annot-print-annotation-latex%s%s%s" page header contents))) (data (pdf-cache-lookup-image page 0 nil hash)) (org-format-latex-header pdf-annot-latex-header) (temporary-file-directory (pdf-util-expand-file-name "pdf-annot-print-annotation-latex"))) (unless (file-directory-p temporary-file-directory) (make-directory temporary-file-directory)) (unless data (setq tempfile (make-temp-file "pdf-annot" nil ".png")) ;; FIXME: Why is this with-temp-buffer needed (which it is) ? (with-temp-buffer (org-create-formula-image contents tempfile org-format-latex-options t)) (setq data (pdf-util-munch-file tempfile)) (if (and (> (length data) 3) (equal (substring data 1 4) "PNG")) (pdf-cache-put-image page 0 data hash) (setq data nil))) (concat header "\n" (if data (propertize contents 'display (pdf-view-create-image data)) (propertize contents 'display (concat (propertize "Failed to compile latex fragment\n" 'face 'error) contents)))))) (when (and tempfile (file-exists-p tempfile)) (delete-file tempfile))))) ;; * ================================================================== * ;; * Editing annotation contents ;; * ================================================================== * (defvar-local pdf-annot-edit-contents--annotation nil) (put 'pdf-annot-edit-contents--annotation 'permanent-local t) (defvar-local pdf-annot-edit-contents--buffer nil) (defcustom pdf-annot-edit-contents-setup-function (lambda (a) (let ((mode (if (funcall pdf-annot-latex-string-predicate (pdf-annot-get a 'contents)) 'latex-mode 'text-mode))) (unless (derived-mode-p mode) (funcall mode)))) "A function for setting up, e.g. the major-mode, of the edit buffer. The function receives one argument, the annotation whose contents is about to be edited in this buffer. The default value turns on `latex-mode' if `pdf-annot-latex-string-predicate' returns non-nil on the annotation's contents and otherwise `text-mode'. " :group 'pdf-annot :type 'function) (defcustom pdf-annot-edit-contents-display-buffer-action '((display-buffer-reuse-window display-buffer-split-below-and-attach) (inhibit-same-window . t) (window-height . 0.25)) "Display action when showing the edit buffer." :group 'pdf-annot :type display-buffer--action-custom-type) (defvar pdf-annot-edit-contents-minor-mode-map (let ((kmap (make-sparse-keymap))) (set-keymap-parent kmap text-mode-map) (define-key kmap (kbd "C-c C-c") 'pdf-annot-edit-contents-commit) (define-key kmap (kbd "C-c C-q") 'pdf-annot-edit-contents-abort) kmap)) (define-minor-mode pdf-annot-edit-contents-minor-mode "Active when editing the contents of annotations." nil nil nil (when pdf-annot-edit-contents-minor-mode (message "%s" (substitute-command-keys "Press \\[pdf-annot-edit-contents-commit] to commit your changes, \\[pdf-annot-edit-contents-abort] to abandon them.")))) (put 'pdf-annot-edit-contents-minor-mode 'permanent-local t) ;; FIXME: Document pdf-annot-edit-* functions below. (defun pdf-annot-edit-contents-finalize (do-save &optional do-kill) (when (buffer-modified-p) (cond ((eq do-save 'ask) (save-window-excursion (display-buffer (current-buffer) nil (selected-frame)) (when (y-or-n-p "Save changes to this annotation ?") (pdf-annot-edit-contents-save-annotation)))) (do-save (pdf-annot-edit-contents-save-annotation))) (set-buffer-modified-p nil)) (dolist (win (get-buffer-window-list)) (quit-window do-kill win))) (defun pdf-annot-edit-contents-save-annotation () (when pdf-annot-edit-contents--annotation (pdf-annot-put pdf-annot-edit-contents--annotation 'contents (buffer-substring-no-properties (point-min) (point-max))) (set-buffer-modified-p nil))) (defun pdf-annot-edit-contents-commit () (interactive) (pdf-annot-edit-contents-finalize t)) (defun pdf-annot-edit-contents-abort () (interactive) (pdf-annot-edit-contents-finalize nil t)) (defun pdf-annot-edit-contents-noselect (a) (with-current-buffer (pdf-annot-get-buffer a) (when (and (buffer-live-p pdf-annot-edit-contents--buffer) (not (eq a pdf-annot-edit-contents--annotation))) (with-current-buffer pdf-annot-edit-contents--buffer (pdf-annot-edit-contents-finalize 'ask))) (unless (buffer-live-p pdf-annot-edit-contents--buffer) (setq pdf-annot-edit-contents--buffer (with-current-buffer (get-buffer-create (format "*Edit Annotation %s*" (buffer-name))) (pdf-annot-edit-contents-minor-mode 1) (current-buffer)))) (with-current-buffer pdf-annot-edit-contents--buffer (setq pdf-annot-edit-contents--annotation a) (funcall pdf-annot-edit-contents-setup-function a) (let ((inhibit-read-only t)) (erase-buffer) (save-excursion (insert (pdf-annot-get a 'contents))) (set-buffer-modified-p nil) (current-buffer))))) (defun pdf-annot-edit-contents (a) (select-window (display-buffer (pdf-annot-edit-contents-noselect a) pdf-annot-edit-contents-display-buffer-action))) (defun pdf-annot-edit-contents-mouse (ev) (interactive "@e") (let* ((pos (posn-object-x-y (event-start ev))) (a (and pos (pdf-annot-at-position pos)))) (unless a (error "No annotation at this position")) (pdf-annot-edit-contents a))) ;; * ================================================================== * ;; * Listing annotations ;; * ================================================================== * (defcustom pdf-annot-list-display-buffer-action '((display-buffer-reuse-window display-buffer-pop-up-window) (inhibit-same-window . t)) "Display action used when displaying the list buffer." :group 'pdf-annot) (defvar-local pdf-annot-list-buffer nil) (defvar-local pdf-annot-list-document-buffer nil) (defvar pdf-annot-list-mode-map (let ((km (make-sparse-keymap))) (define-key km (kbd "C-c C-f") 'pdf-annot-list-follow-minor-mode) (define-key km (kbd "SPC") 'pdf-annot-list-display-annotation-from-id) km)) (defun pdf-annot-property-completions (property) "Return a list of completion candidates for annotation property PROPERTY. Return nil, if not available." (cl-case property (color (pdf-util-color-completions)) (icon (copy-sequence pdf-annot-standard-text-icons)))) (defun pdf-annot-compare-annotations (a1 a2) "Compare annotations A1 and A2. Return non-nil if A1's page is less than A2's one or if they belong to the same page and A1 is displayed above/left of A2." (let ((p1 (pdf-annot-get a1 'page)) (p2 (pdf-annot-get a2 'page))) (or (< p1 p2) (and (= p1 p2) (let ((e1 (pdf-util-scale (car (pdf-annot-get-display-edges a1)) '(1000 . 1000))) (e2 (pdf-util-scale (car (pdf-annot-get-display-edges a2)) '(1000 . 1000)))) (pdf-util-with-edges (e1 e2) (or (< e1-top e2-top) (and (= e1-top e2-top) (<= e1-left e2-left))))))))) (defun pdf-annot-list-entries () (unless (buffer-live-p pdf-annot-list-document-buffer) (error "No PDF document associated with this buffer")) (mapcar 'pdf-annot-list-create-entry (sort (pdf-annot-getannots nil pdf-annot-list-listed-types pdf-annot-list-document-buffer) 'pdf-annot-compare-annotations))) (defun pdf-annot-list-create-entry (a) "Create a `tabulated-list-entries' entry for annotation A." (list (pdf-annot-get-id a) (vector (pdf-annot-print-property a 'page) (pdf-annot-print-property a 'type) (replace-regexp-in-string "\n" " " (pdf-annot-print-property a 'label) t t) (if (pdf-annot-get a 'modified) (pdf-annot-print-property a 'modified) (if (pdf-annot-get a 'created) (pdf-annot-print-property a 'created) "Unknown date"))))) (define-derived-mode pdf-annot-list-mode tablist-mode "Annots" (setq tabulated-list-entries 'pdf-annot-list-entries tabulated-list-format (vector '("Pg." 3 t :read-only t :right-align t) `("Type" 10 t :read-only t) `("Label" 24 t :read-only t) '("Date" 24 t :read-only t)) tabulated-list-padding 2) (set-keymap-parent pdf-annot-list-mode-map tablist-mode-map) (use-local-map pdf-annot-list-mode-map) (setq tablist-current-filter `(not (== "Type" "link"))) (tabulated-list-init-header)) (defun pdf-annot-list-annotations () "List annotations in a dired like buffer. \\{pdf-annot-list-mode-map}" (interactive) (pdf-util-assert-pdf-buffer) (let ((buffer (current-buffer))) (with-current-buffer (get-buffer-create (format "*%s's annots*" (file-name-sans-extension (buffer-name)))) (unless (derived-mode-p 'pdf-annot-list-mode) (pdf-annot-list-mode)) (setq pdf-annot-list-document-buffer buffer) (tabulated-list-print) (setq tablist-context-window-function (lambda (id) (pdf-annot-list-context-function id buffer)) tablist-operations-function 'pdf-annot-list-operation-function) (let ((list-buffer (current-buffer))) (with-current-buffer buffer (setq pdf-annot-list-buffer list-buffer))) (pop-to-buffer (current-buffer) pdf-annot-list-display-buffer-action) (tablist-move-to-major-column) (tablist-display-context-window)) (add-hook 'pdf-info-close-document-hook 'pdf-annot-list-update nil t) (add-hook 'pdf-annot-modified-functions 'pdf-annot-list-update nil t))) (defun pdf-annot-list-goto-annotation (a) (with-current-buffer (pdf-annot-get-buffer a) (unless (and (buffer-live-p pdf-annot-list-buffer) (get-buffer-window pdf-annot-list-buffer)) (pdf-annot-list-annotations)) (with-selected-window (get-buffer-window pdf-annot-list-buffer) (goto-char (point-min)) (let ((id (pdf-annot-get-id a))) (while (and (not (eobp)) (not (eq id (tabulated-list-get-id)))) (forward-line)) (unless (eq id (tabulated-list-get-id)) (error "Unable to find annotation")) (when (invisible-p (point)) (tablist-suspend-filter t)) (tablist-move-to-major-column))))) (defun pdf-annot-list-update (&optional _fn) (when (buffer-live-p pdf-annot-list-buffer) (with-current-buffer pdf-annot-list-buffer (unless tablist-edit-column-minor-mode (tablist-revert)) (tablist-context-window-update)))) (defun pdf-annot-list-context-function (id buffer) (with-current-buffer (get-buffer-create "*Contents*") (set-window-buffer nil (current-buffer)) (let ((inhibit-read-only t)) (erase-buffer) (when id (save-excursion (insert (pdf-annot-print-annotation (pdf-annot-getannot id buffer))))) (read-only-mode 1)))) (defun pdf-annot-list-operation-function (op &rest args) (cl-ecase op (supported-operations '(delete find-entry)) (delete (cl-destructuring-bind (ids) args (when (buffer-live-p pdf-annot-list-document-buffer) (with-current-buffer pdf-annot-list-document-buffer (pdf-annot-with-atomic-modifications (dolist (a (mapcar 'pdf-annot-getannot ids)) (pdf-annot-delete a))))))) (find-entry (cl-destructuring-bind (id) args (unless (buffer-live-p pdf-annot-list-document-buffer) (error "No PDF document associated with this buffer")) (let* ((buffer pdf-annot-list-document-buffer) (a (pdf-annot-getannot id buffer)) (pdf-window (save-selected-window (or (get-buffer-window buffer) (display-buffer buffer)))) window) (with-current-buffer buffer (pdf-annot-activate-annotation a) (setq window (selected-window))) ;; Make it so that quitting the edit window returns to the ;; list window. (unless (memq window (list (selected-window) pdf-window)) (let* ((quit-restore (window-parameter window 'quit-restore))) (when quit-restore (setcar (nthcdr 2 quit-restore) (selected-window)))))))))) (defvar pdf-annot-list-display-annotation--timer nil) (defun pdf-annot-list-display-annotation-from-id (id) (interactive (list (tabulated-list-get-id))) (when id (unless (buffer-live-p pdf-annot-list-document-buffer) (error "PDF buffer was killed")) (when (timerp pdf-annot-list-display-annotation--timer) (cancel-timer pdf-annot-list-display-annotation--timer)) (setq pdf-annot-list-display-annotation--timer (run-with-idle-timer 0.1 nil (lambda (buffer a) (when (buffer-live-p buffer) (with-selected-window (or (get-buffer-window buffer) (display-buffer buffer '(nil (inhibit-same-window . t)))) (pdf-annot-show-annotation a t)))) pdf-annot-list-document-buffer (pdf-annot-getannot id pdf-annot-list-document-buffer))))) (define-minor-mode pdf-annot-list-follow-minor-mode "" nil nil nil (unless (derived-mode-p 'pdf-annot-list-mode) (error "No in pdf-annot-list-mode.")) (cond (pdf-annot-list-follow-minor-mode (add-hook 'tablist-selection-changed-functions 'pdf-annot-list-display-annotation-from-id nil t) (let ((id (tabulated-list-get-id))) (when id (pdf-annot-list-display-annotation-from-id id)))) (t (remove-hook 'tablist-selection-changed-functions 'pdf-annot-list-display-annotation-from-id t)))) (provide 'pdf-annot) ;;; pdf-annot.el ends here pdf-tools-0.80/lisp/pdf-cache.el000066400000000000000000000376141315513274600164750ustar00rootroot00000000000000;;; pdf-cache.el --- Cache time-critical or frequent epdfinfo queries. -*- lexical-binding:t -*- ;; Copyright (C) 2013 Andreas Politz ;; Author: Andreas Politz ;; Keywords: files, doc-view, pdf ;; 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 . ;;; Commentary: ;; ;;; Code: ;; (require 'pdf-info) (require 'pdf-util) ;; * ================================================================== * ;; * Customiazations ;; * ================================================================== * (defcustom pdf-cache-image-limit 64 "Maximum number of cached PNG images per buffer." :type 'integer :group 'pdf-cache :group 'pdf-view) (defcustom pdf-cache-prefetch-delay 0.5 "Idle time in seconds before prefetching images starts." :group 'pdf-view :type 'number) (defcustom pdf-cache-prefetch-pages-function 'pdf-cache-prefetch-pages-function-default "A function returning a list of pages to be prefetched. It is called with no arguments in the PDF window and should return a list of page-numbers, determining the pages that should be prefetched and their order." :group 'pdf-view :type 'function) ;; * ================================================================== * ;; * Simple Value cache ;; * ================================================================== * (defvar-local pdf-cache--data nil) (defvar pdf-annot-modified-functions) (defun pdf-cache--initialize () (unless pdf-cache--data (setq pdf-cache--data (make-hash-table)) (add-hook 'pdf-info-close-document-hook 'pdf-cache-clear-data nil t) (add-hook 'pdf-annot-modified-functions 'pdf-cache--clear-data-of-annotations nil t))) (defun pdf-cache--clear-data-of-annotations (fn) (apply 'pdf-cache-clear-data-of-pages (mapcar (lambda (a) (cdr (assq 'page a))) (funcall fn t)))) (defun pdf-cache--data-put (key value &optional page) "Put KEY with VALUE in the cache of PAGE, return value." (pdf-cache--initialize) (puthash page (cons (cons key value) (assq-delete-all key (gethash page pdf-cache--data))) pdf-cache--data) value) (defun pdf-cache--data-get (key &optional page) "Get value of KEY in the cache of PAGE. Returns a cons \(HIT . VALUE\), where HIT is non-nil if KEY was stored previously for PAGE and VALUE it's value. Otherwise HIT is nil and VALUE undefined." (pdf-cache--initialize) (let ((elt (assq key (gethash page pdf-cache--data)))) (if elt (cons t (cdr elt)) (cons nil nil)))) (defun pdf-cache--data-clear (key &optional page) (pdf-cache--initialize) (puthash page (assq-delete-all key (gethash page pdf-cache--data)) pdf-cache--data) nil) (defun pdf-cache-clear-data-of-pages (&rest pages) (when pdf-cache--data (dolist (page pages) (remhash page pdf-cache--data)))) (defun pdf-cache-clear-data () (interactive) (when pdf-cache--data (clrhash pdf-cache--data))) (defmacro define-pdf-cache-function (command &optional page-arg-p) "Define a simple data cache function. COMMAND is the name of the command, e.g. number-of-pages. It should have a corresponding pdf-info function. If PAGE-ARG-P is non-nil, define a one-dimensional cache indexed by the page number. Otherwise the value is constant for each document, like e.g. number-of-pages. Both args are unevaluated." (let ((args (if page-arg-p (list 'page))) (fn (intern (format "pdf-cache-%s" command))) (ifn (intern (format "pdf-info-%s" command))) (doc (format "Cached version of `pdf-info-%s', which see. Make sure, not to modify it's return value." command))) `(defun ,fn ,args ,doc (let ((hit-value (pdf-cache--data-get ',command ,(if page-arg-p 'page)))) (if (car hit-value) (cdr hit-value) (pdf-cache--data-put ',command ,(if page-arg-p (list ifn 'page) (list ifn)) ,(if page-arg-p 'page))))))) (define-pdf-cache-function pagelinks t) (define-pdf-cache-function number-of-pages) ;; The boundingbox may change if annotations change. (define-pdf-cache-function boundingbox t) (define-pdf-cache-function textregions t) (define-pdf-cache-function pagesize t) ;; * ================================================================== * ;; * PNG image LRU cache ;; * ================================================================== * (defvar pdf-cache-image-inihibit nil "Non-nil, if the image cache should be bypassed.") (defvar-local pdf-cache--image-cache nil) (defmacro pdf-cache--make-image (page width data hash) `(list ,page ,width ,data ,hash)) (defmacro pdf-cache--image/page (img) `(nth 0 ,img)) (defmacro pdf-cache--image/width (img) `(nth 1 ,img)) (defmacro pdf-cache--image/data (img) `(nth 2 ,img)) (defmacro pdf-cache--image/hash (img) `(nth 3 ,img)) (defun pdf-cache--image-match (image page min-width &optional max-width hash) "Match IMAGE with specs. IMAGE should be a list as created by `pdf-cache--make-image'. Return non-nil, if IMAGE's page is the same as PAGE, it's width is at least MIN-WIDTH and at most MAX-WIDTH and it's stored hash-value is `eql' to HASH." (and (= (pdf-cache--image/page image) page) (or (null min-width) (>= (pdf-cache--image/width image) min-width)) (or (null max-width) (<= (pdf-cache--image/width image) max-width)) (eql (pdf-cache--image/hash image) hash))) (defun pdf-cache-lookup-image (page min-width &optional max-width hash) "Return PAGE's cached PNG data as a string or nil. Does not modify the cache. See also `pdf-cache-get-image'." (let ((image (car (cl-member (list page min-width max-width hash) pdf-cache--image-cache :test (lambda (spec image) (apply 'pdf-cache--image-match image spec)))))) (and image (pdf-cache--image/data image)))) (defun pdf-cache-get-image (page min-width &optional max-width hash) "Return PAGE's PNG data as a string. Return an image of at least MIN-WIDTH and, if non-nil, maximum width MAX-WIDTH and `eql' hash value. Remember that image was recently used. Returns nil, if no matching image was found." (let ((cache (cons nil pdf-cache--image-cache)) image) ;; Find it in the cache and remove it. We need to find the ;; element in front of it. (while (and (cdr cache) (not (pdf-cache--image-match (car (cdr cache)) page min-width max-width hash))) (setq cache (cdr cache))) (setq image (cadr cache)) (when (car cache) (setcdr cache (cddr cache))) ;; Now push it at the front. (when image (push image pdf-cache--image-cache) (pdf-cache--image/data image)))) (defun pdf-cache-put-image (page width data &optional hash) "Cache image of PAGE with WIDTH, DATA and HASH. DATA should the string of a PNG image of width WIDTH and from page PAGE in the current buffer. See `pdf-cache-get-image' for the HASH argument. This function always returns nil." (unless pdf-cache--image-cache (add-hook 'pdf-info-close-document-hook 'pdf-cache-clear-images nil t) (add-hook 'pdf-annot-modified-functions 'pdf-cache--clear-images-of-annotations nil t)) (push (pdf-cache--make-image page width data hash) pdf-cache--image-cache) ;; Forget old image(s). (when (> (length pdf-cache--image-cache) pdf-cache-image-limit) (if (> pdf-cache-image-limit 1) (setcdr (nthcdr (1- pdf-cache-image-limit) pdf-cache--image-cache) nil) (setq pdf-cache--image-cache nil))) nil) (defun pdf-cache-clear-images () "Clear the image cache." (setq pdf-cache--image-cache nil)) (defun pdf-cache-clear-images-if (fn) "Remove images from the cache according to FN. FN should be function accepting 4 Arguments \(PAGE WIDTH DATA HASH\). It should return non-nil, if the image should be removed from the cache." (setq pdf-cache--image-cache (cl-remove-if (lambda (image) (funcall fn (pdf-cache--image/page image) (pdf-cache--image/width image) (pdf-cache--image/data image) (pdf-cache--image/hash image))) pdf-cache--image-cache))) (defun pdf-cache--clear-images-of-annotations (fn) (apply 'pdf-cache-clear-images-of-pages (mapcar (lambda (a) (cdr (assq 'page a))) (funcall fn t)))) (defun pdf-cache-clear-images-of-pages (&rest pages) (pdf-cache-clear-images-if (lambda (page &rest _) (memq page pages)))) (defun pdf-cache-renderpage (page min-width &optional max-width) "Render PAGE according to MIN-WIDTH and MAX-WIDTH. Return the PNG data of an image as a string, such that it's width is at least MIN-WIDTH and, if non-nil, at most MAX-WIDTH. If such an image is not available in the cache, call `pdf-info-renderpage' to create one." (if pdf-cache-image-inihibit (pdf-info-renderpage page min-width) (or (pdf-cache-get-image page min-width max-width) (let ((data (pdf-info-renderpage page min-width))) (pdf-cache-put-image page min-width data) data)))) (defun pdf-cache-renderpage-text-regions (page width single-line-p &rest selection) "Render PAGE according to WIDTH, SINGLE-LINE-P and SELECTION. See also `pdf-info-renderpage-text-regions' and `pdf-cache-renderpage'." (if pdf-cache-image-inihibit (apply 'pdf-info-renderpage-text-regions page width single-line-p nil selection) (let ((hash (sxhash (format "%S" (cons 'renderpage-text-regions (cons single-line-p selection)))))) (or (pdf-cache-get-image page width width hash) (let ((data (apply 'pdf-info-renderpage-text-regions page width single-line-p nil selection))) (pdf-cache-put-image page width data hash) data))))) (defun pdf-cache-renderpage-highlight (page width &rest regions) "Highlight PAGE according to WIDTH and REGIONS. See also `pdf-info-renderpage-highlight' and `pdf-cache-renderpage'." (if pdf-cache-image-inihibit (apply 'pdf-info-renderpage-highlight page width nil regions) (let ((hash (sxhash (format "%S" (cons 'renderpage-highlight regions))))) (or (pdf-cache-get-image page width width hash) (let ((data (apply 'pdf-info-renderpage-highlight page width nil regions))) (pdf-cache-put-image page width data hash) data))))) ;; * ================================================================== * ;; * Prefetching images ;; * ================================================================== * (defvar-local pdf-cache--prefetch-pages nil "Pages to be prefetched.") (defvar-local pdf-cache--prefetch-timer nil "Timer used when prefetching images.") (define-minor-mode pdf-cache-prefetch-minor-mode "Try to load images which will probably be needed in a while." nil nil t (pdf-cache--prefetch-cancel) (cond (pdf-cache-prefetch-minor-mode (pdf-util-assert-pdf-buffer) (add-hook 'pre-command-hook 'pdf-cache--prefetch-stop nil t) (setq pdf-cache--prefetch-timer (run-with-idle-timer (or pdf-cache-prefetch-delay 1) t 'pdf-cache--prefetch-start (current-buffer)))) (t (remove-hook 'pre-command-hook 'pdf-cache--prefetch-stop t)))) (defun pdf-cache-prefetch-pages-function-default () (let ((page (pdf-view-current-page))) (pdf-util-remove-duplicates (cl-remove-if-not (lambda (page) (and (>= page 1) (<= page (pdf-cache-number-of-pages)))) (append ;; +1, -1, +2, -2, ... (let ((sign 1) (incr 1)) (mapcar (lambda (_) (setq page (+ page (* sign incr)) sign (- sign) incr (1+ incr)) page) (number-sequence 1 16))) ;; First and last (list 1 (pdf-cache-number-of-pages)) ;; Links (mapcar (apply-partially 'alist-get 'page) (cl-remove-if-not (lambda (link) (eq (alist-get 'type link) 'goto-dest)) (pdf-cache-pagelinks (pdf-view-current-page))))))))) (defun pdf-cache--prefetch-pages (window image-width) (when (and (eq window (selected-window)) (pdf-util-pdf-buffer-p)) (let ((page (pop pdf-cache--prefetch-pages))) (while (and page (pdf-cache-lookup-image page image-width (if (not (pdf-view-use-scaling-p)) image-width (* 2 image-width)))) (setq page (pop pdf-cache--prefetch-pages))) (pdf-util-debug (when (null page) (message "Prefetching done."))) (when page (let* ((buffer (current-buffer)) (pdf-info-asynchronous (lambda (status data) (when (and (null status) (eq window (selected-window)) (eq buffer (window-buffer))) (with-current-buffer (window-buffer) (when (derived-mode-p 'pdf-view-mode) (pdf-cache-put-image page image-width data) (image-size (pdf-view-create-page page)) (pdf-util-debug (message "Prefetched page %s." page)) ;; Avoid max-lisp-eval-depth (run-with-timer 0.001 nil 'pdf-cache--prefetch-pages window image-width))))))) (condition-case err (pdf-info-renderpage page image-width) (error (pdf-cache-prefetch-minor-mode -1) (signal (car err) (cdr err))))))))) (defvar pdf-cache--prefetch-started-p nil "Guard against multiple prefetch starts. Used solely in `pdf-cache--prefetch-start'.") (defun pdf-cache--prefetch-start (buffer) "Start prefetching images in BUFFER." (when (and pdf-cache-prefetch-minor-mode (not pdf-cache--prefetch-started-p) (pdf-util-pdf-buffer-p) (not isearch-mode) (null pdf-cache--prefetch-pages) (eq (window-buffer) buffer) (fboundp pdf-cache-prefetch-pages-function)) (let* ((pdf-cache--prefetch-started-p t) (pages (funcall pdf-cache-prefetch-pages-function))) (setq pdf-cache--prefetch-pages (butlast pages (max 0 (- (length pages) pdf-cache-image-limit)))) (pdf-cache--prefetch-pages (selected-window) (car (pdf-view-desired-image-size)))))) (defun pdf-cache--prefetch-stop () "Stop prefetching images in current buffer." (setq pdf-cache--prefetch-pages nil)) (defun pdf-cache--prefetch-cancel () "Cancel prefetching images in current buffer." (pdf-cache--prefetch-stop) (when pdf-cache--prefetch-timer (cancel-timer pdf-cache--prefetch-timer)) (setq pdf-cache--prefetch-timer nil)) (provide 'pdf-cache) ;;; pdf-cache.el ends here pdf-tools-0.80/lisp/pdf-dev.el000066400000000000000000000055051315513274600162020ustar00rootroot00000000000000;;; pdf-dev.el --- Mother's little development helper -*- lexical-binding: t; -*- ;; Copyright (C) 2015 Andreas Politz ;; Author: Andreas Politz ;; Keywords: ;; 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 . ;;; Commentary: ;; ;; This file is only ment for developers. The entry point is ;; pdf-dev-minor-mode, which see. ;;; Code: (defvar pdf-dev-root-directory (file-name-directory (directory-file-name (file-name-directory load-file-name)))) (defun pdf-dev-reload () "Reload lisp files from source." (interactive) (let ((default-directory (expand-file-name "lisp" pdf-dev-root-directory)) loaded) (dolist (file (directory-files default-directory nil "\\`pdf-\\w*\\.el\\'")) (push file loaded) (load-file file)) (message "Loaded %s" (mapconcat 'identity loaded " ")))) (define-minor-mode pdf-dev-minor-mode "Make developing pdf-tools easier. It does the following: Quits the server and sets `pdf-info-epdfinfo-program' to ../server/epdfinfo. Installs a `compilation-finish-functions' which will restart epdfinfo after a successful recompilation. Sets up `load-path' and reloads all PDF Tools lisp files." nil nil nil (let ((lisp-dir (expand-file-name "lisp" pdf-dev-root-directory))) (setq load-path (remove lisp-dir load-path)) (cond (pdf-dev-minor-mode (add-hook 'compilation-finish-functions 'pdf-dev-compilation-finished) (add-to-list 'load-path lisp-dir) (setq pdf-info-epdfinfo-program (expand-file-name "epdfinfo" (expand-file-name "server" pdf-dev-root-directory))) (pdf-info-quit) (pdf-dev-reload)) (t (remove-hook 'compilation-finish-functions 'pdf-dev-compilation-finished))))) (defun pdf-dev-compilation-finished (buffer status) (with-current-buffer buffer (when (and (equal status "finished\n") (file-equal-p (expand-file-name "server" pdf-dev-root-directory) default-directory)) (message "Restarting epdfinfo server") (pdf-info-quit) (let ((pdf-info-restart-process-p t)) (pdf-info-process-assert-running))))) (provide 'pdf-dev) ;;; pdf-dev.el ends here pdf-tools-0.80/lisp/pdf-history.el000066400000000000000000000117761315513274600171340ustar00rootroot00000000000000;;; pdf-history.el --- A simple stack-based history in PDF buffers. -*- lexical-binding: t -*- ;; Copyright (C) 2013, 2014 Andreas Politz ;; Author: Andreas Politz ;; Keywords: files, multimedia ;; 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 . ;;; Commentary: ;; (require 'pdf-view) (require 'pdf-util) ;;; Code: (defgroup pdf-history nil "A simple stack-based history." :group 'pdf-tools) (defvar-local pdf-history-stack nil "The stack of history items.") (defvar-local pdf-history-index nil "The current index into the `pdf-history-stack'.") (defvar pdf-history-minor-mode-map (let ((kmap (make-sparse-keymap))) (define-key kmap (kbd "B") 'pdf-history-backward) (define-key kmap (kbd "N") 'pdf-history-forward) kmap) "Keymap used in `pdf-history-minor-mode'.") ;;;###autoload (define-minor-mode pdf-history-minor-mode "Keep a history of previously visited pages. This is a simple stack-based history. Turning the page or following a link pushes the left-behind page on the stack, which may be navigated with the following keys. \\{pdf-history-minor-mode-map}" nil nil nil (pdf-util-assert-pdf-buffer) (pdf-history-clear) (cond (pdf-history-minor-mode (pdf-history-push) (add-hook 'pdf-view-after-change-page-hook 'pdf-history-before-change-page-hook nil t)) (t (remove-hook 'pdf-view-after-change-page-hook 'pdf-history-before-change-page-hook t)))) (defun pdf-history-before-change-page-hook () "Push a history item, before leaving this page." (when (and pdf-history-minor-mode (not (bound-and-true-p pdf-isearch-active-mode)) (pdf-view-current-page)) (pdf-history-push))) (defun pdf-history-push () "Push the current page on the stack. This function does nothing, if current stack item already represents the current page." (interactive) (let ((item (pdf-history-create-item))) (unless (and pdf-history-stack (equal (nth pdf-history-index pdf-history-stack) item)) (setq pdf-history-stack (last pdf-history-stack (- (length pdf-history-stack) pdf-history-index)) pdf-history-index 0) (push item pdf-history-stack)))) (defun pdf-history-clear () "Remove all history items." (interactive) (setq pdf-history-stack nil pdf-history-index 0) (pdf-history-push)) (defun pdf-history-create-item () "Create a history item representing the current page." (list (pdf-view-current-page))) (defun pdf-history-beginning-of-history-p () "Return t, if at the beginning of the history." (= pdf-history-index 0)) (defun pdf-history-end-of-history-p () "Return t, if at the end of the history." (= pdf-history-index (1- (length pdf-history-stack)))) (defun pdf-history-backward (n) "Go N-times backward in the history." (interactive "p") (cond ((and (> n 0) (pdf-history-end-of-history-p)) (error "End of history")) ((and (< n 0) (pdf-history-beginning-of-history-p)) (error "Beginning of history")) ((/= n 0) (let ((i (min (max 0 (+ pdf-history-index n)) (1- (length pdf-history-stack))))) (prog1 (- (+ pdf-history-index n) i) (pdf-history-goto i)))) (t 0))) (defun pdf-history-forward (n) "Go N-times forward in the history." (interactive "p") (pdf-history-backward (- n))) (defun pdf-history-goto (n) "Go to item N in the history." (interactive "p") (when (null pdf-history-stack) (error "The history is empty")) (cond ((>= n (length pdf-history-stack)) (error "End of history")) ((< n 0) (error "Beginning of history")) (t (setq pdf-history-index n) (pdf-view-goto-page (car (nth n pdf-history-stack)))))) (defun pdf-history-debug () "Visualize the history in the header-line." (interactive) (setq header-line-format '(:eval (let ((pages (mapcar 'car pdf-history-stack)) (index pdf-history-index) header) (dotimes (i (length pages)) (push (propertize (format "%s" (nth i pages)) 'face (and (= i index) 'match)) header)) (concat "(" (format "%d" index) ") " (mapconcat 'identity (nreverse header) " | ")))))) (provide 'pdf-history) ;;; pdf-history.el ends here pdf-tools-0.80/lisp/pdf-info.el000066400000000000000000001702761315513274600163670ustar00rootroot00000000000000;;; pdf-info.el --- Extract info from pdf-files via a helper process. -*- lexical-binding: t -*- ;; Copyright (C) 2013, 2014 Andreas Politz ;; Author: Andreas Politz ;; Keywords: files, multimedia ;; 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 . ;;; Commentary: ;; ;; This library represents the Lisp side of the epdfinfo server. This ;; program works on a command/response basis, but there should be no ;; need to understand the protocol, since every command has a ;; corresponding Lisp-function (see below under `High level ;; interface'). ;; ;; Most of these functions receive a file-or-buffer argument, which ;; may be what it says and defaults to the current buffer. Also, most ;; functions return some sort of alist, with, in most cases, ;; straight-forward key-value-pairs. Though some may be only ;; understandable in the context of Adobe's PDF spec \(Adobe ;; PDF32000\) or the poppler documentation (e.g. annotation flags). ;; ;; If the poppler library is fairly recent (>= 0.19.4, older versions ;; have a bug, which may corrupt the document), annotations maybe ;; modified to a certain degree, deleted and text-annotations created. ;; The state of these modifications is held in the server. In order ;; to realize, annotations retrieved or created are referenced by a ;; unique symbol. Saving these changes creates a new file, the ;; original document is never touched. ;;; Todo: ;; ;; + Close documents at some time (e.g. when the buffer is killed) ;; ;;; Code: (require 'tq) (require 'cl-lib) ;; * ================================================================== * ;; * Customizations ;; * ================================================================== * (defgroup pdf-info nil "Extract infos from pdf-files via a helper process." :group 'pdf-tools) (defcustom pdf-info-epdfinfo-program (let ((executable (if (eq system-type 'windows-nt) "epdfinfo.exe" "epdfinfo"))) (or (executable-find executable) (expand-file-name executable (let ((directory (file-name-directory (or load-file-name default-directory)))) (cl-find-if 'file-exists-p `(,(expand-file-name "../server" directory) ,directory)))))) "Filename of the epdfinfo executable." :group 'pdf-info :type '(file :must-match t)) (defcustom pdf-info-epdfinfo-error-filename nil "Filename for error output of the epdfinfo executable. If nil, discard any error messages. Useful for debugging." :group 'pdf-info :type `(choice (const :tag "None" nil) ,@(when (file-directory-p "/tmp/") '((const "/tmp/epdfinfo.log"))) (file))) (defcustom pdf-info-log nil "Whether to log the communication with the server. If this is non-nil, all communication with the epdfinfo program will be logged to the buffer \"*pdf-info-log*\"." :group 'pdf-info :type 'boolean) (defcustom pdf-info-log-entry-max 512 "Maximum number of characters in a single log entry. This variable has no effect if `pdf-info-log' is nil." :group 'pdf-info :type 'integer) (defcustom pdf-info-restart-process-p 'ask "What to do when the epdfinfo server died. This should be one of nil -- do nothing, t -- automatically restart it or ask -- ask whether to restart or not. If it is `ask', the server quits and you answer no, this variable is set to nil." :group 'pdf-info :type '(choice (const :tag "Do nothing" nil) (const :tag "Restart silently" t) (const :tag "Always ask" ask))) (defcustom pdf-info-close-document-hook nil "A hook ran after a document was closed in the server. The hook is run in the documents buffer, if it exists. Otherwise in a `with-temp-buffer' form.") ;; * ================================================================== * ;; * Variables ;; * ================================================================== * (defvar pdf-info-asynchronous nil "If non-nil process queries asynchronously. More specifically the value should be a function of at 2 arguments \(fn STATUS RESPONSE\), where STATUS is either nil, for a successful query, or the symbol error. RESPONSE is either the command's response or the error message. This does not work recursive, i.e. if function wants to make another asynchronous query it has to rebind this variable. Alternatively it may be a list \(FN . ARGS\), in which case FN will be invoked like \(apply FN STATUS RESPONSE ARGS\). Also, all pdf-info functions normally returning a response return nil. This variable should only be let-bound.") (defconst pdf-info-pdf-date-regexp ;; Adobe PDF32000.book, 7.9.4 Dates (eval-when-compile (concat ;; allow for preceding garbage ;;"\\`" "[dD]:" "\\([0-9]\\{4\\}\\)" ;year "\\(?:" "\\([0-9]\\{2\\}\\)" ;month "\\(?:" "\\([0-9]\\{2\\}\\)" ;day "\\(?:" "\\([0-9]\\{2\\}\\)" ;hour "\\(?:" "\\([0-9]\\{2\\}\\)" ;minutes "\\(?:" "\\([0-9]\\{2\\}\\)" ;seconds "\\)?\\)?\\)?\\)?\\)?" "\\(?:" "\\([+-Zz]\\)" ;UT delta char "\\(?:" "\\([0-9]\\{2\\}\\)" ;UT delta hours "\\(?:" "'" "\\([0-9]\\{2\\}\\)" ;UT delta minutes "\\)?\\)?\\)?" ;; "\\'" ;; allow for trailing garbage ))) (defvar pdf-info--queue t "Internally used transmission-queue for the server. This variable is initially `t', telling the code starting the server, that it never ran.") ;; * ================================================================== * ;; * Process handling ;; * ================================================================== * (defun pdf-info-process () "Return the process object or nil." (and pdf-info--queue (not (eq t pdf-info--queue)) (tq-process pdf-info--queue))) (defun pdf-info-check-epdfinfo (&optional interactive-p) (interactive "p") (let ((executable pdf-info-epdfinfo-program)) (unless (stringp executable) (error "pdf-info-epdfinfo-program is unset or not a string")) (unless (file-executable-p executable) (error "pdf-info-epdfinfo-program is not executable")) (when pdf-info-epdfinfo-error-filename (unless (file-writable-p pdf-info-epdfinfo-error-filename) (error "pdf-info-epdfinfo-error-filename should be nil or a writable filename"))) (let ((tempfile (make-temp-file "pdf-info-check-epdfinfo")) (default-directory "~")) (unwind-protect (with-temp-buffer (with-temp-file tempfile (insert "quit\n")) (unless (= 0 (apply #'call-process executable tempfile (current-buffer) nil (when pdf-info-epdfinfo-error-filename (list pdf-info-epdfinfo-error-filename)))) (error "Error running `%s': %s" pdf-info-epdfinfo-program (buffer-string)))) (when (file-exists-p tempfile) (delete-file tempfile))))) (when interactive-p (message "The epdfinfo program appears to be working.")) nil) (defun pdf-info-process-assert-running (&optional force) "Assert a running process. If it never ran, i.e. `pdf-info-process' is t, start it unconditionally. Otherwise, if FORCE is non-nil start it, if it is not running. Else restart it with respect to the variable `pdf-info-restart-process-p', which see. If getting the process to run fails, this function throws an error." (interactive "P") (unless (and (processp (pdf-info-process)) (eq (process-status (pdf-info-process)) 'run)) (when (pdf-info-process) (tq-close pdf-info--queue) (setq pdf-info--queue nil)) (unless (or force (eq pdf-info--queue t) (and (eq pdf-info-restart-process-p 'ask) (not noninteractive) (y-or-n-p "The epdfinfo server quit, restart it ? ")) (and pdf-info-restart-process-p (not (eq pdf-info-restart-process-p 'ask)))) (when (eq pdf-info-restart-process-p 'ask) (setq pdf-info-restart-process-p nil)) (error "The epdfinfo server quit")) (pdf-info-check-epdfinfo) (let* ((process-connection-type) ;Avoid 4096 Byte bug #12440. (default-directory "~") (proc (apply #'start-process "epdfinfo" " *epdfinfo*" pdf-info-epdfinfo-program (when pdf-info-epdfinfo-error-filename (list pdf-info-epdfinfo-error-filename))))) (with-current-buffer " *epdfinfo*" (erase-buffer)) (set-process-query-on-exit-flag proc nil) (set-process-coding-system proc 'utf-8-unix 'utf-8-unix) (setq pdf-info--queue (tq-create proc)))) pdf-info--queue) (defadvice tq-process-buffer (around bugfix activate) "Fix a bug in trunk where the wrong callback gets called." ;; FIXME: Make me iterative. (let ((tq (ad-get-arg 0))) (if (not (equal (car (process-command (tq-process tq))) pdf-info-epdfinfo-program)) ad-do-it (let ((buffer (tq-buffer tq)) done) (when (buffer-live-p buffer) (set-buffer buffer) (while (and (not done) (> (buffer-size) 0)) (setq done t) (if (tq-queue-empty tq) (let ((buf (generate-new-buffer "*spurious*"))) (copy-to-buffer buf (point-min) (point-max)) (delete-region (point-min) (point)) (pop-to-buffer buf nil) (error "Spurious communication from process %s, see buffer %s" (process-name (tq-process tq)) (buffer-name buf))) (goto-char (point-min)) (when (re-search-forward (tq-queue-head-regexp tq) nil t) (setq done nil) (let ((answer (buffer-substring (point-min) (point))) (fn (tq-queue-head-fn tq)) (closure (tq-queue-head-closure tq))) (delete-region (point-min) (point)) (tq-queue-pop tq) (condition-case-unless-debug err (funcall fn closure answer) (error (message "Error while processing tq callback: %s" (error-message-string err))))))))))))) ;; * ================================================================== * ;; * Sending and receiving ;; * ================================================================== * (defun pdf-info-query (cmd &rest args) "Query the server using CMD and ARGS." (pdf-info-process-assert-running) (unless (symbolp cmd) (setq cmd (intern cmd))) (let* ((query (concat (mapconcat 'pdf-info-query--escape (cons cmd args) ":") "\n")) (callback (lambda (closure response) (cl-destructuring-bind (status &rest result) (pdf-info-query--parse-response cmd response) (pdf-info-query--log response) (let* (pdf-info-asynchronous) (if (functionp closure) (funcall closure status result) (apply (car closure) status result (cdr closure))))))) response status done (closure (or pdf-info-asynchronous (lambda (s r) (setq status s response r done t))))) (pdf-info-query--log query t) (tq-enqueue pdf-info--queue query "^\\.\n" closure callback) (unless pdf-info-asynchronous (while (and (not done) (eq (process-status (pdf-info-process)) 'run)) (accept-process-output (pdf-info-process) 0.01)) (when (and (not done) (not (eq (process-status (pdf-info-process)) 'run))) (error "The epdfinfo server quit unexpectedly.")) (cond ((null status) response) ((eq status 'error) (error "epdfinfo: %s" response)) ((eq status 'interrupted) (error "epdfinfo: Command was interrupted")) (t (error "internal error: invalid response status")))))) (defun pdf-info-interrupt () "FIXME: This command does currently nothing." (when (and (processp (pdf-info-process)) (eq (process-status (pdf-info-process)) 'run)) (signal-process (pdf-info-process) 'SIGUSR1))) (defun pdf-info-query--escape (arg) "Escape ARG for transmission to the server." (if (null arg) (string) (with-current-buffer (get-buffer-create " *pdf-info-query--escape*") (erase-buffer) (insert (format "%s" arg)) (goto-char 1) (while (not (eobp)) (cond ((memq (char-after) '(?\\ ?:)) (insert ?\\)) ((eq (char-after) ?\n) (delete-char 1) (insert ?\\ ?n) (backward-char))) (forward-char)) (buffer-substring-no-properties 1 (point-max))))) (defmacro pdf-info-query--read-record () "Read a single record of the response in current buffer." `(let (records done (beg (point))) (while (not done) (cl-case (char-after) (?\\ (delete-char 1) (if (not (eq (char-after) ?n)) (forward-char) (delete-char 1) (insert ?\n))) ((?: ?\n) (push (buffer-substring-no-properties beg (point)) records) (forward-char) (setq beg (point) done (bolp))) (t (forward-char)))) (nreverse records))) (defun pdf-info-query--parse-response (cmd response) "Parse one epdfinfo RESPONSE to CMD. Returns a cons \(STATUS . RESULT\), where STATUS is one of nil for a regular response, error for an error \(RESULT contains the error message\) or interrupted, i.e. the command was interrupted." (with-current-buffer (get-buffer-create " *pdf-info-query--parse-response*") (erase-buffer) (insert response) (goto-char 1) (cond ((looking-at "ERR\n") (forward-line) (cons 'error (buffer-substring-no-properties (point) (progn (re-search-forward "^\\.\n") (1- (match-beginning 0)))))) ((looking-at "OK\n") (let (result) (forward-line) (while (not (and (= (char-after) ?.) (= (char-after (1+ (point))) ?\n))) (push (pdf-info-query--read-record) result)) (cons nil (pdf-info-query--transform-response cmd (nreverse result))))) ((looking-at "INT\n") (cons 'interrupted nil)) (t (cons 'error "Invalid server response"))))) (defun pdf-info-query--transform-response (cmd response) "Transform a RESPONSE to CMD into a Lisp form." (cl-case cmd (open nil) (close (equal "1" (caar response))) (number-of-pages (string-to-number (caar response))) (charlayout (mapcar (lambda (elt) (cl-assert (= 1 (length (cadr elt))) t) `(,(aref (cadr elt) 0) ,(mapcar 'string-to-number (split-string (car elt) " " t)))) response)) (regexp-flags (mapcar (lambda (elt) (cons (intern (car elt)) (string-to-number (cadr elt)))) response)) ((search-string search-regexp) (mapcar (lambda (r) `((page . ,(string-to-number (nth 0 r))) (text . ,(let (case-fold-search) (pdf-util-highlight-regexp-in-string (regexp-quote (nth 1 r)) (nth 2 r)))) (edges . ,(mapcar (lambda (m) (mapcar 'string-to-number (split-string m " " t))) (cddr (cdr r)))))) response)) (outline (mapcar (lambda (r) `((depth . ,(string-to-number (pop r))) ,@(pdf-info-query--transform-action r))) response)) (pagelinks (mapcar (lambda (r) `((edges . ,(mapcar 'string-to-number ;area (split-string (pop r) " " t))) ,@(pdf-info-query--transform-action r))) response)) (metadata (let ((md (car response))) (if (= 1 (length md)) (list (cons 'title (car md))) (list (cons 'title (pop md)) (cons 'author (pop md)) (cons 'subject (pop md)) (cons 'keywords-raw (car md)) (cons 'keywords (split-string (pop md) "[\t\n ]*,[\t\n ]*" t)) (cons 'creator (pop md)) (cons 'producer (pop md)) (cons 'format (pop md)) (cons 'created (pop md)) (cons 'modified (pop md)))))) (gettext (or (caar response) "")) (getselection (mapcar (lambda (line) (mapcar 'string-to-number (split-string (car line) " " t))) response)) (features (mapcar 'intern (car response))) (pagesize (setq response (car response)) (cons (round (string-to-number (car response))) (round (string-to-number (cadr response))))) ((getannot editannot addannot) (pdf-info-query--transform-annotation (car response))) (getannots (mapcar 'pdf-info-query--transform-annotation response)) (getattachments (mapcar 'pdf-info-query--transform-attachment response)) ((getattachment-from-annot) (pdf-info-query--transform-attachment (car response))) (boundingbox (mapcar 'string-to-number (car response))) (synctex-forward-search (let ((list (mapcar 'string-to-number (car response)))) `((page . ,(car list)) (edges . ,(cdr list))))) (synctex-backward-search `((filename . ,(caar response)) (line . ,(string-to-number (cadr (car response)))) (column . ,(string-to-number (cadr (cdar response)))))) (delannot nil) ((save) (caar response)) ((renderpage renderpage-text-regions renderpage-highlight) (pdf-util-munch-file (caar response))) ((setoptions getoptions) (let (options) (dolist (key-value response) (let ((key (intern (car key-value))) (value (cadr key-value))) (cl-case key ((:render/printed :render/usecolors) (setq value (equal value "1")))) (push value options) (push key options))) options)) (pagelabels (mapcar 'car response)) (ping (caar response)) (t response))) (defun pdf-info-query--transform-action (action) "Transform ACTION response into a Lisp form." (let ((type (intern (pop action)))) `((type . ,type) (title . ,(pop action)) ,@(cl-case type (goto-dest `((page . ,(string-to-number (pop action))) (top . ,(and (> (length (car action)) 0) (string-to-number (pop action)))))) (goto-remote `((filename . ,(pop action)) (page . ,(string-to-number (pop action))) (top . ,(and (> (length (car action)) 0) (string-to-number (pop action)))))) (t `((uri . ,(pop action)))))))) (defun pdf-info-query--transform-annotation (a) (cl-labels ((not-empty (s) (if (not (equal s "")) s))) (let (a1 a2 a3) (cl-destructuring-bind (page edges type id flags color contents modified &rest rest) a (setq a1 `((page . ,(string-to-number page)) (edges . ,(mapcar 'string-to-number (split-string edges " " t))) (type . ,(intern type)) (id . ,(intern id)) (flags . ,(string-to-number flags)) (color . ,(not-empty color)) (contents . ,contents) (modified . ,(pdf-info-parse-pdf-date modified)))) (when rest (cl-destructuring-bind (label subject opacity popup-edges popup-is-open created &rest rest) rest (setq a2 `((label . ,(not-empty label)) (subject . ,(not-empty subject)) (opacity . ,(let ((o (not-empty opacity))) (and o (string-to-number o)))) (popup-edges . ,(let ((p (not-empty popup-edges))) (when p (mapcar 'string-to-number (split-string p " " t))))) (popup-is-open . ,(equal popup-is-open "1")) (created . ,(pdf-info-parse-pdf-date (not-empty created))))) (cond ((eq (cdr (assoc 'type a1)) 'text) (cl-destructuring-bind (icon state is-open) rest (setq a3 `((icon . ,(not-empty icon)) (state . ,(not-empty state)) (is-open . ,(equal is-open "1")))))) ((memq (cdr (assoc 'type a1)) '(squiggly highlight underline strike-out)) (setq a3 `((markup-edges . ,(mapcar (lambda (r) (mapcar 'string-to-number (split-string r " " t))) rest))))))))) (append a1 a2 a3)))) (defun pdf-info-query--transform-attachment (a) (cl-labels ((not-empty (s) (if (not (equal s "")) s))) (cl-destructuring-bind (id filename description size modified created checksum file) a `((id . ,(intern id)) (filename . ,(not-empty filename)) (description . ,(not-empty description)) (size . ,(let ((n (string-to-number size))) (and (>= n 0) n))) (modified . ,(not-empty modified)) (created . ,(not-empty created)) (checksum . ,(not-empty checksum)) (file . ,(not-empty file)))))) (defun pdf-info-query--log (string &optional query-p) "Log STRING as query/response, depending on QUERY-P. This is a no-op, if `pdf-info-log' is nil." (when pdf-info-log (with-current-buffer (get-buffer-create "*pdf-info-log*") (buffer-disable-undo) (let ((pos (point-max)) (window (get-buffer-window))) (save-excursion (goto-char (point-max)) (unless (bolp) (insert ?\n)) (insert (propertize (format-time-string "%H:%M:%S ") 'face (if query-p 'font-lock-keyword-face 'font-lock-function-name-face)) (if (and (numberp pdf-info-log-entry-max) (> (length string) pdf-info-log-entry-max)) (concat (substring string 0 pdf-info-log-entry-max) "...[truncated]\n") string))) (when (and (window-live-p window) (= pos (window-point window))) (set-window-point window (point-max))))))) ;; * ================================================================== * ;; * Utility functions ;; * ================================================================== * (defvar doc-view-buffer-file-name) (defvar doc-view--buffer-file-name) (defun pdf-info--normalize-file-or-buffer (file-or-buffer) "Return the PDF file corresponding to FILE-OR-BUFFER. FILE-OR-BUFFER may be nil, a PDF buffer, the name of a PDF buffer or a PDF file." (unless file-or-buffer (setq file-or-buffer (current-buffer))) (when (bufferp file-or-buffer) (unless (buffer-live-p file-or-buffer) (error "Buffer is not live :%s" file-or-buffer)) (with-current-buffer file-or-buffer (unless (setq file-or-buffer (cl-case major-mode (doc-view-mode (cond ((boundp 'doc-view-buffer-file-name) doc-view-buffer-file-name) ((boundp 'doc-view--buffer-file-name) doc-view--buffer-file-name))) (pdf-view-mode (pdf-view-buffer-file-name)) (t (buffer-file-name)))) (error "Buffer is not associated with any file :%s" (buffer-name))))) (unless (stringp file-or-buffer) (signal 'wrong-type-argument (list 'stringp 'bufferp 'null file-or-buffer))) ;; is file (when (file-remote-p file-or-buffer) (error "Processing remote files not supported :%s" file-or-buffer)) ;; (unless (file-readable-p file-or-buffer) ;; (error "File not readable :%s" file-or-buffer)) (expand-file-name file-or-buffer)) (defun pdf-info-valid-page-spec-p (pages) "The type predicate for a valid page-spec." (not (not (ignore-errors (pdf-info-normalize-page-range pages))))) (defun pdf-info-normalize-page-range (pages) "Normalize PAGES for sending to the server. PAGES may be a single page number, a cons \(FIRST . LAST\), or nil, which stands for all pages. The result is a cons \(FIRST . LAST\), where LAST may be 0 representing the final page." (cond ((natnump pages) (cons pages pages)) ((null pages) (cons 1 0)) ((and (natnump (car pages)) (natnump (cdr pages))) pages) (t (signal 'wrong-type-argument (list 'pdf-info-valid-page-spec-p pages))))) (defun pdf-info-parse-pdf-date (date) (when (and date (string-match pdf-info-pdf-date-regexp date)) (let ((year (match-string 1 date)) (month (match-string 2 date)) (day (match-string 3 date)) (hour (match-string 4 date)) (min (match-string 5 date)) (sec (match-string 6 date)) (ut-char (match-string 7 date)) (ut-hour (match-string 8 date)) (ut-min (match-string 9 date)) (tz 0)) (when (or (equal ut-char "+") (equal ut-char "-")) (when ut-hour (setq tz (* 3600 (string-to-number ut-hour)))) (when ut-min (setq tz (+ tz (* 60 (string-to-number ut-min))))) (when (equal ut-char "-") (setq tz (- tz)))) (encode-time (if sec (string-to-number sec) 0) (if min (string-to-number min) 0) (if hour (string-to-number hour) 0) (if day (string-to-number day) 1) (if month (string-to-number month) 1) (string-to-number year) tz)))) (defmacro pdf-info-compose-queries (let-forms &rest body) "Let-bind each VAR to QUERIES results and evaluate BODY. All queries in each QUERIES form are run by the server in the order they appear and the results collected in a list, which is bound to VAR. Then BODY is evaluated and its value becomes the final result of all queries, unless at least one of them provoked an error. In this case BODY is ignored and the error is the result. This macro handles synchronous and asynchronous calls, i.e. `pdf-info-asynchronous' is non-nil, transparently. \(FN \(\(VAR QUERIES\)...\) BODY\)" (declare (indent 1) (debug ((&rest &or (symbolp &optional form) symbolp) body))) (unless (cl-every (lambda (form) (when (symbolp form) (setq form (list form))) (and (consp form) (symbolp (car form)) (listp (cdr form)))) let-forms) (error "Invalid let-form: %s" let-forms)) (setq let-forms (mapcar (lambda (form) (if (symbolp form) (list form) form)) let-forms)) (let* ((status (make-symbol "status")) (response (make-symbol "response")) (first-error (make-symbol "first-error")) (done (make-symbol "done")) (callback (make-symbol "callback")) (results (make-symbol "results")) (push-fn (make-symbol "push-fn")) (terminal-fn (make-symbol "terminal-fn")) (buffer (make-symbol "buffer"))) `(let* (,status ,response ,first-error ,done (,buffer (current-buffer)) (,callback pdf-info-asynchronous) ;; Ensure a new alist on every invocation. (,results (mapcar 'copy-sequence ',(cl-mapcar (lambda (form) (list (car form))) let-forms))) (,push-fn (lambda (status result var) ;; Store result in alist RESULTS under key ;; VAR. (if status (unless ,first-error (setq ,first-error result)) (let ((elt (assq var ,results))) (setcdr elt (append (cdr elt) (list result))))))) (,terminal-fn (lambda (&rest _) ;; Let-bind responses corresponding to their variables, ;; i.e. keys in alist RESULTS. (let (,@(mapcar (lambda (var) (list var (list 'cdr (list 'assq (list 'quote var) results)))) (mapcar 'car let-forms))) (setq ,status (not (not ,first-error)) ,response (or ,first-error (with-current-buffer ,buffer ,@body)) ,done t) ;; Maybe invoke the CALLBACK (which was bound to ;; pdf-info-asynchronous). (when ,callback (if (functionp ,callback) (funcall ,callback ,status ,response) (apply (car ,callback) ,status ,response (cdr ,callback)))))))) ;; Wrap each query in an asynchronous call, with its VAR as ;; callback argument, so the PUSH-FN can put it in the alist ;; RESULTS. ,@(mapcar (lambda (form) (list 'let (list (list 'pdf-info-asynchronous (list 'list push-fn (list 'quote (car form))))) (cadr form))) let-forms) ;; Request a no-op, just so we know that we are finished. (let ((pdf-info-asynchronous ,terminal-fn)) (pdf-info-ping)) ;; CALLBACK is the original value of pdf-info-asynchronous. If ;; nil, this is a synchronous query. (unless ,callback (while (and (not ,done) (eq (process-status (pdf-info-process)) 'run)) (accept-process-output (pdf-info-process) 0.01)) (when (and (not ,done) (not (eq (process-status (pdf-info-process)) 'run))) (error "The epdfinfo server quit unexpectedly.")) (when ,status (error "epdfinfo: %s" ,response)) ,response)))) ;; * ================================================================== * ;; * Buffer local server instances ;; * ================================================================== * (put 'pdf-info--queue 'permanent-local t) (defun pdf-info-make-local-server (&optional buffer force-restart-p) "Create a server instance local to BUFFER. Does nothing if BUFFER already has a local instance. Unless FORCE-RESTART-P is non-nil, then quit a potential process and restart it." (unless buffer (setq buffer (current-buffer))) (with-current-buffer buffer (unless (and (not force-restart-p) (local-variable-p 'pdf-info--queue) (processp (pdf-info-process)) (eq (process-status (pdf-info-process)) 'run)) (when (and (local-variable-p 'pdf-info--queue) (processp (pdf-info-process))) (tq-close pdf-info--queue)) (set (make-local-variable 'pdf-info--queue) nil) (pdf-info-process-assert-running t) (add-hook 'kill-buffer-hook 'pdf-info-kill-local-server nil t) pdf-info--queue))) (defun pdf-info-kill-local-server (&optional buffer) "Kill the local server in BUFFER. A No-op, if BUFFER has not running server instance." (save-current-buffer (when buffer (set-buffer buffer)) (when (local-variable-p 'pdf-info--queue) (pdf-info-kill) (kill-local-variable 'pdf-info--queue) t))) (defun pdf-info-local-server-p (&optional buffer) "Return non-nil, if BUFFER has a running server instance." (unless buffer (setq buffer (current-buffer))) (setq buffer (get-buffer buffer)) (and (buffer-live-p buffer) (local-variable-p 'pdf-info--queue buffer))) (defun pdf-info-local-batch-query (producer-fn consumer-fn sentinel-fn args) "Process a set of queries asynchronously in a local instance." (unless (pdf-info-local-server-p) (error "Create a local server first")) (let* ((buffer (current-buffer)) (producer-symbol (make-symbol "producer")) (consumer-symbol (make-symbol "consumer")) (producer (lambda (args) (if (null args) (funcall sentinel-fn 'finished buffer) (let ((pdf-info-asynchronous (apply-partially (symbol-function consumer-symbol) args))) (cond ((pdf-info-local-server-p buffer) (with-current-buffer buffer (apply producer-fn (car args)))) (t (funcall sentinel-fn 'error buffer))))))) (consumer (lambda (args status result) (if (not (pdf-info-local-server-p buffer)) (funcall sentinel-fn 'error buffer) (with-current-buffer buffer (apply consumer-fn status result (car args))) (funcall (symbol-function producer-symbol) (cdr args)))))) (fset producer-symbol producer) (fset consumer-symbol consumer) (funcall producer args))) ;; * ================================================================== * ;; * High level interface ;; * ================================================================== * (defvar pdf-info-features nil) (defun pdf-info-features () "Return a list of symbols describing compile-time features." (or pdf-info-features (setq pdf-info-features (let (pdf-info-asynchronous) (pdf-info-query 'features))))) (defun pdf-info-writable-annotations-p () (not (null (memq 'writable-annotations (pdf-info-features))))) (defun pdf-info-markup-annotations-p () (not (null (memq 'markup-annotations (pdf-info-features))))) (defmacro pdf-info-assert-writable-annotations () `(unless (memq 'writable-annotations (pdf-info-features)) (error "Writing annotations is not supported by this version of epdfinfo"))) (defmacro pdf-info-assert-markup-annotations () `(unless (memq 'markup-annotations (pdf-info-features)) (error "Creating markup annotations is not supported by this version of epdfinfo"))) (defun pdf-info-creatable-annotation-types () (let ((features (pdf-info-features))) (cond ((not (memq 'writable-annotations features)) nil) ((memq 'markup-annotations features) (list 'text 'squiggly 'underline 'strike-out 'highlight)) (t (list 'text))))) (defun pdf-info-open (&optional file-or-buffer password) "Open the document FILE-OR-BUFFER using PASSWORD. Generally, documents are opened and closed automatically on demand, so this function is rarely needed, unless a PASSWORD is set on the document. Manually opened documents are never closed automatically." (pdf-info-query 'open (pdf-info--normalize-file-or-buffer file-or-buffer) password)) (defun pdf-info-close (&optional file-or-buffer) "Close the document FILE-OR-BUFFER. Returns t, if the document was actually open, otherwise nil. This command is rarely needed, see also `pdf-info-open'." (let* ((pdf (pdf-info--normalize-file-or-buffer file-or-buffer)) (buffer (find-buffer-visiting pdf))) (prog1 (pdf-info-query 'close pdf) (if (buffer-live-p buffer) (with-current-buffer buffer (run-hooks 'pdf-info-close-document-hook)) (with-temp-buffer (run-hooks 'pdf-info-close-document-hook)))))) (defun pdf-info-encrypted-p (&optional file-or-buffer) "Return non-nil if FILE-OR-BUFFER requires a password. Note: This function returns nil, if the document is encrypted, but was already opened (presumably using a password)." (condition-case err (pdf-info-open (pdf-info--normalize-file-or-buffer file-or-buffer)) (error (or (string-match-p ":Document is encrypted\\'" (cadr err)) (signal (car err) (cdr err)))))) (defun pdf-info-metadata (&optional file-or-buffer) "Extract the metadata from the document FILE-OR-BUFFER. This returns an alist containing some information about the document." (pdf-info-query 'metadata (pdf-info--normalize-file-or-buffer file-or-buffer))) (defun pdf-info-search-string (string &optional pages file-or-buffer) "Search for STRING in PAGES of document FILE-OR-BUFFER. See `pdf-info-normalize-page-range' for valid PAGES formats. This function returns a list of matches. Each item is an alist containing keys PAGE, TEXT and EDGES, where PAGE and TEXT are the matched page resp. line. EDGES is a list containing a single edges element \(LEFT TOP RIGHT BOTTOM\). This is for consistency with `pdf-info-search-regexp', which may return matches with multiple edges. The TEXT contains `match' face properties on the matched parts. Search is case-insensitive, unless `case-fold-search' is nil and searching case-sensitive is supported by the server." (let ((pages (pdf-info-normalize-page-range pages))) (pdf-info-query 'search-string (pdf-info--normalize-file-or-buffer file-or-buffer) (car pages) (cdr pages) string (if case-fold-search 1 0)))) (defvar pdf-info-regexp-compile-flags nil "PCRE compile flags. Don't use this, but the equally named function.") (defvar pdf-info-regexp-match-flags nil "PCRE match flags. Don't use this, but the equally named function.") (defun pdf-info-regexp-compile-flags () (or pdf-info-regexp-compile-flags (let* (pdf-info-asynchronous (flags (pdf-info-query 'regexp-flags)) (match (cl-remove-if-not (lambda (flag) (string-match-p "\\`match-" (symbol-name (car flag)))) flags)) (compile (cl-set-difference flags match))) (setq pdf-info-regexp-compile-flags compile pdf-info-regexp-match-flags match) pdf-info-regexp-compile-flags))) (defun pdf-info-regexp-match-flags () (or pdf-info-regexp-match-flags (progn (pdf-info-regexp-compile-flags) pdf-info-regexp-match-flags))) (defvar pdf-info-regexp-flags '(multiline) "Compile- and match-flags for the PCRE engine. This is a list of symbols denoting compile- and match-flags when searching for regular expressions. You should not change this directly, but rather `let'-bind it around a call to `pdf-info-search-regexp'. Valid compile-flags are: newline-crlf, newline-lf, newline-cr, dupnames, optimize, no-auto-capture, raw, ungreedy, dollar-endonly, anchored, extended, dotall, multiline and caseless. Note that the last one, caseless, is handled special, as it is always added if `case-fold-search' is non-nil. And valid match-flags: match-anchored, match-notbol, match-noteol, match-notempty, match-partial, match-newline-cr, match-newline-lf, match-newline-crlf and match-newline-any. See the glib documentation at url `https://developer.gnome.org/glib/stable/glib-Perl-compatible-regular-expressions.html'.") (defun pdf-info-search-regexp (pcre &optional pages no-error file-or-buffer) "Search for a PCRE on PAGES of document FILE-OR-BUFFER. See `pdf-info-normalize-page-range' for valid PAGES formats and `pdf-info-search-string' for its return value. Uses the flags in `pdf-info-regexp-flags', which see. If `case-fold-search' is non-nil, the caseless flag is added. If NO-ERROR is non-nil, catch errors due to invalid regexps and return nil. If it is the symbol `invalid-regexp', then re-signal this kind of error as a `invalid-regexp' error." (cl-labels ((orflags (flags alist) (cl-reduce (lambda (v flag) (let ((n (cdr (assq flag alist)))) (if n (logior n v) v))) (cons 0 flags)))) (let ((pages (pdf-info-normalize-page-range pages))) (condition-case err (pdf-info-query 'search-regexp (pdf-info--normalize-file-or-buffer file-or-buffer) (car pages) (cdr pages) pcre (orflags `(,(if case-fold-search 'caseless) ,@pdf-info-regexp-flags) (pdf-info-regexp-compile-flags)) (orflags pdf-info-regexp-flags (pdf-info-regexp-match-flags))) (error (let ((re (concat "\\`epdfinfo: *Invalid *regexp: *" ;; glib error "\\(?:Error while compiling regular expression" " *%s *\\)?\\(.*\\)"))) (if (or (null no-error) (not (string-match (format re (regexp-quote pcre)) (cadr err)))) (signal (car err) (cdr err)) (if (eq no-error 'invalid-regexp) (signal 'invalid-regexp (list (match-string 1 (cadr err)))))))))))) (defun pdf-info-pagelinks (page &optional file-or-buffer) "Return a list of links on PAGE in document FILE-OR-BUFFER. This function returns a list of alists with the following keys. EDGES represents the relative bounding-box of the link , TYPE is the type of the action, TITLE is a, possibly empty, name for this action. TYPE may be one of goto-dest -- This is a internal link to some page. Each element contains additional keys PAGE and TOP, where PAGE is the page of the link and TOP its vertical position. goto-remote -- This a external link to some document. Same as goto-dest, with an additional FILENAME of the external PDF. uri -- A link in form of some URI. Alist contains additional key URI. In the first two cases, PAGE may be 0 and TOP nil, which means these data is unspecified." (cl-check-type page natnum) (pdf-info-query 'pagelinks (pdf-info--normalize-file-or-buffer file-or-buffer) page)) (defun pdf-info-number-of-pages (&optional file-or-buffer) "Return the number of pages in document FILE-OR-BUFFER." (pdf-info-query 'number-of-pages (pdf-info--normalize-file-or-buffer file-or-buffer))) (defun pdf-info-outline (&optional file-or-buffer) "Return the PDF outline of document FILE-OR-BUFFER. This function returns a list of alists like `pdf-info-pagelinks'. Additionally every alist has a DEPTH (>= 1) entry with the depth of this element in the tree." (pdf-info-query 'outline (pdf-info--normalize-file-or-buffer file-or-buffer))) (defun pdf-info-gettext (page edges &optional selection-style file-or-buffer) "Get text on PAGE according to EDGES. EDGES should contain relative coordinates. The selection may extend over multiple lines, which works similar to a Emacs region. SELECTION-STYLE may be one of glyph, word or line and determines the smallest unit of the selected region. Return the text contained in the selection." (pdf-info-query 'gettext (pdf-info--normalize-file-or-buffer file-or-buffer) page (mapconcat 'number-to-string edges " ") (cl-case selection-style (glyph 0) (word 1) (line 2) (t 0)))) (defun pdf-info-getselection (page edges &optional selection-style file-or-buffer) "Return the edges of the selection EDGES on PAGE. Arguments are the same as for `pdf-info-gettext'. Return a list of edges corresponding to the text that would be returned by the aforementioned function, when called with the same arguments." (pdf-info-query 'getselection (pdf-info--normalize-file-or-buffer file-or-buffer) page (mapconcat 'number-to-string edges " ") (cl-case selection-style (glyph 0) (word 1) (line 2) (t 0)))) (defun pdf-info-textregions (page &optional file-or-buffer) "Return a list of edges describing PAGE's text-layout." (pdf-info-getselection page '(0 0 1 1) 'glyph file-or-buffer)) (defun pdf-info-charlayout (page &optional edges-or-pos file-or-buffer) "Return the layout of characters of PAGE in/at EDGES-OR-POS. Returns a list of elements \(CHAR . \(LEFT TOP RIGHT BOT\)\) mapping character to their corresponding relative bounding-boxes. EDGES-OR-POS may be a region \(LEFT TOP RIGHT BOT\) restricting the returned value to include only characters fully contained in it. Or a cons \(LEFT . TOP\) which means to only include the character at this position. In this case the return value contains at most one element." ;; FIXME: Actually returns \(CHAR . LEFT ...\). (unless edges-or-pos (setq edges-or-pos '(0 0 1 1))) (when (numberp (cdr edges-or-pos)) (setq edges-or-pos (list (car edges-or-pos) (cdr edges-or-pos) -1 -1))) (pdf-info-query 'charlayout (pdf-info--normalize-file-or-buffer file-or-buffer) page (mapconcat 'number-to-string edges-or-pos " "))) (defun pdf-info-pagesize (page &optional file-or-buffer) "Return the size of PAGE as a cons \(WIDTH . HEIGHT\) The size is in PDF points." (pdf-info-query 'pagesize (pdf-info--normalize-file-or-buffer file-or-buffer) page)) (defun pdf-info-running-p () "Return non-nil, if the server is running." (and (processp (pdf-info-process)) (eq (process-status (pdf-info-process)) 'run))) (defun pdf-info-quit (&optional timeout) "Quit the epdfinfo server. This blocks until all outstanding requests are answered. Unless TIMEOUT is non-nil, in which case we wait at most TIMEOUT seconds before killing the server." (cl-check-type timeout (or null number)) (when (pdf-info-running-p) (let ((pdf-info-asynchronous (if timeout (lambda (&rest _)) pdf-info-asynchronous))) (pdf-info-query 'quit) (when timeout (setq timeout (+ (float-time) (max 0 timeout))) (while (and (pdf-info-running-p) (> timeout (float-time))) (accept-process-output (pdf-info-process) 0.5 nil t))))) (when (processp (pdf-info-process)) (tq-close pdf-info--queue)) (setq pdf-info--queue nil)) (defun pdf-info-kill () "Kill the epdfinfo server. Immediately delete the server process, see also `pdf-info-quit', for a more sane way to exit the program." (when (processp (pdf-info-process)) (tq-close pdf-info--queue)) (setq pdf-info--queue nil)) (defun pdf-info-getannots (&optional pages file-or-buffer) "Return the annotations on PAGE. See `pdf-info-normalize-page-range' for valid PAGES formats. This function returns the annotations for PAGES as a list of alists. Each element of this list describes one annotation and contains the following keys. page - Its page number. edges - Its area. type - A symbol describing the annotation's type. id - A document-wide unique symbol referencing this annotation. flags - Its flags, binary encoded. color - Its color in standard Emacs notation. contents - The text of this annotation. modified - The last modification date of this annotation. Additionally, if the annotation is a markup annotation, the following keys are present. label - The annotation's label. subject - The subject addressed. opacity - The level of relative opacity. popup-edges - The edges of a associated popup window or nil. popup-is-open - Whether this window should be displayed open. created - The date this markup annotation was created. If the annotation is also a markup text annotation, the alist contains the following keys. text-icon - A string describing the purpose of this annotation. text-state - A string, e.g. accepted or rejected." ;FIXME: Use symbols ? (let ((pages (pdf-info-normalize-page-range pages))) (pdf-info-query 'getannots (pdf-info--normalize-file-or-buffer file-or-buffer) (car pages) (cdr pages)))) (defun pdf-info-getannot (id &optional file-or-buffer) "Return the annotation for ID. ID should be a symbol, which was previously returned in a `pdf-info-getannots' query. Signal an error, if an annotation with ID is not available. See `pdf-info-getannots' for the kind of return value of this function." (pdf-info-query 'getannot (pdf-info--normalize-file-or-buffer file-or-buffer) id)) (defun pdf-info-addannot (page edges type &optional file-or-buffer &rest markup-edges) "Add a new annotation to PAGE with EDGES of TYPE. FIXME: TYPE may be one of `text', `markup-highlight', ... . FIXME: -1 = 24 See `pdf-info-getannots' for the kind of value of this function returns." (pdf-info-assert-writable-annotations) (when (consp file-or-buffer) (push file-or-buffer markup-edges) (setq file-or-buffer nil)) (apply 'pdf-info-query 'addannot (pdf-info--normalize-file-or-buffer file-or-buffer) page type (mapconcat 'number-to-string edges " ") (mapcar (lambda (me) (mapconcat 'number-to-string me " ")) markup-edges))) (defun pdf-info-delannot (id &optional file-or-buffer) "Delete the annotation with ID in FILE-OR-BUFFER. ID should be a symbol, which was previously returned in a `pdf-info-getannots' query. Signal an error, if annotation ID does not exist." (pdf-info-assert-writable-annotations) (pdf-info-query 'delannot (pdf-info--normalize-file-or-buffer file-or-buffer) id)) (defun pdf-info-mvannot (id edges &optional file-or-buffer) "Move/Resize annotation ID to fit EDGES. ID should be a symbol, which was previously returned in a `pdf-info-getannots' query. Signal an error, if annotation ID does not exist. EDGES should be a list \(LEFT TOP RIGHT BOT\). RIGHT and/or BOT may also be negative, which means to keep the width resp. height." (pdf-info-editannot id `((edges . ,edges)) file-or-buffer)) (defun pdf-info-editannot (id modifications &optional file-or-buffer) "Edit annotation ID, applying MODIFICATIONS. ID should be a symbol, which was previously returned in a `pdf-info-getannots' query. MODIFICATIONS is an alist of properties and their new values. The server must support modifying annotations for this to work." (pdf-info-assert-writable-annotations) (let ((edits (mapcar (lambda (elt) (cl-case (car elt) (color (list (car elt) (pdf-util-hexcolor (cdr elt)))) (edges (list (car elt) (mapconcat 'number-to-string (cdr elt) " "))) ((popup-is-open is-open) (list (car elt) (if (cdr elt) 1 0))) (t (list (car elt) (cdr elt))))) modifications))) (apply 'pdf-info-query 'editannot (pdf-info--normalize-file-or-buffer file-or-buffer) id (apply 'append edits)))) (defun pdf-info-save (&optional file-or-buffer) "Save FILE-OR-BUFFER. This saves the document to a new temporary file, which is returned and owned by the caller." (pdf-info-assert-writable-annotations) (pdf-info-query 'save (pdf-info--normalize-file-or-buffer file-or-buffer))) (defun pdf-info-getattachment-from-annot (id &optional do-save file-or-buffer) "Return the attachment associated with annotation ID. ID should be a symbol which was previously returned in a `pdf-info-getannots' query, and referencing an attachment of type `file', otherwise an error is signaled. See `pdf-info-getattachments' for the kind of return value of this function and the meaning of DO-SAVE." (pdf-info-query 'getattachment-from-annot (pdf-info--normalize-file-or-buffer file-or-buffer) id (if do-save 1 0))) (defun pdf-info-getattachments (&optional do-save file-or-buffer) "Return all document level attachments. If DO-SAVE is non-nil, save the attachments data to a local file, which is then owned by the caller, see below. This function returns a list of alists, where every element contains the following keys. All values, except for id, may be nil, i.e. not present. id - A symbol uniquely identifying this attachment. filename - The filename of this attachment. description - A description of this attachment. size - The size in bytes. modified - The last modification date. created - The date of creation. checksum - A MD5 checksum of this attachment's data. file - The name of a tempfile containing the data (only present if DO-SAVE is non-nil)." (pdf-info-query 'getattachments (pdf-info--normalize-file-or-buffer file-or-buffer) (if do-save 1 0))) (defun pdf-info-synctex-forward-search (source &optional line column file-or-buffer) "Perform a forward search with synctex. SOURCE should be a LaTeX buffer or the absolute filename of a corresponding file. LINE and COLUMN represent the position in the buffer or file. Finally FILE-OR-BUFFER corresponds to the PDF document. Returns an alist with entries PAGE and relative EDGES describing the position in the PDF document corresponding to the SOURCE location." (let ((source (if (buffer-live-p (get-buffer source)) (buffer-file-name (get-buffer source)) source))) (pdf-info-query 'synctex-forward-search (pdf-info--normalize-file-or-buffer file-or-buffer) source (or line 1) (or column 1)))) (defun pdf-info-synctex-backward-search (page &optional x y file-or-buffer) "Perform a backward search with synctex. Find the source location corresponding to the coordinates \(X . Y\) on PAGE in FILE-OR-BUFFER. Returns an alist with entries FILENAME, LINE and COLUMN." (pdf-info-query 'synctex-backward-search (pdf-info--normalize-file-or-buffer file-or-buffer) page (or x 0) (or y 0))) (defun pdf-info-renderpage (page width &optional file-or-buffer &rest commands) "Render PAGE with width WIDTH. Return the data of the corresponding PNG image." (when (keywordp file-or-buffer) (push file-or-buffer commands) (setq file-or-buffer nil)) (apply 'pdf-info-query 'renderpage (pdf-info--normalize-file-or-buffer file-or-buffer) page width (let (transformed) (while (cdr commands) (let ((kw (pop commands)) (value (pop commands))) (setq value (cl-case kw ((:crop-to :highlight-line :highlight-region :highlight-text) (mapconcat 'number-to-string value " ")) ((:foreground :background) (pdf-util-hexcolor value)) (:alpha (number-to-string value)) (otherwise value))) (push kw transformed) (push value transformed))) (when commands (error "Keyword is missing a value: %s" (car commands))) (nreverse transformed)))) (defun pdf-info-renderpage-text-regions (page width single-line-p &optional file-or-buffer &rest regions) "Highlight text on PAGE with width WIDTH using REGIONS. REGIONS is a list determining foreground and background color and the regions to render. So each element should look like \(FG BG \(LEFT TOP RIGHT BOT\) \(LEFT TOP RIGHT BOT\) ... \) . The rendering is text-aware. If SINGLE-LINE-P is non-nil, the edges in REGIONS are each supposed to be limited to a single line in the document. Setting this, if applicable, avoids rendering problems. For the other args see `pdf-info-renderpage'. Return the data of the corresponding PNG image." (when (consp file-or-buffer) (push file-or-buffer regions) (setq file-or-buffer nil)) (apply 'pdf-info-renderpage page width file-or-buffer (apply 'append (mapcar (lambda (elt) `(:foreground ,(pop elt) :background ,(pop elt) ,@(cl-mapcan (lambda (edges) `(,(if single-line-p :highlight-line :highlight-text) ,edges)) elt))) regions)))) (defun pdf-info-renderpage-highlight (page width &optional file-or-buffer &rest regions) "Highlight regions on PAGE with width WIDTH using REGIONS. REGIONS is a list determining the background color, a alpha value and the regions to render. So each element should look like \(FILL-COLOR STROKE-COLOR ALPHA \(LEFT TOP RIGHT BOT\) \(LEFT TOP RIGHT BOT\) ... \) . For the other args see `pdf-info-renderpage'. Return the data of the corresponding PNG image." (when (consp file-or-buffer) (push file-or-buffer regions) (setq file-or-buffer nil)) (apply 'pdf-info-renderpage page width file-or-buffer (apply 'append (mapcar (lambda (elt) `(:background ,(pop elt) :foreground ,(pop elt) :alpha ,(pop elt) ,@(cl-mapcan (lambda (edges) `(:highlight-region ,edges)) elt))) regions)))) (defun pdf-info-boundingbox (page &optional file-or-buffer) "Return a bounding-box for PAGE. Returns a list \(LEFT TOP RIGHT BOT\)." (pdf-info-query 'boundingbox (pdf-info--normalize-file-or-buffer file-or-buffer) page)) (defun pdf-info-getoptions (&optional file-or-buffer) (pdf-info-query 'getoptions (pdf-info--normalize-file-or-buffer file-or-buffer))) (defun pdf-info-setoptions (&optional file-or-buffer &rest options) (when (symbolp file-or-buffer) (push file-or-buffer options) (setq file-or-buffer nil)) (unless (= (% (length options) 2) 0) (error "Missing a option value")) (apply 'pdf-info-query 'setoptions (pdf-info--normalize-file-or-buffer file-or-buffer) (let (soptions) (while options (let ((key (pop options)) (value (pop options))) (unless (and (keywordp key) (not (eq key :))) (error "Keyword expected: %s" key)) (cl-case key ((:render/foreground :render/background) (push (pdf-util-hexcolor value) soptions)) ((:render/usecolors :render/printed) (push (if value 1 0) soptions)) (t (push value soptions))) (push key soptions))) soptions))) (defun pdf-info-pagelabels (&optional file-or-buffer) "Return a list of pagelabels. Returns a list of strings corresponding to the labels of the pages in FILE-OR-BUFFER." (pdf-info-query 'pagelabels (pdf-info--normalize-file-or-buffer file-or-buffer))) (defun pdf-info-ping (&optional message) "Ping the server using MESSAGE. Returns MESSAGE, which defaults to \"pong\"." (pdf-info-query 'ping (or message "pong"))) (provide 'pdf-info) ;;; pdf-info.el ends here pdf-tools-0.80/lisp/pdf-isearch.el000066400000000000000000000745531315513274600170530ustar00rootroot00000000000000;;; pdf-isearch.el --- Isearch in pdf buffers. -*- lexical-binding: t -*- ;; Copyright (C) 2013, 2014 Andreas Politz ;; Author: Andreas Politz ;; Keywords: files, multimedia ;; 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 . ;;; Commentary: ;; ;;; Todo: ;; ;; * Add the possibility to limit the search to a range of pages. (require 'cl-lib) (require 'pdf-util) (require 'pdf-info) (require 'pdf-misc) (require 'pdf-view) (require 'pdf-cache) (require 'let-alist) ;;; Code: ;; * ================================================================== * ;; * Customizations ;; * ================================================================== * (defgroup pdf-isearch nil "Isearch in pdf buffers." :group 'pdf-tools) (defface pdf-isearch-match '((((background dark)) (:inherit isearch)) (((background light)) (:inherit isearch))) "Face used to determine the colors of the current match." :group 'pdf-isearch :group 'pdf-tools-faces) (defface pdf-isearch-lazy '((((background dark)) (:inherit lazy-highlight)) (((background light)) (:inherit lazy-highlight))) "Face used to determine the colors of non-current matches." :group 'pdf-isearch :group 'pdf-tools-faces) (defface pdf-isearch-batch '((((background dark)) (:inherit match)) (((background light)) (:inherit match))) "Face used to determine the colors in `pdf-isearch-batch-mode'." :group 'pdf-isearch :group 'pdf-tools-faces) (defcustom pdf-isearch-hyphenation-character "-­" "Characters used as hyphens when word searching." :group 'pdf-isearch :type 'string) (defvar pdf-isearch-search-fun-function nil "Search function used when searching. Like `isearch-search-fun-function', though it should return a function \(FN STRING &optional PAGES\), which in turn should return a result like `pdf-info-search-regexp'.") ;; * ================================================================== * ;; * Internal Variables ;; * ================================================================== * (defvar-local pdf-isearch-current-page nil "The page that is currently searched.") (defvar-local pdf-isearch-current-match nil "A list ((LEFT TOP RIGHT BOT) ...) of the current match or nil. A match may contain more than one edges-element, e.g. when regexp searching across multiple lines.") (defvar-local pdf-isearch-current-matches nil "A list of matches of the last search.") (defvar-local pdf-isearch-current-parameter nil "A list of search parameter \(search-string regex-p case-fold word-search\).") ;; * ================================================================== * ;; * Modes ;; * ================================================================== * (declare-function pdf-occur "pdf-occur.el") (declare-function pdf-sync-backward-search "pdf-sync.el") (defvar pdf-isearch-minor-mode-map (let ((kmap (make-sparse-keymap))) (define-key kmap [remap occur] 'pdf-occur) kmap) "Keymap used in `pdf-isearch-minor-mode'.") (defvar pdf-isearch-active-mode-map (let ((kmap (make-sparse-keymap))) (set-keymap-parent kmap isearch-mode-map) (define-key kmap (kbd "C-d") 'pdf-view-dark-minor-mode) (define-key kmap (kbd "C-b") 'pdf-isearch-batch-mode) (define-key kmap (kbd "M-s o") 'pdf-isearch-occur) (define-key kmap (kbd "M-s s") 'pdf-isearch-sync-backward) kmap) "Keymap used in `pdf-isearch-active-mode'. This keymap is used, when isearching in PDF buffers. Its parent keymap is `isearch-mode-map'.") (put 'image-scroll-up 'isearch-scroll t) (put 'image-scroll-down 'isearch-scroll t) (define-minor-mode pdf-isearch-active-mode "" nil nil nil (cond (pdf-isearch-active-mode (set (make-local-variable 'isearch-mode-map) pdf-isearch-active-mode-map) (setq overriding-terminal-local-map isearch-mode-map)) (t ;;(setq overriding-terminal-local-map nil) ? (kill-local-variable 'isearch-mode-map)))) ;;;###autoload (define-minor-mode pdf-isearch-minor-mode "Isearch mode for PDF buffer. When this mode is enabled \\[isearch-forward], among other keys, starts an incremental search in this PDF document. Since this mode uses external programs to highlight found matches via image-processing, proceeding to the next match may be slow. Therefore two isearch behaviours have been defined: Normal isearch and batch mode. The later one is a minor mode \(`pdf-isearch-batch-mode'\), which when activated inhibits isearch from stopping at and highlighting every single match, but rather display them batch-wise. Here a batch means a number of matches currently visible in the selected window. The kind of highlighting is determined by three faces `pdf-isearch-match' \(for the current match\), `pdf-isearch-lazy' \(for all other matches\) and `pdf-isearch-batch' \(when in batch mode\), which see. Colors may also be influenced by the minor-mode `pdf-view-dark-minor-mode'. If this is minor mode enabled, each face's dark colors, are used (see e.g. `frame-background-mode'), instead of the light ones. \\{pdf-isearch-minor-mode-map} While in `isearch-mode' the following keys are available. Note that not every isearch command work as expected. \\{pdf-isearch-active-mode-map}" :group 'pdf-isearch (pdf-util-assert-pdf-buffer) (cond (pdf-isearch-minor-mode (when (boundp 'character-fold-search) (setq-local character-fold-search nil)) (set (make-local-variable 'isearch-search-fun-function) (lambda nil 'pdf-isearch-search-function)) (set (make-local-variable 'isearch-push-state-function) 'pdf-isearch-push-state-function) (set (make-local-variable 'isearch-wrap-function) 'pdf-isearch-wrap-function) (set (make-local-variable 'isearch-lazy-highlight) nil) ;; Make our commands work in isearch-mode. (set (make-local-variable 'isearch-allow-scroll) t) (set (make-local-variable 'search-exit-option) ;; This maybe edit or t, but edit would suppress our cmds ;; in isearch-other-meta-char. (not (not search-exit-option))) ;; FIXME: Die Variable imagemagick-render-type entweder an anderer ;; Stelle global setzen oder nur irgendwo auf den ;; Performancegewinn hinweisen. (when (and (boundp 'imagemagick-render-type) (= 0 imagemagick-render-type)) ;; This enormously speeds up rendering. (setq imagemagick-render-type 1)) (add-hook 'isearch-mode-hook 'pdf-isearch-mode-initialize nil t) (add-hook 'isearch-mode-end-hook 'pdf-isearch-mode-cleanup nil t) (add-hook 'isearch-update-post-hook 'pdf-isearch-update nil t)) (t (when (boundp 'character-fold-search) (kill-local-variable 'character-fold-search)) (kill-local-variable 'search-exit-option) (kill-local-variable 'isearch-allow-scroll) (kill-local-variable 'isearch-search-fun-function) (kill-local-variable 'isearch-push-state-function) (kill-local-variable 'isearch-wrap-function) (kill-local-variable 'isearch-lazy-highlight) (remove-hook 'isearch-update-post-hook 'pdf-isearch-update t) (remove-hook 'isearch-mode-hook 'pdf-isearch-mode-initialize t) (remove-hook 'isearch-mode-end-hook 'pdf-isearch-mode-cleanup t)))) (define-minor-mode pdf-isearch-batch-mode "Isearch PDF documents batch-wise. If this mode is enabled, isearching does not stop at every match, but rather moves to the next one not currently visible. This behaviour is much faster than ordinary isearch, since far less different images have to be displayed." nil nil nil :group 'pdf-isearch (when isearch-mode (pdf-isearch-redisplay) (pdf-isearch-message (if pdf-isearch-batch-mode "batch mode" "isearch mode")))) ;; * ================================================================== * ;; * Isearch interface ;; * ================================================================== * (defvar pdf-isearch-filter-matches-function nil "A function for filtering isearch matches. The function receives one argument: a list of matches, each being a list of edges. It should return a subset of this list. Edge coordinates are in image-space.") (defvar pdf-isearch-narrow-to-page nil "Non-nil, if the search should be limited to the current page.") (defun pdf-isearch-search-function (string &rest _) "Search for STRING in the current PDF buffer. This is a Isearch interface function." (when (> (length string) 0) (let ((same-search-p (pdf-isearch-same-search-p)) (oldpage pdf-isearch-current-page) (matches (pdf-isearch-search-page string)) next-match) ;; matches is a list of list of edges ((x0 y1 x1 y2) ...), ;; sorted top to bottom ,left to right. Coordinates are in image ;; space. (unless isearch-forward (setq matches (reverse matches))) (when pdf-isearch-filter-matches-function (setq matches (funcall pdf-isearch-filter-matches-function matches))) ;; Where to go next ? (setq pdf-isearch-current-page (pdf-view-current-page) pdf-isearch-current-matches matches next-match (pdf-isearch-next-match oldpage pdf-isearch-current-page pdf-isearch-current-match matches same-search-p isearch-forward) pdf-isearch-current-parameter (list string isearch-regexp isearch-case-fold-search isearch-word)) (cond (next-match (setq pdf-isearch-current-match next-match) (pdf-isearch-hl-matches next-match matches) (pdf-isearch-focus-match next-match) ;; Don't get off track. (when (or (and (bobp) (not isearch-forward)) (and (eobp) isearch-forward)) (goto-char (1+ (/ (buffer-size) 2)))) ;; Signal success to isearch. (if isearch-forward (re-search-forward ".") (re-search-backward "."))) ((and (not pdf-isearch-narrow-to-page) (not (pdf-isearch-empty-match-p matches))) (let ((next-page (pdf-isearch-find-next-matching-page string pdf-isearch-current-page t))) (when next-page (pdf-view-goto-page next-page) (pdf-isearch-search-function string)))))))) (defun pdf-isearch-push-state-function () "Push the current search state. This is a Isearch interface function." (let ((hscroll (window-hscroll)) (vscroll (window-vscroll)) (parms pdf-isearch-current-parameter) (matches pdf-isearch-current-matches) (match pdf-isearch-current-match) (page pdf-isearch-current-page)) (lambda (_state) (setq pdf-isearch-current-parameter parms pdf-isearch-current-matches matches pdf-isearch-current-match match pdf-isearch-current-page page) (pdf-view-goto-page pdf-isearch-current-page) (when pdf-isearch-current-match (pdf-isearch-hl-matches pdf-isearch-current-match pdf-isearch-current-matches)) (image-set-window-hscroll hscroll) (image-set-window-vscroll vscroll)))) (defun pdf-isearch-wrap-function () "Go to first or last page. This is a Isearch interface function." (let ((page (if isearch-forward 1 (pdf-cache-number-of-pages)))) (unless (or pdf-isearch-narrow-to-page (= page (pdf-view-current-page))) (pdf-view-goto-page page) (let ((next-screen-context-lines 0)) (if (= page 1) (image-scroll-down) (image-scroll-up))))) (setq pdf-isearch-current-match nil)) (defun pdf-isearch-mode-cleanup () "Cleanup after exiting Isearch. This is a Isearch interface function." (pdf-isearch-active-mode -1) (pdf-view-redisplay)) (defun pdf-isearch-mode-initialize () "Initialize isearching. This is a Isearch interface function." (pdf-isearch-active-mode 1) (setq pdf-isearch-current-page (pdf-view-current-page) pdf-isearch-current-match nil pdf-isearch-current-matches nil pdf-isearch-current-parameter nil) (goto-char (1+ (/ (buffer-size) 2)))) (defun pdf-isearch-same-search-p (&optional ignore-search-string-p) "Return non-nil, if search parameter have not changed. Parameter inspected are `isearch-string' (unless IGNORE-SEARCH-STRING-P is t) and `isearch-case-fold-search'. If there was no previous search, this function returns t." (or (null pdf-isearch-current-parameter) (let ((parameter (list isearch-string isearch-regexp isearch-case-fold-search isearch-word))) (if ignore-search-string-p (equal (cdr pdf-isearch-current-parameter) (cdr parameter)) (equal pdf-isearch-current-parameter parameter))))) (defun pdf-isearch-next-match (last-page this-page last-match all-matches continued-p forward-p) "Determine the next match." (funcall (if pdf-isearch-batch-mode 'pdf-isearch-next-match-batch 'pdf-isearch-next-match-isearch) last-page this-page last-match all-matches continued-p forward-p)) (defun pdf-isearch-focus-match (current-match) "Make the CURRENT-MATCH visible in the window." (funcall (if pdf-isearch-batch-mode 'pdf-isearch-focus-match-batch 'pdf-isearch-focus-match-isearch) current-match)) (defun pdf-isearch-redisplay () "Redisplay the current highlighting." (pdf-isearch-hl-matches pdf-isearch-current-match pdf-isearch-current-matches)) (defun pdf-isearch-update () "Update search and redisplay, if necessary." (unless (pdf-isearch-same-search-p t) (setq pdf-isearch-current-parameter (list isearch-string isearch-regexp isearch-case-fold-search isearch-word) pdf-isearch-current-matches (pdf-isearch-search-page isearch-string)) (pdf-isearch-redisplay))) (defun pdf-isearch-message (fmt &rest args) "Like `message', but Isearch friendly." (unless args (setq args (list fmt) fmt "%s")) (let ((msg (apply 'format fmt args))) (if (cl-some (lambda (buf) (buffer-local-value 'isearch-mode buf)) (mapcar 'window-buffer (window-list))) (let ((isearch-message-suffix-add (format " [%s]" msg))) (isearch-message) (sit-for 1)) (message "%s" msg)))) (defun pdf-isearch-empty-match-p (matches) (and matches (cl-every (lambda (match) (cl-every (lambda (edges) (cl-every 'zerop edges)) match)) matches))) (defun pdf-isearch-occur () "Run `occur' using the last search string or regexp." (interactive) (let ((case-fold-search isearch-case-fold-search) (regexp (cond ((functionp isearch-word) (funcall isearch-word isearch-string)) (isearch-word (pdf-isearch-word-search-regexp isearch-string nil pdf-isearch-hyphenation-character)) (isearch-regexp isearch-string)))) (save-selected-window (pdf-occur (or regexp isearch-string) regexp)) (isearch-message))) (defun pdf-isearch-sync-backward () "Visit the source of the beginning of the current match." (interactive) (pdf-util-assert-pdf-window) (unless pdf-isearch-current-match (user-error "No current or recent match")) (when isearch-mode (isearch-exit)) (cl-destructuring-bind (left top _right _bot) (car pdf-isearch-current-match) (pdf-sync-backward-search left top))) ;; * ================================================================== * ;; * Interface to epdfinfo ;; * ================================================================== * (defun pdf-isearch-search-page (string &optional page) "Search STRING on PAGE in the current window. Returns a list of edges (LEFT TOP RIGHT BOTTOM) in PDF coordinates, sorted top to bottom, then left to right." (unless page (setq page (pdf-view-current-page))) (mapcar (lambda (match) (let-alist match (pdf-util-scale-relative-to-pixel .edges 'round))) (let ((case-fold-search isearch-case-fold-search)) (funcall (pdf-isearch-search-fun) string page)))) (defun pdf-isearch-search-fun () (funcall (or pdf-isearch-search-fun-function 'pdf-isearch-search-fun-default))) (defun pdf-isearch-search-fun-default () "Return default functions to use for the search." (cond ((eq isearch-word t) (lambda (string &optional pages) ;; Use lax versions to not fail at the end of the word while ;; the user adds and removes characters in the search string ;; (or when using nonincremental word isearch) (let ((lax (not (or isearch-nonincremental (null (car isearch-cmds)) (eq (length isearch-string) (length (isearch--state-string (car isearch-cmds)))))))) (pdf-info-search-regexp (pdf-isearch-word-search-regexp string lax pdf-isearch-hyphenation-character) pages 'invalid-regexp)))) (isearch-regexp (lambda (string &optional pages) (pdf-info-search-regexp string pages 'invalid-regexp))) (t 'pdf-info-search-string))) (defun pdf-isearch-word-search-regexp (string &optional lax hyphenization-chars) "Return a PCRE which matches words, ignoring punctuation." (let ((hyphenization-regexp (and hyphenization-chars (format "(?:[%s]\\n)?" (replace-regexp-in-string "[]^\\\\-]" "\\\\\\&" hyphenization-chars t))))) (cond ((equal string "") "") ((string-match-p "\\`\\W+\\'" string) "\\W+") (t (concat (if (string-match-p "\\`\\W" string) "\\W+" (unless lax "\\b")) (mapconcat (lambda (word) (if hyphenization-regexp (mapconcat (lambda (ch) (pdf-util-pcre-quote (string ch))) (append word nil) hyphenization-regexp) (pdf-util-pcre-quote word))) (split-string string "\\W+" t) "\\W+") (if (string-match-p "\\W\\'" string) "\\W+" (unless lax "\\b"))))))) (defun pdf-isearch-find-next-matching-page (string page &optional interactive-p) "Find STRING after or before page PAGE, according to FORWARD-P. If INTERACTIVE-P is non-nil, give some progress feedback. Returns the page number where STRING was found, or nil if there is no such page." ;; Do a exponentially expanding search. (let* ((incr 1) (pages (if isearch-forward (cons (1+ page) (1+ page)) (cons (1- page) (1- page)))) (fn (pdf-isearch-search-fun)) matched-page reporter) (while (and (null matched-page) (or (and isearch-forward (<= (car pages) (pdf-cache-number-of-pages))) (and (not isearch-forward) (>= (cdr pages) 1)))) (let* ((case-fold-search isearch-case-fold-search) (matches (funcall fn string pages))) (setq matched-page (alist-get 'page (if isearch-forward (car matches) (car (last matches)))))) (setq incr (* incr 2)) (cond (isearch-forward (setcar pages (1+ (cdr pages))) (setcdr pages (min (pdf-cache-number-of-pages) (+ (cdr pages) incr)))) (t (setcdr pages (1- (car pages))) (setcar pages (max 1 (- (car pages) incr))))) (when interactive-p (when (and (not reporter) (= incr 8)) ;;Don't bother right away. (setq reporter (apply 'make-progress-reporter "Searching" (if isearch-forward (list (car pages) (pdf-cache-number-of-pages) nil 0) (list 1 (cdr pages) nil 0))))) (when reporter (progress-reporter-update reporter (if isearch-forward (- (cdr pages) page) (- page (car pages))))))) matched-page)) ;; * ================================================================== * ;; * Isearch Behavior ;; * ================================================================== * (defun pdf-isearch-next-match-isearch (last-page this-page last-match matches same-search-p forward) "Default function for choosing the next match. Implements default isearch behaviour, i.e. it stops at every match." (cond ((null last-match) ;; Goto first match from top or bottom of the window. (let* ((iedges (pdf-util-image-displayed-edges)) (pos (pdf-util-with-edges (iedges) (if forward (list iedges-left iedges-top iedges-left iedges-top) (list iedges-right iedges-bot iedges-right iedges-bot))))) (pdf-isearch-closest-match (list pos) matches forward))) ((not (eq last-page this-page)) ;; First match from top-left or bottom-right of the new ;; page. (car matches)) (same-search-p ;; Next match after the last one. (if last-match (cadr (member last-match matches)))) (matches ;; Next match of new search closest to the last one. (pdf-isearch-closest-match last-match matches forward)))) (defun pdf-isearch-focus-match-isearch (match) "Make the image area in MATCH visible in the selected window." (pdf-util-scroll-to-edges (apply 'pdf-util-edges-union match))) (defun pdf-isearch-next-match-batch (last-page this-page last-match matches same-search-p forward-p) "Select the next match, unseen in the current search direction." (if (or (null last-match) (not same-search-p) (not (eq last-page this-page))) (pdf-isearch-next-match-isearch last-page this-page last-match matches same-search-p forward-p) (pdf-util-with-edges (match iedges) (let ((iedges (pdf-util-image-displayed-edges))) (car (cl-remove-if ;; Filter matches visible on screen. (lambda (edges) (let ((match (apply 'pdf-util-edges-union edges))) (and (<= match-right iedges-right) (<= match-bot iedges-bot) (>= match-left iedges-left) (>= match-top iedges-top)))) (cdr (member last-match matches)))))))) (defun pdf-isearch-focus-match-batch (match) "Make the image area in MATCH eagerly visible in the selected window." (pdf-util-scroll-to-edges (apply 'pdf-util-edges-union match) t)) (cl-deftype pdf-isearch-match () `(satisfies (lambda (match) (cl-every (lambda (edges) (and (consp edges) (= (length edges) 4) (cl-every 'numberp edges))) match)))) (cl-deftype list-of (type) `(satisfies (lambda (l) (and (listp l) (cl-every (lambda (x) (cl-typep x ',type)) l))))) (defun pdf-isearch-closest-match (match matches &optional forward-p) "Find the nearest element to MATCH in MATCHES. The direction in which to look is determined by FORWARD-P. MATCH should be a list of edges, MATCHES a list of such element; it is assumed to be ordered with respect to FORWARD-P." (cl-check-type match pdf-isearch-match) (cl-check-type matches (list-of pdf-isearch-match)) (let ((matched (apply 'pdf-util-edges-union match))) (pdf-util-with-edges (matched) (cl-loop for next in matches do (let ((edges (apply 'pdf-util-edges-union next))) (pdf-util-with-edges (edges) (when (if forward-p (or (>= edges-top matched-bot) (and (or (>= edges-top matched-top) (>= edges-bot matched-bot)) (>= edges-right matched-right))) (or (<= edges-bot matched-top) (and (or (<= edges-bot matched-bot) (<= edges-top matched-top)) (<= edges-left matched-left)))) (cl-return next)))))))) ;; * ================================================================== * ;; * Display ;; * ================================================================== * (defun pdf-isearch-current-colors () "Return the current color set. The return value depends on `pdf-view-dark-minor-mode' and `pdf-isearch-batch-mode'. It is a list of four colors \(MATCH-FG MATCH-BG LAZY-FG LAZY-BG\)." (let ((dark-p pdf-view-dark-minor-mode)) (cond (pdf-isearch-batch-mode (let ((colors (pdf-util-face-colors 'pdf-isearch-batch dark-p))) (list (car colors) (cdr colors) (car colors) (cdr colors)))) (t (let ((match (pdf-util-face-colors 'pdf-isearch-match dark-p)) (lazy (pdf-util-face-colors 'pdf-isearch-lazy dark-p))) (list (car match) (cdr match) (car lazy) (cdr lazy))))))) (defvar pdf-isearch--hl-matches-tick 0) (defun pdf-isearch-hl-matches (current matches &optional occur-hack-p) "Highlighting edges CURRENT and MATCHES." (cl-check-type current pdf-isearch-match) (cl-check-type matches (list-of pdf-isearch-match)) (cl-destructuring-bind (fg1 bg1 fg2 bg2) (pdf-isearch-current-colors) (let* ((width (car (pdf-view-image-size))) (page (pdf-view-current-page)) (window (selected-window)) (buffer (current-buffer)) (tick (cl-incf pdf-isearch--hl-matches-tick)) (pdf-info-asynchronous (lambda (status data) (when (and (null status) (eq tick pdf-isearch--hl-matches-tick) (buffer-live-p buffer) (window-live-p window) (eq (window-buffer window) buffer)) (with-selected-window window (when (and (derived-mode-p 'pdf-view-mode) (or isearch-mode occur-hack-p) (eq page (pdf-view-current-page))) (pdf-view-display-image (pdf-view-create-image data)))))))) (pdf-info-renderpage-text-regions page width t nil `(,fg1 ,bg1 ,@(pdf-util-scale-pixel-to-relative current)) `(,fg2 ,bg2 ,@(pdf-util-scale-pixel-to-relative (apply 'append (remove current matches)))))))) ;; * ================================================================== * ;; * Debug ;; * ================================================================== * ;; The following isearch-search function is debuggable. ;; (when nil (defun isearch-search () ;; Do the search with the current search string. (if isearch-message-function (funcall isearch-message-function nil t) (isearch-message nil t)) (if (and (eq isearch-case-fold-search t) search-upper-case) (setq isearch-case-fold-search (isearch-no-upper-case-p isearch-string isearch-regexp))) (condition-case lossage (let ((inhibit-point-motion-hooks ;; FIXME: equality comparisons on functions is asking for trouble. (and (eq isearch-filter-predicate 'isearch-filter-visible) search-invisible)) (inhibit-quit nil) (case-fold-search isearch-case-fold-search) (retry t)) (setq isearch-error nil) (while retry (setq isearch-success (isearch-search-string isearch-string nil t)) ;; Clear RETRY unless the search predicate says ;; to skip this search hit. (if (or (not isearch-success) (bobp) (eobp) (= (match-beginning 0) (match-end 0)) (funcall isearch-filter-predicate (match-beginning 0) (match-end 0))) (setq retry nil))) (setq isearch-just-started nil) (if isearch-success (setq isearch-other-end (if isearch-forward (match-beginning 0) (match-end 0))))) (quit (isearch-unread ?\C-g) (setq isearch-success nil)) (invalid-regexp (setq isearch-error (car (cdr lossage))) (if (string-match "\\`Premature \\|\\`Unmatched \\|\\`Invalid " isearch-error) (setq isearch-error "incomplete input"))) (search-failed (setq isearch-success nil) (setq isearch-error (nth 2 lossage))) ;; (error ;; ;; stack overflow in regexp search. ;; (setq isearch-error (format "%s" lossage))) ) (if isearch-success nil ;; Ding if failed this time after succeeding last time. (and (isearch--state-success (car isearch-cmds)) (ding)) (if (functionp (isearch--state-pop-fun (car isearch-cmds))) (funcall (isearch--state-pop-fun (car isearch-cmds)) (car isearch-cmds))) (goto-char (isearch--state-point (car isearch-cmds)))))) (provide 'pdf-isearch) ;;; pdf-isearch.el ends here ;; Local Variables: ;; byte-compile-warnings: (not obsolete) ;; End: pdf-tools-0.80/lisp/pdf-links.el000066400000000000000000000317671315513274600165550ustar00rootroot00000000000000;;; pdf-links.el --- Handle PDF links. -*- lexical-binding: t -*- ;; Copyright (C) 2013, 2014 Andreas Politz ;; Author: Andreas Politz ;; Keywords: files, multimedia ;; 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 . ;;; Commentary: ;; (require 'pdf-info) (require 'pdf-util) (require 'pdf-misc) (require 'pdf-cache) (require 'pdf-isearch) (require 'let-alist) ;;; Code: ;; * ================================================================== * ;; * Customizations ;; * ================================================================== * (defgroup pdf-links nil "Following links in PDF documents." :group 'pdf-tools) (defface pdf-links-read-link '((((background dark)) (:background "red" :foreground "yellow")) (((background light)) (:background "red" :foreground "yellow"))) "Face used to determine the colors when reading links." ;; :group 'pdf-links :group 'pdf-tools-faces) (defcustom pdf-links-read-link-convert-commands '(;;"-font" "FreeMono" "-pointsize" "%P" "-undercolor" "%f" "-fill" "%b" "-draw" "text %X,%Y '%c'") "The commands for the convert program, when decorating links for reading. See `pdf-util-convert' for an explanation of the format. Aside from the description there, two additional escape chars are available. %P -- The scaled font pointsize, i.e. IMAGE-WIDTH * SCALE (See `pdf-links-convert-pointsize-scale'). %c -- String describing the current link key (e.g. AA, AB, etc.)." :group 'pdf-links :type '(repeat string) :link '(variable-link pdf-isearch-convert-commands) :link '(url-link "http://www.imagemagick.org/script/convert.php")) (defcustom pdf-links-convert-pointsize-scale 0.01 "The scale factor for the -pointsize convert command. This determines the relative size of the font, when interactively reading links." :group 'pdf-links :type '(restricted-sexp :match-alternatives ((lambda (x) (and (numberp x) (<= x 1) (>= x 0)))))) (defcustom pdf-links-browse-uri-function 'org-open-link-from-string "The function for handling uri links. This function should accept one argument, the URI to follow, and do something with it." :group 'pdf-links :type 'function) ;; * ================================================================== * ;; * Minor Mode ;; * ================================================================== * (defvar pdf-links-minor-mode-map (let ((kmap (make-sparse-keymap))) (define-key kmap (kbd "f") 'pdf-links-isearch-link) (define-key kmap (kbd "F") 'pdf-links-action-perform) kmap)) ;;;###autoload (define-minor-mode pdf-links-minor-mode "Handle links in PDF documents.\\ If this mode is enabled, most links in the document may be activated by clicking on them or by pressing \\[pdf-links-action-perform] and selecting one of the displayed keys, or by using isearch limited to links via \\[pdf-links-isearch-link]. \\{pdf-links-minor-mode-map}" nil nil nil :group 'pdf-links (pdf-util-assert-pdf-buffer) (cond (pdf-links-minor-mode (pdf-view-add-hotspot-function 'pdf-links-hotspots-function 0)) (t (pdf-view-remove-hotspot-function 'pdf-links-hotspots-function))) (pdf-view-redisplay t)) (defun pdf-links-hotspots-function (page size) "Create hotspots for links on PAGE using SIZE." (let ((links (pdf-cache-pagelinks page)) (id-fmt "link-%d-%d") (i 0) (pointer 'hand) hotspots) (dolist (l links) (let ((e (pdf-util-scale (cdr (assq 'edges l)) size 'round)) (id (intern (format id-fmt page (cl-incf i))))) (push `((rect . ((,(nth 0 e) . ,(nth 1 e)) . (,(nth 2 e) . ,(nth 3 e)))) ,id (pointer ,pointer help-echo ,(pdf-links-action-to-string l))) hotspots) (local-set-key (vector id 'mouse-1) (lambda nil (interactive "@") (pdf-links-action-perform l))) (local-set-key (vector id t) 'pdf-util-image-map-mouse-event-proxy))) (nreverse hotspots))) (defun pdf-links-action-to-string (link) "Return a string representation of ACTION." (let-alist link (concat (cl-case .type (goto-dest (if (> .page 0) (format "Goto page %d" .page) "Destination not found")) (goto-remote (if (and .filename (file-exists-p .filename)) (format "Goto %sfile '%s'" (if (> .page 0) (format "p.%d of " .page) "") .filename) (format "Link to nonexistent file '%s'" .filename))) (uri (if (> (length .uri) 0) (format "Link to uri '%s'" .uri) (format "Link to empty uri"))) (t (format "Unrecognized link type: %s" .type))) (if (> (length .title) 0) (format " (%s)" .title))))) ;;;###autoload (defun pdf-links-action-perform (link) "Follow LINK, depending on its type. This may turn to another page, switch to another PDF buffer or invoke `pdf-links-browse-uri-function'. Interactively, link is read via `pdf-links-read-link-action'. This function displays characters around the links in the current page and starts reading characters (ignoring case). After a sufficient number of characters have been read, the corresponding link's link is invoked. Additionally, SPC may be used to scroll the current page." (interactive (list (or (pdf-links-read-link-action "Activate link (SPC scrolls): ") (error "No link selected")))) (let-alist link (cl-case .type ((goto-dest goto-remote) (let ((window (selected-window))) (cl-case .type (goto-dest (unless (> .page 0) (error "Link points to nowhere"))) (goto-remote (unless (and .filename (file-exists-p .filename)) (error "Link points to nonexistent file %s" .filename)) (setq window (display-buffer (or (find-buffer-visiting .filename) (find-file-noselect .filename)))))) (with-selected-window window (when (derived-mode-p 'pdf-view-mode) (when (> .page 0) (pdf-view-goto-page .page)) (when .top ;; Showing the tooltip delays displaying the page for ;; some reason (sit-for/redisplay don't help), do it ;; later. (run-with-idle-timer 0.001 nil (lambda () (when (window-live-p window) (with-selected-window window (when (derived-mode-p 'pdf-view-mode) (pdf-util-tooltip-arrow .top))))))))))) (uri (funcall pdf-links-browse-uri-function .uri)) (t (error "Unrecognized link type: %s" .type))) nil)) (defun pdf-links-read-link-action (prompt) "Using PROMPT, interactively read a link-action. See `pdf-links-action-perform' for the interface." (pdf-util-assert-pdf-window) (let* ((links (pdf-cache-pagelinks (pdf-view-current-page))) (keys (pdf-links-read-link-action--create-keys (length links))) (key-strings (mapcar (apply-partially 'apply 'string) keys)) (alist (cl-mapcar 'cons keys links)) (size (pdf-view-image-size)) (colors (pdf-util-face-colors 'pdf-links-read-link pdf-view-dark-minor-mode)) (args (list :foreground (car colors) :background (cdr colors) :formats `((?c . ,(lambda (_edges) (pop key-strings))) (?P . ,(number-to-string (max 1 (* (cdr size) pdf-links-convert-pointsize-scale))))) :commands pdf-links-read-link-convert-commands :apply (pdf-util-scale-relative-to-pixel (mapcar (lambda (l) (cdr (assq 'edges l))) links))))) (unless links (error "No links on this page")) (unwind-protect (let ((image-data (pdf-cache-get-image (pdf-view-current-page) (car size) (car size) 'pdf-links-read-link-action))) (unless image-data (setq image-data (apply 'pdf-util-convert-page args )) (pdf-cache-put-image (pdf-view-current-page) (car size) image-data 'pdf-links-read-link-action)) (pdf-view-display-image (create-image image-data (pdf-view-image-type) t)) (pdf-links-read-link-action--read-chars prompt alist)) (pdf-view-redisplay)))) (defun pdf-links-read-link-action--read-chars (prompt alist) (catch 'done (let (key) (while t (let* ((chars (append (mapcar 'caar alist) (mapcar 'downcase (mapcar 'caar alist)) (list ?\s))) (ch (read-char-choice prompt chars))) (setq ch (upcase ch)) (cond ((= ch ?\s) (when (= (window-vscroll) (image-scroll-up)) (image-scroll-down (window-vscroll)))) (t (setq alist (delq nil (mapcar (lambda (elt) (and (eq ch (caar elt)) (cons (cdar elt) (cdr elt)))) alist)) key (append key (list ch)) prompt (concat prompt (list ch))) (when (= (length alist) 1) (message nil) (throw 'done (cdar alist)))))))))) (defun pdf-links-read-link-action--create-keys (n) (when (> n 0) (let ((len (1+ (floor (log n 26)))) keys) (dotimes (i n) (let (key) (dotimes (_x len) (push (+ (% i 26) ?A) key) (setq i (/ i 26))) (push key keys))) (nreverse keys)))) (defun pdf-links-isearch-link () (interactive) (let* (quit-p (isearch-mode-end-hook (cons (lambda nil (setq quit-p isearch-mode-end-hook-quit)) isearch-mode-end-hook)) (pdf-isearch-filter-matches-function 'pdf-links-isearch-link-filter-matches) (pdf-isearch-narrow-to-page t) (isearch-message-prefix-add "(Links)") pdf-isearch-batch-mode) (isearch-forward) (unless (or quit-p (null pdf-isearch-current-match)) (let* ((page (pdf-view-current-page)) (match (car pdf-isearch-current-match)) (size (pdf-view-image-size)) (links (sort (cl-remove-if (lambda (e) (= 0 (pdf-util-edges-intersection-area (car e) match))) (mapcar (lambda (l) (cons (pdf-util-scale (alist-get 'edges l) size) l)) (pdf-cache-pagelinks page))) (lambda (e1 e2) (> (pdf-util-edges-intersection-area (alist-get 'edges e1) match) (pdf-util-edges-intersection-area (alist-get 'edges e2) match)))))) (unless links (error "No link found at this position")) (pdf-links-action-perform (car links)))))) (defun pdf-links-isearch-link-filter-matches (matches) (let ((links (pdf-util-scale (mapcar (apply-partially 'alist-get 'edges) (pdf-cache-pagelinks (pdf-view-current-page))) (pdf-view-image-size)))) (cl-remove-if-not (lambda (m) (cl-some (lambda (edges) (cl-some (lambda (link) (pdf-util-with-edges (link edges) (let ((area (min (* link-width link-height) (* edges-width edges-height)))) (> (/ (pdf-util-edges-intersection-area edges link) (float area)) 0.5)))) links)) m)) matches))) (provide 'pdf-links) ;;; pdf-links.el ends here pdf-tools-0.80/lisp/pdf-misc.el000066400000000000000000000243011315513274600163520ustar00rootroot00000000000000;;; pdf-misc.el --- Miscellaneous commands for PDF buffer. ;; Copyright (C) 2013, 2014 Andreas Politz ;; Author: Andreas Politz ;; Keywords: files, multimedia ;; 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 . ;;; Commentary: ;; (require 'pdf-view) (require 'pdf-util) (require 'imenu) (defvar pdf-misc-minor-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "I") 'pdf-misc-display-metadata) (define-key map (kbd "C-c C-p") 'pdf-misc-print-document) map) "Keymap used in `pdf-misc-minor-mode'.") ;;;###autoload (define-minor-mode pdf-misc-minor-mode "FIXME: Not documented." nil nil nil) ;;;###autoload (define-minor-mode pdf-misc-size-indication-minor-mode "Provide a working size indication in the mode-line." nil nil nil (pdf-util-assert-pdf-buffer) (cond (pdf-misc-size-indication-minor-mode (unless (assq 'pdf-misc-size-indication-minor-mode mode-line-position) (setq mode-line-position `((pdf-misc-size-indication-minor-mode (:eval (pdf-misc-size-indication))) ,@mode-line-position)))) (t (setq mode-line-position (cl-remove 'pdf-misc-size-indication-minor-mode mode-line-position :key 'car-safe))))) (defun pdf-misc-size-indication () "Return size indication string for the mode-line." (let ((top (= (window-vscroll nil t) 0)) (bot (>= (+ (- (nth 3 (window-inside-pixel-edges)) (nth 1 (window-inside-pixel-edges))) (window-vscroll nil t)) (cdr (pdf-view-image-size t))))) (cond ((and top bot) " All") (top " Top") (bot " Bot") (t (format " %d%%%%" (ceiling (* 100 (/ (float (window-vscroll nil t)) (cdr (pdf-view-image-size t)))))))))) (defvar pdf-misc-menu-bar-minor-mode-map (make-sparse-keymap) "The keymap used in `pdf-misc-menu-bar-minor-mode'.") (easy-menu-define nil pdf-misc-menu-bar-minor-mode-map "Menu for PDF Tools." `("PDF Tools" ["Go Backward" pdf-history-backward :visible (bound-and-true-p pdf-history-minor-mode) :active (and (bound-and-true-p pdf-history-minor-mode) (not (pdf-history-end-of-history-p)))] ["Go Forward" pdf-history-forward :visible (bound-and-true-p pdf-history-minor-mode) :active (not (pdf-history-end-of-history-p))] ["--" nil :visible (derived-mode-p 'pdf-virtual-view-mode)] ["Next file" pdf-virtual-buffer-forward-file :visible (derived-mode-p 'pdf-virtual-view-mode) :active (pdf-virtual-document-next-file (pdf-view-current-page))] ["Previous file" pdf-virtual-buffer-backward-file :visible (derived-mode-p 'pdf-virtual-view-mode) :active (not (eq 1 (pdf-view-current-page)))] ["--" nil :visible (bound-and-true-p pdf-history-minor-mode)] ["Add text annotation" pdf-annot-mouse-add-text-annotation :visible (bound-and-true-p pdf-annot-minor-mode) :keys "\\[pdf-annot-add-text-annotation]"] ("Add markup annotation" :active (pdf-view-active-region-p) :visible (and (bound-and-true-p pdf-annot-minor-mode) (pdf-info-markup-annotations-p)) ["highlight" pdf-annot-add-highlight-markup-annotation] ["squiggly" pdf-annot-add-squiggly-markup-annotation] ["underline" pdf-annot-add-underline-markup-annotation] ["strikeout" pdf-annot-add-strikeout-markup-annotation]) ["--" nil :visible (bound-and-true-p pdf-annot-minor-mode)] ["Display Annotations" pdf-annot-list-annotations :help "List all annotations" :visible (bound-and-true-p pdf-annot-minor-mode)] ["Display Attachments" pdf-annot-attachment-dired :help "Display attachments in a dired buffer" :visible (featurep 'pdf-annot)] ["Display Metadata" pdf-misc-display-metadata :help "Display information about the document" :visible (featurep 'pdf-misc)] ["Display Outline" pdf-outline :help "Display documents outline" :visible (featurep 'pdf-outline)] "--" ("Render Options" ["Printed Mode" (lambda () (interactive) (pdf-view-printer-minor-mode 'toggle)) :style toggle :selected pdf-view-printer-minor-mode :help "Display the PDF as it would be printed."] ["Midnight Mode" (lambda () (interactive) (pdf-view-midnight-minor-mode 'toggle)) :style toggle :selected pdf-view-midnight-minor-mode :help "Apply a color-filter appropriate for past midnight reading."]) "--" ["Copy region" pdf-view-kill-ring-save :keys "\\[kill-ring-save]" :active (pdf-view-active-region-p)] "--" ["Isearch document" isearch-forward :visible (bound-and-true-p pdf-isearch-minor-mode)] ["Occur document" pdf-occur :visible (featurep 'pdf-occur)] "--" ["Locate TeX source" pdf-sync-backward-search-mouse :visible (and (featurep 'pdf-sync) (equal last-command-event last-nonmenu-event))] ["--" nil :visible (and (featurep 'pdf-sync) (equal last-command-event last-nonmenu-event))] ["Print" pdf-misc-print-document :active (and (pdf-view-buffer-file-name) (file-readable-p (pdf-view-buffer-file-name)))] ["Create image" pdf-view-extract-region-image :help "Create an image of the page or the selected region(s)."] ["Create virtual PDF" pdf-virtual-buffer-create :help "Create a PDF containing all documents in this directory." :visible (bound-and-true-p pdf-virtual-global-minor-mode)] "--" ["Revert buffer" pdf-view-revert-buffer :visible (pdf-info-writable-annotations-p)] "--" ["Customize" pdf-tools-customize])) ;;;###autoload (define-minor-mode pdf-misc-menu-bar-minor-mode "Display a PDF Tools menu in the menu-bar." nil nil nil (pdf-util-assert-pdf-buffer)) (defvar pdf-misc-context-menu-minor-mode-map (let ((kmap (make-sparse-keymap))) (define-key kmap [down-mouse-3] 'pdf-misc-popup-context-menu) kmap)) ;;;###autoload (define-minor-mode pdf-misc-context-menu-minor-mode "Provide a right-click context menu in PDF buffers. \\{pdf-misc-context-menu-minor-mode-map}" nil nil nil (pdf-util-assert-pdf-buffer)) (defun pdf-misc-popup-context-menu (event) "Popup a context menu at position determined by EVENT." (interactive "@e") (popup-menu (cons 'keymap (cddr (lookup-key pdf-misc-menu-bar-minor-mode-map [menu-bar PDF\ Tools]))))) (defun pdf-misc-display-metadata () "Display all available metadata in a separate buffer." (interactive) (pdf-util-assert-pdf-buffer) (let* ((buffer (current-buffer)) (md (pdf-info-metadata))) (with-current-buffer (get-buffer-create "*PDF-Metadata*") (let* ((inhibit-read-only t) (pad (apply' max (mapcar (lambda (d) (length (symbol-name (car d)))) md))) (fmt (format "%%%ds:%%s\n" pad)) window) (erase-buffer) (setq header-line-format (buffer-name buffer) buffer-read-only t) (font-lock-mode 1) (font-lock-add-keywords nil '(("^ *\\(\\(?:\\w\\|-\\)+\\):" (1 font-lock-keyword-face)))) (dolist (d md) (let ((key (car d)) (val (cdr d))) (cl-case key (keywords (setq val (mapconcat 'identity val ", ")))) (let ((beg (+ (length (symbol-name key)) (point) 1)) (fill-prefix (make-string (1+ pad) ?\s))) (insert (format fmt key val)) (fill-region beg (point) ))))) (goto-char 1) (display-buffer (current-buffer))) md)) (defgroup pdf-misc nil "Miscellaneous options for PDF documents." :group 'pdf-tools) (defcustom pdf-misc-print-programm nil "The program used for printing. It is called with one argument, the PDF file." :group 'pdf-misc :type 'file) (defcustom pdf-misc-print-programm-args nil "List of additional arguments passed to `pdf-misc-print-program'." :group 'pdf-misc :type '(repeat string)) (defun pdf-misc-print-programm (&optional interactive-p) (or (and pdf-misc-print-programm (executable-find pdf-misc-print-programm)) (when interactive-p (let* ((default (car (delq nil (mapcar 'executable-find '("gtklp" "xpp" "gpr"))))) buffer-file-name (programm (expand-file-name (read-file-name "Print with: " default nil t nil 'file-executable-p)))) (when (and programm (executable-find programm)) (when (y-or-n-p "Save choice using customize ?") (customize-save-variable 'pdf-misc-print-programm programm)) (setq pdf-misc-print-programm programm)))))) (defun pdf-misc-print-document (filename &optional interactive-p) (interactive (list (pdf-view-buffer-file-name) t)) (cl-check-type filename (and string file-readable)) (let ((programm (pdf-misc-print-programm interactive-p)) (args (append pdf-misc-print-programm-args (list filename)))) (unless programm (error "No print program available")) (apply #'start-process "printing" nil programm args) (message "Print job started: %s %s" programm (mapconcat #'identity args " ")))) (provide 'pdf-misc) ;;; pdf-misc.el ends here pdf-tools-0.80/lisp/pdf-occur.el000066400000000000000000000732531315513274600165440ustar00rootroot00000000000000;;; pdf-occur.el --- Display matching lines of PDF documents. -*- lexical-binding: t -*- ;; Copyright (C) 2013, 2014 Andreas Politz ;; Author: Andreas Politz ;; Keywords: files, multimedia ;; 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 . ;;; Commentary: ;; (require 'pdf-tools) (require 'pdf-view) (require 'pdf-util) (require 'pdf-info) (require 'pdf-isearch) (require 'tablist) (require 'ibuf-ext) (require 'dired) (require 'let-alist) ;;; Code: ;; * ================================================================== * ;; * Custom & Variables ;; * ================================================================== * (defgroup pdf-occur nil "Display matching lines of PDF documents." :group 'pdf-tools) (defface pdf-occur-document-face '((default (:inherit font-lock-string-face))) "Face used to highlight documents in the list buffer." :group 'pdf-occur) (defface pdf-occur-page-face '((default (:inherit font-lock-type-face))) "Face used to highlight page numbers in the list buffer." :group 'pdf-occur) (defcustom pdf-occur-search-batch-size 16 "Maximum number of pages searched in one query. Lower numbers will make Emacs more responsive when searching at the cost of slightly increased search time." :group 'pdf-occur :type 'integer) (defcustom pdf-occur-prefer-string-search nil "If non-nil, reverse the meaning of the regexp-p prefix-arg." :group 'pdf-occur :type 'boolean) (defvar pdf-occur-history nil "The history variable for search strings.") (defvar pdf-occur-search-pages-left nil "The total number of pages left to search.") (defvar pdf-occur-search-documents nil "The list of searched documents. Each element should be either the filename of a PDF document or a cons \(FILENAME . PAGES\), where PAGES is the list of pages to search. See `pdf-info-normalize-page-range' for it's format.") (defvar pdf-occur-number-of-matches 0 "The number of matches in all searched documents.") (defvar pdf-occur-search-string nil "The currently used search string, resp. regexp.") (defvar pdf-occur-search-regexp-p nil "Non-nil, if searching for a regexp.") (defvar pdf-occur-buffer-mode-map (let ((kmap (make-sparse-keymap))) (set-keymap-parent kmap tablist-mode-map) (define-key kmap (kbd "RET") 'pdf-occur-goto-occurrence) (define-key kmap (kbd "C-o") 'pdf-occur-view-occurrence) (define-key kmap (kbd "SPC") 'pdf-occur-view-occurrence) (define-key kmap (kbd "C-c C-f") 'next-error-follow-minor-mode) (define-key kmap (kbd "g") 'pdf-occur-revert-buffer-with-args) (define-key kmap (kbd "K") 'pdf-occur-abort-search) (define-key kmap (kbd "D") 'pdf-occur-tablist-do-delete) (define-key kmap (kbd "x") 'pdf-occur-tablist-do-flagged-delete) (define-key kmap (kbd "A") 'pdf-occur-tablist-gather-documents) kmap) "The keymap used for `pdf-occur-buffer-mode'.") ;; * ================================================================== * ;; * High level functions ;; * ================================================================== * (define-derived-mode pdf-occur-buffer-mode tablist-mode "PDFOccur" "Major mode for output from `pdf-occur`. \\ Some useful keys are: \\[pdf-occur-abort-search] - Abort the search. \\[pdf-occur-revert-buffer-with-args] - Restart the search. \\[universal-argument] \\[pdf-occur-revert-buffer-with-args] - Restart search with different regexp. \\[universal-argument] \\[universal-argument] \\[pdf-occur-revert-buffer-with-args] - Same, but do a plain string search. \\[tablist-push-regexp-filter] - Filter matches by regexp on current or prefix-th column. \\[tablist-pop-filter] - Remove last added filter. \\[pdf-occur-tablist-do-delete] - Remove the current file from the search. \\[pdf-occur-tablist-gather-documents] - Include marked files from displayed `dired'/`ibuffer' and `pdf-view-mode' buffers in the search. \\{pdf-occur-buffer-mode-map}" (setq-local next-error-function 'pdf-occur-next-error) (setq-local revert-buffer-function 'pdf-occur-revert-buffer) (setq next-error-last-buffer (current-buffer)) (setq-local tabulated-list-sort-key nil) (setq-local tabulated-list-use-header-line t) (setq-local tablist-operations-function (lambda (op &rest _) (cl-case op (supported-operations '(find-entry)) (find-entry (let ((display-buffer-overriding-action '(display-buffer-same-window))) (pdf-occur-goto-occurrence))))))) ;;;###autoload (defun pdf-occur (string &optional regexp-p) "List lines matching STRING or PCRE. Interactively search for a regexp. Unless a prefix arg was given, in which case this functions performs a string search. If `pdf-occur-prefer-string-search' is non-nil, the meaning of the prefix-arg is inverted." (interactive (progn (pdf-util-assert-pdf-buffer) (list (pdf-occur-read-string (pdf-occur-want-regexp-search-p)) (pdf-occur-want-regexp-search-p)))) (pdf-util-assert-pdf-buffer) (pdf-occur-search (list (current-buffer)) string regexp-p)) (defvar ibuffer-filtering-qualifiers) ;;;###autoload (defun pdf-occur-multi-command () "Perform `pdf-occur' on multiple buffer. For a programmatic search of multiple documents see `pdf-occur-search'." (interactive) (ibuffer) (with-current-buffer "*Ibuffer*" (pdf-occur-ibuffer-minor-mode) (unless (member '(derived-mode . pdf-view-mode) ibuffer-filtering-qualifiers) (ibuffer-filter-by-derived-mode 'pdf-view-mode)) (message "%s" (substitute-command-keys "Mark a bunch of PDF buffers and type \\[pdf-occur-ibuffer-do-occur]")) (sit-for 3))) (defun pdf-occur-revert-buffer (&rest _) "Restart the search." (pdf-occur-assert-occur-buffer-p) (unless pdf-occur-search-documents (error "No documents to search")) (unless pdf-occur-search-string (error "Nothing to search for")) (let* ((2-columns-p (= 1 (length pdf-occur-search-documents))) (filename-width (min 24 (apply 'max (mapcar 'length (mapcar 'pdf-occur-abbrev-document (mapcar 'car pdf-occur-search-documents)))))) (page-sorter (tablist-generate-sorter (if 2-columns-p 0 1) '< 'string-to-number))) (setq tabulated-list-format (if 2-columns-p `[("Page" 4 ,page-sorter :right-align t) ("Line" 0 t)] `[("Document" ,filename-width t) ("Page" 4 ,page-sorter :right-align t) ("Line" 0 t)]) tabulated-list-entries nil)) (tabulated-list-revert) (pdf-occur-start-search pdf-occur-search-documents pdf-occur-search-string pdf-occur-search-regexp-p) (pdf-occur-update-header-line) (setq mode-line-process '(:propertize ":run" face compilation-mode-line-run))) (defun pdf-occur-revert-buffer-with-args (string &optional regexp-p documents) "Restart the search with modified arguments. Interactively just restart the search, unless a prefix was given. In this case read a new search string. With `C-u C-u' as prefix additionally invert the current state of `pdf-occur-search-regexp-p'." (interactive (progn (pdf-occur-assert-occur-buffer-p) (cond (current-prefix-arg (let ((regexp-p (if (equal current-prefix-arg '(16)) (not pdf-occur-search-regexp-p) pdf-occur-search-regexp-p))) (list (pdf-occur-read-string regexp-p) regexp-p))) (t (list pdf-occur-search-string pdf-occur-search-regexp-p))))) (setq pdf-occur-search-string string pdf-occur-search-regexp-p regexp-p) (when documents (setq pdf-occur-search-documents (pdf-occur-normalize-documents documents))) (pdf-occur-revert-buffer)) (defun pdf-occur-abort-search () "Abort the current search. This immediately kills the search process." (interactive) (unless (pdf-occur-search-in-progress-p) (user-error "No search in progress")) (pdf-info-kill-local-server) (pdf-occur-search-finished t)) ;; * ================================================================== * ;; * Finding occurrences ;; * ================================================================== * (defun pdf-occur-goto-occurrence (&optional no-select-window-p) "Go to the occurrence at point. If EVENT is nil, use occurrence at current line. Select the PDF's window, unless NO-SELECT-WINDOW-P is non-nil. FIXME: EVENT not used at the moment." (interactive) (let ((item (tabulated-list-get-id))) (when item (let* ((doc (plist-get item :document)) (page (plist-get item :page)) (match (plist-get item :match-edges)) (buffer (if (bufferp doc) doc (or (find-buffer-visiting doc) (find-file-noselect doc)))) window) (if no-select-window-p (setq window (display-buffer buffer)) (pop-to-buffer buffer) (setq window (selected-window))) (with-selected-window window (when page (pdf-view-goto-page page)) ;; Abuse isearch. (when match (let ((pixel-match (pdf-util-scale-relative-to-pixel match)) (pdf-isearch-batch-mode t)) (pdf-isearch-hl-matches pixel-match nil t) (pdf-isearch-focus-match-batch pixel-match)))))))) (defun pdf-occur-view-occurrence (&optional _event) "View the occurrence at EVENT. If EVENT is nil, use occurrence at current line." (interactive (list last-nonmenu-event)) (pdf-occur-goto-occurrence t)) (defun pdf-occur-next-error (&optional arg reset) "Move to the Nth (default 1) next match in an PDF Occur mode buffer. Compatibility function for \\[next-error] invocations." (interactive "p") ;; we need to run pdf-occur-find-match from within the Occur buffer (with-current-buffer ;; Choose the buffer and make it current. (if (next-error-buffer-p (current-buffer)) (current-buffer) (next-error-find-buffer nil nil (lambda () (eq major-mode 'pdf-occur-buffer-mode)))) (when (bobp) (setq reset t)) (if reset (goto-char (point-min)) (beginning-of-line)) (when (/= arg 0) (when (eobp) (forward-line -1)) (when reset (cl-decf arg)) (let ((line (line-number-at-pos)) (limit (line-number-at-pos (if (>= arg 0) (1- (point-max)) (point-min))))) (when (= line limit) (error "No more matches")) (forward-line (if (>= arg 0) (min arg (- limit line)) (max arg (- limit line)))))) ;; In case the *Occur* buffer is visible in a nonselected window. (tablist-move-to-major-column) (let ((win (get-buffer-window (current-buffer) t))) (if win (set-window-point win (point)))) (pdf-occur-goto-occurrence))) ;; * ================================================================== * ;; * Integration with other modes ;; * ================================================================== * ;;;###autoload (define-minor-mode pdf-occur-global-minor-mode "Enable integration of Pdf Occur with other modes. This global minor mode enables (or disables) `pdf-occur-ibuffer-minor-mode' and `pdf-occur-dired-minor-mode' in all current and future ibuffer/dired buffer." nil nil nil :global t (let ((arg (if pdf-occur-global-minor-mode 1 -1))) (dolist (buf (buffer-list)) (with-current-buffer buf (cond ((derived-mode-p 'dired-mode) (pdf-occur-dired-minor-mode arg)) ((derived-mode-p 'ibuffer-mode) (pdf-occur-ibuffer-minor-mode arg))))) (cond (pdf-occur-global-minor-mode (add-hook 'dired-mode-hook 'pdf-occur-dired-minor-mode) (add-hook 'ibuffer-mode-hook 'pdf-occur-ibuffer-minor-mode)) (t (remove-hook 'dired-mode-hook 'pdf-occur-dired-minor-mode) (remove-hook 'ibuffer-mode-hook 'pdf-occur-ibuffer-minor-mode))))) (defvar pdf-occur-ibuffer-minor-mode-map (let ((map (make-sparse-keymap))) (define-key map [remap ibuffer-do-occur] 'pdf-occur-ibuffer-do-occur) map) "Keymap used in `pdf-occur-ibuffer-minor-mode'.") ;;;###autoload (define-minor-mode pdf-occur-ibuffer-minor-mode "Hack into ibuffer's do-occur binding. This mode remaps `ibuffer-do-occur' to `pdf-occur-ibuffer-do-occur', which will start the PDF Tools version of `occur', if all marked buffer's are in `pdf-view-mode' and otherwise fallback to `ibuffer-do-occur'." nil nil nil) (defun pdf-occur-ibuffer-do-occur (&optional regexp-p) "Uses `pdf-occur-search', if appropriate. I.e. all marked buffers are in PDFView mode." (interactive (list (pdf-occur-want-regexp-search-p))) (let* ((buffer (or (ibuffer-get-marked-buffers) (and (ibuffer-current-buffer) (list (ibuffer-current-buffer))))) (pdf-only-p (cl-every (lambda (buf) (with-current-buffer buf (derived-mode-p 'pdf-view-mode))) buffer))) (if (not pdf-only-p) (call-interactively 'ibuffer-do-occur) (let ((regexp (pdf-occur-read-string regexp-p))) (pdf-occur-search buffer regexp regexp-p))))) (defvar pdf-occur-dired-minor-mode-map (let ((map (make-sparse-keymap))) (define-key map [remap dired-do-search] 'pdf-occur-dired-do-search) map) "Keymap used in `pdf-occur-dired-minor-mode'.") ;;;###autoload (define-minor-mode pdf-occur-dired-minor-mode "Hack into dired's `dired-do-search' binding. This mode remaps `dired-do-search' to `pdf-occur-dired-do-search', which will start the PDF Tools version of `occur', if all marked buffer's are in `pdf-view-mode' and otherwise fallback to `dired-do-search'." nil nil nil) (defun pdf-occur-dired-do-search () "Uses `pdf-occur-search', if appropriate. I.e. all marked files look like PDF documents." (interactive) (let ((files (dired-get-marked-files))) (if (not (cl-every (lambda (file) (string-match-p (car pdf-tools-auto-mode-alist-entry) file)) files)) (call-interactively 'dired-do-search) (let* ((regex-p (pdf-occur-want-regexp-search-p)) (regexp (pdf-occur-read-string regex-p))) (pdf-occur-search files regexp regex-p))))) ;; * ================================================================== * ;; * Search engine ;; * ================================================================== * (defun pdf-occur-search (documents string &optional regexp-p) "Search DOCUMENTS for STRING. DOCUMENTS should be a list of buffers (objects, not names), filenames or conses \(BUFFER-OR-FILENAME . PAGES\), where PAGES determines the scope of the search of the respective document. See `pdf-info-normalize-page-range' for it's format. STRING is either the string to search for or, if REGEXP-P is non-nil, a Perl compatible regular expression (PCRE). Display the occur buffer and start the search asynchronously. Returns the window where the buffer is displayed." (unless documents (error "No documents to search")) (when (or (null string) (= (length string) 0)) (error "Not searching for the empty string")) (with-current-buffer (get-buffer-create "*PDF-Occur*") (pdf-occur-buffer-mode) (setq-local pdf-occur-search-documents (pdf-occur-normalize-documents documents)) (setq-local pdf-occur-search-string string) (setq-local pdf-occur-search-regexp-p regexp-p) (setq-local pdf-occur-search-pages-left 0) (setq-local pdf-occur-number-of-matches 0) (pdf-occur-revert-buffer) (display-buffer (current-buffer)))) (defadvice tabulated-list-init-header (after update-header activate) "We want our own headers, thank you." (when (derived-mode-p 'pdf-occur-buffer-mode) (save-current-buffer (with-no-warnings (pdf-occur-update-header-line))))) (defun pdf-occur-create-entry (filename page &optional match) "Create a `tabulated-list-entries' entry for a search result. If match is nil, create a fake entry for documents w/o any matches linked with PAGE." (let* ((text (or (car match) "[No matches]")) (edges (cdr match)) (displayed-text (if match (replace-regexp-in-string "\n" "\\n" text t t) (propertize text 'face 'font-lock-warning-face))) (displayed-page (if match (propertize (format "%d" page) 'face 'pdf-occur-page-face) "")) (displayed-document (propertize (pdf-occur-abbrev-document filename) 'face 'pdf-occur-document-face)) (id `(:document ,filename :page ,page :match-text ,(if match text) :match-edges ,(if match edges)))) (list id (if (= (length pdf-occur-search-documents) 1) (vector displayed-page displayed-text) (vector displayed-document displayed-page displayed-text))))) (defun pdf-occur-update-header-line () (pdf-occur-assert-occur-buffer-p) (save-current-buffer ;;force-mode-line-update seems to sometimes spuriously change the ;;current buffer. (setq header-line-format `(:eval (concat (if (= (length pdf-occur-search-documents) 1) (format "%d match%s in document `%s'" pdf-occur-number-of-matches (if (/= 1 pdf-occur-number-of-matches) "es" "") (pdf-occur-abbrev-document (caar pdf-occur-search-documents))) (format "%d match%s in %d documents" pdf-occur-number-of-matches (if (/= 1 pdf-occur-number-of-matches) "es" "") (length pdf-occur-search-documents))) (if (pdf-occur-search-in-progress-p) (propertize (concat " [" (if (numberp pdf-occur-search-pages-left) (format "%d pages left" pdf-occur-search-pages-left) "Searching") "]") 'face 'compilation-mode-line-run))))) (force-mode-line-update))) (defun pdf-occur-search-finished (&optional abort-p) (setq pdf-occur-search-pages-left 0) (setq mode-line-process (if abort-p '(:propertize ":aborted" face compilation-mode-line-fail) '(:propertize ":exit" face compilation-mode-line-exit))) (let ((unmatched (mapcar (lambda (doc) (pdf-occur-create-entry doc 1)) (cl-set-difference (mapcar 'car pdf-occur-search-documents) (mapcar (lambda (elt) (plist-get (car elt) :document)) tabulated-list-entries) :test 'equal)))) (when (and unmatched (> (length pdf-occur-search-documents) 1)) (pdf-occur-insert-entries unmatched))) (tablist-apply-filter) (pdf-occur-update-header-line) (pdf-isearch-message (if abort-p "Search aborted." (format "Occur search finished with %d matches" pdf-occur-number-of-matches)))) (defun pdf-occur-add-matches (filename matches) (pdf-occur-assert-occur-buffer-p) (when matches (let (entries) (dolist (match matches) (let-alist match (push (pdf-occur-create-entry filename .page (cons .text .edges)) entries))) (setq entries (nreverse entries)) (pdf-occur-insert-entries entries)))) (defun pdf-occur-insert-entries (entries) "Insert tabulated-list ENTRIES at the end." (pdf-occur-assert-occur-buffer-p) (let ((inhibit-read-only t) (end-of-buffer (and (eobp) (not (bobp))))) (save-excursion (goto-char (point-max)) (dolist (elt entries) (apply tabulated-list-printer elt)) (set-buffer-modified-p nil)) (when end-of-buffer (dolist (win (get-buffer-window-list)) (set-window-point win (point-max)))) (setq tabulated-list-entries (append tabulated-list-entries entries)))) (defun pdf-occur-search-in-progress-p () (and (numberp pdf-occur-search-pages-left) (> pdf-occur-search-pages-left 0))) (defun pdf-occur-start-search (documents string &optional regexp-p) (pdf-occur-assert-occur-buffer-p) (pdf-info-make-local-server nil t) (let ((batches (pdf-occur-create-batches documents (or pdf-occur-search-batch-size 1)))) (pdf-info-local-batch-query (lambda (document pages) (if regexp-p (pdf-info-search-regexp string pages nil document) (pdf-info-search-string string pages document))) (lambda (status response document pages) (if status (error "%s" response) (when (numberp pdf-occur-search-pages-left) (cl-decf pdf-occur-search-pages-left (1+ (- (cdr pages) (car pages))))) (when (cl-member document pdf-occur-search-documents :key 'car :test 'equal) (cl-incf pdf-occur-number-of-matches (length response)) (pdf-occur-add-matches document response) (pdf-occur-update-header-line)))) (lambda (status buffer) (when (buffer-live-p buffer) (with-current-buffer buffer (pdf-occur-search-finished (eq status 'killed))))) batches) (setq pdf-occur-number-of-matches 0) (setq pdf-occur-search-pages-left (apply '+ (mapcar (lambda (elt) (1+ (- (cdr (nth 1 elt)) (car (nth 1 elt))))) batches))))) ;; * ================================================================== * ;; * Editing searched documents ;; * ================================================================== * (defun pdf-occur-tablist-do-delete (&optional arg) "Delete ARG documents from the search list." (interactive "P") (when (pdf-occur-search-in-progress-p) (user-error "Can't delete while a search is in progress.")) (let* ((items (tablist-get-marked-items arg)) (documents (cl-remove-duplicates (mapcar (lambda (entry) (plist-get (car entry) :document)) items) :test 'equal))) (unless documents (error "No documents selected")) (when (tablist-yes-or-no-p 'Stop\ searching nil (mapcar (lambda (d) (cons nil (vector d))) documents)) (setq pdf-occur-search-documents (cl-remove-if (lambda (elt) (member (car elt) documents)) pdf-occur-search-documents) tabulated-list-entries (cl-remove-if (lambda (elt) (when (member (plist-get (car elt) :document) documents) (when (plist-get (car elt) :match-edges) (cl-decf pdf-occur-number-of-matches)) t)) tabulated-list-entries)) (tablist-revert) (pdf-occur-update-header-line) (tablist-move-to-major-column)))) (defun pdf-occur-tablist-do-flagged-delete (&optional interactive) "Stop searching all documents marked with a D." (interactive "p") (let* ((tablist-marker-char ?D)) (if (save-excursion (goto-char (point-min)) (re-search-forward (tablist-marker-regexp) nil t)) (pdf-occur-tablist-do-delete) (or (not interactive) (message "(No deletions requested)"))))) (defun pdf-occur-tablist-gather-documents () "Gather marked documents in windows. Examine all dired/ibuffer windows and offer to put marked files in the search list." (interactive) (let ((searched (mapcar 'car pdf-occur-search-documents)) files) (dolist (win (window-list)) (with-selected-window win (cond ((derived-mode-p 'dired-mode) (let ((marked (dired-get-marked-files nil nil nil t))) (when (> (length marked) 1) (when (eq t (car marked)) (setq marked (cdr marked))) (setq files (append files marked nil))))) ((derived-mode-p 'ibuffer-mode) (dolist (fname (mapcar 'buffer-file-name (ibuffer-get-marked-buffers))) (when fname (push fname files)))) ((and (derived-mode-p 'pdf-view-mode) (buffer-file-name)) (push (buffer-file-name) files))))) (setq files (cl-sort ;Looks funny. (cl-set-difference (cl-remove-duplicates (cl-remove-if-not (lambda (file) (string-match-p (car pdf-tools-auto-mode-alist-entry) file)) files) :test 'file-equal-p) searched :test 'file-equal-p) 'string-lessp)) (if (null files) (message "No marked, new PDF files found in windows") (when (tablist-yes-or-no-p 'add nil (mapcar (lambda (file) (cons nil (vector file))) (cl-sort files 'string-lessp))) (setq pdf-occur-search-documents (append pdf-occur-search-documents (pdf-occur-normalize-documents files))) (message "Added %d file%s to the list of searched documents%s" (length files) (dired-plural-s (length files)) (substitute-command-keys " - Hit \\[pdf-occur-revert-buffer-with-args]")))))) ;; * ================================================================== * ;; * Utilities ;; * ================================================================== * (defun pdf-occur-read-string (&optional regexp-p) (read-string (concat (format "List lines %s" (if regexp-p "matching PCRE" "containing string")) (if pdf-occur-search-string (format " (default %s)" pdf-occur-search-string)) ": ") nil 'pdf-occur-history pdf-occur-search-string)) (defun pdf-occur-assert-occur-buffer-p () (unless (derived-mode-p 'pdf-occur-buffer-mode) (error "Not in PDF occur buffer"))) (defun pdf-occur-want-regexp-search-p () (or (and current-prefix-arg pdf-occur-prefer-string-search) (and (null current-prefix-arg) (not pdf-occur-prefer-string-search)))) ;; FIXME: This will be confusing when searching documents with the ;; same base file-name. (defun pdf-occur-abbrev-document (file-or-buffer) (if (bufferp file-or-buffer) (buffer-name file-or-buffer) (let ((abbrev (file-name-nondirectory file-or-buffer))) (if (> (length abbrev) 0) abbrev file-or-buffer)))) (defun pdf-occur-create-batches (documents batch-size) (let (queries) (dolist (d documents) (let* ((file-or-buffer (car d)) (pages (pdf-info-normalize-page-range (cdr d))) (first (car pages)) (last (if (eq (cdr pages) 0) (pdf-info-number-of-pages file-or-buffer) (cdr pages))) (npages (1+ (- last first))) (nbatches (ceiling (/ (float npages) batch-size)))) (dotimes (i nbatches) (push (list file-or-buffer (cons (+ first (* i batch-size)) (min last (+ first (1- (* (1+ i) batch-size)))))) queries)))) (nreverse queries))) (defun pdf-occur-normalize-documents (documents) "Normalize list of documents. Replaces buffers with their associated filenames \(if applicable\) and ensures that every element looks like \(FILENAME-OR-BUFFER . PAGES\)." (cl-sort (mapcar (lambda (doc) (unless (consp doc) (setq doc (cons doc nil))) (when (and (bufferp (car doc)) (buffer-file-name (car doc))) (setq doc (cons (buffer-file-name (car doc)) (cdr doc)))) (if (stringp (car doc)) (cons (expand-file-name (car doc)) (cdr doc)) doc)) documents) (lambda (a b) (string-lessp (if (bufferp a) (buffer-name a) a) (if (bufferp b) (buffer-name b) b))) :key 'car)) (provide 'pdf-occur) ;;; pdf-occur.el ends here pdf-tools-0.80/lisp/pdf-outline.el000066400000000000000000000466271315513274600171150ustar00rootroot00000000000000;;; pdf-outline.el --- Outline for PDF buffer -*- lexical-binding: t -*- ;; Copyright (C) 2013, 2014 Andreas Politz ;; Author: Andreas Politz ;; Keywords: files, multimedia ;; 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 . ;;; Commentary: ;; (require 'outline) (require 'pdf-links) (require 'pdf-view) (require 'pdf-util) (require 'cl-lib) (require 'imenu) (require 'let-alist) ;;; Code: ;; ;; User options ;; (defgroup pdf-outline nil "Display a navigatable outline of a PDF document." :group 'pdf-tools) (defcustom pdf-outline-buffer-indent 2 "The level of indent in the Outline buffer." :type 'integer :group 'pdf-outline) (defcustom pdf-outline-enable-imenu t "Whether `imenu' should be enabled in PDF documents." :group 'pdf-outline :type '(choice (const :tag "Yes" t) (const :tag "No" nil))) (defcustom pdf-outline-imenu-keep-order t "Whether `imenu' should be advised not to reorder the outline." :group 'pdf-outline :type '(choice (const :tag "Yes" t) (const :tag "No" nil))) (defcustom pdf-outline-imenu-use-flat-menus nil "Whether the constructed Imenu should be a list, rather than a tree." :group 'pdf-outline :type '(choice (const :tag "Yes" t) (const :tag "No" nil))) (defcustom pdf-outline-display-buffer-action '(nil . nil) "The display action used, when displaying the outline buffer." :group 'pdf-outline :type display-buffer--action-custom-type) (defcustom pdf-outline-display-labels nil "Whether the outline should display labels instead of page numbers. Usually a page's label is it's displayed page number." :group 'pdf-outline :type 'boolean) (defvar pdf-outline-minor-mode-map (let ((km (make-sparse-keymap))) (define-key km (kbd "o") 'pdf-outline) km) "Keymap used for `pdf-outline-minor-mode'.") (defvar pdf-outline-buffer-mode-map (let ((kmap (make-sparse-keymap))) (dotimes (i 10) (define-key kmap (vector (+ i ?0)) 'digit-argument)) (define-key kmap "-" 'negative-argument) (define-key kmap (kbd "p") 'previous-line) (define-key kmap (kbd "n") 'next-line) (define-key kmap (kbd "b") 'outline-backward-same-level) (define-key kmap (kbd "d") 'hide-subtree) (define-key kmap (kbd "a") 'show-all) (define-key kmap (kbd "s") 'show-subtree) (define-key kmap (kbd "f") 'outline-forward-same-level) (define-key kmap (kbd "u") 'pdf-outline-up-heading) (define-key kmap (kbd "Q") 'hide-sublevels) (define-key kmap (kbd "<") 'beginning-of-buffer) (define-key kmap (kbd ">") 'pdf-outline-end-of-buffer) (define-key kmap (kbd "TAB") 'outline-toggle-children) (define-key kmap (kbd "RET") 'pdf-outline-follow-link) (define-key kmap (kbd "C-o") 'pdf-outline-display-link) (define-key kmap (kbd "SPC") 'pdf-outline-display-link) (define-key kmap [mouse-1] 'pdf-outline-mouse-display-link) (define-key kmap (kbd "o") 'pdf-outline-select-pdf-window) (define-key kmap (kbd ".") 'pdf-outline-move-to-current-page) ;; (define-key kmap (kbd "Q") 'pdf-outline-quit) (define-key kmap (kbd "C-c C-q") 'pdf-outline-quit-and-kill) (define-key kmap (kbd "q") 'quit-window) (define-key kmap (kbd "M-RET") 'pdf-outline-follow-link-and-quit) (define-key kmap (kbd "C-c C-f") 'pdf-outline-follow-mode) kmap) "Keymap used in `pdf-outline-buffer-mode'.") ;; ;; Internal Variables ;; (define-button-type 'pdf-outline 'face nil 'keymap nil) (defvar-local pdf-outline-pdf-window nil "The PDF window corresponding to this outline buffer.") (defvar-local pdf-outline-pdf-document nil "The PDF filename or buffer corresponding to this outline buffer.") (defvar-local pdf-outline-follow-mode-last-link nil) ;; ;; Functions ;; ;;;###autoload (define-minor-mode pdf-outline-minor-mode "Display an outline of a PDF document. This provides a PDF's outline on the menu bar via imenu. Additionally the same outline may be viewed in a designated buffer. \\{pdf-outline-minor-mode-map}" nil nil nil (pdf-util-assert-pdf-buffer) (cond (pdf-outline-minor-mode (when pdf-outline-enable-imenu (pdf-outline-imenu-enable))) (t (when pdf-outline-enable-imenu (pdf-outline-imenu-disable))))) (define-derived-mode pdf-outline-buffer-mode outline-mode "PDF Outline" "View and traverse the outline of a PDF file. Press \\[pdf-outline-display-link] to display the PDF document, \\[pdf-outline-select-pdf-window] to select it's window, \\[pdf-outline-move-to-current-page] to move to the outline item of the current page, \\[pdf-outline-follow-link] to goto the corresponding page or \\[pdf-outline-follow-link-and-quit] to additionally quit the Outline. \\[pdf-outline-follow-mode] enters a variant of `next-error-follow-mode'. Most `outline-mode' commands are rebound to their respective last character. \\{pdf-outline-buffer-mode-map}" (setq outline-regexp "\\( *\\)." outline-level (lambda nil (1+ (/ (length (match-string 1)) pdf-outline-buffer-indent)))) (toggle-truncate-lines 1) (setq buffer-read-only t) (when (> (count-lines 1 (point-max)) (* 1.5 (frame-height))) (hide-sublevels 1)) (message "%s" (substitute-command-keys (concat "Try \\[pdf-outline-display-link], " "\\[pdf-outline-select-pdf-window], " "\\[pdf-outline-move-to-current-page] or " "\\[pdf-outline-follow-link-and-quit]")))) (define-minor-mode pdf-outline-follow-mode "Display links as point moves." nil nil nil (setq pdf-outline-follow-mode-last-link nil) (cond (pdf-outline-follow-mode (add-hook 'post-command-hook 'pdf-outline-follow-mode-pch nil t)) (t (remove-hook 'post-command-hook 'pdf-outline-follow-mode-pch t)))) (defun pdf-outline-follow-mode-pch () (let ((link (pdf-outline-link-at-pos (point)))) (when (and link (not (eq link pdf-outline-follow-mode-last-link))) (setq pdf-outline-follow-mode-last-link link) (pdf-outline-display-link (point))))) ;;;###autoload (defun pdf-outline (&optional buffer no-select-window-p) "Display an PDF outline of BUFFER. BUFFER defaults to the current buffer. Select the outline buffer, unless NO-SELECT-WINDOW-P is non-nil." (interactive (list nil (or current-prefix-arg (consp last-nonmenu-event)))) (let ((win (display-buffer (pdf-outline-noselect buffer) pdf-outline-display-buffer-action))) (unless no-select-window-p (select-window win)))) (defun pdf-outline-noselect (&optional buffer) "Create an PDF outline of BUFFER, but don't display it." (save-current-buffer (and buffer (set-buffer buffer)) (pdf-util-assert-pdf-buffer) (let* ((pdf-buffer (current-buffer)) (pdf-file (pdf-view-buffer-file-name)) (pdf-window (and (eq pdf-buffer (window-buffer)) (selected-window))) (bname (pdf-outline-buffer-name)) (buffer-exists-p (get-buffer bname)) (buffer (get-buffer-create bname))) (with-current-buffer buffer (unless buffer-exists-p (when (= 0 (save-excursion (pdf-outline-insert-outline pdf-buffer))) (kill-buffer buffer) (error "PDF has no outline")) (pdf-outline-buffer-mode)) (set (make-local-variable 'other-window-scroll-buffer) pdf-buffer) (setq pdf-outline-pdf-window pdf-window pdf-outline-pdf-document (or pdf-file pdf-buffer)) (current-buffer))))) (defun pdf-outline-buffer-name (&optional pdf-buffer) (unless pdf-buffer (setq pdf-buffer (current-buffer))) (let ((buf (format "*Outline %s*" (buffer-name pdf-buffer)))) ;; (when (buffer-live-p (get-buffer buf)) ;; (kill-buffer buf)) buf)) (defun pdf-outline-insert-outline (pdf-buffer) (let ((labels (and pdf-outline-display-labels (pdf-info-pagelabels pdf-buffer))) (nitems 0)) (dolist (item (pdf-info-outline pdf-buffer)) (let-alist item (when (eq .type 'goto-dest) (insert-text-button (concat (make-string (* (1- .depth) pdf-outline-buffer-indent) ?\s) .title (if (> .page 0) (format " (%s)" (if labels (nth (1- .page) labels) .page)) "(invalid)")) 'type 'pdf-outline 'help-echo (pdf-links-action-to-string item) 'pdf-outline-link item) (newline) (cl-incf nitems)))) nitems)) (defun pdf-outline-get-pdf-window (&optional if-visible-p) (save-selected-window (let* ((buffer (cond ((buffer-live-p pdf-outline-pdf-document) pdf-outline-pdf-document) ((bufferp pdf-outline-pdf-document) (error "PDF buffer was killed")) (t (or (find-buffer-visiting pdf-outline-pdf-document) (find-file-noselect pdf-outline-pdf-document))))) (pdf-window (if (and (window-live-p pdf-outline-pdf-window) (eq buffer (window-buffer pdf-outline-pdf-window))) pdf-outline-pdf-window (or (get-buffer-window buffer) (and (null if-visible-p) (display-buffer buffer '(nil (inhibit-same-window . t)))))))) (setq pdf-outline-pdf-window pdf-window)))) ;; ;; Commands ;; (defun pdf-outline-move-to-current-page () "Move to the item corresponding to the current page. Open nodes as necessary." (interactive) (let (page) (with-selected-window (pdf-outline-get-pdf-window) (setq page (pdf-view-current-page))) (pdf-outline-move-to-page page))) (defun pdf-outline-quit-and-kill () "Quit browsing the outline and kill it's buffer." (interactive) (pdf-outline-quit t)) (defun pdf-outline-quit (&optional kill) "Quit browsing the outline buffer." (interactive "P") (let ((win (selected-window))) (pdf-outline-select-pdf-window t) (quit-window kill win))) (defun pdf-outline-up-heading (arg &optional invisible-ok) "Like `outline-up-heading', but `push-mark' first." (interactive "p") (let ((pos (point))) (outline-up-heading arg invisible-ok) (unless (= pos (point)) (push-mark pos)))) (defun pdf-outline-end-of-buffer () "Move to the end of the outline buffer." (interactive) (let ((pos (point))) (goto-char (point-max)) (when (and (eobp) (not (bobp)) (null (button-at (point)))) (forward-line -1)) (unless (= pos (point)) (push-mark pos)))) (defun pdf-outline-link-at-pos (&optional pos) (unless pos (setq pos (point))) (let ((button (or (button-at pos) (button-at (1- pos))))) (and button (button-get button 'pdf-outline-link)))) (defun pdf-outline-follow-link (&optional pos) "Select PDF window and move to the page corresponding to POS." (interactive) (unless pos (setq pos (point))) (let ((link (pdf-outline-link-at-pos pos))) (unless link (error "Nothing to follow here")) (select-window (pdf-outline-get-pdf-window)) (pdf-links-action-perform link))) (defun pdf-outline-follow-link-and-quit (&optional pos) "Select PDF window and move to the page corresponding to POS. Then quit the outline window." (interactive) (let ((link (pdf-outline-link-at-pos (or pos (point))))) (pdf-outline-quit) (unless link (error "Nothing to follow here")) (pdf-links-action-perform link))) (defun pdf-outline-display-link (&optional pos) "Display the page corresponding to the link at POS." (interactive) (unless pos (setq pos (point))) (let ((inhibit-redisplay t) (link (pdf-outline-link-at-pos pos))) (unless link (error "Nothing to follow here")) (with-selected-window (pdf-outline-get-pdf-window) (pdf-links-action-perform link)) (force-mode-line-update t))) (defun pdf-outline-mouse-display-link (event) "Display the page corresponding to the position of EVENT." (interactive "@e") (pdf-outline-display-link (posn-point (event-start event)))) (defun pdf-outline-select-pdf-window (&optional no-create-p) "Display and select the PDF document window." (interactive) (let ((win (pdf-outline-get-pdf-window no-create-p))) (and (window-live-p win) (select-window win)))) (defun pdf-outline-toggle-subtree () "Toggle hidden state of the current complete subtree." (interactive) (save-excursion (outline-back-to-heading) (if (not (outline-invisible-p (line-end-position))) (hide-subtree) (show-subtree)))) (defun pdf-outline-move-to-page (page) "Move to an outline item corresponding to PAGE." (interactive (list (or (and current-prefix-arg (prefix-numeric-value current-prefix-arg)) (read-number "Page: ")))) (goto-char (pdf-outline-position-of-page page)) (save-excursion (while (outline-invisible-p) (outline-up-heading 1 t) (show-children))) (save-excursion (when (outline-invisible-p) (outline-up-heading 1 t) (show-children))) (back-to-indentation)) (defun pdf-outline-position-of-page (page) (let (curpage) (save-excursion (goto-char (point-min)) (while (and (setq curpage (alist-get 'page (pdf-outline-link-at-pos))) (< curpage page)) (forward-line)) (point)))) ;; ;; Imenu Support ;; ;;;###autoload (defun pdf-outline-imenu-enable () "Enable imenu in the current PDF buffer." (interactive) (pdf-util-assert-pdf-buffer) (setq-local imenu-create-index-function (if pdf-outline-imenu-use-flat-menus 'pdf-outline-imenu-create-index-flat 'pdf-outline-imenu-create-index-tree)) (imenu-add-to-menubar "PDF Outline")) (defun pdf-outline-imenu-disable () "Disable imenu in the current PDF buffer." (interactive) (pdf-util-assert-pdf-buffer) (setq-local imenu-create-index-function nil) (local-set-key [menu-bar index] nil) (when (eq pdf-view-mode-map (keymap-parent (current-local-map))) (use-local-map (keymap-parent (current-local-map))))) (defun pdf-outline-imenu-create-item (link &optional labels) (let-alist link (list (format "%s (%s)" .title (if labels (nth (1- .page) labels) .page)) 0 'pdf-outline-imenu-activate-link link))) (defun pdf-outline-imenu-create-index-flat () (let ((labels (and pdf-outline-display-labels (pdf-info-pagelabels))) index) (dolist (item (pdf-info-outline)) (let-alist item (when (eq .type 'goto-dest) (push (pdf-outline-imenu-create-item item labels) index)))) (nreverse index))) (defun pdf-outline-imenu-create-index-tree () (pdf-outline-imenu-create-index-tree-1 (pdf-outline-treeify-outline-list (cl-remove-if-not (lambda (type) (eq type 'goto-dest)) (pdf-info-outline) :key (apply-partially 'alist-get 'type))) (and pdf-outline-display-labels (pdf-info-pagelabels)))) (defun pdf-outline-imenu-create-index-tree-1 (nodes &optional labels) (mapcar (lambda (node) (let (children) (when (consp (caar node)) (setq children (cdr node) node (car node))) (let ((item (pdf-outline-imenu-create-item node labels))) (if children (cons (alist-get 'title node) (cons item (pdf-outline-imenu-create-index-tree-1 children labels))) item)))) nodes)) (defun pdf-outline-treeify-outline-list (list) (when list (let ((depth (alist-get 'depth (car list))) result) (while (and list (>= (alist-get 'depth (car list)) depth)) (when (= (alist-get 'depth (car list)) depth) (let ((item (car list))) (when (and (cdr list) (> (alist-get 'depth (cadr list)) depth)) (setq item (cons item (pdf-outline-treeify-outline-list (cdr list))))) (push item result))) (setq list (cdr list))) (reverse result)))) (defun pdf-outline-imenu-activate-link (&rest args) ;; bug #14029 (when (eq (nth 2 args) 'pdf-outline-imenu-activate-link) (setq args (cdr args))) (pdf-links-action-perform (nth 2 args))) (defadvice imenu--split-menu (around pdf-outline activate) "Advice to keep the original outline order. Calls `pdf-outline-imenu--split-menu' instead, if in a PDF buffer and `pdf-outline-imenu-keep-order' is non-nil." (if (not (and (pdf-util-pdf-buffer-p) pdf-outline-imenu-keep-order)) ad-do-it (setq ad-return-value (pdf-outline-imenu--split-menu menulist title)))) (defvar imenu--rescan-item) (defvar imenu-sort-function) (defvar imenu-create-index-function) (defvar imenu-max-items) (defun pdf-outline-imenu--split-menu (menulist title) "Replacement function for `imenu--split-menu'. This function does not move sub-menus to the top, therefore keeping the original outline order of the document. Also it does not call `imenu-sort-function'." (let ((menulist (copy-sequence menulist)) keep-at-top) (if (memq imenu--rescan-item menulist) (setq keep-at-top (list imenu--rescan-item) menulist (delq imenu--rescan-item menulist))) (if (> (length menulist) imenu-max-items) (setq menulist (mapcar (lambda (menu) (cons (format "From: %s" (caar menu)) menu)) (imenu--split menulist imenu-max-items)))) (cons title (nconc (nreverse keep-at-top) menulist)))) ;; bugfix for imenu in Emacs 24.3 and below. (when (condition-case nil (progn (imenu--truncate-items '(("" 0))) nil) (error t)) (eval-after-load "imenu" '(defun imenu--truncate-items (menulist) "Truncate all strings in MENULIST to `imenu-max-item-length'." (mapc (lambda (item) ;; Truncate if necessary. (when (and (numberp imenu-max-item-length) (> (length (car item)) imenu-max-item-length)) (setcar item (substring (car item) 0 imenu-max-item-length))) (when (imenu--subalist-p item) (imenu--truncate-items (cdr item)))) menulist)))) (provide 'pdf-outline) ;;; pdf-outline.el ends here ;; Local Variables: ;; byte-compile-warnings: (not obsolete) ;; End: pdf-tools-0.80/lisp/pdf-sync.el000066400000000000000000000724461315513274600164100ustar00rootroot00000000000000;;; pdf-sync.el --- Use synctex to correlate LaTeX-Sources with PDF positions. -*- lexical-binding:t -*- ;; Copyright (C) 2013, 2014 Andreas Politz ;; Author: Andreas Politz ;; Keywords: files, doc-view, pdf ;; 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 . ;;; Commentary: ;; ;; The backward search uses a heuristic, which is pretty simple, but ;; effective: It extracts the text around the click-position in the ;; PDF, normalizes it's whitespace, deletes certain notorious ;; character and translates certain other character into their latex ;; equivalents. This transformed text is split into a series of ;; token. A similar operation is performed on the source code around ;; the position synctex points at. These two sequences of token are ;; aligned with a standard sequence alignment algorithm, resulting in ;; an alist of matched and unmatched tokens. This is then used to ;; find the corresponding word from the PDF file in the LaTeX buffer. (require 'pdf-view) (require 'pdf-info) (require 'pdf-util) (require 'let-alist) ;;; Code: (defgroup pdf-sync nil "Jump from TeX sources to PDF pages and back." :group 'pdf-tools) (defcustom pdf-sync-forward-display-pdf-key "C-c C-g" "Key to jump from a TeX buffer to it's PDF file. This key is added to `TeX-source-correlate-method', when command `pdf-sync-minor-mode' is activated and this map is defined." :group 'pdf-sync :type 'key-sequence) (defcustom pdf-sync-backward-hook nil "Hook ran after going to a source location. The hook is run in the TeX buffer." :group 'pdf-sync :type 'hook :options '(pdf-sync-backward-beginning-of-word)) (defcustom pdf-sync-forward-hook nil "Hook ran after displaying the PDF buffer. The hook is run in the PDF's buffer." :group 'pdf-sync :type 'hook) (defcustom pdf-sync-forward-display-action nil "Display action used when displaying PDF buffers." :group 'pdf-sync :type 'display-buffer--action-custom-type) (defcustom pdf-sync-backward-display-action nil "Display action used when displaying TeX buffers." :group 'pdf-sync :type 'display-buffer--action-custom-type) (defcustom pdf-sync-locate-synctex-file-functions nil "A list of functions for locating the synctex database. Each function on this hook should accept a single argument: The absolute path of a PDF file. It should return the absolute path of the corresponding synctex database or nil, if it was unable to locate it." :group 'pdf-sync :type 'hook) (defvar pdf-sync-minor-mode-map (let ((kmap (make-sparse-keymap))) (define-key kmap [double-mouse-1] 'pdf-sync-backward-search-mouse) (define-key kmap [C-mouse-1] 'pdf-sync-backward-search-mouse) kmap)) (defcustom pdf-sync-backward-redirect-functions nil "List of functions which may redirect a backward search. Functions on this hook should accept three arguments, namely SOURCE, LINE and COLUMN, where SOURCE is the absolute filename of the source file and LINE and COLUMN denote the position in the file. COLUMN may be negative, meaning unspecified. These functions should either return nil, if no redirection is necessary. Or a list of the same structure, with some or all (or none) values modified. AUCTeX installs a function here which changes the backward search location for synthetic `TeX-region' files back to the equivalent position in the original tex file." :group 'pdf-sync) ;;;###autoload (define-minor-mode pdf-sync-minor-mode "Correlate a PDF position with the TeX file. \\ This works via SyncTeX, which means the TeX sources need to have been compiled with `--synctex=1'. In AUCTeX this can be done by setting `TeX-source-correlate-method' to 'synctex \(before AUCTeX is loaded\) and enabling `TeX-source-correlate-mode'. Then \\[pdf-sync-backward-search-mouse] in the PDF buffer will open the corresponding TeX location. If AUCTeX is your preferred tex-mode, this library arranges to bind `pdf-sync-forward-display-pdf-key' \(the default is `C-c C-g'\) to `pdf-sync-forward-search' in `TeX-source-correlate-map'. This function displays the PDF page corresponding to the current position in the TeX buffer. This function only works together with AUCTeX." nil nil nil (pdf-util-assert-pdf-buffer)) (eval-after-load "tex" '(when (and pdf-sync-forward-display-pdf-key (boundp 'TeX-source-correlate-map) (let ((key (lookup-key TeX-source-correlate-map (kbd pdf-sync-forward-display-pdf-key)))) (or (null key) (numberp key)))) (define-key TeX-source-correlate-map (kbd pdf-sync-forward-display-pdf-key) 'pdf-sync-forward-search))) ;; * ================================================================== * ;; * Backward search (PDF -> TeX) ;; * ================================================================== * (defcustom pdf-sync-backward-use-heuristic t "Whether to apply a heuristic when backward searching. If nil, just go where Synctex tells us. Otherwise try to find the exact location of the clicked-upon text in the PDF." :group 'pdf-sync :type 'boolean) (defcustom pdf-sync-backward-text-translations '((88 "X" "sum") (94 "textasciicircum") (126 "textasciitilde") (169 "copyright" "textcopyright") (172 "neg" "textlnot") (174 "textregistered" "textregistered") (176 "textdegree") (177 "pm" "textpm") (181 "upmu" "mu") (182 "mathparagraph" "textparagraph" "P" "textparagraph") (215 "times") (240 "eth" "dh") (915 "Upgamma" "Gamma") (920 "Uptheta" "Theta") (923 "Uplambda" "Lambda") (926 "Upxi" "Xi") (928 "Uppi" "Pi") (931 "Upsigma" "Sigma") (933 "Upupsilon" "Upsilon") (934 "Upphi" "Phi") (936 "Uppsi" "Psi") (945 "upalpha" "alpha") (946 "upbeta" "beta") (947 "upgamma" "gamma") (948 "updelta" "delta") (949 "upvarepsilon" "varepsilon") (950 "upzeta" "zeta") (951 "upeta" "eta") (952 "uptheta" "theta") (953 "upiota" "iota") (954 "upkappa" "varkappa" "kappa") (955 "uplambda" "lambda") (957 "upnu" "nu") (958 "upxi" "xi") (960 "uppi" "pi") (961 "upvarrho" "uprho" "rho") (962 "varsigma") (963 "upvarsigma" "upsigma" "sigma") (964 "uptau" "tau") (965 "upupsilon" "upsilon") (966 "upphi" "phi") (967 "upchi" "chi") (968 "uppsi" "psi") (969 "upomega" "omega") (977 "upvartheta" "vartheta") (981 "upvarphi" "varphi") (8224 "dagger") (8225 "ddagger") (8226 "bullet") (8486 "Upomega" "Omega") (8501 "aleph") (8592 "mapsfrom" "leftarrow") (8593 "uparrow") (8594 "to" "mapsto" "rightarrow") (8595 "downarrow") (8596 "leftrightarrow") (8656 "shortleftarrow" "Leftarrow") (8657 "Uparrow") (8658 "Mapsto" "rightrightarrows" "Rightarrow") (8659 "Downarrow") (8660 "Leftrightarrow") (8704 "forall") (8706 "partial") (8707 "exists") (8709 "varnothing" "emptyset") (8710 "Updelta" "Delta") (8711 "nabla") (8712 "in") (8722 "-") (8725 "setminus") (8727 "*") (8734 "infty") (8743 "wedge") (8744 "vee") (8745 "cap") (8746 "cup") (8756 "therefore") (8757 "because") (8764 "thicksim" "sim") (8776 "thickapprox" "approx") (8801 "equiv") (8804 "leq") (8805 "geq") (8810 "lll") (8811 "ggg") (8814 "nless") (8815 "ngtr") (8822 "lessgtr") (8823 "gtrless") (8826 "prec") (8832 "nprec") (8834 "subset") (8835 "supset") (8838 "subseteq") (8839 "supseteq") (8853 "oplus") (8855 "otimes") (8869 "bot" "perp") (9702 "circ") (9792 "female" "venus") (9793 "earth") (9794 "male" "mars") (9824 "spadesuit") (9827 "clubsuit") (9829 "heartsuit") (9830 "diamondsuit")) "Alist mapping PDF character to a list of LaTeX macro names. Adding a character here with it's LaTeX equivalent names allows the heuristic backward search to find it's location in the source file. These strings should not match `pdf-sync-backward-source-flush-regexp'. Has no effect if `pdf-sync-backward-use-heuristic' is nil." :group 'pdf-sync :type '(alist :key-type character :value-type (repeat string))) (defconst pdf-sync-backward-text-flush-regexp "[][.·{}|\\]\\|\\C.\\|-\n+" "Regexp of ignored text when backward searching.") (defconst pdf-sync-backward-source-flush-regexp "\\(?:\\\\\\(?:begin\\|end\\|\\(?:eq\\)?ref\\|label\\|cite\\){[^}]*}\\)\\|[][\\&{}$_]" "Regexp of ignored source when backward searching.") (defconst pdf-sync-backward-context-limit 64 "Number of character to include in the backward search.") (defun pdf-sync-backward-search-mouse (ev) "Go to the source corresponding to position at event EV." (interactive "@e") (let* ((posn (event-start ev)) (image (posn-image posn)) (xy (posn-object-x-y posn))) (unless image (error "Outside of image area")) (pdf-sync-backward-search (car xy) (cdr xy)))) (defun pdf-sync-backward-search (x y) "Go to the source corresponding to image coordinates X, Y. Try to find the exact position, if `pdf-sync-backward-use-heuristic' is non-nil." (cl-destructuring-bind (source finder) (pdf-sync-backward-correlate x y) (pop-to-buffer (or (find-buffer-visiting source) (find-file-noselect source)) pdf-sync-backward-display-action) (push-mark) (funcall finder) (run-hooks 'pdf-sync-backward-hook))) (defun pdf-sync-backward-correlate (x y) "Find the source corresponding to image coordinates X, Y. Returns a list \(SOURCE FINDER\), where SOURCE is the name of the TeX file and FINDER a function of zero arguments which, when called in the buffer of the aforementioned file, will try to move point to the correct position." (pdf-util-assert-pdf-window) (let ((size (pdf-view-image-size)) (page (pdf-view-current-page))) (setq x (/ x (float (car size))) y (/ y (float (cdr size)))) (let-alist (pdf-info-synctex-backward-search page x y) (let ((data (list (expand-file-name .filename) .line .column))) (cl-destructuring-bind (source line column) (or (save-selected-window (apply 'run-hook-with-args-until-success 'pdf-sync-backward-redirect-functions data)) data) (list source (if (not pdf-sync-backward-use-heuristic) (lambda nil (pdf-util-goto-position line column)) (let ((context (pdf-sync-backward--get-text-context page x y))) (lambda nil (pdf-sync-backward--find-position line column context)))))))))) (defun pdf-sync-backward--find-position (line column context) (pdf-util-goto-position line column) (cl-destructuring-bind (windex chindex words) context (let* ((swords (pdf-sync-backward--get-source-context nil (* 6 pdf-sync-backward-context-limit))) (similarity-fn (lambda (text source) (if (if (consp text) (member source text) (equal text source)) 1024 -1024))) (alignment (pdf-util-seq-alignment words swords similarity-fn 'infix))) (setq alignment (cl-remove-if-not 'car (cdr alignment))) (cl-assert (< windex (length alignment))) (let ((word (cdr (nth windex alignment)))) (unless word (setq chindex 0 word (cdr (nth (1+ windex) alignment)))) (unless word (setq word (cdr (nth (1- windex) alignment)) chindex (length word))) (when word (cl-assert (get-text-property 0 'position word) t) (goto-char (get-text-property 0 'position word)) (forward-char chindex)))))) (defun pdf-sync-backward--get-source-context (&optional position limit) (save-excursion (when position (goto-char position)) (goto-char (line-beginning-position)) (let* ((region (cond ((eq limit 'line) (cons (line-beginning-position) (line-end-position))) ;; Synctex usually jumps to the end macro, in case it ;; does not understand the environment. ((and (fboundp 'LaTeX-find-matching-begin) (looking-at " *\\\\\\(end\\){")) (cons (or (ignore-errors (save-excursion (LaTeX-find-matching-begin) (forward-line 1) (point))) (point)) (point))) ((and (fboundp 'LaTeX-find-matching-end) (looking-at " *\\\\\\(begin\\){")) (goto-char (line-end-position)) (cons (point) (or (ignore-errors (save-excursion (LaTeX-find-matching-end) (forward-line 0) (point))) (point)))) (t (cons (point) (point))))) (begin (car region)) (end (cdr region))) (when (numberp limit) (let ((delta (- limit (- end begin)))) (when (> delta 0) (setq begin (max (point-min) (- begin (/ delta 2))) end (min (point-max) (+ end (/ delta 2))))))) (let ((string (buffer-substring-no-properties begin end))) (dotimes (i (length string)) (put-text-property i (1+ i) 'position (+ begin i) string)) (nth 2 (pdf-sync-backward--tokenize (pdf-sync-backward--source-strip-comments string) nil pdf-sync-backward-source-flush-regexp)))))) (defun pdf-sync-backward--source-strip-comments (string) "Strip all standard LaTeX comments from string." (with-temp-buffer (save-excursion (insert string)) (while (re-search-forward "^\\(?:[^\\\n]\\|\\(?:\\\\\\\\\\)\\)*\\(%.*\\)" nil t) (delete-region (match-beginning 1) (match-end 1))) (buffer-string))) (defun pdf-sync-backward--get-text-context (page x y) (cl-destructuring-bind (&optional char edges) (car (pdf-info-charlayout page (cons x y))) (when edges (setq x (nth 0 edges) y (nth 1 edges))) (let* ((prefix (pdf-info-gettext page (list 0 0 x y))) (suffix (pdf-info-gettext page (list x y 1 1))) (need-suffix-space-p (memq char '(?\s ?\n))) ;; Figure out whether we missed a space by matching the ;; prefix's suffix with the line's prefix. Due to the text ;; extraction in poppler, spaces are only inserted ;; inbetween words. This test may fail, if prefix and line ;; do not overlap, which may happen in various cases, but ;; we don't care. (need-prefix-space-p (and (not need-suffix-space-p) (memq (ignore-errors (aref (pdf-info-gettext page (list x y x y) 'line) (- (length prefix) (or (cl-position ?\n prefix :from-end t) -1) 1))) '(?\s ?\n))))) (setq prefix (concat (substring prefix (max 0 (min (1- (length prefix)) (- (length prefix) pdf-sync-backward-context-limit)))) (if need-prefix-space-p " ")) suffix (concat (if need-suffix-space-p " ") (substring suffix 0 (max 0 (min (1- (length suffix)) pdf-sync-backward-context-limit))))) (pdf-sync-backward--tokenize prefix suffix pdf-sync-backward-text-flush-regexp pdf-sync-backward-text-translations)))) (defun pdf-sync-backward--tokenize (prefix &optional suffix flush-re translation) (with-temp-buffer (when prefix (insert prefix)) (let* ((center (copy-marker (point))) (case-fold-search nil)) (when suffix (insert suffix)) (goto-char 1) ;; Delete ignored text. (when flush-re (save-excursion (while (re-search-forward flush-re nil t) (replace-match " " t t)))) ;; Normalize whitespace. (save-excursion (while (re-search-forward "[ \t\f\n]+" nil t) (replace-match " " t t))) ;; Split words and non-words (save-excursion (while (re-search-forward "[^ ]\\b\\|[^ [:alnum:]]" nil t) (insert-before-markers " "))) ;; Replace character (let ((translate (lambda (string) (or (and (= (length string) 1) (cdr (assq (aref string 0) translation))) string))) words (windex -1) (chindex 0)) (skip-chars-forward " ") (while (and (not (eobp)) (<= (point) center)) (cl-incf windex) (skip-chars-forward "^ ") (skip-chars-forward " ")) (goto-char center) (when (eq ?\s (char-after)) (skip-chars-backward " ")) (setq chindex (- (skip-chars-backward "^ "))) (setq words (split-string (buffer-string))) (when translation (setq words (mapcar translate words))) (list windex chindex words))))) (defun pdf-sync-backward-beginning-of-word () "Maybe move to the beginning of the word. Don't move if already at the beginning, or if not at a word character. This function is meant to be put on `pdf-sync-backward-hook', when word-level searching is desired." (interactive) (unless (or (looking-at "\\b\\w") (not (looking-back "\\w" (1- (point))))) (backward-word))) ;; * ------------------------------------------------------------------ * ;; * Debugging backward search ;; * ------------------------------------------------------------------ * (defvar pdf-sync-backward-debug-trace nil) (defun pdf-sync-backward-debug-wrapper (fn-symbol fn &rest args) (cond ((eq fn-symbol 'pdf-sync-backward-search) (setq pdf-sync-backward-debug-trace nil) (apply fn args)) (t (let ((retval (apply fn args))) (push `(,args . ,retval) pdf-sync-backward-debug-trace) retval)))) (define-minor-mode pdf-sync-backward-debug-minor-mode "Aid in debugging the backward search." nil nil nil (if (and (fboundp 'advice-add) (fboundp 'advice-remove)) (let ((functions '(pdf-sync-backward-search pdf-sync-backward--tokenize pdf-util-seq-alignment))) (cond (pdf-sync-backward-debug-minor-mode (dolist (fn functions) (advice-add fn :around (apply-partially 'pdf-sync-backward-debug-wrapper fn) `((name . ,(format "%s-debug" fn)))))) (t (dolist (fn functions) (advice-remove fn (format "%s-debug" fn)))))) (error "Need Emacs version >= 24.4"))) (defun pdf-sync-backward-debug-explain () "Explain the last backward search. Needs to have `pdf-sync-backward-debug-minor-mode' enabled." (interactive) (unless pdf-sync-backward-debug-trace (error "No last search or `pdf-sync-backward-debug-minor-mode' not enabled.")) (with-current-buffer (get-buffer-create "*pdf-sync-backward trace*") (cl-destructuring-bind (text source alignment &rest ignored) (reverse pdf-sync-backward-debug-trace) (let* ((fill-column 68) (sep (format "\n%s\n" (make-string fill-column ?-))) (highlight '(:background "chartreuse" :foreground "black")) (or-sep "|") (inhibit-read-only t) (windex (nth 0 (cdr text))) (chindex (nth 1 (cdr text)))) (erase-buffer) (font-lock-mode -1) (view-mode 1) (insert (propertize "Text Raw:" 'face 'font-lock-keyword-face)) (insert sep) (insert (nth 0 (car text))) (insert (propertize "<|>" 'face highlight)) (insert (nth 1 (car text))) (insert sep) (insert (propertize "Text Token:" 'face 'font-lock-keyword-face)) (insert sep) (fill-region (point) (progn (insert (mapconcat (lambda (elt) (if (consp elt) (mapconcat 'identity elt or-sep) elt)) (nth 2 (cdr text)) " ")) (point))) (insert sep) (insert (propertize "Source Raw:" 'face 'font-lock-keyword-face)) (insert sep) (insert (nth 0 (car source))) (insert sep) (insert (propertize "Source Token:" 'face 'font-lock-keyword-face)) (insert sep) (fill-region (point) (progn (insert (mapconcat 'identity (nth 2 (cdr source)) " ")) (point))) (insert sep) (insert (propertize "Alignment:" 'face 'font-lock-keyword-face)) (insert (format " (windex=%d, chindex=%d" windex chindex)) (insert sep) (save-excursion (newline 2)) (let ((column 0) (index 0)) (dolist (a (cdr (cdr alignment))) (let* ((source (cdr a)) (text (if (consp (car a)) (mapconcat 'identity (car a) or-sep) (car a))) (extend (max (length text) (length source)))) (when (and (not (bolp)) (> (+ column extend) fill-column)) (forward-line 2) (newline 3) (forward-line -2) (setq column 0)) (when text (insert (propertize text 'face (if (= index windex) highlight (if source 'match 'lazy-highlight))))) (move-to-column (+ column extend) t) (insert " ") (save-excursion (forward-line) (move-to-column column t) (when source (insert (propertize source 'face (if text 'match 'lazy-highlight)))) (move-to-column (+ column extend) t) (insert " ")) (cl-incf column (+ 1 extend)) (when text (cl-incf index))))) (goto-char (point-max)) (insert sep) (goto-char 1) (pop-to-buffer (current-buffer)))))) ;; * ================================================================== * ;; * Forward search (TeX -> PDF) ;; * ================================================================== * (defun pdf-sync-forward-search (&optional line column) "Display the PDF location corresponding to LINE, COLUMN." (interactive) (cl-destructuring-bind (pdf page _x1 y1 _x2 _y2) (pdf-sync-forward-correlate line column) (let ((buffer (or (find-buffer-visiting pdf) (find-file-noselect pdf)))) (with-selected-window (display-buffer buffer pdf-sync-forward-display-action) (pdf-util-assert-pdf-window) (when page (pdf-view-goto-page page) (when y1 (let ((top (* y1 (cdr (pdf-view-image-size))))) (pdf-util-tooltip-arrow (round top)))))) (with-current-buffer buffer (run-hooks 'pdf-sync-forward-hook))))) (defun pdf-sync-forward-correlate (&optional line column) "Find the PDF location corresponding to LINE, COLUMN. Returns a list \(PDF PAGE X1 Y1 X2 Y2\), where PAGE, X1, Y1, X2 and Y2 may be nil, if the destination could not be found." (unless (fboundp 'TeX-master-file) (error "This function works only with AUCTeX")) (unless line (setq line (line-number-at-pos))) (unless column (setq column (current-column))) (let* ((pdf (expand-file-name (with-no-warnings (TeX-master-file "pdf")))) (sfilename (pdf-sync-synctex-file-name (buffer-file-name) pdf))) (cons pdf (condition-case error (let-alist (pdf-info-synctex-forward-search (or sfilename (buffer-file-name)) line column pdf) (cons .page .edges)) (error (message "%s" (error-message-string error)) (list nil nil nil nil nil)))))) ;; * ================================================================== * ;; * Dealing with synctex files. ;; * ================================================================== * (defun pdf-sync-locate-synctex-file (pdffile) "Locate the synctex database corresponding to PDFFILE. Returns either the absolute path of the database or nil. See also `pdf-sync-locate-synctex-file-functions'." (cl-check-type pdffile string) (setq pdffile (expand-file-name pdffile)) (or (run-hook-with-args-until-success 'pdf-sync-locate-synctex-file-functions pdffile) (pdf-sync-locate-synctex-file-default pdffile))) (defun pdf-sync-locate-synctex-file-default (pdffile) "The default function for locating a synctex database for PDFFILE. See also `pdf-sync-locate-synctex-file'." (let ((default-directory (file-name-directory pdffile)) (basename (file-name-sans-extension (file-name-nondirectory pdffile)))) (cl-labels ((file-if-exists-p (file) (and (file-exists-p file) file))) (or (file-if-exists-p (expand-file-name (concat basename ".synctex.gz"))) (file-if-exists-p (expand-file-name (concat basename ".synctex"))) ;; Some pdftex quote the basename. (file-if-exists-p (expand-file-name (concat "\"" basename "\"" ".synctex.gz"))) (file-if-exists-p (expand-file-name (concat "\"" basename "\"" ".synctex"))))))) (defun pdf-sync-synctex-file-name (filename pdffile) "Find SyncTeX filename corresponding to FILENAME in the context of PDFFILE. This function consults the synctex.gz database of PDFFILE and searches for a filename, which is `file-equal-p' to FILENAME. The first such filename is returned, or nil if none was found." (when (file-exists-p filename) (setq filename (expand-file-name filename)) (let* ((synctex (pdf-sync-locate-synctex-file pdffile)) (basename (file-name-nondirectory filename)) (regexp (format "^ *Input *: *[^:\n]+ *:\\(.*%s\\)$" (regexp-quote basename)))) (when (and synctex (file-readable-p synctex)) (with-current-buffer (let ((revert-without-query (list ""))) (find-file-noselect synctex)) ;; Keep point in front of the found filename. It will ;; probably be queried for again next time. (let ((beg (point)) (end (point-max))) (catch 'found (dotimes (_x 2) (while (re-search-forward regexp end t) (let ((syncname (match-string-no-properties 1))) (when (and (file-exists-p syncname) (file-equal-p filename syncname)) (goto-char (point-at-bol)) (throw 'found syncname)))) (setq end beg beg (point-min)) (goto-char beg))))))))) ;; * ================================================================== * ;; * Compatibility ;; * ================================================================== * ;;;###autoload (define-obsolete-variable-alias 'pdf-sync-tex-display-pdf-key 'pdf-sync-forward-display-pdf-key nil) ;;;###autoload (define-obsolete-variable-alias 'pdf-sync-goto-tex-hook 'pdf-sync-backward-hook nil) ;;;###autoload (define-obsolete-variable-alias 'pdf-sync-display-pdf-hook 'pdf-sync-forward-hook nil) ;;;###autoload (define-obsolete-variable-alias 'pdf-sync-display-pdf-action 'pdf-sync-forward-display-action nil) (define-obsolete-function-alias 'pdf-sync-mouse-goto-tex 'pdf-sync-backward-search-mouse) (define-obsolete-function-alias 'pdf-sync-goto-tex 'pdf-sync-backward-search) (define-obsolete-function-alias 'pdf-sync-correlate-tex 'pdf-sync-backward-correlate) (define-obsolete-function-alias 'pdf-sync-display-pdf 'pdf-sync-forward-search) (define-obsolete-function-alias 'pdf-sync-correlate-pdf 'pdf-sync-forward-correlate) (provide 'pdf-sync) ;;; pdf-sync.el ends here pdf-tools-0.80/lisp/pdf-tools.el000066400000000000000000000366011315513274600165650ustar00rootroot00000000000000;;; pdf-tools.el --- Support library for PDF documents. -*- lexical-binding:t -*- ;; Copyright (C) 2013, 2014 Andreas Politz ;; Author: Andreas Politz ;; Keywords: files, multimedia ;; Package: pdf-tools ;; Version: 0.80 ;; Package-Requires: ((emacs "24.3") (tablist "0.70") (let-alist "1.0.4")) ;; 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 . ;;; Commentary: ;; ;; PDF Tools is, among other things, a replacement of DocView for PDF ;; files. The key difference is, that pages are not prerendered by ;; e.g. ghostscript and stored in the file-system, but rather created ;; on-demand and stored in memory. ;; ;; Note: This package requires external libraries and works currently ;; only on GNU/Linux systems. ;; ;; Note: If you ever update it, you need to restart Emacs afterwards. ;; ;; To activate the package put ;; ;; (pdf-tools-install) ;; ;; somewhere in your .emacs.el . ;; ;; M-x pdf-tools-help RET ;; ;; gives some help on using the package and ;; ;; M-x pdf-tools-customize RET ;; ;; offers some customization options. ;; Features: ;; ;; * View ;; View PDF documents in a buffer with DocView-like bindings. ;; ;; * Isearch ;; Interactively search PDF documents like any other buffer. (Though ;; there is currently no regexp support.) ;; ;; * Follow links ;; Click on highlighted links, moving to some part of a different ;; page, some external file, a website or any other URI. Links may ;; also be followed by keyboard commands. ;; ;; * Annotations ;; Display and list text and markup annotations (like underline), ;; edit their contents and attributes (e.g. color), move them around, ;; delete them or create new ones and then save the modifications ;; back to the PDF file. ;; ;; * Attachments ;; Save files attached to the PDF-file or list them in a dired buffer. ;; ;; * Outline ;; Use imenu or a special buffer to examine and navigate the PDF's ;; outline. ;; ;; * SyncTeX ;; Jump from a position on a page directly to the TeX source and ;; vice-versa. ;; ;; * Misc ;; + Display PDF's metadata. ;; + Mark a region and kill the text from the PDF. ;; + Search for occurrences of a string. ;; + Keep track of visited pages via a history. ;;; Code: (require 'pdf-view) (require 'pdf-util) (require 'pdf-info) (require 'cus-edit) (require 'compile) ;; * ================================================================== * ;; * Customizables ;; * ================================================================== * (defgroup pdf-tools nil "Support library for PDF documents." :group 'doc-view) (defgroup pdf-tools-faces nil "Faces determining the colors used in the pdf-tools package. In order to customize dark and light colors use `pdf-tools-customize-faces', or set `custom-face-default-form' to 'all." :group 'pdf-tools) (defconst pdf-tools-modes '(pdf-history-minor-mode pdf-isearch-minor-mode pdf-links-minor-mode pdf-misc-minor-mode pdf-outline-minor-mode pdf-misc-size-indication-minor-mode pdf-misc-menu-bar-minor-mode pdf-annot-minor-mode pdf-sync-minor-mode pdf-misc-context-menu-minor-mode pdf-cache-prefetch-minor-mode pdf-view-auto-slice-minor-mode pdf-occur-global-minor-mode pdf-virtual-global-minor-mode)) (defcustom pdf-tools-enabled-modes '(pdf-history-minor-mode pdf-isearch-minor-mode pdf-links-minor-mode pdf-misc-minor-mode pdf-outline-minor-mode pdf-misc-size-indication-minor-mode pdf-misc-menu-bar-minor-mode pdf-annot-minor-mode pdf-sync-minor-mode pdf-misc-context-menu-minor-mode pdf-cache-prefetch-minor-mode pdf-occur-global-minor-mode ;; pdf-virtual-global-minor-mode ) "A list of automatically enabled minor-modes. PDF Tools is build as a series of minor-modes. This variable and the function `pdf-tools-install' merely serve as a convenient wrapper in order to load these modes in current and newly created PDF buffers." :group 'pdf-tools :type `(set ,@(mapcar (lambda (mode) `(function-item ,mode)) pdf-tools-modes))) (defcustom pdf-tools-enabled-hook nil "A hook ran after PDF Tools is enabled in a buffer." :group 'pdf-tools :type 'hook) (defconst pdf-tools-auto-mode-alist-entry '("\\.[pP][dD][fF]\\'" . pdf-view-mode) "The entry to use for `auto-mode-alist'.") (defun pdf-tools-customize () "Customize Pdf Tools." (interactive) (customize-group 'pdf-tools)) (defun pdf-tools-customize-faces () "Customize PDF Tool's faces." (interactive) (let ((buffer (format "*Customize Group: %s*" (custom-unlispify-tag-name 'pdf-tools-faces)))) (when (buffer-live-p (get-buffer buffer)) (with-current-buffer (get-buffer buffer) (rename-uniquely))) (customize-group 'pdf-tools-faces) (with-current-buffer buffer (set (make-local-variable 'custom-face-default-form) 'all)))) ;; * ================================================================== * ;; * Initialization ;; * ================================================================== * ;;;###autoload (defcustom pdf-tools-handle-upgrades t "Whether PDF Tools should handle upgrading itself." :group 'pdf-tools :type 'boolean) ;;;###autoload (when (and pdf-tools-handle-upgrades (boundp 'pdf-info-epdfinfo-program) (stringp pdf-info-epdfinfo-program) (boundp 'package-user-dir) (stringp package-user-dir) (stringp load-file-name)) (let* ((package-dir (file-name-directory load-file-name)) (server-dir (file-name-directory pdf-info-epdfinfo-program)) (upgrading-p (and (not (file-equal-p package-dir server-dir)) (file-in-directory-p package-dir package-user-dir) (file-in-directory-p server-dir package-user-dir) (file-executable-p pdf-info-epdfinfo-program)))) (when upgrading-p (require 'cl-lib) (when (cl-some (lambda (buffer) (and (eq 'pdf-view-mode (buffer-local-value 'major-mode buffer)) (buffer-modified-p buffer))) (buffer-list)) (when (y-or-n-p (concat "Warning: Upgrading will abandon ALL pdf modifications," "save some of them ?")) (save-some-buffers nil (lambda () (and (eq 'pdf-view-mode major-mode) (buffer-modified-p)))))) (dolist (buffer (buffer-list)) (with-current-buffer buffer (when (eq major-mode 'pdf-view-mode) (set-buffer-modified-p nil) (fundamental-mode) (let ((ov (make-overlay (point-min) (point-max)))) (overlay-put ov 'pdf-view t) (overlay-put ov 'display "Recompiling, stand by..."))))) (pdf-info-quit) (setq pdf-info-epdfinfo-program (expand-file-name "epdfinfo" package-dir)) (let ((build-hook (make-symbol "pdf-tools--upgrade"))) (fset build-hook `(lambda () (remove-hook 'post-command-hook ',build-hook) (let ((load-path (cons ,package-dir load-path)) (elc (directory-files ,package-dir nil "\\.elc\\'"))) ;; Recompile because package does it wrong. (let ((load-suffixes '(".el"))) (dolist (file elc) (load (file-name-sans-extension file)))) (byte-recompile-directory ,package-dir 0 t) (dolist (file elc) (load file))) (pdf-tools-install 'compile 'skip-deps 'no-error))) (add-hook 'post-command-hook build-hook))))) ;;;###autoload (defun pdf-tools--melpa-build-server (&optional build-directory skip-dependencies-p callback) "Compile the epdfinfo program in BUILD-DIRECTORY. This is a helper function when installing via melpa. Don't try to install dependencies if SKIP-DEPENDENCIES-P is non-nil. CALLBACK may be a function, which will be locally put on `compilation-finish-functions', which see." (if (file-executable-p pdf-info-epdfinfo-program) (message "%s" "Server already build.") (let* ((make-cmd (if (eq system-type 'berkeley-unix) "gmake" "make")) (have-apt-and-sudo (and (executable-find "apt-get") (executable-find "sudo"))) (install-server-deps (and have-apt-and-sudo (not skip-dependencies-p) (y-or-n-p "Should I try to install dependencies with apt-get ?"))) (compilation-auto-jump-to-first-error nil) (compilation-scroll-output t) compilation-buffer (compilation-buffer-name-function (lambda (&rest _) (setq compilation-buffer (generate-new-buffer-name "*compile pdf-tools*"))))) (unless (eq system-type 'windows-nt) (unless (executable-find make-cmd) (error "Executable `%s' command not found" make-cmd))) (unless build-directory (setq build-directory (expand-file-name "build" (file-name-directory pdf-info-epdfinfo-program)))) (unless (file-directory-p build-directory) (error "No such directory: %s" build-directory)) (if (not (eq system-type 'windows-nt)) (compile (format "%s V=0 -kC '%s' %smelpa-build" make-cmd build-directory (if install-server-deps "install-server-deps " " ")) install-server-deps) (let* ((arch (if (equal "X86_64" (upcase (car (split-string system-configuration "-")))) "MINGW64" "MINGW32")) (msys2-install-directory (file-name-directory (read-file-name "Path to msys2_shell.bat: ")))) (compile (format "%susr/bin/bash.exe --login -c 'MSYSTEM=%s source /etc/profile; LANG=C make V=0 -kC \"%s\" melpa-build'" msys2-install-directory arch build-directory)))) (when (and compilation-buffer (buffer-live-p (get-buffer compilation-buffer))) (when callback (with-current-buffer compilation-buffer (add-hook 'compilation-finish-functions callback nil t)))) compilation-buffer))) (defun pdf-tools-pdf-buffer-p (&optional buffer) "Return non-nil if BUFFER contains a PDF document." (save-current-buffer (when buffer (set-buffer buffer)) (save-excursion (save-restriction (widen) (goto-char 1) (looking-at "%PDF"))))) (defun pdf-tools-assert-pdf-buffer (&optional buffer) (unless (pdf-tools-pdf-buffer-p buffer) (error "Buffer does not contain a PDF document"))) (defun pdf-tools-set-modes-enabled (enable &optional modes) (dolist (m (or modes pdf-tools-enabled-modes)) (let ((enabled-p (and (boundp m) (symbol-value m)))) (unless (or (and enabled-p enable) (and (not enabled-p) (not enable))) (funcall m (if enable 1 -1)))))) ;;;###autoload (defun pdf-tools-enable-minor-modes (&optional modes) "Enable MODES in the current buffer. MODES defaults to `pdf-tools-enabled-modes'." (interactive) (pdf-util-assert-pdf-buffer) (pdf-tools-set-modes-enabled t modes) (run-hooks 'pdf-tools-enabled-hook)) (defun pdf-tools-disable-minor-modes (&optional modes) "Disable MODES in the current buffer. MODES defaults to `pdf-tools-enabled-modes'." (interactive) (pdf-tools-set-modes-enabled nil modes)) (declare-function pdf-occur-global-minor-mode "pdf-occur.el") (declare-function pdf-virtual-global-minor-mode "pdf-virtual.el") ;;;###autoload (defun pdf-tools-install (&optional force-compile-p skip-dependencies-p no-error) "Install PDF-Tools in all current and future PDF buffers. See `pdf-view-mode' and `pdf-tools-enabled-modes'." (interactive) (cond ((not (file-executable-p pdf-info-epdfinfo-program)) (when (or force-compile-p (y-or-n-p "Need to build the server, do it now ? ")) (pdf-tools--melpa-build-server nil skip-dependencies-p (lambda (buffer _status) (when (buffer-live-p buffer) (display-buffer buffer)) (when (file-executable-p pdf-info-epdfinfo-program) (let ((pdf-info-restart-process-p t)) (pdf-tools-install)))))) (funcall (if no-error 'message 'error) "%s" "No executable `epdfinfo' found")) (t (add-to-list 'auto-mode-alist pdf-tools-auto-mode-alist-entry) ;; FIXME: Generalize this sometime. (when (memq 'pdf-occur-global-minor-mode pdf-tools-enabled-modes) (pdf-occur-global-minor-mode 1)) (when (memq 'pdf-virtual-global-minor-mode pdf-tools-enabled-modes) (pdf-virtual-global-minor-mode 1)) (add-hook 'pdf-view-mode-hook 'pdf-tools-enable-minor-modes) (dolist (buf (buffer-list)) (with-current-buffer buf (when (and (not (derived-mode-p 'pdf-view-mode)) (pdf-tools-pdf-buffer-p) (buffer-file-name)) (pdf-view-mode))))))) (defun pdf-tools-uninstall () "Uninstall PDF-Tools in all current and future PDF buffers." (interactive) (pdf-info-quit) (setq-default auto-mode-alist (remove pdf-tools-auto-mode-alist-entry auto-mode-alist)) (pdf-occur-global-minor-mode -1) (pdf-virtual-global-minor-mode -1) (remove-hook 'pdf-view-mode-hook 'pdf-tools-enable-minor-modes) (dolist (buf (buffer-list)) (with-current-buffer buf (when (pdf-util-pdf-buffer-p buf) (pdf-tools-disable-minor-modes pdf-tools-modes) (normal-mode))))) ;;;###autoload (defun pdf-tools-help () (interactive) (help-setup-xref (list #'pdf-tools-help) (called-interactively-p 'interactive)) (with-help-window (help-buffer) (princ "PDF Tools Help\n\n") (princ " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n") (dolist (m (cons 'pdf-view-mode (sort (copy-sequence pdf-tools-modes) 'string<))) (princ (format "`%s' is " m)) (describe-function-1 m) (terpri) (terpri) (princ " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n")))) ;; * ================================================================== * ;; * Debugging ;; * ================================================================== * (defvar pdf-tools-debug nil "Non-nil, if debugging PDF Tools.") (defun pdf-tools-toggle-debug () (interactive) (setq pdf-tools-debug (not pdf-tools-debug)) (when (called-interactively-p 'any) (message "Toggled debugging %s" (if pdf-tools-debug "on" "off")))) (provide 'pdf-tools) ;;; pdf-tools.el ends here pdf-tools-0.80/lisp/pdf-util.el000066400000000000000000001410761315513274600164050ustar00rootroot00000000000000;;; pdf-util.el --- PDF Utility functions. -*- lexical-binding: t -*- ;; Copyright (C) 2013, 2014 Andreas Politz ;; Author: Andreas Politz ;; Keywords: files, multimedia ;; 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 . ;;; Commentary: ;; ;;; Todo: ;; ;;; Code: (require 'cl-lib) (require 'format-spec) (require 'faces) ;; These functions are only used after a PdfView window was asserted, ;; which won't succeed, if pdf-view.el isn't loaded. (declare-function pdf-view-image-size "pdf-view") (declare-function pdf-view-image-offset "pdf-view") (declare-function pdf-view-current-image "pdf-view") (declare-function pdf-view-current-overlay "pdf-view") (declare-function pdf-cache-pagesize "pdf-cache") ;; * ================================================================== * ;; * Compatibility with older Emacssen (< 25.1) ;; * ================================================================== * ;; The with-file-modes macro is only available in recent Emacs ;; versions. (eval-when-compile (unless (fboundp 'with-file-modes) (defmacro with-file-modes (modes &rest body) "Execute BODY with default file permissions temporarily set to MODES. MODES is as for `set-default-file-modes'." (declare (indent 1) (debug t)) (let ((umask (make-symbol "umask"))) `(let ((,umask (default-file-modes))) (unwind-protect (progn (set-default-file-modes ,modes) ,@body) (set-default-file-modes ,umask))))))) (unless (fboundp 'alist-get) ;;25.1 (defun alist-get (key alist &optional default remove) "Get the value associated to KEY in ALIST. DEFAULT is the value to return if KEY is not found in ALIST. REMOVE, if non-nil, means that when setting this element, we should remove the entry if the new value is `eql' to DEFAULT." (ignore remove) ;;Silence byte-compiler. (let ((x (assq key alist))) (if x (cdr x) default)))) (require 'register) (unless (fboundp 'register-read-with-preview) (defalias 'register-read-with-preview 'read-char "Compatibility alias for pdf-tools.")) ;; In Emacs 24.3 window-width does not have a PIXELWISE argument. (defmacro pdf-util-window-pixel-width (&optional window) "Return the width of WINDOW in pixel." (if (< (cdr (subr-arity (symbol-function 'window-body-width))) 2) (let ((window* (make-symbol "window"))) `(let ((,window* ,window)) (* (window-body-width ,window*) (frame-char-width (window-frame ,window*))))) `(window-body-width ,window t))) ;; In Emacs 24.3 image-mode-winprops leads to infinite recursion. (unless (or (> emacs-major-version 24) (and (= emacs-major-version 24) (>= emacs-minor-version 4))) (require 'image-mode) (defvar image-mode-winprops-original-function (symbol-function 'image-mode-winprops)) (eval-after-load "image-mode" '(defun image-mode-winprops (&optional window cleanup) (if (not (eq major-mode 'pdf-view-mode)) (funcall image-mode-winprops-original-function window cleanup) (cond ((null window) (setq window (if (eq (current-buffer) (window-buffer)) (selected-window) t))) ((eq window t)) ((not (windowp window)) (error "Not a window: %s" window))) (when cleanup (setq image-mode-winprops-alist (delq nil (mapcar (lambda (winprop) (let ((w (car-safe winprop))) (if (or (not (windowp w)) (window-live-p w)) winprop))) image-mode-winprops-alist)))) (let ((winprops (assq window image-mode-winprops-alist))) ;; For new windows, set defaults from the latest. (if winprops ;; Move window to front. (setq image-mode-winprops-alist (cons winprops (delq winprops image-mode-winprops-alist))) (setq winprops (cons window (copy-alist (cdar image-mode-winprops-alist)))) ;; Add winprops before running the hook, to avoid inf-loops if the hook ;; triggers window-configuration-change-hook. (setq image-mode-winprops-alist (cons winprops image-mode-winprops-alist)) (run-hook-with-args 'image-mode-new-window-functions winprops)) winprops))))) ;; * ================================================================== * ;; * Transforming coordinates ;; * ================================================================== * (defun pdf-util-scale (list-of-edges-or-pos scale &optional rounding-fn) "Scale LIST-OF-EDGES-OR-POS by SCALE. SCALE is a cons (SX . SY), by which edges/positions are scaled. If ROUNDING-FN is non-nil, it should be a function of one argument, a real value, returning a rounded value (e.g. `ceiling'). The elements in LIST-OF-EDGES-OR-POS should be either a list \(LEFT TOP RIGHT BOT\) or a position \(X . Y\). LIST-OF-EDGES-OR-POS may also be a single such element. Return scaled list of edges if LIST-OF-EDGES-OR-POS was indeed a list, else return the scaled singleton." (let ((have-list-p (listp (car list-of-edges-or-pos)))) (unless have-list-p (setq list-of-edges-or-pos (list list-of-edges-or-pos))) (let* ((sx (car scale)) (sy (cdr scale)) (result (mapcar (lambda (edges) (cond ((consp (cdr edges)) (let ((e (list (* (nth 0 edges) sx) (* (nth 1 edges) sy) (* (nth 2 edges) sx) (* (nth 3 edges) sy)))) (if rounding-fn (mapcar rounding-fn e) e))) (rounding-fn (cons (funcall rounding-fn (* (car edges) sx)) (funcall rounding-fn (* (cdr edges) sy)))) (t (cons (* (car edges) sx) (* (cdr edges) sy))))) list-of-edges-or-pos))) (if have-list-p result (car result))))) (defun pdf-util-scale-to (list-of-edges from to &optional rounding-fn) "Scale LIST-OF-EDGES in FROM basis to TO. FROM and TO should both be a cons \(WIDTH . HEIGTH\). See also `pdf-util-scale'." (pdf-util-scale list-of-edges (cons (/ (float (car to)) (float (car from))) (/ (float (cdr to)) (float (cdr from)))) rounding-fn)) (defun pdf-util-scale-pixel-to-points (list-of-pixel-edges &optional rounding-fn displayed-p window) "Scale LIST-OF-PIXEL-EDGES to point values. The result depends on the currently displayed page in WINDOW. See also `pdf-util-scale'." (pdf-util-assert-pdf-window window) (pdf-util-scale-to list-of-pixel-edges (pdf-view-image-size displayed-p window) (pdf-cache-pagesize (pdf-view-current-page window)) rounding-fn)) (defun pdf-util-scale-points-to-pixel (list-of-points-edges &optional rounding-fn displayed-p window) "Scale LIST-OF-POINTS-EDGES to point values. The result depends on the currently displayed page in WINDOW. See also `pdf-util-scale'." (pdf-util-assert-pdf-window window) (pdf-util-scale-to list-of-points-edges (pdf-cache-pagesize (pdf-view-current-page window)) (pdf-view-image-size displayed-p window) rounding-fn)) (defun pdf-util-scale-relative-to-points (list-of-relative-edges &optional rounding-fn window) "Scale LIST-OF-RELATIVE-EDGES to point values. The result depends on the currently displayed page in WINDOW. See also `pdf-util-scale'." (pdf-util-assert-pdf-window window) (pdf-util-scale-to list-of-relative-edges '(1.0 . 1.0) (pdf-cache-pagesize (pdf-view-current-page window)) rounding-fn)) (defun pdf-util-scale-points-to-relative (list-of-points-edges &optional rounding-fn window) "Scale LIST-OF-POINTS-EDGES to relative values. See also `pdf-util-scale'." (pdf-util-assert-pdf-window window) (pdf-util-scale-to list-of-points-edges (pdf-cache-pagesize (pdf-view-current-page window)) '(1.0 . 1.0) rounding-fn)) (defun pdf-util-scale-pixel-to-relative (list-of-pixel-edges &optional rounding-fn displayed-p window) "Scale LIST-OF-PIXEL-EDGES to relative values. The result depends on the currently displayed page in WINDOW. See also `pdf-util-scale'." (pdf-util-assert-pdf-window window) (pdf-util-scale-to list-of-pixel-edges (pdf-view-image-size displayed-p window) '(1.0 . 1.0) rounding-fn)) (defun pdf-util-scale-relative-to-pixel (list-of-relative-edges &optional rounding-fn displayed-p window) "Scale LIST-OF-EDGES to match SIZE. The result depends on the currently displayed page in WINDOW. See also `pdf-util-scale'." (pdf-util-assert-pdf-window window) (pdf-util-scale-to list-of-relative-edges '(1.0 . 1.0) (pdf-view-image-size displayed-p window) rounding-fn)) (defun pdf-util-translate (list-of-edges-or-pos offset &optional opposite-direction-p) "Translate LIST-OF-EDGES-OR-POS by OFFSET OFFSET should be a cons \(X . Y\), by which to translate LIST-OF-EDGES-OR-POS. If OPPOSITE-DIRECTION-P is non-nil translate by \(-X . -Y\). See `pdf-util-scale' for the LIST-OF-EDGES-OR-POS argument." (let ((have-list-p (listp (car list-of-edges-or-pos)))) (unless have-list-p (setq list-of-edges-or-pos (list list-of-edges-or-pos))) (let* ((ox (if opposite-direction-p (- (car offset)) (car offset))) (oy (if opposite-direction-p (- (cdr offset)) (cdr offset))) (result (mapcar (lambda (edges) (cond ((consp (cdr edges)) (list (+ (nth 0 edges) ox) (+ (nth 1 edges) oy) (+ (nth 2 edges) ox) (+ (nth 3 edges) oy))) (t (cons (+ (car edges) ox) (+ (cdr edges) oy))))) list-of-edges-or-pos))) (if have-list-p result (car result))))) (defun pdf-util-edges-transform (region elts &optional to-region-p) "Translate ELTS according to REGION. ELTS may be one edges list or a position or a list thereof. Translate each from region coordinates to (0 0 1 1) or the opposite, if TO-REGION-P is non-nil. All coordinates should be relative. Returns the translated list of elements or the single one depending on the input." (when elts (let ((have-list-p (consp (car-safe elts)))) (unless have-list-p (setq elts (list elts))) (let ((result (if (null region) elts (mapcar (lambda (edges) (let ((have-pos-p (numberp (cdr edges)))) (when have-pos-p (setq edges (list (car edges) (cdr edges) (car edges) (cdr edges)))) (pdf-util-with-edges (edges region) (let ((newedges (mapcar (lambda (n) (min 1.0 (max 0.0 n))) (if to-region-p `(,(/ (- edges-left region-left) region-width) ,(/ (- edges-top region-top) region-height) ,(/ (- edges-right region-left) region-width) ,(/ (- edges-bot region-top) region-height)) `(,(+ (* edges-left region-width) region-left) ,(+ (* edges-top region-height) region-top) ,(+ (* edges-right region-width) region-left) ,(+ (* edges-bot region-height) region-top)))))) (if have-pos-p (cons (car newedges) (cadr newedges)) newedges))))) elts)))) (if have-list-p result (car result)))))) (defmacro pdf-util-with-edges (list-of-edges &rest body) "Provide some convenient macros for the edges in LIST-OF-EDGES. LIST-OF-EDGES should be a list of variables \(X ...\), each one holding a list of edges. Inside BODY the symbols X-left, X-top, X-right, X-bot, X-width and X-height expand to their respective values." (declare (indent 1) (debug (sexp &rest form))) (unless (cl-every 'symbolp list-of-edges) (error "Argument should be a list of symbols")) (let ((list-of-syms (mapcar (lambda (edge) (cons edge (mapcar (lambda (kind) (intern (format "%s-%s" edge kind))) '(left top right bot width height)))) list-of-edges))) (macroexpand-all `(cl-symbol-macrolet ,(apply 'nconc (mapcar (lambda (edge-syms) (let ((edge (nth 0 edge-syms)) (syms (cdr edge-syms))) `((,(pop syms) (nth 0 ,edge)) (,(pop syms) (nth 1 ,edge)) (,(pop syms) (nth 2 ,edge)) (,(pop syms) (nth 3 ,edge)) (,(pop syms) (- (nth 2 ,edge) (nth 0 ,edge))) (,(pop syms) (- (nth 3 ,edge) (nth 1 ,edge)))))) list-of-syms)) ,@body)))) ;; * ================================================================== * ;; * Scrolling ;; * ================================================================== * (defun pdf-util-image-displayed-edges (&optional window displayed-p) "Return the visible region of the image in WINDOW. Returns a list of pixel edges." (pdf-util-assert-pdf-window) (let* ((edges (window-inside-pixel-edges window)) (isize (pdf-view-image-size displayed-p window)) (offset (if displayed-p `(0 . 0) (pdf-view-image-offset window))) (hscroll (* (window-hscroll window) (frame-char-width (window-frame window)))) (vscroll (window-vscroll window t)) (x0 (+ hscroll (car offset))) (y0 (+ vscroll (cdr offset))) (x1 (min (car isize) (+ x0 (- (nth 2 edges) (nth 0 edges))))) (y1 (min (cdr isize) (+ y0 (- (nth 3 edges) (nth 1 edges)))))) (mapcar 'round (list x0 y0 x1 y1)))) (defun pdf-util-required-hscroll (edges &optional eager-p context-pixel) "Return the amount of scrolling nescessary, to make image EDGES visible. Scroll as little as necessary. Unless EAGER-P is non-nil, in which case scroll as much as possible. Keep CONTEXT-PIXEL pixel of the image visible at the bottom and top of the window. CONTEXT-PIXEL defaults to 0. Return the required hscroll in columns or nil, if scrolling is not needed." (pdf-util-assert-pdf-window) (unless context-pixel (setq context-pixel 0)) (let* ((win (window-inside-pixel-edges)) (image-width (car (pdf-view-image-size t))) (image-left (* (frame-char-width) (window-hscroll))) (edges (pdf-util-translate edges (pdf-view-image-offset) t))) (pdf-util-with-edges (win edges) (let* ((edges-left (- edges-left context-pixel)) (edges-right (+ edges-right context-pixel))) (if (< edges-left image-left) (round (/ (max 0 (if eager-p (- edges-right win-width) edges-left)) (frame-char-width))) (if (> (min image-width edges-right) (+ image-left win-width)) (round (/ (min (- image-width win-width) (if eager-p edges-left (- edges-right win-width))) (frame-char-width))))))))) (defun pdf-util-required-vscroll (edges &optional eager-p context-pixel) "Return the amount of scrolling nescessary, to make image EDGES visible. Scroll as little as necessary. Unless EAGER-P is non-nil, in which case scroll as much as possible. Keep CONTEXT-PIXEL pixel of the image visible at the bottom and top of the window. CONTEXT-PIXEL defaults to an equivalent pixel value of `next-screen-context-lines'. Return the required vscroll in lines or nil, if scrolling is not needed." (pdf-util-assert-pdf-window) (let* ((win (window-inside-pixel-edges)) (image-height (cdr (pdf-view-image-size t))) (image-top (window-vscroll nil t)) (edges (pdf-util-translate edges (pdf-view-image-offset) t))) (pdf-util-with-edges (win edges) (let* ((context-pixel (or context-pixel (* next-screen-context-lines (frame-char-height)))) ;;Be careful not to modify edges. (edges-top (- edges-top context-pixel)) (edges-bot (+ edges-bot context-pixel))) (if (< edges-top image-top) (round (/ (max 0 (if eager-p (- edges-bot win-height) edges-top)) (float (frame-char-height)))) (if (> (min image-height edges-bot) (+ image-top win-height)) (round (/ (min (- image-height win-height) (if eager-p edges-top (- edges-bot win-height))) (float (frame-char-height)))))))))) (defun pdf-util-scroll-to-edges (edges &optional eager-p) "Scroll window such that image EDGES are visible. Scroll as little as necessary. Unless EAGER-P is non-nil, in which case scroll as much as possible." (let ((vscroll (pdf-util-required-vscroll edges eager-p)) (hscroll (pdf-util-required-hscroll edges eager-p))) (when vscroll (image-set-window-vscroll vscroll)) (when hscroll (image-set-window-hscroll hscroll)))) ;; * ================================================================== * ;; * Temporary files ;; * ================================================================== * (defvar pdf-util--base-directory nil "Base directory for temporary files.") (defvar-local pdf-util--dedicated-directory nil "The relative name of buffer's dedicated directory.") (defun pdf-util-dedicated-directory () "Return the name of a existing dedicated directory. The directory is exclusive to the current buffer. It will be automatically deleted, if Emacs or the current buffer are killed." (with-file-modes #o0700 (unless (and pdf-util--base-directory (file-directory-p pdf-util--base-directory) (not (file-symlink-p pdf-util--base-directory))) (add-hook 'kill-emacs-hook (lambda nil (when (and pdf-util--base-directory (file-directory-p pdf-util--base-directory)) (delete-directory pdf-util--base-directory t)))) (setq pdf-util--base-directory (make-temp-file "pdf-tools-" t))) (unless (and pdf-util--dedicated-directory (file-directory-p pdf-util--dedicated-directory) (not (file-symlink-p pdf-util--base-directory))) (let ((temporary-file-directory pdf-util--base-directory)) (setq pdf-util--dedicated-directory (make-temp-file (concat (if buffer-file-name (file-name-nondirectory buffer-file-name) (buffer-name)) "-") t)) (add-hook 'kill-buffer-hook 'pdf-util-delete-dedicated-directory nil t))) pdf-util--dedicated-directory)) (defun pdf-util-delete-dedicated-directory () "Delete current buffer's dedicated directory." (delete-directory (pdf-util-dedicated-directory) t)) (defun pdf-util-expand-file-name (name) "Expand filename against current buffer's dedicated directory." (expand-file-name name (pdf-util-dedicated-directory))) (defun pdf-util-make-temp-file (prefix &optional dir-flag suffix) "Create a temporary file in current buffer's dedicated directory. See `make-temp-file' for the arguments." (let ((temporary-file-directory (pdf-util-dedicated-directory))) (make-temp-file prefix dir-flag suffix))) ;; * ================================================================== * ;; * Various ;; * ================================================================== * (defmacro pdf-util-debug (&rest body) "Execute BODY only if debugging is enabled." (declare (indent 0) (debug t)) `(when (bound-and-true-p pdf-tools-debug) ,@body)) (defun pdf-util-pdf-buffer-p (&optional buffer) (and (or (null buffer) (buffer-live-p buffer)) (save-current-buffer (and buffer (set-buffer buffer)) (derived-mode-p 'pdf-view-mode)))) (defun pdf-util-assert-pdf-buffer (&optional buffer) (unless (pdf-util-pdf-buffer-p buffer) (error "Buffer is not in PDFView mode"))) (defun pdf-util-pdf-window-p (&optional window) (unless (or (null window) (window-live-p window)) (signal 'wrong-type-argument (list 'window-live-p window))) (unless window (setq window (selected-window))) (and (window-live-p window) (with-selected-window window (pdf-util-pdf-buffer-p)))) (defun pdf-util-assert-pdf-window (&optional window) (unless (pdf-util-pdf-window-p window) (error "Window's buffer is not in PdfView mode"))) (defun pdf-util-munch-file (filename &optional multibyte-p) "Read contents from FILENAME and delete it. Return the file's content as a unibyte string, unless MULTIBYTE-P is non-nil." (unwind-protect (with-temp-buffer (set-buffer-multibyte multibyte-p) (insert-file-contents-literally filename) (buffer-substring-no-properties (point-min) (point-max))) (when (and filename (file-exists-p filename)) (delete-file filename)))) (defun pdf-util-hexcolor (color) "Return COLOR in hex-format. Singal an error, if color is invalid." (if (string-match "\\`#[[:xdigit:]]\\{6\\}\\'" color) color (let ((values (color-values color))) (unless values (signal 'wrong-type-argument (list 'color-defined-p color))) (apply 'format "#%02x%02x%02x" (mapcar (lambda (c) (lsh c -8)) values))))) (defun pdf-util-highlight-regexp-in-string (regexp string &optional face) "Highlight all occurrences of REGEXP in STRING using FACE. FACE defaults to the `match' face. Returns the new fontified string." (with-temp-buffer (save-excursion (insert string)) (while (and (not (eobp)) (re-search-forward regexp nil t)) (if (= (match-beginning 0) (match-end 0)) (forward-char) (put-text-property (match-beginning 0) (point) 'face (or face 'match)))) (buffer-string))) (defun pdf-util-color-completions () "Return a fontified list of defined colors." (let ((color-list (list-colors-duplicates)) colors) (dolist (cl color-list) (dolist (c (reverse cl)) (push (propertize c 'face `(:background ,c)) colors))) (nreverse colors))) (defun pdf-util-tooltip-in-window (text x y &optional window) (let* ((we (window-inside-absolute-pixel-edges window)) (dx (round (+ x (nth 0 we)))) (dy (round (+ y (nth 1 we)))) (tooltip-frame-parameters `((left . ,dx) (top . ,dy) ,@tooltip-frame-parameters))) (tooltip-show text))) (defun pdf-util-tooltip-arrow (image-top &optional timeout) (pdf-util-assert-pdf-window) (when (floatp image-top) (setq image-top (round (* image-top (cdr (pdf-view-image-size)))))) (let* (x-gtk-use-system-tooltips ;allow for display property in tooltip (dx (+ (or (car (window-margins)) 0) (car (window-fringes)))) (dy image-top) (pos (list dx dy dx (+ dy (* 2 (frame-char-height))))) (vscroll (pdf-util-required-vscroll pos)) (tooltip-frame-parameters `((border-width . 0) (internal-border-width . 0) ,@tooltip-frame-parameters)) (tooltip-hide-delay (or timeout 3))) (when vscroll (image-set-window-vscroll vscroll)) (setq dy (max 0 (- dy (cdr (pdf-view-image-offset)) (window-vscroll nil t) (frame-char-height)))) (when (overlay-get (pdf-view-current-overlay) 'before-string) (let* ((e (window-inside-pixel-edges)) (xw (pdf-util-with-edges (e) e-width))) (cl-incf dx (/ (- xw (car (pdf-view-image-size t))) 2)))) (pdf-util-tooltip-in-window (propertize " " 'display (propertize "\u2192" ;;right arrow 'display '(height 2) 'face `(:foreground "orange red" :background ,(if (bound-and-true-p pdf-view-midnight-minor-mode) (cdr pdf-view-midnight-colors) "white")))) dx dy))) (defvar pdf-util--face-colors-cache (make-hash-table)) (defadvice enable-theme (after pdf-util-clear-faces-cache activate) (clrhash pdf-util--face-colors-cache)) (defun pdf-util-face-colors (face &optional dark-p) "Return both colors of FACE as a cons. Look also in inherited faces. If DARK-P is non-nil, return dark colors, otherwise light." (let* ((bg (if dark-p 'dark 'light)) (spec (list (get face 'face-defface-spec) (get face 'theme-face) (get face 'customized-face))) (cached (gethash face pdf-util--face-colors-cache))) (cl-destructuring-bind (&optional cspec color-alist) cached (or (and color-alist (equal cspec spec) (cdr (assq bg color-alist))) (let* ((this-bg (frame-parameter nil 'background-mode)) (frame-background-mode bg) (f (and (not (eq bg this-bg)) (x-create-frame-with-faces '((visibility . nil)))))) (with-selected-frame (or f (selected-frame)) (unwind-protect (let ((colors (cons (face-attribute face :foreground nil 'default) (face-attribute face :background nil 'default)))) (puthash face `(,(mapcar 'copy-sequence spec) ((,bg . ,colors) ,@color-alist)) pdf-util--face-colors-cache) colors) (when (and f (frame-live-p f)) (delete-frame f))))))))) (defun pdf-util-window-attach (awindow &optional window) "Attach AWINDOW to WINDOW. This has the following effect. Whenever WINDOW, defaulting to the selected window, stops displaying the buffer it currently displays (e.g., by switching buffers or because it was deleted) AWINDOW is deleted." (unless window (setq window (selected-window))) (let ((buffer (window-buffer window)) (hook (make-symbol "window-attach-hook"))) (fset hook (lambda () (when (or (not (window-live-p window)) (not (eq buffer (window-buffer window)))) (remove-hook 'window-configuration-change-hook hook) ;; Deleting windows inside wcch may cause errors in ;; windows.el . (run-with-timer 0 nil (lambda (win) (when (and (window-live-p win) (not (eq win (selected-window)))) (delete-window win))) awindow)))) (add-hook 'window-configuration-change-hook hook))) (defun display-buffer-split-below-and-attach (buf alist) "Display buffer action using `pdf-util-window-attach'." (let ((window (selected-window)) (height (cdr (assq 'window-height alist))) newwin) (when height (when (floatp height) (setq height (round (* height (frame-height))))) (setq height (- (max height window-min-height)))) (setq newwin (window--display-buffer buf (split-window-below height) 'window alist display-buffer-mark-dedicated)) (pdf-util-window-attach newwin window) newwin)) (defun pdf-util-goto-position (line &optional column) "Goto LINE and COLUMN in the current buffer. COLUMN defaults to 0. Widen the buffer, if the position is outside the current limits." (let ((pos (when (> line 0) (save-excursion (save-restriction (widen) (goto-char 1) (when (= 0 (forward-line (1- line))) (when (and column (> column 0)) (forward-char (1- column))) (point))))))) (when pos (when (or (< pos (point-min)) (> pos (point-max))) (widen)) (goto-char pos)))) (defun pdf-util-seq-alignment (seq1 seq2 &optional similarity-fn alignment-type) "Return an alignment of sequences SEQ1 and SEQ2. SIMILARITY-FN should be a function. It is called with two arguments: One element from SEQ1 and one from SEQ2. It should return a number determining how similar the elements are, where higher values mean `more similar'. The default returns 1 if the elements are equal, else -1. ALIGNMENT-TYPE may be one of the symbols `prefix', `suffix', `infix' or nil. If it is `prefix', trailing elements in SEQ2 may be ignored. For example the alignment of \(0 1\) and \(0 1 2\) using prefix matching is 0, since the prefixes are equal and the trailing 2 is ignored. The other possible values have similar effects. The default is nil, which means to match the whole sequences. Return a cons \(VALUE . ALINGMENT\), where VALUE says how similar the sequences are and ALINGMENT is a list of \(E1 . E2\), where E1 is an element from SEQ1 or nil, likewise for E2. If one of them is nil, it means there is gap at this position in the respective sequence." (cl-macrolet ((make-matrix (rows columns) (list 'apply (list 'quote 'vector) (list 'cl-loop 'for 'i 'from 1 'to rows 'collect (list 'make-vector columns nil)))) (mset (matrix row column newelt) (list 'aset (list 'aref matrix row) column newelt)) (mref (matrix row column) (list 'aref (list 'aref matrix row) column))) (let* ((nil-value nil) (len1 (length seq1)) (len2 (length seq2)) (d (make-matrix (1+ len1) (1+ len2))) (prefix-p (memq alignment-type '(prefix infix))) (suffix-p (memq alignment-type '(suffix infix))) (similarity-fn (or similarity-fn (lambda (a b) (if (equal a b) 1 -1))))) (cl-loop for i from 0 to len1 do (mset d i 0 (- i))) (cl-loop for j from 0 to len2 do (mset d 0 j (if suffix-p 0 (- j)))) (cl-loop for i from 1 to len1 do (cl-loop for j from 1 to len2 do (let ((max (max (1- (mref d (1- i) j)) (+ (mref d i (1- j)) (if (and prefix-p (= i len1)) 0 -1)) (+ (mref d (1- i) (1- j)) (funcall similarity-fn (elt seq1 (1- i)) (elt seq2 (1- j))))))) (mset d i j max)))) (let ((i len1) (j len2) alignment) (while (or (> i 0) (> j 0)) (cond ((and (> i 0) (= (mref d i j) (1- (mref d (1- i) j)))) (cl-decf i) (push (cons (elt seq1 i) nil-value) alignment)) ((and (> j 0) (= (mref d i j) (+ (mref d i (1- j)) (if (or (and (= i 0) suffix-p) (and (= i len1) prefix-p)) 0 -1)))) (cl-decf j) (push (cons nil-value (elt seq2 j)) alignment)) (t (cl-assert (and (> i 0) (> j 0)) t) (cl-decf i) (cl-decf j) (push (cons (elt seq1 i) (elt seq2 j)) alignment)))) (cons (mref d len1 len2) alignment))))) (defun pdf-util-pcre-quote (string) "Escape STRING for use as a PCRE. See also `regexp-quote'." (let ((to-escape (eval-when-compile (append "\0\\|()[]{}^$*+?." nil))) (chars (append string nil)) escaped) (dolist (ch chars) (when (memq ch to-escape) (push ?\\ escaped)) (push ch escaped)) (apply 'string (nreverse escaped)))) ;; * ================================================================== * ;; * Imagemagick's convert ;; * ================================================================== * (defcustom pdf-util-convert-program (executable-find "convert") "Absolute path to the convert program." :group 'pdf-tools :type 'executable) (defcustom pdf-util-fast-image-format nil "An image format appropriate for fast displaying. This should be a cons \(TYPE . EXT\) where type is the Emacs image-type and EXT the appropriate file extension starting with a dot. If nil, the value is determined automatically. Different formats have different properties, with respect to Emacs loading time, convert creation time and the file-size. In general, uncompressed formats are faster, but may need a fair amount of (temporary) disk space." :group 'pdf-tools) (defun pdf-util-assert-convert-program () (unless (and pdf-util-convert-program (file-executable-p pdf-util-convert-program)) (error "The pdf-util-convert-program is unset or non-executable"))) (defun pdf-util-image-file-size (image-file) "Determine the size of the image in IMAGE-FILE. Returns a cons \(WIDTH . HEIGHT\)." (pdf-util-assert-convert-program) (with-temp-buffer (when (save-excursion (= 0 (call-process pdf-util-convert-program nil (current-buffer) nil image-file "-format" "%w %h" "info:"))) (let ((standard-input (current-buffer))) (cons (read) (read)))))) (defun pdf-util-convert (in-file out-file &rest spec) "Convert image IN-FILE to OUT-FILE according to SPEC. IN-FILE should be the name of a file containing an image. Write the result to OUT-FILE. The extension of this filename ususally determines the resulting image-type. SPEC is a property list, specifying what the convert programm should do with the image. All manipulations operate on a rectangle, see below. SPEC may contain the following keys, respectively values. `:foreground' Set foreground color for all following operations. `:background' Dito, for the background color. `:commands' A list of strings representing arguments to convert for image manipulations. It may contain %-escape characters, as follows. %f -- Expands to the foreground color. %b -- Expands to the background color. %g -- Expands to the geometry of the current rectangle, i.e. WxH+X+Y. %x -- Expands to the left edge of rectangle. %X -- Expands to the right edge of rectangle. %y -- Expands to the top edge of rectangle. %Y -- Expands to the bottom edge of rectangle. %w -- Expands to the width of rectangle. %h -- Expands to the height of rectangle. Keep in mind, that every element of this list is seen by convert as a single argument. `:formats' An alist of additional %-escapes. Every element should be a cons \(CHAR . STRING\) or \(CHAR . FUNCTION\). In the first case, all occurences of %-CHAR in the above commands will be replaced by STRING. In the second case FUNCTION is called with the current rectangle and it should return the replacement string. `:apply' A list of rectangles \(\(LEFT TOP RIGHT BOT\) ...\) in IN-FILE coordinates. Each such rectangle triggers one execution of the last commands given earlier in SPEC. E.g. a call like \(pdf-util-convert image-file out-file :foreground \"black\" :background \"white\" :commands '\(\"-fill\" \"%f\" \"-draw\" \"rectangle %x,%y,%X,%Y\"\) :apply '\(\(0 0 10 10\) \(10 10 20 20\)\) :commands '\(\"-fill\" \"%b\" \"-draw\" \"rectangle %x,%y,%X,%Y\"\) :apply '\(\(10 0 20 10\) \(0 10 10 20\)\)\) would draw a 4x4 checkerboard pattern in the left corner of the image, while leaving the rest of it as it was. Returns OUT-FILE. See url `http://www.imagemagick.org/script/convert.php'." (pdf-util-assert-convert-program) (let* ((cmds (pdf-util-convert--create-commands spec)) (status (apply 'call-process pdf-util-convert-program nil (get-buffer-create "*pdf-util-convert-output*") nil `(,in-file ,@cmds ,out-file)))) (unless (and (numberp status) (= 0 status)) (error "The convert program exited with error status: %s" status)) out-file)) (defun pdf-util-convert-asynch (in-file out-file &rest spec-and-callback) "Like `pdf-util-convert', but asynchronous. If the last argument is a function, it is installed as the process sentinel. Returns the convert process." (pdf-util-assert-convert-program) (let ((callback (car (last spec-and-callback))) spec) (if (functionp callback) (setq spec (butlast spec-and-callback)) (setq spec spec-and-callback callback nil)) (let* ((cmds (pdf-util-convert--create-commands spec)) (proc (apply 'start-process "pdf-util-convert" (get-buffer-create "*pdf-util-convert-output*") pdf-util-convert-program `(,in-file ,@cmds ,out-file)))) (when callback (set-process-sentinel proc callback)) proc))) (defun pdf-util-convert-page (&rest specs) "Convert image of current page according to SPECS. Return the converted PNG image as a string. See also `pdf-util-convert'." (pdf-util-assert-pdf-window) (let ((in-file (make-temp-file "pdf-util-convert" nil ".png")) (out-file (make-temp-file "pdf-util-convert" nil ".png"))) (unwind-protect (let ((image-data (plist-get (cdr (pdf-view-current-image)) :data))) (with-temp-file in-file (set-buffer-multibyte nil) (insert image-data)) (pdf-util-munch-file (apply 'pdf-util-convert in-file out-file specs))) (when (file-exists-p in-file) (delete-file in-file)) (when (file-exists-p out-file) (delete-file out-file))))) (defun pdf-util-convert--create-commands (spec) (let ((fg "red") (bg "red") formats result cmds s) (while (setq s (pop spec)) (unless spec (error "Missing value in convert spec:%s" (cons s spec))) (cl-case s (:foreground (setq fg (pop spec))) (:background (setq bg (pop spec))) (:commands (setq cmds (pop spec))) (:formats (setq formats (append formats (pop spec) nil))) (:apply (dolist (m (pop spec)) (pdf-util-with-edges (m) (let ((alist (append (mapcar (lambda (f) (cons (car f) (if (stringp (cdr f)) (cdr f) (funcall (cdr f) m)))) formats) `((?g . ,(format "%dx%d+%d+%d" m-width m-height m-left m-top)) (?x . ,m-left) (?X . ,m-right) (?y . ,m-top) (?Y . ,m-bot) (?w . ,(- m-right m-left)) (?h . ,(- m-bot m-top)) (?f . ,fg) (?b . ,bg))))) (dolist (fmt cmds) (push (format-spec fmt alist) result)))))))) (nreverse result))) ;; FIXME: Check code below and document. (defun pdf-util-edges-p (obj &optional relative-p) "Return non-nil, if OBJ look like edges. If RELATIVE-P is non-nil, also check that all values <= 1." (and (consp obj) (ignore-errors (= 4 (length obj))) (cl-every (lambda (x) (and (numberp x) (>= x 0) (or (null relative-p) (<= x 1)))) obj))) (defun pdf-util-edges-empty-p (edges) "Return non-nil, if EDGES area is empty." (pdf-util-with-edges (edges) (or (<= edges-width 0) (<= edges-height 0)))) (defun pdf-util-edges-inside-p (edges pos &optional epsilon) (pdf-util-edges-contained-p edges (list (car pos) (cdr pos) (car pos) (cdr pos)) epsilon)) (defun pdf-util-edges-contained-p (edges contained &optional epsilon) (unless epsilon (setq epsilon 0)) (pdf-util-with-edges (edges contained) (and (<= (- edges-left epsilon) contained-left) (>= (+ edges-right epsilon) contained-right) (<= (- edges-top epsilon) contained-top) (>= (+ edges-bot epsilon) contained-bot)))) (defun pdf-util-edges-intersection (e1 e2) (pdf-util-with-edges (edges1 e1 e2) (let ((left (max e1-left e2-left)) (top (max e1-top e2-top)) (right (min e1-right e2-right)) (bot (min e1-bot e2-bot))) (when (and (<= left right) (<= top bot)) (list left top right bot))))) (defun pdf-util-edges-union (&rest edges) (if (null (cdr edges)) (car edges) (list (apply 'min (mapcar 'car edges)) (apply 'min (mapcar 'cadr edges)) (apply 'max (mapcar 'cl-caddr edges)) (apply 'max (mapcar 'cl-cadddr edges))))) (defun pdf-util-edges-intersection-area (e1 e2) (let ((inters (pdf-util-edges-intersection e1 e2))) (if (null inters) 0 (pdf-util-with-edges (inters) (* inters-width inters-height))))) (defun pdf-util-read-image-position (prompt) "Read a image position using prompt. Return the event position object." (save-selected-window (let ((ev (pdf-util-read-click-event (propertize prompt 'face 'minibuffer-prompt))) (buffer (current-buffer))) (unless (mouse-event-p ev) (error "Not a mouse event")) (let ((posn (event-start ev))) (unless (and (eq (window-buffer (posn-window posn)) buffer) (eq 'image (car-safe (posn-object posn)))) (error "Invalid image position")) posn)))) (defun pdf-util-read-click-event (&optional prompt seconds) (let ((down (read-event prompt seconds))) (unless (and (mouse-event-p down) (equal (event-modifiers down) '(down))) (error "No a mouse click event")) (let ((up (read-event prompt seconds))) (unless (and (mouse-event-p up) (equal (event-modifiers up) '(click))) (error "No a mouse click event")) up))) (defun pdf-util-image-map-mouse-event-proxy (event) "Set POS-OR-AREA in EVENT to 1 and unread it." (interactive "e") (setcar (cdr (cadr event)) 1) (setq unread-command-events (list event))) (defun pdf-util-image-map-divert-mouse-clicks (id &optional buttons) (dolist (kind '("" "down-" "drag-")) (dolist (b (or buttons '(2 3 4 5 6))) (local-set-key (vector id (intern (format "%smouse-%d" kind b))) 'pdf-util-image-map-mouse-event-proxy)))) (defmacro pdf-util-do-events (event-resolution-unread-p condition &rest body) "Read EVENTs while CONDITION executing BODY. Process at most 1/RESOLUTION events per second. If UNREAD-p is non-nil, unread the final non-processed event. \(FN (EVENT RESOLUTION &optional UNREAD-p) CONDITION &rest BODY\)" (declare (indent 2) (debug ((symbolp form &optional form) form body))) (cl-destructuring-bind (event resolution &optional unread-p) event-resolution-unread-p (let ((*seconds (make-symbol "seconds")) (*timestamp (make-symbol "timestamp")) (*clock (make-symbol "clock")) (*unread-p (make-symbol "unread-p")) (*resolution (make-symbol "resolution"))) `(let* ((,*unread-p ,unread-p) (,*resolution ,resolution) (,*seconds 0) (,*timestamp (float-time)) (,*clock (lambda (&optional secs) (when secs (setq ,*seconds secs ,*timestamp (float-time))) (- (+ ,*timestamp ,*seconds) (float-time)))) (,event (read-event))) (while ,condition (when (<= (funcall ,*clock) 0) (progn ,@body) (setq ,event nil) (funcall ,*clock ,*resolution)) (setq ,event (or (read-event nil nil (and ,event (max 0 (funcall ,*clock)))) ,event))) (when (and ,*unread-p ,event) (setq unread-command-events (append unread-command-events (list ,event)))))))) (defmacro pdf-util-track-mouse-dragging (event-resolution &rest body) "Read mouse movement events executing BODY. See also `pdf-util-do-events'. This macro should be used inside a command bound to a down-mouse event. It evaluates to t, if at least one event was processed in BODY, otherwise nil. In the latter case, the only event (usually a mouse click event) is unread. \(FN (EVENT RESOLUTION) &rest BODY\)" (declare (indent 1) (debug ((symbolp form) body))) (let ((ran-once-p (make-symbol "ran-once-p"))) `(let (,ran-once-p) (track-mouse (pdf-util-do-events (,@event-resolution t) (mouse-movement-p ,(car event-resolution)) (setq ,ran-once-p t) ,@body)) (when (and ,ran-once-p unread-command-events) (setq unread-command-events (butlast unread-command-events))) ,ran-once-p))) (defun pdf-util-remove-duplicates (list) "Remove duplicates from LIST stably using `equal'." (let ((ht (make-hash-table :test 'equal)) result) (dolist (elt list (nreverse result)) (unless (gethash elt ht) (push elt result) (puthash elt t ht))))) (provide 'pdf-util) ;;; pdf-util.el ends here pdf-tools-0.80/lisp/pdf-view.el000066400000000000000000001664161315513274600164070ustar00rootroot00000000000000;;; pdf-view.el --- View PDF documents. -*- lexical-binding:t -*- ;; Copyright (C) 2013 Andreas Politz ;; Author: Andreas Politz ;; Keywords: files, doc-view, pdf ;; 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 . ;;; Commentary: ;; Functions related to viewing PDF documents. ;;; Code: (require 'image-mode) (require 'pdf-util) (require 'pdf-info) (require 'pdf-cache) (require 'jka-compr) (require 'bookmark) (require 'password-cache) ;; * ================================================================== * ;; * Customizations ;; * ================================================================== * (defgroup pdf-view nil "View PDF documents." :group 'pdf-tools) (defcustom pdf-view-display-size 'fit-width "The desired size of displayed pages. This may be one of `fit-height', `fit-width', `fit-page' or a number as a scale factor applied to the document's size. Any other value behaves like `fit-width'." :group 'pdf-view :type '(choice number (const fit-height) (const fit-width) (const fit-page))) (make-variable-buffer-local 'pdf-view-display-size) (defcustom pdf-view-resize-factor 1.25 "Fractional amount of resizing of one resize command." :group 'pdf-view :type 'number) (defcustom pdf-view-continuous t "In Continuous mode reaching the page edge advances to next/previous page. When non-nil, scrolling a line upward at the bottom edge of the page moves to the next page, and scrolling a line downward at the top edge of the page moves to the previous page." :type 'boolean :group 'pdf-view) (defcustom pdf-view-bounding-box-margin 0.05 "Fractional margin used for slicing with the bounding-box." :group 'pdf-view :type 'number) (defcustom pdf-view-use-imagemagick nil "Whether imagemagick should be used for rendering. This variable has no effect, if imagemagick was not compiled into Emacs or if imagemagick is the only way to display PNG images. FIXME: Explain dis-/advantages of imagemagick and png." :group 'pdf-view :type 'boolean) (defcustom pdf-view-use-scaling nil "Whether images should be allowed to be scaled down for rendering. This variable has no effect, if imagemagick was not compiled into Emacs or `pdf-view-use-imagemagick' is nil. FIXME: Explain dis-/advantages of imagemagick and png." :group 'pdf-view :type 'boolean) (defface pdf-view-region '((((background dark)) (:inherit region)) (((background light)) (:inherit region))) "Face used to determine the colors of the region." :group 'pdf-view :group 'pdf-tools-faces) (defface pdf-view-rectangle '((((background dark)) (:inherit highlight)) (((background light)) (:inherit highlight))) "Face used to determine the colors of the highlighted rectangle." :group 'pdf-view :group 'pdf-tools-faces) (defcustom pdf-view-midnight-colors '("#839496" . "#002b36" ) "Colors used when `pdf-view-midnight-minor-mode' is activated. This should be a cons \(FOREGROUND . BACKGROUND\) of colors." :group 'pdf-view :type '(cons (color :tag "Foreground") (color :tag "Background"))) (defcustom pdf-view-change-page-hook nil "Hook run after changing to another page, but before displaying it. See also `pdf-view-before-change-page-hook' and `pdf-view-after-change-page-hook'." :group 'pdf-view :type 'hook) (defcustom pdf-view-before-change-page-hook nil "Hook run before changing to another page. See also `pdf-view-change-page-hook' and `pdf-view-after-change-page-hook'." :group 'pdf-view :type 'hook) (defcustom pdf-view-after-change-page-hook nil "Hook run after changing to and displaying another page. See also `pdf-view-change-page-hook' and `pdf-view-before-change-page-hook'." :group 'pdf-view :type 'hook) (defcustom pdf-view-use-dedicated-register t "Whether to use dedicated register for PDF positions. If this is non-nil, the commands `pdf-view-position-to-register' and `pdf-view-jump-to-register' use the buffer-local variable `pdf-view-register-alist' to store resp. retrieve marked positions. Otherwise the common variable `register-alist' is used." :group 'pdf-view :type 'boolean) (defcustom pdf-view-image-relief 0 "Add a shadow rectangle around the page's image. See :relief property in Info node `(elisp) Image Descriptors'." :group 'pdf-view :type '(integer :tag "Pixel") :link '(info-link "(elisp) Image Descriptors")) (defcustom pdf-view-max-image-width 4800 "Maximum width of any image displayed in pixel." :group 'pdf-view :type '(integer :tag "Pixel")) (defcustom pdf-view-use-unicode-ligther t "Whether to use unicode symbols in the mode-line On some systems finding a font which supports those symbols can take some time. If you don't want to spend that time waiting and don't care for a nicer looking mode-line, set this variable to nil. Note, that this option has only an effect when this library is loaded." :group 'pdf-view :type 'boolean) (defcustom pdf-view-incompatible-modes '(linum-mode linum-relative-mode helm-linum-relative-mode nlinum-mode nlinum-hl-mode nlinum-relative-mode yalinum-mode) "A list of modes incompatible with `pdf-view-mode'. Issue a warning, if one of them is active in a PDF buffer." :group 'pdf-view :type '(repeat symbol)) ;; * ================================================================== * ;; * Internal variables and macros ;; * ================================================================== * (defvar-local pdf-view-active-region nil "The active region as a list of edges. Edge values are relative coordinates.") (defvar-local pdf-view--have-rectangle-region nil "Non-nil if the region is currently rendered as a rectangle. This variable is set in `pdf-view-mouse-set-region' and used in `pdf-view-mouse-extend-region' to determine the right choice regarding display of the region in the later function.") (defvar-local pdf-view--buffer-file-name nil "Local copy of remote file or nil.") (defvar-local pdf-view--server-file-name nil "The servers notion of this buffer's filename.") (defvar-local pdf-view--next-page-timer nil "Timer used in `pdf-view-next-page-command'.") (defvar-local pdf-view--hotspot-functions nil "Alist of hotspot functions.") (defvar-local pdf-view-register-alist nil "Local, dedicated register for PDF positions.") (defmacro pdf-view-current-page (&optional window) ;;TODO: write documentation! `(image-mode-window-get 'page ,window)) (defmacro pdf-view-current-overlay (&optional window) ;;TODO: write documentation! `(image-mode-window-get 'overlay ,window)) (defmacro pdf-view-current-image (&optional window) ;;TODO: write documentation! `(image-mode-window-get 'image ,window)) (defmacro pdf-view-current-slice (&optional window) ;;TODO: write documentation! `(image-mode-window-get 'slice ,window)) (defmacro pdf-view-current-window-size (&optional window) ;;TODO: write documentation! `(image-mode-window-get 'window-size ,window)) (defun pdf-view-active-region-p nil "Return t if there are active regions." (not (null pdf-view-active-region))) (defmacro pdf-view-assert-active-region () "Signal an error if there are no active regions." `(unless (pdf-view-active-region-p) (error "The region is not active"))) ;; * ================================================================== * ;; * Major Mode ;; * ================================================================== * (defvar pdf-view-mode-map (let ((map (make-sparse-keymap))) (set-keymap-parent map image-mode-map) (define-key map (kbd "Q") 'kill-this-buffer) ;; Navigation in the document (define-key map (kbd "n") 'pdf-view-next-page-command) (define-key map (kbd "p") 'pdf-view-previous-page-command) (define-key map (kbd "") 'forward-page) (define-key map (kbd "") 'backward-page) (define-key map [remap forward-page] 'pdf-view-next-page-command) (define-key map [remap backward-page] 'pdf-view-previous-page-command) (define-key map (kbd "SPC") 'pdf-view-scroll-up-or-next-page) (define-key map (kbd "S-SPC") 'pdf-view-scroll-down-or-previous-page) (define-key map (kbd "DEL") 'pdf-view-scroll-down-or-previous-page) (define-key map (kbd "C-n") 'pdf-view-next-line-or-next-page) (define-key map (kbd "") 'pdf-view-next-line-or-next-page) (define-key map (kbd "C-p") 'pdf-view-previous-line-or-previous-page) (define-key map (kbd "") 'pdf-view-previous-line-or-previous-page) (define-key map (kbd "M-<") 'pdf-view-first-page) (define-key map (kbd "M->") 'pdf-view-last-page) (define-key map [remap goto-line] 'pdf-view-goto-page) (define-key map (kbd "M-g l") 'pdf-view-goto-label) (define-key map (kbd "RET") 'image-next-line) ;; Zoom in/out. (define-key map "+" 'pdf-view-enlarge) (define-key map "=" 'pdf-view-enlarge) (define-key map "-" 'pdf-view-shrink) (define-key map "0" 'pdf-view-scale-reset) ;; Fit the image to the window (define-key map "W" 'pdf-view-fit-width-to-window) (define-key map "H" 'pdf-view-fit-height-to-window) (define-key map "P" 'pdf-view-fit-page-to-window) ;; Slicing the image (define-key map (kbd "s m") 'pdf-view-set-slice-using-mouse) (define-key map (kbd "s b") 'pdf-view-set-slice-from-bounding-box) (define-key map (kbd "s r") 'pdf-view-reset-slice) ;; Reconvert (define-key map (kbd "C-c C-c") 'doc-view-mode) (define-key map (kbd "g") 'revert-buffer) (define-key map (kbd "r") 'revert-buffer) ;; Region (define-key map [down-mouse-1] 'pdf-view-mouse-set-region) (define-key map [M-down-mouse-1] 'pdf-view-mouse-set-region-rectangle) (define-key map [C-down-mouse-1] 'pdf-view-mouse-extend-region) (define-key map [remap kill-region] 'pdf-view-kill-ring-save) (define-key map [remap kill-ring-save] 'pdf-view-kill-ring-save) (define-key map [remap mark-whole-buffer] 'pdf-view-mark-whole-page) ;; Other (define-key map (kbd "C-c C-d") 'pdf-view-dark-minor-mode) (define-key map (kbd "m") 'pdf-view-position-to-register) (define-key map (kbd "'") 'pdf-view-jump-to-register) (define-key map (kbd "C-c C-i") 'pdf-view-extract-region-image) ;; Rendering (define-key map (kbd "C-c C-r m") 'pdf-view-midnight-minor-mode) (define-key map (kbd "C-c C-r p") 'pdf-view-printer-minor-mode) map) "Keymap used by `pdf-view-mode' when displaying a doc as a set of images.") (define-derived-mode pdf-view-mode special-mode "PDFView" "Major mode in PDF buffers. PDFView Mode is an Emacs PDF viewer. It displays PDF files as PNG images in Emacs buffers." :group 'pdf-view :abbrev-table nil :syntax-table nil ;; Setup a local copy for remote files. (when (and (or jka-compr-really-do-compress (let ((file-name-handler-alist nil)) (not (and buffer-file-name (file-readable-p buffer-file-name))))) (pdf-tools-pdf-buffer-p)) (let ((tempfile (pdf-util-make-temp-file (concat (if buffer-file-name (file-name-nondirectory buffer-file-name) (buffer-name)) "-")))) (write-region nil nil tempfile nil 'no-message) (setq-local pdf-view--buffer-file-name tempfile))) ;; Setup scroll functions (if (boundp 'mwheel-scroll-up-function) ; not --without-x build (setq-local mwheel-scroll-up-function #'pdf-view-scroll-up-or-next-page)) (if (boundp 'mwheel-scroll-down-function) (setq-local mwheel-scroll-down-function #'pdf-view-scroll-down-or-previous-page)) ;; Clearing overlays (add-hook 'change-major-mode-hook (lambda () (remove-overlays (point-min) (point-max) 'pdf-view t)) nil t) (remove-overlays (point-min) (point-max) 'pdf-view t) ;Just in case. ;; Setup other local variables. (setq-local mode-line-position '(" P" (:eval (number-to-string (pdf-view-current-page))) "/" (:eval (number-to-string (pdf-cache-number-of-pages))))) (setq-local auto-hscroll-mode nil) (setq-local pdf-view--server-file-name (pdf-view-buffer-file-name)) ;; High values of scroll-conservatively seem to trigger ;; some display bug in xdisp.c:try_scrolling . (setq-local scroll-conservatively 0) (setq-local cursor-type nil) (setq-local buffer-read-only t) (setq-local view-read-only nil) (setq-local bookmark-make-record-function 'pdf-view-bookmark-make-record) (setq-local revert-buffer-function #'pdf-view-revert-buffer) ;; No auto-save at the moment. (setq-local buffer-auto-save-file-name nil) ;; No undo at the moment. (unless buffer-undo-list (buffer-disable-undo)) ;; Enable transient-mark-mode, so region deactivation when quitting ;; will work. (setq-local transient-mark-mode t) (add-hook 'window-configuration-change-hook 'pdf-view-maybe-redisplay-resized-windows nil t) (add-hook 'deactivate-mark-hook 'pdf-view-deactivate-region nil t) (add-hook 'write-contents-functions 'pdf-view--write-contents-function nil t) (add-hook 'kill-buffer-hook 'pdf-view-close-document nil t) (pdf-view-add-hotspot-function 'pdf-view-text-regions-hotspots-function -9) ;; Keep track of display info (add-hook 'image-mode-new-window-functions 'pdf-view-new-window-function nil t) (image-mode-setup-winprops) ;; Decryption needs to be done before any other function calls into ;; pdf-info.el . (pdf-view-decrypt-document) ;; Issue a warning in the future about incompatible modes. (run-with-timer 1 nil #'pdf-view-check-incompatible-modes (current-buffer)) ;; Setup initial page and start display (pdf-view-goto-page (or (pdf-view-current-page) 1))) (defun pdf-view-check-incompatible-modes (&optional buffer) "Check BUFFER for incompatible modes, maybe issue a warning." (with-current-buffer (or buffer (current-buffer)) (let ((modes (cl-remove-if-not (lambda (mode) (and (symbolp mode) (boundp mode) (symbol-value mode))) pdf-view-incompatible-modes))) (when modes (display-warning 'pdf-view (format "These modes are incompatible with `pdf-view-mode', please deactivate them (or customize pdf-view-incompatible-modes): %s" (mapconcat #'symbol-name modes ","))))))) (defun pdf-view-decrypt-document () "Read a password, if the document is encrypted and open it." (interactive) (when (pdf-info-encrypted-p) (let ((prompt (format "Enter password for `%s': " (abbreviate-file-name (buffer-file-name)))) (key (concat "/pdf-tools" (buffer-file-name))) (i 3) password) (while (and (> i 0) (pdf-info-encrypted-p)) (setq i (1- i)) (setq password (password-read prompt key)) (setq prompt "Invalid password, try again: ") (ignore-errors (pdf-info-open nil password))) (pdf-info-open nil password) (password-cache-add key password))) nil) (defun pdf-view-buffer-file-name () "Return the local filename of the PDF in the current buffer. This may be different from variable `buffer-file-name' when operating on a local copy of a remote file." (or pdf-view--buffer-file-name (buffer-file-name))) (defun pdf-view--write-contents-function () "Function for `write-contents-functions' to save the buffer." (when (pdf-util-pdf-buffer-p) (let ((tempfile (pdf-info-save pdf-view--server-file-name))) (unwind-protect (progn ;; Order matters here: We need to first copy the new ;; content (tempfile) to the PDF, and then close the PDF. ;; Since while closing the file (and freeing its resources ;; in the process), it may be immediately reopened due to ;; redisplay happening inside the pdf-info-close function ;; (while waiting for a response from the process.). (copy-file tempfile (buffer-file-name) t) (pdf-info-close pdf-view--server-file-name) (when pdf-view--buffer-file-name (copy-file tempfile pdf-view--buffer-file-name t)) (clear-visited-file-modtime) (set-buffer-modified-p nil) (setq pdf-view--server-file-name (pdf-view-buffer-file-name)) t) (when (file-exists-p tempfile) (delete-file tempfile)))))) (defun pdf-view-revert-buffer (&optional ignore-auto noconfirm) "Revert buffer while preseving current modes. Optional parameters IGNORE-AUTO and NOCONFIRM are defined as in `revert-buffer'." (interactive (list (not current-prefix-arg))) ;; Bind to default so that we can use pdf-view-revert-buffer as ;; revert-buffer-function. A binding of nil is needed in Emacs 24.3, but in ;; later versions the semantics that nil means the default function should ;; not relied upon. (let ((revert-buffer-function (when (fboundp #'revert-buffer--default) #'revert-buffer--default)) (after-revert-hook (cons #'pdf-info-close after-revert-hook))) (prog1 (revert-buffer ignore-auto noconfirm 'preserve-modes) (pdf-view-redisplay t)))) (defun pdf-view-close-document () "Return immediately after closing document. See also `pdf-info-close', which does not return immediately." (when (pdf-info-running-p) (let ((pdf-info-asynchronous 'ignore)) (pdf-info-close)))) ;; * ================================================================== * ;; * Scaling ;; * ================================================================== * (defun pdf-view-fit-page-to-window () "Fit PDF to window. Choose the larger of PDF's height and width, and fits that dimension to window." (interactive) (setq pdf-view-display-size 'fit-page) (image-set-window-vscroll 0) (image-set-window-hscroll 0) (pdf-view-redisplay t)) (defun pdf-view-fit-height-to-window () "Fit PDF height to window." (interactive) (setq pdf-view-display-size 'fit-height) (image-set-window-vscroll 0) (pdf-view-redisplay t)) (defun pdf-view-fit-width-to-window () "Fit PDF size to window." (interactive) (setq pdf-view-display-size 'fit-width) (image-set-window-hscroll 0) (pdf-view-redisplay t)) (defun pdf-view-enlarge (factor) "Enlarge PDF by FACTOR. When called interactively, uses the value of `pdf-view-resize-factor'. For example, (pdf-view-enlarge 1.25) increases size by 25%." (interactive (list (float pdf-view-resize-factor))) (let* ((size (pdf-view-image-size)) (pagesize (pdf-cache-pagesize (pdf-view-current-page))) (scale (/ (float (car size)) (float (car pagesize))))) (setq pdf-view-display-size (* factor scale)) (pdf-view-redisplay t))) (defun pdf-view-shrink (factor) "Shrink PDF by FACTOR. When called interactively, uses the value of `pdf-view-resize-factor'. For example, (pdf-view-shrink 1.25) decreases size by 20%." (interactive (list (float pdf-view-resize-factor))) (pdf-view-enlarge (/ 1.0 factor))) (defun pdf-view-scale-reset () "Reset PDF to its default set." (interactive) (setq pdf-view-display-size 1.0) (pdf-view-redisplay t)) ;; * ================================================================== * ;; * Moving by pages and scrolling ;; * ================================================================== * (defun pdf-view-goto-page (page &optional window) "Go to PAGE in PDF. If optional parameter WINDOW, go to PAGE in all `pdf-view' windows." (interactive (list (if current-prefix-arg (prefix-numeric-value current-prefix-arg) (read-number "Page: ")))) (unless (and (>= page 1) (<= page (pdf-cache-number-of-pages))) (error "No such page: %d" page)) (unless window (setq window (if (pdf-util-pdf-window-p) (selected-window) t))) (save-selected-window ;; Select the window for the hooks below. (when (window-live-p window) (select-window window)) (let ((changing-p (not (eq page (pdf-view-current-page window))))) (when changing-p (run-hooks 'pdf-view-before-change-page-hook) (setf (pdf-view-current-page window) page) (run-hooks 'pdf-view-change-page-hook)) (when (window-live-p window) (pdf-view-redisplay window)) (when changing-p (pdf-view-deactivate-region) (force-mode-line-update) (run-hooks 'pdf-view-after-change-page-hook)))) nil) (defun pdf-view-next-page (&optional n) "View the next page in the PDF. Optional parameter N moves N pages forward." (interactive "p") (pdf-view-goto-page (+ (pdf-view-current-page) (or n 1)))) (defun pdf-view-previous-page (&optional n) "View the previous page in the PDF. Optional parameter N moves N pages backward." (interactive "p") (pdf-view-next-page (- (or n 1)))) (defun pdf-view-next-page-command (&optional n) "View the next page in the PDF. Optional parameter N moves N pages forward. This command is a wrapper for `pdf-view-next-page' that will indicate to the user if they are on the last page and more." (declare (interactive-only pdf-view-next-page)) (interactive "p") (unless n (setq n 1)) (when (> (+ (pdf-view-current-page) n) (pdf-cache-number-of-pages)) (user-error "Last page")) (when (< (+ (pdf-view-current-page) n) 1) (user-error "First page")) (let ((pdf-view-inhibit-redisplay t)) (pdf-view-goto-page (+ (pdf-view-current-page) n))) (force-mode-line-update) (sit-for 0) (when pdf-view--next-page-timer (cancel-timer pdf-view--next-page-timer) (setq pdf-view--next-page-timer nil)) (if (or (not (input-pending-p)) (and (> n 0) (= (pdf-view-current-page) (pdf-cache-number-of-pages))) (and (< n 0) (= (pdf-view-current-page) 1))) (pdf-view-redisplay) (setq pdf-view--next-page-timer (run-with-idle-timer 0.001 nil 'pdf-view-redisplay (selected-window))))) (defun pdf-view-previous-page-command (&optional n) "View the previous page in the PDF. Optional parameter N moves N pages backward. This command is a wrapper for `pdf-view-previous-page'." (declare (interactive-only pdf-view-previous-page)) (interactive "p") (with-no-warnings (pdf-view-next-page-command (- (or n 1))))) (defun pdf-view-first-page () "View the first page." (interactive) (pdf-view-goto-page 1)) (defun pdf-view-last-page () "View the last page." (interactive) (pdf-view-goto-page (pdf-cache-number-of-pages))) (defun pdf-view-scroll-up-or-next-page (&optional arg) "Scroll page up ARG lines if possible, else go to the next page. When `pdf-view-continuous' is non-nil, scrolling upward at the bottom edge of the page moves to the next page. Otherwise, go to next page only on typing SPC (ARG is nil)." (interactive "P") (if (or pdf-view-continuous (null arg)) (let ((hscroll (window-hscroll)) (cur-page (pdf-view-current-page))) (when (or (= (window-vscroll) (image-scroll-up arg)) ;; Workaround rounding/off-by-one issues. (memq pdf-view-display-size '(fit-height fit-page))) (pdf-view-next-page) (when (/= cur-page (pdf-view-current-page)) (image-bob) (image-bol 1)) (set-window-hscroll (selected-window) hscroll))) (image-scroll-up arg))) (defun pdf-view-scroll-down-or-previous-page (&optional arg) "Scroll page down ARG lines if possible, else go to the previous page. When `pdf-view-continuous' is non-nil, scrolling downward at the top edge of the page moves to the previous page. Otherwise, go to previous page only on typing DEL (ARG is nil)." (interactive "P") (if (or pdf-view-continuous (null arg)) (let ((hscroll (window-hscroll)) (cur-page (pdf-view-current-page))) (when (or (= (window-vscroll) (image-scroll-down arg)) ;; Workaround rounding/off-by-one issues. (memq pdf-view-display-size '(fit-height fit-page))) (pdf-view-previous-page) (when (/= cur-page (pdf-view-current-page)) (image-eob) (image-bol 1)) (set-window-hscroll (selected-window) hscroll))) (image-scroll-down arg))) (defun pdf-view-next-line-or-next-page (&optional arg) "Scroll upward by ARG lines if possible, else go to the next page. When `pdf-view-continuous' is non-nil, scrolling a line upward at the bottom edge of the page moves to the next page." (interactive "p") (if pdf-view-continuous (let ((hscroll (window-hscroll)) (cur-page (pdf-view-current-page))) (when (= (window-vscroll) (image-next-line arg)) (pdf-view-next-page) (when (/= cur-page (pdf-view-current-page)) (image-bob) (image-bol 1)) (set-window-hscroll (selected-window) hscroll))) (image-next-line 1))) (defun pdf-view-previous-line-or-previous-page (&optional arg) "Scroll downward by ARG lines if possible, else go to the previous page. When `pdf-view-continuous' is non-nil, scrolling a line downward at the top edge of the page moves to the previous page." (interactive "p") (if pdf-view-continuous (let ((hscroll (window-hscroll)) (cur-page (pdf-view-current-page))) (when (= (window-vscroll) (image-previous-line arg)) (pdf-view-previous-page) (when (/= cur-page (pdf-view-current-page)) (image-eob) (image-bol 1)) (set-window-hscroll (selected-window) hscroll))) (image-previous-line arg))) (defun pdf-view-goto-label (label) "Go to the page corresponding to LABEL. Usually, the label of a document's page is the same as its displayed page number." (interactive (list (let ((labels (pdf-info-pagelabels))) (completing-read "Goto label: " labels nil t)))) (let ((index (cl-position label (pdf-info-pagelabels) :test 'equal))) (unless index (error "No such label: %s" label)) (pdf-view-goto-page (1+ index)))) ;; * ================================================================== * ;; * Slicing ;; * ================================================================== * (defun pdf-view-set-slice (x y width height &optional window) ;; TODO: add WINDOW to docstring. "Set the slice of the pages that should be displayed. X, Y, WIDTH and HEIGHT should be relative coordinates, i.e. in \[0;1\]. To reset the slice use `pdf-view-reset-slice'." (unless (equal (pdf-view-current-slice window) (list x y width height)) (setf (pdf-view-current-slice window) (mapcar (lambda (v) (max 0 (min 1 v))) (list x y width height))) (pdf-view-redisplay window))) (defun pdf-view-set-slice-using-mouse () "Set the slice of the images that should be displayed. Set the slice by pressing `mouse-1' at its top-left corner and dragging it to its bottom-right corner. See also `pdf-view-set-slice' and `pdf-view-reset-slice'." (interactive) (let ((size (pdf-view-image-size)) x y w h done) (while (not done) (let ((e (read-event (concat "Press mouse-1 at the top-left corner and " "drag it to the bottom-right corner!")))) (when (eq (car e) 'drag-mouse-1) (setq x (car (posn-object-x-y (event-start e)))) (setq y (cdr (posn-object-x-y (event-start e)))) (setq w (- (car (posn-object-x-y (event-end e))) x)) (setq h (- (cdr (posn-object-x-y (event-end e))) y)) (setq done t)))) (apply 'pdf-view-set-slice (pdf-util-scale (list x y w h) (cons (/ 1.0 (float (car size))) (/ 1.0 (float (cdr size)))))))) (defun pdf-view-set-slice-from-bounding-box (&optional window) ;; TODO: add WINDOW to docstring. "Set the slice from the page's bounding-box. The result is that the margins are almost completely cropped, much more accurate than could be done manually using `pdf-view-set-slice-using-mouse'. See also `pdf-view-bounding-box-margin'." (interactive) (let* ((bb (pdf-cache-boundingbox (pdf-view-current-page window))) (margin (max 0 (or pdf-view-bounding-box-margin 0))) (slice (list (- (nth 0 bb) (/ margin 2.0)) (- (nth 1 bb) (/ margin 2.0)) (+ (- (nth 2 bb) (nth 0 bb)) margin) (+ (- (nth 3 bb) (nth 1 bb)) margin)))) (apply 'pdf-view-set-slice (append slice (and window (list window)))))) (defun pdf-view-reset-slice (&optional window) ;; TODO: add WINDOW to doctring. "Reset the current slice. After calling this function the whole page will be visible again." (interactive) (when (pdf-view-current-slice window) (setf (pdf-view-current-slice window) nil) (pdf-view-redisplay window)) nil) (define-minor-mode pdf-view-auto-slice-minor-mode "Automatically slice pages according to their bounding boxes. See also `pdf-view-set-slice-from-bounding-box'." nil nil nil (pdf-util-assert-pdf-buffer) (cond (pdf-view-auto-slice-minor-mode (dolist (win (get-buffer-window-list nil nil t)) (when (pdf-util-pdf-window-p win) (pdf-view-set-slice-from-bounding-box win))) (add-hook 'pdf-view-change-page-hook 'pdf-view-set-slice-from-bounding-box nil t)) (t (remove-hook 'pdf-view-change-page-hook 'pdf-view-set-slice-from-bounding-box t)))) ;; * ================================================================== * ;; * Display ;; * ================================================================== * (defvar pdf-view-inhibit-redisplay nil) (defvar pdf-view-inhibit-hotspots nil) (defun pdf-view-image-type () "Return the image type that should be used. The return value is either `imagemagick' (if available and wanted or if png is not available) or `png'. Signal an error, if neither `imagemagick' nor `png' is available. See also `pdf-view-use-imagemagick'." (cond ((and pdf-view-use-imagemagick (fboundp 'imagemagick-types)) 'imagemagick) ((image-type-available-p 'png) 'png) ((fboundp 'imagemagick-types) 'imagemagick) (t (error "PNG image supported not compiled into Emacs")))) (defun pdf-view-use-scaling-p () "Return t if scaling should be used." (and (eq 'imagemagick (pdf-view-image-type)) pdf-view-use-scaling)) (defmacro pdf-view-create-image (data &rest props) ;; TODO: add DATA and PROPS to docstring. "Like `create-image', but with set DATA-P and TYPE arguments." (declare (indent 1) (debug t)) `(create-image ,data (pdf-view-image-type) t ,@props :relief (or pdf-view-image-relief 0))) (defun pdf-view-create-page (page &optional window) "Create an image of PAGE for display on WINDOW." (let* ((size (pdf-view-desired-image-size page window)) (data (pdf-cache-renderpage page (car size) (if (not (pdf-view-use-scaling-p)) (car size) (* 2 (car size))))) (hotspots (pdf-view-apply-hotspot-functions window page size))) (pdf-view-create-image data :width (car size) :map hotspots :pointer 'arrow))) (defun pdf-view-image-size (&optional displayed-p window) ;; TODO: add WINDOW to docstring. "Return the size in pixel of the current image. If DISPLAYED-P is non-nil, return the size of the displayed image. These values may be different, if slicing is used." (if displayed-p (with-selected-window (or window (selected-window)) (image-display-size (image-get-display-property) t)) (image-size (pdf-view-current-image window) t))) (defun pdf-view-image-offset (&optional window) ;; TODO: add WINDOW to docstring. "Return the offset of the current image. It is equal to \(LEFT . TOP\) of the current slice in pixel." (let* ((slice (pdf-view-current-slice window))) (cond (slice (pdf-util-scale-relative-to-pixel (cons (nth 0 slice) (nth 1 slice)) window)) (t (cons 0 0))))) (defun pdf-view-display-page (page &optional window) "Display page PAGE in WINDOW." (pdf-view-display-image (pdf-view-create-page page window) window)) (defun pdf-view-display-image (image &optional window inhibit-slice-p) ;; TODO: write documentation! (let ((ol (pdf-view-current-overlay window))) (when (window-live-p (overlay-get ol 'window)) (let* ((size (image-size image t)) (slice (if (not inhibit-slice-p) (pdf-view-current-slice window))) (displayed-width (floor (if slice (* (nth 2 slice) (car (image-size image))) (car (image-size image)))))) (setf (pdf-view-current-image window) image) (move-overlay ol (point-min) (point-max)) ;; In case the window is wider than the image, center the image ;; horizontally. (overlay-put ol 'before-string (when (> (window-width window) displayed-width) (propertize " " 'display `(space :align-to ,(/ (- (window-width window) displayed-width) 2))))) (overlay-put ol 'display (if slice (list (cons 'slice (pdf-util-scale slice size 'round)) image) image)) (let* ((win (overlay-get ol 'window)) (hscroll (image-mode-window-get 'hscroll win)) (vscroll (image-mode-window-get 'vscroll win))) ;; Reset scroll settings, in case they were changed. (if hscroll (set-window-hscroll win hscroll)) (if vscroll (set-window-vscroll win vscroll))))))) (defun pdf-view-redisplay (&optional window) "Redisplay page in WINDOW. If WINDOW is t, redisplay pages in all windows." (unless pdf-view-inhibit-redisplay (if (not (eq t window)) (pdf-view-display-page (pdf-view-current-page window) window) (dolist (win (get-buffer-window-list nil nil t)) (pdf-view-display-page (pdf-view-current-page win) win))) (force-mode-line-update))) (defun pdf-view-redisplay-pages (&rest pages) "Redisplay PAGES in all windows." (pdf-util-assert-pdf-buffer) (dolist (window (get-buffer-window-list nil nil t)) (when (memq (pdf-view-current-page window) pages) (pdf-view-redisplay window)))) (defun pdf-view-maybe-redisplay-resized-windows () "Redisplay some windows needing redisplay." (unless (or (numberp pdf-view-display-size) (pdf-view-active-region-p) (> (minibuffer-depth) 0)) (dolist (window (get-buffer-window-list nil nil t)) (let ((stored (pdf-view-current-window-size window)) (size (cons (window-width window) (window-height window)))) (unless (equal size stored) (setf (pdf-view-current-window-size window) size) (unless (or (null stored) (and (eq pdf-view-display-size 'fit-width) (eq (car size) (car stored))) (and (eq pdf-view-display-size 'fit-height) (eq (cdr size) (cdr stored)))) (pdf-view-redisplay window))))))) (defun pdf-view-new-window-function (winprops) ;; TODO: write documentation! ;; (message "New window %s for buf %s" (car winprops) (current-buffer)) (cl-assert (or (eq t (car winprops)) (eq (window-buffer (car winprops)) (current-buffer)))) (let ((ol (image-mode-window-get 'overlay winprops))) (if ol (progn (setq ol (copy-overlay ol)) ;; `ol' might actually be dead. (move-overlay ol (point-min) (point-max))) (setq ol (make-overlay (point-min) (point-max) nil t)) (overlay-put ol 'pdf-view t)) (overlay-put ol 'window (car winprops)) (unless (windowp (car winprops)) ;; It's a pseudo entry. Let's make sure it's not displayed (the ;; `window' property is only effective if its value is a window). (cl-assert (eq t (car winprops))) (delete-overlay ol)) (image-mode-window-put 'overlay ol winprops) ;; Clean up some overlays. (dolist (ov (overlays-in (point-min) (point-max))) (when (and (windowp (overlay-get ov 'window)) (not (window-live-p (overlay-get ov 'window)))) (delete-overlay ov))) (when (and (windowp (car winprops)) (null (image-mode-window-get 'image winprops))) ;; We're not displaying an image yet, so let's do so. This ;; happens when the buffer is displayed for the first time. (with-selected-window (car winprops) (pdf-view-goto-page (or (image-mode-window-get 'page t) 1)))))) (defun pdf-view-desired-image-size (&optional page window) ;; TODO: write documentation! (let* ((pagesize (pdf-cache-pagesize (or page (pdf-view-current-page window)))) (slice (pdf-view-current-slice window)) (width-scale (/ (/ (float (pdf-util-window-pixel-width window)) (or (nth 2 slice) 1.0)) (float (car pagesize)))) (height (- (nth 3 (window-inside-pixel-edges window)) (nth 1 (window-inside-pixel-edges window)) 1)) (height-scale (/ (/ (float height) (or (nth 3 slice) 1.0)) (float (cdr pagesize)))) (scale width-scale)) (if (numberp pdf-view-display-size) (setq scale (float pdf-view-display-size)) (cl-case pdf-view-display-size (fit-page (setq scale (min height-scale width-scale))) (fit-height (setq scale height-scale)) (t (setq scale width-scale)))) (let ((width (floor (* (car pagesize) scale))) (height (floor (* (cdr pagesize) scale)))) (when (> width (max 1 (or pdf-view-max-image-width width))) (setq width pdf-view-max-image-width height (* height (/ (float pdf-view-max-image-width) width)))) (cons (max 1 width) (max 1 height))))) (defun pdf-view-text-regions-hotspots-function (page size) "Return a list of hotspots for text regions on PAGE using SIZE. This will display a text cursor, when hovering over them." (local-set-key [pdf-view-text-region t] 'pdf-util-image-map-mouse-event-proxy) (mapcar (lambda (region) (let ((e (pdf-util-scale region size 'round))) `((rect . ((,(nth 0 e) . ,(nth 1 e)) . (,(nth 2 e) . ,(nth 3 e)))) pdf-view-text-region (pointer text)))) (pdf-cache-textregions page))) (define-minor-mode pdf-view-dark-minor-mode "Mode for PDF documents with dark background. This tells the various modes to use their face's dark colors." nil nil nil (pdf-util-assert-pdf-buffer) ;; FIXME: This should really be run in a hook. (when (bound-and-true-p pdf-isearch-active-mode) (with-no-warnings (pdf-isearch-redisplay) (pdf-isearch-message (if pdf-view-dark-minor-mode "dark mode" "light mode"))))) (define-minor-mode pdf-view-printer-minor-mode "Display the PDF as it would be printed." nil " Prn" nil (pdf-util-assert-pdf-buffer) (let ((enable (lambda () (pdf-info-setoptions :render/printed t)))) (cond (pdf-view-printer-minor-mode (add-hook 'after-save-hook enable nil t) (add-hook 'after-revert-hook enable nil t)) (t (remove-hook 'after-save-hook enable t) (remove-hook 'after-revert-hook enable t)))) (pdf-info-setoptions :render/printed pdf-view-printer-minor-mode) (pdf-cache-clear-images) (pdf-view-redisplay t)) (define-minor-mode pdf-view-midnight-minor-mode "Apply a color-filter appropriate for past midnight reading. The colors are determined by the variable `pdf-view-midnight-colors', which see. " nil " Mid" nil (pdf-util-assert-pdf-buffer) ;; FIXME: Maybe these options should be passed stateless to pdf-info-renderpage ? (let ((enable (lambda () (pdf-info-setoptions :render/foreground (or (car pdf-view-midnight-colors) "black") :render/background (or (cdr pdf-view-midnight-colors) "white") :render/usecolors t)))) (cond (pdf-view-midnight-minor-mode (add-hook 'after-save-hook enable nil t) (add-hook 'after-revert-hook enable nil t) (funcall enable)) (t (remove-hook 'after-save-hook enable t) (remove-hook 'after-revert-hook enable t) (pdf-info-setoptions :render/usecolors nil)))) (pdf-cache-clear-images) (pdf-view-redisplay t)) (when pdf-view-use-unicode-ligther ;; This check uses an implementation detail, which hopefully gets the ;; right answer. (and (fontp (char-displayable-p ?⎙)) (setcdr (assq 'pdf-view-printer-minor-mode minor-mode-alist) (list " ⎙" ))) (and (fontp (char-displayable-p ?🌙)) (setcdr (assq 'pdf-view-midnight-minor-mode minor-mode-alist) (list " 🌙" )))) ;; * ================================================================== * ;; * Hotspot handling ;; * ================================================================== * (defun pdf-view-add-hotspot-function (fn &optional layer) "Register FN as a hotspot function in the current buffer, using LAYER. FN will be called in the PDF buffer with the page-number and the image size \(WIDTH . HEIGHT\) as arguments. It should return a list of hotspots applicable to the the :map image-property. LAYER determines the order: Functions in a higher LAYER will supercede hotspots in lower ones." (push (cons (or layer 0) fn) pdf-view--hotspot-functions)) (defun pdf-view-remove-hotspot-function (fn) "Unregister FN as a hotspot function in the current buffer." (setq pdf-view--hotspot-functions (cl-remove fn pdf-view--hotspot-functions :key 'cdr))) (defun pdf-view-sorted-hotspot-functions () ;; TODO: write documentation! (mapcar 'cdr (cl-sort (copy-sequence pdf-view--hotspot-functions) '> :key 'car))) (defun pdf-view-apply-hotspot-functions (window page image-size) ;; TODO: write documentation! (unless pdf-view-inhibit-hotspots (save-selected-window (when window (select-window window)) (apply 'nconc (mapcar (lambda (fn) (funcall fn page image-size)) (pdf-view-sorted-hotspot-functions)))))) ;; * ================================================================== * ;; * Region ;; * ================================================================== * (defun pdf-view--push-mark () ;; TODO: write documentation! (let (mark-ring) (push-mark-command nil)) (setq deactivate-mark nil)) (defun pdf-view-active-region (&optional deactivate-p) "Return the active region, a list of edges. Deactivate the region if DEACTIVATE-P is non-nil." (pdf-view-assert-active-region) (prog1 pdf-view-active-region (when deactivate-p (pdf-view-deactivate-region)))) (defun pdf-view-deactivate-region () "Deactivate the region." (interactive) (when pdf-view-active-region (setq pdf-view-active-region nil) (deactivate-mark) (pdf-view-redisplay t))) (defun pdf-view-mouse-set-region (event &optional allow-extend-p rectangle-p) "Select a region of text using the mouse with mouse event EVENT. Allow for stacking of regions, if ALLOW-EXTEND-P is non-nil. Create a rectangular region, if RECTANGLE-P is non-nil. Stores the region in `pdf-view-active-region'." (interactive "@e") (setq pdf-view--have-rectangle-region rectangle-p) (unless (and (eventp event) (mouse-event-p event)) (signal 'wrong-type-argument (list 'mouse-event-p event))) (unless (and allow-extend-p (or (null (get this-command 'pdf-view-region-window)) (equal (get this-command 'pdf-view-region-window) (selected-window)))) (pdf-view-deactivate-region)) (put this-command 'pdf-view-region-window (selected-window)) (let* ((window (selected-window)) (pos (event-start event)) (begin-inside-image-p t) (begin (if (posn-image pos) (posn-object-x-y pos) (setq begin-inside-image-p nil) (posn-x-y pos))) (abs-begin (posn-x-y pos)) pdf-view-continuous region) (when (pdf-util-track-mouse-dragging (event 0.15) (let* ((pos (event-start event)) (end (posn-object-x-y pos)) (end-inside-image-p (and (eq window (posn-window pos)) (posn-image pos)))) (when (or end-inside-image-p begin-inside-image-p) (cond ((and end-inside-image-p (not begin-inside-image-p)) ;; Started selection ouside the image, setup begin. (let* ((xy (posn-x-y pos)) (dxy (cons (- (car xy) (car begin)) (- (cdr xy) (cdr begin)))) (size (pdf-view-image-size t))) (setq begin (cons (max 0 (min (car size) (- (car end) (car dxy)))) (max 0 (min (cdr size) (- (cdr end) (cdr dxy))))) ;; Store absolute position for later. abs-begin (cons (- (car xy) (- (car end) (car begin))) (- (cdr xy) (- (cdr end) (cdr begin)))) begin-inside-image-p t))) ((and begin-inside-image-p (not end-inside-image-p)) ;; Moved outside the image, setup end. (let* ((xy (posn-x-y pos)) (dxy (cons (- (car xy) (car abs-begin)) (- (cdr xy) (cdr abs-begin)))) (size (pdf-view-image-size t))) (setq end (cons (max 0 (min (car size) (+ (car begin) (car dxy)))) (max 0 (min (cdr size) (+ (cdr begin) (cdr dxy))))))))) (let ((iregion (if rectangle-p (list (min (car begin) (car end)) (min (cdr begin) (cdr end)) (max (car begin) (car end)) (max (cdr begin) (cdr end))) (list (car begin) (cdr begin) (car end) (cdr end))))) (setq region (pdf-util-scale-pixel-to-relative iregion)) (pdf-view-display-region (cons region pdf-view-active-region) rectangle-p) (pdf-util-scroll-to-edges iregion))))) (setq pdf-view-active-region (append pdf-view-active-region (list region))) (pdf-view--push-mark)))) (defun pdf-view-mouse-extend-region (event) "Extend the currently active region with mouse event EVENT." (interactive "@e") (pdf-view-mouse-set-region event t pdf-view--have-rectangle-region)) (defun pdf-view-mouse-set-region-rectangle (event) "Like `pdf-view-mouse-set-region' but displays as a rectangle. EVENT is the mouse event. This is more useful for commands like `pdf-view-extract-region-image'." (interactive "@e") (pdf-view-mouse-set-region event nil t)) (defun pdf-view-display-region (&optional region rectangle-p) ;; TODO: write documentation! (unless region (pdf-view-assert-active-region) (setq region pdf-view-active-region)) (let ((colors (pdf-util-face-colors (if rectangle-p 'pdf-view-rectangle 'pdf-view-region) (bound-and-true-p pdf-view-dark-minor-mode))) (page (pdf-view-current-page)) (width (car (pdf-view-image-size)))) (pdf-view-display-image (pdf-view-create-image (if rectangle-p (pdf-info-renderpage-highlight page width nil `(,(car colors) ,(cdr colors) 0.35 ,@region)) (pdf-info-renderpage-text-regions page width nil nil `(,(car colors) ,(cdr colors) ,@region))))))) (defun pdf-view-kill-ring-save () "Copy the region to the `kill-ring'." (interactive) (pdf-view-assert-active-region) (let* ((txt (pdf-view-active-region-text))) (pdf-view-deactivate-region) (kill-new (mapconcat 'identity txt "\n")))) (defun pdf-view-mark-whole-page () "Mark the whole page." (interactive) (pdf-view-deactivate-region) (setq pdf-view-active-region (list (list 0 0 1 1))) (pdf-view--push-mark) (pdf-view-display-region)) (defun pdf-view-active-region-text () "Return the text of the active region as a list of strings." (pdf-view-assert-active-region) (mapcar (apply-partially 'pdf-info-gettext (pdf-view-current-page)) pdf-view-active-region)) (defun pdf-view-extract-region-image (regions &optional page size output-buffer no-display-p) ;; TODO: what is "resp."? Avoid contractions. "Create a PNG image of REGIONS. REGIONS should have the same form as `pdf-view-active-region', which see. PAGE and SIZE are the page resp. base-size of the image from which the image-regions will be created; they default to `pdf-view-current-page' resp. `pdf-view-image-size'. Put the image in OUTPUT-BUFFER, defaulting to \"*PDF region image*\" and display it, unless NO-DISPLAY-P is non-nil. In case of multiple regions, the resulting image is constructed by joining them horizontally. For this operation (and this only) the `convert' programm is used." (interactive (list (if (pdf-view-active-region-p) (pdf-view-active-region t) '((0 0 1 1))))) (unless page (setq page (pdf-view-current-page))) (unless size (setq size (pdf-view-image-size))) (unless output-buffer (setq output-buffer (get-buffer-create "*PDF image*"))) (let* ((images (mapcar (lambda (edges) (let ((file (make-temp-file "pdf-view")) (coding-system-for-write 'binary)) (write-region (pdf-info-renderpage page (car size) :crop-to edges) nil file nil 'no-message) file)) regions)) result) (unwind-protect (progn (if (= (length images) 1) (setq result (car images)) (setq result (make-temp-file "pdf-view")) ;; Join the images horizontally with a gap of 10 pixel. (pdf-util-convert "-noop" ;; workaround limitations of this function result :commands `("(" ,@images "-background" "white" "-splice" "0x10+0+0" ")" "-gravity" "Center" "-append" "+gravity" "-chop" "0x10+0+0") :apply '((0 0 0 0)))) (with-current-buffer output-buffer (let ((inhibit-read-only t)) (erase-buffer)) (set-buffer-multibyte nil) (insert-file-contents-literally result) (image-mode) (unless no-display-p (pop-to-buffer (current-buffer))))) (dolist (f (cons result images)) (when (file-exists-p f) (delete-file f)))))) ;; * ================================================================== * ;; * Bookmark + Register Integration ;; * ================================================================== * (defun pdf-view-bookmark-make-record (&optional no-page no-slice no-size no-origin) ;; TODO: add NO-PAGE, NO-SLICE, NO-SIZE, NO-ORIGIN to the docstring. "Create a bookmark PDF record. The optional, boolean args exclude certain attributes." (let ((displayed-p (eq (current-buffer) (window-buffer)))) (cons (buffer-name) (append (bookmark-make-record-default nil t 1) `(,(unless no-page (cons 'page (pdf-view-current-page))) ,(unless no-slice (cons 'slice (and displayed-p (pdf-view-current-slice)))) ,(unless no-size (cons 'size pdf-view-display-size)) ,(unless no-origin (cons 'origin (and displayed-p (let ((edges (pdf-util-image-displayed-edges nil t))) (pdf-util-scale-pixel-to-relative (cons (car edges) (cadr edges)) nil t))))) (handler . pdf-view-bookmark-jump-handler)))))) ;;;###autoload (defun pdf-view-bookmark-jump-handler (bmk) "The bookmark handler-function interface for bookmark BMK. See also `pdf-view-bookmark-make-record'." (let ((page (bookmark-prop-get bmk 'page)) (slice (bookmark-prop-get bmk 'slice)) (size (bookmark-prop-get bmk 'size)) (origin (bookmark-prop-get bmk 'origin)) (file (bookmark-prop-get bmk 'filename)) (show-fn-sym (make-symbol "pdf-view-bookmark-after-jump-hook"))) (fset show-fn-sym (lambda () (remove-hook 'bookmark-after-jump-hook show-fn-sym) (unless (derived-mode-p 'pdf-view-mode) (pdf-view-mode)) (with-selected-window (or (get-buffer-window (current-buffer) 0) (selected-window)) (when size (setq-local pdf-view-display-size size)) (when slice (apply 'pdf-view-set-slice slice)) (when (numberp page) (pdf-view-goto-page page)) (when origin (let ((size (pdf-view-image-size t))) (image-set-window-hscroll (round (/ (* (car origin) (car size)) (frame-char-width)))) (image-set-window-vscroll (round (/ (* (cdr origin) (cdr size)) (frame-char-height))))))))) (add-hook 'bookmark-after-jump-hook show-fn-sym) (set-buffer (or (find-buffer-visiting file) (find-file-noselect file))))) (defun pdf-view-bookmark-jump (bmk) "Switch to bookmark BMK. This function is like `bookmark-jump', but it always uses the selected window for display and does not run any hooks. Also, it works only with bookmarks created by `pdf-view-bookmark-make-record'." (let* ((file (bookmark-prop-get bmk 'filename)) (buffer (or (find-buffer-visiting file) (find-file-noselect file)))) (switch-to-buffer buffer) (let (bookmark-after-jump-hook) (pdf-view-bookmark-jump-handler bmk) (run-hooks 'bookmark-after-jump-hook)))) (defun pdf-view-registerv-make () "Create a PDF register entry of the current position." (registerv-make (pdf-view-bookmark-make-record nil t t) :print-func 'pdf-view-registerv-print-func :jump-func 'pdf-view-bookmark-jump :insert-func (lambda (bmk) (insert (format "%S" bmk))))) (defun pdf-view-registerv-print-func (bmk) "Print a textual representation of bookmark BMK. This function is used as the `:print-func' property with `registerv-make'." (let* ((file (bookmark-prop-get bmk 'filename)) (buffer (find-buffer-visiting file)) (page (bookmark-prop-get bmk 'page)) (origin (bookmark-prop-get bmk 'origin))) (princ (format "PDF position: %s, page %d, %d%%" (if buffer (buffer-name buffer) file) (or page 1) (if origin (round (* 100 (cdr origin))) 0))))) (defmacro pdf-view-with-register-alist (&rest body) "Setup the proper binding for `register-alist' in BODY. This macro may not work as desired when it is nested. See also `pdf-view-use-dedicated-register'." (declare (debug t) (indent 0)) (let ((dedicated-p (make-symbol "dedicated-p"))) `(let* ((,dedicated-p pdf-view-use-dedicated-register) (register-alist (if ,dedicated-p pdf-view-register-alist register-alist))) (unwind-protect (progn ,@body) (when ,dedicated-p (setq pdf-view-register-alist register-alist)))))) (defun pdf-view-position-to-register (register) "Store current PDF position in register REGISTER. See also `point-to-register'." (interactive (list (pdf-view-with-register-alist (register-read-with-preview "Position to register: ")))) (pdf-view-with-register-alist (set-register register (pdf-view-registerv-make)))) (defun pdf-view-jump-to-register (register &optional delete return-register) ;; TODO: add RETURN-REGISTER to the docstring. "Move point to a position stored in a REGISTER. Optional parameter DELETE is defined as in `jump-to-register'." (interactive (pdf-view-with-register-alist (list (register-read-with-preview "Jump to register: ") current-prefix-arg (and (or pdf-view-use-dedicated-register (local-variable-p 'register-alist)) (characterp last-command-event) last-command-event)))) (pdf-view-with-register-alist (let ((return-pos (and return-register (pdf-view-registerv-make)))) (jump-to-register register delete) (when return-register (set-register return-register return-pos))))) (provide 'pdf-view) ;;; pdf-view.el ends here pdf-tools-0.80/lisp/pdf-virtual.el000066400000000000000000001171311315513274600171110ustar00rootroot00000000000000;;; pdf-virtual.el --- Virtual PDF documents -*- lexical-binding: t; -*- ;; Copyright (C) 2015 Andreas Politz ;; Author: Andreas Politz ;; Keywords: multimedia, files ;; 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 . ;;; Commentary: ;; A virtual PDF is a collection of pages, or parts thereof, of ;; arbitrary documents in one particular order. This library acts as ;; an intermediate between pdf-info.el and all other packages, in ;; order to transparently make this collection appear as one single ;; document. ;; ;; The trickiest part is to make theses intermediate functions behave ;; like the pdf-info-* equivalents in both the synchronous and ;; asynchronous case. ;;; Code: (eval-when-compile (unless (or (> emacs-major-version 24) (and (= emacs-major-version 24) (>= emacs-minor-version 4))) (error "pdf-virtual.el only works with Emacs >= 24.4"))) (require 'let-alist) (require 'pdf-info) (require 'pdf-util) ;; * ================================================================== * ;; * Variables ;; * ================================================================== * (defconst pdf-virtual-magic-mode-regexp "^ *;+ *%VPDF\\_>" "A regexp matching the first line in a vpdf file.") (defvar-local pdf-virtual-document nil "A list representing the virtual document.") (put 'pdf-virtual-document 'permanent-local t) (defvar pdf-virtual-adapter-alist nil "Alist of server functions. Each element looks like \(PDF-VIRTUAL-FN . PDF-INFO-FN\). This list is filled by the macro `pdf-virtual-define-adapter' and used to enable/disable the corresponding advices.") ;; * ================================================================== * ;; * VPDF datastructure ;; * ================================================================== * (defun pdf-virtual-pagespec-normalize (page-spec &optional filename) "Normalize PAGE-SPEC using FILENAME. PAGE-SPEC should be as described in `pdf-virtual-document-create'. FILENAME is used to determine the last page number, if needed. The `current-buffer', if it is nil. Returns a list \(\(FIRST . LAST\) . REGION\)\)." (let ((page-spec (cond ((natnump page-spec) (list (cons page-spec page-spec))) ((null (car page-spec)) (let ((npages (pdf-info-number-of-pages filename))) (cons (cons 1 npages) (cdr page-spec)))) ((natnump (car page-spec)) (cond ((natnump (cdr page-spec)) (list page-spec)) (t (cons (cons (car page-spec) (car page-spec)) (cdr page-spec))))) (t page-spec)))) (when (equal (cdr page-spec) '(0 0 1 1)) (setq page-spec `((,(caar page-spec) . ,(cdar page-spec))))) page-spec)) (cl-defstruct pdf-virtual-range ;; The PDF's filename. filename ;; First page in this range. first ;; Last page. last ;; The edges selected for these pages. region ;; The page-index corresponding to the first page in this range. index-start) (cl-defstruct pdf-virtual-document ;; Array of shared pdf-virtual-range structs, one element for each ;; page. page-array ;; An alist mapping filenames to a list of pages. file-map) (defun pdf-virtual-range-length (page) "Return the number of pages in PAGE." (1+ (- (pdf-virtual-range-last page) (pdf-virtual-range-first page)))) (defun pdf-virtual-document-create (list &optional directory file-error-handler) "Create a virtual PDF from LIST using DIRECTORY. LIST should be a list of elements \(FILENAME . PAGE-SPECS\), where FILENAME is a PDF document and PAGE-SPECS is a list of PAGE-RANGE and/or \(PAGE-RANGE . EDGES\). In the later case, EDGES should be a list of relative coordinates \(LEFT TOP RIGHT BOT\) selecting a region of the page(s) in PAGE-RANGE. Giving no PAGE-SPECs at all is equivalent to all pages of FILENAME. See `pdf-info-normalize-page-range' for the valid formats of PAGE-RANGE. " (unless (cl-every 'consp list) (error "Every element should be a cons: %s" list)) (unless (cl-every 'stringp (mapcar 'car list)) (error "The car of every element should be a filename.")) (unless (cl-every (lambda (elt) (cl-every (lambda (page) (or (pdf-info-valid-page-spec-p page) (and (consp page) (pdf-info-valid-page-spec-p (car page)) (pdf-util-edges-p (cdr page) 'relative)))) elt)) (mapcar 'cdr list)) (error "The cdr of every element should be a list of page-specs")) (let* ((doc (pdf-virtual-document--normalize list (or directory default-directory) file-error-handler)) (npages 0) document file-map) (while doc (let* ((elt (pop doc)) (filename (car elt)) (mapelt (assoc filename file-map)) (page-specs (cdr elt))) (if mapelt (setcdr mapelt (cons (1+ npages) (cdr mapelt))) (push (list filename (1+ npages)) file-map)) (while page-specs (let* ((ps (pop page-specs)) (first (caar ps)) (last (cdar ps)) (region (cdr ps)) (clx (make-pdf-virtual-range :filename filename :first first :last last :region region :index-start npages))) (cl-incf npages (1+ (- last first))) (push (make-vector (1+ (- last first)) clx) document))))) (make-pdf-virtual-document :page-array (apply 'vconcat (nreverse document)) :file-map (nreverse (mapcar (lambda (f) (setcdr f (nreverse (cdr f))) f) file-map))))) (defun pdf-virtual-document--normalize (list &optional directory file-error-handler) (unless file-error-handler (setq file-error-handler (lambda (filename err) (signal (car err) (append (cdr err) (list filename)))))) (let ((default-directory (or directory default-directory))) (setq list (cl-remove-if-not (lambda (filename) (condition-case err (progn (unless (file-readable-p filename) (signal 'file-error (list "File not readable: " filename))) (pdf-info-open filename) t) (error (funcall file-error-handler filename err) nil))) list :key 'car)) (let* ((file-attributes (make-hash-table :test 'equal)) (file-equal-p (lambda (f1 f2) (let ((a1 (gethash f1 file-attributes)) (a2 (gethash f2 file-attributes))) (if (and a1 a2) (equal a1 a2) (file-equal-p f1 f2))))) files normalized) ;; Optimize file-equal-p by caching file-attributes, which is slow ;; and would be called quadratic times otherwise. (We don't want ;; the same file under different names.) (dolist (f (mapcar 'car list)) (unless (find-file-name-handler f 'file-equal-p) (puthash f (file-attributes f) file-attributes))) (dolist (elt list) (let ((file (cl-find (car elt) files :test file-equal-p))) (unless file (push (car elt) files) (setq file (car elt))) (let ((pages (mapcar (lambda (p) (pdf-virtual-pagespec-normalize p file)) (or (cdr elt) '(nil)))) newpages) (while pages (let* ((spec (pop pages)) (first (caar spec)) (last (cdar spec)) (region (cdr spec))) (while (and pages (eq (1+ last) (caar (car pages))) (equal region (cdr (car pages)))) (setq last (cdar (pop pages)))) (push `((,first . ,last) . ,region) newpages))) (push (cons file (nreverse newpages)) normalized)))) (nreverse normalized)))) (defmacro pdf-virtual-document-defun (name args &optional documentation &rest body) "Define a PDF Document function. Args are just like for `defun'. This macro will ensure, that the DOCUMENT argument, which should be last, is setup properly in case it is nil, i.e. check that the buffer passes `pdf-virtual-buffer-assert-p' and use the variable `pdf-virtual-document'." (declare (doc-string 3) (indent defun) (debug (&define name lambda-list [&optional stringp] def-body)) (font-lock defun) (imenu "Document")) (unless (stringp documentation) (push documentation body) (setq documentation nil)) (unless (memq '&optional args) (setq args (append (butlast args) (list '&optional) (last args)))) (when (memq '&rest args) (error "&rest argument not supported")) (let ((doc-arg (car (last args))) (fn (intern (format "pdf-virtual-document-%s" name)))) `(progn (put ',fn 'definition-name ',name) (defun ,fn ,args ,documentation (setq ,doc-arg (or ,doc-arg (progn (pdf-virtual-buffer-assert-p) pdf-virtual-document))) (cl-check-type ,doc-arg pdf-virtual-document) ,@body)))) (pdf-virtual-document-defun filenames (doc) "Return the list of filenames in DOC." (mapcar 'car (pdf-virtual-document-file-map doc))) (pdf-virtual-document-defun normalize-pages (pages doc) "Normalize PAGES using DOC. Like `pdf-info-normalize-page-range', except 0 is replaced by DOC's last page." (setq pages (pdf-info-normalize-page-range pages)) (if (eq 0 (cdr pages)) `(,(car pages) . ,(pdf-virtual-document-number-of-pages doc)) pages)) (pdf-virtual-document-defun page (page doc) "Get PAGE of DOC. Returns a list \(FILENAME FILE-PAGE REGION\)." (let ((page (car (pdf-virtual-document-pages (cons page page) doc)))) (when page (cl-destructuring-bind (filename first-last region) page (list filename (car first-last) region))))) (pdf-virtual-document-defun pages (pages doc) "Get PAGES of DOC. PAGES should be a cons \(FIRST . LAST\). Return a list of ranges corresponding to PAGES. Each element has the form \(FILENAME \(FILE-FIRT-PAGE . FILE-LAST-PAGE\) REGION\) . " (let ((begin (car pages)) (end (cdr pages))) (unless (<= begin end) (error "begin should not exceed end: %s" (cons begin end))) (let ((arr (pdf-virtual-document-page-array doc)) result) (when (or (< begin 1) (> end (length arr))) (signal 'args-out-of-range (list 'pages pages))) (while (<= begin end) (let* ((page (aref arr (1- begin))) (filename (pdf-virtual-range-filename page)) (offset (- (1- begin) (pdf-virtual-range-index-start page))) (first (+ (pdf-virtual-range-first page) offset)) (last (min (+ first (- end begin)) (pdf-virtual-range-last page))) (region (pdf-virtual-range-region page))) (push `(,filename (,first . ,last) ,region) result) (cl-incf begin (1+ (- last first))))) (nreverse result)))) (pdf-virtual-document-defun number-of-pages (doc) "Return the number of pages in DOC." (length (pdf-virtual-document-page-array doc))) (pdf-virtual-document-defun page-of (filename &optional file-page limit doc) "Return a page number displaying FILENAME's page FILE-PAGE in DOC. If FILE-PAGE is nil, return the first page displaying FILENAME. If LIMIT is non-nil, it should be a range \(FIRST . LAST\) in which the returned page should fall. This is useful if there are more than one page displaying FILE-PAGE. LIMIT is ignored, if FILE-PAGE is nil. Return nil if there is no matching page." (if (null file-page) (cadr (assoc filename (pdf-virtual-document-file-map doc))) (let ((pages (pdf-virtual-document-page-array doc))) (catch 'found (mapc (lambda (pn) (while (and (<= pn (length pages)) (equal (pdf-virtual-range-filename (aref pages (1- pn))) filename)) (let* ((page (aref pages (1- pn))) (first (pdf-virtual-range-first page)) (last (pdf-virtual-range-last page))) (when (and (>= file-page first) (<= file-page last)) (let ((r (+ (pdf-virtual-range-index-start page) (- file-page (pdf-virtual-range-first page)) 1))) (when (or (null limit) (and (>= r (car limit)) (<= r (cdr limit)))) (throw 'found r)))) (cl-incf pn (1+ (- last first)))))) (cdr (assoc filename (pdf-virtual-document-file-map doc)))) nil)))) (pdf-virtual-document-defun find-matching-page (page predicate &optional backward-p doc) (unless (and (>= page 1) (<= page (length (pdf-virtual-document-page-array doc)))) (signal 'args-out-of-range (list 'page page))) (let* ((pages (pdf-virtual-document-page-array doc)) (i (1- page)) (this (aref pages i)) other) (while (and (< i (length pages)) (>= i 0) (null other)) (setq i (if backward-p (1- (pdf-virtual-range-index-start this)) (+ (pdf-virtual-range-length this) (pdf-virtual-range-index-start this)))) (when (and (< i (length pages)) (>= i 0)) (setq other (aref pages i)) (unless (funcall predicate this other) (setq other nil)))) other)) (pdf-virtual-document-defun next-matching-page (page predicate doc) (pdf-virtual-document-find-matching-page page predicate nil doc)) (pdf-virtual-document-defun previous-matching-page (page predicate doc) (declare (indent 1)) (pdf-virtual-document-find-matching-page page predicate t doc)) (pdf-virtual-document-defun next-file (page doc) "Return the next page displaying a different file than PAGE. PAGE should be a page-number." (let ((page (pdf-virtual-document-next-matching-page page (lambda (this other) (not (equal (pdf-virtual-range-filename this) (pdf-virtual-range-filename other))))))) (when page (1+ (pdf-virtual-range-index-start page))))) (pdf-virtual-document-defun previous-file (page doc) "Return the previous page displaying a different file than PAGE. PAGE should be a page-number." (let ((page (pdf-virtual-document-previous-matching-page page (lambda (this other) (not (equal (pdf-virtual-range-filename this) (pdf-virtual-range-filename other))))))) (when page (1+ (pdf-virtual-range-index-start page))))) ;; * ================================================================== * ;; * Modes ;; * ================================================================== * (defvar pdf-virtual-edit-mode-map (let ((map (make-sparse-keymap))) (set-keymap-parent map emacs-lisp-mode-map) (define-key map (kbd "C-c C-c") 'pdf-virtual-view-mode) map)) ;;;###autoload (define-derived-mode pdf-virtual-edit-mode emacs-lisp-mode "VPDF-Edit" "Major mode when editing a virtual PDF buffer." (buffer-enable-undo) (setq-local buffer-read-only nil) (unless noninteractive (message (substitute-command-keys "Press \\[pdf-virtual-view-mode] to view.")))) ;; FIXME: Provide filename/region from-windows-gathering functions. (defvar pdf-virtual-view-mode-map (let ((map (make-sparse-keymap))) (set-keymap-parent map pdf-view-mode-map) (define-key map (kbd "C-c C-c") 'pdf-virtual-edit-mode) (define-key map [remap backward-paragraph] 'pdf-virtual-buffer-backward-file) (define-key map [remap forward-paragraph] 'pdf-virtual-buffer-forward-file) (define-key map (kbd "C-c C-c") 'pdf-virtual-edit-mode) map)) ;;;###autoload (define-derived-mode pdf-virtual-view-mode pdf-view-mode "VPDF-View" "Major mode in virtual PDF buffers." (setq-local write-contents-functions nil) (remove-hook 'kill-buffer-hook 'pdf-view-close-document t) (setq-local header-line-format `(:eval (pdf-virtual-buffer-current-file))) (unless noninteractive (message (substitute-command-keys "Press \\[pdf-virtual-edit-mode] to edit.")))) ;;;###autoload (define-minor-mode pdf-virtual-global-minor-mode "Enable recognition and handling of VPDF files." nil nil nil :global t (let ((elt `(,pdf-virtual-magic-mode-regexp . pdf-virtual-view-mode))) (cond (pdf-virtual-global-minor-mode (add-to-list 'magic-mode-alist elt)) (t (setq magic-mode-alist (remove elt magic-mode-alist)))) (dolist (elt pdf-virtual-adapter-alist) (let ((fn (car elt)) (orig (cdr elt))) (advice-remove orig fn) (when pdf-virtual-global-minor-mode (advice-add orig :around fn)))))) (advice-add 'pdf-virtual-view-mode :around 'pdf-virtual-view-mode-prepare) ;; This needs to run before pdf-view-mode does its thing. (defun pdf-virtual-view-mode-prepare (fn) (let (list unreadable) (save-excursion (goto-char 1) (unless (looking-at pdf-virtual-magic-mode-regexp) (pdf-virtual-buffer-assert-p)) (setq list (read (current-buffer)))) (setq pdf-virtual-document (pdf-virtual-document-create list nil (lambda (filename _error) (push filename unreadable)))) (when unreadable (display-warning 'pdf-virtual (format "Some documents could not be opened:\n%s" (mapconcat (lambda (f) (concat " " f)) unreadable "\n")))) (if (= (pdf-virtual-document-number-of-pages) 0) (error "Docüment is empty.") (unless pdf-virtual-global-minor-mode (pdf-virtual-global-minor-mode 1)) (funcall fn)))) ;; * ================================================================== * ;; * Buffer handling ;; * ================================================================== * ;;;###autoload (defun pdf-virtual-buffer-create (&optional filenames buffer-name display-p) (interactive (list (directory-files default-directory nil "\\.pdf\\'") (read-string "Buffer name (default: all.vpdf): " nil nil "all.vpdf") t)) (with-current-buffer (generate-new-buffer buffer-name) (insert ";; %VPDF 1.0\n\n") (insert ";; File Format ;; ;; FORMAT ::= ( FILES* ) ;; FILES ::= ( FILE . PAGE-SPEC* ) ;; PAGE-SPEC ::= PAGE | ( PAGE . REGION ) ;; PAGE ::= NUMBER | ( FIRST . LAST ) ;; REGION ::= ( LEFT TOP RIGHT BOT ) ;; ;; 0 <= X <= 1, forall X in REGION . ") (if (null filenames) (insert "nil\n") (insert "(") (dolist (f filenames) (insert (format "(%S)\n " f))) (delete-char -2) (insert ")\n")) (pdf-virtual-edit-mode) (when display-p (pop-to-buffer (current-buffer))) (current-buffer))) (defun pdf-virtual-buffer-p (&optional buffer) (save-current-buffer (when buffer (set-buffer buffer)) (or (derived-mode-p 'pdf-virtual-view-mode 'pdf-virtual-edit-mode) pdf-virtual-document))) (defun pdf-virtual-view-window-p (&optional window) (save-selected-window (when window (select-window window)) (derived-mode-p 'pdf-virtual-view-mode))) (defun pdf-virtual-filename-p (filename) (and (stringp filename) (file-exists-p filename) (with-temp-buffer (save-excursion (insert-file-contents filename nil 0 128)) (looking-at pdf-virtual-magic-mode-regexp)))) (defun pdf-virtual-buffer-assert-p (&optional buffer) (unless (pdf-virtual-buffer-p buffer) (error "Buffer is not a virtual PDF buffer"))) (defun pdf-virtual-view-window-assert-p (&optional window) (unless (pdf-virtual-view-window-p window) (error "Window's buffer is not in `pdf-virtual-view-mode'."))) (defun pdf-virtual-buffer-current-file (&optional window) (pdf-virtual-view-window-assert-p window) (pdf-virtual-range-filename (aref (pdf-virtual-document-page-array pdf-virtual-document) (1- (pdf-view-current-page window))))) (defun pdf-virtual-buffer-forward-file (&optional n interactive-p) (interactive "p\np") (pdf-virtual-view-window-assert-p) (let* ((pn (pdf-view-current-page)) (pages (pdf-virtual-document-page-array pdf-virtual-document)) (page (aref pages (1- pn))) (first-filepage (1+ (pdf-virtual-range-index-start page)))) (when (and (< n 0) (not (= first-filepage pn))) (cl-incf n)) (setq pn first-filepage) (let (next) (while (and (> n 0) (setq next (pdf-virtual-document-next-file pn))) (setq pn next) (cl-decf n))) (let (previous) (while (and (< n 0) (setq previous (pdf-virtual-document-previous-file pn))) (setq pn previous) (cl-incf n))) (when interactive-p (when (< n 0) (message "First file.")) (when (> n 0) (message "Last file."))) (pdf-view-goto-page pn) n)) (defun pdf-virtual-buffer-backward-file (&optional n interactive-p) (interactive "p\np") (pdf-virtual-buffer-forward-file (- (or n 1)) interactive-p)) ;; * ================================================================== * ;; * Helper functions ;; * ================================================================== * (defmacro pdf-virtual-dopages (bindings pages &rest body) (declare (indent 2) (debug (sexp form &rest form))) (let ((page (make-symbol "page"))) `(dolist (,page ,pages) (cl-destructuring-bind ,bindings ,page ,@body)))) (defun pdf-virtual--perform-search (string pages &optional regexp-p no-error) (let* ((pages (pdf-virtual-document-normalize-pages pages)) (file-pages (pdf-virtual-document-pages pages))) (pdf-info-compose-queries ((responses (pdf-virtual-dopages (filename pages _region) file-pages (if regexp-p (pdf-info-search-string string pages filename) ;; FIXME: no-error won't work with synchronous calls. (pdf-info-search-regexp string pages no-error filename))))) (let (result) (pdf-virtual-dopages (filename _ region) file-pages (let ((matches (pop responses))) (when region (setq matches (mapcar (lambda (m) (let-alist m `((edges . ,(pdf-util-edges-transform region .edges t)) ,@m))) (pdf-virtual--filter-edges region matches (apply-partially 'alist-get 'edges))))) (dolist (m matches) (push `((page . ,(pdf-virtual-document-page-of filename (alist-get 'page m) pages)) ,@m) result)))) (nreverse result))))) (defun pdf-virtual--filter-edges (region elts &optional edges-key-fn) (if (null region) elts (cl-remove-if-not (lambda (edges) (or (null edges) (if (consp (car edges)) (cl-some (apply-partially 'pdf-util-edges-intersection region) edges) (pdf-util-edges-intersection region edges)))) elts :key edges-key-fn))) (defun pdf-virtual--transform-goto-dest (link filename region) (let-alist link (let ((local-page (pdf-virtual-document-page-of filename .page))) (if local-page `((type . ,'goto-dest) (title . , .title) (page . ,local-page) (top . ,(car (pdf-util-edges-transform region (cons .top .top) t)))) `((type . ,'goto-remote) (title . , .title) (filename . ,filename) (page . , .page) (top . , .top)))))) ;; * ================================================================== * ;; * Server adapter ;; * ================================================================== * (defmacro pdf-virtual-define-adapter (name arglist &optional doc &rest body) ;; FIXME: Handle &optional + &rest argument. (declare (doc-string 3) (indent 2) (debug (&define name lambda-list [&optional stringp] def-body)) (font-lock defun) (imenu "Adapter")) (unless (stringp doc) (push doc body) (setq doc nil)) (let ((fn (intern (format "pdf-virtual-%s" name))) (base-fn (intern (format "pdf-info-%s" name))) (base-fn-arg (make-symbol "fn")) (true-file-or-buffer (make-symbol "true-file-or-buffer")) (args (cl-remove-if (lambda (elt) (memq elt '(&optional &rest))) arglist))) (unless (fboundp base-fn) (error "Base function is undefined: %s" base-fn)) (unless (memq 'file-or-buffer arglist) (error "Argument list is missing a `file-or-buffer' argument: %s" arglist)) `(progn (put ',fn 'definition-name ',name) (add-to-list 'pdf-virtual-adapter-alist ',(cons fn base-fn)) (defun ,fn ,(cons base-fn-arg arglist) ,(format "%sPDF virtual adapter to `%s'. This function delegates to `%s', unless the FILE-OR-BUFFER argument denotes a VPDF document." (if doc (concat doc "\n\n") "") base-fn base-fn) (let ((,true-file-or-buffer (cond ((or (bufferp file-or-buffer) (stringp file-or-buffer)) file-or-buffer) ((or (null file-or-buffer) ,(not (null (memq '&rest arglist)))) (current-buffer))))) (if (cond ((null ,true-file-or-buffer) t) ((bufferp ,true-file-or-buffer) (not (pdf-virtual-buffer-p ,true-file-or-buffer))) ((stringp ,true-file-or-buffer) (not (pdf-virtual-filename-p ,true-file-or-buffer)))) (,(if (memq '&rest arglist) 'apply 'funcall) ,base-fn-arg ,@args) (when (stringp ,true-file-or-buffer) (setq ,true-file-or-buffer (find-file-noselect ,true-file-or-buffer))) (save-current-buffer (when (bufferp ,true-file-or-buffer) (set-buffer ,true-file-or-buffer)) ,@body))))))) (define-error 'pdf-virtual-unsupported-operation "Operation not supported in VPDF buffer") (pdf-virtual-define-adapter open (&optional file-or-buffer password) (mapc (lambda (file) (pdf-info-open file password)) (pdf-virtual-document-filenames))) (pdf-virtual-define-adapter close (&optional file-or-buffer) (let ((files (cl-remove-if 'find-buffer-visiting (pdf-virtual-document-filenames)))) (pdf-info-compose-queries ((results (mapc 'pdf-info-close files))) (cl-some 'identity results)))) (pdf-virtual-define-adapter metadata (&optional file-or-buffer) (pdf-info-compose-queries ((md (mapc 'pdf-info-metadata (pdf-virtual-document-filenames)))) (apply 'cl-mapcar (lambda (&rest elts) (cons (caar elts) (cl-mapcar 'cdr elts))) md))) (pdf-virtual-define-adapter search-string (string &optional pages file-or-buffer) (pdf-virtual--perform-search string (pdf-virtual-document-normalize-pages pages))) (pdf-virtual-define-adapter search-regexp (pcre &optional pages no-error file-or-buffer) (pdf-virtual--perform-search pcre (pdf-virtual-document-normalize-pages pages) 'regexp no-error)) (pdf-virtual-define-adapter pagelinks (page &optional file-or-buffer) (cl-destructuring-bind (filename ext-page region) (pdf-virtual-document-page page) (pdf-info-compose-queries ((links (pdf-info-pagelinks ext-page filename))) (mapcar (lambda (link) (let-alist link (if (not (eq .type 'goto-dest)) link `((edges . ,(pdf-util-edges-transform region .edges t)) ,@(pdf-virtual--transform-goto-dest link filename region))))) (pdf-virtual--filter-edges region (car links) 'car))))) (pdf-virtual-define-adapter number-of-pages (&optional file-or-buffer) (pdf-info-compose-queries nil (pdf-virtual-document-number-of-pages))) (pdf-virtual-define-adapter outline (&optional file-or-buffer) (let ((files (pdf-virtual-document-filenames))) (pdf-info-compose-queries ((outlines (mapc 'pdf-info-outline files))) (cl-mapcan (lambda (outline filename) `(((depth . 1) (type . goto-dest) (title . ,filename) (page . ,(pdf-virtual-document-page-of filename)) (top . 0)) ,@(delq nil (mapcar (lambda (item) (let-alist item (if (not (eq .type 'goto-dest)) `((depth . ,(1+ .depth)) ,@item) (cl-check-type filename string) (let ((page (pdf-virtual-document-page-of filename .page))) (when page `((depth . ,(1+ .depth)) ,@(pdf-virtual--transform-goto-dest item filename (nth 2 (pdf-virtual-document-page page))))))))) outline)))) outlines files)))) (pdf-virtual-define-adapter gettext (page edges &optional selection-style file-or-buffer) (cl-destructuring-bind (filename file-page region) (pdf-virtual-document-page page) (let ((edges (pdf-util-edges-transform region edges))) (pdf-info-gettext file-page edges selection-style filename)))) (pdf-virtual-define-adapter getselection (page edges &optional selection-style file-or-buffer) (cl-destructuring-bind (filename file-page region) (pdf-virtual-document-page page) (let ((edges (pdf-util-edges-transform region edges))) (pdf-info-compose-queries ((results (pdf-info-getselection file-page edges selection-style filename))) (pdf-util-edges-transform region (pdf-virtual--filter-edges region (car results)) t))))) (pdf-virtual-define-adapter charlayout (page &optional edges-or-pos file-or-buffer) (cl-destructuring-bind (filename file-page region) (pdf-virtual-document-page page) (let ((edges-or-pos (pdf-util-edges-transform region edges-or-pos))) (pdf-info-compose-queries ((results (pdf-info-charlayout file-page edges-or-pos filename))) (mapcar (lambda (elt) `(,(car elt) . ,(pdf-util-edges-transform region (cdr elt) t))) (pdf-virtual--filter-edges region (car results) 'cadr)))))) (pdf-virtual-define-adapter pagesize (page &optional file-or-buffer) (cl-destructuring-bind (filename file-page region) (pdf-virtual-document-page page) (pdf-info-compose-queries ((result (pdf-info-pagesize file-page filename))) (if (null region) (car result) (pdf-util-with-edges (region) (pdf-util-scale (car result) (cons region-width region-height))))))) (pdf-virtual-define-adapter getannots (&optional pages file-or-buffer) (let* ((pages (pdf-virtual-document-normalize-pages pages)) (file-pages (pdf-virtual-document-pages pages))) (pdf-info-compose-queries ((annotations (pdf-virtual-dopages (filename file-pages _region) file-pages (pdf-info-getannots file-pages filename)))) (let ((page (car pages)) result) (pdf-virtual-dopages (_filename file-pages region) file-pages (dolist (a (pop annotations)) (let ((edges (delq nil `(,(cdr (assq 'edges a)) ,@(cdr (assq 'markup-edges a)))))) (when (pdf-virtual--filter-edges region edges) (let-alist a (setcdr (assq 'page a) (+ page (- .page (car file-pages)))) (setcdr (assq 'id a) (intern (format "%s/%d" .id (cdr (assq 'page a))))) (when region (when .edges (setcdr (assq 'edges a) (pdf-util-edges-transform region .edges t))) (when .markup-edges (setcdr (assq 'markup-edges a) (pdf-util-edges-transform region .markup-edges t)))) (push a result))))) (cl-incf page (1+ (- (cdr file-pages) (car file-pages))))) (nreverse result))))) (pdf-virtual-define-adapter getannot (id &optional file-or-buffer) (let ((name (symbol-name id)) page) (save-match-data (when (string-match "\\(.*\\)/\\([0-9]+\\)\\'" name) (setq id (intern (match-string 1 name)) page (string-to-number (match-string 2 name))))) (if page (cl-destructuring-bind (filename _ _) (pdf-virtual-document-page page) (pdf-info-compose-queries ((result (pdf-info-getannot id filename))) (let ((a (car result))) (cl-destructuring-bind (_ _ region) (pdf-virtual-document-page page) (setcdr (assq 'page a) page) (let-alist a (setcdr (assq 'id a) (intern (format "%s/%d" .id (cdr (assq 'page a))))) (when region (when .edges (setcdr (assq 'edges a) (pdf-util-edges-transform region .edges t))) (when .markup-edges (setcdr (assq 'markup-edges a) (pdf-util-edges-transform region .markup-edges t)))))) a))) (pdf-info-compose-queries nil (error "No such annotation: %s" id))))) (pdf-virtual-define-adapter addannot (page edges type &optional file-or-buffer &rest markup-edges) (signal 'pdf-virtual-unsupported-operation (list 'addannot))) (pdf-virtual-define-adapter delannot (id &optional file-or-buffer) (signal 'pdf-virtual-unsupported-operation (list 'delannot))) (pdf-virtual-define-adapter mvannot (id edges &optional file-or-buffer) (signal 'pdf-virtual-unsupported-operation (list 'mvannot))) (pdf-virtual-define-adapter editannot (id modifications &optional file-or-buffer) (signal 'pdf-virtual-unsupported-operation (list 'editannot))) (pdf-virtual-define-adapter save (&optional file-or-buffer) (signal 'pdf-virtual-unsupported-operation (list 'save))) ;;(defvar-local pdf-virtual-annotation-mapping nil) (pdf-virtual-define-adapter getattachment-from-annot (id &optional do-save file-or-buffer) (let ((name (symbol-name id)) page) (save-match-data (when (string-match "\\(.*\\)/\\([0-9]+\\)\\'" name) (setq id (intern (match-string 1 name)) page (string-to-number (match-string 2 name))))) (if page (cl-destructuring-bind (filename _ _) (pdf-virtual-document-page page) (pdf-info-getattachment-from-annot id do-save filename)) (pdf-info-compose-queries nil (error "No such annotation: %s" id))))) (pdf-virtual-define-adapter getattachments (&optional do-save file-or-buffer) (pdf-info-compose-queries ((results (mapc (lambda (f) (pdf-info-getattachments do-save f)) (pdf-virtual-document-filenames)))) (apply 'append results))) (pdf-virtual-define-adapter synctex-forward-search (source &optional line column file-or-buffer) (signal 'pdf-virtual-unsupported-operation (list 'synctex-forward-search))) (pdf-virtual-define-adapter synctex-backward-search (page &optional x y file-or-buffer) (cl-destructuring-bind (filename file-page region) (pdf-virtual-document-page page) (cl-destructuring-bind (x &rest y) (pdf-util-edges-transform region (cons x y)) (pdf-info-synctex-backward-search file-page x y filename)))) (pdf-virtual-define-adapter renderpage (page width &optional file-or-buffer &rest commands) (when (keywordp file-or-buffer) (push file-or-buffer commands) (setq file-or-buffer nil)) (cl-destructuring-bind (filename file-page region) (pdf-virtual-document-page page) (when region (setq commands (append (list :crop-to region) commands) width (pdf-util-with-edges (region) (round (* width (max 1 (/ 1.0 (max 1e-6 region-width)))))))) (apply 'pdf-info-renderpage file-page width filename commands))) (pdf-virtual-define-adapter boundingbox (page &optional file-or-buffer) (cl-destructuring-bind (filename file-page region) (pdf-virtual-document-page page) (pdf-info-compose-queries ((results (unless region (pdf-info-boundingbox file-page filename)))) (if region (list 0 0 1 1) (car results))))) (pdf-virtual-define-adapter pagelabels (&optional file-or-buffer) (signal 'pdf-virtual-unsupported-operation (list 'pagelabels))) (pdf-virtual-define-adapter setoptions (&optional file-or-buffer &rest options) (when (keywordp file-or-buffer) (push file-or-buffer options) (setq file-or-buffer nil)) (pdf-info-compose-queries ((_ (dolist (f (pdf-virtual-document-filenames)) (apply 'pdf-info-setoptions f options)))) nil)) (pdf-virtual-define-adapter getoptions (&optional file-or-buffer) (signal 'pdf-virtual-unsupported-operation (list 'getoptions))) (pdf-virtual-define-adapter encrypted-p (&optional file-or-buffer) nil) (provide 'pdf-virtual) ;;; pdf-virtual.el ends here pdf-tools-0.80/server/000077500000000000000000000000001315513274600146655ustar00rootroot00000000000000pdf-tools-0.80/server/.gitignore000066400000000000000000000004061315513274600166550ustar00rootroot00000000000000*.o .deps/ Makefile Makefile.in aclocal.m4 autom4te.cache/ config.h config.h.in config.log config.status configure depcomp epdfinfo install-sh libsynctex.a missing stamp-h1 ar-lib compile config.h.in~ .clang_complete callgrind.out.* config.guess config.sub TAGS pdf-tools-0.80/server/Makefile.am000066400000000000000000000013441315513274600167230ustar00rootroot00000000000000bin_PROGRAMS = epdfinfo epdfinfo_CFLAGS = -Wall $(glib_CFLAGS) $(poppler_glib_CFLAGS) $(poppler_CFLAGS) \ $(png_CFLAGS) epdfinfo_CXXFLAGS = -Wall $(epdfinfo_CFLAGS) epdfinfo_LDADD = $(glib_LIBS) $(poppler_glib_LIBS) $(poppler_LIBS) \ $(png_LIBS) libsynctex.a $(zlib_LIBS) epdfinfo_SOURCES = epdfinfo.c epdfinfo.h poppler-hack.cc noinst_LIBRARIES = libsynctex.a libsynctex_a_SOURCES = synctex_parser.c synctex_parser_utils.c synctex_parser.h \ synctex_parser_local.h synctex_parser_utils.h libsynctex_a_CFLAGS = -w $(zlib_CFLAGS) GITCLEANFILES = Makefile.in aclocal.m4 config.h.in \ configure depcomp install-sh missing autom4te.cache \ $(PACKAGE_NAME)-$(PACKAGE_VERSION).tar.gz distclean-local: rm -rf -- $(GITCLEANFILES) pdf-tools-0.80/server/autogen.sh000077500000000000000000000000671315513274600166710ustar00rootroot00000000000000#!/bin/sh echo "Running autoreconf..." autoreconf -i pdf-tools-0.80/server/configure.ac000066400000000000000000000063671315513274600171670ustar00rootroot00000000000000# -*- Autoconf -*- # Process this file with autoconf to produce a configure script. AC_PREREQ([2.67]) AC_INIT([epdfinfo], 0.80, [politza@fh-trier.de]) AM_INIT_AUTOMAKE([-Wall -Wno-override foreign silent-rules]) AC_CONFIG_SRCDIR([epdfinfo.h]) AC_CONFIG_HEADERS([config.h]) # Checks for programs. AC_PROG_CC AM_PROG_CC_C_O AC_PROG_CXX AC_PROG_RANLIB AM_PROG_AR # Checks for libraries. HAVE_POPPLER_FIND_OPTS="no (requires poppler-glib >= 0.22)" HAVE_POPPLER_ANNOT_WRITE="no (requires poppler-glib >= 0.19.4)" HAVE_POPPLER_ANNOT_MARKUP="no (requires poppler-glib >= 0.26)" PKG_CHECK_MODULES([png], [libpng]) PKG_CHECK_MODULES([glib], [glib-2.0]) PKG_CHECK_MODULES([poppler], [poppler]) PKG_CHECK_MODULES([poppler_glib], [poppler-glib >= 0.16.0]) PKG_CHECK_EXISTS([poppler-glib >= 0.19.4], [HAVE_POPPLER_ANNOT_WRITE=yes]) PKG_CHECK_EXISTS([poppler-glib >= 0.22], [HAVE_POPPLER_FIND_OPTS=yes]) PKG_CHECK_EXISTS([poppler-glib >= 0.26], [HAVE_POPPLER_ANNOT_MARKUP=yes]) PKG_CHECK_MODULES([zlib], [zlib]) # glib won't work properly on msys2 without it. if test "$MSYSTEM" = MINGW32 -o "$MSYSTEM" = MINGW64; then CFLAGS="-D__USE_MINGW_ANSI_STDIO=1 $CFLAGS" fi SAVED_CPPFLAGS=$CPPFLAGS CPPFLAGS=$poppler_CFLAGS AC_LANG_PUSH([C++]) # Check if we can use the -std=c++11 option. m4_include([m4/ax_check_compile_flag.m4]) AX_CHECK_COMPILE_FLAG([-std=c++11], [HAVE_STD_CXX11=yes]) if test "$HAVE_STD_CXX11" = yes; then CXXFLAGS="-std=c++11 $CXXFLAGS" fi # Check for private poppler header. AC_CHECK_HEADERS([Annot.h PDFDocEncoding.h], [], AC_MSG_ERROR([cannot find necessary poppler-private header (see README.org)])) AC_LANG_POP([C++]) CPPFLAGS=$SAVED_CPPFLAGS # Setup compile time features. if test "$HAVE_POPPLER_FIND_OPTS" = yes; then AC_DEFINE([HAVE_POPPLER_FIND_OPTS],1, [Define to 1 to enable case sensitive searching (requires poppler-glib >= 0.22).]) fi if test "$HAVE_POPPLER_ANNOT_WRITE" = yes; then AC_DEFINE([HAVE_POPPLER_ANNOT_WRITE],1, [Define to 1 to enable writing of annotations (requires poppler-glib >= 0.19.4).]) fi if test "$HAVE_POPPLER_ANNOT_MARKUP" = yes; then AC_DEFINE([HAVE_POPPLER_ANNOT_MARKUP],1, [Define to 1 to enable adding of markup annotations (requires poppler-glib >= 0.26).]) fi AC_CANONICAL_HOST # Checks for header files. AC_CHECK_HEADERS([stdlib.h string.h strings.h err.h]) AC_MSG_CHECKING([for error.h]) SAVED_CFLAGS=$CFLAGS CFLAGS="$poppler_CFLAGS $poppler_glib_CFLAGS" AC_LANG_PUSH([C]) AC_COMPILE_IFELSE([AC_LANG_PROGRAM([ #include ],[error (0, 0, "");])], [AC_DEFINE([HAVE_ERROR_H],1, [Define to 1 if error.h is useable.]) AC_MSG_RESULT([yes])], AC_MSG_RESULT([no])) AC_LANG_POP([C]) CFLAGS=$SAVED_CFLAGS # Checks for typedefs, structures, and compiler characteristics. AC_TYPE_SIZE_T AC_TYPE_SSIZE_T AC_CHECK_TYPES([ptrdiff_t]) AC_C_BIGENDIAN # Checks for library functions. AC_FUNC_ERROR_AT_LINE AC_FUNC_STRTOD AC_CHECK_FUNCS([strcspn strtol getline]) AC_CONFIG_FILES([Makefile]) AC_OUTPUT echo echo "Is case-sensitive searching enabled ? ${HAVE_POPPLER_FIND_OPTS}" echo "Is modifying text annotations enabled ? ${HAVE_POPPLER_ANNOT_WRITE}" echo "Is modifying markup annotations enabled ? ${HAVE_POPPLER_ANNOT_MARKUP}" echo pdf-tools-0.80/server/epdfinfo.c000066400000000000000000003034041315513274600166270ustar00rootroot00000000000000/* Copyright (C) 2013, 2014 Andreas Politz * * Author: Andreas Politz * * 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 . */ #include #include #ifdef HAVE_ERR_H # include #endif #ifdef HAVE_ERROR_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "synctex_parser.h" #include "epdfinfo.h" /* ================================================================== * * Helper Functions * ================================================================== */ #ifndef HAVE_ERR_H /** * Print error message and quit. * * @param eval Return code * @param fmt Formatting string */ static void err(int eval, const char *fmt, ...) { va_list args; fprintf (stderr, "epdfinfo: "); if (fmt != NULL) { va_start (args, fmt); vfprintf (stderr, fmt, args); va_end (args); fprintf (stderr, ": %s\n", strerror(errno)); } else { fprintf (stderr, "\n"); } fflush (stderr); exit (eval); } #endif #ifndef HAVE_GETLINE /** * Read one line from a file. * * @param lineptr Pointer to malloc() allocated buffer * @param n Pointer to size of buffer * @param stream File pointer to read from */ static ssize_t getline(char **lineptr, size_t *n, FILE *stream) { size_t len = 0; int ch; if ((lineptr == NULL) || (n == NULL)) { errno = EINVAL; return -1; } if (*lineptr == NULL) { *lineptr = malloc (128); *n = 128; } while ((ch = fgetc (stream)) != EOF) { (*lineptr)[len] = ch; if (++len >= *n) { *n += 128; *lineptr = realloc (*lineptr, *n); } if (ch == '\n') break; } (*lineptr)[len] = '\0'; if (!len) { len = -1; } return len; } #endif /** * Free a list of command arguments. * * @param args An array of command arguments. * @param n The length of the array. */ static void free_command_args (command_arg_t *args, size_t n) { if (! args) return; g_free (args); } /** * Free resources held by document. * * @param doc The document to be freed. */ static void free_document (document_t *doc) { if (! doc) return; g_free (doc->filename); g_free (doc->passwd); if (doc->annotations.pages) { int npages = poppler_document_get_n_pages (doc->pdf); int i; for (i = 0; i < npages; ++i) { GList *item; GList *annots = doc->annotations.pages[i]; for (item = annots; item; item = item->next) { annotation_t *a = (annotation_t*) item->data; poppler_annot_mapping_free(a->amap); g_free (a->key); g_free (a); } g_list_free (annots); } g_hash_table_destroy (doc->annotations.keys); g_free (doc->annotations.pages); } g_object_unref (doc->pdf); g_free (doc); } /** * Parse a list of whitespace separated double values. * * @param str The input string. * @param values[out] Values are put here. * @param nvalues How many values to parse. * * @return TRUE, if str contained exactly nvalues, else FALSE. */ static gboolean parse_double_list (const char *str, gdouble *values, size_t nvalues) { char *end; int i; if (! str) return FALSE; errno = 0; for (i = 0; i < nvalues; ++i) { gdouble n = g_ascii_strtod (str, &end); if (str == end || errno) return FALSE; values[i] = n; str = end; } if (*end) return FALSE; return TRUE; } static gboolean parse_rectangle (const char *str, PopplerRectangle *r) { gdouble values[4]; if (! r) return FALSE; if (! parse_double_list (str, values, 4)) return FALSE; r->x1 = values[0]; r->y1 = values[1]; r->x2 = values[2]; r->y2 = values[3]; return TRUE; } static gboolean parse_edges_or_position (const char *str, PopplerRectangle *r) { return (parse_rectangle (str, r) && r->x1 >= 0 && r->x1 <= 1 && r->x2 <= 1 && r->y1 >= 0 && r->y1 <= 1 && r->y2 <= 1); } static gboolean parse_edges (const char *str, PopplerRectangle *r) { return (parse_rectangle (str, r) && r->x1 >= 0 && r->x1 <= 1 && r->x2 >= 0 && r->x2 <= 1 && r->y1 >= 0 && r->y1 <= 1 && r->y2 >= 0 && r->y2 <= 1); } /** * Print a string properly escaped for a response. * * @param str The string to be printed. * @param suffix_char Append a newline if NEWLINE, a colon if COLON. */ static void print_response_string (const char *str, enum suffix_char suffix) { if (str) { while (*str) { switch (*str) { case '\n': printf ("\\n"); break; case '\\': printf ("\\\\"); break; case ':': printf ("\\:"); break; default: putchar (*str); } ++str; } } switch (suffix) { case NEWLINE: putchar ('\n'); break; case COLON: putchar (':'); break; default: ; } } /** * Print a formatted error response. * * @param fmt The printf-like format string. */ static void printf_error_response (const char *fmt, ...) { va_list va; puts ("ERR"); va_start (va, fmt); vprintf (fmt, va); va_end (va); puts ("\n."); fflush (stdout); } /** * Remove one trailing newline character. Does nothing, if str does * not end with a newline. * * @param str The string. * * @return str with trailing newline removed. */ static char* strchomp (char *str) { size_t length; if (! str) return str; length = strlen (str); if (str[length - 1] == '\n') str[length - 1] = '\0'; return str; } /** * Create a new, temporary file and returns it's name. * * @return The filename. */ static char* mktempfile() { char *filename = NULL; int tries = 3; while (! filename && tries-- > 0) { filename = tempnam(NULL, "epdfinfo"); if (filename) { int fd = open(filename, O_CREAT | O_EXCL | O_RDONLY, S_IRWXU); if (fd > 0) close (fd); else { free (filename); filename = NULL; } } } if (! filename) fprintf (stderr, "Unable to create tempfile"); return filename; } static void image_recolor (cairo_surface_t * surface, const PopplerColor * fg, const PopplerColor * bg) { /* uses a representation of a rgb color as follows: - a lightness scalar (between 0,1), which is a weighted average of r, g, b, - a hue vector, which indicates a radian direction from the grey axis, inside the equal lightness plane. - a saturation scalar between 0,1. It is 0 when grey, 1 when the color is in the boundary of the rgb cube. */ const unsigned int page_width = cairo_image_surface_get_width (surface); const unsigned int page_height = cairo_image_surface_get_height (surface); const int rowstride = cairo_image_surface_get_stride (surface); unsigned char *image = cairo_image_surface_get_data (surface); /* RGB weights for computing lightness. Must sum to one */ static const double a[] = { 0.30, 0.59, 0.11 }; const double f = 65535.; const double rgb_fg[] = { fg->red / f, fg->green / f, fg->blue / f }; const double rgb_bg[] = { bg->red / f, bg->green / f, bg->blue / f }; const double rgb_diff[] = { rgb_bg[0] - rgb_fg[0], rgb_bg[1] - rgb_fg[1], rgb_bg[2] - rgb_fg[2] }; unsigned int y; for (y = 0; y < page_height * rowstride; y += rowstride) { unsigned char *data = image + y; unsigned int x; for (x = 0; x < page_width; x++, data += 4) { /* Careful. data color components blue, green, red. */ const double rgb[3] = { (double) data[2] / 256., (double) data[1] / 256., (double) data[0] / 256. }; /* compute h, s, l data */ double l = a[0] * rgb[0] + a[1] * rgb[1] + a[2] * rgb[2]; /* linear interpolation between dark and light with color ligtness as * a parameter */ data[2] = (unsigned char) round (255. * (l * rgb_diff[0] + rgb_fg[0])); data[1] = (unsigned char) round (255. * (l * rgb_diff[1] + rgb_fg[1])); data[0] = (unsigned char) round (255. * (l * rgb_diff[2] + rgb_fg[2])); } } } /** * Render a PDF page. * * @param pdf The PDF document. * @param page The page to be rendered. * @param width The desired width of the image. * * @return A cairo_t context encapsulating the rendered image, or * NULL, if rendering failed for some reason. */ static cairo_surface_t* image_render_page(PopplerDocument *pdf, PopplerPage *page, int width, gboolean do_render_annotaions, const render_options_t *options) { cairo_t *cr = NULL; cairo_surface_t *surface = NULL; double pt_width, pt_height; int height; double scale = 1; if (! page || ! pdf) return NULL; if (width < 1) width = 1; poppler_page_get_size (page, &pt_width, &pt_height); scale = width / pt_width; height = (int) ((scale * pt_height) + 0.5); surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS) { fprintf (stderr, "Failed to create cairo surface\n"); goto error; } cr = cairo_create (surface); if (cairo_status(cr) != CAIRO_STATUS_SUCCESS) { fprintf (stderr, "Failed to create cairo handle\n"); goto error; } cairo_translate (cr, 0, 0); cairo_scale (cr, scale, scale); /* Render w/o annotations. */ if (! do_render_annotaions || (options && options->printed)) poppler_page_render_for_printing_with_options (page, cr, POPPLER_PRINT_DOCUMENT); else poppler_page_render (page, cr) ; if (cairo_status(cr) != CAIRO_STATUS_SUCCESS) { fprintf (stderr, "Failed to render page\n"); goto error; } /* This makes the colors look right. */ cairo_set_operator (cr, CAIRO_OPERATOR_DEST_OVER); cairo_set_source_rgb (cr, 1., 1., 1.); cairo_paint (cr); if (options && options->usecolors) image_recolor (surface, &options->fg, &options->bg); cairo_destroy (cr); return surface; error: if (surface != NULL) cairo_surface_destroy (surface); if (cr != NULL) cairo_destroy (cr); return NULL; } /** * Write an image to a filename. * * @param cr The cairo context encapsulating the image. * @param filename The filename to be written to. * @param type The desired image type. * * @return 1 if the image was written successfully, else 0. */ static gboolean image_write (cairo_surface_t *surface, const char *filename, enum image_type type) { int i, j; unsigned char *data; int width, height; FILE *file = NULL; gboolean success = 0; if (! surface || cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) { fprintf (stderr, "Invalid cairo surface\n"); return 0; } if (! (file = fopen (filename, "wb"))) { fprintf (stderr, "Can not open file: %s\n", filename); return 0; } cairo_surface_flush (surface); width = cairo_image_surface_get_width (surface); height = cairo_image_surface_get_height (surface); data = cairo_image_surface_get_data (surface); switch (type) { case PPM: { unsigned char *buffer = g_malloc (width * height * 3); unsigned char *buffer_p = buffer; fprintf (file, "P6\n%d %d\n255\n", width, height); for (i = 0; i < width * height; ++i, data += 4, buffer_p += 3) ARGB_TO_RGB (buffer_p, data); fwrite (buffer, 1, width * height * 3, file); g_free (buffer); success = 1; } break; case PNG: { png_infop info_ptr = NULL; png_structp png_ptr = NULL; unsigned char *row = NULL; png_ptr = png_create_write_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (!png_ptr) goto finalize; info_ptr = png_create_info_struct(png_ptr); if (!info_ptr) goto finalize; if (setjmp(png_jmpbuf(png_ptr))) goto finalize; png_init_io (png_ptr, file); png_set_compression_level (png_ptr, 1); png_set_IHDR (png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_DEFAULT); png_set_filter (png_ptr, PNG_FILTER_TYPE_BASE, PNG_FILTER_NONE); png_write_info (png_ptr, info_ptr); row = g_malloc (3 * width); for (i = 0; i < height; ++i) { unsigned char *row_p = row; for (j = 0; j < width; ++j, data += 4, row_p += 3) { ARGB_TO_RGB (row_p, data); } png_write_row (png_ptr, row); } png_write_end (png_ptr, NULL); success = 1; finalize: if (png_ptr) png_destroy_write_struct (&png_ptr, &info_ptr); if (row) g_free (row); if (! success) fprintf (stderr, "Error writing png data\n"); } break; default: internal_error ("switch fell through"); } fclose (file); return success; } static void image_write_print_response(cairo_surface_t *surface, enum image_type type) { char *filename = mktempfile (); perror_if_not (filename, "Unable to create temporary file"); if (image_write (surface, filename, type)) { OK_BEGIN (); print_response_string (filename, NEWLINE); OK_END (); } else { printf_error_response ("Unable to write image"); } free (filename); error: return; } static void region_print (cairo_region_t *region, double width, double height) { int i; for (i = 0; i < cairo_region_num_rectangles (region); ++i) { cairo_rectangle_int_t r; cairo_region_get_rectangle (region, i, &r); printf ("%f %f %f %f", r.x / width, r.y / height, (r.x + r.width) / width, (r.y + r.height) / height); if (i < cairo_region_num_rectangles (region) - 1) putchar (':'); } if (0 == cairo_region_num_rectangles (region)) printf ("0.0 0.0 0.0 0.0"); } /** * Return a string representation of a PopplerActionType. * * @param type The PopplerActionType. * * @return It's string representation. */ static const char * xpoppler_action_type_string(PopplerActionType type) { switch (type) { case POPPLER_ACTION_UNKNOWN: return "unknown"; case POPPLER_ACTION_NONE: return "none"; case POPPLER_ACTION_GOTO_DEST: return "goto-dest"; case POPPLER_ACTION_GOTO_REMOTE: return "goto-remote"; case POPPLER_ACTION_LAUNCH: return "launch"; case POPPLER_ACTION_URI: return "uri"; case POPPLER_ACTION_NAMED: return "goto-dest"; /* actually "named" */ case POPPLER_ACTION_MOVIE: return "movie"; case POPPLER_ACTION_RENDITION: return "rendition"; case POPPLER_ACTION_OCG_STATE: return "ocg-state"; case POPPLER_ACTION_JAVASCRIPT: return "javascript"; default: return "invalid"; } } /** * Return a string representation of a PopplerAnnotType. * * @param type The PopplerAnnotType. * * @return It's string representation. */ static const char * xpoppler_annot_type_string (PopplerAnnotType type) { switch (type) { case POPPLER_ANNOT_UNKNOWN: return "unknown"; case POPPLER_ANNOT_TEXT: return "text"; case POPPLER_ANNOT_LINK: return "link"; case POPPLER_ANNOT_FREE_TEXT: return "free-text"; case POPPLER_ANNOT_LINE: return "line"; case POPPLER_ANNOT_SQUARE: return "square"; case POPPLER_ANNOT_CIRCLE: return "circle"; case POPPLER_ANNOT_POLYGON: return "polygon"; case POPPLER_ANNOT_POLY_LINE: return "poly-line"; case POPPLER_ANNOT_HIGHLIGHT: return "highlight"; case POPPLER_ANNOT_UNDERLINE: return "underline"; case POPPLER_ANNOT_SQUIGGLY: return "squiggly"; case POPPLER_ANNOT_STRIKE_OUT: return "strike-out"; case POPPLER_ANNOT_STAMP: return "stamp"; case POPPLER_ANNOT_CARET: return "caret"; case POPPLER_ANNOT_INK: return "ink"; case POPPLER_ANNOT_POPUP: return "popup"; case POPPLER_ANNOT_FILE_ATTACHMENT: return "file"; case POPPLER_ANNOT_SOUND: return "sound"; case POPPLER_ANNOT_MOVIE: return "movie"; case POPPLER_ANNOT_WIDGET: return "widget"; case POPPLER_ANNOT_SCREEN: return "screen"; case POPPLER_ANNOT_PRINTER_MARK: return "printer-mark"; case POPPLER_ANNOT_TRAP_NET: return "trap-net"; case POPPLER_ANNOT_WATERMARK: return "watermark"; case POPPLER_ANNOT_3D: return "3d"; default: return "invalid"; } } /** * Return a string representation of a PopplerAnnotTextState. * * @param type The PopplerAnnotTextState. * * @return It's string representation. */ static const char * xpoppler_annot_text_state_string (PopplerAnnotTextState state) { switch (state) { case POPPLER_ANNOT_TEXT_STATE_MARKED: return "marked"; case POPPLER_ANNOT_TEXT_STATE_UNMARKED: return "unmarked"; case POPPLER_ANNOT_TEXT_STATE_ACCEPTED: return "accepted"; case POPPLER_ANNOT_TEXT_STATE_REJECTED: return "rejected"; case POPPLER_ANNOT_TEXT_STATE_CANCELLED: return "cancelled"; case POPPLER_ANNOT_TEXT_STATE_COMPLETED: return "completed"; case POPPLER_ANNOT_TEXT_STATE_NONE: return "none"; case POPPLER_ANNOT_TEXT_STATE_UNKNOWN: default: return "unknown"; } }; static document_t* document_open (const epdfinfo_t *ctx, const char *filename, const char *passwd, GError **gerror) { char *uri; document_t *doc = g_hash_table_lookup (ctx->documents, filename); if (NULL != doc) return doc; doc = g_malloc0(sizeof (document_t)); uri = g_filename_to_uri (filename, NULL, gerror); if (uri != NULL) doc->pdf = poppler_document_new_from_file(uri, passwd, gerror); if (NULL == doc->pdf) { g_free (doc); doc = NULL; } else { doc->filename = g_strdup (filename); doc->passwd = g_strdup (passwd); g_hash_table_insert (ctx->documents, doc->filename, doc); } g_free (uri); return doc; } /** * Split command args into a list of strings. * * @param args The colon separated list of arguments. * @param nargs[out] The number of returned arguments. * * @return The list of arguments, which should be freed by the caller. */ static char ** command_arg_split (const char *args, int *nargs) { char **list = g_malloc (sizeof (char*) * 16); int i = 0; size_t allocated = 16; char *buffer = NULL; gboolean last = FALSE; if (! args) goto theend; buffer = g_malloc (strlen (args) + 1); while (*args || last) { gboolean esc = FALSE; char *buffer_p = buffer; while (*args && (*args != ':' || esc)) { if (esc) { if (*args == 'n') { ++args; *buffer_p++ = '\n'; } else { *buffer_p++ = *args++; } esc = FALSE; } else if (*args == '\\') { ++args; esc = TRUE; } else { *buffer_p++ = *args++; } } *buffer_p = '\0'; if (i >= allocated) { allocated = 2 * allocated + 1; list = g_realloc (list, sizeof (char*) * allocated); } list[i++] = g_strdup (buffer); last = FALSE; if (*args) { ++args; if (! *args) last = TRUE; } } theend: g_free (buffer); *nargs = i; return list; } static gboolean command_arg_parse_arg (const epdfinfo_t *ctx, const char *arg, command_arg_t *cmd_arg, command_arg_type_t type, gchar **error_msg) { GError *gerror = NULL; if (! arg || !cmd_arg) return FALSE; switch (type) { case ARG_DOC: { document_t *doc = document_open (ctx, arg, NULL, &gerror); cerror_if_not (doc, error_msg, "Error opening %s:%s", arg, gerror ? gerror->message : "Unknown reason"); cmd_arg->value.doc = doc; break; } case ARG_BOOL: cerror_if_not (! strcmp (arg, "0") || ! strcmp (arg, "1"), error_msg, "Expected 0 or 1:%s", arg); cmd_arg->value.flag = *arg == '1'; break; case ARG_NONEMPTY_STRING: cerror_if_not (*arg, error_msg, "Non-empty string expected"); /* fall through */ case ARG_STRING: cmd_arg->value.string = arg; break; case ARG_NATNUM: { char *endptr; long n = strtol (arg, &endptr, 0); cerror_if_not (! (*endptr || (n < 0)), error_msg, "Expected natural number:%s", arg); cmd_arg->value.natnum = n; } break; case ARG_EDGES_OR_POSITION: { PopplerRectangle *r = &cmd_arg->value.rectangle; cerror_if_not (parse_edges_or_position (arg, r), error_msg, "Expected a relative position or rectangle: %s", arg); } break; case ARG_EDGES: { PopplerRectangle *r = &cmd_arg->value.rectangle; cerror_if_not (parse_edges (arg, r), error_msg, "Expected a relative rectangle: %s", arg); } break; case ARG_EDGE_OR_NEGATIVE: case ARG_EDGE: { char *endptr; double n = strtod (arg, &endptr); cerror_if_not (! (*endptr || (type != ARG_EDGE_OR_NEGATIVE && n < 0.0) || n > 1.0), error_msg, "Expected a relative edge: %s", arg); cmd_arg->value.edge = n; } break; case ARG_COLOR: { guint r,g,b; cerror_if_not ((strlen (arg) == 7 && 3 == sscanf (arg, "#%2x%2x%2x", &r, &g, &b)), error_msg, "Invalid color: %s", arg); cmd_arg->value.color.red = r << 8; cmd_arg->value.color.green = g << 8; cmd_arg->value.color.blue = b << 8; } break; case ARG_INVALID: default: internal_error ("switch fell through"); } cmd_arg->type = type; return TRUE; error: if (gerror) { g_error_free (gerror); gerror = NULL; } return FALSE; } /** * Parse arguments for a command. * * @param ctx The epdfinfo context. * @param args A string holding the arguments. This is either empty * or the suffix of the command starting at the first * colon after the command name. * @param len The length of args. * @param cmd The command for which the arguments should be parsed. * * @return */ static command_arg_t* command_arg_parse(epdfinfo_t *ctx, char **args, int nargs, const command_t *cmd, gchar **error_msg) { command_arg_t *cmd_args = g_malloc0 (cmd->nargs * sizeof (command_arg_t)); int i; if (nargs < cmd->nargs - 1 || (nargs == cmd->nargs - 1 && cmd->args_spec[cmd->nargs - 1] != ARG_REST) || (nargs > cmd->nargs && (cmd->nargs == 0 || cmd->args_spec[cmd->nargs - 1] != ARG_REST))) { if (error_msg) { *error_msg = g_strdup_printf ("Command `%s' expects %d argument(s), %d given", cmd->name, cmd->nargs, nargs); } goto failure; } for (i = 0; i < cmd->nargs; ++i) { if (i == cmd->nargs - 1 && cmd->args_spec[i] == ARG_REST) { cmd_args[i].value.rest.args = args + i; cmd_args[i].value.rest.nargs = nargs - i; cmd_args[i].type = ARG_REST; } else if (i >= nargs || ! command_arg_parse_arg (ctx, args[i], cmd_args + i, cmd->args_spec[i], error_msg)) { goto failure; } } return cmd_args; failure: free_command_args (cmd_args, cmd->nargs); return NULL; } static void command_arg_print(const command_arg_t *arg) { switch (arg->type) { case ARG_INVALID: printf ("[invalid]"); break; case ARG_DOC: print_response_string (arg->value.doc->filename, NONE); break; case ARG_BOOL: printf ("%d", arg->value.flag ? 1 : 0); break; case ARG_NONEMPTY_STRING: /* fall */ case ARG_STRING: print_response_string (arg->value.string, NONE); break; case ARG_NATNUM: printf ("%ld", arg->value.natnum); break; case ARG_EDGE_OR_NEGATIVE: /* fall */ case ARG_EDGE: printf ("%f", arg->value.edge); break; case ARG_EDGES_OR_POSITION: /* fall */ case ARG_EDGES: { const PopplerRectangle *r = &arg->value.rectangle; if (r->x2 < 0 && r->y2 < 0) printf ("%f %f", r->x1, r->y1); else printf ("%f %f %f %f", r->x1, r->y1, r->x2, r->y2); break; } case ARG_COLOR: { const PopplerColor *c = &arg->value.color; printf ("#%.2x%.2x%.2x", c->red >> 8, c->green >> 8, c->blue >> 8); break; } case ARG_REST: { int i; for (i = 0; i < arg->value.rest.nargs; ++i) print_response_string (arg->value.rest.args[i], COLON); if (arg->value.rest.nargs > 0) print_response_string (arg->value.rest.args[i], NONE); break; } default: internal_error ("switch fell through"); } } static size_t command_arg_type_size(command_arg_type_t type) { command_arg_t arg; switch (type) { case ARG_INVALID: return 0; case ARG_DOC: return sizeof (arg.value.doc); case ARG_BOOL: return sizeof (arg.value.flag); case ARG_NONEMPTY_STRING: /* fall */ case ARG_STRING: return sizeof (arg.value.string); case ARG_NATNUM: return sizeof (arg.value.natnum); case ARG_EDGE_OR_NEGATIVE: /* fall */ case ARG_EDGE: return sizeof (arg.value.edge); case ARG_EDGES_OR_POSITION: /* fall */ case ARG_EDGES: return sizeof (arg.value.rectangle); case ARG_COLOR: return sizeof (arg.value.color); case ARG_REST: return sizeof (arg.value.rest); default: internal_error ("switch fell through"); return 0; } } /* ------------------------------------------------------------------ * * PDF Actions * ------------------------------------------------------------------ */ static gboolean action_is_handled (PopplerAction *action) { if (! action) return FALSE; switch (action->any.type) { case POPPLER_ACTION_GOTO_REMOTE: case POPPLER_ACTION_GOTO_DEST: case POPPLER_ACTION_NAMED: /* case POPPLER_ACTION_LAUNCH: */ case POPPLER_ACTION_URI: return TRUE; default: ; } return FALSE; } static void action_print_destination (PopplerDocument *doc, PopplerAction *action) { PopplerDest *dest = NULL; gboolean free_dest = FALSE; double width, height, top; PopplerPage *page; int saved_stdin; if (action->any.type == POPPLER_ACTION_GOTO_DEST && action->goto_dest.dest->type == POPPLER_DEST_NAMED) { DISCARD_STDOUT (saved_stdin); /* poppler_document_find_dest reports errors to stdout, so discard them. */ dest = poppler_document_find_dest (doc, action->goto_dest.dest->named_dest); UNDISCARD_STDOUT (saved_stdin); free_dest = TRUE; } else if (action->any.type == POPPLER_ACTION_NAMED) { DISCARD_STDOUT (saved_stdin); dest = poppler_document_find_dest (doc, action->named.named_dest); UNDISCARD_STDOUT (saved_stdin); free_dest = TRUE; } else if (action->any.type == POPPLER_ACTION_GOTO_REMOTE) { print_response_string (action->goto_remote.file_name, COLON); dest = action->goto_remote.dest; } else if (action->any.type == POPPLER_ACTION_GOTO_DEST) dest = action->goto_dest.dest; if (!dest || dest->type == POPPLER_DEST_UNKNOWN || dest->page_num < 1 || dest->page_num > poppler_document_get_n_pages (doc)) { printf (":"); goto theend; } printf ("%d:", dest->page_num); if (action->type == POPPLER_ACTION_GOTO_REMOTE || NULL == (page = poppler_document_get_page (doc, dest->page_num - 1))) { goto theend; } poppler_page_get_size (page, &width, &height); g_object_unref (page); top = (height - dest->top) / height; /* adapted from xpdf */ switch (dest->type) { case POPPLER_DEST_XYZ: if (dest->change_top) printf ("%f", top); break; case POPPLER_DEST_FIT: case POPPLER_DEST_FITB: case POPPLER_DEST_FITH: case POPPLER_DEST_FITBH: putchar ('0'); break; case POPPLER_DEST_FITV: case POPPLER_DEST_FITBV: case POPPLER_DEST_FITR: printf ("%f", top); break; default: ; } theend: if (free_dest) poppler_dest_free (dest); } static void action_print (PopplerDocument *doc, PopplerAction *action) { if (! action_is_handled (action)) return; print_response_string (xpoppler_action_type_string (action->any.type), COLON); print_response_string (action->any.title, COLON); switch (action->any.type) { case POPPLER_ACTION_GOTO_REMOTE: case POPPLER_ACTION_GOTO_DEST: case POPPLER_ACTION_NAMED: action_print_destination (doc, action); putchar ('\n'); break; case POPPLER_ACTION_LAUNCH: print_response_string (action->launch.file_name, COLON); print_response_string (action->launch.params, NEWLINE); break; case POPPLER_ACTION_URI: print_response_string (action->uri.uri, NEWLINE); break; default: ; } } /* ------------------------------------------------------------------ * * PDF Annotations and Attachments * ------------------------------------------------------------------ */ /* static gint * annotation_cmp_edges (const annotation_t *a1, const annotation_t *a2) * { * PopplerRectangle *e1 = &a1->amap->area; * PopplerRectangle *e2 = &a2->amap->area; * * return (e1->y1 > e2->y1 ? -1 * : e1->y1 < e2->y1 ? 1 * : e1->x1 < e2->x1 ? -1 * : e1->x1 != e2->x1); * } */ static GList* annoation_get_for_page (document_t *doc, gint pn) { GList *annot_list, *item; PopplerPage *page; gint i = 0; gint npages = poppler_document_get_n_pages (doc->pdf); if (pn < 1 || pn > npages) return NULL; if (! doc->annotations.pages) doc->annotations.pages = g_malloc0 (npages * sizeof(GList*)); if (doc->annotations.pages[pn - 1]) return doc->annotations.pages[pn - 1]; if (! doc->annotations.keys) doc->annotations.keys = g_hash_table_new (g_str_hash, g_str_equal); page = poppler_document_get_page (doc->pdf, pn - 1); if (NULL == page) return NULL; annot_list = poppler_page_get_annot_mapping (page); for (item = annot_list; item; item = item->next) { PopplerAnnotMapping *map = (PopplerAnnotMapping *)item->data; gchar *key = g_strdup_printf ("annot-%d-%d", pn, i); annotation_t *a = g_malloc (sizeof (annotation_t)); a->amap = map; a->key = key; doc->annotations.pages[pn - 1] = g_list_prepend (doc->annotations.pages[pn - 1], a); assert (NULL == g_hash_table_lookup (doc->annotations.keys, key)); g_hash_table_insert (doc->annotations.keys, key, a); ++i; } g_list_free (annot_list); g_object_unref (page); return doc->annotations.pages[pn - 1]; } static annotation_t* annotation_get_by_key (document_t *doc, const gchar *key) { if (! doc->annotations.keys) return NULL; return g_hash_table_lookup (doc->annotations.keys, key); } #ifdef HAVE_POPPLER_ANNOT_MARKUP void annotation_translate_quadrilateral (PopplerPage *page, PopplerQuadrilateral *q, gboolean inverse) { PopplerRectangle cbox; gdouble xs, ys; poppler_page_get_crop_box (page, &cbox); xs = MIN (cbox.x1, cbox.x2); ys = MIN (cbox.y1, cbox.y2); if (inverse) { xs = -xs; ys = -ys; } q->p1.x -= xs, q->p2.x -= xs; q->p3.x -= xs; q->p4.x -= xs; q->p1.y -= ys, q->p2.y -= ys; q->p3.y -= ys; q->p4.y -= ys; } static cairo_region_t* annotation_markup_get_text_regions (PopplerPage *page, PopplerAnnotTextMarkup *a) { GArray *quads = poppler_annot_text_markup_get_quadrilaterals (a); int i; cairo_region_t *region = cairo_region_create (); gdouble height; poppler_page_get_size (page, NULL, &height); for (i = 0; i < quads->len; ++i) { PopplerQuadrilateral *q = &g_array_index (quads, PopplerQuadrilateral, i); cairo_rectangle_int_t r; annotation_translate_quadrilateral (page, q, FALSE); q->p1.y = height - q->p1.y; q->p2.y = height - q->p2.y; q->p3.y = height - q->p3.y; q->p4.y = height - q->p4.y; r.x = (int) (MIN (q->p1.x, MIN (q->p2.x, MIN (q->p3.x, q->p4.x))) + 0.5); r.y = (int) (MIN (q->p1.y, MIN (q->p2.y, MIN (q->p3.y, q->p4.y))) + 0.5); r.width = (int) (MAX (q->p1.x, MAX (q->p2.x, MAX (q->p3.x, q->p4.x))) + 0.5) - r.x; r.height = (int) (MAX (q->p1.y, MAX (q->p2.y, MAX (q->p3.y, q->p4.y))) + 0.5) - r.y; cairo_region_union_rectangle (region, &r); } g_array_unref (quads); return region; } /** * Append quadrilaterals equivalent to region to an array. * * @param page The page of the annotation. This is used to get the * text regions and pagesize. * @param region The region to add. * @param garray[in,out] An array of PopplerQuadrilateral, where the * new quadrilaterals will be appended. */ static void annotation_markup_append_text_region (PopplerPage *page, PopplerRectangle *region, GArray *garray) { gdouble height; /* poppler_page_get_selection_region is deprecated w/o a replacement. (poppler_page_get_selected_region returns a union of rectangles.) */ GList *regions = poppler_page_get_selection_region (page, 1.0, POPPLER_SELECTION_GLYPH, region); GList *item; poppler_page_get_size (page, NULL, &height); for (item = regions; item; item = item->next) { PopplerRectangle *r = item->data; PopplerQuadrilateral q; q.p1.x = r->x1; q.p1.y = height - r->y1; q.p2.x = r->x2; q.p2.y = height - r->y1; q.p4.x = r->x2; q.p4.y = height - r->y2; q.p3.x = r->x1; q.p3.y = height - r->y2; annotation_translate_quadrilateral (page, &q, TRUE); g_array_append_val (garray, q); } g_list_free (regions); } #endif /** * Create a new annotation. * * @param doc The document for which to create it. * @param type The type of the annotation. * @param r The rectangle where annotation will end up on the page. * * @return The new annotation, or NULL, if the annotation type is * not available. */ static PopplerAnnot* annotation_new (const epdfinfo_t *ctx, document_t *doc, PopplerPage *page, const char *type, PopplerRectangle *r, const command_arg_t *rest, char **error_msg) { PopplerAnnot *a = NULL; int nargs = rest->value.rest.nargs; #ifdef HAVE_POPPLER_ANNOT_MARKUP char * const *args = rest->value.rest.args; int i; GArray *garray = NULL; command_arg_t carg; double width, height; cairo_region_t *region = NULL; #endif if (! strcmp (type, "text")) { cerror_if_not (nargs == 0, error_msg, "%s", "Too many arguments"); return poppler_annot_text_new (doc->pdf, r); } #ifdef HAVE_POPPLER_ANNOT_MARKUP garray = g_array_new (FALSE, FALSE, sizeof (PopplerQuadrilateral)); poppler_page_get_size (page, &width, &height); for (i = 0; i < nargs; ++i) { PopplerRectangle *rr = &carg.value.rectangle; error_if_not (command_arg_parse_arg (ctx, args[i], &carg, ARG_EDGES, error_msg)); rr->x1 *= width; rr->x2 *= width; rr->y1 *= height; rr->y2 *= height; annotation_markup_append_text_region (page, rr, garray); } cerror_if_not (garray->len > 0, error_msg, "%s", "Unable to create empty markup annotation"); if (! strcmp (type, "highlight")) a = poppler_annot_text_markup_new_highlight (doc->pdf, r, garray); else if (! strcmp (type, "squiggly")) a = poppler_annot_text_markup_new_squiggly (doc->pdf, r, garray); else if (! strcmp (type, "strike-out")) a = poppler_annot_text_markup_new_strikeout (doc->pdf, r, garray); else if (! strcmp (type, "underline")) a = poppler_annot_text_markup_new_underline (doc->pdf, r, garray); else cerror_if_not (0, error_msg, "Unknown annotation type: %s", type); #endif error: #ifdef HAVE_POPPLER_ANNOT_MARKUP if (garray) g_array_unref (garray); if (region) cairo_region_destroy (region); #endif return a; } static gboolean annotation_edit_validate (const epdfinfo_t *ctx, const command_arg_t *rest, PopplerAnnot *annotation, char **error_msg) { int nargs = rest->value.rest.nargs; char * const *args = rest->value.rest.args; int i = 0; command_arg_t carg; const char* error_fmt = "Can modify `%s' property only for %s annotations"; while (i < nargs) { command_arg_type_t atype = ARG_INVALID; const char *key = args[i++]; cerror_if_not (i < nargs, error_msg, "Missing a value argument"); if (! strcmp (key, "flags")) atype = ARG_NATNUM; else if (! strcmp (key, "color")) atype = ARG_COLOR; else if (! strcmp (key, "contents")) atype = ARG_STRING; else if (! strcmp (key, "edges")) atype = ARG_EDGES_OR_POSITION; else if (! strcmp (key, "label")) { cerror_if_not (POPPLER_IS_ANNOT_MARKUP (annotation), error_msg, error_fmt, key, "markup"); atype = ARG_STRING; } else if (! strcmp (key, "opacity")) { cerror_if_not (POPPLER_IS_ANNOT_MARKUP (annotation), error_msg, error_fmt, key, "markup"); atype = ARG_EDGE; } else if (! strcmp (key, "popup")) { cerror_if_not (POPPLER_IS_ANNOT_MARKUP (annotation), error_msg, error_fmt, key, "markup"); atype = ARG_EDGES; } else if (! strcmp (key, "popup-is-open")) { cerror_if_not (POPPLER_IS_ANNOT_MARKUP (annotation), error_msg, error_fmt, key, "markup"); atype = ARG_BOOL; } else if (! strcmp (key, "icon")) { cerror_if_not (POPPLER_IS_ANNOT_TEXT (annotation), error_msg, error_fmt, key, "text"); atype = ARG_STRING; } else if (! strcmp (key, "is-open")) { cerror_if_not (POPPLER_IS_ANNOT_TEXT (annotation), error_msg, error_fmt, key, "text"); atype = ARG_BOOL; } else { cerror_if_not (0, error_msg, "Unable to modify property `%s'", key); } if (! command_arg_parse_arg (ctx, args[i++], &carg, atype, error_msg)) return FALSE; } return TRUE; error: return FALSE; } static void annotation_print (const annotation_t *annot, /* const */ PopplerPage *page) { double width, height; PopplerAnnotMapping *m; const gchar *key; PopplerAnnot *a; PopplerAnnotMarkup *ma; PopplerAnnotText *ta; PopplerRectangle r; PopplerColor *color; gchar *text; gdouble opacity; cairo_region_t *region = NULL; if (! annot || ! page) return; m = annot->amap; key = annot->key; a = m->annot; poppler_page_get_size (page, &width, &height); r.x1 = m->area.x1; r.x2 = m->area.x2; r.y1 = height - m->area.y2; r.y2 = height - m->area.y1; #ifdef HAVE_POPPLER_ANNOT_MARKUP if (POPPLER_IS_ANNOT_TEXT_MARKUP (a)) { region = annotation_markup_get_text_regions (page, POPPLER_ANNOT_TEXT_MARKUP (a)); perror_if_not (region, "%s", "Unable to extract annotation's text regions"); } #endif /* >>> Any Annotation >>> */ /* Page */ printf ("%d:", poppler_page_get_index (page) + 1); /* Area */ printf ("%f %f %f %f:", r.x1 / width, r.y1 / height , r.x2 / width, r.y2 / height); /* Type */ printf ("%s:", xpoppler_annot_type_string (poppler_annot_get_annot_type (a))); /* Internal Key */ print_response_string (key, COLON); /* Flags */ printf ("%d:", poppler_annot_get_flags (a)); /* Color */ color = poppler_annot_get_color (a); if (color) { /* Reduce 2 Byte to 1 Byte color space */ printf ("#%.2x%.2x%.2x", (color->red >> 8) , (color->green >> 8) , (color->blue >> 8)); g_free (color); } putchar (':'); /* Text Contents */ text = poppler_annot_get_contents (a); print_response_string (text, COLON); g_free (text); /* Modified Date */ text = poppler_annot_get_modified (a); print_response_string (text, NONE); g_free (text); /* <<< Any Annotation <<< */ /* >>> Markup Annotation >>> */ if (! POPPLER_IS_ANNOT_MARKUP (a)) { putchar ('\n'); goto theend; } putchar (':'); ma = POPPLER_ANNOT_MARKUP (a); /* Label */ text = poppler_annot_markup_get_label (ma); print_response_string (text, COLON); g_free (text); /* Subject */ text = poppler_annot_markup_get_subject (ma); print_response_string (text, COLON); g_free (text); /* Opacity */ opacity = poppler_annot_markup_get_opacity (ma); printf ("%f:", opacity); /* Popup (Area + isOpen) */ if (poppler_annot_markup_has_popup (ma) && poppler_annot_markup_get_popup_rectangle (ma, &r)) { gdouble tmp = r.y1; r.y1 = height - r.y2; r.y2 = height - tmp; printf ("%f %f %f %f:%d:", r.x1 / width, r.y1 / height , r.x2 / width, r.y2 / height , poppler_annot_markup_get_popup_is_open (ma) ? 1 : 0); } else printf ("::"); /* Creation Date */ text = xpoppler_annot_markup_get_created (ma); if (text) { print_response_string (text, NONE); g_free (text); } /* <<< Markup Annotation <<< */ /* >>> Text Annotation >>> */ if (POPPLER_IS_ANNOT_TEXT (a)) { putchar (':'); ta = POPPLER_ANNOT_TEXT (a); /* Text Icon */ text = poppler_annot_text_get_icon (ta); print_response_string (text, COLON); g_free (text); /* Text State */ printf ("%s:%d", xpoppler_annot_text_state_string (poppler_annot_text_get_state (ta)), poppler_annot_text_get_is_open (ta)); } #ifdef HAVE_POPPLER_ANNOT_MARKUP /* <<< Text Annotation <<< */ else if (POPPLER_IS_ANNOT_TEXT_MARKUP (a)) { /* >>> Markup Text Annotation >>> */ putchar (':'); region_print (region, width, height); /* <<< Markup Text Annotation <<< */ } #endif putchar ('\n'); theend: #ifdef HAVE_POPPLER_ANNOT_MARKUP error: #endif if (region) cairo_region_destroy (region); } static void attachment_print (PopplerAttachment *att, const char *id, gboolean do_save) { time_t time; print_response_string (id, COLON); print_response_string (att->name, COLON); print_response_string (att->description, COLON); if (att->size + 1 != 0) printf ("%" G_GSIZE_FORMAT ":", att->size); else printf ("-1:"); time = (time_t) att->mtime; print_response_string (time > 0 ? strchomp (ctime (&time)) : "", COLON); time = (time_t) att->ctime; print_response_string (time > 0 ? strchomp (ctime (&time)) : "", COLON); print_response_string (att->checksum ? att->checksum->str : "" , COLON); if (do_save) { char *filename = mktempfile (); GError *error = NULL; if (filename) { if (! poppler_attachment_save (att, filename, &error)) { fprintf (stderr, "Writing attachment failed: %s" , error ? error->message : "reason unknown"); if (error) g_free (error); } else { print_response_string (filename, NONE); } free (filename); } } putchar ('\n'); } /* ================================================================== * * Server command implementations * ================================================================== */ /* Name: features Args: None Returns: A list of compile-time features. Errors: None */ const command_arg_type_t cmd_features_spec[] = {}; static void cmd_features (const epdfinfo_t *ctx, const command_arg_t *args) { const char *features[] = { #ifdef HAVE_POPPLER_FIND_OPTS "case-sensitive-search", #else "no-case-sensitive-search", #endif #ifdef HAVE_POPPLER_ANNOT_WRITE "writable-annotations", #else "no-writable-annotations", #endif #ifdef HAVE_POPPLER_ANNOT_MARKUP "markup-annotations" #else "no-markup-annotations" #endif }; int i; OK_BEGIN (); for (i = 0; i < G_N_ELEMENTS (features); ++i) { printf ("%s", features[i]); if (i < G_N_ELEMENTS (features) - 1) putchar (':'); } putchar ('\n'); OK_END (); } /* Name: open Args: filename password Returns: Nothing Errors: If file can't be opened or is not a PDF document. */ const command_arg_type_t cmd_open_spec[] = { ARG_NONEMPTY_STRING, /* filename */ ARG_STRING, /* password */ }; static void cmd_open (const epdfinfo_t *ctx, const command_arg_t *args) { const char *filename = args[0].value.string; const char *passwd = args[1].value.string; GError *gerror = NULL; document_t *doc; if (! *passwd) passwd = NULL; doc = document_open(ctx, filename, passwd, &gerror); perror_if_not (doc, "Error opening %s:%s", filename, gerror ? gerror->message : "unknown error"); OK (); error: if (gerror) { g_error_free (gerror); gerror = NULL; } } /* Name: close Args: filename Returns: 1 if file was open, otherwise 0. Errors: None */ const command_arg_type_t cmd_close_spec[] = { ARG_NONEMPTY_STRING /* filename */ }; static void cmd_close (const epdfinfo_t *ctx, const command_arg_t *args) { document_t *doc = g_hash_table_lookup(ctx->documents, args->value.string); g_hash_table_remove (ctx->documents, args->value.string); free_document (doc); OK_BEGIN (); puts (doc ? "1" : "0"); OK_END (); } /* Name: closeall Args: None Returns: Nothing Errors: None */ static void cmd_closeall (const epdfinfo_t *ctx, const command_arg_t *args) { GHashTableIter iter; gpointer key, value; g_hash_table_iter_init (&iter, ctx->documents); while (g_hash_table_iter_next (&iter, &key, &value)) { document_t *doc = (document_t*) value; free_document (doc); g_hash_table_iter_remove (&iter); } OK (); } const command_arg_type_t cmd_search_regexp_spec[] = { ARG_DOC, ARG_NATNUM, /* first page */ ARG_NATNUM, /* last page */ ARG_NONEMPTY_STRING, /* regexp */ ARG_NATNUM, /* compile flags */ ARG_NATNUM /* match flags */ }; static void cmd_search_regexp(const epdfinfo_t *ctx, const command_arg_t *args) { PopplerDocument *doc = args[0].value.doc->pdf; int first = args[1].value.natnum; int last = args[2].value.natnum; const gchar *regexp = args[3].value.string; GRegexCompileFlags cflags = args[4].value.natnum; GRegexMatchFlags mflags = args[5].value.natnum; double width, height; int pn; GError *gerror = NULL; GRegex *re = NULL; NORMALIZE_PAGE_ARG (doc, &first, &last); re = g_regex_new (regexp, cflags, mflags, &gerror); perror_if_not (NULL == gerror, "Invalid regexp: %s", gerror->message); OK_BEGIN (); for (pn = first; pn <= last; ++pn) { PopplerPage *page = poppler_document_get_page(doc, pn - 1); char *text; PopplerRectangle *rectangles = NULL; guint nrectangles; GMatchInfo *match = NULL; if (! page) continue; text = poppler_page_get_text (page); poppler_page_get_text_layout (page, &rectangles, &nrectangles); poppler_page_get_size (page, &width, &height); g_regex_match (re, text, 0, &match); while (g_match_info_matches (match)) { const double scale = 100.0; gint start, end, ustart, ulen; gchar *string = NULL; gchar *line = NULL; int i; /* Does this ever happen ? */ if (! g_match_info_fetch_pos (match, 0, &start, &end)) continue; string = g_match_info_fetch (match, 0); ustart = g_utf8_strlen (text, start); ulen = g_utf8_strlen (string, -1); cairo_region_t *region = cairo_region_create (); /* Merge matched glyph rectangles. Scale them so we're able to use cairo . */ if (ulen > 0) { assert (ustart < nrectangles && ustart + ulen <= nrectangles); line = poppler_page_get_selected_text (page, POPPLER_SELECTION_LINE, rectangles + ustart); for (i = ustart; i < ustart + ulen; ++i) { PopplerRectangle *r = rectangles + i; cairo_rectangle_int_t c; c.x = (int) (scale * r->x1 + 0.5); c.y = (int) (scale * r->y1 + 0.5); c.width = (int) (scale * (r->x2 - r->x1) + 0.5); c.height = (int) (scale * (r->y2 - r->y1) + 0.5); cairo_region_union_rectangle (region, &c); } } printf ("%d:", pn); print_response_string (string, COLON); print_response_string (strchomp (line), COLON); region_print (region, width * scale, height * scale); putchar ('\n'); cairo_region_destroy (region); g_free (string); g_free (line); g_match_info_next (match, NULL); } g_free (rectangles); g_object_unref (page); g_free (text); g_match_info_free (match); } OK_END (); error: if (re) g_regex_unref (re); if (gerror) g_error_free (gerror); } const command_arg_type_t cmd_regexp_flags_spec[] = { }; static void cmd_regexp_flags (const epdfinfo_t *ctx, const command_arg_t *args) { OK_BEGIN (); printf ("caseless:%d\n", G_REGEX_CASELESS); printf ("multiline:%d\n", G_REGEX_MULTILINE); printf ("dotall:%d\n", G_REGEX_DOTALL); printf ("extended:%d\n", G_REGEX_EXTENDED); printf ("anchored:%d\n", G_REGEX_ANCHORED); printf ("dollar-endonly:%d\n", G_REGEX_DOLLAR_ENDONLY); printf ("ungreedy:%d\n", G_REGEX_UNGREEDY); printf ("raw:%d\n", G_REGEX_RAW); printf ("no-auto-capture:%d\n", G_REGEX_NO_AUTO_CAPTURE); printf ("optimize:%d\n", G_REGEX_OPTIMIZE); printf ("dupnames:%d\n", G_REGEX_DUPNAMES); printf ("newline-cr:%d\n", G_REGEX_NEWLINE_CR); printf ("newline-lf:%d\n", G_REGEX_NEWLINE_LF); printf ("newline-crlf:%d\n", G_REGEX_NEWLINE_CRLF); printf ("match-anchored:%d\n", G_REGEX_MATCH_ANCHORED); printf ("match-notbol:%d\n", G_REGEX_MATCH_NOTBOL); printf ("match-noteol:%d\n", G_REGEX_MATCH_NOTEOL); printf ("match-notempty:%d\n", G_REGEX_MATCH_NOTEMPTY); printf ("match-partial:%d\n", G_REGEX_MATCH_PARTIAL); printf ("match-newline-cr:%d\n", G_REGEX_MATCH_NEWLINE_CR); printf ("match-newline-lf:%d\n", G_REGEX_MATCH_NEWLINE_LF); printf ("match-newline-crlf:%d\n", G_REGEX_MATCH_NEWLINE_CRLF); printf ("match-newline-any:%d\n", G_REGEX_MATCH_NEWLINE_ANY); OK_END (); } const command_arg_type_t cmd_search_string_spec[] = { ARG_DOC, ARG_NATNUM, /* first page */ ARG_NATNUM, /* last page */ ARG_NONEMPTY_STRING, /* search string */ ARG_BOOL, /* ignore-case */ }; static void cmd_search_string(const epdfinfo_t *ctx, const command_arg_t *args) { PopplerDocument *doc = args[0].value.doc->pdf; int first = args[1].value.natnum; int last = args[2].value.natnum; const char *string = args[3].value.string; gboolean ignore_case = args[4].value.flag; GList *list, *item; double width, height; int pn; #ifdef HAVE_POPPLER_FIND_OPTS PopplerFindFlags flags = ignore_case ? 0 : POPPLER_FIND_CASE_SENSITIVE; #endif NORMALIZE_PAGE_ARG (doc, &first, &last); OK_BEGIN (); for (pn = first; pn <= last; ++pn) { PopplerPage *page = poppler_document_get_page(doc, pn - 1); if (! page) continue; #ifdef HAVE_POPPLER_FIND_OPTS list = poppler_page_find_text_with_options(page, string, flags); #else list = poppler_page_find_text(page, string); #endif poppler_page_get_size (page, &width, &height); for (item = list; item; item = item->next) { gchar *line, *match; PopplerRectangle *r = item->data; gdouble y1 = r->y1; r->y1 = height - r->y2; r->y2 = height - y1; printf ("%d:", pn); line = strchomp (poppler_page_get_selected_text (page, POPPLER_SELECTION_LINE, r)); match = strchomp (poppler_page_get_selected_text (page, POPPLER_SELECTION_GLYPH, r)); print_response_string (match, COLON); print_response_string (line, COLON); printf ("%f %f %f %f\n", r->x1 / width, r->y1 / height, r->x2 / width, r->y2 / height); g_free (line); g_free (match); poppler_rectangle_free (r); } g_list_free (list); g_object_unref (page); } OK_END (); } /* Name: metadata Args: filename Returns: PDF's metadata Errors: None title author subject keywords creator producer pdf-version create-date mod-date Dates are in seconds since the epoche. */ const command_arg_type_t cmd_metadata_spec[] = { ARG_DOC, }; static void cmd_metadata (const epdfinfo_t *ctx, const command_arg_t *args) { PopplerDocument *doc = args[0].value.doc->pdf; time_t date; gchar *md[6]; gchar *title; int i; char *time_str; OK_BEGIN (); title = poppler_document_get_title (doc); print_response_string (title, COLON); g_free (title); md[0] = poppler_document_get_author (doc); md[1] = poppler_document_get_subject (doc); md[2] = poppler_document_get_keywords (doc); md[3] = poppler_document_get_creator (doc); md[4] = poppler_document_get_producer (doc); md[5] = poppler_document_get_pdf_version_string (doc); for (i = 0; i < 6; ++i) { print_response_string (md[i], COLON); g_free (md[i]); } date = poppler_document_get_creation_date (doc); time_str = strchomp (ctime (&date)); print_response_string (time_str ? time_str : "", COLON); date = poppler_document_get_modification_date (doc); time_str = strchomp (ctime (&date)); print_response_string (time_str ? time_str : "", NEWLINE); OK_END (); } /* Name: outline Args: filename Returns: The documents outline (or index) as a, possibly empty, list of records: tree-level ACTION See cmd_pagelinks for how ACTION is constructed. Errors: None */ static void cmd_outline_walk (PopplerDocument *doc, PopplerIndexIter *iter, int depth) { do { PopplerIndexIter *child; PopplerAction *action = poppler_index_iter_get_action (iter); if (! action) continue; if (action_is_handled (action)) { printf ("%d:", depth); action_print (doc, action); } child = poppler_index_iter_get_child (iter); if (child) { cmd_outline_walk (doc, child, depth + 1); } poppler_action_free (action); poppler_index_iter_free (child); } while (poppler_index_iter_next (iter)); } const command_arg_type_t cmd_outline_spec[] = { ARG_DOC, }; static void cmd_outline (const epdfinfo_t *ctx, const command_arg_t *args) { PopplerIndexIter *iter = poppler_index_iter_new (args->value.doc->pdf); OK_BEGIN (); if (iter) { cmd_outline_walk (args->value.doc->pdf, iter, 1); poppler_index_iter_free (iter); } OK_END (); } /* Name: quit Args: None Returns: Nothing Errors: None Close all documents and exit. */ const command_arg_type_t cmd_quit_spec[] = {}; static void cmd_quit (const epdfinfo_t *ctx, const command_arg_t *args) { cmd_closeall (ctx, args); exit (EXIT_SUCCESS); } /* Name: number-of-pages Args: filename Returns: The number of pages. Errors: None */ const command_arg_type_t cmd_number_of_pages_spec[] = { ARG_DOC }; static void cmd_number_of_pages (const epdfinfo_t *ctx, const command_arg_t *args) { int npages = poppler_document_get_n_pages (args->value.doc->pdf); OK_BEGIN (); printf ("%d\n", npages); OK_END (); } /* Name: pagelinks Args: filename page Returns: A list of linkmaps: edges ACTION , where ACTION is one of 'goto-dest' title page top 'goto-remote' title filename page top 'uri' title URI 'launch' title program arguments top is desired vertical position, filename is the target PDF of the `goto-remote' link. Errors: None */ const command_arg_type_t cmd_pagelinks_spec[] = { ARG_DOC, ARG_NATNUM /* page number */ }; static void cmd_pagelinks(const epdfinfo_t *ctx, const command_arg_t *args) { PopplerDocument *doc = args[0].value.doc->pdf; PopplerPage *page = NULL; int pn = args[1].value.natnum; double width, height; GList *link_map = NULL, *item; page = poppler_document_get_page (doc, pn - 1); perror_if_not (page, "No such page %d", pn); poppler_page_get_size (page, &width, &height); link_map = poppler_page_get_link_mapping (page); OK_BEGIN (); for (item = g_list_last (link_map); item; item = item->prev) { PopplerLinkMapping *link = item->data; PopplerRectangle *r = &link->area; gdouble y1 = r->y1; /* LinkMappings have a different gravity. */ r->y1 = height - r->y2; r->y2 = height - y1; if (! action_is_handled (link->action)) continue; printf ("%f %f %f %f:", r->x1 / width, r->y1 / height, r->x2 / width, r->y2 / height); action_print (doc, link->action); } OK_END (); error: if (page) g_object_unref (page); if (link_map) poppler_page_free_link_mapping (link_map); } /* Name: gettext Args: filename page edges selection-style Returns: The selection's text. Errors: If page is out of range. For the selection-style argument see getselection command. */ const command_arg_type_t cmd_gettext_spec[] = { ARG_DOC, ARG_NATNUM, /* page number */ ARG_EDGES, /* selection */ ARG_NATNUM /* selection-style */ }; static void cmd_gettext(const epdfinfo_t *ctx, const command_arg_t *args) { PopplerDocument *doc = args[0].value.doc->pdf; int pn = args[1].value.natnum; PopplerRectangle r = args[2].value.rectangle; int selection_style = args[3].value.natnum; PopplerPage *page = NULL; double width, height; gchar *text = NULL; switch (selection_style) { case POPPLER_SELECTION_GLYPH: break; case POPPLER_SELECTION_LINE: break; case POPPLER_SELECTION_WORD: break; default: selection_style = POPPLER_SELECTION_GLYPH; } page = poppler_document_get_page (doc, pn - 1); perror_if_not (page, "No such page %d", pn); poppler_page_get_size (page, &width, &height); r.x1 = r.x1 * width; r.x2 = r.x2 * width; r.y1 = r.y1 * height; r.y2 = r.y2 * height; /* printf ("%f %f %f %f , %f %f\n", r.x1, r.y1, r.x2, r.y2, width, height); */ text = poppler_page_get_selected_text (page, selection_style, &r); OK_BEGIN (); print_response_string (text, NEWLINE); OK_END (); error: g_free (text); if (page) g_object_unref (page); } /* Name: getselection Args: filename page edges selection-selection_style Returns: The selection's text. Errors: If page is out of range. selection-selection_style should be as follows. 0 (POPPLER_SELECTION_GLYPH) glyph is the minimum unit for selection 1 (POPPLER_SELECTION_WORD) word is the minimum unit for selection 2 (POPPLER_SELECTION_LINE) line is the minimum unit for selection */ const command_arg_type_t cmd_getselection_spec[] = { ARG_DOC, ARG_NATNUM, /* page number */ ARG_EDGES, /* selection */ ARG_NATNUM /* selection-style */ }; static void cmd_getselection (const epdfinfo_t *ctx, const command_arg_t *args) { PopplerDocument *doc = args[0].value.doc->pdf; int pn = args[1].value.natnum; PopplerRectangle r = args[2].value.rectangle; int selection_style = args[3].value.natnum; gdouble width, height; cairo_region_t *region = NULL; PopplerPage *page = NULL; int i; switch (selection_style) { case POPPLER_SELECTION_GLYPH: break; case POPPLER_SELECTION_LINE: break; case POPPLER_SELECTION_WORD: break; default: selection_style = POPPLER_SELECTION_GLYPH; } page = poppler_document_get_page (doc, pn - 1); perror_if_not (page, "No such page %d", pn); poppler_page_get_size (page, &width, &height); r.x1 = r.x1 * width; r.x2 = r.x2 * width; r.y1 = r.y1 * height; r.y2 = r.y2 * height; region = poppler_page_get_selected_region (page, 1.0, selection_style, &r); OK_BEGIN (); for (i = 0; i < cairo_region_num_rectangles (region); ++i) { cairo_rectangle_int_t r; cairo_region_get_rectangle (region, i, &r); printf ("%f %f %f %f\n", r.x / width, r.y / height, (r.x + r.width) / width, (r.y + r.height) / height); } OK_END (); error: if (region) cairo_region_destroy (region); if (page) g_object_unref (page); } /* Name: pagesize Args: filename page Returns: width height Errors: If page is out of range. */ const command_arg_type_t cmd_pagesize_spec[] = { ARG_DOC, ARG_NATNUM /* page number */ }; static void cmd_pagesize(const epdfinfo_t *ctx, const command_arg_t *args) { PopplerDocument *doc = args[0].value.doc->pdf; int pn = args[1].value.natnum; PopplerPage *page = NULL; double width, height; page = poppler_document_get_page (doc, pn - 1); perror_if_not (page, "No such page %d", pn); poppler_page_get_size (page, &width, &height); OK_BEGIN (); printf ("%f:%f\n", width, height); OK_END (); error: if (page) g_object_unref (page); } /* Annotations */ /* Name: getannots Args: filename firstpage lastpage Returns: The list of annotations of this page. For all annotations page edges type key flags color contents mod-date ,where name is a document-unique name, flags is PopplerAnnotFlag bitmask, color is 3-byte RGB hex number and Then label subject opacity popup-edges popup-is-open create-date if this is a markup annotation and additionally text-icon text-state for markup text annotations. Errors: If page is out of range. */ const command_arg_type_t cmd_getannots_spec[] = { ARG_DOC, ARG_NATNUM, /* first page */ ARG_NATNUM /* last page */ }; static void cmd_getannots(const epdfinfo_t *ctx, const command_arg_t *args) { PopplerDocument *doc = args[0].value.doc->pdf; gint first = args[1].value.natnum; gint last = args[2].value.natnum; GList *list; gint pn; first = MAX(1, first); if (last <= 0) last = poppler_document_get_n_pages (doc); else last = MIN(last, poppler_document_get_n_pages (doc)); OK_BEGIN (); for (pn = first; pn <= last; ++pn) { GList *annots = annoation_get_for_page (args->value.doc, pn); PopplerPage *page = poppler_document_get_page (doc, pn - 1); if (! page) continue; for (list = annots; list; list = list->next) { annotation_t *annot = (annotation_t *)list->data; annotation_print (annot, page); } g_object_unref (page); } OK_END (); } /* Name: getannot Args: filename name Returns: The annotation for name, see cmd_getannots. Errors: If no annotation named ,name' exists. */ const command_arg_type_t cmd_getannot_spec[] = { ARG_DOC, ARG_NONEMPTY_STRING, /* annotation's key */ }; static void cmd_getannot (const epdfinfo_t *ctx, const command_arg_t *args) { document_t *doc = args->value.doc; const gchar *key = args[1].value.string; PopplerPage *page = NULL; annotation_t *a = annotation_get_by_key (doc, key); gint index; perror_if_not (a, "No such annotation: %s", key); index = poppler_annot_get_page_index (a->amap->annot); if (index >= 0) page = poppler_document_get_page (doc->pdf, index); perror_if_not (page, "Unable to get page %d", index + 1); OK_BEGIN (); annotation_print (a, page); OK_END (); error: if (page) g_object_unref (page); } /* Name: getannot_attachment Args: filename name [output-filename] Returns: name description size mtime ctime output-filename Errors: If no annotation named ,name' exists or output-filename is not writable. */ const command_arg_type_t cmd_getattachment_from_annot_spec[] = { ARG_DOC, ARG_NONEMPTY_STRING, /* annotation's name */ ARG_BOOL /* save attachment */ }; static void cmd_getattachment_from_annot (const epdfinfo_t *ctx, const command_arg_t *args) { document_t *doc = args->value.doc; const gchar *key = args[1].value.string; gboolean do_save = args[2].value.flag; PopplerAttachment *att = NULL; annotation_t *a = annotation_get_by_key (doc, key); gchar *id = NULL; perror_if_not (a, "No such annotation: %s", key); perror_if_not (POPPLER_IS_ANNOT_FILE_ATTACHMENT (a->amap->annot), "Not a file annotation: %s", key); att = poppler_annot_file_attachment_get_attachment (POPPLER_ANNOT_FILE_ATTACHMENT (a->amap->annot)); perror_if_not (att, "Unable to get attachment: %s", key); id = g_strdup_printf ("attachment-%s", key); OK_BEGIN (); attachment_print (att, id, do_save); OK_END (); error: if (att) g_object_unref (att); if (id) g_free (id); } /* document-level attachments */ const command_arg_type_t cmd_getattachments_spec[] = { ARG_DOC, ARG_BOOL, /* save attachments */ }; static void cmd_getattachments (const epdfinfo_t *ctx, const command_arg_t *args) { document_t *doc = args->value.doc; gboolean do_save = args[1].value.flag; GList *item; GList *attmnts = poppler_document_get_attachments (doc->pdf); int i; OK_BEGIN (); for (item = attmnts, i = 0; item; item = item->next, ++i) { PopplerAttachment *att = (PopplerAttachment*) item->data; gchar *id = g_strdup_printf ("attachment-document-%d", i); attachment_print (att, id, do_save); g_object_unref (att); g_free (id); } g_list_free (attmnts); OK_END (); } #ifdef HAVE_POPPLER_ANNOT_WRITE const command_arg_type_t cmd_addannot_spec[] = { ARG_DOC, ARG_NATNUM, /* page number */ ARG_STRING, /* type */ ARG_EDGES_OR_POSITION, /* edges or position (uses default size) */ ARG_REST, /* markup regions */ }; static void cmd_addannot (const epdfinfo_t *ctx, const command_arg_t *args) { document_t *doc = args->value.doc; gint pn = args[1].value.natnum; const char *type_string = args[2].value.string; PopplerRectangle r = args[3].value.rectangle; int i; PopplerPage *page = NULL; double width, height; PopplerAnnot *pa; PopplerAnnotMapping *amap; annotation_t *a; gchar *key; GList *annotations; gdouble y2; char *error_msg = NULL; page = poppler_document_get_page (doc->pdf, pn - 1); perror_if_not (page, "Unable to get page %d", pn); poppler_page_get_size (page, &width, &height); r.x1 *= width; r.x2 *= width; r.y1 *= height; r.y2 *= height; if (r.y2 < 0) r.y2 = r.y1 + 24; if (r.x2 < 0) r.x2 = r.x1 + 24; y2 = r.y2; r.y2 = height - r.y1; r.y1 = height - y2; pa = annotation_new (ctx, doc, page, type_string, &r, &args[4], &error_msg); perror_if_not (pa, "Creating annotation failed: %s", error_msg ? error_msg : "Reason unknown"); amap = poppler_annot_mapping_new (); amap->area = r; amap->annot = pa; annotations = annoation_get_for_page (doc, pn); i = g_list_length (annotations); key = g_strdup_printf ("annot-%d-%d", pn, i); while (g_hash_table_lookup (doc->annotations.keys, key)) { g_free (key); key = g_strdup_printf ("annot-%d-%d", pn, ++i); } a = g_malloc (sizeof (annotation_t)); a->amap = amap; a->key = key; doc->annotations.pages[pn - 1] = g_list_prepend (annotations, a); g_hash_table_insert (doc->annotations.keys, key, a); poppler_page_add_annot (page, pa); OK_BEGIN (); annotation_print (a, page); OK_END (); error: if (page) g_object_unref (page); if (error_msg) g_free (error_msg); } const command_arg_type_t cmd_delannot_spec[] = { ARG_DOC, ARG_NONEMPTY_STRING /* Annotation's key */ }; static void cmd_delannot (const epdfinfo_t *ctx, const command_arg_t *args) { document_t *doc = args->value.doc; const gchar *key = args[1].value.string; PopplerPage *page = NULL; annotation_t *a = annotation_get_by_key (doc, key); gint pn; perror_if_not (a, "No such annotation: %s", key); pn = poppler_annot_get_page_index (a->amap->annot) + 1; if (pn >= 1) page = poppler_document_get_page (doc->pdf, pn - 1); perror_if_not (page, "Unable to get page %d", pn); poppler_page_remove_annot (page, a->amap->annot); doc->annotations.pages[pn - 1] = g_list_remove (doc->annotations.pages[pn - 1], a); g_hash_table_remove (doc->annotations.keys, a->key); poppler_annot_mapping_free(a->amap); OK (); error: if (a) { g_free (a->key); g_free (a); } if (page) g_object_unref (page); } const command_arg_type_t cmd_editannot_spec[] = { ARG_DOC, ARG_NONEMPTY_STRING, /* annotation key */ ARG_REST /* (KEY VALUE ...) */ }; static void cmd_editannot (const epdfinfo_t *ctx, const command_arg_t *args) { document_t *doc = args->value.doc; const char *key = args[1].value.string; int nrest_args = args[2].value.rest.nargs; char * const *rest_args = args[2].value.rest.args; annotation_t *a = annotation_get_by_key (doc, key); PopplerAnnot *pa; PopplerPage *page = NULL; int i = 0; gint index; char *error_msg = NULL; command_arg_t carg; const char *unexpected_parse_error = "Internal error while parsing arg `%s'"; perror_if_not (a, "No such annotation: %s", key); pa = a->amap->annot; perror_if_not (annotation_edit_validate (ctx, &args[2], pa, &error_msg), "%s", error_msg); index = poppler_annot_get_page_index (pa); page = poppler_document_get_page (doc->pdf, index); perror_if_not (page, "Unable to get page %d for annotation", index); for (i = 0; i < nrest_args; ++i) { const char *key = rest_args[i++]; if (! strcmp (key, "flags")) { perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &carg, ARG_NATNUM, NULL), unexpected_parse_error, rest_args[i]); poppler_annot_set_flags (pa, carg.value.natnum); } else if (! strcmp (key, "color")) { perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &carg, ARG_COLOR, NULL), unexpected_parse_error, rest_args[i]); poppler_annot_set_color (pa, &carg.value.color); } else if (! strcmp (key, "contents")) { perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &carg, ARG_STRING, NULL), unexpected_parse_error, rest_args[i]); poppler_annot_set_contents (pa, carg.value.string); } else if (! strcmp (key, "edges")) { PopplerRectangle *area = &a->amap->area; gdouble width, height; PopplerRectangle r; perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &carg, ARG_EDGES_OR_POSITION, NULL), unexpected_parse_error, rest_args[i]); r = carg.value.rectangle; poppler_page_get_size (page, &width, &height); /* Translate Gravity and maybe keep the width and height. */ if (r.x2 < 0) area->x2 += (r.x1 * width) - area->x1; else area->x2 = r.x2 * width; if (r.y2 < 0) area->y1 -= (r.y1 * height) - (height - area->y2); else area->y1 = height - (r.y2 * height); area->x1 = r.x1 * width; area->y2 = height - (r.y1 * height); xpoppler_annot_set_rectangle (pa, area); } else if (! strcmp (key, "label")) { PopplerAnnotMarkup *ma = POPPLER_ANNOT_MARKUP (pa); perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &carg, ARG_STRING, NULL), unexpected_parse_error, rest_args[i]); poppler_annot_markup_set_label (ma, carg.value.string); } else if (! strcmp (key, "opacity")) { PopplerAnnotMarkup *ma = POPPLER_ANNOT_MARKUP (pa); perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &carg, ARG_EDGE, NULL), unexpected_parse_error, rest_args[i]); poppler_annot_markup_set_opacity (ma, carg.value.edge); } else if (! strcmp (key, "popup")) { PopplerAnnotMarkup *ma = POPPLER_ANNOT_MARKUP (pa); perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &carg, ARG_EDGES, NULL), unexpected_parse_error, rest_args[i]); poppler_annot_markup_set_popup (ma, &carg.value.rectangle); } else if (! strcmp (key, "popup-is-open")) { PopplerAnnotMarkup *ma = POPPLER_ANNOT_MARKUP (pa); perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &carg, ARG_BOOL, NULL), unexpected_parse_error, rest_args[i]); poppler_annot_markup_set_popup_is_open (ma, carg.value.flag); } else if (! strcmp (key, "icon")) { PopplerAnnotText *ta = POPPLER_ANNOT_TEXT (pa); perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &carg, ARG_STRING, NULL), unexpected_parse_error, rest_args[i]); poppler_annot_text_set_icon (ta, carg.value.string); } else if (! strcmp (key, "is-open")) { PopplerAnnotText *ta = POPPLER_ANNOT_TEXT (pa); perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &carg, ARG_BOOL, NULL), unexpected_parse_error, rest_args[i]); poppler_annot_text_set_is_open (ta, carg.value.flag); } else { perror_if_not (0, "internal error: annotation property validation failed"); } } OK_BEGIN (); annotation_print (a, page); OK_END (); error: if (error_msg) g_free (error_msg); if (page) g_object_unref (page); } const command_arg_type_t cmd_save_spec[] = { ARG_DOC, }; static void cmd_save (const epdfinfo_t *ctx, const command_arg_t *args) { document_t *doc = args->value.doc; char *filename = mktempfile (); GError *gerror = NULL; gchar *uri; gboolean success = FALSE; if (!filename) { printf_error_response ("Unable to create temporary file"); return; } uri = g_filename_to_uri (filename, NULL, &gerror); if (uri) { success = poppler_document_save (doc->pdf, uri, &gerror); g_free (uri); } if (! success) { printf_error_response ("Error while saving %s:%s" , filename, gerror ? gerror->message : "?"); if (gerror) g_error_free (gerror); return; } OK_BEGIN (); print_response_string (filename, NEWLINE); OK_END (); } #endif /* HAVE_POPPLER_ANNOT_WRITE */ const command_arg_type_t cmd_synctex_forward_search_spec[] = { ARG_DOC, ARG_NONEMPTY_STRING, /* source file */ ARG_NATNUM, /* line number */ ARG_NATNUM /* column number */ }; static void cmd_synctex_forward_search (const epdfinfo_t *ctx, const command_arg_t *args) { document_t *doc = args[0].value.doc; const char *source = args[1].value.string; int line = args[2].value.natnum; int column = args[3].value.natnum; synctex_scanner_t scanner = NULL; synctex_node_t node; float x1, y1, x2, y2; PopplerPage *page = NULL; double width, height; int pn; scanner = synctex_scanner_new_with_output_file (doc->filename, NULL, 1); perror_if_not (scanner, "Unable to create synctex scanner,\ did you run latex with `--synctex=1' ?"); perror_if_not (synctex_display_query (scanner, source, line, column) && (node = synctex_next_result (scanner)), "Destination not found"); pn = synctex_node_page (node); page = poppler_document_get_page(doc->pdf, pn - 1); perror_if_not (page, "Page not found"); x1 = synctex_node_box_visible_h (node); y1 = synctex_node_box_visible_v (node) - synctex_node_box_visible_height (node); x2 = synctex_node_box_visible_width (node) + x1; y2 = synctex_node_box_visible_depth (node) + synctex_node_box_visible_height (node) + y1; poppler_page_get_size (page, &width, &height); x1 /= width; y1 /= height; x2 /= width; y2 /= height; OK_BEGIN (); printf("%d:%f:%f:%f:%f\n", pn, x1, y1, x2, y2); OK_END (); error: if (page) g_object_unref (page); if (scanner) synctex_scanner_free (scanner); } const command_arg_type_t cmd_synctex_backward_search_spec[] = { ARG_DOC, ARG_NATNUM, /* page number */ ARG_EDGE, /* x */ ARG_EDGE /* y */ }; static void cmd_synctex_backward_search (const epdfinfo_t *ctx, const command_arg_t *args) { document_t *doc = args[0].value.doc; int pn = args[1].value.natnum; double x = args[2].value.edge; double y = args[3].value.edge; synctex_scanner_t scanner = NULL; const char *filename; PopplerPage *page = NULL; synctex_node_t node; double width, height; int line, column; scanner = synctex_scanner_new_with_output_file (doc->filename, NULL, 1); perror_if_not (scanner, "Unable to create synctex scanner,\ did you run latex with `--synctex=1' ?"); page = poppler_document_get_page(doc->pdf, pn - 1); perror_if_not (page, "Page not found"); poppler_page_get_size (page, &width, &height); x = x * width; y = y * height; if (! synctex_edit_query (scanner, pn, x, y) || ! (node = synctex_next_result (scanner)) || ! (filename = synctex_scanner_get_name (scanner, synctex_node_tag (node)))) { printf_error_response ("Destination not found"); goto error; } line = synctex_node_line (node); column = synctex_node_column (node); OK_BEGIN (); print_response_string (filename, COLON); printf("%d:%d\n", line, column); OK_END (); error: if (page) g_object_unref (page); if (scanner) synctex_scanner_free (scanner); } const command_arg_type_t cmd_renderpage_spec[] = { ARG_DOC, ARG_NATNUM, /* page number */ ARG_NATNUM, /* width */ ARG_REST, /* commands */ }; static void cmd_renderpage (const epdfinfo_t *ctx, const command_arg_t *args) { document_t *doc = args[0].value.doc; int pn = args[1].value.natnum; int width = args[2].value.natnum; int nrest_args = args[3].value.rest.nargs; char * const *rest_args = args[3].value.rest.args; PopplerPage *page = poppler_document_get_page(doc->pdf, pn - 1); cairo_surface_t *surface = NULL; cairo_t *cr = NULL; command_arg_t rest_arg; gchar *error_msg = NULL; double pt_width, pt_height; PopplerColor fg = { 0, 0, 0 }; PopplerColor bg = { 65535, 0, 0 }; double alpha = 1.0; double line_width = 1.5; PopplerRectangle cb = {0.0, 0.0, 1.0, 1.0}; int i = 0; perror_if_not (page, "No such page %d", pn); poppler_page_get_size (page, &pt_width, &pt_height); surface = image_render_page (doc->pdf, page, width, 1, &doc->options.render); perror_if_not (surface, "Failed to render page %d", pn); if (! nrest_args) goto theend; cr = cairo_create (surface); cairo_scale (cr, width / pt_width, width / pt_width); while (i < nrest_args) { const char* keyword; perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &rest_arg, ARG_STRING, &error_msg), "%s", error_msg); keyword = rest_arg.value.string; ++i; perror_if_not (i < nrest_args, "Keyword is `%s' missing an argument", keyword); if (! strcmp (keyword, ":foreground") || ! strcmp (keyword, ":background")) { perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &rest_arg, ARG_COLOR, &error_msg), "%s", error_msg); ++i; if (! strcmp (keyword, ":foreground")) fg = rest_arg.value.color; else bg = rest_arg.value.color; } else if (! strcmp (keyword, ":alpha")) { perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &rest_arg, ARG_EDGE, &error_msg), "%s", error_msg); ++i; alpha = rest_arg.value.edge; } else if (! strcmp (keyword, ":crop-to") || ! strcmp (keyword, ":highlight-region") || ! strcmp (keyword, ":highlight-text") || ! strcmp (keyword, ":highlight-line")) { PopplerRectangle *r; perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &rest_arg, ARG_EDGES, &error_msg), "%s", error_msg); ++i; r = &rest_arg.value.rectangle; if (! strcmp (keyword, ":crop-to")) { gdouble w = (cb.x2 - cb.x1); gdouble h = (cb.y2 - cb.y1); gdouble x1 = cb.x1; gdouble y1 = cb.y1; cb.x1 = r->x1 * w + x1; cb.x2 = r->x2 * w + x1; cb.y1 = r->y1 * h + y1; cb.y2 = r->y2 * h + y1; } else { r->x1 = pt_width * r->x1 * (cb.x2 - cb.x1) + pt_width * cb.x1; r->x2 = pt_width * r->x2 * (cb.x2 - cb.x1) + pt_width * cb.x1; r->y1 = pt_height * r->y1 * (cb.y2 - cb.y1) + pt_height * cb.y1; r->y2 = pt_height * r->y2 * (cb.y2 - cb.y1) + pt_height * cb.y1; if (! strcmp (keyword, ":highlight-region")) { const double deg = M_PI / 180.0; double rad; r->x1 += line_width / 2; r->x2 -= line_width / 2; r->y1 += line_width / 2; r->y2 -= line_width / 2; rad = MIN (5, MIN (r->x2 - r->x1, r->y2 - r->y1) / 2.0); cairo_move_to (cr, r->x1 , r->y1 + rad); cairo_arc (cr, r->x1 + rad, r->y1 + rad, rad, 180 * deg, 270 * deg); cairo_arc (cr, r->x2 - rad, r->y1 + rad, rad, 270 * deg, 360 * deg); cairo_arc (cr, r->x2 - rad, r->y2 - rad, rad, 0 * deg, 90 * deg); cairo_arc (cr, r->x1 + rad, r->y2 - rad, rad, 90 * deg, 180 * deg); cairo_close_path (cr); cairo_set_source_rgba (cr, bg.red / 65535.0, bg.green / 65535.0, bg.blue / 65535.0, alpha); cairo_fill_preserve (cr); cairo_set_source_rgba (cr, fg.red / 65535.0, fg.green / 65535.0, fg.blue / 65535.0, 1.0); cairo_set_line_width (cr, line_width); cairo_stroke (cr); } else { gboolean is_single_line = ! strcmp (keyword, ":highlight-line"); if (is_single_line) { gdouble m = r->y1 + (r->y2 - r->y1) / 2; /* Make the rectangle flat, otherwise poppler frequently renders neighboring lines.*/ r->y1 = m; r->y2 = m; } poppler_page_render_selection (page, cr, r, NULL, POPPLER_SELECTION_GLYPH, &fg, &bg); } } } else perror_if_not (0, "Unknown render command: %s", keyword); } if (cb.x1 != 0 || cb.y1 != 0 || cb.x2 != 1 || cb.y2 != 1) { int height = cairo_image_surface_get_height (surface); cairo_rectangle_int_t r = {(int) (width * cb.x1 + 0.5), (int) (height * cb.y1 + 0.5), (int) (width * (cb.x2 - cb.x1) + 0.5), (int) (height * (cb.y2 - cb.y1) + 0.5)}; cairo_surface_t *nsurface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, r.width, r.height); perror_if_not (cairo_surface_status (surface) == CAIRO_STATUS_SUCCESS, "%s", "Failed to create cairo surface"); cairo_destroy (cr); cr = cairo_create (nsurface); perror_if_not (cairo_status (cr) == CAIRO_STATUS_SUCCESS, "%s", "Failed to create cairo context"); cairo_set_source_surface (cr, surface, -r.x, -r.y); cairo_paint (cr); cairo_surface_destroy (surface); surface = nsurface; } theend: image_write_print_response (surface, PNG); error: if (error_msg) g_free (error_msg); if (cr) cairo_destroy (cr); if (surface) cairo_surface_destroy (surface); if (page) g_object_unref (page); } const command_arg_type_t cmd_boundingbox_spec[] = { ARG_DOC, ARG_NATNUM, /* page number */ /* region */ }; static void cmd_boundingbox (const epdfinfo_t *ctx, const command_arg_t *args) { document_t *doc = args[0].value.doc; int pn = args[1].value.natnum; PopplerPage *page = poppler_document_get_page(doc->pdf, pn - 1); cairo_surface_t *surface = NULL; int width, height; double pt_width, pt_height; unsigned char *data, *data_p; PopplerRectangle bbox; int i, j; perror_if_not (page, "No such page %d", pn); poppler_page_get_size (page, &pt_width, &pt_height); surface = image_render_page (doc->pdf, page, (int) pt_width, 1, &doc->options.render); perror_if_not (cairo_surface_status(surface) == CAIRO_STATUS_SUCCESS, "Failed to render page"); width = cairo_image_surface_get_width (surface); height = cairo_image_surface_get_height (surface); data = cairo_image_surface_get_data (surface); /* Determine the bbox by comparing each pixel in the 4 corner stripes with the origin. */ for (i = 0; i < width; ++i) { data_p = data + 4 * i; for (j = 0; j < height; ++j, data_p += 4 * width) { if (! ARGB_EQUAL (data, data_p)) break; } if (j < height) break; } bbox.x1 = i; for (i = width - 1; i > -1; --i) { data_p = data + 4 * i; for (j = 0; j < height; ++j, data_p += 4 * width) { if (! ARGB_EQUAL (data, data_p)) break; } if (j < height) break; } bbox.x2 = i + 1; for (i = 0; i < height; ++i) { data_p = data + 4 * i * width; for (j = 0; j < width; ++j, data_p += 4) { if (! ARGB_EQUAL (data, data_p)) break; } if (j < width) break; } bbox.y1 = i; for (i = height - 1; i > -1; --i) { data_p = data + 4 * i * width; for (j = 0; j < width; ++j, data_p += 4) { if (! ARGB_EQUAL (data, data_p)) break; } if (j < width) break; } bbox.y2 = i + 1; OK_BEGIN (); if (bbox.x1 >= bbox.x2 || bbox.y1 >= bbox.y2) { /* empty page */ puts ("0:0:1:1"); } else { printf ("%f:%f:%f:%f\n", bbox.x1 / width, bbox.y1 / height, bbox.x2 / width, bbox.y2 / height); } OK_END (); error: if (surface) cairo_surface_destroy (surface); if (page) g_object_unref (page); } const command_arg_type_t cmd_charlayout_spec[] = { ARG_DOC, ARG_NATNUM, /* page number */ ARG_EDGES_OR_POSITION, /* region or position */ }; static void cmd_charlayout(const epdfinfo_t *ctx, const command_arg_t *args) { PopplerDocument *doc = args[0].value.doc->pdf; int pn = args[1].value.natnum; PopplerRectangle region = args[2].value.rectangle; double width, height; PopplerPage *page = poppler_document_get_page(doc, pn - 1); char *text = NULL; char *text_p; PopplerRectangle *rectangles = NULL; guint nrectangles; int i; gboolean have_position = region.y2 < 0; perror_if_not (page, "No such page %d", pn); text = poppler_page_get_text (page); text_p = text; poppler_page_get_text_layout (page, &rectangles, &nrectangles); poppler_page_get_size (page, &width, &height); region.x1 *= width; region.x2 *= width; region.y1 *= height; region.y2 *= height; OK_BEGIN (); for (i = 0; i < nrectangles && *text_p; ++i) { PopplerRectangle *r = &rectangles[i]; char *nextc = g_utf8_offset_to_pointer (text_p, 1); if ((have_position && region.x1 >= r->x1 && region.x1 <= r->x2 && region.y1 >= r->y1 && region.y1 <= r->y2) || (! have_position && r->x1 >= region.x1 && r->y1 >= region.y1 && r->x2 <= region.x2 && r->y2 <= region.y2)) { char endc = *nextc; printf ("%f %f %f %f:", r->x1 / width, r->y1 / height, r->x2 / width, r->y2 / height); *nextc = '\0'; print_response_string (text_p, NEWLINE); *nextc = endc; } text_p = nextc; } OK_END (); g_free (rectangles); g_object_unref (page); g_free (text); error: return; } const document_option_t document_options [] = { DEC_DOPT (":render/usecolors", ARG_BOOL, render.usecolors), DEC_DOPT (":render/printed", ARG_BOOL, render.printed), DEC_DOPT (":render/foreground", ARG_COLOR, render.fg), DEC_DOPT (":render/background", ARG_COLOR, render.bg), }; const command_arg_type_t cmd_getoptions_spec[] = { ARG_DOC, }; static void cmd_getoptions(const epdfinfo_t *ctx, const command_arg_t *args) { document_t *doc = args[0].value.doc; int i; OK_BEGIN (); for (i = 0; i < G_N_ELEMENTS (document_options); ++i) { command_arg_t arg; arg.type = document_options[i].type; memcpy (&arg.value, ((char*) &doc->options) + document_options[i].offset, command_arg_type_size (arg.type)); print_response_string (document_options[i].name, COLON); command_arg_print (&arg); puts(""); } OK_END (); } const command_arg_type_t cmd_setoptions_spec[] = { ARG_DOC, ARG_REST /* key value pairs */ }; static void cmd_setoptions(const epdfinfo_t *ctx, const command_arg_t *args) { int i = 0; document_t *doc = args[0].value.doc; int nrest = args[1].value.rest.nargs; char * const *rest = args[1].value.rest.args; gchar *error_msg = NULL; document_options_t opts = doc->options; const size_t nopts = G_N_ELEMENTS (document_options); perror_if_not (nrest % 2 == 0, "Even number of key/value pairs expected"); while (i < nrest) { int j; command_arg_t key, value; perror_if_not (command_arg_parse_arg (ctx, rest[i], &key, ARG_NONEMPTY_STRING, &error_msg), "%s", error_msg); ++i; for (j = 0; j < nopts; ++j) { const document_option_t *dopt = &document_options[j]; if (! strcmp (key.value.string, dopt->name)) { perror_if_not (command_arg_parse_arg (ctx, rest[i], &value, dopt->type, &error_msg), "%s", error_msg); memcpy (((char*) &opts) + dopt->offset, &value.value, command_arg_type_size (value.type)); break; } } perror_if_not (j < nopts, "Unknown option: %s", key.value.string); ++i; } doc->options = opts; cmd_getoptions (ctx, args); error: if (error_msg) g_free (error_msg); } const command_arg_type_t cmd_pagelabels_spec[] = { ARG_DOC, }; static void cmd_pagelabels(const epdfinfo_t *ctx, const command_arg_t *args) { PopplerDocument *doc = args[0].value.doc->pdf; int i; OK_BEGIN (); for (i = 0; i < poppler_document_get_n_pages (doc); ++i) { PopplerPage *page = poppler_document_get_page(doc, i); gchar *label = poppler_page_get_label (page); print_response_string (label ? label : "", NEWLINE); g_object_unref (page); g_free (label); } OK_END (); } const command_arg_type_t cmd_ping_spec[] = { ARG_STRING /* any message */ }; static void cmd_ping (const epdfinfo_t *ctx, const command_arg_t *args) { const gchar *msg = args[0].value.string; OK_BEGIN (); print_response_string (msg, NEWLINE); OK_END (); } /* ================================================================== * * Main * ================================================================== */ static const command_t commands [] = { /* Basic */ DEC_CMD (ping), DEC_CMD (features), DEC_CMD (open), DEC_CMD (close), DEC_CMD (quit), DEC_CMD (getoptions), DEC_CMD (setoptions), /* Searching */ DEC_CMD2 (search_string, "search-string"), DEC_CMD2 (search_regexp, "search-regexp"), DEC_CMD2 (regexp_flags, "regexp-flags"), /* General Informations */ DEC_CMD (metadata), DEC_CMD (outline), DEC_CMD2 (number_of_pages, "number-of-pages"), DEC_CMD (pagelinks), DEC_CMD (gettext), DEC_CMD (getselection), DEC_CMD (pagesize), DEC_CMD (boundingbox), DEC_CMD (charlayout), /* General Informations */ DEC_CMD (metadata), DEC_CMD (outline), DEC_CMD2 (number_of_pages, "number-of-pages"), DEC_CMD (pagelinks), DEC_CMD (gettext), DEC_CMD (getselection), DEC_CMD (pagesize), DEC_CMD (boundingbox), DEC_CMD (charlayout), DEC_CMD (pagelabels), /* Annotations */ DEC_CMD (getannots), DEC_CMD (getannot), #ifdef HAVE_POPPLER_ANNOT_WRITE DEC_CMD (addannot), DEC_CMD (delannot), DEC_CMD (editannot), DEC_CMD (save), #endif /* Attachments */ DEC_CMD2 (getattachment_from_annot, "getattachment-from-annot"), DEC_CMD (getattachments), /* Synctex */ DEC_CMD2 (synctex_forward_search, "synctex-forward-search"), DEC_CMD2 (synctex_backward_search, "synctex-backward-search"), /* Rendering */ DEC_CMD (renderpage), }; int main(int argc, char **argv) { epdfinfo_t ctx = {0}; char *line = NULL; ssize_t read; size_t line_size; const char *error_log = "/dev/null"; #ifdef __MINGW32__ error_log = "NUL"; _setmode(_fileno(stdin), _O_BINARY); _setmode(_fileno(stdout), _O_BINARY); #endif if (argc > 2) { fprintf(stderr, "usage: epdfinfo [ERROR-LOGFILE]\n"); exit (EXIT_FAILURE); } if (argc == 2) error_log = argv[1]; if (! freopen (error_log, "a", stderr)) err (2, "Unable to redirect stderr"); #if ! GLIB_CHECK_VERSION(2,36,0) g_type_init (); #endif ctx.documents = g_hash_table_new (g_str_hash, g_str_equal); setvbuf (stdout, NULL, _IOFBF, BUFSIZ); while ((read = getline (&line, &line_size, stdin)) != -1) { int nargs = 0; command_arg_t *cmd_args = NULL; char **args = NULL; gchar *error_msg = NULL; int i; if (read <= 1 || line[read - 1] != '\n') { fprintf (stderr, "Skipped parts of a line: `%s'\n", line); goto next_line; } line[read - 1] = '\0'; args = command_arg_split (line, &nargs); if (nargs == 0) continue; for (i = 0; i < G_N_ELEMENTS (commands); i++) { if (! strcmp (commands[i].name, args[0])) { if (commands[i].nargs == 0 || (cmd_args = command_arg_parse (&ctx, args + 1, nargs - 1, commands + i, &error_msg))) { commands[i].execute (&ctx, cmd_args); if (commands[i].nargs > 0) free_command_args (cmd_args, commands[i].nargs); } else { printf_error_response ("%s", error_msg ? error_msg : "Unknown error (this is a bug)"); } if (error_msg) g_free (error_msg); break; } } if (G_N_ELEMENTS (commands) == i) { printf_error_response ("Unknown command: %s", args[0]); } for (i = 0; i < nargs; ++i) g_free (args[i]); g_free (args); next_line: free (line); line = NULL; } if (ferror (stdin)) err (2, NULL); exit (EXIT_SUCCESS); } pdf-tools-0.80/server/epdfinfo.h000066400000000000000000000171021315513274600166310ustar00rootroot00000000000000// Copyright (C) 2013, 2014 Andreas Politz // Author: Andreas Politz // 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 . #ifndef _EPDF_H_ #define _EPDF_H_ _EPDF_H_ #include "config.h" #include #include #include /* Some library functions print warnings to stdout, inhibit it. */ #define DISCARD_STDOUT(saved_fd) \ do { \ int fd; \ fflush(stdout); \ saved_fd = dup(1); \ fd = open("/dev/null", O_WRONLY); \ dup2(fd, 1); \ close(fd); \ } while (0) #define UNDISCARD_STDOUT(saved_fd) \ do { \ fflush(stdout); \ dup2(saved_fd, 1); \ close(saved_fd); \ } while (0) /* Writing responses */ #define OK_BEGIN() \ do { \ puts("OK"); \ } while (0) #define OK_END() \ do { \ puts("."); \ fflush (stdout); \ } while (0) #define OK() \ do { \ puts ("OK\n."); \ fflush (stdout); \ } while (0) /* Dealing with image data. */ #ifdef WORDS_BIGENDIAN #define ARGB_TO_RGB(rgb, argb) \ do { \ rgb[0] = argb[1]; \ rgb[1] = argb[2]; \ rgb[2] = argb[3]; \ } while (0) #define ARGB_EQUAL(argb1, argb2) \ (argb1[1] == argb2[1] \ && argb1[2] == argb2[2] \ && argb1[3] == argb2[3]) #else #define ARGB_TO_RGB(rgb, argb) \ do { \ rgb[0] = argb[2]; \ rgb[1] = argb[1]; \ rgb[2] = argb[0]; \ } while (0) #define ARGB_EQUAL(argb1, argb2) \ (argb1[0] == argb2[0] \ && argb1[1] == argb2[1] \ && argb1[2] == argb2[2]) #endif #define NORMALIZE_PAGE_ARG(doc, first, last) \ *first = MAX(1, *first); \ if (*last <= 0) \ *last = poppler_document_get_n_pages (doc); \ else \ *last = MIN(*last, poppler_document_get_n_pages (doc)); /* png_jmpbuf is supposed to be not available in older versions of libpng. */ #ifndef png_jmpbuf # define png_jmpbuf(png_ptr) ((png_ptr)->jmpbuf) #endif #ifndef HAVE_ERROR_H # define error(status, errno, fmt, args...) \ do { \ int error = (errno); \ fflush (stdout); \ fprintf (stderr, "%s: " fmt, PACKAGE_NAME, ## args); \ if (error) \ fprintf (stderr, ": %s", strerror (error)); \ fprintf (stderr, "\n"); \ exit (status); \ } while (0) #endif #define internal_error(fmt, args...) \ error (2, 0, "internal error in %s: " fmt, __func__, ## args) #define error_if_not(expr) \ if (! (expr)) goto error; #define perror_if_not(expr, fmt, args...) \ do { \ if (! (expr)) \ { \ printf_error_response ((fmt), ## args); \ goto error; \ } \ } while (0) #define cerror_if_not(expr, error_msg, fmt, args...) \ do { \ if (! (expr)) \ { \ if (error_msg) \ *(error_msg) = g_strdup_printf((fmt), ## args); \ goto error; \ } \ } while (0) /* Declare commands */ #define DEC_CMD(name) \ {#name, cmd_ ## name, cmd_ ## name ## _spec, \ G_N_ELEMENTS (cmd_ ## name ## _spec)} #define DEC_CMD2(command, name) \ {name, cmd_ ## command, cmd_ ## command ## _spec, \ G_N_ELEMENTS (cmd_ ## command ## _spec)} /* Declare option */ #define DEC_DOPT(name, type, sname) \ {name, type, offsetof (document_options_t, sname)} enum suffix_char { NONE, COLON, NEWLINE}; enum image_type { PPM, PNG }; typedef struct { PopplerAnnotMapping *amap; gchar *key; } annotation_t; typedef enum { ARG_INVALID = 0, ARG_DOC, ARG_BOOL, ARG_STRING, ARG_NONEMPTY_STRING, ARG_NATNUM, ARG_EDGE, ARG_EDGE_OR_NEGATIVE, ARG_EDGES, ARG_EDGES_OR_POSITION, ARG_COLOR, ARG_REST } command_arg_type_t; typedef struct { const char *name; command_arg_type_t type; size_t offset; } document_option_t; typedef struct { PopplerColor bg, fg; gboolean usecolors; gboolean printed; } render_options_t; typedef struct { render_options_t render; } document_options_t; typedef struct { PopplerDocument *pdf; char *filename; char *passwd; struct { GHashTable *keys; /* key => page */ GList **pages; /* page array */ } annotations; document_options_t options; } document_t; typedef struct { command_arg_type_t type; union { gboolean flag; const char *string; long natnum; document_t *doc; gdouble edge; PopplerColor color; PopplerRectangle rectangle; #ifdef HAVE_POPPLER_ANNOT_MARKUP PopplerQuadrilateral quadrilateral; #endif struct { char * const *args; int nargs; } rest; } value; } command_arg_t; typedef struct { GHashTable *documents; } epdfinfo_t; typedef struct { const char *name; void (* execute) (const epdfinfo_t *ctxt, const command_arg_t *args); const command_arg_type_t *args_spec; int nargs; } command_t; /* Defined in poppler-hack.cc */ #ifdef HAVE_POPPLER_ANNOT_WRITE extern void xpoppler_annot_set_rectangle (PopplerAnnot*, PopplerRectangle*); #endif extern gchar *xpoppler_annot_markup_get_created (PopplerAnnotMarkup*); #endif /* _EPDF_H_ */ pdf-tools-0.80/server/m4/000077500000000000000000000000001315513274600152055ustar00rootroot00000000000000pdf-tools-0.80/server/m4/ax_check_compile_flag.m4000066400000000000000000000064031315513274600217200ustar00rootroot00000000000000# =========================================================================== # https://www.gnu.org/software/autoconf-archive/ax_check_compile_flag.html # =========================================================================== # # SYNOPSIS # # AX_CHECK_COMPILE_FLAG(FLAG, [ACTION-SUCCESS], [ACTION-FAILURE], [EXTRA-FLAGS], [INPUT]) # # DESCRIPTION # # Check whether the given FLAG works with the current language's compiler # or gives an error. (Warnings, however, are ignored) # # ACTION-SUCCESS/ACTION-FAILURE are shell commands to execute on # success/failure. # # If EXTRA-FLAGS is defined, it is added to the current language's default # flags (e.g. CFLAGS) when the check is done. The check is thus made with # the flags: "CFLAGS EXTRA-FLAGS FLAG". This can for example be used to # force the compiler to issue an error when a bad flag is given. # # INPUT gives an alternative input source to AC_COMPILE_IFELSE. # # NOTE: Implementation based on AX_CFLAGS_GCC_OPTION. Please keep this # macro in sync with AX_CHECK_{PREPROC,LINK}_FLAG. # # LICENSE # # Copyright (c) 2008 Guido U. Draheim # Copyright (c) 2011 Maarten Bosmans # # 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 . # # As a special exception, the respective Autoconf Macro's copyright owner # gives unlimited permission to copy, distribute and modify the configure # scripts that are the output of Autoconf when processing the Macro. You # need not follow the terms of the GNU General Public License when using # or distributing such scripts, even though portions of the text of the # Macro appear in them. The GNU General Public License (GPL) does govern # all other use of the material that constitutes the Autoconf Macro. # # This special exception to the GPL applies to versions of the Autoconf # Macro released by the Autoconf Archive. When you make and distribute a # modified version of the Autoconf Macro, you may extend this special # exception to the GPL to apply to your modified version as well. #serial 5 AC_DEFUN([AX_CHECK_COMPILE_FLAG], [AC_PREREQ(2.64)dnl for _AC_LANG_PREFIX and AS_VAR_IF AS_VAR_PUSHDEF([CACHEVAR],[ax_cv_check_[]_AC_LANG_ABBREV[]flags_$4_$1])dnl AC_CACHE_CHECK([whether _AC_LANG compiler accepts $1], CACHEVAR, [ ax_check_save_flags=$[]_AC_LANG_PREFIX[]FLAGS _AC_LANG_PREFIX[]FLAGS="$[]_AC_LANG_PREFIX[]FLAGS $4 $1" AC_COMPILE_IFELSE([m4_default([$5],[AC_LANG_PROGRAM()])], [AS_VAR_SET(CACHEVAR,[yes])], [AS_VAR_SET(CACHEVAR,[no])]) _AC_LANG_PREFIX[]FLAGS=$ax_check_save_flags]) AS_VAR_IF(CACHEVAR,yes, [m4_default([$2], :)], [m4_default([$3], :)]) AS_VAR_POPDEF([CACHEVAR])dnl ])dnl AX_CHECK_COMPILE_FLAGS pdf-tools-0.80/server/poppler-hack.cc000066400000000000000000000062221315513274600175630ustar00rootroot00000000000000// Copyright (C) 2013, 2014 Andreas Politz // 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 . #include #include #include #include #include extern "C" { GType poppler_annot_get_type (void) G_GNUC_CONST; GType poppler_annot_markup_get_type (void) G_GNUC_CONST; #define POPPLER_TYPE_ANNOT (poppler_annot_get_type ()) #define POPPLER_ANNOT(obj) \ (G_TYPE_CHECK_INSTANCE_CAST ((obj), POPPLER_TYPE_ANNOT, PopplerAnnot)) #define POPPLER_IS_ANNOT_MARKUP(obj) \ (G_TYPE_CHECK_INSTANCE_TYPE ((obj), POPPLER_TYPE_ANNOT_MARKUP)) #define POPPLER_TYPE_ANNOT_MARKUP (poppler_annot_markup_get_type ()) struct PopplerAnnot { GObject parent_instance; Annot *annot; }; struct PopplerAnnotMarkup { GObject parent_instance; }; struct PopplerRectangle { double x1; double y1; double x2; double y2; }; char *_xpoppler_goo_string_to_utf8(GooString *s) { char *result; if (! s) return NULL; if (s->hasUnicodeMarker()) { result = g_convert (s->getCString () + 2, s->getLength () - 2, "UTF-8", "UTF-16BE", NULL, NULL, NULL); } else { int len; gunichar *ucs4_temp; int i; len = s->getLength (); ucs4_temp = g_new (gunichar, len + 1); for (i = 0; i < len; ++i) { ucs4_temp[i] = pdfDocEncoding[(unsigned char)s->getChar(i)]; } ucs4_temp[i] = 0; result = g_ucs4_to_utf8 (ucs4_temp, -1, NULL, NULL, NULL); g_free (ucs4_temp); } return result; } #ifdef HAVE_POPPLER_ANNOT_WRITE // Set the rectangle of an annotation. It was first added in v0.26. void xpoppler_annot_set_rectangle (PopplerAnnot *a, PopplerRectangle *rectangle) { GooString *state = a->annot->getAppearState (); char *ustate = _xpoppler_goo_string_to_utf8 (state); a->annot->setRect (rectangle->x1, rectangle->y1, rectangle->x2, rectangle->y2); a->annot->setAppearanceState (ustate); g_free (ustate); } #endif // This function is in the library, but the enforced date parsing is // incomplete (at least in some versions), because it ignores the // timezone. gchar *xpoppler_annot_markup_get_created (PopplerAnnotMarkup *poppler_annot) { AnnotMarkup *annot; GooString *text; g_return_val_if_fail (POPPLER_IS_ANNOT_MARKUP (poppler_annot), NULL); annot = static_cast(POPPLER_ANNOT (poppler_annot)->annot); text = annot->getDate (); return text ? _xpoppler_goo_string_to_utf8 (text) : NULL; } } pdf-tools-0.80/server/poppler-versions000066400000000000000000000005421315513274600201400ustar00rootroot00000000000000HAVE_POPPLER_ANNOT_WRITE 0.19.4 solves bug 49080, which potentially corrupts PDF files. HAVE_POPPLER_FIND_OPTS 0.22 PopplerFindFlags 0.22 poppler_page_find_text_with_options HAVE_POPPLER_ANNOT_SET_RECT 0.26 Adds function poppler_annot_set_rectangle HAVE_POPPLER_ANNOT_MARKUP 0.26 poppler_annot_text_markup_new_{highlight,squiggly,strikeout,underline} pdf-tools-0.80/server/synctex_parser.c000066400000000000000000004652371315513274600201230ustar00rootroot00000000000000/* Copyright (c) 2008, 2009, 2010 , 2011 jerome DOT laurens AT u-bourgogne DOT fr This file is part of the SyncTeX package. Latest Revision: Tue Jun 14 08:23:30 UTC 2011 Version: 1.16 See synctex_parser_readme.txt for more details License: -------- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE Except as contained in this notice, the name of the copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from the copyright holder. Acknowledgments: ---------------- The author received useful remarks from the pdfTeX developers, especially Hahn The Thanh, and significant help from XeTeX developer Jonathan Kew Nota Bene: ---------- If you include or use a significant part of the synctex package into a software, I would appreciate to be listed as contributor and see "SyncTeX" highlighted. Version 1 Thu Jun 19 09:39:21 UTC 2008 */ /* We assume that high level application like pdf viewers will want * to embed this code as is. We assume that they also have locale.h and setlocale. * For other tools such as TeXLive tools, you must define SYNCTEX_USE_LOCAL_HEADER, * when building. You also have to create and customize synctex_parser_local.h to fit your system. * In particular, the HAVE_LOCALE_H and HAVE_SETLOCALE macros should be properly defined. * With this design, you should not need to edit this file. */ # if defined(SYNCTEX_USE_LOCAL_HEADER) # include "synctex_parser_local.h" # else # define HAVE_LOCALE_H 1 # define HAVE_SETLOCALE 1 # if defined(_MSC_VER) # define SYNCTEX_INLINE __inline # else # define SYNCTEX_INLINE inline # endif # endif #include #include #include #include #include #if defined(HAVE_LOCALE_H) #include #endif /* The data is organized in a graph with multiple entries. * The root object is a scanner, it is created with the contents on a synctex file. * Each leaf of the tree is a synctex_node_t object. * There are 3 subtrees, two of them sharing the same leaves. * The first tree is the list of input records, where input file names are associated with tags. * The second tree is the box tree as given by TeX when shipping pages out. * First level objects are sheets, containing boxes, glues, kerns... * The third tree allows to browse leaves according to tag and line. */ #include "synctex_parser.h" #include "synctex_parser_utils.h" #define printf(fmt, args...) (fprintf (stderr, (fmt), ## args)) /* These are the possible extensions of the synctex file */ const char * synctex_suffix = ".synctex"; const char * synctex_suffix_gz = ".gz"; /* each synctex node has a class */ typedef struct __synctex_class_t _synctex_class_t; typedef _synctex_class_t * synctex_class_t; /* synctex_node_t is a pointer to a node * _synctex_node is the target of the synctex_node_t pointer * It is a pseudo object oriented program. * class is a pointer to the class object the node belongs to. * implementation is meant to contain the private data of the node * basically, there are 2 kinds of information: navigation information and * synctex information. Both will depend on the type of the node, * thus different nodes will have different private data. * There is no inheritancy overhead. */ typedef union _synctex_info_t { int INT; char * PTR; } synctex_info_t; struct _synctex_node { synctex_class_t class; synctex_info_t * implementation; }; /* Each node of the tree, except the scanner itself belongs to a class. * The class object is just a struct declaring the owning scanner * This is a pointer to the scanner as root of the tree. * The type is used to identify the kind of node. * The class declares pointers to a creator and a destructor method. * The log and display fields are used to log and display the node. * display will also display the child, sibling and parent sibling. * parent, child and sibling are used to navigate the tree, * from TeX box hierarchy point of view. * The friend field points to a method which allows to navigate from friend to friend. * A friend is a node with very close tag and line numbers. * Finally, the info field point to a method giving the private node info offset. */ typedef synctex_node_t *(*_synctex_node_getter_t)(synctex_node_t); typedef synctex_info_t *(*_synctex_info_getter_t)(synctex_node_t); struct __synctex_class_t { synctex_scanner_t scanner; int type; synctex_node_t (*new)(synctex_scanner_t scanner); void (*free)(synctex_node_t); void (*log)(synctex_node_t); void (*display)(synctex_node_t); _synctex_node_getter_t parent; _synctex_node_getter_t child; _synctex_node_getter_t sibling; _synctex_node_getter_t friend; _synctex_node_getter_t next_box; _synctex_info_getter_t info; }; # ifdef SYNCTEX_NOTHING # pragma mark - # pragma mark Abstract OBJECTS and METHODS # endif /* These macros are shortcuts * This macro checks if a message can be sent. */ # define SYNCTEX_CAN_PERFORM(NODE,SELECTOR)\ (NULL!=((((NODE)->class))->SELECTOR)) /* This macro is some kind of objc_msg_send. * It takes care of sending the proper message if possible. */ # define SYNCTEX_MSG_SEND(NODE,SELECTOR) if (NODE && SYNCTEX_CAN_PERFORM(NODE,SELECTOR)) {\ (*((((NODE)->class))->SELECTOR))(NODE);\ } /* read only safe getter */ # define SYNCTEX_GET(NODE,SELECTOR)((NODE && SYNCTEX_CAN_PERFORM(NODE,SELECTOR))?SYNCTEX_GETTER(NODE,SELECTOR)[0]:(NULL)) /* read/write getter */ # define SYNCTEX_GETTER(NODE,SELECTOR)\ ((synctex_node_t *)((*((((NODE)->class))->SELECTOR))(NODE))) # define SYNCTEX_FREE(NODE) SYNCTEX_MSG_SEND(NODE,free); /* Parent getter and setter */ # define SYNCTEX_PARENT(NODE) SYNCTEX_GET(NODE,parent) # define SYNCTEX_SET_PARENT(NODE,NEW_PARENT) if (NODE && NEW_PARENT && SYNCTEX_CAN_PERFORM(NODE,parent)){\ SYNCTEX_GETTER(NODE,parent)[0]=NEW_PARENT;\ } /* Child getter and setter */ # define SYNCTEX_CHILD(NODE) SYNCTEX_GET(NODE,child) # define SYNCTEX_SET_CHILD(NODE,NEW_CHILD) if (NODE && NEW_CHILD){\ SYNCTEX_GETTER(NODE,child)[0]=NEW_CHILD;\ SYNCTEX_GETTER(NEW_CHILD,parent)[0]=NODE;\ } /* Sibling getter and setter */ # define SYNCTEX_SIBLING(NODE) SYNCTEX_GET(NODE,sibling) # define SYNCTEX_SET_SIBLING(NODE,NEW_SIBLING) if (NODE && NEW_SIBLING) {\ SYNCTEX_GETTER(NODE,sibling)[0]=NEW_SIBLING;\ if (SYNCTEX_CAN_PERFORM(NEW_SIBLING,parent) && SYNCTEX_CAN_PERFORM(NODE,parent)) {\ SYNCTEX_GETTER(NEW_SIBLING,parent)[0]=SYNCTEX_GETTER(NODE,parent)[0];\ }\ } /* Friend getter and setter. A friend is a kern, math, glue or void box node which tag and line numbers are similar. * This is a first filter on the nodes that avoids testing all of them. * Friends are used mainly in forward synchronization aka from source to output. */ # define SYNCTEX_FRIEND(NODE) SYNCTEX_GET(NODE,friend) # define SYNCTEX_SET_FRIEND(NODE,NEW_FRIEND) if (NODE && NEW_FRIEND){\ SYNCTEX_GETTER(NODE,friend)[0]=NEW_FRIEND;\ } /* Next box getter and setter. The box tree can be traversed from one horizontal box to the other. * Navigation starts with the deeper boxes. */ # define SYNCTEX_NEXT_HORIZ_BOX(NODE) SYNCTEX_GET(NODE,next_box) # define SYNCTEX_SET_NEXT_HORIZ_BOX(NODE,NEXT_BOX) if (NODE && NEXT_BOX){\ SYNCTEX_GETTER(NODE,next_box)[0]=NEXT_BOX;\ } void _synctex_free_node(synctex_node_t node); void _synctex_free_leaf(synctex_node_t node); /* A node is meant to own its child and sibling. * It is not owned by its parent, unless it is its first child. * This destructor is for all nodes with children. */ void _synctex_free_node(synctex_node_t node) { if (node) { (*((node->class)->sibling))(node); SYNCTEX_FREE(SYNCTEX_SIBLING(node)); SYNCTEX_FREE(SYNCTEX_CHILD(node)); free(node); } return; } /* A node is meant to own its child and sibling. * It is not owned by its parent, unless it is its first child. * This destructor is for nodes with no child. */ void _synctex_free_leaf(synctex_node_t node) { if (node) { SYNCTEX_FREE(SYNCTEX_SIBLING(node)); free(node); } return; } # ifdef __SYNCTEX_WORK__ # include "/usr/include/zlib.h" # else # include # endif /* The synctex scanner is the root object. * Is is initialized with the contents of a text file or a gzipped file. * The buffer_? are first used to parse the text. */ struct __synctex_scanner_t { gzFile file; /* The (possibly compressed) file */ char * buffer_cur; /* current location in the buffer */ char * buffer_start; /* start of the buffer */ char * buffer_end; /* end of the buffer */ char * output_fmt; /* dvi or pdf, not yet used */ char * output; /* the output name used to create the scanner */ char * synctex; /* the .synctex or .synctex.gz name used to create the scanner */ int version; /* 1, not yet used */ struct { unsigned has_parsed:1; /* Whether the scanner has parsed its underlying synctex file. */ unsigned reserved:sizeof(unsigned)-1; /* alignment */ } flags; int pre_magnification; /* magnification from the synctex preamble */ int pre_unit; /* unit from the synctex preamble */ int pre_x_offset; /* X offste from the synctex preamble */ int pre_y_offset; /* Y offset from the synctex preamble */ int count; /* Number of records, from the synctex postamble */ float unit; /* real unit, from synctex preamble or post scriptum */ float x_offset; /* X offset, from synctex preamble or post scriptum */ float y_offset; /* Y Offset, from synctex preamble or post scriptum */ synctex_node_t sheet; /* The first sheet node, its siblings are the other sheet nodes */ synctex_node_t input; /* The first input node, its siblings are the other input nodes */ int number_of_lists; /* The number of friend lists */ synctex_node_t * lists_of_friends;/* The friend lists */ _synctex_class_t class[synctex_node_number_of_types]; /* The classes of the nodes of the scanner */ }; /* SYNCTEX_CUR, SYNCTEX_START and SYNCTEX_END are convenient shortcuts */ # define SYNCTEX_CUR (scanner->buffer_cur) # define SYNCTEX_START (scanner->buffer_start) # define SYNCTEX_END (scanner->buffer_end) # ifdef SYNCTEX_NOTHING # pragma mark - # pragma mark OBJECTS, their creators and destructors. # endif /* Here, we define the indices for the different informations. * They are used to declare the size of the implementation. * For example, if one object uses SYNCTEX_HORIZ_IDX is its size, * then its info will contain a tag, line, column, horiz but no width nor height nor depth */ /* The sheet is a first level node. * It has no parent (the parent is the scanner itself) * Its sibling points to another sheet. * Its child points to its first child, in general a box. * A sheet node contains only one synctex information: the page. * This is the 1 based page index as given by TeX. */ /* The next macros are used to access the node info * SYNCTEX_INFO(node) points to the first synctex integer or pointer data of node * SYNCTEX_INFO(node)[index] is the information at index * for example, the page of a sheet is stored in SYNCTEX_INFO(sheet)[SYNCTEX_PAGE_IDX] */ # define SYNCTEX_INFO(NODE) ((*((((NODE)->class))->info))(NODE)) # define SYNCTEX_PAGE_IDX 0 # define SYNCTEX_PAGE(NODE) SYNCTEX_INFO(NODE)[SYNCTEX_PAGE_IDX].INT /* This macro defines implementation offsets * It is only used for pointer values */ # define SYNCTEX_MAKE_GET(SYNCTEX_GETTER,OFFSET)\ synctex_node_t * SYNCTEX_GETTER (synctex_node_t node);\ synctex_node_t * SYNCTEX_GETTER (synctex_node_t node) {\ return node?(synctex_node_t *)((&((node)->implementation))+OFFSET):NULL;\ } SYNCTEX_MAKE_GET(_synctex_implementation_0,0) SYNCTEX_MAKE_GET(_synctex_implementation_1,1) SYNCTEX_MAKE_GET(_synctex_implementation_2,2) SYNCTEX_MAKE_GET(_synctex_implementation_3,3) SYNCTEX_MAKE_GET(_synctex_implementation_4,4) SYNCTEX_MAKE_GET(_synctex_implementation_5,5) typedef struct { synctex_class_t class; synctex_info_t implementation[3+SYNCTEX_PAGE_IDX+1];/* child, sibling, next box, * SYNCTEX_PAGE_IDX */ } synctex_sheet_t; synctex_node_t _synctex_new_sheet(synctex_scanner_t scanner); void _synctex_display_sheet(synctex_node_t sheet); void _synctex_log_sheet(synctex_node_t sheet); static _synctex_class_t synctex_class_sheet = { NULL, /* No scanner yet */ synctex_node_type_sheet, /* Node type */ &_synctex_new_sheet, /* creator */ &_synctex_free_node, /* destructor */ &_synctex_log_sheet, /* log */ &_synctex_display_sheet, /* display */ NULL, /* No parent */ &_synctex_implementation_0, /* child */ &_synctex_implementation_1, /* sibling */ NULL, /* No friend */ &_synctex_implementation_2, /* Next box */ (_synctex_info_getter_t)&_synctex_implementation_3 /* info */ }; /* sheet node creator */ synctex_node_t _synctex_new_sheet(synctex_scanner_t scanner) { synctex_node_t node = _synctex_malloc(sizeof(synctex_sheet_t)); if (node) { node->class = scanner?scanner->class+synctex_node_type_sheet:(synctex_class_t)&synctex_class_sheet; } return node; } /* A box node contains navigation and synctex information * There are different kind of boxes. * Only horizontal boxes are treated differently because of their visible size. */ # define SYNCTEX_TAG_IDX 0 # define SYNCTEX_LINE_IDX (SYNCTEX_TAG_IDX+1) # define SYNCTEX_COLUMN_IDX (SYNCTEX_LINE_IDX+1) # define SYNCTEX_HORIZ_IDX (SYNCTEX_COLUMN_IDX+1) # define SYNCTEX_VERT_IDX (SYNCTEX_HORIZ_IDX+1) # define SYNCTEX_WIDTH_IDX (SYNCTEX_VERT_IDX+1) # define SYNCTEX_HEIGHT_IDX (SYNCTEX_WIDTH_IDX+1) # define SYNCTEX_DEPTH_IDX (SYNCTEX_HEIGHT_IDX+1) /* the corresponding info accessors */ # define SYNCTEX_TAG(NODE) SYNCTEX_INFO(NODE)[SYNCTEX_TAG_IDX].INT # define SYNCTEX_LINE(NODE) SYNCTEX_INFO(NODE)[SYNCTEX_LINE_IDX].INT # define SYNCTEX_COLUMN(NODE) SYNCTEX_INFO(NODE)[SYNCTEX_COLUMN_IDX].INT # define SYNCTEX_HORIZ(NODE) SYNCTEX_INFO(NODE)[SYNCTEX_HORIZ_IDX].INT # define SYNCTEX_VERT(NODE) SYNCTEX_INFO(NODE)[SYNCTEX_VERT_IDX].INT # define SYNCTEX_WIDTH(NODE) SYNCTEX_INFO(NODE)[SYNCTEX_WIDTH_IDX].INT # define SYNCTEX_HEIGHT(NODE) SYNCTEX_INFO(NODE)[SYNCTEX_HEIGHT_IDX].INT # define SYNCTEX_DEPTH(NODE) SYNCTEX_INFO(NODE)[SYNCTEX_DEPTH_IDX].INT # define SYNCTEX_ABS_WIDTH(NODE) ((SYNCTEX_WIDTH(NODE)>0?SYNCTEX_WIDTH(NODE):-SYNCTEX_WIDTH(NODE))) # define SYNCTEX_ABS_HEIGHT(NODE) ((SYNCTEX_HEIGHT(NODE)>0?SYNCTEX_HEIGHT(NODE):-SYNCTEX_HEIGHT(NODE))) # define SYNCTEX_ABS_DEPTH(NODE) ((SYNCTEX_DEPTH(NODE)>0?SYNCTEX_DEPTH(NODE):-SYNCTEX_DEPTH(NODE))) typedef struct { synctex_class_t class; synctex_info_t implementation[5+SYNCTEX_DEPTH_IDX+1]; /* parent,child,sibling,friend,next box, * SYNCTEX_TAG,SYNCTEX_LINE,SYNCTEX_COLUMN, * SYNCTEX_HORIZ,SYNCTEX_VERT,SYNCTEX_WIDTH,SYNCTEX_HEIGHT,SYNCTEX_DEPTH */ } synctex_vert_box_node_t; synctex_node_t _synctex_new_vbox(synctex_scanner_t scanner); void _synctex_log_box(synctex_node_t sheet); void _synctex_display_vbox(synctex_node_t node); /* These are static class objects, each scanner will make a copy of them and setup the scanner field. */ static _synctex_class_t synctex_class_vbox = { NULL, /* No scanner yet */ synctex_node_type_vbox, /* Node type */ &_synctex_new_vbox, /* creator */ &_synctex_free_node, /* destructor */ &_synctex_log_box, /* log */ &_synctex_display_vbox, /* display */ &_synctex_implementation_0, /* parent */ &_synctex_implementation_1, /* child */ &_synctex_implementation_2, /* sibling */ &_synctex_implementation_3, /* friend */ &_synctex_implementation_4, /* next box */ (_synctex_info_getter_t)&_synctex_implementation_5 }; /* vertical box node creator */ synctex_node_t _synctex_new_vbox(synctex_scanner_t scanner) { synctex_node_t node = _synctex_malloc(sizeof(synctex_vert_box_node_t)); if (node) { node->class = scanner?scanner->class+synctex_node_type_vbox:(synctex_class_t)&synctex_class_vbox; } return node; } # define SYNCTEX_HORIZ_V_IDX (SYNCTEX_DEPTH_IDX+1) # define SYNCTEX_VERT_V_IDX (SYNCTEX_HORIZ_V_IDX+1) # define SYNCTEX_WIDTH_V_IDX (SYNCTEX_VERT_V_IDX+1) # define SYNCTEX_HEIGHT_V_IDX (SYNCTEX_WIDTH_V_IDX+1) # define SYNCTEX_DEPTH_V_IDX (SYNCTEX_HEIGHT_V_IDX+1) /* the corresponding info accessors */ # define SYNCTEX_HORIZ_V(NODE) SYNCTEX_INFO(NODE)[SYNCTEX_HORIZ_V_IDX].INT # define SYNCTEX_VERT_V(NODE) SYNCTEX_INFO(NODE)[SYNCTEX_VERT_V_IDX].INT # define SYNCTEX_WIDTH_V(NODE) SYNCTEX_INFO(NODE)[SYNCTEX_WIDTH_V_IDX].INT # define SYNCTEX_HEIGHT_V(NODE) SYNCTEX_INFO(NODE)[SYNCTEX_HEIGHT_V_IDX].INT # define SYNCTEX_DEPTH_V(NODE) SYNCTEX_INFO(NODE)[SYNCTEX_DEPTH_V_IDX].INT # define SYNCTEX_ABS_WIDTH_V(NODE) ((SYNCTEX_WIDTH_V(NODE)>0?SYNCTEX_WIDTH_V(NODE):-SYNCTEX_WIDTH_V(NODE))) # define SYNCTEX_ABS_HEIGHT_V(NODE) ((SYNCTEX_HEIGHT_V(NODE)>0?SYNCTEX_HEIGHT_V(NODE):-SYNCTEX_HEIGHT_V(NODE))) # define SYNCTEX_ABS_DEPTH_V(NODE) ((SYNCTEX_DEPTH_V(NODE)>0?SYNCTEX_DEPTH_V(NODE):-SYNCTEX_DEPTH_V(NODE))) /* Horizontal boxes must contain visible size, because 0 width does not mean emptiness */ typedef struct { synctex_class_t class; synctex_info_t implementation[5+SYNCTEX_DEPTH_V_IDX+1]; /*parent,child,sibling,friend,next box, * SYNCTEX_TAG,SYNCTEX_LINE,SYNCTEX_COLUMN, * SYNCTEX_HORIZ,SYNCTEX_VERT,SYNCTEX_WIDTH,SYNCTEX_HEIGHT,SYNCTEX_DEPTH, * SYNCTEX_HORIZ_V,SYNCTEX_VERT_V,SYNCTEX_WIDTH_V,SYNCTEX_HEIGHT_V,SYNCTEX_DEPTH_V*/ } synctex_horiz_box_node_t; synctex_node_t _synctex_new_hbox(synctex_scanner_t scanner); void _synctex_display_hbox(synctex_node_t node); void _synctex_log_horiz_box(synctex_node_t sheet); static _synctex_class_t synctex_class_hbox = { NULL, /* No scanner yet */ synctex_node_type_hbox, /* Node type */ &_synctex_new_hbox, /* creator */ &_synctex_free_node, /* destructor */ &_synctex_log_horiz_box, /* log */ &_synctex_display_hbox, /* display */ &_synctex_implementation_0, /* parent */ &_synctex_implementation_1, /* child */ &_synctex_implementation_2, /* sibling */ &_synctex_implementation_3, /* friend */ &_synctex_implementation_4, /* next box */ (_synctex_info_getter_t)&_synctex_implementation_5 }; /* horizontal box node creator */ synctex_node_t _synctex_new_hbox(synctex_scanner_t scanner) { synctex_node_t node = _synctex_malloc(sizeof(synctex_horiz_box_node_t)); if (node) { node->class = scanner?scanner->class+synctex_node_type_hbox:(synctex_class_t)&synctex_class_hbox; } return node; } /* This void box node implementation is either horizontal or vertical * It does not contain a child field. */ typedef struct { synctex_class_t class; synctex_info_t implementation[3+SYNCTEX_DEPTH_IDX+1]; /* parent,sibling,friend, * SYNCTEX_TAG,SYNCTEX_LINE,SYNCTEX_COLUMN, * SYNCTEX_HORIZ,SYNCTEX_VERT,SYNCTEX_WIDTH,SYNCTEX_HEIGHT,SYNCTEX_DEPTH*/ } synctex_void_box_node_t; synctex_node_t _synctex_new_void_vbox(synctex_scanner_t scanner); void _synctex_log_void_box(synctex_node_t sheet); void _synctex_display_void_vbox(synctex_node_t node); static _synctex_class_t synctex_class_void_vbox = { NULL, /* No scanner yet */ synctex_node_type_void_vbox,/* Node type */ &_synctex_new_void_vbox, /* creator */ &_synctex_free_node, /* destructor */ &_synctex_log_void_box, /* log */ &_synctex_display_void_vbox,/* display */ &_synctex_implementation_0, /* parent */ NULL, /* No child */ &_synctex_implementation_1, /* sibling */ &_synctex_implementation_2, /* friend */ NULL, /* No next box */ (_synctex_info_getter_t)&_synctex_implementation_3 }; /* vertical void box node creator */ synctex_node_t _synctex_new_void_vbox(synctex_scanner_t scanner) { synctex_node_t node = _synctex_malloc(sizeof(synctex_void_box_node_t)); if (node) { node->class = scanner?scanner->class+synctex_node_type_void_vbox:(synctex_class_t)&synctex_class_void_vbox; } return node; } synctex_node_t _synctex_new_void_hbox(synctex_scanner_t scanner); void _synctex_display_void_hbox(synctex_node_t node); static _synctex_class_t synctex_class_void_hbox = { NULL, /* No scanner yet */ synctex_node_type_void_hbox,/* Node type */ &_synctex_new_void_hbox, /* creator */ &_synctex_free_node, /* destructor */ &_synctex_log_void_box, /* log */ &_synctex_display_void_hbox,/* display */ &_synctex_implementation_0, /* parent */ NULL, /* No child */ &_synctex_implementation_1, /* sibling */ &_synctex_implementation_2, /* friend */ NULL, /* No next box */ (_synctex_info_getter_t)&_synctex_implementation_3 }; /* horizontal void box node creator */ synctex_node_t _synctex_new_void_hbox(synctex_scanner_t scanner) { synctex_node_t node = _synctex_malloc(sizeof(synctex_void_box_node_t)); if (node) { node->class = scanner?scanner->class+synctex_node_type_void_hbox:(synctex_class_t)&synctex_class_void_hbox; } return node; } /* The medium nodes correspond to kern, glue, penalty and math nodes. */ typedef struct { synctex_class_t class; synctex_info_t implementation[3+SYNCTEX_WIDTH_IDX+1]; /* parent,sibling,friend, * SYNCTEX_TAG,SYNCTEX_LINE,SYNCTEX_COLUMN, * SYNCTEX_HORIZ,SYNCTEX_VERT,SYNCTEX_WIDTH */ } synctex_medium_node_t; #define SYNCTEX_IS_BOX(NODE)\ ((NODE->class->type == synctex_node_type_vbox)\ || (NODE->class->type == synctex_node_type_void_vbox)\ || (NODE->class->type == synctex_node_type_hbox)\ || (NODE->class->type == synctex_node_type_void_hbox)) #define SYNCTEX_HAS_CHILDREN(NODE) (NODE && SYNCTEX_CHILD(NODE)) void _synctex_log_medium_node(synctex_node_t node); /* math node creator */ synctex_node_t _synctex_new_math(synctex_scanner_t scanner); void _synctex_display_math(synctex_node_t node); static _synctex_class_t synctex_class_math = { NULL, /* No scanner yet */ synctex_node_type_math, /* Node type */ &_synctex_new_math, /* creator */ &_synctex_free_leaf, /* destructor */ &_synctex_log_medium_node, /* log */ &_synctex_display_math, /* display */ &_synctex_implementation_0, /* parent */ NULL, /* No child */ &_synctex_implementation_1, /* sibling */ &_synctex_implementation_2, /* friend */ NULL, /* No next box */ (_synctex_info_getter_t)&_synctex_implementation_3 }; synctex_node_t _synctex_new_math(synctex_scanner_t scanner) { synctex_node_t node = _synctex_malloc(sizeof(synctex_medium_node_t)); if (node) { node->class = scanner?scanner->class+synctex_node_type_math:(synctex_class_t)&synctex_class_math; } return node; } /* kern node creator */ synctex_node_t _synctex_new_kern(synctex_scanner_t scanner); void _synctex_display_kern(synctex_node_t node); static _synctex_class_t synctex_class_kern = { NULL, /* No scanner yet */ synctex_node_type_kern, /* Node type */ &_synctex_new_kern, /* creator */ &_synctex_free_leaf, /* destructor */ &_synctex_log_medium_node, /* log */ &_synctex_display_kern, /* display */ &_synctex_implementation_0, /* parent */ NULL, /* No child */ &_synctex_implementation_1, /* sibling */ &_synctex_implementation_2, /* friend */ NULL, /* No next box */ (_synctex_info_getter_t)&_synctex_implementation_3 }; synctex_node_t _synctex_new_kern(synctex_scanner_t scanner) { synctex_node_t node = _synctex_malloc(sizeof(synctex_medium_node_t)); if (node) { node->class = scanner?scanner->class+synctex_node_type_kern:(synctex_class_t)&synctex_class_kern; } return node; } /* The small nodes correspond to glue and boundary nodes. */ typedef struct { synctex_class_t class; synctex_info_t implementation[3+SYNCTEX_VERT_IDX+1]; /* parent,sibling,friend, * SYNCTEX_TAG,SYNCTEX_LINE,SYNCTEX_COLUMN, * SYNCTEX_HORIZ,SYNCTEX_VERT */ } synctex_small_node_t; void _synctex_log_small_node(synctex_node_t node); /* glue node creator */ synctex_node_t _synctex_new_glue(synctex_scanner_t scanner); void _synctex_display_glue(synctex_node_t node); static _synctex_class_t synctex_class_glue = { NULL, /* No scanner yet */ synctex_node_type_glue, /* Node type */ &_synctex_new_glue, /* creator */ &_synctex_free_leaf, /* destructor */ &_synctex_log_medium_node, /* log */ &_synctex_display_glue, /* display */ &_synctex_implementation_0, /* parent */ NULL, /* No child */ &_synctex_implementation_1, /* sibling */ &_synctex_implementation_2, /* friend */ NULL, /* No next box */ (_synctex_info_getter_t)&_synctex_implementation_3 }; synctex_node_t _synctex_new_glue(synctex_scanner_t scanner) { synctex_node_t node = _synctex_malloc(sizeof(synctex_medium_node_t)); if (node) { node->class = scanner?scanner->class+synctex_node_type_glue:(synctex_class_t)&synctex_class_glue; } return node; } /* boundary node creator */ synctex_node_t _synctex_new_boundary(synctex_scanner_t scanner); void _synctex_display_boundary(synctex_node_t node); static _synctex_class_t synctex_class_boundary = { NULL, /* No scanner yet */ synctex_node_type_boundary, /* Node type */ &_synctex_new_boundary, /* creator */ &_synctex_free_leaf, /* destructor */ &_synctex_log_small_node, /* log */ &_synctex_display_boundary, /* display */ &_synctex_implementation_0, /* parent */ NULL, /* No child */ &_synctex_implementation_1, /* sibling */ &_synctex_implementation_2, /* friend */ NULL, /* No next box */ (_synctex_info_getter_t)&_synctex_implementation_3 }; synctex_node_t _synctex_new_boundary(synctex_scanner_t scanner) { synctex_node_t node = _synctex_malloc(sizeof(synctex_small_node_t)); if (node) { node->class = scanner?scanner->class+synctex_node_type_boundary:(synctex_class_t)&synctex_class_boundary; } return node; } # define SYNCTEX_NAME_IDX (SYNCTEX_TAG_IDX+1) # define SYNCTEX_NAME(NODE) SYNCTEX_INFO(NODE)[SYNCTEX_NAME_IDX].PTR /* Input nodes only know about their sibling, which is another input node. * The synctex information is the SYNCTEX_TAG and SYNCTEX_NAME*/ typedef struct { synctex_class_t class; synctex_info_t implementation[1+SYNCTEX_NAME_IDX+1]; /* sibling, * SYNCTEX_TAG,SYNCTEX_NAME */ } synctex_input_t; synctex_node_t _synctex_new_input(synctex_scanner_t scanner); void _synctex_free_input(synctex_node_t node); void _synctex_display_input(synctex_node_t node); void _synctex_log_input(synctex_node_t sheet); static _synctex_class_t synctex_class_input = { NULL, /* No scanner yet */ synctex_node_type_input, /* Node type */ &_synctex_new_input, /* creator */ &_synctex_free_input, /* destructor */ &_synctex_log_input, /* log */ &_synctex_display_input, /* display */ NULL, /* No parent */ NULL, /* No child */ &_synctex_implementation_0, /* sibling */ NULL, /* No friend */ NULL, /* No next box */ (_synctex_info_getter_t)&_synctex_implementation_1 }; synctex_node_t _synctex_new_input(synctex_scanner_t scanner) { synctex_node_t node = _synctex_malloc(sizeof(synctex_input_t)); if (node) { node->class = scanner?scanner->class+synctex_node_type_input:(synctex_class_t)&synctex_class_input; } return node; } void _synctex_free_input(synctex_node_t node){ if (node) { SYNCTEX_FREE(SYNCTEX_SIBLING(node)); free(SYNCTEX_NAME(node)); free(node); } } # ifdef SYNCTEX_NOTHING # pragma mark - # pragma mark Navigation # endif synctex_node_t synctex_node_parent(synctex_node_t node) { return SYNCTEX_PARENT(node); } synctex_node_t synctex_node_sheet(synctex_node_t node) { while(node && node->class->type != synctex_node_type_sheet) { node = SYNCTEX_PARENT(node); } /* exit the while loop either when node is NULL or node is a sheet */ return node; } synctex_node_t synctex_node_child(synctex_node_t node) { return SYNCTEX_CHILD(node); } synctex_node_t synctex_node_sibling(synctex_node_t node) { return SYNCTEX_SIBLING(node); } synctex_node_t synctex_node_next(synctex_node_t node) { if (SYNCTEX_CHILD(node)) { return SYNCTEX_CHILD(node); } sibling: if (SYNCTEX_SIBLING(node)) { return SYNCTEX_SIBLING(node); } if ((node = SYNCTEX_PARENT(node))) { if (node->class->type == synctex_node_type_sheet) {/* EXC_BAD_ACCESS? */ return NULL; } goto sibling; } return NULL; } # ifdef SYNCTEX_NOTHING # pragma mark - # pragma mark CLASS # endif /* Public node accessor: the type */ synctex_node_type_t synctex_node_type(synctex_node_t node) { if (node) { return (((node)->class))->type; } return synctex_node_type_error; } /* Public node accessor: the human readable type */ const char * synctex_node_isa(synctex_node_t node) { static const char * isa[synctex_node_number_of_types] = {"Not a node","input","sheet","vbox","void vbox","hbox","void hbox","kern","glue","math","boundary"}; return isa[synctex_node_type(node)]; } # ifdef SYNCTEX_NOTHING # pragma mark - # pragma mark SYNCTEX_LOG # endif # define SYNCTEX_LOG(NODE) SYNCTEX_MSG_SEND(NODE,log) /* Public node logger */ void synctex_node_log(synctex_node_t node) { SYNCTEX_LOG(node); } # define SYNCTEX_DISPLAY(NODE) SYNCTEX_MSG_SEND(NODE,display) void synctex_node_display(synctex_node_t node) { SYNCTEX_DISPLAY(node); } void _synctex_display_input(synctex_node_t node) { printf("....Input:%i:%s\n", SYNCTEX_TAG(node), SYNCTEX_NAME(node)); SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node)); } void _synctex_log_sheet(synctex_node_t sheet) { if (sheet) { printf("%s:%i\n",synctex_node_isa(sheet),SYNCTEX_PAGE(sheet)); printf("SELF:%p",(void *)sheet); printf(" SYNCTEX_PARENT:%p",(void *)SYNCTEX_PARENT(sheet)); printf(" SYNCTEX_CHILD:%p",(void *)SYNCTEX_CHILD(sheet)); printf(" SYNCTEX_SIBLING:%p",(void *)SYNCTEX_SIBLING(sheet)); printf(" SYNCTEX_FRIEND:%p\n",(void *)SYNCTEX_FRIEND(sheet)); } } void _synctex_log_small_node(synctex_node_t node) { printf("%s:%i,%i:%i,%i\n", synctex_node_isa(node), SYNCTEX_TAG(node), SYNCTEX_LINE(node), SYNCTEX_HORIZ(node), SYNCTEX_VERT(node)); printf("SELF:%p",(void *)node); printf(" SYNCTEX_PARENT:%p",(void *)SYNCTEX_PARENT(node)); printf(" SYNCTEX_CHILD:%p",(void *)SYNCTEX_CHILD(node)); printf(" SYNCTEX_SIBLING:%p",(void *)SYNCTEX_SIBLING(node)); printf(" SYNCTEX_FRIEND:%p\n",(void *)SYNCTEX_FRIEND(node)); } void _synctex_log_medium_node(synctex_node_t node) { printf("%s:%i,%i:%i,%i:%i\n", synctex_node_isa(node), SYNCTEX_TAG(node), SYNCTEX_LINE(node), SYNCTEX_HORIZ(node), SYNCTEX_VERT(node), SYNCTEX_WIDTH(node)); printf("SELF:%p",(void *)node); printf(" SYNCTEX_PARENT:%p",(void *)SYNCTEX_PARENT(node)); printf(" SYNCTEX_CHILD:%p",(void *)SYNCTEX_CHILD(node)); printf(" SYNCTEX_SIBLING:%p",(void *)SYNCTEX_SIBLING(node)); printf(" SYNCTEX_FRIEND:%p\n",(void *)SYNCTEX_FRIEND(node)); } void _synctex_log_void_box(synctex_node_t node) { printf("%s",synctex_node_isa(node)); printf(":%i",SYNCTEX_TAG(node)); printf(",%i",SYNCTEX_LINE(node)); printf(",%i",0); printf(":%i",SYNCTEX_HORIZ(node)); printf(",%i",SYNCTEX_VERT(node)); printf(":%i",SYNCTEX_WIDTH(node)); printf(",%i",SYNCTEX_HEIGHT(node)); printf(",%i",SYNCTEX_DEPTH(node)); printf("\nSELF:%p",(void *)node); printf(" SYNCTEX_PARENT:%p",(void *)SYNCTEX_PARENT(node)); printf(" SYNCTEX_CHILD:%p",(void *)SYNCTEX_CHILD(node)); printf(" SYNCTEX_SIBLING:%p",(void *)SYNCTEX_SIBLING(node)); printf(" SYNCTEX_FRIEND:%p\n",(void *)SYNCTEX_FRIEND(node)); } void _synctex_log_box(synctex_node_t node) { printf("%s",synctex_node_isa(node)); printf(":%i",SYNCTEX_TAG(node)); printf(",%i",SYNCTEX_LINE(node)); printf(",%i",0); printf(":%i",SYNCTEX_HORIZ(node)); printf(",%i",SYNCTEX_VERT(node)); printf(":%i",SYNCTEX_WIDTH(node)); printf(",%i",SYNCTEX_HEIGHT(node)); printf(",%i",SYNCTEX_DEPTH(node)); printf("\nSELF:%p",(void *)node); printf(" SYNCTEX_PARENT:%p",(void *)SYNCTEX_PARENT(node)); printf(" SYNCTEX_CHILD:%p",(void *)SYNCTEX_CHILD(node)); printf(" SYNCTEX_SIBLING:%p",(void *)SYNCTEX_SIBLING(node)); printf(" SYNCTEX_FRIEND:%p\n",(void *)SYNCTEX_FRIEND(node)); } void _synctex_log_horiz_box(synctex_node_t node) { printf("%s",synctex_node_isa(node)); printf(":%i",SYNCTEX_TAG(node)); printf(",%i",SYNCTEX_LINE(node)); printf(",%i",0); printf(":%i",SYNCTEX_HORIZ(node)); printf(",%i",SYNCTEX_VERT(node)); printf(":%i",SYNCTEX_WIDTH(node)); printf(",%i",SYNCTEX_HEIGHT(node)); printf(",%i",SYNCTEX_DEPTH(node)); printf("/%i",SYNCTEX_HORIZ_V(node)); printf(",%i",SYNCTEX_VERT_V(node)); printf(":%i",SYNCTEX_WIDTH_V(node)); printf(",%i",SYNCTEX_HEIGHT_V(node)); printf(",%i",SYNCTEX_DEPTH_V(node)); printf("\nSELF:%p",(void *)node); printf(" SYNCTEX_PARENT:%p",(void *)SYNCTEX_PARENT(node)); printf(" SYNCTEX_CHILD:%p",(void *)SYNCTEX_CHILD(node)); printf(" SYNCTEX_SIBLING:%p",(void *)SYNCTEX_SIBLING(node)); printf(" SYNCTEX_FRIEND:%p\n",(void *)SYNCTEX_FRIEND(node)); } void _synctex_log_input(synctex_node_t node) { printf("%s",synctex_node_isa(node)); printf(":%i",SYNCTEX_TAG(node)); printf(",%s",SYNCTEX_NAME(node)); printf(" SYNCTEX_SIBLING:%p",(void *)SYNCTEX_SIBLING(node)); } void _synctex_display_sheet(synctex_node_t sheet) { if (sheet) { printf("....{%i\n",SYNCTEX_PAGE(sheet)); SYNCTEX_DISPLAY(SYNCTEX_CHILD(sheet)); printf("....}\n"); SYNCTEX_DISPLAY(SYNCTEX_SIBLING(sheet)); } } void _synctex_display_vbox(synctex_node_t node) { printf("....[%i,%i:%i,%i:%i,%i,%i\n", SYNCTEX_TAG(node), SYNCTEX_LINE(node), SYNCTEX_HORIZ(node), SYNCTEX_VERT(node), SYNCTEX_WIDTH(node), SYNCTEX_HEIGHT(node), SYNCTEX_DEPTH(node)); SYNCTEX_DISPLAY(SYNCTEX_CHILD(node)); printf("....]\n"); SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node)); } void _synctex_display_hbox(synctex_node_t node) { printf("....(%i,%i:%i,%i:%i,%i,%i\n", SYNCTEX_TAG(node), SYNCTEX_LINE(node), SYNCTEX_HORIZ(node), SYNCTEX_VERT(node), SYNCTEX_WIDTH(node), SYNCTEX_HEIGHT(node), SYNCTEX_DEPTH(node)); SYNCTEX_DISPLAY(SYNCTEX_CHILD(node)); printf("....)\n"); SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node)); } void _synctex_display_void_vbox(synctex_node_t node) { printf("....v%i,%i;%i,%i:%i,%i,%i\n", SYNCTEX_TAG(node), SYNCTEX_LINE(node), SYNCTEX_HORIZ(node), SYNCTEX_VERT(node), SYNCTEX_WIDTH(node), SYNCTEX_HEIGHT(node), SYNCTEX_DEPTH(node)); SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node)); } void _synctex_display_void_hbox(synctex_node_t node) { printf("....h%i,%i:%i,%i:%i,%i,%i\n", SYNCTEX_TAG(node), SYNCTEX_LINE(node), SYNCTEX_HORIZ(node), SYNCTEX_VERT(node), SYNCTEX_WIDTH(node), SYNCTEX_HEIGHT(node), SYNCTEX_DEPTH(node)); SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node)); } void _synctex_display_glue(synctex_node_t node) { printf("....glue:%i,%i:%i,%i\n", SYNCTEX_TAG(node), SYNCTEX_LINE(node), SYNCTEX_HORIZ(node), SYNCTEX_VERT(node)); SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node)); } void _synctex_display_math(synctex_node_t node) { printf("....math:%i,%i:%i,%i\n", SYNCTEX_TAG(node), SYNCTEX_LINE(node), SYNCTEX_HORIZ(node), SYNCTEX_VERT(node)); SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node)); } void _synctex_display_kern(synctex_node_t node) { printf("....kern:%i,%i:%i,%i:%i\n", SYNCTEX_TAG(node), SYNCTEX_LINE(node), SYNCTEX_HORIZ(node), SYNCTEX_VERT(node), SYNCTEX_WIDTH(node)); SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node)); } void _synctex_display_boundary(synctex_node_t node) { printf("....boundary:%i,%i:%i,%i\n", SYNCTEX_TAG(node), SYNCTEX_LINE(node), SYNCTEX_HORIZ(node), SYNCTEX_VERT(node)); SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node)); } # ifdef SYNCTEX_NOTHING # pragma mark - # pragma mark SCANNER # endif /* Here are gathered all the possible status that the next scanning functions will return. * All these functions return a status, and pass their result through pointers. * Negative values correspond to errors. * The management of the buffer is causing some significant overhead. * Every function that may access the buffer returns a status related to the buffer and file state. * status >= SYNCTEX_STATUS_OK means the function worked as expected * status < SYNCTEX_STATUS_OK means the function did not work as expected * status == SYNCTEX_STATUS_NOT_OK means the function did not work as expected but there is still some material to parse. * status == SYNCTEX_STATUS_EOF means the function did not work as expected and there is no more material. * statusfile) /* Actually, the minimum buffer size is driven by integer and float parsing. * 0.123456789e123 */ # define SYNCTEX_BUFFER_MIN_SIZE 16 # define SYNCTEX_BUFFER_SIZE 32768 # ifdef SYNCTEX_NOTHING # pragma mark - # pragma mark Prototypes # endif void _synctex_log_void_box(synctex_node_t node); void _synctex_log_box(synctex_node_t node); void _synctex_log_horiz_box(synctex_node_t node); void _synctex_log_input(synctex_node_t node); synctex_status_t _synctex_buffer_get_available_size(synctex_scanner_t scanner, size_t * size_ptr); synctex_status_t _synctex_next_line(synctex_scanner_t scanner); synctex_status_t _synctex_match_string(synctex_scanner_t scanner, const char * the_string); synctex_status_t _synctex_decode_int(synctex_scanner_t scanner, int* value_ref); synctex_status_t _synctex_decode_string(synctex_scanner_t scanner, char ** value_ref); synctex_status_t _synctex_scan_input(synctex_scanner_t scanner); synctex_status_t _synctex_scan_preamble(synctex_scanner_t scanner); synctex_status_t _synctex_scan_float_and_dimension(synctex_scanner_t scanner, float * value_ref); synctex_status_t _synctex_scan_post_scriptum(synctex_scanner_t scanner); int _synctex_scan_postamble(synctex_scanner_t scanner); synctex_status_t _synctex_setup_visible_box(synctex_node_t box); synctex_status_t _synctex_horiz_box_setup_visible(synctex_node_t node,int h, int v); synctex_status_t _synctex_scan_sheet(synctex_scanner_t scanner, synctex_node_t parent); synctex_status_t _synctex_scan_nested_sheet(synctex_scanner_t scanner); synctex_status_t _synctex_scan_content(synctex_scanner_t scanner); int synctex_scanner_pre_x_offset(synctex_scanner_t scanner); int synctex_scanner_pre_y_offset(synctex_scanner_t scanner); const char * synctex_scanner_get_output_fmt(synctex_scanner_t scanner); int _synctex_node_is_box(synctex_node_t node); int _synctex_bail(void); /* Try to ensure that the buffer contains at least size bytes. * Passing a huge size argument means the whole buffer length. * Passing a null size argument means return the available buffer length, without reading the file. * In that case, the return status is always SYNCTEX_STATUS_OK unless the given scanner is NULL, * in which case, SYNCTEX_STATUS_BAD_ARGUMENT is returned. * The value returned in size_ptr is the number of bytes now available in the buffer. * This is a nonnegative integer, it may take the value 0. * It is the responsibility of the caller to test whether this size is conforming to its needs. * Negative values may return in case of error, actually * when there was an error reading the synctex file. */ synctex_status_t _synctex_buffer_get_available_size(synctex_scanner_t scanner, size_t * size_ptr) { size_t available = 0; if (NULL == scanner || NULL == size_ptr) { return SYNCTEX_STATUS_BAD_ARGUMENT; } # define size (* size_ptr) if (size>SYNCTEX_BUFFER_SIZE){ size = SYNCTEX_BUFFER_SIZE; } available = SYNCTEX_END - SYNCTEX_CUR; /* available is the number of unparsed chars in the buffer */ if (size<=available) { /* There are already sufficiently many characters in the buffer */ size = available; return SYNCTEX_STATUS_OK; } if (SYNCTEX_FILE) { /* Copy the remaining part of the buffer to the beginning, * then read the next part of the file */ int already_read = 0; if (available) { memmove(SYNCTEX_START, SYNCTEX_CUR, available); } SYNCTEX_CUR = SYNCTEX_START + available; /* the next character after the move, will change. */ /* Fill the buffer up to its end */ already_read = gzread(SYNCTEX_FILE,(void *)SYNCTEX_CUR,SYNCTEX_BUFFER_SIZE - available); if (already_read>0) { /* We assume that 0already_read) { /* There is a possible error in reading the file */ int errnum = 0; const char * error_string = gzerror(SYNCTEX_FILE, &errnum); if (Z_ERRNO == errnum) { /* There is an error in zlib caused by the file system */ _synctex_error("gzread error from the file system (%i)",errno); return SYNCTEX_STATUS_ERROR; } else if (errnum) { _synctex_error("gzread error (%i:%i,%s)",already_read,errnum,error_string); return SYNCTEX_STATUS_ERROR; } } /* Nothing was read, we are at the end of the file. */ gzclose(SYNCTEX_FILE); SYNCTEX_FILE = NULL; SYNCTEX_END = SYNCTEX_CUR; SYNCTEX_CUR = SYNCTEX_START; * SYNCTEX_END = '\0';/* Terminate the string properly.*/ size = SYNCTEX_END - SYNCTEX_CUR; return SYNCTEX_STATUS_EOF; /* there might be a bit of text left */ } /* We cannot enlarge the buffer because the end of the file was reached. */ size = available; return SYNCTEX_STATUS_EOF; # undef size } /* Used when parsing the synctex file. * Advance to the next character starting a line. * Actually, only '\n' is recognized as end of line marker. * On normal completion, the returned value is the number of unparsed characters available in the buffer. * In general, it is a positive value, 0 meaning that the end of file was reached. * -1 is returned in case of error, actually because there was an error while feeding the buffer. * When the function returns with no error, SYNCTEX_CUR points to the first character of the next line, if any. * J. Laurens: Sat May 10 07:52:31 UTC 2008 */ synctex_status_t _synctex_next_line(synctex_scanner_t scanner) { synctex_status_t status = SYNCTEX_STATUS_OK; size_t available = 0; if (NULL == scanner) { return SYNCTEX_STATUS_BAD_ARGUMENT; } infinite_loop: while(SYNCTEX_CUR=remaining_len) { /* The buffer is sufficiently big to hold the expected number of characters. */ if (strncmp((char *)SYNCTEX_CUR,the_string,remaining_len)) { return SYNCTEX_STATUS_NOT_OK; } return_OK: /* Advance SYNCTEX_CUR to the next character after the_string. */ SYNCTEX_CUR += remaining_len; return SYNCTEX_STATUS_OK; } else if (strncmp((char *)SYNCTEX_CUR,the_string,available)) { /* No need to goo further, this is not the expected string in the buffer. */ return SYNCTEX_STATUS_NOT_OK; } else if (SYNCTEX_FILE) { /* The buffer was too small to contain remaining_len characters. * We have to cut the string into pieces. */ z_off_t offset = 0L; /* the first part of the string is found, advance the_string to the next untested character. */ the_string += available; /* update the remaining length and the parsed length. */ remaining_len -= available; tested_len += available; SYNCTEX_CUR += available; /* We validate the tested characters. */ if (0 == remaining_len) { /* Nothing left to test, we have found the given string, we return the length. */ return tested_len; } /* We also have to record the current state of the file cursor because * if the_string does not match, all this should be a totally blank operation, * for which the file and buffer states should not be modified at all. * In fact, the states of the buffer before and after this function are in general different * but they are totally equivalent as long as the values of the buffer before SYNCTEX_CUR * can be safely discarded. */ offset = gztell(SYNCTEX_FILE); /* offset now corresponds to the first character of the file that was not buffered. */ available = SYNCTEX_CUR - SYNCTEX_START; /* available can be used as temporary placeholder. */ /* available now corresponds to the number of chars that where already buffered and * that match the head of the_string. If in fine the_string does not match, all these chars must be recovered * because the buffer contents is completely replaced by _synctex_buffer_get_available_size. * They were buffered from offset-len location in the file. */ offset -= available; more_characters: /* There is still some work to be done, so read another bunch of file. * This is the second call to _synctex_buffer_get_available_size, * which means that the actual contents of the buffer will be discarded. * We will definitely have to recover the previous state in case we do not find the expected string. */ available = remaining_len; status = _synctex_buffer_get_available_size(scanner,&available); if (statusptr) { SYNCTEX_CUR = end; if (value_ref) { * value_ref = result; } return SYNCTEX_STATUS_OK;/* Successfully scanned an int */ } return SYNCTEX_STATUS_NOT_OK;/* Could not scan an int */ } /* The purpose of this function is to read a string. * A string is an array of characters from the current parser location * and before the next '\n' character. * If a string was properly decoded, it is returned in value_ref and * the cursor points to the new line marker. * The returned string was alloced on the heap, the caller is the owner and * is responsible to free it in due time. * If no string is parsed, * value_ref is undefined. * The maximum length of a string that a scanner can decode is platform dependent, namely UINT_MAX. * If you just want to blindly parse the file up to the end of the current line, * use _synctex_next_line instead. * On return, the scanner cursor is unchanged if a string could not be scanned or * points to the terminating '\n' character otherwise. As a consequence, * _synctex_next_line is necessary after. * If either scanner or value_ref is NULL, it is considered as an error and * SYNCTEX_STATUS_BAD_ARGUMENT is returned. */ synctex_status_t _synctex_decode_string(synctex_scanner_t scanner, char ** value_ref) { char * end = NULL; size_t current_size = 0; size_t new_size = 0; size_t len = 0;/* The number of bytes to copy */ size_t available = 0; synctex_status_t status = 0; if (NULL == scanner || NULL == value_ref) { return SYNCTEX_STATUS_BAD_ARGUMENT; } /* The buffer must at least contain one character: the '\n' end of line marker */ if (SYNCTEX_CUR>=SYNCTEX_END) { available = 1; status = _synctex_buffer_get_available_size(scanner,&available); if (status < 0) { return status; } if (0 == available) { return SYNCTEX_STATUS_EOF; } } /* Now we are sure that there is at least one available character, either because * SYNCTEX_CUR was already < SYNCTEX_END, or because the buffer has been properly filled. */ /* end will point to the next unparsed '\n' character in the file, when mapped to the buffer. */ end = SYNCTEX_CUR; * value_ref = NULL;/* Initialize, it will be realloc'ed */ /* We scan all the characters up to the next '\n' */ next_character: if (endUINT_MAX-len-1) { /* But we have reached the limit: we do not have current_size+len+1>UINT_MAX. * We return the missing amount of memory. * This will never occur in practice. */ return UINT_MAX-len-1 - current_size; } new_size = current_size+len; /* We have current_size+len+1<=UINT_MAX * or equivalently new_sizeUINT_MAX-len-1) { /* We have reached the limit. */ _synctex_error("limit reached (missing %i).",current_size-(UINT_MAX-len-1)); return SYNCTEX_STATUS_ERROR; } new_size = current_size+len; if ((* value_ref = realloc(* value_ref,new_size+1)) != NULL) { if (memcpy((*value_ref)+current_size,SYNCTEX_CUR,len)) { (* value_ref)[new_size]='\0'; /* Terminate the string */ SYNCTEX_CUR = SYNCTEX_END;/* Advance the cursor to the end of the bufer */ return SYNCTEX_STATUS_OK; } free(* value_ref); * value_ref = NULL; _synctex_error("could not copy memory (2)."); return SYNCTEX_STATUS_ERROR; } /* Huge memory problem */ _synctex_error("could not allocate memory (2)."); return SYNCTEX_STATUS_ERROR; } } /* Used when parsing the synctex file. * Read an Input record. */ synctex_status_t _synctex_scan_input(synctex_scanner_t scanner) { synctex_status_t status = 0; size_t available = 0; synctex_node_t input = NULL; if (NULL == scanner) { return SYNCTEX_STATUS_BAD_ARGUMENT; } status = _synctex_match_string(scanner,"Input:"); if (statusinput); scanner->input = input; return _synctex_next_line(scanner);/* read the line termination character, if any */ /* Now, set up the path */ } typedef synctex_status_t (*synctex_decoder_t)(synctex_scanner_t,void *); synctex_status_t _synctex_scan_named(synctex_scanner_t scanner,const char * name,void * value_ref,synctex_decoder_t decoder); /* Used when parsing the synctex file. * Read one of the settings. * On normal completion, returns SYNCTEX_STATUS_OK. * On error, returns SYNCTEX_STATUS_ERROR. * Both arguments must not be NULL. * On return, the scanner points to the next character after the decoded object whatever it is. * It is the responsibility of the caller to prepare the scanner for the next line. */ synctex_status_t _synctex_scan_named(synctex_scanner_t scanner,const char * name,void * value_ref,synctex_decoder_t decoder) { synctex_status_t status = 0; if (NULL == scanner || NULL == name || NULL == value_ref || NULL == decoder) { return SYNCTEX_STATUS_BAD_ARGUMENT; } not_found: status = _synctex_match_string(scanner,name); if (statusversion),(synctex_decoder_t)&_synctex_decode_int); if (statusoutput_fmt),(synctex_decoder_t)&_synctex_decode_string); if (statuspre_magnification),(synctex_decoder_t)&_synctex_decode_int); if (statuspre_unit),(synctex_decoder_t)&_synctex_decode_int); if (statuspre_x_offset),(synctex_decoder_t)&_synctex_decode_int); if (statuspre_y_offset),(synctex_decoder_t)&_synctex_decode_int); if (status= SYNCTEX_STATUS_OK) { f *= 72.27f*65536; } else if (status= SYNCTEX_STATUS_OK) { f *= 72.27f*65536/2.54f; } else if (status<0) { goto report_unit_error; } else if ((status = _synctex_match_string(scanner,"mm")) >= SYNCTEX_STATUS_OK) { f *= 72.27f*65536/25.4f; } else if (status<0) { goto report_unit_error; } else if ((status = _synctex_match_string(scanner,"pt")) >= SYNCTEX_STATUS_OK) { f *= 65536.0f; } else if (status<0) { goto report_unit_error; } else if ((status = _synctex_match_string(scanner,"bp")) >= SYNCTEX_STATUS_OK) { f *= 72.27f/72*65536.0f; } else if (status<0) { goto report_unit_error; } else if ((status = _synctex_match_string(scanner,"pc")) >= SYNCTEX_STATUS_OK) { f *= 12.0*65536.0f; } else if (status<0) { goto report_unit_error; } else if ((status = _synctex_match_string(scanner,"sp")) >= SYNCTEX_STATUS_OK) { f *= 1.0f; } else if (status<0) { goto report_unit_error; } else if ((status = _synctex_match_string(scanner,"dd")) >= SYNCTEX_STATUS_OK) { f *= 1238.0f/1157*65536.0f; } else if (status<0) { goto report_unit_error; } else if ((status = _synctex_match_string(scanner,"cc")) >= SYNCTEX_STATUS_OK) { f *= 14856.0f/1157*65536; } else if (status<0) { goto report_unit_error; } else if ((status = _synctex_match_string(scanner,"nd")) >= SYNCTEX_STATUS_OK) { f *= 685.0f/642*65536; } else if (status<0) { goto report_unit_error; } else if ((status = _synctex_match_string(scanner,"nc")) >= SYNCTEX_STATUS_OK) { f *= 1370.0f/107*65536; } else if (status<0) { goto report_unit_error; } *value_ref = f; return SYNCTEX_STATUS_OK; } /* parse the post scriptum * SYNCTEX_STATUS_OK is returned on completion * a negative error is returned otherwise */ synctex_status_t _synctex_scan_post_scriptum(synctex_scanner_t scanner) { synctex_status_t status = 0; char * endptr = NULL; #ifdef HAVE_SETLOCALE char * loc = setlocale(LC_NUMERIC, NULL); #endif if (NULL == scanner) { return SYNCTEX_STATUS_BAD_ARGUMENT; } /* Scan the file until a post scriptum line is found */ post_scriptum_not_found: status = _synctex_match_string(scanner,"Post scriptum:"); if (statusunit = strtod(SYNCTEX_CUR,&endptr); #ifdef HAVE_SETLOCALE setlocale(LC_NUMERIC, loc); #endif if (endptr == SYNCTEX_CUR) { _synctex_error("bad magnification in the post scriptum, a float was expected."); return SYNCTEX_STATUS_ERROR; } if (scanner->unit<=0) { _synctex_error("bad magnification in the post scriptum, a positive float was expected."); return SYNCTEX_STATUS_ERROR; } SYNCTEX_CUR = endptr; goto next_line; } if (statusx_offset)); if (statusy_offset)); if (statuscount),(synctex_decoder_t)&_synctex_decode_int); if (status < SYNCTEX_STATUS_EOF) { return status; /* forward the error */ } else if (status < SYNCTEX_STATUS_OK) { /* No Count record found */ status = _synctex_next_line(scanner); /* Advance one more line */ if (statusclass->type) { case synctex_node_type_hbox: if (SYNCTEX_INFO(box) != NULL) { SYNCTEX_HORIZ_V(box) = SYNCTEX_HORIZ(box); SYNCTEX_VERT_V(box) = SYNCTEX_VERT(box); SYNCTEX_WIDTH_V(box) = SYNCTEX_WIDTH(box); SYNCTEX_HEIGHT_V(box) = SYNCTEX_HEIGHT(box); SYNCTEX_DEPTH_V(box) = SYNCTEX_DEPTH(box); return SYNCTEX_STATUS_OK; } return SYNCTEX_STATUS_ERROR; } } return SYNCTEX_STATUS_BAD_ARGUMENT; } /* This method is sent to an horizontal box to setup the visible size * Some box have 0 width but do contain text material. * With this method, one can enlarge the box to contain the given point (h,v). */ synctex_status_t _synctex_horiz_box_setup_visible(synctex_node_t node,int h, int v) { # ifdef __DARWIN_UNIX03 # pragma unused(v) # endif int itsBtm, itsTop; if (NULL == node || node->class->type != synctex_node_type_hbox) { return SYNCTEX_STATUS_BAD_ARGUMENT; } if (SYNCTEX_WIDTH_V(node)<0) { itsBtm = SYNCTEX_HORIZ_V(node); itsTop = SYNCTEX_HORIZ_V(node)-SYNCTEX_WIDTH_V(node); if (hitsTop) { SYNCTEX_WIDTH_V(node) = SYNCTEX_HORIZ_V(node) - h; } } else { itsBtm = SYNCTEX_HORIZ_V(node); itsTop = SYNCTEX_HORIZ_V(node)+SYNCTEX_WIDTH_V(node); if (hitsTop) { SYNCTEX_WIDTH_V(node) = h - SYNCTEX_HORIZ_V(node); } } return SYNCTEX_STATUS_OK; } /* Here are the control characters that strat each line of the synctex output file. * Their values define the meaning of the line. */ # define SYNCTEX_CHAR_BEGIN_SHEET '{' # define SYNCTEX_CHAR_END_SHEET '}' # define SYNCTEX_CHAR_BEGIN_VBOX '[' # define SYNCTEX_CHAR_END_VBOX ']' # define SYNCTEX_CHAR_BEGIN_HBOX '(' # define SYNCTEX_CHAR_END_HBOX ')' # define SYNCTEX_CHAR_ANCHOR '!' # define SYNCTEX_CHAR_VOID_VBOX 'v' # define SYNCTEX_CHAR_VOID_HBOX 'h' # define SYNCTEX_CHAR_KERN 'k' # define SYNCTEX_CHAR_GLUE 'g' # define SYNCTEX_CHAR_MATH '$' # define SYNCTEX_CHAR_BOUNDARY 'x' # define SYNCTEX_RETURN(STATUS) return STATUS; /* Used when parsing the synctex file. A '{' character has just been parsed. * The purpose is to gobble everything until the closing '}'. * Actually only one nesting depth has been observed when using the clip option * of \includegraphics option. Here we use arbitrary level of depth. */ synctex_status_t _synctex_scan_nested_sheet(synctex_scanner_t scanner) { unsigned int depth = 0; deeper: ++depth; if (_synctex_next_line(scanner)0) { goto scan_next_line; } else { SYNCTEX_RETURN(SYNCTEX_STATUS_OK); } } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_BEGIN_SHEET) { ++SYNCTEX_CUR; goto deeper; } else if (_synctex_next_line(scanner)class->type != synctex_node_type_sheet || _synctex_next_line(scanner)0){ _synctex_error("Uncomplete sheet(0)"); SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR); } else { goto prepare_loop; } } _synctex_bail(); /* The child loop means that we go do one level, when we just created a box node, * the next node created is a child of this box. */ child_loop: if (SYNCTEX_CURclass->type == synctex_node_type_vbox) { #define SYNCTEX_UPDATE_BOX_FRIEND(NODE)\ friend_index = ((SYNCTEX_INFO(NODE))[SYNCTEX_TAG_IDX].INT+(SYNCTEX_INFO(NODE))[SYNCTEX_LINE_IDX].INT)%(scanner->number_of_lists);\ SYNCTEX_SET_FRIEND(NODE,(scanner->lists_of_friends)[friend_index]);\ (scanner->lists_of_friends)[friend_index] = NODE; if (NULL == SYNCTEX_CHILD(parent)) { /* only void boxes are friends */ SYNCTEX_UPDATE_BOX_FRIEND(parent); } child = parent; parent = SYNCTEX_PARENT(child); } else { _synctex_error("Unexpected end of vbox, ignored."); } if (_synctex_next_line(scanner)class->type == synctex_node_type_hbox) { if (NULL == child) { /* Only boxes with no children are friends, * boxes with children are indirectly friends through one of their descendants. */ SYNCTEX_UPDATE_BOX_FRIEND(parent); } /* setting the next horizontal box at the end ensures that a child is recorded before any of its ancestors. */ SYNCTEX_SET_NEXT_HORIZ_BOX(box,parent); box = parent; child = parent; parent = SYNCTEX_PARENT(child); } else { _synctex_error("Unexpected enf of hbox, ignored."); } if (_synctex_next_line(scanner)number_of_lists);\ SYNCTEX_SET_FRIEND(NODE,(scanner->lists_of_friends)[friend_index]);\ (scanner->lists_of_friends)[friend_index] = NODE; SYNCTEX_UPDATE_FRIEND(child); goto sibling_loop; } else { _synctex_error("Can't create vbox record."); SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR); } } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_VOID_HBOX) { ++SYNCTEX_CUR; if (NULL != (child = _synctex_new_void_hbox(scanner)) && NULL != (info = SYNCTEX_INFO(child))) { if (SYNCTEX_DECODE_FAILED(SYNCTEX_TAG_IDX) || SYNCTEX_DECODE_FAILED(SYNCTEX_LINE_IDX) || SYNCTEX_DECODE_FAILED(SYNCTEX_HORIZ_IDX) || SYNCTEX_DECODE_FAILED(SYNCTEX_VERT_IDX) || SYNCTEX_DECODE_FAILED(SYNCTEX_WIDTH_IDX) || SYNCTEX_DECODE_FAILED(SYNCTEX_HEIGHT_IDX) || SYNCTEX_DECODE_FAILED(SYNCTEX_DEPTH_IDX) || _synctex_next_line(scanner)0){ _synctex_error("Uncomplete sheet(0)"); SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR); } else { goto child_loop; } } _synctex_bail(); /* The vertical loop means that we are on the same level, for example when we just ended a box. * If a node is created now, it will be a sibling of the current node, sharing the same parent. */ sibling_loop: if (SYNCTEX_CUR0){ goto sibling_loop; } else { _synctex_error("Uncomplete sheet(2)"); SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR); } } # undef SYNCTEX_DECODE_FAILED } /* Used when parsing the synctex file */ synctex_status_t _synctex_scan_content(synctex_scanner_t scanner) { synctex_node_t sheet = NULL; synctex_status_t status = 0; if (NULL == scanner) { return SYNCTEX_STATUS_BAD_ARGUMENT; } /* set up the lists of friends */ if (NULL == scanner->lists_of_friends) { scanner->number_of_lists = 1024; scanner->lists_of_friends = (synctex_node_t *)_synctex_malloc(scanner->number_of_lists*sizeof(synctex_node_t)); if (NULL == scanner->lists_of_friends) { _synctex_error("malloc:2"); return SYNCTEX_STATUS_ERROR; } } /* Find where this section starts */ content_not_found: status = _synctex_match_string(scanner,"Content:"); if (statussheet); scanner->sheet = sheet; sheet = NULL; /* Now read the list of Inputs between 2 sheets. */ do { status = _synctex_scan_input(scanner); if (status= SYNCTEX_STATUS_OK); goto next_sheet; } int _synctex_open(const char * output, const char * build_directory, char ** synctex_name_ref, gzFile * file_ref, synctex_bool_t add_quotes, synctex_io_mode_t * io_modeRef); /* Where the synctex scanner is created. */ synctex_scanner_t synctex_scanner_new_with_output_file(const char * output, const char * build_directory, int parse) { gzFile file = NULL; char * synctex = NULL; synctex_scanner_t scanner = NULL; synctex_io_mode_t io_mode = 0; /* Here we assume that int are smaller than void * */ if (sizeof(int)>sizeof(void*)) { _synctex_error("INTERNAL INCONSISTENCY: int's are unexpectedly bigger than pointers, bailing out."); return NULL; } /* We ensure that SYNCTEX_BUFFER_SIZE < UINT_MAX, I don't know if it makes sense... */ if (SYNCTEX_BUFFER_SIZE >= UINT_MAX) { _synctex_error("SyncTeX BUG: Internal inconsistency, bad SYNCTEX_BUFFER_SIZE (1)"); return NULL; } /* for integers: */ if (SYNCTEX_BUFFER_SIZE < SYNCTEX_BUFFER_MIN_SIZE) { _synctex_error("SyncTeX BUG: Internal inconsistency, bad SYNCTEX_BUFFER_SIZE (2)"); return NULL; } /* now open the synctex file */ if (_synctex_open(output,build_directory,&synctex,&file,synctex_ADD_QUOTES,&io_mode) || !file) { if (_synctex_open(output,build_directory,&synctex,&file,synctex_DONT_ADD_QUOTES,&io_mode) || !file) { return NULL; } } scanner = (synctex_scanner_t)_synctex_malloc(sizeof(_synctex_scanner_t)); if (NULL == scanner) { _synctex_error("SyncTeX: malloc problem"); free(synctex); gzclose(file); return NULL; } /* make a private copy of output for the scanner */ if (NULL == (scanner->output = (char *)malloc(strlen(output)+1))){ _synctex_error("! synctex_scanner_new_with_output_file: Memory problem (2), scanner's output is not reliable."); } else if (scanner->output != strcpy(scanner->output,output)) { _synctex_error("! synctex_scanner_new_with_output_file: Copy problem, scanner's output is not reliable."); } scanner->synctex = synctex;/* Now the scanner owns synctex */ SYNCTEX_FILE = file; return parse? synctex_scanner_parse(scanner):scanner; } int __synctex_open(const char * output, char ** synctex_name_ref, gzFile * file_ref, synctex_bool_t add_quotes, synctex_io_mode_t * io_mode_ref); /* This functions opens the file at the "output" given location. * It manages the problem of quoted filenames that appear with pdftex and filenames containing the space character. * In TeXLive 2008, the synctex file created with pdftex did contain unexpected quotes. * This function will remove them if possible. * All the reference arguments will take a value on return. They must be non NULL. * 0 on success, non 0 on error. */ int __synctex_open(const char * output, char ** synctex_name_ref, gzFile * file_ref, synctex_bool_t add_quotes, synctex_io_mode_t * io_mode_ref) { if (synctex_name_ref && file_ref && io_mode_ref) { /* 1 local variables that uses dynamic memory */ char * synctex_name = NULL; gzFile the_file = NULL; char * quoteless_synctex_name = NULL; size_t size = 0; synctex_io_mode_t io_mode = *io_mode_ref; const char * mode = _synctex_get_io_mode_name(io_mode); /* now create the synctex file name */ size = strlen(output)+strlen(synctex_suffix)+strlen(synctex_suffix_gz)+1; synctex_name = (char *)malloc(size); if (NULL == synctex_name) { _synctex_error("! __synctex_open: Memory problem (1)\n"); return 1; } /* we have reserved for synctex enough memory to copy output (including its 2 eventual quotes), both suffices, * including the terminating character. size is free now. */ if (synctex_name != strcpy(synctex_name,output)) { _synctex_error("! __synctex_open: Copy problem\n"); return_on_error: free(synctex_name); free(quoteless_synctex_name); return 2; } /* remove the last path extension if any */ _synctex_strip_last_path_extension(synctex_name); if (!strlen(synctex_name)) { goto return_on_error; } /* now insert quotes. */ if (add_quotes) { char * quoted = NULL; if (_synctex_copy_with_quoting_last_path_component(synctex_name,"ed,size) || (NULL == quoted)) { /* There was an error or quoting does not make sense: */ goto return_on_error; } quoteless_synctex_name = synctex_name; synctex_name = quoted; } /* Now add to synctex_name the first path extension. */ if (synctex_name != strcat(synctex_name,synctex_suffix)){ _synctex_error("! __synctex_open: Concatenation problem (can't add suffix '%s')\n",synctex_suffix); goto return_on_error; } /* Add to quoteless_synctex_name as well, if relevant. */ if (quoteless_synctex_name && (quoteless_synctex_name != strcat(quoteless_synctex_name,synctex_suffix))){ free(quoteless_synctex_name); quoteless_synctex_name = NULL; } if (NULL == (the_file = gzopen(synctex_name,mode))) { /* Could not open this file */ if (errno != ENOENT) { /* The file does exist, this is a lower level error, I can't do anything. */ _synctex_error("SyncTeX: could not open %s, error %i\n",synctex_name,errno); goto return_on_error; } /* Apparently, there is no uncompressed synctex file. Try the compressed version */ if (synctex_name != strcat(synctex_name,synctex_suffix_gz)){ _synctex_error("! __synctex_open: Concatenation problem (can't add suffix '%s')\n",synctex_suffix_gz); goto return_on_error; } io_mode |= synctex_io_gz_mask; mode = _synctex_get_io_mode_name(io_mode); /* the file is a compressed and is a binary file, this caused errors on Windows */ /* Add the suffix to the quoteless_synctex_name as well. */ if (quoteless_synctex_name && (quoteless_synctex_name != strcat(quoteless_synctex_name,synctex_suffix_gz))){ free(quoteless_synctex_name); quoteless_synctex_name = NULL; } if (NULL == (the_file = gzopen(synctex_name,mode))) { /* Could not open this file */ if (errno != ENOENT) { /* The file does exist, this is a lower level error, I can't do anything. */ _synctex_error("SyncTeX: could not open %s, error %i\n",synctex_name,errno); } goto return_on_error; } } /* At this point, the file is properly open. * If we are in the add_quotes mode, we change the file name by removing the quotes. */ if (quoteless_synctex_name) { gzclose(the_file); if (rename(synctex_name,quoteless_synctex_name)) { _synctex_error("SyncTeX: could not rename %s to %s, error %i\n",synctex_name,quoteless_synctex_name,errno); /* We could not rename, reopen the file with the quoted name. */ if (NULL == (the_file = gzopen(synctex_name,mode))) { /* No luck, could not re open this file, something has happened meanwhile */ if (errno != ENOENT) { /* The file does not exist any more, it has certainly be removed somehow * this is a lower level error, I can't do anything. */ _synctex_error("SyncTeX: could not open again %s, error %i\n",synctex_name,errno); } goto return_on_error; } } else { /* The file has been successfully renamed */ if (NULL == (the_file = gzopen(quoteless_synctex_name,mode))) { /* Could not open this file */ if (errno != ENOENT) { /* The file does exist, this is a lower level error, I can't do anything. */ _synctex_error("SyncTeX: could not open renamed %s, error %i\n",quoteless_synctex_name,errno); } goto return_on_error; } /* The quote free file name should replace the old one:*/ free(synctex_name); synctex_name = quoteless_synctex_name; quoteless_synctex_name = NULL; } } /* The operation is successfull, return the arguments by value. */ * file_ref = the_file; * io_mode_ref = io_mode; * synctex_name_ref = synctex_name; return 0; } return 3; /* Bad parameter. */ } /* Opens the ouput file, taking into account the eventual build_directory. * 0 on success, non 0 on error. */ int _synctex_open(const char * output, const char * build_directory, char ** synctex_name_ref, gzFile * file_ref, synctex_bool_t add_quotes, synctex_io_mode_t * io_mode_ref) { # define synctex_name (*synctex_name_ref) # define the_file (*file_ref) int result = __synctex_open(output,synctex_name_ref,file_ref,add_quotes,io_mode_ref); if ((result || !*file_ref) && build_directory && strlen(build_directory)) { char * build_output; const char *lpc; size_t size; synctex_bool_t is_absolute; build_output = NULL; lpc = _synctex_last_path_component(output); size = strlen(build_directory)+strlen(lpc)+2; /* One for the '/' and one for the '\0'. */ is_absolute = _synctex_path_is_absolute(build_directory); if (!is_absolute) { size += strlen(output); } if ((build_output = (char *)malloc(size))) { if (is_absolute) { build_output[0] = '\0'; } else { if (build_output != strcpy(build_output,output)) { return -4; } build_output[lpc-output]='\0'; } if (build_output == strcat(build_output,build_directory)) { /* Append a path separator if necessary. */ if (!SYNCTEX_IS_PATH_SEPARATOR(build_output[strlen(build_directory)-1])) { if (build_output != strcat(build_output,"/")) { return -2; } } /* Append the last path component of the output. */ if (build_output != strcat(build_output,lpc)) { return -3; } return __synctex_open(build_output,synctex_name_ref,file_ref,add_quotes,io_mode_ref); } } return -1; } return result; # undef synctex_name # undef the_file } /* The scanner destructor */ void synctex_scanner_free(synctex_scanner_t scanner) { if (NULL == scanner) { return; } if (SYNCTEX_FILE) { gzclose(SYNCTEX_FILE); SYNCTEX_FILE = NULL; } SYNCTEX_FREE(scanner->sheet); SYNCTEX_FREE(scanner->input); free(SYNCTEX_START); free(scanner->output_fmt); free(scanner->output); free(scanner->synctex); free(scanner->lists_of_friends); free(scanner); } /* Where the synctex scanner parses the contents of the file. */ synctex_scanner_t synctex_scanner_parse(synctex_scanner_t scanner) { synctex_status_t status = 0; if (!scanner || scanner->flags.has_parsed) { return scanner; } scanner->flags.has_parsed=1; scanner->pre_magnification = 1000; scanner->pre_unit = 8192; scanner->pre_x_offset = scanner->pre_y_offset = 578; /* initialize the offset with a fake unprobable value, * If there is a post scriptum section, this value will be overriden by the real life value */ scanner->x_offset = scanner->y_offset = 6.027e23f; scanner->class[synctex_node_type_sheet] = synctex_class_sheet; scanner->class[synctex_node_type_input] = synctex_class_input; (scanner->class[synctex_node_type_input]).scanner = scanner; (scanner->class[synctex_node_type_sheet]).scanner = scanner; scanner->class[synctex_node_type_vbox] = synctex_class_vbox; (scanner->class[synctex_node_type_vbox]).scanner = scanner; scanner->class[synctex_node_type_void_vbox] = synctex_class_void_vbox; (scanner->class[synctex_node_type_void_vbox]).scanner = scanner; scanner->class[synctex_node_type_hbox] = synctex_class_hbox; (scanner->class[synctex_node_type_hbox]).scanner = scanner; scanner->class[synctex_node_type_void_hbox] = synctex_class_void_hbox; (scanner->class[synctex_node_type_void_hbox]).scanner = scanner; scanner->class[synctex_node_type_kern] = synctex_class_kern; (scanner->class[synctex_node_type_kern]).scanner = scanner; scanner->class[synctex_node_type_glue] = synctex_class_glue; (scanner->class[synctex_node_type_glue]).scanner = scanner; scanner->class[synctex_node_type_math] = synctex_class_math; (scanner->class[synctex_node_type_math]).scanner = scanner; scanner->class[synctex_node_type_boundary] = synctex_class_boundary; (scanner->class[synctex_node_type_boundary]).scanner = scanner; SYNCTEX_START = (char *)malloc(SYNCTEX_BUFFER_SIZE+1); /* one more character for null termination */ if (NULL == SYNCTEX_START) { _synctex_error("SyncTeX: malloc error"); synctex_scanner_free(scanner); return NULL; } SYNCTEX_END = SYNCTEX_START+SYNCTEX_BUFFER_SIZE; /* SYNCTEX_END always points to a null terminating character. * Maybe there is another null terminating character between SYNCTEX_CUR and SYNCTEX_END-1. * At least, we are sure that SYNCTEX_CUR points to a string covering a valid part of the memory. */ *SYNCTEX_END = '\0'; SYNCTEX_CUR = SYNCTEX_END; status = _synctex_scan_preamble(scanner); if (statuspre_unit)/65536 pt = (scanner->pre_unit)/65781.76 bp * 1 pt = 65536 sp */ if (scanner->pre_unit<=0) { scanner->pre_unit = 8192; } if (scanner->pre_magnification<=0) { scanner->pre_magnification = 1000; } if (scanner->unit <= 0) { /* no post magnification */ scanner->unit = scanner->pre_unit / 65781.76;/* 65781.76 or 65536.0*/ } else { /* post magnification */ scanner->unit *= scanner->pre_unit / 65781.76; } scanner->unit *= scanner->pre_magnification / 1000.0; if (scanner->x_offset > 6e23) { /* no post offset */ scanner->x_offset = scanner->pre_x_offset * (scanner->pre_unit / 65781.76); scanner->y_offset = scanner->pre_y_offset * (scanner->pre_unit / 65781.76); } else { /* post offset */ scanner->x_offset /= 65781.76f; scanner->y_offset /= 65781.76f; } return scanner; #undef SYNCTEX_FILE } /* Scanner accessors. */ int synctex_scanner_pre_x_offset(synctex_scanner_t scanner){ return scanner?scanner->pre_x_offset:0; } int synctex_scanner_pre_y_offset(synctex_scanner_t scanner){ return scanner?scanner->pre_y_offset:0; } int synctex_scanner_x_offset(synctex_scanner_t scanner){ return scanner?scanner->x_offset:0; } int synctex_scanner_y_offset(synctex_scanner_t scanner){ return scanner?scanner->y_offset:0; } float synctex_scanner_magnification(synctex_scanner_t scanner){ return scanner?scanner->unit:1; } void synctex_scanner_display(synctex_scanner_t scanner) { if (NULL == scanner) { return; } printf("The scanner:\noutput:%s\noutput_fmt:%s\nversion:%i\n",scanner->output,scanner->output_fmt,scanner->version); printf("pre_unit:%i\nx_offset:%i\ny_offset:%i\n",scanner->pre_unit,scanner->pre_x_offset,scanner->pre_y_offset); printf("count:%i\npost_magnification:%f\npost_x_offset:%f\npost_y_offset:%f\n", scanner->count,scanner->unit,scanner->x_offset,scanner->y_offset); printf("The input:\n"); SYNCTEX_DISPLAY(scanner->input); if (scanner->count<1000) { printf("The sheets:\n"); SYNCTEX_DISPLAY(scanner->sheet); printf("The friends:\n"); if (scanner->lists_of_friends) { int i = scanner->number_of_lists; synctex_node_t node; while(i--) { printf("Friend index:%i\n",i); node = (scanner->lists_of_friends)[i]; while(node) { printf("%s:%i,%i\n", synctex_node_isa(node), SYNCTEX_TAG(node), SYNCTEX_LINE(node) ); node = SYNCTEX_FRIEND(node); } } } } else { printf("SyncTeX Warning: Too many objects\n"); } } /* Public*/ const char * synctex_scanner_get_name(synctex_scanner_t scanner,int tag) { synctex_node_t input = NULL; if (NULL == scanner) { return NULL; } input = scanner->input; do { if (tag == SYNCTEX_TAG(input)) { return (SYNCTEX_NAME(input)); } } while((input = SYNCTEX_SIBLING(input)) != NULL); return NULL; } int _synctex_scanner_get_tag(synctex_scanner_t scanner,const char * name); int _synctex_scanner_get_tag(synctex_scanner_t scanner,const char * name) { synctex_node_t input = NULL; if (NULL == scanner) { return 0; } input = scanner->input; do { if (_synctex_is_equivalent_file_name(name,(SYNCTEX_NAME(input)))) { return SYNCTEX_TAG(input); } } while((input = SYNCTEX_SIBLING(input)) != NULL); return 0; } int synctex_scanner_get_tag(synctex_scanner_t scanner,const char * name) { size_t char_index = strlen(name); if ((scanner = synctex_scanner_parse(scanner)) && (0 < char_index)) { /* the name is not void */ char_index -= 1; if (!SYNCTEX_IS_PATH_SEPARATOR(name[char_index])) { /* the last character of name is not a path separator */ int result = _synctex_scanner_get_tag(scanner,name); if (result) { return result; } else { /* the given name was not the one known by TeX * try a name relative to the enclosing directory of the scanner->output file */ const char * relative = name; const char * ptr = scanner->output; while((strlen(relative) > 0) && (strlen(ptr) > 0) && (*relative == *ptr)) { relative += 1; ptr += 1; } /* Find the last path separator before relative */ while(relative > name) { if (SYNCTEX_IS_PATH_SEPARATOR(*(relative-1))) { break; } relative -= 1; } if ((relative > name) && (result = _synctex_scanner_get_tag(scanner,relative))) { return result; } if (SYNCTEX_IS_PATH_SEPARATOR(name[0])) { /* No tag found for the given absolute name, * Try each relative path starting from the shortest one */ while(0input:NULL; } const char * synctex_scanner_get_output_fmt(synctex_scanner_t scanner) { return NULL != scanner && scanner->output_fmt?scanner->output_fmt:""; } const char * synctex_scanner_get_output(synctex_scanner_t scanner) { return NULL != scanner && scanner->output?scanner->output:""; } const char * synctex_scanner_get_synctex(synctex_scanner_t scanner) { return NULL != scanner && scanner->synctex?scanner->synctex:""; } # ifdef SYNCTEX_NOTHING # pragma mark - # pragma mark Public node attributes # endif int synctex_node_h(synctex_node_t node){ if (!node) { return 0; } return SYNCTEX_HORIZ(node); } int synctex_node_v(synctex_node_t node){ if (!node) { return 0; } return SYNCTEX_VERT(node); } int synctex_node_width(synctex_node_t node){ if (!node) { return 0; } return SYNCTEX_WIDTH(node); } int synctex_node_box_h(synctex_node_t node){ if (!node) { return 0; } if (SYNCTEX_IS_BOX(node)) { result: return SYNCTEX_HORIZ(node); } if ((node = SYNCTEX_PARENT(node)) && (node->class->type != synctex_node_type_sheet)) { goto result; } return 0; } int synctex_node_box_v(synctex_node_t node){ if (!node) { return 0; } if (SYNCTEX_IS_BOX(node)) { result: return SYNCTEX_VERT(node); } if ((node = SYNCTEX_PARENT(node)) && (node->class->type != synctex_node_type_sheet)) { goto result; } return 0; } int synctex_node_box_width(synctex_node_t node){ if (!node) { return 0; } if (SYNCTEX_IS_BOX(node)) { result: return SYNCTEX_WIDTH(node); } if ((node = SYNCTEX_PARENT(node)) && (node->class->type != synctex_node_type_sheet)) { goto result; } return 0; } int synctex_node_box_height(synctex_node_t node){ if (!node) { return 0; } if (SYNCTEX_IS_BOX(node)) { result: return SYNCTEX_HEIGHT(node); } if ((node = SYNCTEX_PARENT(node)) && (node->class->type != synctex_node_type_sheet)) { goto result; } return 0; } int synctex_node_box_depth(synctex_node_t node){ if (!node) { return 0; } if (SYNCTEX_IS_BOX(node)) { result: return SYNCTEX_DEPTH(node); } if ((node = SYNCTEX_PARENT(node)) && (node->class->type != synctex_node_type_sheet)) { goto result; } return 0; } # ifdef SYNCTEX_NOTHING # pragma mark - # pragma mark Public node visible attributes # endif float synctex_node_visible_h(synctex_node_t node){ if (!node) { return 0; } return SYNCTEX_HORIZ(node)*node->class->scanner->unit+node->class->scanner->x_offset; } float synctex_node_visible_v(synctex_node_t node){ if (!node) { return 0; } return SYNCTEX_VERT(node)*node->class->scanner->unit+node->class->scanner->y_offset; } float synctex_node_visible_width(synctex_node_t node){ if (!node) { return 0; } return SYNCTEX_WIDTH(node)*node->class->scanner->unit; } float synctex_node_box_visible_h(synctex_node_t node){ if (!node) { return 0; } switch(node->class->type) { case synctex_node_type_vbox: case synctex_node_type_void_vbox: case synctex_node_type_void_hbox: return SYNCTEX_HORIZ(node)*node->class->scanner->unit+node->class->scanner->x_offset; case synctex_node_type_hbox: result: return SYNCTEX_HORIZ_V(node)*node->class->scanner->unit+node->class->scanner->x_offset; } if ((node = SYNCTEX_PARENT(node)) && (node->class->type != synctex_node_type_sheet)) { goto result; } return 0; } float synctex_node_box_visible_v(synctex_node_t node){ if (!node) { return 0; } switch(node->class->type) { case synctex_node_type_vbox: case synctex_node_type_void_vbox: case synctex_node_type_void_hbox: return SYNCTEX_VERT(node)*node->class->scanner->unit+node->class->scanner->y_offset; case synctex_node_type_hbox: result: return SYNCTEX_VERT_V(node)*node->class->scanner->unit+node->class->scanner->y_offset; } if ((node = SYNCTEX_PARENT(node)) && (node->class->type != synctex_node_type_sheet)) { goto result; } return 0; } float synctex_node_box_visible_width(synctex_node_t node){ if (!node) { return 0; } switch(node->class->type) { case synctex_node_type_vbox: case synctex_node_type_void_vbox: case synctex_node_type_void_hbox: return SYNCTEX_WIDTH(node)*node->class->scanner->unit; case synctex_node_type_hbox: result: return SYNCTEX_WIDTH_V(node)*node->class->scanner->unit; } if ((node = SYNCTEX_PARENT(node)) && (node->class->type != synctex_node_type_sheet)) { goto result; } return 0; } float synctex_node_box_visible_height(synctex_node_t node){ if (!node) { return 0; } switch(node->class->type) { case synctex_node_type_vbox: case synctex_node_type_void_vbox: case synctex_node_type_void_hbox: return SYNCTEX_HEIGHT(node)*node->class->scanner->unit; case synctex_node_type_hbox: result: return SYNCTEX_HEIGHT_V(node)*node->class->scanner->unit; } if ((node = SYNCTEX_PARENT(node)) && (node->class->type != synctex_node_type_sheet)) { goto result; } return 0; } float synctex_node_box_visible_depth(synctex_node_t node){ if (!node) { return 0; } switch(node->class->type) { case synctex_node_type_vbox: case synctex_node_type_void_vbox: case synctex_node_type_void_hbox: return SYNCTEX_DEPTH(node)*node->class->scanner->unit; case synctex_node_type_hbox: result: return SYNCTEX_DEPTH_V(node)*node->class->scanner->unit; } if ((node = SYNCTEX_PARENT(node)) && (node->class->type != synctex_node_type_sheet)) { goto result; } return 0; } # ifdef SYNCTEX_NOTHING # pragma mark - # pragma mark Other public node attributes # endif int synctex_node_page(synctex_node_t node){ synctex_node_t parent = NULL; if (!node) { return -1; } parent = SYNCTEX_PARENT(node); while(parent) { node = parent; parent = SYNCTEX_PARENT(node); } if (node->class->type == synctex_node_type_sheet) { return SYNCTEX_PAGE(node); } return -1; } int synctex_node_tag(synctex_node_t node) { return node?SYNCTEX_TAG(node):-1; } int synctex_node_line(synctex_node_t node) { return node?SYNCTEX_LINE(node):-1; } int synctex_node_column(synctex_node_t node) { # ifdef __DARWIN_UNIX03 # pragma unused(node) # endif return -1; } # ifdef SYNCTEX_NOTHING # pragma mark - # pragma mark Sheet # endif synctex_node_t synctex_sheet_content(synctex_scanner_t scanner,int page) { if (scanner) { synctex_node_t sheet = scanner->sheet; while(sheet) { if (page == SYNCTEX_PAGE(sheet)) { return SYNCTEX_CHILD(sheet); } sheet = SYNCTEX_SIBLING(sheet); } } return NULL; } # ifdef SYNCTEX_NOTHING # pragma mark - # pragma mark Query # endif int synctex_display_query(synctex_scanner_t scanner,const char * name,int line,int column) { # ifdef __DARWIN_UNIX03 # pragma unused(column) # endif int tag = synctex_scanner_get_tag(scanner,name); size_t size = 0; int friend_index = 0; int max_line = 0; synctex_node_t node = NULL; if (tag == 0) { printf("SyncTeX Warning: No tag for %s\n",name); return -1; } free(SYNCTEX_START); SYNCTEX_CUR = SYNCTEX_END = SYNCTEX_START = NULL; max_line = line < INT_MAX-scanner->number_of_lists ? line+scanner->number_of_lists:INT_MAX; while(linenumber_of_lists); if ((node = (scanner->lists_of_friends)[friend_index])) { do { if ((synctex_node_type(node)>=synctex_node_type_boundary) && (tag == SYNCTEX_TAG(node)) && (line == SYNCTEX_LINE(node))) { if (SYNCTEX_CUR == SYNCTEX_END) { size += 16; SYNCTEX_END = realloc(SYNCTEX_START,size*sizeof(synctex_node_t *)); SYNCTEX_CUR += SYNCTEX_END - SYNCTEX_START; SYNCTEX_START = SYNCTEX_END; SYNCTEX_END = SYNCTEX_START + size*sizeof(synctex_node_t *); } *(synctex_node_t *)SYNCTEX_CUR = node; SYNCTEX_CUR += sizeof(synctex_node_t); } } while((node = SYNCTEX_FRIEND(node))); if (SYNCTEX_START == NULL) { /* We did not find any matching boundary, retry with glue or kern */ node = (scanner->lists_of_friends)[friend_index];/* no need to test it again, already done */ do { if ((synctex_node_type(node)>=synctex_node_type_kern) && (tag == SYNCTEX_TAG(node)) && (line == SYNCTEX_LINE(node))) { if (SYNCTEX_CUR == SYNCTEX_END) { size += 16; SYNCTEX_END = realloc(SYNCTEX_START,size*sizeof(synctex_node_t *)); SYNCTEX_CUR += SYNCTEX_END - SYNCTEX_START; SYNCTEX_START = SYNCTEX_END; SYNCTEX_END = SYNCTEX_START + size*sizeof(synctex_node_t *); } *(synctex_node_t *)SYNCTEX_CUR = node; SYNCTEX_CUR += sizeof(synctex_node_t); } } while((node = SYNCTEX_FRIEND(node))); if (SYNCTEX_START == NULL) { /* We did not find any matching glue or kern, retry with boxes */ node = (scanner->lists_of_friends)[friend_index];/* no need to test it again, already done */ do { if ((tag == SYNCTEX_TAG(node)) && (line == SYNCTEX_LINE(node))) { if (SYNCTEX_CUR == SYNCTEX_END) { size += 16; SYNCTEX_END = realloc(SYNCTEX_START,size*sizeof(synctex_node_t *)); SYNCTEX_CUR += SYNCTEX_END - SYNCTEX_START; SYNCTEX_START = SYNCTEX_END; SYNCTEX_END = SYNCTEX_START + size*sizeof(synctex_node_t *); } *(synctex_node_t *)SYNCTEX_CUR = node; SYNCTEX_CUR += sizeof(synctex_node_t); } } while((node = SYNCTEX_FRIEND(node))); } } SYNCTEX_END = SYNCTEX_CUR; /* Now reverse the order to have nodes in display order, and keep just a few nodes */ if ((SYNCTEX_START) && (SYNCTEX_END)) { synctex_node_t * start_ref = (synctex_node_t *)SYNCTEX_START; synctex_node_t * end_ref = (synctex_node_t *)SYNCTEX_END; end_ref -= 1; while(start_ref < end_ref) { node = *start_ref; *start_ref = *end_ref; *end_ref = node; start_ref += 1; end_ref -= 1; } /* Basically, we keep the first node for each parent. * More precisely, we keep only nodes that are not descendants of * their predecessor's parent. */ start_ref = (synctex_node_t *)SYNCTEX_START; end_ref = (synctex_node_t *)SYNCTEX_START; next_end: end_ref += 1; /* we allways have start_ref<= end_ref*/ if (end_ref < (synctex_node_t *)SYNCTEX_END) { node = *end_ref; while((node = SYNCTEX_PARENT(node))) { if (SYNCTEX_PARENT(*start_ref) == node) { goto next_end; } } start_ref += 1; *start_ref = *end_ref; goto next_end; } start_ref += 1; SYNCTEX_END = (char *)start_ref; SYNCTEX_CUR = NULL;// added on behalf of Jose Alliste return (SYNCTEX_END-SYNCTEX_START)/sizeof(synctex_node_t);// added on behalf Jan Sundermeyer } SYNCTEX_CUR = NULL; // return (SYNCTEX_END-SYNCTEX_START)/sizeof(synctex_node_t); removed on behalf Jan Sundermeyer } # if defined(__SYNCTEX_STRONG_DISPLAY_QUERY__) break; # else ++line; # endif } return 0; } synctex_node_t synctex_next_result(synctex_scanner_t scanner) { if (NULL == SYNCTEX_CUR) { SYNCTEX_CUR = SYNCTEX_START; } else { SYNCTEX_CUR+=sizeof(synctex_node_t); } if (SYNCTEX_CUR= scanner->unit) {/* scanner->unit must be >0 */ return 0; } /* Convert the given point to scanner integer coordinates */ hitPoint.h = (h-scanner->x_offset)/scanner->unit; hitPoint.v = (v-scanner->y_offset)/scanner->unit; /* We will store in the scanner's buffer the result of the query. */ free(SYNCTEX_START); SYNCTEX_START = SYNCTEX_END = SYNCTEX_CUR = NULL; /* Find the proper sheet */ sheet = scanner->sheet; while((sheet) && SYNCTEX_PAGE(sheet) != page) { sheet = SYNCTEX_SIBLING(sheet); } if (NULL == sheet) { return -1; } /* Now sheet points to the sheet node with proper page number */ /* Here is how we work: * At first we do not consider the visible box dimensions. This will cover the most frequent cases. * Then we try with the visible box dimensions. * We try to find a non void box containing the hit point. * We browse all the horizontal boxes until we find one containing the hit point. */ if ((node = SYNCTEX_NEXT_HORIZ_BOX(sheet))) { do { if (_synctex_point_in_box(hitPoint,node,synctex_YES)) { /* Maybe the hitPoint belongs to a contained vertical box. */ end: /* This trick is for catching overlapping boxes */ if ((other_node = SYNCTEX_NEXT_HORIZ_BOX(node))) { do { if (_synctex_point_in_box(hitPoint,other_node,synctex_YES)) { node = _synctex_smallest_container(other_node,node); } } while((other_node = SYNCTEX_NEXT_HORIZ_BOX(other_node))); } /* node is the smallest horizontal box that contains hitPoint. */ if ((bestContainer = _synctex_eq_deepest_container(hitPoint,node,synctex_YES))) { node = bestContainer; } _synctex_eq_get_closest_children_in_box(hitPoint,node,&bestNodes,&bestDistances,synctex_YES); if (bestNodes.right && bestNodes.left) { if ((SYNCTEX_TAG(bestNodes.right)!=SYNCTEX_TAG(bestNodes.left)) || (SYNCTEX_LINE(bestNodes.right)!=SYNCTEX_LINE(bestNodes.left)) || (SYNCTEX_COLUMN(bestNodes.right)!=SYNCTEX_COLUMN(bestNodes.left))) { if ((SYNCTEX_START = malloc(2*sizeof(synctex_node_t)))) { if (bestDistances.left>bestDistances.right) { ((synctex_node_t *)SYNCTEX_START)[0] = bestNodes.right; ((synctex_node_t *)SYNCTEX_START)[1] = bestNodes.left; } else { ((synctex_node_t *)SYNCTEX_START)[0] = bestNodes.left; ((synctex_node_t *)SYNCTEX_START)[1] = bestNodes.right; } SYNCTEX_END = SYNCTEX_START + 2*sizeof(synctex_node_t); SYNCTEX_CUR = NULL; return (SYNCTEX_END-SYNCTEX_START)/sizeof(synctex_node_t); } return SYNCTEX_STATUS_ERROR; } /* both nodes have the same input coordinates * We choose the one closest to the hit point */ if (bestDistances.left>bestDistances.right) { bestNodes.left = bestNodes.right; } bestNodes.right = NULL; } else if (bestNodes.right) { bestNodes.left = bestNodes.right; } else if (!bestNodes.left){ bestNodes.left = node; } if ((SYNCTEX_START = malloc(sizeof(synctex_node_t)))) { * (synctex_node_t *)SYNCTEX_START = bestNodes.left; SYNCTEX_END = SYNCTEX_START + sizeof(synctex_node_t); SYNCTEX_CUR = NULL; return (SYNCTEX_END-SYNCTEX_START)/sizeof(synctex_node_t); } return SYNCTEX_STATUS_ERROR; } } while ((node = SYNCTEX_NEXT_HORIZ_BOX(node))); /* All the horizontal boxes have been tested, * None of them contains the hit point. */ } /* We are not lucky */ if ((node = SYNCTEX_CHILD(sheet))) { goto end; } return 0; } # ifdef SYNCTEX_NOTHING # pragma mark - # pragma mark Utilities # endif int _synctex_bail(void) { _synctex_error("SyncTeX ERROR\n"); return -1; } /* Rougly speaking, this is: * node's h coordinate - hitPoint's h coordinate. * If node is to the right of the hit point, then this distance is positive, * if node is to the left of the hit point, this distance is negative.*/ int _synctex_point_h_distance(synctex_point_t hitPoint, synctex_node_t node, synctex_bool_t visible); int _synctex_point_h_distance(synctex_point_t hitPoint, synctex_node_t node, synctex_bool_t visible) { if (node) { int min,med,max; switch(node->class->type) { /* The distance between a point and a box is special. * It is not the euclidian distance, nor something similar. * We have to take into account the particular layout, * and the box hierarchy. * Given a box, there are 9 regions delimited by the lines of the edges of the box. * The origin being at the top left corner of the page, * we also give names to the vertices of the box. * * 1 | 2 | 3 * ---A---B---> * 4 | 5 | 6 * ---C---D---> * 7 | 8 | 9 * v v */ case synctex_node_type_hbox: /* getting the box bounds, taking into account negative width, height and depth. */ min = visible?SYNCTEX_HORIZ_V(node):SYNCTEX_HORIZ(node); max = min + (visible?SYNCTEX_ABS_WIDTH_V(node):SYNCTEX_ABS_WIDTH(node)); /* We allways have min <= max */ if (hitPoint.h 0 */ } else if (hitPoint.h>max) { return max - hitPoint.h; /* regions 3+6+9, result is < 0 */ } else { return 0; /* regions 2+5+8, inside the box, except for vertical coordinates */ } break; case synctex_node_type_vbox: case synctex_node_type_void_vbox: case synctex_node_type_void_hbox: /* getting the box bounds, taking into account negative width, height and depth. * For these boxes, no visible dimension available */ min = SYNCTEX_HORIZ(node); max = min + SYNCTEX_ABS_WIDTH(node); /* We allways have min <= max */ if (hitPoint.h 0 */ } else if (hitPoint.h>max) { return max - hitPoint.h; /* regions 3+6+9, result is < 0 */ } else { return 0; /* regions 2+5+8, inside the box, except for vertical coordinates */ } break; case synctex_node_type_kern: /* IMPORTANT NOTICE: the location of the kern is recorded AFTER the move. * The distance to the kern is very special, * in general, there is no text material in the kern, * this is why we compute the offset relative to the closest edge of the kern.*/ max = SYNCTEX_WIDTH(node); if (max<0) { min = SYNCTEX_HORIZ(node); max = min - max; } else { min = -max; max = SYNCTEX_HORIZ(node); min += max; } med = (min+max)/2; /* positive kern: '.' means text, '>' means kern offset * ............. * min>>>>med>>>>max * ............... * negative kern: '.' means text, '<' means kern offset * ............................ * min<<<max) { return max - hitPoint.h - 1; /* same kind of penalty */ } else if (hitPoint.h>med) { /* do things like if the node had 0 width and was placed at the max edge + 1*/ return max - hitPoint.h + 1; /* positive, the kern is to the right of the hitPoint */ } else { return min - hitPoint.h - 1; /* negative, the kern is to the left of the hitPoint */ } case synctex_node_type_glue: case synctex_node_type_math: return SYNCTEX_HORIZ(node) - hitPoint.h; } } return INT_MAX;/* We always assume that the node is faraway to the right*/ } /* Rougly speaking, this is: * node's v coordinate - hitPoint's v coordinate. * If node is at the top of the hit point, then this distance is positive, * if node is at the bottom of the hit point, this distance is negative.*/ int _synctex_point_v_distance(synctex_point_t hitPoint, synctex_node_t node,synctex_bool_t visible); int _synctex_point_v_distance(synctex_point_t hitPoint, synctex_node_t node,synctex_bool_t visible) { # ifdef __DARWIN_UNIX03 # pragma unused(visible) # endif if (node) { int min,max; switch(node->class->type) { /* The distance between a point and a box is special. * It is not the euclidian distance, nor something similar. * We have to take into account the particular layout, * and the box hierarchy. * Given a box, there are 9 regions delimited by the lines of the edges of the box. * The origin being at the top left corner of the page, * we also give names to the vertices of the box. * * 1 | 2 | 3 * ---A---B---> * 4 | 5 | 6 * ---C---D---> * 7 | 8 | 9 * v v */ case synctex_node_type_hbox: /* getting the box bounds, taking into account negative width, height and depth. */ min = SYNCTEX_VERT_V(node); max = min + SYNCTEX_ABS_DEPTH_V(node); min -= SYNCTEX_ABS_HEIGHT_V(node); /* We allways have min <= max */ if (hitPoint.v 0 */ } else if (hitPoint.v>max) { return max - hitPoint.v; /* regions 7+8+9, result is < 0 */ } else { return 0; /* regions 4.5.6, inside the box, except for horizontal coordinates */ } break; case synctex_node_type_vbox: case synctex_node_type_void_vbox: case synctex_node_type_void_hbox: /* getting the box bounds, taking into account negative width, height and depth. */ min = SYNCTEX_VERT(node); max = min + SYNCTEX_ABS_DEPTH(node); min -= SYNCTEX_ABS_HEIGHT(node); /* We allways have min <= max */ if (hitPoint.v 0 */ } else if (hitPoint.v>max) { return max - hitPoint.v; /* regions 7+8+9, result is < 0 */ } else { return 0; /* regions 4.5.6, inside the box, except for horizontal coordinates */ } break; case synctex_node_type_kern: case synctex_node_type_glue: case synctex_node_type_math: return SYNCTEX_VERT(node) - hitPoint.v; } } return INT_MAX;/* We always assume that the node is faraway to the top*/ } SYNCTEX_INLINE static synctex_node_t _synctex_smallest_container(synctex_node_t node, synctex_node_t other_node) { float height, other_height; if (SYNCTEX_ABS_WIDTH(node)SYNCTEX_ABS_WIDTH(other_node)) { return other_node; } height = SYNCTEX_ABS_DEPTH(node) + SYNCTEX_ABS_HEIGHT(node); other_height = SYNCTEX_ABS_DEPTH(other_node) + SYNCTEX_ABS_HEIGHT(other_node); if (heightother_height) { return other_node; } return node; } synctex_bool_t _synctex_point_in_box(synctex_point_t hitPoint, synctex_node_t node, synctex_bool_t visible) { if (node) { if (0 == _synctex_point_h_distance(hitPoint,node,visible) && 0 == _synctex_point_v_distance(hitPoint,node,visible)) { return synctex_YES; } } return synctex_NO; } int _synctex_node_distance_to_point(synctex_point_t hitPoint, synctex_node_t node, synctex_bool_t visible) { # ifdef __DARWIN_UNIX03 # pragma unused(visible) # endif int result = INT_MAX; /* when the distance is meaning less (sheet, input...) */ if (node) { int minH,maxH,minV,maxV; switch(node->class->type) { /* The distance between a point and a box is special. * It is not the euclidian distance, nor something similar. * We have to take into account the particular layout, * and the box hierarchy. * Given a box, there are 9 regions delimited by the lines of the edges of the box. * The origin being at the top left corner of the page, * we also give names to the vertices of the box. * * 1 | 2 | 3 * ---A---B---> * 4 | 5 | 6 * ---C---D---> * 7 | 8 | 9 * v v * In each region, there is a different formula. * In the end we have a continuous distance which may not be a mathematical distance but who cares. */ case synctex_node_type_vbox: case synctex_node_type_void_vbox: case synctex_node_type_hbox: case synctex_node_type_void_hbox: /* getting the box bounds, taking into account negative widths. */ minH = SYNCTEX_HORIZ(node); maxH = minH + SYNCTEX_ABS_WIDTH(node); minV = SYNCTEX_VERT(node); maxV = minV + SYNCTEX_ABS_DEPTH(node); minV -= SYNCTEX_ABS_HEIGHT(node); /* In what region is the point hitPoint=(H,V) ? */ if (hitPoint.vminV) { result = hitPoint.v - minV + minH - hitPoint.h; } else { result = minV - hitPoint.v + minH - hitPoint.h; } } else if (hitPoint.h>maxH) { if (hitPoint.v>minV) { result = hitPoint.v - minV + hitPoint.h - maxH; } else { result = minV - hitPoint.v + hitPoint.h - maxH; } } else if (hitPoint.v>minV) { result = hitPoint.v - minV; } else { result = minV - hitPoint.v; } break; case synctex_node_type_glue: case synctex_node_type_math: minH = SYNCTEX_HORIZ(node); minV = SYNCTEX_VERT(node); if (hitPoint.hminV) { result = hitPoint.v - minV + minH - hitPoint.h; } else { result = minV - hitPoint.v + minH - hitPoint.h; } } else if (hitPoint.v>minV) { result = hitPoint.v - minV + hitPoint.h - minH; } else { result = minV - hitPoint.v + hitPoint.h - minH; } break; } } return result; } static synctex_node_t _synctex_eq_deepest_container(synctex_point_t hitPoint,synctex_node_t node, synctex_bool_t visible) { if (node) { synctex_node_t result = NULL; synctex_node_t child = NULL; switch(node->class->type) { case synctex_node_type_vbox: case synctex_node_type_hbox: /* test the deep nodes first */ if ((child = SYNCTEX_CHILD(node))) { do { if ((result = _synctex_eq_deepest_container(hitPoint,child,visible))) { return result; } } while((child = SYNCTEX_SIBLING(child))); } /* is the hit point inside the box? */ if (_synctex_point_in_box(hitPoint,node,visible)) { /* for vboxes we try to use some node inside. * Walk through the list of siblings until we find the closest one. * Only consider siblings with children. */ if ((node->class->type == synctex_node_type_vbox) && (child = SYNCTEX_CHILD(node))) { int bestDistance = INT_MAX; do { if (SYNCTEX_CHILD(child)) { int distance = _synctex_node_distance_to_point(hitPoint,child,visible); if (distance < bestDistance) { bestDistance = distance; node = child; } } } while((child = SYNCTEX_SIBLING(child))); } return node; } } } return NULL; } /* Compares the locations of the hitPoint with the locations of the various nodes contained in the box. * As it is an horizontal box, we only compare horizontal coordinates. */ SYNCTEX_INLINE static int __synctex_eq_get_closest_children_in_hbox(synctex_point_t hitPoint, synctex_node_t node, synctex_node_set_t* bestNodesRef,synctex_distances_t* bestDistancesRef, synctex_bool_t visible); SYNCTEX_INLINE static int __synctex_eq_get_closest_children_in_hbox(synctex_point_t hitPoint, synctex_node_t node, synctex_node_set_t* bestNodesRef,synctex_distances_t* bestDistancesRef, synctex_bool_t visible) { int result = 0; if ((node = SYNCTEX_CHILD(node))) { do { int off7 = _synctex_point_h_distance(hitPoint,node,visible); if (off7 > 0) { /* node is to the right of the hit point. * We compare node and the previously recorded one, through the recorded distance. * If the nodes have the same tag, prefer the one with the smallest line number, * if the nodes also have the same line number, prefer the one with the smallest column. */ if (bestDistancesRef->right > off7) { bestDistancesRef->right = off7; bestNodesRef->right = node; result |= SYNCTEX_MASK_RIGHT; } else if (bestDistancesRef->right == off7 && bestNodesRef->right) { if (SYNCTEX_TAG(bestNodesRef->right) == SYNCTEX_TAG(node) && (SYNCTEX_LINE(bestNodesRef->right) > SYNCTEX_LINE(node) || (SYNCTEX_LINE(bestNodesRef->right) == SYNCTEX_LINE(node) && SYNCTEX_COLUMN(bestNodesRef->right) > SYNCTEX_COLUMN(node)))) { bestNodesRef->right = node; result |= SYNCTEX_MASK_RIGHT; } } } else if (off7 == 0) { /* hitPoint is inside node. */ bestDistancesRef->left = bestDistancesRef->right = 0; bestNodesRef->left = node; bestNodesRef->right = NULL; result |= SYNCTEX_MASK_LEFT; } else { /* here off7 < 0, hitPoint is to the right of node */ off7 = -off7; if (bestDistancesRef->left > off7) { bestDistancesRef->left = off7; bestNodesRef->left = node; result |= SYNCTEX_MASK_LEFT; } else if (bestDistancesRef->left == off7 && bestNodesRef->left) { if (SYNCTEX_TAG(bestNodesRef->left) == SYNCTEX_TAG(node) && (SYNCTEX_LINE(bestNodesRef->left) > SYNCTEX_LINE(node) || (SYNCTEX_LINE(bestNodesRef->left) == SYNCTEX_LINE(node) && SYNCTEX_COLUMN(bestNodesRef->left) > SYNCTEX_COLUMN(node)))) { bestNodesRef->left = node; result |= SYNCTEX_MASK_LEFT; } } } } while((node = SYNCTEX_SIBLING(node))); if (result & SYNCTEX_MASK_LEFT) { /* the left node is new, try to narrow the result */ if ((node = _synctex_eq_deepest_container(hitPoint,bestNodesRef->left,visible))) { bestNodesRef->left = node; } if ((node = _synctex_eq_closest_child(hitPoint,bestNodesRef->left,visible))) { bestNodesRef->left = node; } } if (result & SYNCTEX_MASK_RIGHT) { /* the right node is new, try to narrow the result */ if ((node = _synctex_eq_deepest_container(hitPoint,bestNodesRef->right,visible))) { bestNodesRef->right = node; } if ((node = _synctex_eq_closest_child(hitPoint,bestNodesRef->right,visible))) { bestNodesRef->right = node; } } } return result; } SYNCTEX_INLINE static int __synctex_eq_get_closest_children_in_vbox(synctex_point_t hitPoint, synctex_node_t node, synctex_node_set_t* bestNodesRef,synctex_distances_t* bestDistancesRef,synctex_bool_t visible); SYNCTEX_INLINE static int __synctex_eq_get_closest_children_in_vbox(synctex_point_t hitPoint, synctex_node_t node, synctex_node_set_t* bestNodesRef,synctex_distances_t* bestDistancesRef,synctex_bool_t visible) { int result = 0; if ((node = SYNCTEX_CHILD(node))) { do { int off7 = _synctex_point_v_distance(hitPoint,node,visible);/* this is what makes the difference with the h version above */ if (off7 > 0) { /* node is to the top of the hit point (below because TeX is oriented from top to bottom. * We compare node and the previously recorded one, through the recorded distance. * If the nodes have the same tag, prefer the one with the smallest line number, * if the nodes also have the same line number, prefer the one with the smallest column. */ if (bestDistancesRef->right > off7) { bestDistancesRef->right = off7; bestNodesRef->right = node; result |= SYNCTEX_MASK_RIGHT; } else if (bestDistancesRef->right == off7 && bestNodesRef->right) { if (SYNCTEX_TAG(bestNodesRef->right) == SYNCTEX_TAG(node) && (SYNCTEX_LINE(bestNodesRef->right) > SYNCTEX_LINE(node) || (SYNCTEX_LINE(bestNodesRef->right) == SYNCTEX_LINE(node) && SYNCTEX_COLUMN(bestNodesRef->right) > SYNCTEX_COLUMN(node)))) { bestNodesRef->right = node; result |= SYNCTEX_MASK_RIGHT; } } } else if (off7 == 0) { bestDistancesRef->left = bestDistancesRef->right = 0; bestNodesRef->left = node; bestNodesRef->right = NULL; result |= SYNCTEX_MASK_LEFT; } else { /* here off7 < 0 */ off7 = -off7; if (bestDistancesRef->left > off7) { bestDistancesRef->left = off7; bestNodesRef->left = node; result |= SYNCTEX_MASK_LEFT; } else if (bestDistancesRef->left == off7 && bestNodesRef->left) { if (SYNCTEX_TAG(bestNodesRef->left) == SYNCTEX_TAG(node) && (SYNCTEX_LINE(bestNodesRef->left) > SYNCTEX_LINE(node) || (SYNCTEX_LINE(bestNodesRef->left) == SYNCTEX_LINE(node) && SYNCTEX_COLUMN(bestNodesRef->left) > SYNCTEX_COLUMN(node)))) { bestNodesRef->left = node; result |= SYNCTEX_MASK_LEFT; } } } } while((node = SYNCTEX_SIBLING(node))); if (result & SYNCTEX_MASK_LEFT) { /* the left node is new, try to narrow the result */ if ((node = _synctex_eq_deepest_container(hitPoint,bestNodesRef->left,visible))) { bestNodesRef->left = node; } if ((node = _synctex_eq_closest_child(hitPoint,bestNodesRef->left,visible))) { bestNodesRef->left = node; } } if (result & SYNCTEX_MASK_RIGHT) { /* the right node is new, try to narrow the result */ if ((node = _synctex_eq_deepest_container(hitPoint,bestNodesRef->right,visible))) { bestNodesRef->right = node; } if ((node = _synctex_eq_closest_child(hitPoint,bestNodesRef->right,visible))) { bestNodesRef->right = node; } } } return result; } SYNCTEX_INLINE static int _synctex_eq_get_closest_children_in_box(synctex_point_t hitPoint, synctex_node_t node, synctex_node_set_t* bestNodesRef,synctex_distances_t* bestDistancesRef,synctex_bool_t visible) { if (node) { switch(node->class->type) { case synctex_node_type_hbox: return __synctex_eq_get_closest_children_in_hbox(hitPoint, node, bestNodesRef, bestDistancesRef,visible); case synctex_node_type_vbox: return __synctex_eq_get_closest_children_in_vbox(hitPoint, node, bestNodesRef, bestDistancesRef,visible); } } return 0; } SYNCTEX_INLINE static synctex_node_t __synctex_eq_closest_child(synctex_point_t hitPoint, synctex_node_t node,int* distanceRef, synctex_bool_t visible); SYNCTEX_INLINE static synctex_node_t __synctex_eq_closest_child(synctex_point_t hitPoint, synctex_node_t node,int* distanceRef, synctex_bool_t visible) { synctex_node_t best_node = NULL; if ((node = SYNCTEX_CHILD(node))) { do { int distance = _synctex_node_distance_to_point(hitPoint,node,visible); synctex_node_t candidate = NULL; if (distance<=*distanceRef) { *distanceRef = distance; best_node = node; } switch(node->class->type) { case synctex_node_type_vbox: case synctex_node_type_hbox: if ((candidate = __synctex_eq_closest_child(hitPoint,node,distanceRef,visible))) { best_node = candidate; } } } while((node = SYNCTEX_SIBLING(node))); } return best_node; } SYNCTEX_INLINE static synctex_node_t _synctex_eq_closest_child(synctex_point_t hitPoint,synctex_node_t node, synctex_bool_t visible) { if (node) { switch(node->class->type) { case synctex_node_type_hbox: case synctex_node_type_vbox: { int best_distance = INT_MAX; synctex_node_t best_node = __synctex_eq_closest_child(hitPoint,node,&best_distance,visible); if ((best_node)) { synctex_node_t child = NULL; switch(best_node->class->type) { case synctex_node_type_vbox: case synctex_node_type_hbox: if ((child = SYNCTEX_CHILD(best_node))) { best_distance = _synctex_node_distance_to_point(hitPoint,child,visible); while((child = SYNCTEX_SIBLING(child))) { int distance = _synctex_node_distance_to_point(hitPoint,child,visible); if (distance<=best_distance) { best_distance = distance; best_node = child; } } } } } return best_node; } } } return NULL; } # ifdef SYNCTEX_NOTHING # pragma mark - # pragma mark Updater # endif typedef int (*synctex_fprintf_t)(void *, const char * , ...); /* print formatted to either FILE * or gzFile */ # define SYNCTEX_BITS_PER_BYTE 8 struct __synctex_updater_t { void *file; /* the foo.synctex or foo.synctex.gz I/O identifier */ synctex_fprintf_t fprintf; /* either fprintf or gzprintf */ int length; /* the number of chars appended */ struct _flags { unsigned int no_gz:1; /* Whether zlib is used or not */ unsigned int reserved:SYNCTEX_BITS_PER_BYTE*sizeof(int)-1; /* Align */ } flags; }; # define SYNCTEX_FILE updater->file # define SYNCTEX_NO_GZ ((updater->flags).no_gz) # define SYNCTEX_fprintf (*(updater->fprintf)) synctex_updater_t synctex_updater_new_with_output_file(const char * output, const char * build_directory) { synctex_updater_t updater = NULL; char * synctex = NULL; synctex_io_mode_t io_mode = 0; const char * mode = NULL; /* prepare the updater, the memory is the only one dynamically allocated */ updater = (synctex_updater_t)_synctex_malloc(sizeof(synctex_updater_t)); if (NULL == updater) { _synctex_error("! synctex_updater_new_with_file: malloc problem"); return NULL; } if (_synctex_open(output,build_directory,&synctex,&SYNCTEX_FILE,synctex_ADD_QUOTES,&io_mode) && _synctex_open(output,build_directory,&synctex,&SYNCTEX_FILE,synctex_DONT_ADD_QUOTES,&io_mode)) { return_on_error: free(updater); updater = NULL; return NULL; } /* OK, the file exists, we close it and reopen it with the correct mode. * The receiver is now the owner of the "synctex" variable. */ gzclose(SYNCTEX_FILE); SYNCTEX_FILE = NULL; SYNCTEX_NO_GZ = (io_mode&synctex_io_gz_mask)?synctex_NO:synctex_YES; mode = _synctex_get_io_mode_name(io_mode|synctex_io_append_mask);/* either "a" or "ab", depending on the file extension */ if (SYNCTEX_NO_GZ) { if (NULL == (SYNCTEX_FILE = (void *)fopen(synctex,mode))) { no_write_error: _synctex_error("! synctex_updater_new_with_file: Can't append to %s",synctex); free(synctex); goto return_on_error; } updater->fprintf = (synctex_fprintf_t)(&fprintf); } else { if (NULL == (SYNCTEX_FILE = (void *)gzopen(synctex,mode))) { goto no_write_error; } updater->fprintf = (synctex_fprintf_t)(&gzprintf); } printf("SyncTeX: updating %s...",synctex); free(synctex); return updater; } void synctex_updater_append_magnification(synctex_updater_t updater, char * magnification){ if (NULL==updater) { return; } if (magnification && strlen(magnification)) { updater->length += SYNCTEX_fprintf(SYNCTEX_FILE,"Magnification:%s\n",magnification); } } void synctex_updater_append_x_offset(synctex_updater_t updater, char * x_offset){ if (NULL==updater) { return; } if (x_offset && strlen(x_offset)) { updater->length += SYNCTEX_fprintf(SYNCTEX_FILE,"X Offset:%s\n",x_offset); } } void synctex_updater_append_y_offset(synctex_updater_t updater, char * y_offset){ if (NULL==updater) { return; } if (y_offset && strlen(y_offset)) { updater->length += SYNCTEX_fprintf(SYNCTEX_FILE,"Y Offset:%s\n",y_offset); } } void synctex_updater_free(synctex_updater_t updater){ if (NULL==updater) { return; } if (updater->length>0) { SYNCTEX_fprintf(SYNCTEX_FILE,"!%i\n",updater->length); } if (SYNCTEX_NO_GZ) { fclose((FILE *)SYNCTEX_FILE); } else { gzclose((gzFile)SYNCTEX_FILE); } free(updater); printf("... done.\n"); return; } pdf-tools-0.80/server/synctex_parser.h000066400000000000000000000356061315513274600201210ustar00rootroot00000000000000/* Copyright (c) 2008, 2009, 2010 , 2011 jerome DOT laurens AT u-bourgogne DOT fr This file is part of the SyncTeX package. Latest Revision: Tue Jun 14 08:23:30 UTC 2011 Version: 1.16 See synctex_parser_readme.txt for more details License: -------- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE Except as contained in this notice, the name of the copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from the copyright holder. Acknowledgments: ---------------- The author received useful remarks from the pdfTeX developers, especially Hahn The Thanh, and significant help from XeTeX developer Jonathan Kew Nota Bene: ---------- If you include or use a significant part of the synctex package into a software, I would appreciate to be listed as contributor and see "SyncTeX" highlighted. Version 1 Thu Jun 19 09:39:21 UTC 2008 */ #ifndef __SYNCTEX_PARSER__ # define __SYNCTEX_PARSER__ #ifdef __cplusplus extern "C" { #endif /* synctex_node_t is the type for all synctex nodes. * The synctex file is parsed into a tree of nodes, either sheet, boxes, math nodes... */ typedef struct _synctex_node * synctex_node_t; /* The main synctex object is a scanner * Its implementation is considered private. * The basic workflow is * - create a "synctex scanner" with the contents of a file * - perform actions on that scanner like display or edit queries * - free the scanner when the work is done */ typedef struct __synctex_scanner_t _synctex_scanner_t; typedef _synctex_scanner_t * synctex_scanner_t; /* This is the designated method to create a new synctex scanner object. * output is the pdf/dvi/xdv file associated to the synctex file. * If necessary, it can be the tex file that originated the synctex file * but this might cause problems if the \jobname has a custom value. * Despite this method can accept a relative path in practice, * you should only pass a full path name. * The path should be encoded by the underlying file system, * assuming that it is based on 8 bits characters, including UTF8, * not 16 bits nor 32 bits. * The last file extension is removed and replaced by the proper extension. * Then the private method _synctex_scanner_new_with_contents_of_file is called. * NULL is returned in case of an error or non existent file. * Once you have a scanner, use the synctex_display_query and synctex_edit_query below. * The new "build_directory" argument is available since version 1.5. * It is the directory where all the auxiliary stuff is created. * Sometimes, the synctex output file and the pdf, dvi or xdv files are not created in the same directory. * This is the case in MikTeX (I will include this into TeX Live). * This directory path can be nil, it will be ignored then. * It can be either absolute or relative to the directory of the output pdf (dvi or xdv) file. * If no synctex file is found in the same directory as the output file, then we try to find one in the build directory. * Please note that this new "build_directory" is provided as a convenient argument but should not be used. * In fact, this is implempented as a work around of a bug in MikTeX where the synctex file does not follow the pdf file. * The new "parse" argument is available since version 1.5. In general, use 1. * Use 0 only if you do not want to parse the content but just check the existence. */ synctex_scanner_t synctex_scanner_new_with_output_file(const char * output, const char * build_directory, int parse); /* This is the designated method to delete a synctex scanner object. * Frees all the memory, you must call it when you are finished with the scanner. */ void synctex_scanner_free(synctex_scanner_t scanner); /* Send this message to force the scanner to parse the contents of the synctex output file. * Nothing is performed if the file was already parsed. * In each query below, this message is sent, but if you need to access information more directly, * you must be sure that the parsing did occur. * Usage: * if((my_scanner = synctex_scanner_parse(my_scanner))) { * continue with my_scanner... * } else { * there was a problem * } */ synctex_scanner_t synctex_scanner_parse(synctex_scanner_t scanner); /* The main entry points. * Given the file name, a line and a column number, synctex_display_query returns the number of nodes * satisfying the contrain. Use code like * * if(synctex_display_query(scanner,name,line,column)>0) { * synctex_node_t node; * while((node = synctex_next_result(scanner))) { * // do something with node * ... * } * } * * For example, one can * - highlight each resulting node in the output, using synctex_node_h and synctex_node_v * - highlight all the rectangles enclosing those nodes, using synctex_box_... functions * - highlight just the character using that information * * Given the page and the position in the page, synctex_edit_query returns the number of nodes * satisfying the contrain. Use code like * * if(synctex_edit_query(scanner,page,h,v)>0) { * synctex_node_t node; * while(node = synctex_next_result(scanner)) { * // do something with node * ... * } * } * * For example, one can * - highlight each resulting line in the input, * - highlight just the character using that information * * page is 1 based * h and v are coordinates in 72 dpi unit, relative to the top left corner of the page. * If you make a new query, the result of the previous one is discarded. * If one of this function returns a non positive integer, * it means that an error occurred. * * Both methods are conservative, in the sense that matching is weak. * If the exact column number is not found, there will be an answer with the whole line. * * Sumatra-PDF, Skim, iTeXMac2 and Texworks are examples of open source software that use this library. * You can browse their code for a concrete implementation. */ int synctex_display_query(synctex_scanner_t scanner,const char * name,int line,int column); int synctex_edit_query(synctex_scanner_t scanner,int page,float h,float v); synctex_node_t synctex_next_result(synctex_scanner_t scanner); /* Display all the information contained in the scanner object. * If the records are too numerous, only the first ones are displayed. * This is mainly for informatinal purpose to help developers. */ void synctex_scanner_display(synctex_scanner_t scanner); /* The x and y offset of the origin in TeX coordinates. The magnification These are used by pdf viewers that want to display the real box size. For example, getting the horizontal coordinates of a node would require synctex_node_box_h(node)*synctex_scanner_magnification(scanner)+synctex_scanner_x_offset(scanner) Getting its TeX width would simply require synctex_node_box_width(node)*synctex_scanner_magnification(scanner) but direct methods are available for that below. */ int synctex_scanner_x_offset(synctex_scanner_t scanner); int synctex_scanner_y_offset(synctex_scanner_t scanner); float synctex_scanner_magnification(synctex_scanner_t scanner); /* Managing the input file names. * Given a tag, synctex_scanner_get_name will return the corresponding file name. * Conversely, given a file name, synctex_scanner_get_tag will retur, the corresponding tag. * The file name must be the very same as understood by TeX. * For example, if you \input myDir/foo.tex, the file name is myDir/foo.tex. * No automatic path expansion is performed. * Finally, synctex_scanner_input is the first input node of the scanner. * To browse all the input node, use a loop like * * if((input_node = synctex_scanner_input(scanner))){ * do { * blah * } while((input_node=synctex_node_sibling(input_node))); * } * * The output is the name that was used to create the scanner. * The synctex is the real name of the synctex file, * it was obtained from output by setting the proper file extension. */ const char * synctex_scanner_get_name(synctex_scanner_t scanner,int tag); int synctex_scanner_get_tag(synctex_scanner_t scanner,const char * name); synctex_node_t synctex_scanner_input(synctex_scanner_t scanner); const char * synctex_scanner_get_output(synctex_scanner_t scanner); const char * synctex_scanner_get_synctex(synctex_scanner_t scanner); /* Browsing the nodes * parent, child and sibling are standard names for tree nodes. * The parent is one level higher, the child is one level deeper, * and the sibling is at the same level. * The sheet of a node is the first ancestor, it is of type sheet. * A node and its sibling have the same parent. * A node is the parent of its child. * A node is either the child of its parent, * or belongs to the sibling chain of its parent's child. * The next node is either the child, the sibling or the parent's sibling, * unless the parent is a sheet. * This allows to navigate through all the nodes of a given sheet node: * * synctex_node_t node = sheet; * while((node = synctex_node_next(node))) { * // do something with node * } * * With synctex_sheet_content, you can retrieve the sheet node given the page. * The page is 1 based, according to TeX standards. * Conversely synctex_node_sheet allows to retrieve the sheet containing a given node. */ synctex_node_t synctex_node_parent(synctex_node_t node); synctex_node_t synctex_node_sheet(synctex_node_t node); synctex_node_t synctex_node_child(synctex_node_t node); synctex_node_t synctex_node_sibling(synctex_node_t node); synctex_node_t synctex_node_next(synctex_node_t node); synctex_node_t synctex_sheet_content(synctex_scanner_t scanner,int page); /* These are the types of the synctex nodes */ typedef enum { synctex_node_type_error = 0, synctex_node_type_input, synctex_node_type_sheet, synctex_node_type_vbox, synctex_node_type_void_vbox, synctex_node_type_hbox, synctex_node_type_void_hbox, synctex_node_type_kern, synctex_node_type_glue, synctex_node_type_math, synctex_node_type_boundary, synctex_node_number_of_types } synctex_node_type_t; /* synctex_node_type gives the type of a given node, * synctex_node_isa gives the same information as a human readable text. */ synctex_node_type_t synctex_node_type(synctex_node_t node); const char * synctex_node_isa(synctex_node_t node); /* This is primarily used for debugging purpose. * The second one logs information for the node and recursively displays information for its next node */ void synctex_node_log(synctex_node_t node); void synctex_node_display(synctex_node_t node); /* Given a node, access to its tag, line and column. * The line and column numbers are 1 based. * The latter is not yet fully supported in TeX, the default implementation returns 0 which means the whole line. * When the tag is known, the scanner of the node will give the corresponding file name. * When the tag is known, the scanner of the node will give the name. */ int synctex_node_tag(synctex_node_t node); int synctex_node_line(synctex_node_t node); int synctex_node_column(synctex_node_t node); /* This is the page where the node appears. * This is a 1 based index as given by TeX. */ int synctex_node_page(synctex_node_t node); /* For quite all nodes, horizontal, vertical coordinates, and width. * These are expressed in TeX small points coordinates, with origin at the top left corner. */ int synctex_node_h(synctex_node_t node); int synctex_node_v(synctex_node_t node); int synctex_node_width(synctex_node_t node); /* For all nodes, dimensions of the enclosing box. * These are expressed in TeX small points coordinates, with origin at the top left corner. * A box is enclosing itself. */ int synctex_node_box_h(synctex_node_t node); int synctex_node_box_v(synctex_node_t node); int synctex_node_box_width(synctex_node_t node); int synctex_node_box_height(synctex_node_t node); int synctex_node_box_depth(synctex_node_t node); /* For quite all nodes, horizontal, vertical coordinates, and width. * The visible dimensions are bigger than real ones to compensate 0 width boxes * that do contain nodes. * These are expressed in page coordinates, with origin at the top left corner. * A box is enclosing itself. */ float synctex_node_visible_h(synctex_node_t node); float synctex_node_visible_v(synctex_node_t node); float synctex_node_visible_width(synctex_node_t node); /* For all nodes, visible dimensions of the enclosing box. * A box is enclosing itself. * The visible dimensions are bigger than real ones to compensate 0 width boxes * that do contain nodes. */ float synctex_node_box_visible_h(synctex_node_t node); float synctex_node_box_visible_v(synctex_node_t node); float synctex_node_box_visible_width(synctex_node_t node); float synctex_node_box_visible_height(synctex_node_t node); float synctex_node_box_visible_depth(synctex_node_t node); /* The main synctex updater object. * This object is used to append information to the synctex file. * Its implementation is considered private. * It is used by the synctex command line tool to take into account modifications * that could occur while postprocessing files by dvipdf like filters. */ typedef struct __synctex_updater_t _synctex_updater_t; typedef _synctex_updater_t * synctex_updater_t; /* Designated initializer. * Once you are done with your whole job, * free the updater */ synctex_updater_t synctex_updater_new_with_output_file(const char * output, const char * directory); /* Use the next functions to append records to the synctex file, * no consistency tests made on the arguments */ void synctex_updater_append_magnification(synctex_updater_t updater, char * magnification); void synctex_updater_append_x_offset(synctex_updater_t updater, char * x_offset); void synctex_updater_append_y_offset(synctex_updater_t updater, char * y_offset); /* You MUST free the updater, once everything is properly appended */ void synctex_updater_free(synctex_updater_t updater); #ifdef __cplusplus } #endif #endif pdf-tools-0.80/server/synctex_parser_local.h000066400000000000000000000033031315513274600212600ustar00rootroot00000000000000/* Copyright (c) 2008, 2009, 2010 , 2011 jerome DOT laurens AT u-bourgogne DOT fr This file is part of the SyncTeX package. Latest Revision: Tue Jun 14 08:23:30 UTC 2011 Version: 1.16 See synctex_parser_readme.txt for more details License: -------- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE Except as contained in this notice, the name of the copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from the copyright holder. */ /* This local header file is for TEXLIVE, use your own header to fit your system */ # include /* for inline && HAVE_xxx */ /* No inlining for synctex tool in texlive. */ # define SYNCTEX_INLINE pdf-tools-0.80/server/synctex_parser_readme.txt000066400000000000000000000136151315513274600220220ustar00rootroot00000000000000This file is part of the SyncTeX package. The Synchronization TeXnology named SyncTeX is a new feature of recent TeX engines designed by Jerome Laurens. It allows to synchronize between input and output, which means to navigate from the source document to the typeset material and vice versa. More informations on http://itexmac2.sourceforge.net/SyncTeX.html This package is mainly for developers, it mainly contains the following files: synctex_parser_readme.txt synctex_parser_version.txt synctex_parser_utils.c synctex_parser_utils.h synctex_parser_local.h synctex_parser.h synctex_parser.c The file you are reading contains more informations about the SyncTeX parser history. In order to support SyncTeX in a viewer, it is sufficient to include in the source the files synctex_parser.h and synctex_parser.c. The synctex parser usage is described in synctex_parser.h header file. The other files are used by tex engines or by the synctex command line utility: ChangeLog README.txt am man1 man5 synctex-common.h synctex-convert.sh synctex-e-mem.ch0 synctex-e-mem.ch1 synctex-e-rec.ch0 synctex-e-rec.ch1 synctex-etex.h synctex-mem.ch0 synctex-mem.ch1 synctex-mem.ch2 synctex-pdf-rec.ch2 synctex-pdftex.h synctex-rec.ch0 synctex-rec.ch1 synctex-rec.ch2 synctex-tex.h synctex-xe-mem.ch2 synctex-xe-rec.ch2 synctex-xe-rec.ch3 synctex-xetex.h synctex.c synctex.defines synctex.h synctex_main.c tests Version: -------- This is version 1, which refers to the synctex output file format. The files are identified by a build number. In order to help developers to automatically manage the version and build numbers and download the parser only when necessary, the synctex_parser.version is an ASCII text file just containing the current version and build numbers. History: -------- 1.1: Thu Jul 17 09:28:13 UTC 2008 - First official version available in TeXLive 2008 DVD. Unfortunately, the backwards synchronization is not working properly mainly for ConTeXt users, see below. 1.2: Tue Sep 2 10:28:32 UTC 2008 - Correction for ConTeXt support in the edit query. The previous method was assuming that TeX boxes do not overlap, which is reasonable for LaTeX but not for ConTeXt. This assumption is no longer considered. 1.3: Fri Sep 5 09:39:57 UTC 2008 - Local variable "read" renamed to "already_read" to avoid conflicts. - "inline" compiler directive renamed to "SYNCTEX_INLINE" for code support and maintenance - _synctex_error cannot be inlined due to variable arguments (thanks Christiaan Hofman) - Correction in the display query, extra boundary nodes are used for a more precise forwards synchronization 1.4: Fri Sep 12 08:12:34 UTC 2008 - For an unknown reason, the previous version was not the real 1.3 (as used in iTeXMac2 build 747). As a consequence, a crash was observed. - Some typos are fixed. 1.6: Mon Nov 3 20:20:02 UTC 2008 - The bug that prevented synchronization with compressed files on windows has been fixed. - New interface to allow system specific customization. - Note that some APIs have changed. 1.8: Mer 8 jul 2009 11:32:38 UTC Note that version 1.7 was delivered privately. - bug fix: synctex was causing a memory leak in pdftex and xetex, thus some processing speed degradation - bug fix: the synctex command line tool was broken when updating a .synctex file - enhancement: better accuracy of the synchronization process - enhancement: the pdf output file and the associated .synctex file no longer need to live in the same directory. The new -d option of the synctex command line tool manages this situation. This is handy when using something like tex -output-directory=DIR ... 1.9: Wed Nov 4 11:52:35 UTC 2009 - Various typo fixed - OutputDebugString replaced by OutputDebugStringA to deliberately disable unicode preprocessing - New conditional created because OutputDebugStringA is only available since Windows 2K professional 1.10: Sun Jan 10 10:12:32 UTC 2010 - Bug fix in synctex_parser.c to solve a synchronization problem with amsmath's gather environment. Concerns the synctex tool. 1.11: Sun Jan 17 09:12:31 UTC 2010 - Bug fix in synctex_parser.c, function synctex_node_box_visible_v: 'x' replaced by 'y'. Only 3rd party tools are concerned. 1.12: Mon Jul 19 21:52:10 UTC 2010 - Bug fix in synctex_parser.c, function __synctex_open: the io_mode was modified even in case of a non zero return, causing a void .synctex.gz file to be created even if it was not expected. Reported by Marek Kasik concerning a bug on evince. 1.13: Fri Mar 11 07:39:12 UTC 2011 - Bug fix in synctex_parser.c, better synchronization as suggested by Jan Sundermeyer (near line 3388). - Stronger code design in synctex_parser_utils.c, function _synctex_get_name (really neutral behavior). Only 3rd party tools are concerned. 1.14: Fri Apr 15 19:10:57 UTC 2011 - taking output_directory into account - Replaced FOPEN_WBIN_MODE by FOPEN_W_MODE when opening the text version of the .synctex file. - Merging with LuaTeX's version of synctex.c 1.15: Fri Jun 10 14:10:17 UTC 2011 This concerns the synctex command line tool and 3rd party developers. TeX and friends are not concerned by these changes. - Bug fixed in _synctex_get_io_mode_name, sometimes the wrong mode was returned - Support for LuaTeX convention of './' file prefixing 1.16: Tue Jun 14 08:23:30 UTC 2011 This concerns the synctex command line tool and 3rd party developers. TeX and friends are not concerned by these changes. - Better forward search (thanks Jose Alliste) - Support for LuaTeX convention of './' file prefixing now for everyone, not only for Windows Acknowledgments: ---------------- The author received useful remarks from the pdfTeX developers, especially Hahn The Thanh, and significant help from XeTeX developer Jonathan Kew Nota Bene: ---------- If you include or use a significant part of the synctex package into a software, I would appreciate to be listed as contributor and see "SyncTeX" highlighted. Copyright (c) 2008-2011 jerome DOT laurens AT u-bourgogne DOT fr pdf-tools-0.80/server/synctex_parser_utils.c000066400000000000000000000400051315513274600213210ustar00rootroot00000000000000/* Copyright (c) 2008, 2009, 2010 , 2011 jerome DOT laurens AT u-bourgogne DOT fr This file is part of the SyncTeX package. Latest Revision: Tue Jun 14 08:23:30 UTC 2011 Version: 1.16 See synctex_parser_readme.txt for more details License: -------- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE Except as contained in this notice, the name of the copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from the copyright holder. */ /* In this file, we find all the functions that may depend on the operating system. */ #include #include #include #include #include #include #include #include #include #if defined(_WIN32) || defined(__WIN32__) || defined(__TOS_WIN__) || defined(__WINDOWS__) #define SYNCTEX_WINDOWS 1 #endif #ifdef _WIN32_WINNT_WINXP #define SYNCTEX_RECENT_WINDOWS 1 #endif #ifdef SYNCTEX_WINDOWS #include #endif void *_synctex_malloc(size_t size) { void * ptr = malloc(size); if(ptr) { /* There used to be a switch to use bzero because it is more secure. JL */ memset(ptr,0, size); } return (void *)ptr; } int _synctex_error(const char * reason,...) { va_list arg; int result; va_start (arg, reason); # ifdef SYNCTEX_RECENT_WINDOWS {/* This code is contributed by William Blum. As it does not work on some older computers, the _WIN32 conditional here is replaced with a SYNCTEX_RECENT_WINDOWS one. According to http://msdn.microsoft.com/en-us/library/aa363362(VS.85).aspx Minimum supported client Windows 2000 Professional Minimum supported server Windows 2000 Server People running Windows 2K standard edition will not have OutputDebugStringA. JL.*/ char *buff; size_t len; OutputDebugStringA("SyncTeX ERROR: "); len = _vscprintf(reason, arg) + 1; buff = (char*)malloc( len * sizeof(char) ); result = vsprintf(buff, reason, arg) +strlen("SyncTeX ERROR: "); OutputDebugStringA(buff); OutputDebugStringA("\n"); free(buff); } # else result = fprintf(stderr,"SyncTeX ERROR: "); result += vfprintf(stderr, reason, arg); result += fprintf(stderr,"\n"); # endif va_end (arg); return result; } /* strip the last extension of the given string, this string is modified! */ void _synctex_strip_last_path_extension(char * string) { if(NULL != string){ char * last_component = NULL; char * last_extension = NULL; char * next = NULL; /* first we find the last path component */ if(NULL == (last_component = strstr(string,"/"))){ last_component = string; } else { ++last_component; while((next = strstr(last_component,"/"))){ last_component = next+1; } } # ifdef SYNCTEX_WINDOWS /* On Windows, the '\' is also a path separator. */ while((next = strstr(last_component,"\\"))){ last_component = next+1; } # endif /* then we find the last path extension */ if((last_extension = strstr(last_component,"."))){ ++last_extension; while((next = strstr(last_extension,"."))){ last_extension = next+1; } --last_extension;/* back to the "." */ if(last_extension>last_component){/* filter out paths like ....my/dir/.hidden"*/ last_extension[0] = '\0'; } } } } const char * synctex_ignore_leading_dot_slash(const char * name) { while(SYNCTEX_IS_DOT(*name) && SYNCTEX_IS_PATH_SEPARATOR(name[1])) { name += 2; while (SYNCTEX_IS_PATH_SEPARATOR(*name)) { ++name; } } return name; } /* Compare two file names, windows is sometimes case insensitive... */ synctex_bool_t _synctex_is_equivalent_file_name(const char *lhs, const char *rhs) { /* Remove the leading regex '(\./+)*' in both rhs and lhs */ lhs = synctex_ignore_leading_dot_slash(lhs); rhs = synctex_ignore_leading_dot_slash(rhs); # if SYNCTEX_WINDOWS /* On Windows, filename should be compared case insensitive. * The characters '/' and '\' are both valid path separators. * There will be a very serious problem concerning UTF8 because * not all the characters must be toupper... * I would like to have URL's instead of filenames. */ next_character: if(SYNCTEX_IS_PATH_SEPARATOR(*lhs)) {/* lhs points to a path separator */ if(!SYNCTEX_IS_PATH_SEPARATOR(*rhs)) {/* but not rhs */ return synctex_NO; } } else if(SYNCTEX_IS_PATH_SEPARATOR(*rhs)) {/* rhs points to a path separator but not lhs */ return synctex_NO; } else if(toupper(*lhs) != toupper(*rhs)){/* uppercase do not match */ return synctex_NO; } else if (!*lhs) {/* lhs is at the end of the string */ return *rhs ? synctex_NO : synctex_YES; } else if(!*rhs) {/* rhs is at the end of the string but not lhs */ return synctex_NO; } ++lhs; ++rhs; goto next_character; # else return 0 == strcmp(lhs,rhs)?synctex_YES:synctex_NO; # endif } synctex_bool_t _synctex_path_is_absolute(const char * name) { if(!strlen(name)) { return synctex_NO; } # if SYNCTEX_WINDOWS if(strlen(name)>2) { return (name[1]==':' && SYNCTEX_IS_PATH_SEPARATOR(name[2]))?synctex_YES:synctex_NO; } return synctex_NO; # else return SYNCTEX_IS_PATH_SEPARATOR(name[0])?synctex_YES:synctex_NO; # endif } /* We do not take care of UTF-8 */ const char * _synctex_last_path_component(const char * name) { const char * c = name+strlen(name); if(c>name) { if(!SYNCTEX_IS_PATH_SEPARATOR(*c)) { do { --c; if(SYNCTEX_IS_PATH_SEPARATOR(*c)) { return c+1; } } while(c>name); } return c;/* the last path component is the void string*/ } return c; } int _synctex_copy_with_quoting_last_path_component(const char * src, char ** dest_ref, size_t size) { const char * lpc; if(src && dest_ref) { # define dest (*dest_ref) dest = NULL; /* Default behavior: no change and sucess. */ lpc = _synctex_last_path_component(src); if(strlen(lpc)) { if(strchr(lpc,' ') && lpc[0]!='"' && lpc[strlen(lpc)-1]!='"') { /* We are in the situation where adding the quotes is allowed. */ /* Time to add the quotes. */ /* Consistency test: we must have dest+size>dest+strlen(dest)+2 * or equivalently: strlen(dest)+20) { char * result = NULL; ++size; /* Create the memory storage */ if(NULL!=(result = (char *)malloc(size))) { char * dest = result; va_start (arg, first); temp = first; do { if((size = strlen(temp))>0) { /* There is something to merge */ if(dest != strncpy(dest,temp,size)) { _synctex_error("! _synctex_merge_strings: Copy problem"); free(result); result = NULL; return NULL; } dest += size; } } while( (temp = va_arg(arg, const char *)) != NULL); va_end(arg); dest[0]='\0';/* Terminate the merged string */ return result; } _synctex_error("! _synctex_merge_strings: Memory problem"); return NULL; } return NULL; } /* The purpose of _synctex_get_name is to find the name of the synctex file. * There is a list of possible filenames from which we return the most recent one and try to remove all the others. * With two runs of pdftex or xetex we are sure the the synctex file is really the most appropriate. */ int _synctex_get_name(const char * output, const char * build_directory, char ** synctex_name_ref, synctex_io_mode_t * io_mode_ref) { if(output && synctex_name_ref && io_mode_ref) { /* If output is already absolute, we just have to manage the quotes and the compress mode */ size_t size = 0; char * synctex_name = NULL; synctex_io_mode_t io_mode = *io_mode_ref; const char * base_name = _synctex_last_path_component(output); /* do not free, output is the owner. base name of output*/ /* Do we have a real base name ? */ if(strlen(base_name)>0) { /* Yes, we do. */ const char * temp = NULL; char * core_name = NULL; /* base name of output without path extension. */ char * dir_name = NULL; /* dir name of output */ char * quoted_core_name = NULL; char * basic_name = NULL; char * gz_name = NULL; char * quoted_name = NULL; char * quoted_gz_name = NULL; char * build_name = NULL; char * build_gz_name = NULL; char * build_quoted_name = NULL; char * build_quoted_gz_name = NULL; struct stat buf; time_t the_time = 0; /* Create core_name: let temp point to the dot before the path extension of base_name; * We start form the \0 terminating character and scan the string upward until we find a dot. * The leading dot is not accepted. */ if((temp = strrchr(base_name,'.')) && (size = temp - base_name)>0) { /* There is a dot and it is not at the leading position */ if(NULL == (core_name = (char *)malloc(size+1))) { _synctex_error("! _synctex_get_name: Memory problem 1"); return -1; } if(core_name != strncpy(core_name,base_name,size)) { _synctex_error("! _synctex_get_name: Copy problem 1"); free(core_name); dir_name = NULL; return -2; } core_name[size] = '\0'; } else { /* There is no path extension, * Just make a copy of base_name */ core_name = _synctex_merge_strings(base_name); } /* core_name is properly set up, owned by "self". */ /* creating dir_name. */ size = strlen(output)-strlen(base_name); if(size>0) { /* output contains more than one path component */ if(NULL == (dir_name = (char *)malloc(size+1))) { _synctex_error("! _synctex_get_name: Memory problem"); free(core_name); dir_name = NULL; return -1; } if(dir_name != strncpy(dir_name,output,size)) { _synctex_error("! _synctex_get_name: Copy problem"); free(dir_name); dir_name = NULL; free(core_name); dir_name = NULL; return -2; } dir_name[size] = '\0'; } /* dir_name is properly set up. It ends with a path separator, if non void. */ /* creating quoted_core_name. */ if(strchr(core_name,' ')) { quoted_core_name = _synctex_merge_strings("\"",core_name,"\""); } /* quoted_core_name is properly set up. */ if(dir_name &&strlen(dir_name)>0) { basic_name = _synctex_merge_strings(dir_name,core_name,synctex_suffix,NULL); if(quoted_core_name && strlen(quoted_core_name)>0) { quoted_name = _synctex_merge_strings(dir_name,quoted_core_name,synctex_suffix,NULL); } } else { basic_name = _synctex_merge_strings(core_name,synctex_suffix,NULL); if(quoted_core_name && strlen(quoted_core_name)>0) { quoted_name = _synctex_merge_strings(quoted_core_name,synctex_suffix,NULL); } } if(!_synctex_path_is_absolute(output) && build_directory && (size = strlen(build_directory))) { temp = build_directory + size - 1; if(_synctex_path_is_absolute(temp)) { build_name = _synctex_merge_strings(build_directory,basic_name,NULL); if(quoted_core_name && strlen(quoted_core_name)>0) { build_quoted_name = _synctex_merge_strings(build_directory,quoted_name,NULL); } } else { build_name = _synctex_merge_strings(build_directory,"/",basic_name,NULL); if(quoted_core_name && strlen(quoted_core_name)>0) { build_quoted_name = _synctex_merge_strings(build_directory,"/",quoted_name,NULL); } } } if(basic_name) { gz_name = _synctex_merge_strings(basic_name,synctex_suffix_gz,NULL); } if(quoted_name) { quoted_gz_name = _synctex_merge_strings(quoted_name,synctex_suffix_gz,NULL); } if(build_name) { build_gz_name = _synctex_merge_strings(build_name,synctex_suffix_gz,NULL); } if(build_quoted_name) { build_quoted_gz_name = _synctex_merge_strings(build_quoted_name,synctex_suffix_gz,NULL); } /* All the others names are properly set up... */ /* retain the most recently modified file */ # define TEST(FILENAME,COMPRESS_MODE) \ if(FILENAME) {\ if (stat(FILENAME, &buf)) { \ free(FILENAME);\ FILENAME = NULL;\ } else if (buf.st_mtime>the_time) { \ the_time=buf.st_mtime; \ synctex_name = FILENAME; \ if (COMPRESS_MODE) { \ io_mode |= synctex_io_gz_mask; \ } else { \ io_mode &= ~synctex_io_gz_mask; \ } \ } \ } TEST(basic_name,synctex_DONT_COMPRESS); TEST(gz_name,synctex_COMPRESS); TEST(quoted_name,synctex_DONT_COMPRESS); TEST(quoted_gz_name,synctex_COMPRESS); TEST(build_name,synctex_DONT_COMPRESS); TEST(build_gz_name,synctex_COMPRESS); TEST(build_quoted_name,synctex_DONT_COMPRESS); TEST(build_quoted_gz_name,synctex_COMPRESS); # undef TEST /* Free all the intermediate filenames, except the one that will be used as returned value. */ # define CLEAN_AND_REMOVE(FILENAME) \ if(FILENAME && (FILENAME!=synctex_name)) {\ remove(FILENAME);\ printf("synctex tool info: %s removed\n",FILENAME);\ free(FILENAME);\ FILENAME = NULL;\ } CLEAN_AND_REMOVE(basic_name); CLEAN_AND_REMOVE(gz_name); CLEAN_AND_REMOVE(quoted_name); CLEAN_AND_REMOVE(quoted_gz_name); CLEAN_AND_REMOVE(build_name); CLEAN_AND_REMOVE(build_gz_name); CLEAN_AND_REMOVE(build_quoted_name); CLEAN_AND_REMOVE(build_quoted_gz_name); # undef CLEAN_AND_REMOVE /* set up the returned values */ * synctex_name_ref = synctex_name; * io_mode_ref = io_mode; return 0; } return -1;/* bad argument */ } return -2; } const char * _synctex_get_io_mode_name(synctex_io_mode_t io_mode) { static const char * synctex_io_modes[4] = {"r","rb","a","ab"}; unsigned index = ((io_mode & synctex_io_gz_mask)?1:0) + ((io_mode & synctex_io_append_mask)?2:0);// bug pointed out by Jose Alliste return synctex_io_modes[index]; } pdf-tools-0.80/server/synctex_parser_utils.h000066400000000000000000000122601315513274600213300ustar00rootroot00000000000000/* Copyright (c) 2008, 2009, 2010, 2011 jerome DOT laurens AT u-bourgogne DOT fr This file is part of the SyncTeX package. Latest Revision: Tue Jun 14 08:23:30 UTC 2011 Version: 1.16 See synctex_parser_readme.txt for more details License: -------- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE Except as contained in this notice, the name of the copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from the copyright holder. */ /* The utilities declared here are subject to conditional implementation. * All the operating system special stuff goes here. * The problem mainly comes from file name management: path separator, encoding... */ # define synctex_bool_t int # define synctex_YES -1 # define synctex_ADD_QUOTES -1 # define synctex_COMPRESS -1 # define synctex_NO 0 # define synctex_DONT_ADD_QUOTES 0 # define synctex_DONT_COMPRESS 0 #ifndef __SYNCTEX_PARSER_UTILS__ # define __SYNCTEX_PARSER_UTILS__ #include #ifdef __cplusplus extern "C" { #endif # if _WIN32 # define SYNCTEX_IS_PATH_SEPARATOR(c) ('/' == c || '\\' == c) # else # define SYNCTEX_IS_PATH_SEPARATOR(c) ('/' == c) # endif # if _WIN32 # define SYNCTEX_IS_DOT(c) ('.' == c) # else # define SYNCTEX_IS_DOT(c) ('.' == c) # endif /* This custom malloc functions initializes to 0 the newly allocated memory. * There is no bzero function on windows. */ void *_synctex_malloc(size_t size); /* This is used to log some informational message to the standard error stream. * On Windows, the stderr stream is not exposed and another method is used. * The return value is the number of characters printed. */ int _synctex_error(const char * reason,...); /* strip the last extension of the given string, this string is modified! * This function depends on the OS because the path separator may differ. * This should be discussed more precisely. */ void _synctex_strip_last_path_extension(char * string); /* Compare two file names, windows is sometimes case insensitive... * The given strings may differ stricto sensu, but represent the same file name. * It might not be the real way of doing things. * The return value is an undefined non 0 value when the two file names are equivalent. * It is 0 otherwise. */ synctex_bool_t _synctex_is_equivalent_file_name(const char *lhs, const char *rhs); /* Description forthcoming.*/ synctex_bool_t _synctex_path_is_absolute(const char * name); /* Description forthcoming...*/ const char * _synctex_last_path_component(const char * name); /* If the core of the last path component of src is not already enclosed with double quotes ('"') * and contains a space character (' '), then a new buffer is created, the src is copied and quotes are added. * In all other cases, no destination buffer is created and the src is not copied. * 0 on success, which means no error, something non 0 means error, mainly due to memory allocation failure, or bad parameter. * This is used to fix a bug in the first version of pdftex with synctex (1.40.9) for which names with spaces * were not managed in a standard way. * On success, the caller owns the buffer pointed to by dest_ref (is any) and * is responsible of freeing the memory when done. * The size argument is the size of the src buffer. On return the dest_ref points to a buffer sized size+2.*/ int _synctex_copy_with_quoting_last_path_component(const char * src, char ** dest_ref, size_t size); /* These are the possible extensions of the synctex file */ extern const char * synctex_suffix; extern const char * synctex_suffix_gz; typedef unsigned int synctex_io_mode_t; typedef enum { synctex_io_append_mask = 1, synctex_io_gz_mask = synctex_io_append_mask<<1 } synctex_io_mode_masks_t; typedef enum { synctex_compress_mode_none = 0, synctex_compress_mode_gz = 1 } synctex_compress_mode_t; int _synctex_get_name(const char * output, const char * build_directory, char ** synctex_name_ref, synctex_io_mode_t * io_mode_ref); /* returns the correct mode required by fopen and gzopen from the given io_mode */ const char * _synctex_get_io_mode_name(synctex_io_mode_t io_mode); const char * synctex_ignore_leading_dot_slash(const char * name); #ifdef __cplusplus } #endif #endif pdf-tools-0.80/server/synctex_parser_version.txt000066400000000000000000000000041315513274600222360ustar00rootroot000000000000001.16pdf-tools-0.80/test/000077500000000000000000000000001315513274600143365ustar00rootroot00000000000000pdf-tools-0.80/test/encrypted.pdf000066400000000000000000003333641315513274600170420ustar00rootroot00000000000000%PDF-1.5 % 1 0 obj << /D (C`7:) /S /GoTo >> endobj 2 0 obj << /D [3 0 R /XYZ 133.768 667.198 null] >> endobj 4 0 obj << /Title 5 0 R /Parent 6 0 R /A 1 0 R /Next 7 0 R >> endobj 5 0 obj (_) endobj 8 0 obj << /D (jzeE~) /S /GoTo >> endobj 9 0 obj << /D [10 0 R /XYZ 133.768 667.198 null] >> endobj 7 0 obj << /Title 11 0 R /Parent 6 0 R /A 8 0 R /Next 12 0 R /Prev 4 0 R >> endobj 11 0 obj (wјI]) endobj 13 0 obj << /D (猞˭i) /S /GoTo >> endobj 14 0 obj << /D [15 0 R /XYZ 133.768 667.198 null] >> endobj 12 0 obj << /Title 16 0 R /Parent 6 0 R /A 13 0 R /Next 17 0 R /Prev 7 0 R >> endobj 16 0 obj (CdJ) endobj 18 0 obj << /D (M6TC) /S /GoTo >> endobj 19 0 obj << /D [20 0 R /XYZ 133.768 667.198 null] >> endobj 17 0 obj << /Title 21 0 R /Parent 6 0 R /A 18 0 R /Next 22 0 R /Prev 12 0 R >> endobj 21 0 obj (p$Lv%J) endobj 23 0 obj << /D (u{]'&) /S /GoTo >> endobj 24 0 obj << /D [25 0 R /XYZ 133.768 667.198 null] >> endobj 22 0 obj << /First 26 0 R /Title 27 0 R /Parent 6 0 R /A 23 0 R /Count -1 /Last 26 0 R /Prev 17 0 R >> endobj 27 0 obj (# ) endobj 28 0 obj << /D (mq|aD) /S /GoTo >> endobj 29 0 obj << /D [25 0 R /XYZ 133.768 647.37 null] >> endobj 26 0 obj << /First 30 0 R /Title 31 0 R /Parent 22 0 R /A 28 0 R /Count -1 /Last 30 0 R >> endobj 31 0 obj (l]Os7:) endobj 32 0 obj << /D (=Wiس{N|) /S /GoTo >> endobj 33 0 obj << /D [25 0 R /XYZ 133.768 626.988 null] >> endobj 30 0 obj << /Title 34 0 R /Parent 26 0 R /A 32 0 R >> endobj 34 0 obj (M^WsA*) endobj 35 0 obj << /D [3 0 R /Fit] /S /GoTo >> endobj 3 0 obj << /Parent 36 0 R /MediaBox [0 0 612 792] /Resources 37 0 R /Contents 38 0 R /Type /Page >> endobj 37 0 obj << /Font << /F19 39 0 R /F16 40 0 R >> /ProcSet [/PDF /Text] >> endobj 38 0 obj << /Filter /FlateDecode /Length 486 >> stream _CDT+b]ԋ 326&$Jm匆vk5N MF2gvpVs BxʦvYFިNr+`oMnD%,=4aXۡ]Ʈ#B8o*-\'/`rcu~/=T/OڶROz.vg< Ks&~|}Q*ZclL]!7-5" { CBBtҊp71bW@,@>WA1Exx9K;~$<.\父?!%cLb|%8`L!$P]S۸Ѣ1Nr+5CKSAlFߋƃ1h|K~vԉU e-G_=\aD'VLD=m19uq&`ig-Nsɇ endstream endobj 41 0 obj << /D [3 0 R /XYZ 132.768 705.06 null] >> endobj 42 0 obj << /D [3 0 R /XYZ 133.768 667.198 null] >> endobj 39 0 obj << /LastChar 120 /BaseFont /PXJMFE+LMRoman12-Bold /Subtype /Type1 /Widths 43 0 R /FontDescriptor 44 0 R /Encoding 45 0 R /Type /Font /FirstChar 46 >> endobj 40 0 obj << /LastChar 121 /BaseFont /UBBXIQ+LMRoman10-Regular /Subtype /Type1 /Widths 46 0 R /FontDescriptor 47 0 R /Encoding 45 0 R /Type /Font /FirstChar 27 >> endobj 36 0 obj << /Kids [3 0 R 10 0 R 15 0 R 20 0 R 25 0 R 48 0 R] /Count 6 /Type /Pages >> endobj 49 0 obj << /CA 1.0 /M (-5>m,\n- ʅ) /Name /Insert /Contents (yR) /F 4 /Open false /C [1 0 0] /Subtype /Text /Type /Annot /Rect [158.675 635.415 158.675 647.37] /CreationDate (-5>m,\n- ʅ) /T (Stj) /Subj () >> endobj 50 0 obj << /CA 1.0 /M (֐YR"_披?֯) /QuadPoints [158.67459 612.38814 197.22485 612.38814 158.67459 603.53262 197.22485 603.53262] /Contents () /F 4 /Open false /C [1 0 0] /Subtype /StrikeOut /Type /Annot /Rect [158.675 603.534 158.675 603.534] /CreationDate (֐YR"_披?֯) /T (-B؝܀) /Subj () >> endobj 51 0 obj << /CA 1.0 /M (Z&dtZr03 _CtOg) /QuadPoints [158.67459 591.46664 197.41798 591.46664 158.67459 580.67398 197.41798 580.67398] /Contents () /F 4 /Open false /C [1 0 0] /Subtype /Highlight /Type /Annot /Rect [158.675 583.609 158.675 583.609] /CreationDate (Z&dtZr03 _CtOg) /T (NC>+0) /Subj () >> endobj 52 0 obj << /CA 1.0 /M (\tx85KC\n\\88) /QuadPoints [158.67459 571.54141 199.10605 571.54141 158.67459 560.74875 199.10605 560.74875] /Contents () /F 4 /Open false /C [1 0 0] /Subtype /Underline /Type /Annot /Rect [158.675 563.684 158.675 563.684] /CreationDate (\tx85KC\n\\88) /T (w\f\(j) /Subj () >> endobj 53 0 obj << /CA 1.0 /M (VXI摀 Q) /QuadPoints [158.67459 551.61618 194.15294 551.61618 158.67459 540.82352 194.15294 540.82352] /Contents () /F 4 /Open false /C [1 0 0] /Subtype /Squiggly /Type /Annot /Rect [158.675 543.758 158.675 543.758] /CreationDate (VXI摀 Q) /T (\(Hs) /Subj () >> endobj 54 0 obj << /Font << /F19 39 0 R /F16 40 0 R >> /ProcSet [/PDF /Text] >> endobj 10 0 obj << /Parent 36 0 R /Annots [49 0 R 50 0 R 51 0 R 52 0 R 53 0 R] /MediaBox [0 0 612 792] /Resources 54 0 R /Contents 55 0 R /Type /Page >> endobj 55 0 obj << /Filter /FlateDecode /Length 196 >> stream *-WC7#B_Հr&otdD%F7 ƒ!r*%r)>K`B`ZRixǬr PvhU-5}A}2;8/:na1ǿjX_+&Ec u뢒,g%j嚱:DHwP1PK5yk&IFh2R0 endstream endobj 56 0 obj << /D [10 0 R /XYZ 132.768 705.06 null] >> endobj 57 0 obj << /D [10 0 R /XYZ 133.768 651.355 null] >> endobj 58 0 obj << /D [10 0 R /XYZ 158.675 647.37 null] >> endobj 59 0 obj << /D [10 0 R /XYZ 133.768 619.474 null] >> endobj 60 0 obj << /D [10 0 R /XYZ 197.225 615.489 null] >> endobj 61 0 obj << /D [10 0 R /XYZ 133.768 599.549 null] >> endobj 62 0 obj << /D [10 0 R /XYZ 197.418 595.564 null] >> endobj 63 0 obj << /D [10 0 R /XYZ 133.768 577.687 null] >> endobj 64 0 obj << /D [10 0 R /XYZ 199.107 575.639 null] >> endobj 65 0 obj << /D [10 0 R /XYZ 133.768 559.698 null] >> endobj 66 0 obj << /D [10 0 R /XYZ 194.153 555.713 null] >> endobj 67 0 obj << /Border [0 0 1] /Subtype /Link /C [1 0 0] /A << /D (RᫎK) /S /GoTo >> /Type /Annot /Rect [217.759 634.418 224.733 643.274] /H /I >> endobj 68 0 obj << /Border [0 0 1] /Subtype /Link /C [0 1 1] /A << /URI (͎NfV 4=) /Type /Action /S /URI >> /Type /Annot /Rect [220.942 612.279 317.081 623.404] /H /I >> endobj 69 0 obj << /Font << /F20 70 0 R /F19 39 0 R /F16 40 0 R >> /ProcSet [/PDF /Text] >> endobj 15 0 obj << /Parent 36 0 R /Annots [67 0 R 68 0 R] /MediaBox [0 0 612 792] /Resources 69 0 R /Contents 71 0 R /Type /Page >> endobj 71 0 obj << /Filter /FlateDecode /Length 199 >> stream mP *ԫmarGT>2m !,!t3إxzf6U)Zp.Ϲ endstream endobj 72 0 obj << /D [15 0 R /XYZ 132.768 705.06 null] >> endobj 73 0 obj << /D [15 0 R /XYZ 133.768 651.355 null] >> endobj 74 0 obj << /D [15 0 R /XYZ 133.768 631.429 null] >> endobj 70 0 obj << /LastChar 119 /BaseFont /RPVHCD+LMMono10-Regular /Subtype /Type1 /Widths 75 0 R /FontDescriptor 76 0 R /Encoding 45 0 R /Type /Font /FirstChar 46 >> endobj 77 0 obj << /Filter /FlateDecode /Length 582 /Params << >> /Type /EmbeddedFile >> stream g$%XDsf Pgnkɬ+qHھ;,8>ICM~& EIrȻFp)> stream s7Ka% W8B:ٞe6NBZm.IRkf&e-?}`\ zF"^dp\>9FclW&\hꖂɈ1= endstream endobj 79 0 obj << /ProcSet [/PDF] >> endobj 80 0 obj << /AP << /R 78 0 R /D 78 0 R /N 78 0 R >> /FS << /EF << /F 77 0 R >> /F () /Type /Filespec >> /M (ӆ{Wl5) /Name /PushPin /F 4 /C [1 0.9255 0.7765] /Subtype /FileAttachment /Type /Annot /Rect [133.768 632.373 157.768 646.373] >> endobj 81 0 obj << /Font << /F19 39 0 R >> /ProcSet [/PDF /Text] >> endobj 20 0 obj << /Parent 36 0 R /Annots [80 0 R] /MediaBox [0 0 612 792] /Resources 81 0 R /Contents 82 0 R /Type /Page >> endobj 82 0 obj << /Filter /FlateDecode /Length 105 >> stream  9t'4°*Wc;$l3ErQ:K)//&놈p;;! endstream endobj 83 0 obj << /D [20 0 R /XYZ 132.768 705.06 null] >> endobj 84 0 obj << /Font << /F19 39 0 R /F39 85 0 R >> /ProcSet [/PDF /Text] >> endobj 25 0 obj << /Parent 36 0 R /MediaBox [0 0 612 792] /Resources 84 0 R /Contents 86 0 R /Type /Page >> endobj 86 0 obj << /Filter /FlateDecode /Length 146 >> stream qN%{C;6y˴'33ݮZOޚy_n`G b0nbQ^yI.BO~I` ᥟi]Vyl%|n%=9xmasj~h]])Q>O endstream endobj 87 0 obj << /D [25 0 R /XYZ 132.768 705.06 null] >> endobj 85 0 obj << /LastChar 117 /BaseFont /SKYQML+LMRoman10-Bold /Subtype /Type1 /Widths 88 0 R /FontDescriptor 89 0 R /Encoding 45 0 R /Type /Font /FirstChar 46 >> endobj 90 0 obj << /ProcSet [/PDF] >> endobj 48 0 obj << /Parent 36 0 R /MediaBox [0 0 612 792] /Resources 90 0 R /Contents 91 0 R /Type /Page >> endobj 91 0 obj << /Filter /FlateDecode /Length 24 >> stream Oj% endstream endobj 92 0 obj << /D [48 0 R /XYZ 132.768 705.06 null] >> endobj 45 0 obj << /Differences [27 /ff /fi 44 /comma /hyphen /period /slash 49 /one /two /three /four /five 58 /colon 65 /A 68 /D /E /F 73 /I 76 /L 79 /O /P 84 /T 86 /V 97 /a /b /c /d /e /f /g /h /i 107 /k /l /m /n /o /p /q /r /s /t /u 119 /w /x /y] /Type /Encoding >> endobj 88 0 obj [319.5 575 575 575 575 575 575 575 575 575 575 575 319.5 319.5 894.4 894.4 894.4 543.1 894.4 869.4 818.1 830.6 881.9 755.6 723.6 904.2 900 436.1 594.5 901.4 691.7 1091.7 900 863.9 786.1 863.9 862.5 638.9 800 884.7 869.4 1188.9 869.4 869.4 702.8 319.5 575 319.5 555.6 869.4 319.5 559 638.9 511.1 638.9 527.1 351.4 575 638.9 319.5 351.4 607 319.5 958.3 638.9 575 638.9 607 473.6 453.6 447.2 638.9] endobj 75 0 obj [525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525 525] endobj 46 0 obj [583.3 555.6 555.6 833.3 833.3 500 277.8 373.8 833.3 500 833.3 777.8 277.8 388.9 388.9 500 777.8 277.8 333.3 277.8 500 500 500 500 500 500 500 500 500 500 500 277.8 277.8 777.8 777.8 777.8 472.2 777.8 750 708.3 722.2 763.9 680.6 652.8 784.7 750 361.1 513.9 777.8 625 916.7 750 777.8 680.6 777.8 736.1 555.6 722.2 750 750 1027.8 750 750 611.1 277.8 500 277.8 555.6 750 277.8 500 555.6 444.5 555.6 444.5 305.6 500 555.6 277.8 305.6 527.8 277.8 833.3 555.6 500 555.6 527.8 391.7 394.5 388.9 555.6 527.8 722.2 527.8 527.8] endobj 43 0 obj [312.5 562.5 562.5 562.5 562.5 562.5 562.5 562.5 562.5 562.5 562.5 562.5 312.5 312.5 894.4 875 894.4 531.2 875 849.5 799.7 812.5 862.3 738.4 707.2 884.2 879.6 419 581 880.7 675.9 1067.2 879.6 844.9 768.5 844.9 839.2 625 782.4 864.6 849.5 1162 849.5 849.5 687.5 312.5 575 312.5 555.6 849.5 312.5 546.9 625 500 625 513.3 343.7 562.5 625 312.5 343.7 593.7 312.5 937.5 625 562.5 625 593.7 459.5 443.7 437.5 625 593.7 812.5 593.7] endobj 93 0 obj << /Filter /FlateDecode /Length 21997 /Length3 0 /Length2 20856 /Length1 1795 >> stream OFbIBVPJdmSЌl|Iʒ0Ō}8SJH r,FxRol|NH Wy2%uuƓkto[2 zyG1t+5./ֻ/Qύ|q13Scω| :II&M@W0$VPc%̥YH9IabY 8&e iL̹6{$8UUǷz s-U1gd4! \!;NHaĽ{4'gWQEuFԈL9/N)dŵ?2gLkiw57p8/ dcZq>P(p9P ^h'RqmB!2^Z a]/ňӑM`T:ъu7}e,mܵkG J^ t͇ZRX+psjT: .tv?BY,:N$EٓIr՛[G C)寡[ mV,+ó'ShS7>JUoniE7$\$aʬ X3Oؕn)B<|Iky.1t\gP^Y"l67؟E@0DҲTIC~e+!ʱ@3u {UIHݺb <.jLæ}JdJ3kJʹ\'# Hfmprtv?aK:i](/S4muh7.{‹=XViャ=vO=/3}{زGa!(]~tsm ߣdL8 [|*Ut19^宆[ƋeNؗߧv1C!+TPA 1CgJGP4R%bazSOA&8>Ekv5FRDnP>q^9Yv^ĎX0=y6fѯrrmiƎ'"vŃLM}6'mU 7I|%WwIpޓtM >ih  :9!/DxDeX=LKWئ㐃 ӱ+ O9Lm]$5QxQMf1P;l0&bބ+]#xɼ#횆>KT^Re,nZ>ݐ +7+R h4ʭF%XGp㾠dtZص^j$@ p}:>B`+<^#ިеPR[W) Nl'FCK!`%`)H6Tϴi|"Ц#/AHn:Ӊ0'1?բR޺TqtGO[b~tsdp1*_]S~4[?xGš RA_98!sD((6JKLL4ΰUqfFϐG'zpd@96Ng&Wȋ<{ɒg""yE3j#^Zwp}Q gdrʃ`Z]B$BP|B~-&5hâ$?ߪoyK9+XuI8]F滖@ѱ_ g0mW EN Ye ^cP-~X|{Aq0 F<mX׬džt?`ZwĩttT\9e[l[kiS`]u[h2̂ ߤR@ZϺg ϊ>CIֱN!;ct?:l}:o݋S'}5_>HTˆ(#G\e2䯫X-du F̌5:1M_WGסWJ".8.k̎M{ S`QZkE,+',;>-8#qKS>!_[Os~긻Mm/P( bTrݰ}/͟rT{dGl1/5*t)f=@(h0剤WqN awb^H -a UF#:(tN8\ ).hncţÂ#eޭN/RCb䄧m e J+/@& QۅY7Isʿ(R>ﴒmzˌ-OoǕ_u"P{'#.zaTM׹l>صB&oiR%oAAo8 skJ&4=e[X/T >ZuHNYt6/2Έ``<%Yt&[,~&gnßFm--Gک>.X+pMfgG(jb-CH_9M{C޳%!rx!v]\ >dў !#8P^$Tb( |U e:=V X}PhU$sɱ?*p/#6d4HAok^c8 8ܥ1tE> i(H@>ZBc=st綵Wulýۆt*&PNِ@XpUPHso0`[(m>)=3~Ud|3߷/D Z#9bnS(%-a[.GL$g FxK]/CPV!/nYx͙ɁkEUOCDH/Z{ k!f[;A>F; ybXVh:{Ž[5ڟ& +J)e#Ų9f3(J{6Kz (Gp GL1)+)D4޽p|4@?p護棦J L- D0dMUhEàQG4;4+p-N'hj! ;kIa0}4Šb9g|ar"wH"1@dۜvg8-^pdJ?6>5Ii"Jfըsg$kL #CvNmWaGO̔ 1th)Ww<#URVDp" Ddc=?e g&fַh6GX۟TOh 7j䍹[a|#Rί:"_l+fU*{۪oi_܁DEJ4R?R_Ty3 G1]U#UisF Mdx̲ gAЈjt 񐆹  XkGzU=yq|Oj%^@g( ɢB_v<]\߻9#JN0i/sHŊX BFr<?E< p1%2}qߗeZ9k7#QHLt0kA8E{QՅ& Ze#+miзXT![pBW/TYGm@V{۹mtqk~YSx=rˬKSalߵba;#Fvz"gA㒹 "'U|痺Y!cWXrF5S+PLCzG]NMVwo#Rs2WSH| gY[5!:ҷ p-nYt $8Dա1?[6>P$:(ˤ CPߖX[-ݤ]^<\ͦq2"CCm];7N;,퀖B a#m*vg:yy&ֿ E6-XKMbkNHGTEy{p$jp ҫ)ScAo⮛OOaϏEߧIUtvUï_?r] T`,>tH: yX%3(pw,qh_bKA"d%Dn5c{-f[=X?U*a+ѦchBrtjN7Q=`@(7o\Fy HgβaWCٌ0\٦a#oKZsft4$vPu9g5Z)J=z¥*f疌M*:9щTC:ueg5w??Oċ4刧֋-G|`QܭHȓ}ƅrHF+^eStOFo*k]cĕZ$PrkM_YS}F8Դ[fϹpK!yRFO0{ـ& G55 V{߃ Nj[W9@\Zy%6eÀTH.JXإzت}m^ʼ*yԾ4)NôL137x#/*x=GwÑTS4 _^^Z}Wѵ(-T0RPo"ZȢ |f;΄̐˓KN ph)'7@Ԧk"XzGzT#H6`WW*Tʲ "3% .-Z"mqw6O.DlUjrdOЈ:3[\րG ڌ%P ,km%)\.>JEcf+>Q c9;JU .:0> f(.뗄]ܲN(}r&O'bq:H띔o/`f9 ,iu]k*qћhv9^Z_@t&p&2ò]),vZʚ)wĉ8ܝ"{$+K $fֹK`4lOl7 @(%vDpJiX0tv,ϓCl߀: ͏:⊝pw6LzO/A_elkPx{< WU!c[ɚU4K^ +!ZrqEuu37 &/8\9Qq+ocTo4T}(<]ɮ*ad`񜊌}b}, v|6-WCWu~&{S( xa HI˹mTTPdQr6F0ݧc UX*GX&{s!(ƀ5g:7Z/VhCjZtZ\|0;QHژ4cms" fH$R8W9EDOg<}~?__ų؎A"9mI+CGíN'a `Mގ`7+@#lW 솰kBj #kO|"!jC .0)*H)Zcu!07xE>8e3^I!YOjmU硌U:sVvԞ'-Dh#FlGvWOɇI:aP`h{F?4{:VϷ(g\I#]1^lZO/}˿n$W 0<~$Jڹܦ5yIa F "Xö V7_dτvXY*x/TJ4tg|rvbzxJΚIuHYRށ,7(x䣚!&,m_ -Va8\8z,cKediGlK sq [Ps>R!jmpwq}R FJ_X cÛUHT'/\[oK,ٕQYs?d1C"XIhGWŃ]|ƫ3E0j8GʿT#P0(F?N&9!L)Ffot,LN\XF3e\F䧰̓b$c S'Tj9b ëc*Gn|U)ȕ]ϯ`.pXwee*Xol@z'f~O,+p['OmHFk:3L@lgnLl&H6\ #_A_. G}I~fr}e$o+'D#4V;X C!C#uOW(s1"OR:?`/:' RnqҬ4K=/NvjfuMK&?1@os܊ ]d2m'Ň3&TpNӀ%$ |T* t+dFJ0{m#K.bd2xlj(p^~qx&=)cIv^$2h2[6z/Pu/_x0 xoa-2,+cyfZ"KVzX]ݵG% %5Y.seuؠ|' Ôd 4};X5tV\0$gjۣuMkXDaxש,p}Y肝4t0bA2$\˛mfdQ$W֕KTg.V0c[Ai!L%rYazZ?@*wg,Br.A:&dU} <$>(fq|/(/'Ր0Xbh=j:@H#-JPlj5 |Y!jŠ`'ůt+o`0y[Yl؛_2kQ&3}ޭMReET0A0GvSf jY" _ fz5%gD;glm%T9?xwK`fԊ9 {Yʄ[6u,y.|nb)p`5&ǘ%Q~ Rr0@ lckyXe0zn;BT812/uBv?TR8IdrkNѡ@r {WA)My;\SѪت6E:rkEQℒ(奞 nדG_ !$3&_Q1VɭL$i ށ}khHT]51 a!PT't#Gee U[m_o a~@~{gtVM7ۼ п0៨yFΗ*kZVUdb!1!M5:3@x)N8o-]~sȼoGt9y\k+iȸs? Ap1XnU}澍)@j.7#޾t ǝ܆|1$rJbF{*dx25{oП% rZ:v !#iK@LłӮ/5eq=+ez@rCC8bftO.o? _ Dt^KA|%=b *- Þ S>(Q?(u]*?ϧ5'U>SzaUk==wQI4{r噧"|_=Bfb=v Lz*goRcDߏH{>{nU yb4 M|A&b:]Y~%T(ħzUϋ=νW 6ȍiLB7ˇP\(e7 nY^[:qKٳ_)֜ 쳶#Wa4<{ ,b?2pY>:v"QXkᄸL["ͤx(MԒQp]H9W99 }Mt6H2ڕP&|1#ݸvxh&Q*6!-G&B(V!^ጌ-$5DL Qk @۳} {R㘉 ]]'Ye R:BXbψhX3X'OyfkEC7˒>yg|T+9^\J'/w‰NX1ˤ%xI[h-0* ݯSqFT@h!XЁzʊ~YLݫo6xr( /,?;op@> ē82GgTnX2hZlÎ9 մ6䩷<(! u 9AmL;q>R9@ěe~ީo;'H$f"n>Zo8-Yz}L*eg8F>(sv OD8#@k5k5LzYÜ#;pɊ},'i"])Q>ZRMI^~o)#Vc}#Ct9WlқuIK9Q$-KK,A:aу[0Ψ1N9DX I4 cxT"xùw4*[VYM(7V>|%ioo~T# k$uM v(trETk͔"zx^s֫xR/ z!h#  0RX9م#KZCg()12Td2eO #c J)E+ #H;e @1LмaB-$h䵾~ i?u>eD{OzӢ|#0#WSdIжs+EQc,ǝvCft㟜QVĥĈvs{MKc\?^X,^"i~}mT9A|o Y!ޅyۛ t3Fx;x_ aĚ7YsyMX\<xSE!I[P,{IJ%B{9T8dRD\Jr_h cixK>]$䇦vwlnZmu"j \IWf+?>GҺ˄ya|hE7&eag @{>oV7%@xO*rK\Vl*m E`Zv1z :kU:CTnpq$Mz(Vb1X[AFQXH<> j_;</ԷV5EZ wu?v$ --x8@Xljյ؃u\#m8]htuRL{{C/%w}_f*ȍ?%d܇_ݡi8U$TX]86!V)Lu] /uFcv] 2b-[[:Tci3~ )lךa3:0?!O Dz*7=òdep(Bk +Xf^V%{Y>qA#s]W0"O6J8**.هKR}Ǧ'Vjx`7U[[M8je,e%y{䶉!,HY;J{$4> 4؂LjJ79z.{oZd;OZTh:9{ FzvNaaiQa'.X)>@+cl`f+d/tX5[jrC8Sٕ@oDh_"rd!w7:^r̍aZ rQ#E}lmPȖw+GMU KQ X\pCrN#;NJﱇi3[P?AN.N`aN NH01-'3D𻙷,@OPlMhQ2V]Ds;k2>T[tY͉~#=g̯1Gqm ڣ^2=;,{8 ՙ^cU&E;ߔەnz$/Y]mr$`L% $ȳG`"Ȕ &F{1Voeb"yaۗWzoz@ŢZ* ^~~Dߝ{$)_:|kB$N0[uyR,u5Pvh"yr6͕V^'u?:mzuqz1ȉśAL:<24.Be:|ΒTáӲ~ֶ/7/9$n ewf+~v(3clEr>=$XUbM] 8<^20 /zIQYoLyuPxZݛC+%?E MFZ\t3J:'gw:tUMߙ3::Ͻ^ O AѝYSy<7[Rg{-)Qza: Uxin硹yLqѴ<߿'OtDF n%0=?1EXu9u?U_,s>O_ZdBۤ7*:W*C)vrT&ϯo1LF۱Ǭ]AB3u( VDבy" tC* ]qofq,[ gLS;u: qZoxogVk5׏3?Q[31=G*qU<9\ HC[&o7OR^0-L5݈jJ:mf2ryI]]Ny1>3CW&D%%`21N)_RIظ)2_z%jb[r$ȸ'$Te'z=. t3q=U+yfLrPhylB>cJh羄W2$t=eI?*YkKUR8Ck3X0ԭ% ~ 4gh4h£o:SEX'?P{nhh2:Y0^.u 2 M)C[go,ou&Qin;_<xcV3BmԞa812j}^`#խz7T18qTAIqdma\O A0 \ XYcEN ux&u5k̓@ri2c v)G^8kӴN[ % C]SU=b^ NRQ'R(z^B}J<F Dr,L.zs`ܞO$RW:\*xPj9l*ْt*)i@e@(PUrI5yxm<A.5txD]FK8E>UÞ+8cp''z̋Ҭ2eZj.&y:6ǍIet\cVBw3%[yc ӉZ+?s̲Ѵ,&BIHmu1pVMӍhKȫ# GX)riV=R -a+0Z'Vg!2;HJhߣ[nVPM,v͋ENyTCnlzTA1\2a'$vBo9DcCj{[^UXA\z UdܖwRdzǓȗB"D<*%h $S{,GYe^?4L>#OPN^3GйpWٙƫt=7N4$)c,<8-r^; @8@ DT3: nJf̻q`i^%:FL-"=S3 tzD+4_eFHEl!bRK^f140dha?NFQޡs+m V@B ΰ&s5MfH!T*yf >0¼D&AG45$15O˜S /S<|Oe+a։\'oW-ZKΦi-ǐE'Z/ʊ;(u Mx\EOH{ ;v!,Kd<-0$2L ?W}jw_+\PJTA5" ~FB:տP4E@.9 Oŷ%LӢ$ALs#Ҡ4PzA_ydQױ.͊g:Mۇ*V;:\O.<.ّͪ'^۔a5K0C 2$0GhN`Gr|Nւ%hm)L:݈#^)zX+·.d͢&ۧFCc6AP>E1:&";bT5w*"}Y<[>]s$bNu}$=Rb'q.²낸I? 19Eb^Eޟ903k(}ZG**j@DaJp6Lt,_vq6">aO;  _(?1Di|tSW3tr],#۽(NG,vՈ0n\`a]A/v@zx+"v7,7-Why~^OK8;]Y\?$Ðlp@Nav'צ-8lv kQ'Ѡ;)9<瑠S7 6/+Z]%@j}H'x?}syϽ@d_K<%p.QkruFHWyk$;(b!A7k=9i}G[Ļ-\eIoJFkKww7b`4rআI[ߓ:ɮ~/_e)PZ#pǑM{0Ò!uB{=h5ؿ(QaG%@ PoNJLFs*&4kXH2E,}@wk"س0jy}0)~r%%;}n1)xY;lܧE'U6ŮL (A(ask1B*W?-I7K괅8ynjy.,n&|Xe*ЯKHе$턝So\0Jh5^.Npj)i:jM9~bA㰛ϡ(bҡ>d# kWFWi}RI^|mcm}d46a J쯲nSio'L_h\k?j;[9_1G7k ū:/嚀QK?@!,@| &t] 6:+_6/S\$q}VC*rXLS>֘oǣJH*LɝNBQQyuFnߑuioڶnt}NkK̫3\vwN4?^ha!Ε2%S6tqVb%_,BPv)6]ЕS5- X`Ntn!.|s8 S(=ߩn߶# .c.ensr8ɬ> Q,CpTTBVy\g%C 3n9&y bQywO-ʯt_g%m2:6\I"QZPbr6w HUO:Yu͠6yhcpRRGq0bG):,t';ѳ6gpG׿)V~,t=3FkQ/_SvLjM,d z&{D &~sy lSrw;$m>v}ׯL%]+|( {vN1v4R~ }Sc*+vJD8t_ %=J?8&<`TcV!*B#:R@(봨lIJ5 kh&, ZJK8V)$͕W2?u c+qk-Ӵh hu嵐0(gݔe/d4[(]4m9LX62#(" ODPxX4ջM4;9PnmR|s _d v=H<iN4Lc&ˀ!7>$f69dfibPSbΝC7*s1+wy8y$Ǫ8<:m yg]IE&ThOi] 6 Jtk4e ,¡YJ=۩DY:aLwM'xWsƔ΅tb`?i&2'b@2Bz,H]$Sdɲ!a*w9gws-2!gHkza(?]vn%nR .Y2K7n2 V-nn4!O ĕSxʫ~qaN!*9JïGn' Pg~i&hY39jn+A ަDAΒ7sbZyΩ'Hxݓyʅ$Y (as,oTNJqd ]3`4^_{=F4f}Ajhϕ]mI;HDZ+th?d{zϋWRQ冱ڥ[5;UCw; (󯌦dϏ8!a0Bd)dxJgLHh'NTSqD~I CN{bs_u`]^< M :&]:7<tM&_\aܒ+9ĉ{o>F d0 q 9f*.Ւ0+ ~I b?$kUâwGG,WPG[@Sؑ~A:H&& sJm~ҍ`m- MpEB(ΔϹ+ CdHx!}Sú5Ur#ޫ…Pg%~;&@^vYh!ۼ%4]iƴͣSh]ݪ%7~s ?uh_8bEOUxfY+H2"#0;#DȰf"boR'Z<>wG |qݨ_3xsve᷄T 7ICNt]$XEH8Mx&qXLAv`8A5QUtTASߔͨU:všRi$*xuXr̹c0zP'}x:b;{"m0XkM K]La&95?!U[tx%CDrOI50G'?..~E<26k m| gZlKU=3bi=7Lü6>&]=%Z3SXX^u /P  7|=Nd(2~%hsKꀼ*}yA1y_4^Sga1UI< K}#Q!Xp"4"a+#u=`fZ_Ũ{=-z|Ksl][싯>TR$ |&oafMO<.OM?UoJNbScHz.vholi_/ ж["b\rXX3zg5cuML֞tpɴZK@I)n] lWv!Yؓؖ>;B s^!A>bp T )j2??GeEDk֌Q&I~7'`r>@ht g tًPO&/qdCTI%|;[zIN!,"n&+=LgN!)]WqgAq{S6 xED0edY1O]Yd$ @Q"ъ ~7Dfdm`c QF\Kc<o@l )*P͑%>q),6Nض] uWn$l+d SNw̯M30Vkʝ⣫ 5xTVu(`3w+ݵtG㶦Ʊ>yXʊ v k=c^ endstream endobj 89 0 obj << /FontName /SKYQML+LMRoman10-Bold /StemV 114 /CharSet (ur3<0|cVJװ՝^T) /FontFile 93 0 R /Ascent 699 /Flags 4 /XHeight 444 /Descent -194 /ItalicAngle 0 /FontBBox [-486 -295 1607 1133] /Type /FontDescriptor /CapHeight 699 >> endobj 94 0 obj << /Filter /FlateDecode /Length 23671 /Length3 0 /Length2 22469 /Length1 1991 >> stream P*"f^!O3J 1Ի ޳݆ӯUm^DQ![&ۨߜ;QmB.hLVw10<*͂mII5nmlOJg,L% wh xL +Op1CM}s^ "<hdQ[T"{M4\| a 摃-$2bN5CCMsݰ%إ>m.Mz-ɀc')[D$E5..**ifbX>@ؐ6eɭj$p{>8PQp/P(֚u ^ >N94T 1|ǹ8WY@b%b~ˣ;hZmv!aGNfwOPҜ-Sv@2F~n9}:vg;zۏ\Tˈ.g|Gɾy923 u}+ I I$ĐcUhՈǿh-)őѺM Qܠ]ҫJ>}hC`Q3t5ȸ)YrXܭ:aظiU9׊i8Z\mhr6*pEh9sxc1F_~\TmWiPjR˟|*d=dv`jlԑQk9fǎ<IlpwbeGuv͝&n dOO:})_ݐ7#~N<**Ve&8e+ޱԆ OM8SJ-9(yB- R\^FµގIV%0Ӑ7bE2/Q2NUDUw JND߹ QS22(pyK (< {yk60𧈙} Vq܉*<ic78b۠FjgQ{gM7]CuwҺI#g=x7;<'DCH^\ŮÐ6^0n"،o{"Vt4/ T &ώ،l>Jm^j--Gs>{͎̊ [/~\6U%KoΌ۲ 誕QnRqc(c)׳g Dۏg$_`g,8e`O*FhVo)Jq˾C#м:֒Wb4#QqG01`U!Nʋ@ \[Be&?ӷۖqP*eF'7O&|$6rc2a4#hXM*8 4;'XV.NiMB\)8u+TJW͎!plK7t@`?zH=͕ i@2 &7t<W!0 /nwΟ~33 bާ  xP ~Ն-+}AM/8Λ!K.48Q jBĺ9BᲮ,| TRS{TiݡWru?Yt |R6c &.2B֢t4=wRx)@a@РlXtxp\P>nz;b99n]}(r_#1`}܅Q 4"C,m\Lzt,O4U$/CK!Smt%ݙKe|U2#])}W${ GUIή5kXY֦dq:_# UgۓwmX~)I2 >"$c@Q[3Ϲ=ZT(v38뀓?["!)_X6D/u6g䷫rFSB|g OEZM06+B3PtOPIP_w dg-{EUxzT"^N!Y+5.8W9#ה}4N`r6ͻ^DY!$3PߩOޣJ?)3* <0cudӒOB 8 ܻxY+VIh@AhЇ3FߍQ%e<*܌5ݐS" Yש4zƔ`MRVʡt^@7FbDP8Aw\F}wj`s@6zw{%!%(ͧܘ*NzDcXdSV nq(``2J=jLZy 0E#`B{̔Pvm?G^0(c& M;oS| Ij#&[K|7o/o.e֡Ql&j@C]H'~3Kv&6yvw9!U`y}3@<[!]qYH X.-/A4R@o\ U݇ @[\Dp{\Xqd>71ZnoRNL!`˃5h$8\%">*Z;}¥*3ܫϦ * {u3G2 E"N0䫁Dl'q 9'28ܲہ2˜/ RkaZHX_.9a^ŜƲ J۬>sU󔑠КBEj"& S6 Xqw3j1e<KL5ĄlSEUNAj% 3]'9xۗ’BLVBvV gxS.1j_U]q:hغ9Y/=m#ȕKf<W+%/3Q`ophk $$1e&=t\\~ڇtsq:$מ G1f`IB\ƃg*JΦu$/N7E4Ď7j2?tp+s ~M?Aj֧2Qʁv@[ bҴ:7K)Ün΁.&T"ua/hnRR7F/jb }O-ON_sA8X @]-Tݘ j͈OǕh tYcйMYm.jKľz5ɾppHWgBt-i݌ %p&Zqoo$k97ahwn1C aR#Ox-_AW16d13OD4U]7?~~nE!v(4?(S)W,@b8ۈr[J ~Īho+Nd|fw15uuC+m>.U!>!ʤnN~i+w8]v@q&&bfF^V9W$d?{ՅYnO+p̊{ Bސ_qbUr7Q۩3IT `RiCLiE,exU^wyE՗-hvFJcHxn ҡbsֵыS`غvz;X,o[/G)5: }ؙ̀Ky4=k#f -4d&{y :ܿI[kX&ScI@V:[l_W:^PArl[%a;2UŖ"*=-V 68wyܒh#FJw87eZy?՜qoQ"- uU u;+kbS1uwG :>3D~u ]' 8/>_OHɍH! vt=.j''UmG8nC N@ X默g3#e5\!YZzѲ7&xe1?=U}2t7!ohWq >d+%(RRRd38J7sk11T|sP45eUQ![^:*I%uXS{ήUΑ]GH`yct\H^}6mKIP%P/Z!0!e߯˄_|xAKtc%, )n57jh]z"ݧt@C6pEdy7psK!RЃy?Rp4qJxT?իajB";/]"*eMNP m!Į)u/'O`H0V+Lr!"ځOp`ԅuQl3`CdGqFwk) hFaS?9rIL[^gInZxlf; EF ^# 'l1Uε >y'=x>ʰ I;*L;Ӹ:]#`+~*eہPK“"%Nhڼp {'Y`)+@՗LBC{ip\D{YFh)`@12eN!pK&2abAuC9y?JjXqt"0V>RMaC,a\u׎zhH9mGZc{.uC*j|t>݀eg֓BRZb=CpMږ CER^EoRTIl~sB,1 ^,foFqih|p$ $1s;Z d,JSѰu=&"Fg\n $词.S2FyHt^c]eujƴ5(ۧ,B{ʺQZ|cYcD{}opԍUtz2Vp*g[h%*WH] |B.Sl3vtS)sqX S?8-RKwfan(>Eq4?[XHn 'k8qI+cF^O˿EtO`͹}?K);~.hGH#lfd$Y2}\&wuT{ \p5~cCA:~4fT'l5g>֌L *lES _SQ9_F@6WTp={L0GП1z]&҈wZRd?bmE3W ~~rHΤq&)v.O>t nt(š UpzAz;v RUicV>cXP|"ǡ9p?܋/+%䀸pX1\]sذ|2Pyk~,}ZSt 0)\sO )68`a8>R7>6L7е\>a7S빆?+ V dcfG0`q6 ZsD'}&hs/UT+ܩڄ񯏧7E.?`7ٷ`+i 7մF9,d. VRӻO5pz AY¯vxj%j)|xAvb۸%9Roс~ݤ"h iO҉…p ,q:50q.#|-a+ApRp_&A]-^u<)>dW)b3P^Vvy6 M;F <{y`Tc@gA )8_yt3!/sPUR ᜚z+,UzNзCIO  sʟSAi%αf^t ݍ,e"C*QYꄶ&Մ8K=4d_{.EZVU`JAǼUgF@G/:xts5dZ B:#eAl%m3Mctm[XWD^βϐMeg hj9T|):k xa zM¾A' a/yzjEJ1`<眃b2p4O{]҉.Kv.YHFj (6e3KAJY1FDW5#"jsصla.DJf O"|: EYx{e4مAr*m2Gg"ĉ)XxB1>5s#ŮQ_'tHs"d]|QN`7`l3bԮW~XnJq۵Et7b?~۳x0^˞K񘆺BYv鿻*">Α|bhVS/ko_LUHcW24oP(>S ^m-) ld}5VPձo9b#ɋJ{OdkTe{6@{[Ԇlm. w_W73I + #II ?JԫӀI΂}PzN7IaK¨Vײ΁M@!jϲTu7|s%Q3Rl)n圮!CCB-WXƌt%#t^xWuZ7=,E{~MT8}%1oS¬ђΏ^C8 }I,Jcl9bZsК.[/[nGR_¾"+g2|4mg캺`c6*ֶ?HtgI$dj1+9Gdʐ<_=Qq%o\A+03EC+ p7U ^0R\~ P2A}kw(PXfB@4T]߿-,Uhl oP*,m._&=; TZ\p.7)Zc (wPpDb+ei5YҚ4Jy {rn~gLS0w"R#=/T^֞x)Ig+6Bc(* k]&"njnCy*QLS7 g~l)Cb'X^4Qx*<(ZKҸXDF@N֙aB a6 [Fx)$N% aY.<zY&RnpO%h|h~^u9тnIH\)g+~tG4J Dֆ|Mґ<ʖ 4k(r(71o0p$ YGٵ8%YT7lSx5[pp}E5Ag~&x#yMR><7&&7ʍyzA E(_Aq\l8ܗ:rA=G njU';9Z0_U'רBPLȊR*υZkx>. , \< qY 'SV3Wfˀ(h cבEhO,ι*15 M.qAi2C F/Skz!3 V)FV8Q# k@fP0Ȼ <;5jOXta $A-7F(Ynď [h[q gYU]k.k-xsmȼg+Ib@;$v]}WL0L"όzEIyEhLe;P^nj n(.fj@NƘm!%ďaT9kl rl6k>\'"$P3+_Um#&r9` !^t"tĬA;  ݋1b@_$F'zqGK{%\JB\VB z]oSvPc[n&۹TQ k8GD0 K涜=} ZBQle1DuLe{RL{®%2%'Lu:[zv^h9^m;hl0W',pĮ!J*S׹rȒ  \5ovJ+Urw6bUVU_!'."uv_m[ōUҿc -ՄƓOXL>TݫVΝG̋Cr 3OJ8w+H"x6q5{eDvp`Zp-lӓ+AU*\ DƎ D  ܆C r!jv(ctOjI,`(uRm(N̠t-v܃S||OSX]j#ڴ& ,o+:?N?fi/ccp(#wY3FbnmެD. S8 +,z[h]S-({=I ~ƆOϦ eYX%MG8z>t/0@AI۶G'$vT4dNǫ SútAפ'wʥT0BoW޿;c9|-TemZ:7i/W4ffCLx-IzCYHL.K0GL% |[Kx'd đ(?:p)biR " F\2M}VӛцqJ|h* `͋:`ی.,QjKHg+C$+M"l¤;ѫs!~DZ-[ԩcBwU>d\AHRw> AcVv0< f#ʌR?vϿVѼ`z< [_[N̷bSS*]C/O\"˃] )2CpYk% N @)X]$f+5DR% )au nV/O,̿tNsej?@0+KՃ@v[5uqql`?8S#L566.аѳ! @NV)J E+jED7YL\(^ZDpÆ# nttYCU-q*.^G"!m!=vO߅8Ҁ!=}@qS!U)x4q1^$7 X'yҗBwxs>Fc\8odn.f%܄rz=[i꨾t,+C`/ (-@GP#:N~z٩dm#R,|V`PTрdH@kD:NenuT2<ɱj3Ch3=A؉K1DKkԔ7KUK"9]cb5-KK@M?f^0*5M]ܧ$e(J{W'|hrXX*b+(3#!GWG6;eogK)g[hxiw7w'# UVm^Mĺt ,Nff/}~6mV](%Za]MH1>}Bn~qqI:*Ox=^29B@x='W?dA`\¨̼)5hc!fۯ5V3Wc 0 dCMvSݢ&C 0FIGў>rzú؛Φ+'!%tx#$pjWKiW,-b*KؑcVI ~RwUo%8=渲w-&tM4N[ɗ7@&*ri`xlb-0Fy/4\$Iٿ \pL_&$`㷯ojاc ,*Ya}~~yomyY=sa_}IgEdY ݣ{vXGs,<\]u %D$[;W8 p&_I=4=+d&F.^tBMtov ,qAYc^ \* D8;SWq1އt5x| KLLs9E'Q_r8RXߣG[PHc0&5杷$B}tF(qˉLSY~C58v(>zE@ ~yw7'gǷG"nvສ'š c [|E;zG}۳aۥa>4y] DYTd u!,?u,.l=vla `RIp)Xy]֏)OcT51h1:DE0-9Ak3|=M[~i]DE̘?bS5'#jy.d |e#-&nysecE 蹜#\t j{)jUr@w$>>*t=csPzfr|ˇ 4›]XN$pp2aV4sB;y=:vqu0IPe )޷Tod#ng g+1L*xbZH'1d:S.NX]4"^]7PP})}K8NWؐ+= Ie᪫噝M)2(wZCTEuQ#/7Cmu2?O/Ο40*FǕYFtrQ{\j U\)VJ4H|*$~oLyk@LwSkˏC gnNE {AG4 q|LZ ?`\h.zԴG68R L ɹ..5H)'F#l:"޴ ǟ ~C[O1@ْ,jGTRR߄7(4lh2mm0ÈsfNyZ~`a +fVڱ&j| HwEgd`xuhnRTqfGgRӦ5";C=*A谹d'<SHE>g}1V5D#J{/̵UK‹_+ێx>~> ;I+SG>fKmX`?>C&áרuBIACI 7V@AEKrs nmyS0Hwe_ H &"\aNOڹ+!5lȿU0ģM5},NU 38.GA?¥I Ʊ_#qB/L(mgv|\dlq0&,]&+Y}Н{ظ'9X? >G"̡P HvUP Go9"jˆ1a$Vk?aQC[mq8b$cA6> LJͨ:s,]@] 7X-P@#/gI ;BsG \&t;G @BpΒhdp.5&1@țUn6+(K,EΘxg֣1NNąXKoo?,p~7D"h<פZTM6T3Y OMG0VXv~ٻȪ`ާ8̑w4F32\VT[h9ơML$Ajިsw@wuw#>)}*aﻏS(Ew~zZT!&ML @ZX+\,nŚlK/]BpRL55tMoM#i#]u)einĚGuF}H%9TPQC5~ݑcI^E+(D,pKNy0߱$}S2RS!!zc< aNp=;RGZ2ZJ'*+l,C%!aI_g:*&g/zFHVY]wBC&@ 8Hꞔt IIgQ?ꗏI;3>k(lJ 'AASk )hrS٧\N E)t >R(}"rd.a.2K<'s.U|lvE)͢0Omd$2rM ԱZU[z5L{(hx8A9VtQI6m/)Ǣh 7{ڑ'.|UM(>x'$`Ŋ戠u~A3æs1}-ң#ƿ2ePn;b0o(Z+_' -2;ԥ Ͻ#2l[Lj Ik琵3܀~uW}"„4#[PF wsp4=/o,9~`8E :GjT🙾V2:fdshgxSכczMdi>6,9]hPa%no¦{[?3fLӼ9v4`& 4dkk7kw8g`4QN 8W~|]T/F@<IJ+ddI ݄@I.}9tQ;HsL|F̨K):u\<+ Cm.#VJ"F?dNcC;AIz_VhK5ϲ"`s"5zC7 bLCvShV1-ić^Fjqsd;\^ZLA (.0nBݞ~1ioKU_$:P׏XӾ=K*aw|i ;\{Krґ_LW6>u6֞kZ;⤹?r8 J J?L/yJ&bYb2_S0"Cp<\Bt_Yz#nl>䧖-gi$tYnYgAL18Zx8+0 "՜&q|P]YكY! ]hmM-5DulUt5D;PQN\dat{NÃ9Ti /_V>h5 6 KS8W/~ܫtbuR''$MZ(\?a`,7fKsz -皣 xb1z}c yGݾStpz%s ΂rүz1SSX׹#Pz p'qjOзq;ix̉Ϙ6ٚNi-D"Ud*ӏu͇tR466r;Ң1q6gO7f|{]]\_auDhj,; oǫC W8#8roΉoF\j:zsF(nD4uȹ_RPS,)a>[niLd9T Imw:1SuUlւ~&LB>L>qXW*Q״4w J []ZrMŸycZ=c)oL~vguѿU͢g*]cF~Xa źCXdσBp:-Ɗ#zD_S۸,*b`XN@=R( \Qbm;#S2^oz&U7 E'\rW}EL?n`@hU-c'idw$j4~ U Y 6Fl geML_ Y:_^[f<8pqɱ=լQ<{#Da߹I+tun ;by]znswZ=&5,2J&ٴ go\;&D,9}'K^)U#m(5 u !×"f T4]<jpω F A1j{k&+b㈖lq}3uW8ևv~TN7C^2n KΌvyv!5]e~ӵ*kR)eU'-z}Y#3̩ զbM!qAt=EˬoA Mb{:s!J 9z1NjwE_uP8+(%!ȡVP4;ۨ"<+_O`jBHzfh =_oKЍ0 #/\~-NQgJkԹ L!NGgW E{N G"LaM| L)5Wi˶St]|Dve(l Vc1n?Tgu_{417g.;O4D6>Ԍ@ h(:ځ$❱ VYY;sh:MA9Ʉx#;]_O%wBk.|orůBA>\jodRE>l=M-Z+{e|(?cemruh~ jfc1`phT^e%DKIC&^QX׮KY2=H_T,y"y^kk[\L3F%5*L"QvF! H9&K;OfEޡ" ÓIX%7>F110{rQ70[ Kmfװ{12AAƏZqNiL^kϿ1f_ si ۡ6n)?96@YWXFzI,; ˘|G JHrџGL.e*X3/CM.%hްy[UA4CeZlHTݮDַab[l}ʏj5|R5N&).^&=Pht 3wC @%<$NRP2JUZٺs: ?=f;sIq]ll:ދɆ-Čfݘk=,cuc^ZӂXHe!wV)KLݥWE)A^9cjN,³6fK[pA-sMŢeW(vn{9yAe,q^׊%mIsMxhRRnb^p6!z$Q8ŅTR(2;b$Xd"rUFPPc3hzp8LIsȟ6;fΖ /r߶jV74pSϩn+ o⚏rMDfq_;'q1};MB#T1Xp}{>-OvSʼnpͦQY5_ )|^Xajdkۻ9pG5/Mcl`V1'ԗޒgc<6o|ZRU05z,h_)H*Cd*TyWف> endstream endobj 44 0 obj << /FontName /PXJMFE+LMRoman12-Bold /StemV 109 /CharSet (lG2#äz?ěIkv\b>|W',cd?uP o;߷8#) /FontFile 94 0 R /Ascent 684 /Flags 4 /XHeight 444 /Descent -194 /ItalicAngle 0 /FontBBox [-476 -289 1577 1137] /Type /FontDescriptor /CapHeight 684 >> endobj 95 0 obj << /Filter /FlateDecode /Length 28633 /Length3 0 /Length2 27295 /Length1 2241 >> stream 1o;7Q!3gr9?i/*u _k%> #nYj.0 @2E/~*2s$ d[M/#wQZdi!=.Х2PT:U뽉>,IXA+:)_T4]4QL a@n|TfA諡l񰌀܆/K9}͝ VBݪ@mbxQGTyz 5hMo\Hx-jzʆ0etE򊽊P'r|_]f߃""MwIe3;7L:8]T˄wmUc@PCBO]!,Z/Rv MM"`o)͖EpXp7Z;=\Ochl<QcDb'51LbzXM1sa;F~V&X:N7>+gX]yU28C!*8RI6Ҁ𸯤 OǴ΋5ےS:STOAj'+ q3mF~V"K=(^<.61EXޠ (Aj}pz~=#׫Me]pȻ.$ZܞX1̒#9}Cp(#*7rBP9J\<}t5RPڄy xlp\CL>v&J.BU6"S/]္9khҬFc"o:dvforp(}a$Բ[|~S}5oʹ)(Zk,3H28vJ]a 06]U<ڠF%RtۛhrSQH$E u^XV%2ƺ؋YH^.9vny=nY<NmFT5* ӿl*z׶%)4I%=Z2[!K;¯ķkJ*-(&y(=.E"5q/4{$#k!spewM w҅ ! ء;7/Rb9&eVJ db/9÷(puD\͵_Y:’ϟ2v))Z==+zo: AWŴ ug؅ik jA+Q)lzF, r` 5F<~/&ଇXEzR<5⬤"# FU7ܪyS 8Pdi+ (=!IJq' Uvmq(/p?34=)ܠraEe @e<9OWm`-:R׍UΊ=[Fz{ EŶ_uka FF͘[)@R|#_ND-8zW i՜!N*@m,hWʘu R.Qh+x3VSd"TOOeqQ;$Ds/.qA#xoMqIo7{ C\M_7)GӏD(C"%Uƶ4[s [ڝ]Uԝ9ϻ+ېXi7DVeSp'/lO˵|'t!QM @O^GHbͤyǰ߶ii&LK\`ᜁdBI=Eg7DmdJnk yC_xcqgjk`iy[\SaVo-DIΜf3X{#3wiOVΡ *RY?{ゟ!ϡe}EJ _l%6R{DyCF<HdJ@9' tĒh{^yA/t2.})XܕC>9l(i>E/v`%! AtG^`^m=̡zǐ9L<][L: n1+] $mcUyX2 A̡, ױJ׺26eԙ0eK5,ϼ$Q黳{#tZE`3j Ha_@Ia!N%n;lf%O"r6ێ#y`L;K<8Mh*+4oL[%7}۝%/[ghݗո{%Loh)'$M^^R}+\3{ސo93#|2+Tw jтN*t`$IFj.wf|t"&ޱPcte8dfr@q 4-AY{t۝D|еg[ )+'ʏ=K%"ڢ9l*eS iaFeyՇb.гr "e0!-O[$xq ;2T $S֏7{aIo,=d|'p"jh 0:}[Ƹ9 ں],9 0`)j@ZP:SտQDM&$}؋Ƚɽj"r-&~ eVo zjLX(S(=5 lӊaUYkRQ>Kb -f™U,8I 3{f\Ŀc~/%z.J}-]Gˆ(ÐeX"2zZ2gEt&^s(W*G#Ӝ r%sj]ۏF(`bNx JyG(KfBɋ tiV/}?s^|NDC%p 7Q|\eP^W#SK Θ7ܼ$fHTdx-UbES؞u?^er:fl8 U0>lSؘNH"bC~dIX0|u3!"B[P|̦;%H`>e #2a>jhksF(eN}N`&;Ɓu&ZϾؘ??-?HndJ-iFdF`\)_&|ZOiTBjNE;A#qIr|@*у}Sa~zuڢU1bO v =L|XZ+b3m#8P&R}f9ϧcB74}3RwЇVEU 玏ajk ]uö @td0J*$"~`RgmBcC Yyh\rA?8u-1t-.jUjnR ~K1E=^r=emi#j{)0xLJGkїQl?i1VR)mTRD2trճv <" jjαC:F-*W_*2-,22տ$.mk&`a=T()u}X]Kk^ׅ~ߐVXR)Wזe޷bg 5Iubg`fiquEZp!z`'|K?MDQYǂ 6 +ZH[J8g`OeOft[ ܝ2D/a*F¬v >;7^Q= o@"$^Xڕ[AZϯ7MqUQ+aLv2 :OT1A߽(P*5$D?ZJ*w|o96QK\ 8;Ti:"i25j |'J)OFИpD>> bOHlN:jP_)LݾfĮEQne=yr@"wv'{v%I"Zðĕ95MKA KT0g ߅8=pm~4 &okl%g<$˃VxR@hvPje UX9<<6֧vtpD A;"r}D:@C~WZ?_KeHV͕/tR8N?ӣTꓟB"W{i*7#vETZZih/Nj5[jt.Ǡ6 3%[?I]{ݸ\4K ccWyff3D+ۄu(k{:ږRtS[g28r.zDpiڕjzxɸsk{0qB<)X4{3 I4E$kB n boC cHn[݅# VǨ>K}#D20 u6ϒhQ*G\j92EIdϬ!,y]XqMED-=1V9LtZ[Y3V`(1_nuS쩭wp_`f=A"sN7*˝ 4oA9ݼ/y([jI0Tº MCu,2?8J}i-7GNn%Vy(3jgʌUCD6?戣ү ] eN{֭@BWXˠ9'dn&&yn,4%'F掋t̥>!vX㷥<Y|㚛\ @BtX:3ٖ^r VsfVbטNȅ$19$/8z6'{]'>b T ߔ GkS{A4zN7cFY,kϻWCC[Uޥ .$6QRI`Wl"~6Оsa&ŭk̑:>k b嘱%Dm 0 Hb>dOd2M"a_Yum3{b2[LeNf'9TY6jX*NG~Y"{Y, 负(f ѻ7OekXH|y{ZۧG Ρ-3 us$xEG2Ñ4ϰ\]¦,|.uO̍:~ iǭmA5w"ˠWWߵ,c)ͬD3wγ)j<oR{oX 9$@[YIos5g)w=ߺt詔zP&]obp+Ji;(_`D;LH cuY X{2gOtI ,͗~ƀVqIB8P϶qjIj)1)@ P'/.9YQpítmz=ߤ[m @eAߖ '= 8 Saϑ QʃPb7Wp%B]lnY@DIc~|] f5Y^% gQ3.ZEuX=4^ aX[ofزi}(c洲R&MGmj$vEELC'SaAj!y:I!M+ղA ӐKi&CTZ"OnO+Uw a1qfٸHʬɸ$4< (L36՘g{=ȯ+}-Ñ*tþx%<)NeԨ!%BꞄ%]tR_sxGAsvH]Ou< =urp+޴29zn[Jըn/Um$ۀay;B}'~ɷibCM4JQ R % *H*L>=g~p$ɹ7g-3{1U/+3E|:kĝi˪Slds^i$/*=Vn3٥ҀN-s'^KDAX}JɗVaLE/ ͸@i (x{_>Nl`> -c~#/^J~l \1s!`OP[5O%LLBAqWAo.eCL&+fP``]Ƶ~!G!LCU,mO06́))S% чd/~HP쵪ceT=]}EԛP tXن.ȿ+5W_'P\ }$w^kÅxD`0 \ή nVC_bG_@eYWuĠ.*z? \3۝ 6Scb27v5ۺ:,KR:Q {h jDK-_Lw1$ <>KkM/DVt+G|Ų_`ܣ=(2"C]u̯QMA8G^%¤I^F;KU s6^HCm:@ڊATt>17̛)ѓ|nɦ{钍/~Ɛ,+¬r?Sbt>SLυԈW7hVƻ’BVe{Yj"@OwG֊۱(R (N4%pTZݗT PP1Aʳx #z=JfiiC_w~qR,C>ĝu}Qb6fECG>ݡbu*~X6:W>$g>3#xGf2cϴcFKAgJUШZ @EUp8g LO8tc38Jm L/ǩK]P).ʉ0>4R \MHd\BiK9;4yzۚ$v| vuys$3`W+08 oAvd%Gu~`S|zM&Cq clVn e|~WfmQ>d\X ȥ@8G)|@pa gD23q~=Z]Kr/Rlv{T4!_3:[Nx%Ik@8oRA2s*+mqG]^"*<[ê6vD=?ɳD`ӕ3qMhXV+_*+&`IMf崡Jv<~6đL ox+-I8qf}(ՋKPmcؕL&ˋgf?='WgB\MyWzU1z4r=33(WpyL7Q-NveܱҨ33|q ,elK;&M2G21AD=~6G;R'!R۶`ce+=qM,-\Q؋R4XMRl5VAJޭDJUstBXDv<u+c?r2W V%YrT㏟XMg ? eQE9 74 D@e/D.>WH}7j8ͭ :"Hm R@ hHA avCjҫ< * ;1 "Pb$i-o6U{hځvK k;(#0 ]Y =ZK\5 ] pJ\c˴>zz@~.|0*Jo[$ $W?Iq_!ÂJQSIϹ _a"ѕǡ?ks`Ϊ —{K |?8<>,ne؅Ά4ReQj{^UUfǠp`]O++ Ub6k܈u΃Bq*nKSLe+!.T!9JǣV;cxMM6[cXN4Dȯ>0`,0%G<]QLЁao,8}9ud`97d?j1HUU, }b f,K*?<@A081M?e6R/>Bf'FSMWesFQ]ӗ8?: '.p4GvGoP.惤2hbP+|)oB3=a8g ,|I)j oC(Qóc>J)Kvd|1E^⯖x}9ͨhgs}_[83vn3QNrA;`!^% gG*;a@w&0QΑ|D&U.AW?kaaw<̚C(;!/AԓU_=d^P|Z)#*QtAasP0T$Sts(◆.Z-d kƝ"-:Lg-˔2v7_Mp szPű7,֑.؇hP)(\'%_v!>~cZ-Jn2Zn#P@2TVjOtNӊ'+%9'q6܍oY9[mpHM\C$˾Ri/XZ B}N`Լ4=x 4$!qWjN@o|<&x#'ӄvΊel!~8+w#%zNBR=F/T,fhbnXNN{0ˡjWF|lZ?%#8B?n Oդ1Ic mx=P$0IEib@Fu1,~ ȕߏ>:*]]1~4@?y9*_ ˽p]C@;F@9:02pYZIdTcs}j?[|JN==Pl,WbQ%̸ MiMbTZ[ Rb5-E@8\Óu~%!^ =SJ0N35graDf˓{MuJqQDS30 !Ow#mţPyS∌~)l-+NJ( b>a%q@ :ihҧCbd&?$mi&)OTk"|QLS8[1uƅRFd FFk*!LAֶj=0%  #X j#)]n=`:L@Tm BdYڕsʞ 1O;\/5wq]ڃQ(€/}!f+xr|3/I ^0=f#!Rfj3ywc5i^>VoO8o=WWPi ӃQ4tmÓ 'B @AR; ϠJQ \*-?$BnU A` w*LW Dȹe$gL=g08 ((GWCG۷$`|gp%MJ$Htl,H{§o'vifF_zޗ,CkWˬ>&7:_x8 rq&<݌iB"ʺ ,2ꬄnWTq7& 5x¥4^$CBqٷX7P Bzز .tJ,UDH@"R|8n.5ɏ(<{].ő7BH1ux =`Hъ@$2OմVQ? \(n9~`Uu+ ŞGba5 4;({EetSDh ?0k'Q}J 9Ɓq!,Ic f2h:5I8Nky1喚'%{8u"Lt Śywnijb,yYE'T<_pQ1>jL.-tyHvfPqyw⩐ e~{R8ouӁ>p)#LU {YOP~TWy.'5p.m̓_3[XjKsKQr0^;7Ϫk)#>1seL-=+ɹ SZExZo8΃LՋ#QVSc+8ᚓC@ XNg|p=zE}ڟ8 *䲠!wS$lm:CѢ;%?M*QlQj.]-mF"I'+֒Bok#Ż>~.5 dZZJY#CP'9ZV*zqOICj =#YMMS|^{;1$kiTMBW\"vZ%4I=7{'+V~alV@>#i2dD(qjiaJf}ćpL_du<&"J\=UF4gƲP&-r4\]JDfSSp}q6T|#ZeO> -LV/&L]K39㷱A:S7>*AMlY7 n; c1R|ȍ<%0h3wluGXvAG\Xg4yjijcWF4!:c|9lV~Zda,FgO,y8S_Eeհ蠨Wp+p?Q<9;vTBA 2UDOaaM 69ezWTiOhKs< D]~9u+eԤ nbp`Ey% \8B}Y\*o Z+az٦++_ V\&VdyZ5*,BpLHǻ͡[r]$*qZ BdWܣeu6<?36omϙ]Q3)<)Cr >-Ц#rc窨nV%ѢqXEOP܇ك9%j]rA)zs0`B.^6S'_ޛegX`/ƛyWg~kCzHd"2 I[) t(n^c? 2`xQEA":׿SɃ"8:t@{p .gg|^n'].ZgkK,[Fst~]@a Um;):.Ρv Jk vmZ@k[1x[msgKT4Nl ?e-P t4A Y \Y氻iY>P{]|[VcّSx'eCkfxJeȾ*4u}]RRl{%TF(&d?o8qo]Fv#ZLfܑ/w٥T 5{nTg֤MS>phlt8iS ˼<=BnI;gqnJj^ktEi63?<u\:~3?19Re"~  r]&+= >ÕhUSǦO/'.{M?gQ&b1OFtzPw5c~e(1"@e =$)bg@&KnDͱx p9v髛r!El V d=sq뜨{Ir{җv Sg3Z(_zxyq|~3vLfΦ[B"sN Ӄ>9 ~)#'aZW& ]dZy w XN!.<-%x!@=H8z1>LxXu)0wK+_3{f*"\/`@kN;ɍ ΎTSGرlHSld[2"ug$a ?5 -J]Iy jNwkd8_۳5;X j9.A"@%A~mz,8J:,`hD#:@-F"XrOf0v?1Dz`3¯v)ߪMu ?֥ NZޣ%.oԀO]> BcVYY;2[&mSy>i0FvˑJiP*'Я"а%w'tS$6$ =;iCv&*CXP-׹xI+)z(, E=\^bÀZZ`k# &}>t5D!ч$b& apsB=/<΋O̐$/LBN?"7jV]Qx 6F3;i3IY15}*k6w)kd< c7Qswv&e'OJنsAVz߶bX7w턅naΎ.nY/z\6N/ڍfמ nm=Yh^׏$QZ1C).X]t՝c+Zv>U4->8aDq}p䨌I6q,CLxiD")rb2޹uRݢ"l0?TǕկzY87e"[CN-n Vet˗sK:'[DHITVOrexy].#C{ހnځGG52 чf‡M{uzCb}aQ&DH7 dXlHD<$NYKU˛7VYS B@(FT:-1l.;8BNLwFkR|ϡغq?~/ˑ穉L WR:6"ڻm6#C z='"y_/"kKkf\\]Z ҆ś Ia?8 xTcEOeDRə1h27|܋/ ?lsv.8JZ/Mzvi  %\(#_> ;)d*,CVh)6*9wD/S(:9L'Q#RZTj;})Uof~m5nz#Lcd3¼-_wA] p B089a[;d:#Iq0Z)Fjc 퍄(OkްDs|-w2TP0aJ dׅ޼I  QFWhޔ`,Kױ},9V B8z*?+f\UZd;30'tzYlwxRg@Avm+=W?A|`r$~^`9D%GJ +`!0 6h%Cae]/g!e/@py('KC 1~CΓ6c/5azrh ܋ɬDR.]W kE>Q ήǕrtEXz/ ndLZkJ~+$j.>RAO \+iAƂ2_.>KTVJssIWOVj_MHŽ.v FXN?*qHy9 45 AER -F3v͝}%Jr(f"`>Q@v. ۟VbLiPOX֎V@1{S)BZMy{GH06Gvׁzml pe1!xwZf9Ӭ~9=R߃ORL5.yi* &`s+:@2&gw:r?537:9ѵ3U5NMIܜ?U߾H:Mi&[e}Nݰ\-W@n7b)gޝToYBNSklyq09--7V#/#&Xf^}{-uZЗ쓉| tbQk ݣHg7ϰ!4<>lPe8̸> ?"SAoES mdԼPӄ!H2Djfg=m!TBoDL=uˮRpS^1cވxCbYHu 1@|MM P(ؿ h hY*±lbYĹah(r4zg3`&Psy21b/(BskHB\t8f- 1[ pjA/!p=t݇Af`2K .QP O9@.FȌ.QG<9FԛӯIbUx.\?䕩5v^_2DĈKК.\Z6> p9d?#CJqxҫ.Qez&bY8n4ٟ*߇9nqHH: $G%r?'T\4=\C^#yb"O|@V:ԈQ'(pZM"_ᾸQO$Ii w.{!G7Ԋ'CU |JxymuA"{vi/3ܪ V4~{cλLLjl= si 2|t%D!,P _D!P$ߦq>(e/z=bv: [$ZBT8%f ][%+?JMuvv!uJ1Ko~l&!Q] :2X#`8 >,$ݸZݼ~R96a{Q}_S" k~ve=.pgqQ4Q&ÌBL [Y^yzAouJ[g%=s'~ MD۠9Z!Rl]}`V4XqN#w%D=<]b1|zhw9ldF)ZF;ԇ ;R-5$<D۪xWg,eor=hQ?8G}p6UAnb^9r6+r˳DujY6 HqL~Zl5<||=W +rJL?DZ)څޏ0Mc!Km"Kr֜Æ98Wؤ=ٕxآ*h- ˘_=ZeK7$ 3$[GNva"$qad{H^0>/SXKL9NEB1y+']XczwO떷yAmf)5,Ѳ{Sikřeh΁s+WSu$7m^`n=,u:Tl)JA"SmL%(_mBY5%h^&xb!^ƻot`9cz # vd?=|qʱ =+p7smFu 5Lttι%}y!22ZFAطuJ6:" &\II=d7rb4T$\"[ U±FbDresTyLl Ro*$;Gn ;=gkwBaA7sb8Z>yln[?ޡjSLQ׌QCpP{b]1i\yrZRҚꄡHt[q 1t-7G#c(wQ>uI*vꋺNVm5®)Kg#7^&+TDzeeҙ/֣(]j/|ŮtF':ޡM6?TIn0H>[>j%p4I5>*ۮf0@EJ6ZU@ =Ǟ̯7©mH +P<]|H) Sv`?%Sli`W&bbuĴaVBNٮ6~`+ mi>AIbokzrzFS1\kަqSG }JDQ: #|(:k|gM㘵_;,PgՐ#H%گIVX';,MBdH˃?uKx%]eګw]b TߢcE@?Y*DIUfhg?S׬2#%xV"hh Oqà*&nfCVUgorpEYcA(RGTd@I*?94Nfhu,QSiƛn+EQz\C#B~ISOי&ں_  Ɣ[fPzjXn9&=b@`>K'&D;sk`NXQ 7R+BZB|a*^T:SS4e"* 8D\)]m6bg%1h(<4 B@u@/\~%:?C!&FÄw*6ڧr  p{8:/+kdل". >7@ B64?#2d׽nY_ LpѶeڽ&p7ìs <ެŻϊG56FmJ6Y_Pb[C8c~;ϛ.nݡ " Vl%ebl]S%oϱ.ց].ۦ숛Vq6Յ'Ȝ*c7.gQM;тP2چ]<舽뼞)H;:\,c| n@](0_CK?gkAeM&$%\ZϮ4{{Q#pn1^eD쿛؎DܦLf%zwБ7qUeAÄ'a8Ѕ&^p,JN`=t:r‹ Ic׾|2j_YHw@ng J@@6ȉ|U v rDW"6ky#m`~6>KudJ( `tIF} aYB.e_ zG6Ø^ІA~*匿3|"j3IɌ r5[|y 9f\"TtdfVʟ\l Fo;V$ɉ$:1ۿ$.MD?sTlg#,%FMOq Z J4u7-.؟L?l_ Fd Q6?\?&񮂾?i4X.bql4H I,1G\L X2,QQ ء57䖘$ ն0?rWy4so.NvKe_yL~!` zhȹ~^΍6|>FCx-^syqEA/Oh~hOxf CCH.B9];ݭ1/4= v|a([ |]'SJ&N #K[eˮh}#|fЌnV"݌ xtR4(y) ;pfK]@qL-;*o;iREu~qK58d= /ML˳ feYM'RN]ʃ6 =u $oc Ը(ç@}%G@ I#f̲)G+"(ԍ<\<:$N3GIsp7/5fr"n P2ܑ!hI.jNs MlVwWŻmʗQF & LK_ߑKǁC|Ǟj+H wqK+LKt l]ei"v=48h_Y ] KwhB4q!{װtD2g\ )jHTXqhL8g&}*@z}Q!CNAjgA$nŒ#u9MEG]6۶ %j ͱtGzjLH:)zuqEg98vhmj#Rz;7,87ՉJkytDd~7ŝjF2h)JkKd[i2,iofm&`JQQV׃>{޺mqqsI~j;d ۨm=hYU*i>q̆$3]7fg!4}5 K3K"NN~QS(397ZܹuM8^1LGH6کD)ئgLˏDK |O3cZ_N 6T!xB];mO\F)2h%ǸVcBNХ'Y~T2l R@ ~H}lv>v<'Ѣeݘ!N{]*k>BrIlG7=PA̶[oH^Lf-Z P4çŪZD|E񘯢|+YҔxі#JGcOhQcE,FcPqSfuO=O=hv뒤uv?,{zcCkRHc"Nc  u$hlvt ˱?.G $լ$;PׁAamv?aHM!"jl`:ۋRUEpo-mC-]Qxс=6צgU㝜`H>~B/ݚ~V #{l4y6Lf^ XiĊ O(!6QB3J._ )M){],o؏|d*"uϴLŴCf/UF$5M$e;!xޣR>[7Rkd8!fkXIUX?>R Bޝm4,wTsQM^ v| ׃+HL}qoAֈ[ҬzL\Ar'Jhٮ$~G/%*"nQ&Օ ۉiޖu$_,Y,Zg#9%x|vh&&զ؎s{\*#r\\CM>R54$e^A6DIZˬ82̈P,g#/R)] }h/RYlVKjɘ6-1(3K! 8^ *PpLH+1XtF*_3[ޖ&,:5pYan%p\R}+BCA%C8ȝŒ۞|>A>HCs|9n~jϓjCQ.L >}9!N/XcHۊMK-mQa FҾXF,Pa5A 4'8]`эh]g_CC뾸T+F}!R mhk$':J3+P8.<ƁƗ*2`o@q2fM*[ޜu/./pppz{dI!WVtk5#8cCWQ5=#f<@71xSC/ i\%Y\~3fփ> ю]Ĩ/utpɷKnV=0Hr ? Vߌ04qGf?@M \CJ .OI'o]8Q^|K[+orD4팸8X^f}q9݊e[r6:\0,-VԋI>Z`oT("TilaV(~ʷ|z:VPC{Cڪ i*<*X[WyٵS>W1!ZҌDWT6@櫳GĖgԺ: 1ZBuG Ni?9/8 MҲE3zr ļuQ4V/@cBߖmʪW A&?y$)zOD&Nyޔ*=e*0Zf);W5xmEP=b=Z?fgaT7?W;H;2 0+flKYͳR /]%$@jQ"KB=>lqDye5[IJxcy֎Mlvj$]rqӑjgl%RHq<%ܱ+كJR^/-֥3k*K |@[! d:loū3xڧV 婪;Ib5N5s Ll 'xWKʌFc=s endstream endobj 47 0 obj << /FontName /UBBXIQ+LMRoman10-Regular /StemV 69 /CharSet ("nKݣw\){WL+J=TPfb]yO2RrV=6TL`E{}jLg!!\fNi}Y\bt叆?7Z{``>%) /FontFile 95 0 R /Ascent 689 /Flags 4 /XHeight 431 /Descent -194 /ItalicAngle 0 /FontBBox [-430 -290 1417 1127] /Type /FontDescriptor /CapHeight 689 >> endobj 96 0 obj << /Filter /FlateDecode /Length 20817 /Length3 0 /Length2 19669 /Length1 1809 >> stream "ёL \$(ͮzX{ yu o^,z#Zhu 6U,;s FM,$ke:7!~i}kAe{YRzP),rBD|iՆ%S10AHrEs[Cd]mدG )FXUlTc{m 3 ȇM_勖ZnOB﮹}(BtySl[uJz=VJZAch`Y1 ɣG?Qo˩d+Sdr^ȫ%5P'|y 8@"K*+ZF̐M/4t>ÎIa;E8 !ßnͼ<+ vBCD2u |M^6:)qEuo2**w?ԋYk1{E7+һpseǽXV6~l8(s\ךz\/4MP;Ƒ^}܏ q)[!Xi]坕v]ٸ/@ŨX̾r=,aG|ьY@nr8t<#Wh\^)b/}6|0x)*Kiދ:~|@bD!RzX>8ͷf-DY0ѸVeUujԼZj_\!iêshQux/֙$ (>k=;cCk@mOhyAbAמ-]-dEk`% >Z.vsyތ+-@f*TV3\y{^K TlT3#3-f@ҵ!JZhMHbIջ 2~ ϺHBmʯ ] x/;ñ? xno7R+_5߉`6~jnWj >iWOD \C-%\$fϙ#QvꎦOl9n^ Fc\~r5("T ~|[{VoYi4e`piϺ2^| 5՗{?1(gu 7JL6]|y~cF<gWq:E .]o[ "; xv$=AsM)du0xP2_5M](v]pqE 1g4)0/;} 1 )0#=-۔Y&)7Yj"\p|67GGK>b>x茏@7:A Qn$ |j()6*2I6޵:P+FӃ;3G怤o vԕ0:&s Wp9]-["T,I)!h G(ЬkR9#NzI>>uv_0c'iaLkqmϏ| Ga/yt:=mO8}CqFJOkEeE-R-"\zHƓ6 ' (fC$OEϴ`zEQP˳LJ@EZ-<(wkbثoW\GUh53(tdw,8Oz,0I gL.P(\jlvR9\q.wqTR![*pNF1#W`ƸQ2sћ R1l\ŀepجӖ511\/)n}#FnZLnh\6GK10oj'e8f}ep[La#ʂUVOg> /9nw-~`P]^_׉PrÑ:+/՛ۜ.~8ԚxHs.<84֜VMiy6HM.!F+fsj4cVvR,u190)5ЇTqzӒr%X:a=+?, 4qȁ,1au2pT=-w` l ý~x<{A*@Ny M bOmG%)}>{(eC>ⷣ@Lǂ\\xviT+%[fp+ fX:ilf&\ZO5ne՗T02Ԓ+bR@ٓqi-+ns Uiߋ&gmY}we-Ij읻Lpg䍰`C9so?|wPPVMn<haEP=RrїɵCf5m9]q9K[9.1) c۩S^Wvʽ1JwiUZ @cI:N#￯pCB19J󺏳vl?򗯑~4ED޾FLM._Dt77=y0AbiiVp`Ab,MF|$*  vp``Z /soUlz9aO*p6O0QT/懢 6$St4S#Lz)xC~3֢p#Ruj2]0.r38n/$ # q!Gnc3t^na <|r z]ރcQg h{Zp9*aJF=p5lH1ǙVx#|I.1FIg*c3ĜHZ-4u[')Nɔ?q*s8gYK6S7#GȻv-uPT9:+^s-ACf<1Cʦ~i} tsF_L-rݡQ/9Ee9Wm`ҺYAΠpOR}btv9;]3BlHqy]3U-oPafs D1keӃHQ1UM =cފ>UyG,/]C`< urS>X7s>׃^*A{6&Yo%l^. |4gvx\ҽF|TmoPG̢b;}}h0dFWQ{/=.W78u gor*@5K31?1\7cCkYkcyo  q;Qb%Ksߴk4 ͉W@k!܏ 5P \p! ڲJAgHX9Csܒ~0 k-vS$d0bXLes_Pz)\C#]JG2M1Lx!aDwb:DŽ{aQnQ6>iBRoՍ  ZOũ66:"] ]ղ3LLy P(U'.uF%&zaNP:VBM*y):M)*Ǿ"q\ϴ<+wDXi:y]/S͏~sƉO`#4< H:AH0O6q4^VP&f>Zs|j+r=_S*#:Oǡd=Sr*#.w;&6%(]j9jҥ=_وxr+yMp3TT Zy>c|sٝAǦz[zfjp%_&)( 6)7 G5y8+E{/XèVTcaG:{ի] .Rӊ8CXkAJU [kO*:z}4jv&;ɴ/q]3ny7 w`׮Fճr5IC Ag[]E3.¹C)I| Z%)5*H|ZȢm|(c/}y=V =KۥPvVΟ◨H-Ӂe @[VTE lW B^C]D?ozVxr]?|NGp9/IV s2CjD%xbxQBL=p"ؾe]?24˿+Ȩ}!"ʩo-v%P}8^dA /\]8BP EU j^LB=+ZHĪ( kE wzT2a)~Z߼=KUn"9< xQϯ @hi{8gbs*tա߾rcbZ,'悖A1Mz\sa~IJ~w$%;ța_0 tSONҰVRd ^N85S 5@CQ* 2 Q"T' q e׸!TE3P GjjfMizɔsm2gIl@NC6i)SDSH\8W/H{ "'t+ewOE.ˏK!my6/\hNkI.nt" k:~;o7)jn /- h 9R6hHZ6ȶe&]u]8ETlgFڋF53A{|}%vb/tyy<dFާ}pl-b<Nx2GmE\ nڤ&`k3{mHgHX'[#$VO|o[5H;6#R?rnU:6R MlW΀3ͺ#05 6qc |jB|x2uKVmCa*Y5Gպ7,nSP`]zr / ]MXWf$2?dyob4{b:\ys_}-]SHεo *yG&lܴ%2<͌(R$<gx- /f+xR*AsxM$E.),b5`3o~8T2M!PZ>Iٟ M^|m0N{^fj3=az߄Zas:1I|ݛ 5PpM*c@ >IS-G30F\+$N=(sj )ҁWYRpQRS8O vF޾$ªHJC ;[+;[ vſ Q{{dkI },TFG-OM urdŵcc8H~Ɓc`b)\6Nu#ڪƪ[ekd">P2lNb6 Lݧog-G J]d ~i7 FfxfvGl~a16%"\hA(%X$Lsy<)Ց=u3âgr)F5L{`3pBSx!Mcw.ա/JXbkxj6G(2@C,j@zZ, HhV(xjHsIJ0 @ .‰0uG)Tu+aW40&3aVhۈqj͹>oI?X( }K.e@$2Nm[l>gD;҃n:z"p› ~dTІJtKGAƆ{eIiySV8iuĥ#kWIh4)þJ2D5f%Pǔl]>owi>!/F"-E2QuBwmuLzD#_  M(nלbALB38_Ą&&ڵ_ ϼDQ3=ֵk y܁rk-zJlָ[=x`%S }å`56_mVU1rri( AT;hڹX /Eܦ6b&\(zr,z1.~ImKF-Qes`:jW#հI'o 0|)!-I2 T)UqWo#g Miqs\SVzX90ۄ́lU~~ ~q]sT  e^2VJ7rs7%6&%K+p/e ŲFF&6Jgu ϱsrH IWsXٛz;_,H:ATtR_VysPO?#\c'Zr:sSE#;Ys0cRM39 wKR(;|»qLM Օ-aJf#el0ńѪNB$e)/ᴒ:e&vkR՞ddO~q#FjJVMғ˷$Ց٩ǜ3;%G%r|6hv-Kv+cf-]?@D_ jjuHu=l~H*rSEE4_3*Kkp/oY5^`ma!u3/ C|~%C? ܫΜ<墜OVյJs4hf-{Y:NϤC"v ocqh]}V.͂u̢Q>w}W=}17$U%|}^KC4sv\#ջ53.*f5"m6A!G+3*6uXOL˧ 70IwH/YK=wB=zZa&oLhEr Õu|3M,Tp8$YDh78I6mGbTɂ$5>3`yA-=1]K*d%6[ x5 }#;$.n}2'Ùʺ4&IR%#-CAaGe_yJA;IT@Y>qxm$#F0'׭P35~Өd|vgn@3ޝQ4 m->h:zw3s(੯w"=oG ;]Td\z|XM6VKz.aP#^i šFn؅v{8}j0yofu~95CjՐU8&_ս G4 Z! {xA$ݚ No Ē’|&F.V MƝdp?;+`iR[itAo@yg2grnSȘܞpP4Ƥ+߶Ee]G^ N2$4p*;'Lp/N/{1Mnn84 Z1\$a Rnv0K$B"Hc,V@(fM<'r1CNȹO>f??/sn-ҵ $ΊO(:tX(*h(TAxJMF/e3xӌ]HC()=p I;7K۔]݇2Uzp20;XJ a#KrGSSx&~JĒ]f5X<4&FdM . ꂨMYL0&gûi9ՍHFn<꿏Y[PQEi0yK~{a6c`aggN/ L'EJpIUTgSyY@,C_ܽaC͐lUVc›C_/-#4q~Z W2FmK#T28hJUϳᨱxn2~ؽ8NL%Ilν}D*<Ԅނ7/x G3-J91V_j[VqKp~U8rȠOAhJkxf~g8`dPok']z {r'3&"+7-c 1W1G=>R'29? tI4aT)}\fimuIFÞQH(X^a>UYr*|Jt~H_`~ڗC~%"r$⊒/'r\L_SxRQ?loS8SHB<Sw.n;|;]~[CGeй DG!с`,mMr"TrQ  lJ`2!o6Ų#0s:KoC-EñC~hNh4yC\1O9ÊS1Zs!Q1<"oL /:^(ϥ2t@ERKp={ϙeNj<ŅM( ~ nlP?39Ҝ\ T$jcF/BSS }Xh-%}\l+b<7D5oAMAB.H[m!k4CM" Iv#0R̜EvX+ގ,MkDE@5 #rz:uBQy/̣4}0N*x'xdAIiu\uCȗKF U4?W\+xà4pG%1Ji/Kf5AS&Pz/>uL'و ~, UH혁B铆-MzXH9G cQd\&t[b GGd)4Ac1zc&C"Bt?d  ]i, Xf\ZdݓD3h# rֶ\%Щ$W+Z=.[.q;t͗IvGV|zLT3 pR .y .]|p%a&lcugil pɽ 17W1cu쫝axa#χ%aA M; -m_tɥ^*\ܴ^nfE>Վ & a3pyFQD?^$jZa ,hyxr(:]8O ӽ30>n݂8\\ G"n*a43ᑷv} QCsV Ġ| kK^+K-7`IV1#0"g_F0krvM$Y!& Wuv'74(12kˇd4֬1BT"z)!CH ^ -ِt6ȳMEvVujbUW.lvpe| @;H0mߍBm0R 7(e2~%+6W1xVݎ.JGS4n^|yF7|TT̐BtKRз$m9'ĦuKds+fE}EZUk6Ӽ:ކӧqwm)a[sww YW%tdmTl UbZ־:l_4_Ltd_9|_..ϟ)# | )=59Ō浯v񤈌]k^ajg4vjEj K~Qk5%}ѽZk:ڡ 3}/^<";FuF(xR;sk䩫_|+8fS2лg96I+zLaPr:ZMa*h߲Ÿ Ȋ 떑"a֙ y:?GYׯ ǯLl%#LQǼJCtF,l^=MJVo@'dL am9phHi4M?$X iO|UTq<5"wBs"]ɳiNLQ>T8 ,_󥣙2/lMQLSC6sx{jVXk78m@ ڵ;qZ>AM[6QC$L{9:~S3H!kI݇h0jH9bꁤw+=(-è*m1iē`uu~v0c ]q F5餝-Ck;LCI~FYDGp?<(T-i*+I$:_\XgPiq]l!^{\&h"eͨU \-nTbJZԲ 64UYJ|f =Oi_!]+4&\LwNRsj  Z%s^ z3شMwASi7x|vKڽzU"dy-a(U5Y\$)@ʑ/\g/Æ>*ܐjQԖgn&O(|9g&!G CtBns}Y6azJtܘihH`l^Q=j.K7 z_aug ʛWB=i=˫t8ZM WtȂyCj!i[z 韱RN`w&TRqf_~sY-y6S]~u}3]-zb*8}1? i ^l3/ HUF1D:ſ ^{HxĪ Sg$MN!n d^=l%6\?]D 3[ߴӟSkq 7Ww;} ZD7 j?A'Ia!E Դg!BT p{%̲=iFʽ vkusYNZ#%.ٷ~2J:A|$L}l*wzZˉX犪J+V/Q[r( !˃XeIh l)#13l:P!;aIT#vkӍ`~"l?XHl*='Hr=-ina:59j=`ĂhvzЧ"8%5|-MAd ۳ i}YCju:eNy? &X6ԍ}<]U4-= oWejJ=xA> KFm1-%8>ƗVIzI,3bd)W^ Oyh9%\jUK* + zGRUf taN!U{ af (v BS-_$ũM>1=ƽP pDr2J ζjTa{$i3ef&zGj(Lz̛{ qw&6 r­ }uYf`ĿΉ7BZtG G2߱ d.u>n4b6 >cq]tgR`U>}3NQKgiIc$Pt41OKQ, &PV\ odQ Gߚ3;Rݚ+kTn ."O<WfIx6PJW\e$X_Gw(TցIғ/Q;UE#5ӖU|hkie©6" h* MX,ӳe4[]aȮmOóvI&`|̰kLe6Y=qǦַP!F.B1C] 6!P r,™b3릎Ng4/>iQEfZ>ޤA; b\98;yΤɖ :hm?1߾ q%?knӓTFNAP2!gӢȆʊD #֋' RKр[h'9_AcxH0l\R;( jD@Y:W̔r.]dP-.b eo˗B\}@ W.˔h^) 6N-D i5(6ƧdSscQY~uvXY Z`uWJg..9+Cxg~gﺼpz63ʃIy<iii]=UI91(.:=)Te`CU&El2u [ȺJVݑ Oޱ{yG9 9RL&gکxf=MWL#*"B)gTW1?{#/, ѾVe[2ʍVP: Z{sˎ$=shڢOi8[V4* [[9g@_Inf1-<ԛ2[|۶/k].G#|#f#JA 8IDsQKv $U3G57ְmӾeJ3UCGMX3}.X?̓'1QCƿ&&6P>8}l @l@[d ?@t.a0szf7;^TV. *uLF>8Ǒx gඡmAW+Imi*6 u'Ğ8*n`?cZ: ԊG.|`A< 1U5rGO+/RC'yWcR=!2تc~xlG{[ .('@6ckH (ZPWp2mkoftL2bXV!$5&(Lgi箄-_=⼤nƶ +ĸ:Vn?yz08^O[k4DmS2X`U)LIcK ݆ۗEs@ f{M}Dqg_~0;]bq|al3 Hc}+şv0#uIn\BIa^o0'ؓWc`iپe}e ?[`B{B !6zӴ|T?J7I@ubE58r&Mʼnv̼\Bk_8LM2/*SȀP̆ҔzͮD>">~)7սJwfkj bgE`8= SBAa6Bw_ŒS'μֿzSw`h-yaA|⡍F7TMr/Q=RJPk endstream endobj 76 0 obj << /FontName /RPVHCD+LMMono10-Regular /StemV 69 /CharSet (na®stp"ier>HO") /FontFile 96 0 R /Ascent 599 /Flags 4 /XHeight 431 /Descent -222 /ItalicAngle 0 /FontBBox [-451 -316 731 1016] /Type /FontDescriptor /CapHeight 599 >> endobj 6 0 obj << /First 4 0 R /Count 5 /Type /Outlines /Last 22 0 R >> endobj 97 0 obj << /Names [(N@Dy E) 42 0 R (UF9<) 57 0 R (UF9?) 59 0 R (UF9>) 61 0 R (UF99) 63 0 R (UF98) 65 0 R] /Limits [(N@Dy E) (UF98)] >> endobj 98 0 obj << /Names [(;w[N) 73 0 R (;w[N) 74 0 R (bYF) 41 0 R (bYF) 56 0 R (bYF) 72 0 R (bYF) 83 0 R] /Limits [(;w[N) (bYF)] >> endobj 99 0 obj << /Names [(2,~) 87 0 R (2,~) 92 0 R (6=920) 58 0 R (6=923) 60 0 R (6=922) 62 0 R (6=925) 64 0 R] /Limits [(2,~) (6=925)] >> endobj 100 0 obj << /Names [(i⋠|Y) 66 0 R (i⋠c) 2 0 R (i⋠`) 9 0 R (i⋠a) 14 0 R (i⋠f) 19 0 R (i⋠g) 24 0 R] /Limits [(i⋠|Y) (i⋠g)] >> endobj 101 0 obj << /Names [(`yq\f) 29 0 R (`y\fkK]G) 33 0 R] /Limits [(`yq\f) (`y\fkK]G)] >> endobj 102 0 obj << /Limits [(`}T) (aY[CJXgCب)] /Kids [97 0 R 98 0 R 99 0 R 100 0 R 101 0 R] >> endobj 103 0 obj << /Dests 102 0 R >> endobj 104 0 obj << /Names 103 0 R /Outlines 6 0 R /OpenAction 35 0 R /Pages 36 0 R /PageMode /UseOutlines /Type /Catalog >> endobj 105 0 obj << /R 3 /P -3904 /O (:bG"hU3"עF5\f%An̥"8E) /Filter /Standard /Length 128 /V 2 /U (7h/2-l) >> endobj 106 0 obj << /Author () /Producer (lfSGm9) /Title () /Subject () /CreationDate (hԜq!/@,-f") >> endobj xref 0 107 0000000000 65535 f 0000000015 00000 n 0000000062 00000 n 0000001649 00000 n 0000000122 00000 n 0000000193 00000 n 0000108389 00000 n 0000000324 00000 n 0000000216 00000 n 0000000263 00000 n 0000004636 00000 n 0000000409 00000 n 0000000550 00000 n 0000000440 00000 n 0000000488 00000 n 0000006180 00000 n 0000000637 00000 n 0000000772 00000 n 0000000662 00000 n 0000000710 00000 n 0000008319 00000 n 0000000860 00000 n 0000001001 00000 n 0000000891 00000 n 0000000939 00000 n 0000008771 00000 n 0000001254 00000 n 0000001113 00000 n 0000001140 00000 n 0000001193 00000 n 0000001504 00000 n 0000001354 00000 n 0000001384 00000 n 0000001442 00000 n 0000001567 00000 n 0000001600 00000 n 0000002863 00000 n 0000001758 00000 n 0000001841 00000 n 0000002524 00000 n 0000002692 00000 n 0000002403 00000 n 0000002463 00000 n 0000011178 00000 n 0000057788 00000 n 0000009642 00000 n 0000010642 00000 n 0000086838 00000 n 0000009372 00000 n 0000002958 00000 n 0000003213 00000 n 0000003546 00000 n 0000003879 00000 n 0000004220 00000 n 0000004553 00000 n 0000004791 00000 n 0000005063 00000 n 0000005124 00000 n 0000005186 00000 n 0000005247 00000 n 0000005309 00000 n 0000005371 00000 n 0000005433 00000 n 0000005495 00000 n 0000005557 00000 n 0000005619 00000 n 0000005681 00000 n 0000005743 00000 n 0000005902 00000 n 0000006085 00000 n 0000006774 00000 n 0000006314 00000 n 0000006589 00000 n 0000006650 00000 n 0000006712 00000 n 0000010327 00000 n 0000108123 00000 n 0000006944 00000 n 0000007637 00000 n 0000007939 00000 n 0000007979 00000 n 0000008248 00000 n 0000008446 00000 n 0000008627 00000 n 0000008688 00000 n 0000009164 00000 n 0000008881 00000 n 0000009103 00000 n 0000009914 00000 n 0000033736 00000 n 0000009332 00000 n 0000009482 00000 n 0000009581 00000 n 0000011621 00000 n 0000033999 00000 n 0000058087 00000 n 0000087188 00000 n 0000108463 00000 n 0000108626 00000 n 0000108783 00000 n 0000108960 00000 n 0000109142 00000 n 0000109282 00000 n 0000109396 00000 n 0000109436 00000 n 0000109563 00000 n 0000109714 00000 n trailer << /Encrypt 105 0 R /Info 106 0 R /Root 104 0 R /Size 107 /ID [] >> startxref 110053 %%EOF pdf-tools-0.80/test/pdf-info.ert000066400000000000000000000222651315513274600165630ustar00rootroot00000000000000;; -*- lexical-binding: t -*- (require 'let-alist) (require 'pdf-info) (require 'ert) (ert-deftest pdf-info-open/close () (pdf-test-with-test-pdf (should-not (pdf-info-open)) (should (pdf-info-close))) (pdf-test-with-encrypted-pdf (should-error (pdf-info-open nil "Invalid Password")) (should-not (pdf-info-open nil "pdftool")))) (ert-deftest pdf-info-metadata () (pdf-test-with-test-pdf (should (cl-every (lambda (elt) (and (consp elt) (symbolp (car elt)))) (pdf-info-metadata))))) (ert-deftest pdf-info-search-string () (pdf-test-with-test-pdf (let (matches) (should (setq matches (pdf-info-search-string "PDF Tools"))) (should (= 2 (length matches))) (should (cl-every (lambda (m) (let-alist m (and (stringp .text) (cl-every 'pdf-test-relative-edges-p .edges) (= 1 .page)))) matches))))) (ert-deftest pdf-info-search-regexp () (pdf-test-with-test-pdf (let (case-fold-search matches) (should (setq matches (pdf-info-search-regexp "PDF Tools"))) (should (= 2 (length matches))) (should (cl-every (lambda (m) (let-alist m (and (stringp .text) (cl-every 'pdf-test-relative-edges-p .edges) (= 1 .page)))) matches))))) (ert-deftest pdf-info-pagelinks () (pdf-test-with-test-pdf (let ((links (pdf-info-pagelinks 3))) (should (= 2 (length links))) (should (cl-every 'pdf-test-relative-edges-p (mapcar (apply-partially 'alist-get 'edges) links))) (should (equal (mapcar (apply-partially 'alist-get 'type) links) '(goto-dest uri))) (should (equal (mapcar (apply-partially 'alist-get 'uri) links) '(nil "http://www.gnu.org"))) (should (equal (mapcar (apply-partially 'alist-get 'page) links) '(1 nil)))))) (ert-deftest pdf-info-number-of-pages () (pdf-test-with-test-pdf (should (= 6 (pdf-info-number-of-pages))))) (ert-deftest pdf-info-outline () (pdf-test-with-test-pdf (let ((outline (pdf-info-outline))) (should (= 7 (length outline))) (should (equal (mapcar (apply-partially 'alist-get 'depth) outline) '(1 1 1 1 1 2 3))) (should (cl-every (lambda (elt) (eq (alist-get 'type elt) 'goto-dest)) outline))))) (ert-deftest pdf-info-gettext () (pdf-test-with-test-pdf (should (string-match "PDF Tools\\(?:.\\|\n\\)*in memory" (pdf-info-gettext 1 '(0 0 1 1)))))) (ert-deftest pdf-info-getselection () (pdf-test-with-test-pdf (should (consp (pdf-info-getselection 1 '(0 0 1 1)))) (should (cl-every 'pdf-test-relative-edges-p (pdf-info-getselection 1 '(0 0 1 1)))))) (ert-deftest pdf-info-textregions () (pdf-test-with-test-pdf (should (consp (pdf-info-textregions 1))))) (ert-deftest pdf-info-pagesize () (pdf-test-with-test-pdf (should (cl-every (lambda (size) (and (consp size) (natnump (car size)) (natnump (cdr size)))) (list (pdf-info-pagesize 1)))))) (ert-deftest pdf-info-quit () (pdf-test-with-test-pdf (pdf-info-quit) (let (pdf-info-restart-process-p) (should-error (pdf-info-open))) (let ((pdf-info-restart-process-p t)) (pdf-info-open) (should (pdf-info-close))))) (ert-deftest pdf-info-getannots () (skip-unless (pdf-info-writable-annotations-p)) (pdf-test-with-test-pdf (when (memq 'markup-annotations (pdf-info-features)) (cl-labels ((alists-get (alists key) (mapcar (lambda (alist) (cdr (assoc key alist))) alists))) (let (annots) (should (= 5 (length (setq annots (cl-remove-if (lambda (elt) (memq (cdr (assq 'type elt)) '(file link))) (pdf-info-getannots)))))) (should (equal (alists-get annots 'page) '(2 2 2 2 2))) (should (equal (sort (copy-sequence (alists-get annots 'type)) 'string-lessp) (sort (copy-sequence '(text strike-out highlight underline squiggly)) 'string-lessp))) (should (equal (alists-get annots 'color) '("#ff0000" "#ff0000" "#ff0000" "#ff0000" "#ff0000"))) (should (cl-every 'pdf-test-relative-edges-p (alists-get annots 'edges)))))))) (ert-deftest pdf-info-getannot () (skip-unless (pdf-info-writable-annotations-p)) (pdf-test-with-test-pdf (let* ((text-annot (car (cl-remove-if-not (lambda (elt) (eq (cdr (assq 'type elt)) 'text)) (pdf-info-getannots)))) (key (cdr (assq 'id text-annot)))) (should (consp text-annot)) (should (symbolp key)) (should key) (should (equal (cl-sort text-annot 'string-lessp :key 'car) (cl-sort (pdf-info-getannot key) 'string-lessp :key 'car)))))) (ert-deftest pdf-info-addannot () (skip-unless (pdf-info-writable-annotations-p)) (pdf-test-with-test-pdf (let (annot) (should (consp (setq annot (pdf-info-addannot 1 '(0 0 1 1) 'text)))) (should (eq 1 (cdr (assq 'page annot)))) (should (eq 'text (cdr (assq 'type annot)))) (should (equal "" (cdr (assq 'contents annot))))))) (ert-deftest pdf-info-delannot () (skip-unless (pdf-info-writable-annotations-p)) (pdf-test-with-test-pdf (let ((nannots (length (pdf-info-getannots))) annots) (push (pdf-info-addannot 1 '(0 0 1 1) 'text) annots) (when (memq 'markup-annotations (pdf-info-features)) (push (pdf-info-addannot 1 '(0 0 1 1) 'squiggly '(0 0 1 1)) annots) (push (pdf-info-addannot 1 '(0 0 1 1) 'highlight '(0 0 1 1)) annots) (push (pdf-info-addannot 1 '(0 0 1 1) 'underline '(0 0 1 1)) annots) (push (pdf-info-addannot 1 '(0 0 1 1) 'strike-out '(0 0 1 1)) annots)) (dolist (a annots) (pdf-info-delannot (cdr (assq 'id a)))) (should (= nannots (length (pdf-info-getannots))))))) (ert-deftest pdf-info-mvannot () (skip-unless (pdf-info-writable-annotations-p)) (pdf-test-with-test-pdf (let ((edges '(0.25 0.25 1.0 1.0)) (id (cdr (assq 'id (car (pdf-info-getannots)))))) (pdf-info-mvannot id edges) (should (equal (cdr (assq 'edges (pdf-info-getannot id))) edges))))) (ert-deftest pdf-info-editannot () (skip-unless (pdf-info-writable-annotations-p)) (pdf-test-with-test-pdf (let ((color "#ffa500") (id (cdr (assq 'id (car (pdf-info-getannots)))))) (should (and id (symbolp id))) (pdf-info-editannot id `((color . ,color))) (should (equal (cdr (assq 'color (pdf-info-getannot id))) color))))) (ert-deftest pdf-info-save () (skip-unless (pdf-info-writable-annotations-p)) (pdf-test-with-test-pdf (dolist (id (mapcar (lambda (a) (cdr (assq 'id a))) (pdf-info-getannots))) (pdf-info-delannot id)) (let (tempfile) (unwind-protect (progn (setq tempfile (pdf-info-save)) (should (file-exists-p tempfile)) (should (= 0 (length (pdf-info-getannots nil tempfile))))) (when (file-exists-p tempfile) (delete-file tempfile)))))) (ert-deftest pdf-info-check-epdfinfo () (should (progn (pdf-info-check-epdfinfo) t)) (should-error (let (pdf-info-epdfinfo-program) (pdf-info-check-epdfinfo))) (should-error (let ((pdf-info-epdfinfo-program 42)) (pdf-info-check-epdfinfo))) (should-error (let ((pdf-info-epdfinfo-program (make-temp-name "pdf-info"))) (pdf-info-check-epdfinfo)))) ;; FIXME: Write me. ;; (ert-deftest pdf-info-getattachment-from-annot () ;; (pdf-test-with-test-pdf ;; )) ;; (ert-deftest pdf-info-getattachments () ;; (pdf-test-with-test-pdf ;; )) ;; (ert-deftest pdf-info-synctex-forward-search () ;; (pdf-test-with-test-pdf ;; )) ;; (ert-deftest pdf-info-synctex-backward-search () ;; (pdf-test-with-test-pdf ;; )) ;; (ert-deftest pdf-info-renderpage () ;; (pdf-test-with-test-pdf ;; )) ;; (ert-deftest pdf-info-renderpage-text-regions () ;; (pdf-test-with-test-pdf ;; )) ;; (ert-deftest pdf-info-renderpage-highlight () ;; (pdf-test-with-test-pdf ;; )) ;; (ert-deftest pdf-info-boundingbox () ;; (pdf-test-with-test-pdf ;; )) pdf-tools-0.80/test/pdf-sync.ert000066400000000000000000000037241315513274600166030ustar00rootroot00000000000000 ;; * ================================================================== * ;; * Tests for pdf-sync.el ;; * ================================================================== * (require 'ert) (require 'pdf-sync) (ert-deftest pdf-sync-backward--source-strip-comments () (should-not (cl-remove-if (lambda (ch) (memq ch '(?\\ ?\s ?\n))) (append (pdf-sync-backward--source-strip-comments (concat "%comment\n" " %comment\n" "\\\\%comment\n" "\\\\ %comment\n" " \\\\ %comment\n" " \\\\%comment\n")) nil))) (let ((source (concat "\\%comment\n" " \\%comment\n" "\\ %comment\n" " \\ %comment\n" " \\%comment\n"))) (should (equal (pdf-sync-backward--source-strip-comments source) source)))) (ert-deftest pdf-sync-backward--get-text-context () (pdf-test-with-test-pdf (should (= 3 (length (pdf-sync-backward--get-text-context 1 0.5 0.25)))) ;; Empty page (should (equal '(-1 0 nil) (pdf-sync-backward--get-text-context 6 0.5 0.5))))) (ert-deftest pdf-sync-backward--get-source-context () (with-temp-buffer (should-not (pdf-sync-backward--get-source-context)) (insert "\\begin{foo}\nsource\n\\end{foo}") (should (cl-every 'stringp (pdf-sync-backward--get-source-context))))) (ert-deftest pdf-sync-backward--find-position () (let ((context '(3 2 ("000" "1111" "222222" "333333" "4444" "55555"))) (prefix "000 XXXX 222222 YYYY 333") (suffix "333 4444 XXXX")) (with-temp-buffer (insert prefix suffix) (should (progn (pdf-sync-backward--find-position 1 -1 context) (eq (length prefix) (point))))))) pdf-tools-0.80/test/pdf-tools.ert000066400000000000000000000005231315513274600167610ustar00rootroot00000000000000 ;; * ================================================================== * ;; * Tests not fitting anywhere else ;; * ================================================================== * (require 'ert) (ert-deftest pdf-tools-semantic-workaround () (let (python-mode-hook) (require 'tablist) (should (null python-mode-hook)))) pdf-tools-0.80/test/pdf-util.ert000066400000000000000000000031201315513274600165720ustar00rootroot00000000000000 ;; * ================================================================== * ;; * Tests for pdf-util.el ;; * ================================================================== * (require 'ert) (ert-deftest pdf-util-seq-alignment () (let ((s1 '(?a ?b ?c)) (s2 '(?a ?b ?c ?d)) (s3 '(-1 ?a ?b ?c)) (s4 '(?e ?f ?g)) (s5 '(?A ?B ?C)) (s6 '(?b))) (should (equal '(2 . ((?a . ?a) (?b . ?b) (?c . ?c) (nil . ?d))) (pdf-util-seq-alignment s1 s2))) (should (equal '(3 . ((?a . ?a) (?b . ?b) (?c . ?c) (nil . ?d))) (pdf-util-seq-alignment s1 s2 nil 'prefix))) (should (equal '(3 . ((nil . -1) (?a . ?a) (?b . ?b) (?c . ?c))) (pdf-util-seq-alignment s1 s3 nil 'suffix))) (should (equal '(3 . ((nil . -1) (?a . ?a) (?b . ?b) (?c . ?c))) (pdf-util-seq-alignment s1 s3 nil 'infix))) (should (equal '(1 . ((nil . ?a) (?b . ?b) (nil . ?c))) (pdf-util-seq-alignment s6 s1 nil 'infix))) (should (equal '(-3 . ((?a . ?e) (?b . ?f) (?c . ?g))) (pdf-util-seq-alignment s1 s4 nil))) (should (equal '(3 . ((?a . ?e) (?b . ?f) (?c . ?g))) (pdf-util-seq-alignment s1 s4 (lambda (a b) 1)))) (should (equal '(3 . ((?A . ?a) (?B . ?b) (?C . ?c))) (pdf-util-seq-alignment s5 s1 (lambda (a b) (if (equal (downcase a) (downcase b)) 1 -1))))))) pdf-tools-0.80/test/pdf-view.ert000066400000000000000000000010441315513274600165720ustar00rootroot00000000000000;; -*- lexical-binding: t -*- (require 'pdf-view) (require 'ert) (ert-deftest pdf-view-handle-archived-file () :expected-result :failed (skip-unless (executable-find "gzip")) (let ((tramp-verbose 0) (temp (make-temp-file "pdf-test"))) (unwind-protect (progn (copy-file "test.pdf" temp t) (call-process "gzip" nil nil nil temp) (setq temp (concat temp ".gz")) (should (numberp (pdf-info-number-of-pages temp))))) (when (file-exists-p temp) (delete-file temp)))) pdf-tools-0.80/test/pdf-virtual.ert000066400000000000000000000210321315513274600173050ustar00rootroot00000000000000;; -*- lexical-binding: t -*- (unless (version< emacs-version "24.4") (require 'pdf-virtual) (require 'ert) (defvar pdf-virtual-test-document '(("test.pdf" ;; Annotations 3,4,5 (2 . (0.1805 0.2462 0.4046 0.3392)) ;; Should match first paragraph. (1 . (0.2163 0.1879 0.7848 0.22)) 4 3 5 6))) (defmacro with-pdf-virtual-test-document (var &rest body) (declare (indent 1) (debug t)) `(let ((,var (pdf-virtual-document-create pdf-virtual-test-document))) ,@body)) (defmacro with-pdf-virtual-test-buffer (&rest body) (declare (indent 0) (debug t)) `(let ((doc pdf-virtual-test-document)) (pdf-info-process-assert-running t) (with-temp-buffer (insert ";; %VPDF 1.0\n\n") (let (print-length) (pp doc (current-buffer))) (pdf-virtual-view-mode) (progn ,@body)))) (ert-deftest pdf-virtual-document-create () (let ((doc (pdf-virtual-document-create pdf-virtual-test-document))) (should (pdf-virtual-document-p doc)) (should (= (length (pdf-virtual-document-page-array doc)) 6)) (should (equal (pdf-virtual-document-file-map doc) '(("test.pdf" 1)))))) (ert-deftest pdf-virtual-document-filenames () (with-pdf-virtual-test-document doc (should (equal (pdf-virtual-document-filenames doc) '("test.pdf"))))) (ert-deftest pdf-virtual-document-pages () (with-pdf-virtual-test-document doc (should (equal '(("test.pdf" (4 . 4) nil) ("test.pdf" (3 . 3) nil) ("test.pdf" (5 . 6) nil)) (pdf-virtual-document-pages '(3 . 6) doc))))) (ert-deftest pdf-virtual-document-page () (with-pdf-virtual-test-document doc (should (equal '("test.pdf" 6 nil) (pdf-virtual-document-page 6 doc))))) (ert-deftest pdf-virtual-document-page-of () (with-pdf-virtual-test-document doc (let ((pages '(2 1 4 3 5 6))) (dotimes (i (length pages)) (should (equal (1+ i) (pdf-virtual-document-page-of "test.pdf" (nth i pages) nil doc))))))) (ert-deftest pdf-virtual-open () (with-pdf-virtual-test-buffer (should (progn (pdf-info-open) t)))) (ert-deftest pdf-virtual-close () (with-pdf-virtual-test-buffer (should (progn (pdf-info-close) t)))) (ert-deftest pdf-virtual-metadata () (with-pdf-virtual-test-buffer (should (consp (pdf-info-metadata))))) (ert-deftest pdf-virtual-search () (with-pdf-virtual-test-buffer (dolist (m (list (pdf-info-search-string "PDF" 2) (pdf-info-search-regexp "PDF" 2))) (should (= 2 (length m))) (should (equal (mapcar (apply-partially 'alist-get 'page) m) '(2 2))) (should (cl-every (lambda (elt) (cl-every 'pdf-test-relative-edges-p elt)) (mapcar (apply-partially 'alist-get 'edges) m)))))) (ert-deftest pdf-virtual-pagelinks () (with-pdf-virtual-test-buffer (let ((links (pdf-info-pagelinks 4))) (should (cl-every 'pdf-test-relative-edges-p (mapcar (apply-partially 'alist-get 'edges) links))) (should (equal (mapcar (apply-partially 'alist-get 'type) links) '(goto-dest uri))) (should (equal (mapcar (apply-partially 'alist-get 'uri) links) '(nil "http://www.gnu.org")))))) (ert-deftest pdf-virtual-number-of-pages () (with-pdf-virtual-test-buffer (should (= 6 (pdf-info-number-of-pages))))) (ert-deftest pdf-virtual-outline () (with-pdf-virtual-test-buffer (let ((outline (pdf-info-outline))) (should (= 8 (length outline))) (should (equal (mapcar (apply-partially 'alist-get 'depth) outline) '(1 2 2 2 2 2 3 4))) (should (cl-every (lambda (type) (equal type 'goto-dest)) (mapcar (apply-partially 'alist-get 'type) (cdr outline))))))) (ert-deftest pdf-virtual-gettext () (with-pdf-virtual-test-buffer (let ((text (pdf-info-gettext 2 '(0 0 1 1)))) (should (= 2 (with-temp-buffer (insert text) (count-matches "PDF" 1 (point)))))))) (ert-deftest pdf-virtual-getselection () (with-pdf-virtual-test-buffer (should (consp (pdf-info-getselection 1 '(0 0 1 1)))) (should (cl-every 'pdf-test-relative-edges-p (pdf-info-getselection 1 '(0 0 1 1)))))) (ert-deftest pdf-virtual-charlayout () (with-pdf-virtual-test-buffer (let ((cl (pdf-info-charlayout 1))) (should (eq ?3 (car (car cl)))) (should (eq ?y (car (car (last cl))))) (should (cl-every 'characterp (mapcar 'car cl))) (should (cl-every (apply-partially 'cl-every 'pdf-test-relative-edges-p) (mapcar 'cdr cl)))))) (ert-deftest pdf-virtual-pagesize () (with-pdf-virtual-test-buffer (let* ((os '(612 . 792)) (s (pdf-info-pagesize 1)) (ds (cons (* (- 0.4046 0.1879) (car os)) (* (- 0.3392 0.2462) (cdr os))))) (should (< (abs (- (car s) (car ds))) 10)) (should (< (abs (- (cdr s) (cdr ds))) 10))))) (ert-deftest pdf-virtual-getannots () (with-pdf-virtual-test-buffer (let ((a (pdf-info-getannots 1))) (should (= 3 (length a))) (should (equal (sort (copy-sequence '(highlight underline squiggly)) 'string<) (sort (mapcar (lambda (elt) (cdr (assq 'type elt))) a) 'string<)))))) (ert-deftest pdf-virtual-getannot () (with-pdf-virtual-test-buffer (let* ((a1 (car (pdf-info-getannots 1))) (a2 (pdf-info-getannot (cdr (assq 'id a1))))) (should (equal a1 a2))))) (ert-deftest pdf-virtual-addannot () (with-pdf-virtual-test-buffer (should-error (pdf-info-addannot 1 '(0 0 1 1) 'text)))) (ert-deftest pdf-virtual-delannot () (skip-unless (pdf-info-writable-annotations-p)) (with-pdf-virtual-test-buffer (should-error (pdf-info-delannot (cdr (assq 'id (car (pdf-info-getannots 1)))))))) (ert-deftest pdf-virtual-mvannot () (skip-unless (pdf-info-writable-annotations-p)) (with-pdf-virtual-test-buffer (should-error (pdf-info-mvannot (cdr (assq 'id (car (pdf-info-getannots 1)))) '(0 0 0 0))))) (ert-deftest pdf-virtual-editannot () (skip-unless (pdf-info-writable-annotations-p)) (with-pdf-virtual-test-buffer (should-error (pdf-info-editannot (cdr (assq 'id (car (pdf-info-getannots 1)))) '((color . "blue")))))) (ert-deftest pdf-virtual-save () (skip-unless (pdf-info-writable-annotations-p)) (with-pdf-virtual-test-buffer (should-error (pdf-info-save)))) (ert-deftest pdf-virtual-adapter-argument-handling () (let ((enabled-p pdf-virtual-global-minor-mode)) (unwind-protect (progn (pdf-virtual-global-minor-mode 1) (with-pdf-virtual-test-buffer (should (stringp (pdf-info-renderpage 1 100 :alpha 0.1))) (should (stringp (pdf-info-renderpage 1 100 (current-buffer) :alpha 0.2)))) (pdf-test-with-test-pdf (should (plist-get (pdf-info-setoptions :render/printed t) :render/printed)) (should-not (plist-get (pdf-info-setoptions (current-buffer) :render/printed nil) :render/printed)) (should (plist-get (pdf-info-setoptions (buffer-file-name) :render/printed t) :render/printed)))) (unless enabled-p (pdf-virtual-global-minor-mode -1))))) ;; (ert-deftest pdf-virtual-getattachment-from-annot () ;; ) ;; (ert-deftest pdf-virtual-getattachments () ;; ) ;; (ert-deftest pdf-virtual-synctex-forward-search () ;; ) ;; (ert-deftest pdf-virtual-synctex-backward-search () ;; ) ;; (ert-deftest pdf-virtual-renderpage () ;; ) ;; (ert-deftest pdf-virtual-boundingbox () ;; ) ;; (ert-deftest pdf-virtual-pagelabels () ;; ) ;; (ert-deftest pdf-virtual-setoptions () ;; ) ;; (ert-deftest pdf-virtual-getoptions () ;; ) ) pdf-tools-0.80/test/run-tests.el000066400000000000000000000060671315513274600166350ustar00rootroot00000000000000 (require 'package) (require 'ert) (require 'cl-lib) (unless (= 1 (length command-line-args-left)) (error "Missing package tar or too many arguments")) (defvar pdf-tools-package (expand-file-name (car command-line-args-left))) (unless (and (file-exists-p pdf-tools-package) (string-match "\\.tar\\'" pdf-tools-package)) (error "Invalid tar package:" pdf-tools-package)) (unless load-file-name (error "load-file-name is unset")) (cd (file-name-directory load-file-name)) (setq package-user-dir (expand-file-name "elpa" (make-temp-file "package" t))) (defvar cask-elpa (cl-labels ((directory-if-exists-p (directory) (and (file-directory-p directory) directory))) (or (directory-if-exists-p (format "../.cask/%s/elpa" emacs-version)) (directory-if-exists-p (format "../.cask/%d.%d/elpa" emacs-major-version emacs-minor-version)) (error "Do `cask install' first")))) (add-to-list 'package-directory-list cask-elpa) (add-hook 'kill-emacs-hook (lambda nil (when (file-exists-p package-user-dir) (delete-directory package-user-dir t)))) (package-initialize) (package-install-file pdf-tools-package) ;; FIXME: Move functions to new, loadable file. ;; Fake skipped as accepted failures if skip-unless is not available. (unless (fboundp 'ert--skip-unless) (defun skip-unless (arg) (unless arg (setf (ert-test-expected-result-type (car ert--running-tests)) :failed) (ert-fail (list nil))))) (defun pdf-test-relative-edges-p (edges) (and (consp edges) (cl-every (lambda (x) (and (numberp x) (<= x 1) (>= x 0))) edges))) (defmacro pdf-test-with-pdf (pdf-filename &rest body) (declare (indent 0) (debug t)) (let ((buffer (make-symbol "buffer"))) `(let ((,buffer (find-file-noselect (expand-file-name ,pdf-filename))) (pdf-info-epdfinfo-error-filename (make-temp-file "epdfinfo.log"))) (unwind-protect (progn (pdf-info-quit) (pdf-info-process-assert-running t) (with-current-buffer ,buffer ,@body)) (when (buffer-live-p ,buffer) (with-current-buffer ,buffer (set-buffer-modified-p nil) (let (kill-buffer-hook) (kill-buffer)))) (when (file-exists-p pdf-info-epdfinfo-error-filename) (with-temp-buffer (insert-file-contents pdf-info-epdfinfo-error-filename) (unless (= 1 (point-max)) (message ">>> %s <<<" (buffer-string)))) (delete-file pdf-info-epdfinfo-error-filename)) (pdf-info-quit))))) (defmacro pdf-test-with-test-pdf (&rest body) `(pdf-test-with-pdf "test.pdf" ,@body)) (defmacro pdf-test-with-encrypted-pdf (&rest body) `(pdf-test-with-pdf "encrypted.pdf" ,@body)) (dolist (file (directory-files "." t "\\.ert\\'")) (load-file file)) (ert-run-tests-batch-and-exit t) pdf-tools-0.80/test/test.pdf000066400000000000000000003075161315513274600160240ustar00rootroot00000000000000%PDF-1.5 % 33 0 obj << /Length 486 /Filter /FlateDecode >> stream xڭTM0WkJ{*K A 8j&WgxۗM̧j,J&UHVbϊKزe?fBmRJ_^BcJ^eR𽤆Oi&w2t!1!'`ƎBR-%tX}>ՍI~9 %^XJ-<~NQ+ i!x-o/i =MH J'\wQX۩l4sbwZ^qfhqBq5?O}8;o$H? 3wq^y*5]0ƍյƒVݡӅi>> ={AQyS*\tp'>btB#6"~,!Ns#ǒ">2(!2%7td)B@,Z &-^A.PR+#`%"[tu#.?+n\eG>W:1~ endstream endobj 46 0 obj << /Length 196 /Filter /FlateDecode >> stream xڍ= 0_q::lҡPC%R?ukE Ipܯ֌9*N&L$@?`@ɔPň$ýsMm/D)YH1LS YFl![g,|2ù(<־f藞y}s4%Vʚ{x{mm]ɓkkLG|]y endstream endobj 62 0 obj << /Length 199 /Filter /FlateDecode >> stream xڍj@y9n;ٙf= *OeoC6%J&^ ,sƀI2?ẙ%t HY,l䚈%oOX `pF#y +S'nrTkcEDo6MI;X ,nT(l|- 82<+nxj c/G\V endstream endobj 67 0 obj << /Type /EmbeddedFile /Params << >> /Length 582 /Filter /FlateDecode >> stream xTK0W̥B ҅B M{Y cYDW^ɉ7.{0i:˃FC\1#s$cU5 |1ff[֢:@mf my'J)Af4'3|!<|tDnu[O_`cm[}㙈񡗧yF#MIE!M\*5q&*%&f)H`i@4H#2G8fF=P]:hR(,_}9Zp4ip4^LA@sėyvJrkƣy¸X?~Wm3jM_TJaAo;",M R5yA{Q&MKiC9dhWoP@4>ZP['D8oѹ4):EW]ƇvѼ\x)^0/C89: -[ endstream endobj 68 0 obj << /Type /XObject /Subtype /Form /Subtype /Form /BBox [0 0 24 14] /FormType 1 /Matrix [1 0 0 1 0 0] /Resources 69 0 R /Length 126 /Filter /FlateDecode >> stream x==! S8IvYVakKXo&?^(rZֶ ; ЀԘra˪t1L8/>Y8 r%qƊ|Q@7u/ endstream endobj 73 0 obj << /Length 105 /Filter /FlateDecode >> stream x3P0¢t0ee@SBHBHBFIIbFFn*PX#.)֌ "N.\$o endstream endobj 77 0 obj << /Length 146 /Filter /FlateDecode >> stream xڍ1 0=vy*"H6qQmox3`YGrQjg!ށEq6hE!ThY1[-<}vA59 zY f.C{WubOe]h= endstream endobj 82 0 obj << /Length 24 /Filter /FlateDecode >> stream x3P0¢t0ee@%m endstream endobj 89 0 obj << /Length1 1795 /Length2 20856 /Length3 0 /Length 21997 /Filter /FlateDecode >> stream xڴsto>=m4m3nlhl6liK?wk`z{6;?}H 'wnH哴{ rSy}km3JN徻gc\V^[3)8fnV~vVoH>nfVϿ FAIw% `ieĪ ?{?zI;:*9YoA3T_/6{z1б{fٹx#>@+77Ͽ`Vwj())2Ǹ$Ym<3WWOHl3 aeK+!gww!]mXZyX[j?&;o;&{i:[ځ, ^𿙼V-gv9XYi[ˀ}vdP϶78ˇ `a}:7 _`eee Oo *g9֕OY͜"—ߥ)j̢)) KuCq_:Rjfn-DP&YV+{( ʸ:;IZ']0( +;7 `=K۰]1V Vf!o1fb ḓp酚&͵KGp1uDNtZ^Au8Y+5r\IC(,Yi, FMEp6$!Cp&:Dq>>tB|=dx?gLo_\KX 7 Ѯ\u%>Tv %Vf1Q}fؖ%D="?VpM+V~\s5aՁ]{`Z*M؝6/70#=V8"@[9CL0} w`_>5rj9%0ͻ:|Ӆ=KW8הGMQţ {fWB7{ ˁu/\d1գCzsr9>*GRٰŦ懪eG]͙o(5sp%s_G옲~+Ft^e6k~osL49컗;̇p1xp h+}_H^/S1+_, .]Zgלs~N{!R1qmc .ubR-QȳqqNy4i*uC׸Q !!P*BȘC_(TJDg :6U)|@zD7؍/4׌4E".D6 QQxO.M-SD_+i:+0ߧhb/b~Dd sboy8,ciK}zr ݪnMGӥܺxR S 2 tQ &y <\Q@ЎDNfЭwL8:tSGba~aN]NR{,Acitľ|p^ca~= ƁO v=yHc<4]yxK[KKOXd_ͷ3ƘtUtd}O_^ Z^WBD]cBE HfO? BJGfA4 iho;˱pYt_^C>E^bVmqtLZ7S$#%̈́h(壥1{$Cq{Ab1*RV~6^`i|Mg[GF>(ayGo,Eս&tv ;$-m% Q> nGQ]8 +Cv~H['+ކ /N9|rSTՉDjb_xSfM7<f( -(^ȟ8QIwnOg3;NJB`5~]F 6W̑$ nfRq cم?2ʐ hsaF URރBӊ(A*:K |+?)m/GWYbo4G8U ]&rONPvOZ*Ø! 7J%S$3.ɩh.Jǘ2 Bw?S1=W _7~$\ϐ:@VO]qP_Y2}: EpR1*['elN!*d;@ ON{$?>% $b/2]-D0wDo!MLtH'$0d836R !/&YzNC+ʷK1Q։ $I21?n R]b֜4EjuʺK4S22SfpoOV;'n|H (Eߌ=Śjm&S/aG̜>C05ŽUS=uXT= "&^ߣP)py `Za0cC$?5[S~bIm01791#oLiY ~yRp҉XC@iSY͗/?0 1,~ H>~w!p~s-4 g.u)I.g ~U􇋻Uۏh* M@qZΖ\%.m Frw9|:!w_ldPaTy6 kU}`%ÌՄ`E@FV":1?rqP)\W#:ro1qm_`@S-IУui7/ӢTzЩ}!5jowkd6lPhx<p5xRGakIcJJ>bȫ%Mq3X!Ud>S/upxv< lJ~>z%Z[wv%.ԙAb+nsw9IjkN͏؄gtaA3*m[t#cBV?Q0yY j)Nzy7$ؖLJi tSaTv*ҫ0f`"YB-"XH#- F)5Ґ?BQW٤tFȒj$ θD ؤ\|ez1%HNZ܋ȼl0S˼ّ!45\C3 oakrFGT2F @"9VD<6al'9-)!wUxZR+UuKM^$uILG1:,Xcb5Wq9+GVXML8i-nfsRh+4u:Rg牗Uߘ wHK?Ͼ#P@(ǖ&hT7'/~\C~rЛܸնD< }ka75c<~o,4PuCA<^) ܈gaP4%(ŊẦӬL HU<M2`\nU,S$&a[Ca+F˗:Lm[.ɒ4}/hޯv-?:w12H![b-&?a(]0VKFOJ#/!ɸUuio ݽTUяjZmGٗC ҷ_ɚ 3A c^2v^rॏk %PWSκ\!8_?}O̴Lו#Ql,Ua-u) 31O\'GRs޷+y<eW1`^<(4H=Ed`9>N~ݟ̳.DHrXFMq E\Sn̫[6r/%&7P@1v'[iyt}:as~~_O@ TSQ=-?.wiux\UrSe}=Vf5yϛR-k*w(RyX__oJ}Pc~ q{]O~TːFڦ6Hl厎|ݭ{ nQZFT %R&Ǭѭz!ΙN)a.OlQL"w~E mו&ϔCY@YtZa09O^)awOU{Zbj'16#=x-ZJۈk4ce)TTY&OҳHR0'š)B k Mqg"0N{vW-EIHつM:AKjϱ0|ÿwW PٮÉ*bT k8|\y2r yj9b_yWjuKDŽ_뤾ʔJQъWMpIй5ܶ"Q!M!mwyI}c8V.lXeڃ_Oi]jJ#8Pt㘠[qo~pQ8ͱz|Ԩx %)`Ƨ>ܷ C2K~G ̝s˰go]4KܵsML0w5AF+bџ] (p2Jƕ q5s̅1MsNu!A/0[O,j0e3HfB DT jy*(@_=PO ,u2i:}=A0ӵÛV2BN `{)Z׼aϳ_Y9SU1?\.r6V2꿩IBlU~| 7 tSܥyz( Ƽ,1<b$X97Q6B"ᳳrF,k*or6Ћ>Ƞ]ROރRw+Wi1#\Xثd71Sb)Vq-3vLHu ԉxAךsKٽb^.7hE$u{wGss,up;kgoFO]aȸ3ETdA?l 75|K&@k\8v`㹢|Kߡ qCܴP;ٯ{(gnaB-Z08-Ou!|6cxV ,HP885@c{H "rna)&YЩk<1cIs_S5d$].tt\yT "x_%Ø.ByÂxhr~lDqö i"B~j<,)bnb*RBݮkxQms<* *W`׃ڋOsEJa/qTZ7ݕ8p{:M1yT=q N'n~| 9NȐcق,%u_@(քҊ /z/aì5t?H.a 3ij/`r$Tk@MOӾ* -„<]k6ZU UgH:wRs&;dsƌ_l)@&xMkշys(-*:YgV]׺?]D撪y~̀WLA հUgG޸$H>_=y3 CR Sۇ?+B\0aȍ%P C+$Fn_)1Re_鯘D7D~ DXBäς'-{dguT FJ咖ڮcը|@Bv y!l|祡ijNID\2j7U?9'd=o%8ŇdZ)F[AKߤsO :Udɔ=|5,Fe8ܞ2N&!wO;];_??1fS} QzS˟bdƖ%rCʮՎu":N}ԇt(^*&Pgَ`&9=;3FAG/}9ڙYк*zXC%Vh)YL\|`k A^b۸R gJ ikv@ad+tuȕ%:CSN#g[tl>Ku/G Zo%lՔM 슦w] /!E_-ERC[s݆.IZ9O pxp,'Wkcx;P/.rOJƳ:_f7 ̱RYֿKϭpUH}x5ŘA!@?Ut}K)3 d!N.!NjHqg+)#Vq;26 namrJA_p>ېo3iMZS#Of9b_#M&hO p:˒sr6j&|=S⽊"u q`1Z+{@*6duCCaѳӻ\l[ɑaAYe0筛Ch9ҟ&|uޤvvº+#MAc 9.]6m+fL& ۤ # ԪA=Nw [yhxF!y>Y}j|wp{dv%E8Eh]x zvC4_y~! %3)s󜰪mM88(Tچ%عm4͵)UyT?02ܪ<ü8ĨviD!qdt di{so>eGC*jĭʩz*k-3fM).\딩򊎛 [yYe\q72&fgD'O3ќP1) rnMI߸s)_W}NꗨX(+G)ri_mh Ga2YPnN_&Ncbko|HOc7P6"\$UhM.Az)䭢it2E?aac0 Q19)NוR^>aln7BRǖN8|{^uii'a8 dS6>qjRd؟M}I|)bU3GA?|(B|?/]!g;T,fm)$KvK op/u:_!ًσvvS3qӇW@/(d;\;blň*;[˗-oyL$s_5崳1_n UuvO?,ζdOv>az0Nh'2;q$h4d21HψLE2_9gM .Z6Fa r\`7`_^.(tI'p}UC,g{/Q͚C %qai YS/XBն3щ4uY"FJN@SJvSŐ5"?{Kl2kQVjc NC\o5酏s}wثnbaӎcB1Ŭ"cʋ;۰飃:7j˲C&:X5p+&oЩ ZzlqiK;l.)q4/BdQW_NÕQ] ):&_1aQ'YFn%6#`/V^@ u`˅quJ XFR BشA pDx%(X i X}qg *n]>l4O{{'|73 ]e، d,8m|T{&s[$}Wg^D/L-.Cpr\O5Zг*meGTMͻnxbO?#$Q[':T-uoc i_?W$w6^'UŬɌQG|>F(LP$k-dfh.^UچG^jIdJEO`dS0{ռB#cmdOQU* WBW-O]2nad>qpjms=$ȯݛ Ͷ .`;_U2csK_R!%6CzUդ }8˧AMm顽Mv/9.' 'D>GcAAje^yq%aͺ5imϩ#OXC~g? ξϙxrr _QtY)gaB?&yq;M0\m6N)=oW:÷%()` p/Cv:D>.xaǎnf<\߂cb +EWs"wX"xo<-+xUMNË /!.hCO8i<Na't+mh:DrN2ފ-mk{9 ߂26xRc*P?Hw ZCjgd7Q4 yThQDj">Zisf6:` J`T㢺t{7wQ[{N.hjX$R^/pjUR4n3:ό OěO8(aҤ'vL hl)˃b0#mUo2 HB ŸDw%k T:iJǃrkǝZzZDkGݸ9QvޜQk#&"Al ƌYg@_Nӊh\ݑ_.Y`dO,x=T͝ω? |PgHX4?J?יd~2 {]eAsO$+rf B>ḑLI෇g^!&nTE5s4n42tK 3Xr,Pfe`XX[ȮPly ? "Pꗆ (٘Uy7tcG(_n bR 0cHw2}>ڤIBd%uk걻nB4 Qn@dAǀRUn "/$0>] n0+u,壱KaрrS'bDusG|ȂO]ntRAnw y:A8>^/Eڕej]qdRJK?>8׏G,8sN}1Զ^t"2UIg(lYm@.gʂ33{=AYv;.xmgLĺeH6l G'55̃?@Y}I0~eV]lxxQ׬ M4ct?gLVa2D{3W>WEFO<G&E"[j|C]yid /2T K 5Ṃ}i`uOVݧ² vMXq>L>w\6Kh{\Eg̫yCCqzߪ6=`uՌ, Ե t-Ȅ/bDVBr+Xwy,Zmی璇]j0.Kؔ,:7VIΌlAU1-WډYZdYGMO[ ^:y[#O½q_4O$1l ]$j -\.{) ,-] MNˀgCz@8 a7҅Ko]Qo6ϭB&pB֩j5_-zHc,kTG{],yNH!Mp1zE"tjwk ..cO)q;O͓  ұpnyiJvrc_ ǿۺ0ƙ`bx]e{B0AG鏐 U}>*^8zbs5/uF(2}2՘-unbУ=o1v,EуicQ B _dkV?OLSMNEmIu,cxLi5WXt8yKyˬ:#!f$ ߑJ_xm<-9 Bs>j++,rE[,Ub [ -Kix!S&T&Zi+;98Y\rn^EܗE/\]4_ \Oݣ\\,kkud/tQ7Pri@sw NJ*2l?k#wEbKp#Jl`?a.%xF~c,XWn0e9gO:QYlNK#5wP~*BKƼ[gv>Q&+)3yKR75^`W!ɼnz֦i}3 H?wҭ\E|}v3lS0V=n]6>Uuj`Gu2$N&3sAU1;G ISh?q"[Q 8xz!RiD%O*TP;l?y džR1"CN?u p / dV`5㠟DB(m!7ew3cFֽg4;q/Xyo_˽OC?׵Dh!7ydoh*R⿰-n06i[`(B{miL!\m]"d C̛ڹB%R6˻iTNu?꒛&x6] ԞWD>tw.~ۗ:MM: m}ǡv=ޭ[Q@̟Q6On5H|ѣߒ%l5N*qa:>hO`3 SY3IkHvb2Z_|]Vu]l)4Or o~ЪW@Gy[|gEmYv+QYR'O-E'V;)0.m}U%"T6!)(!k37W|0,t+DU dV[A5zlEvTO`ALPiч~t{ysƢjb4;0"ϸd:o}=4E̽oXl~jDd62xώb߬t"ь&"GA YJ}64𭬇|6RZ=_6CX,eِ7;B|g '`Kv68avkդ@,d"׽zq# -TƜ&Ah,FAfwZY9G))闓 Z87ݺ>x-K:?4㤎Sm%]D-,.Bb/O-x#`՞ֶfᢵ$} Ƕ|tǩK)ܛƯlNEHZfƶ "zMҮIi oU|ȤĚ5'nO28mRJhzD/|~w@C:ըڙQܨj]S3Bqˏ6z75vrÇx [ W6E!H p:L8L3/jNc`^JY# ٔץ8D  +6閴bpPN0iCMҀ]jPߕ3(-ttP2qG<Ԅz& -m{nnh~ýIX ӅvYz2!KduHUtoҧ?;2z(;dNuENWmĀ#>/پK5e)&x56Byg]OjJѺ_ɞWfp'72WAA0lb2?PҚ6n1`[4VIA]Jnk:J\Zۊ0qV ؗ [m/_oLS ZO`*f&ZXw>x[ BEΌk^:nيB$8ۗz!߂-Ј\VR{3z&t^%5\ݕAGWn N&> XL(ϳoR96`f4V\pJh$6Npt,ܣ8 ~D3ܘ!$k^K{P)<Rc^'`Pd`am"e)@*zFmS; g^܋v5Zu .}0srsB=Riɸ`@:V0+I4 '!z%r vRK@F(R3\y-g]-|tsݾSb1Sn  3LHl#%F[_Ta^Z`xTApLր+ u[1+̭=Л? |] ҠEb~Yn 5<3l7>L5PC7!Ɵ Nwh֋' /UAv;}P qfNFxI0S+s^WQz9QcyҬVRpU{3d+ux yP~7vq04*5M7*RZj惧`ۡԖw98.Ȳx{5YThPĥࣔ2DBЂUZ]}!tsDxDxlt<-vjՙ"U'I7,%<}2DCrq|7qp4"q$YQhbL '칽Ez8@Y\`"#,IJrlJVm6 俰5_Wޮ?EU |fL% " 'f#xu_bd0H5[=p٘2(Jd6RnmzH`Kv>7:Lւ6W:y9d `mJ]9o+r9oqlcZj2_) n| )LomOu n^w !QZ#3 \R(]Ħ/81 q=D`XxWw*36^39j=| &~9!doAG/7DP~&caRJUAAqwd+9f+w蕗9O''NnDLa[' '% uhavaERe]0dw8JVHO5ߛ5i1OCvi~(-+Ђ/OcRr*S\6]vv ~\#<&<"8׹JIeO+n-%רBL`y߾՗Ζ ok߻Ihv,,'VP~W]wFNfeh wHxyA&},F/cJmsu:7!6e%MAvʇ a]a7븘HyvGy~.6[H]ZkRoiHm'ߣćUFȻz>Ye䀂+ 3 )R?,y5)q뜡ފ;]"A"mkWFpThgc qdf%M)A-o-nߴpU$VZ9bp\iYs XVt‡ͺO kA7"QXX[`븠P >'Ozqvy+h] Wkğd1VSxe2 ojs:gwER1'[}U*> 7c[f׏:1٢Mz p,>-|R-{PTxnrZ!G㪈M:dW6W|y4/-&R=O1k} 3Ҟ׽wSX?[w|9ԓ` ˜KwR W11&?KꑹVeѭK'WOAzVh =!4?V څQOJ>MmјrzIvΚ\g+۱j7֡2cBmKQ œsL*p!dbRt t NFb !TMknWg-ݑϬmUF>8̑b15}]H#:_ndYe_#[V0мP-`q'2orMytމ%q;:[r\eLlt"깈Dgljug!:LEdEpx`19׈TK{/5$2rYw( ɿ_MI̙#9LOt$fa/CNx-Y~w2\4~GB;6꒤$gk{T?-U.vÌzq XoUɿz~{`q`]ѬN E \أGk筎|u'{lQJև}EKce#"_? [ +A֮%;C|t?}9|k '?h|:=p#ks-K~'y_ϻGH؟.oIdI}1s% .R@{oG޾LaP[:K&|G :nG:sDe5 ђc R-+NyqW .U>u}V^-6K]q'ZIRADXQ @ΗU3pQ!~-*-̶4Fl 8k2(3[6ov,'Yƒ/g70<$C=.Etҙ":rV8nqrL%?J2hèS';cGgCτzĿǗaw +Q`,eD PIq W4*ɽh?pX镍2Ky`N$ UZ]!DE b@,)dե4@Y> stream xڴ{eP۶-C5;www !hpns_QcʚcM #VT2AvtLY9e30L OF&4rى9wGG3##<@ht|W=r@g#U{ /rr36rzW-T." {GKs ?g9鏷0=@dm 03HAnBK%` 01@M @BYAMQ`{{pQQU ɫ 5?Uvi?q ˉ j)11 tt3MdW=73=ќ/~N75;t:[ud-MvN?N)mS.w7D89_' ?X9+( 5sٙ:98 "8:!*_ xwŌ\< _'f6?/*{Ɂ޳cGDe&.V{ٙlmY;I{A v 7;Yڙɺ= PJlÉFt7`N#f#~O=`fd4?ཛྷ\gG?8&M>(.egpKUS~ʿ}BMAv6S<<(w7RgB?D*l$n4Ut6WV%r6zoz!;s{E#}_:vthbmtr'[_Fbv& SK;s3;^Ll tI v w l?;Ao`P7d0qqFѻɿ#o`0t]rq{$wCǶ21ߩ YmAv3a}=dN&S?˼sv᝭ߐ]R+q0ߵVqvY5,M?a"gh>L}^{ѱW Θ?|M&_g @w Ȅ'*)Wp _S:r)cW4o(PI^M+$ ²y՚T=}m$m+K,&4N)WEBu([U=-6~$-]S䢾5Xe gMX0rSɪK._xWCse9j93y0tހ4Z;`$ aCˮgK?kuKN7( Ut< _I/0QBN_ɽσ\8Ex%mc?eGLiYI`یh-:$k M%-K(Q=Gc&sAR;b]@rz]ZU_`z]ĤB| tYzBV֯r"19h&-,#EV@@ޑhra%BG˱P=tQ|;b}9po6Bv+yY Y,NZFSFv*vUd2G5^짍bTch9 np1/0kO82D9k"k뽒!G{ .,t|_M%bw͖~{礭-+qlTo0Qr81RZ3VxU0ǕK8w]P ɚ:5W]c(gz몽`qSh=J{< x>Ijɷ%#ԗk_Fvׂ(et5|H™iFCgngZ$O+>B j6++uk{v Br#¸͹SM(7ϗ h8~q7*clx_Eg;x BA~`"^A:ް@׀eaxcI]h&F1)nPYVkv0ġfcAhl4eL#G /vX&T&[=B^([I@"䔿j"h5<Ưn2bڢ"ƁwX粣eUN,d=1<(Qo92&@3JKW,ND̞Vyq1-c0T~aơoOJQ$*ZHD}mOZ7y\m tۖ/o %Ti;>lO&nEMe>K , m~J%Z8 vgrVR.VgtA'8T^C`,e) LTyRCaUIS5Ha<0MQeʯşێֽN?i8"=rٽF[g]^"7>?Ϛ6w=ܣMAMioSFjX62>?~_=c%׼uL[$$0@8)wxO6pIw7t@vF\F 6Mukz3)Ef?GS`=-=5y }-~qiLX6 +#P sՃjEgT%޳AiӒ+L/N8߶ 1=&!> E |?Q5thK UNGilL5eE9S4T&e jx2ҥnƪfW|$(aM:艳?H-c"߀?j5YS}2y)#gQYdt/r9,g|JQ)-zΨꋄ#sЁ&݋_ȍƕe:Xgw_*=ԚF`CљufʑˠK!I ap_G(DyJ|^l A5#;NY%&ze|L~kR=`]?CN0C37 ]Dpx5ݯ9?ͥ&19%T2>ho;*zʷRc{jjm#ཻ{]Pz'|[#XQ>Kg98 2J(4*O' G^ҁ5qGQC(ODGfE2Gq6TU5ŷf Fb-j2zo>x{-sF8M3-pZ>rwe޴!#voPf0e$ÿvU²,`B|\Kc(/[_u^`S.Ud5yXP_٫O'_TVVwقh| y̵S dtGy9`$]5223}nIkxL1sւฮglL';B9a e(֩ੱX>#A͊b\B#p$`xY>{n;nXoe[EƥPd;C֣US B,~2ƒw2{}mt ܪ$fY 1 1= 8df u*r AzX<˖*$ e 5Y Sc| (7 e~h$Dqd#ƶf~/cKMx_|Cݿrwљi$"y2\"i}+3NJD7/MZ4yB.͒9Bó/ܾ(* rbPOp$4yݠ8 "] jzo&и ؓ4aIT]:*1ЀZM<(੃&5ܲG3`Mb Y>(O>7av' q6xp+gCϳlX]$QA1T-ϹӇc&XH{>£v-OJ'w5܅9"puYbƋcLJ O엉z[5)SUJi{- l9!澁[r?`!w0EÂTƤIz"k-nLKr%69]7J.JCӢva%d7Ӓ$,xK>aOz nG2=& wOZ* \:ESͪCyTTG8NkM9Uł^tbv*G0ת1"$ !6bam]`}^cЬƺ*C{YJMQxݒLmgbviw?4Ҽ`ѷZV֥/+|Šg Ҹ2Ay|my riR a*F [sZo!8(ԶMd]u_-ٍ *"wž[;w%a˚"ź!z C2J^BVB<*:C~(i1IP<p838'u{e#k܆D (HwjY =xu1ya8\4mUo`YpUT$tRJ(aX)Wrt͔{iԴGzKc?_|lax>rɔ3$CCeّ͛QcR'D?'5wBݓkw卞z'"[,v-BXx$Hk[XIOAI{9WTqph@7iqAd cvT˷]%ڨ3Zj\[kQV)#Y~Dh4DSޤBHņmfb=qٻ3C_O%9mV4]OwlQ?RhޚT_x+,l@=*ȭ^7%"3e`"P '3t[&W=^sDBk0m ɥ9P:Z-z5[3;P?|>%nS\}2)<vksl+Vs fU1BaJ~+q7{X'VlgB~x=1iIKhDgFlu?'+/GIM쟨qRBڋ/5m1 0~ Q3V(ELPdm".UWVD ,W¼gyRu{6.(`2|n!#2cd5גbTl؃#/FC$+ӕ/~!ۛ^R}Bp!>|+*XU+oT)|E?gO hsu鸓kR"u+P YMMْzzt7P=̑.“5{ Zw IG\3-,m xM~8Xs ڀ9~u3A,m36,M(zV2ϜD7g@(`@a^|aOCIa ("sMpѠ 11qAFZuH_h*ub{u>UlhI"k4e,*~ ttvuIaZ\A1Ge(Qc0-wL0mFk3U/,o"gm΀ss>];R ܸRbN$i&5Dt9ڀgvAIdYhP _,aY8~ lO!J[Q3J:~JÌu͋'Qu]smHVSܔH,Hyc}{z>-5fddJWλ m\m> JX-#! j)7JܜSnZ`rFD&q1wK(IH=eȱ(DTSR'1)(REb~WmagM >9A^?);jD9GlCa 08hF6V Cx܋&T7O k' ^ti$}9!;3N<՞sV}Oz ͗(9sy(ȢulӒ&oW(ʢ1Yr ̋%vVoҍ#X9 +DNH 1hPC;t[e jVpvЖU [[;09>B pfwš}Q6ہ "ҬTՉ;6sTGSY@atq<,[b>Pa o%lym왅? CuLl湋a*xqnyJ.vzE(v]f*lxj#r=QIDoo^da`x`)dKG%/bE}D8+t0hΠA x˔Vo*H=;-@L8A@x_y3(ȸ`[hHgXBQ $5:GVQJ$9T跉bQ3jI"EGCM+/Qq\es2c#fn-BB"ߧP׊ #1NX)roPLFAkJTgA MZzH2@Ē JD=3Ԥ"^X27|k  g}GeQ.JͺDU;]ld9z֔XJw&Tv,J.c kuhM\,eĿ6j7qXZYҮ#5aCbQqGb~E1)3^EAS-?AJ(vfGI6*gTlsFQXx;I|l=eNc ~9Rʃ\hCX4{9C$812}8xX}|ڲ;3{rF:0weEqBmOGs)r ygJg T')}7c<Tؙ6N`FbW|,0V'RM_Xah!_aM__HO[ފbמ$֜hsYc] <]ʝ 1"ي,Lo5M ~قʋjVfHE!2C&"Q3Nq㺩A>{c\k}uo$?HnʾSjԶUKpmPIu6<4G6fT_&2a#5wWz-*K6:LElc"(k{ Zu>k(K(0-ϔ<dzqL㸸Ǥ@U^ՔĹփ. usK{|Īi~VY &QG*9U;{!qkXxcD#Rk:jOf35Xwڪ5fáu0))”Mx> ih*õAԠ|;M&QYW\dXK4R(;bkuUY* 9I7ø5 E?]I lAO o왾5M3CPSEE&Wruݯe{06ٮ~n*9-1kA ]Ixx&ർ%Ia>"E?h_/ݐtکiLEpt;ҕ\,!qE>|N1(2@ Gϸvc@udЍDpk|[`Aa{ }0~ K8H4!F>滖FN<+D1aIkZ"QLomj4^ahX*^Oݭ c`y E5wm(B69Ѫ[,=auCaW{j`sYԊQ(R|weE5O&'H.xvZt>Qj6Rz݃BS% ׏B-,zABēlSw2Ȗ _.6'x֘lO-P}ތGv",Ty'^˓t׋iT9\v2{AP|,mZD,-iRhBa|gry*[/v=BA+{ h14~[.oqtL/CWǞs3h?;Áj- ZGu¤&al.! [iZD0 K7}Z~*- z` :ɿBoq>4}CpuR z FIp>? :.5YXϱpizEh&سm3Z`G1NO@|y<4grqڗmH~ʱ)~}!9hk<[)0nQ<͆eExϮAO󶖡Tbd((1Om~([Jì^ɞiv(RR23 Ikyk??EAr6AZ~Q Qɞr rs]h)/0pW%C"cNށKK6Z}]P`8@ 8ƓiqK qq:[\TojR&ejJ3Lԯ);ղ"g#p1oqT>]6^X6Iζia8')~ p^*Hg>D-"~ AٙmunE3P\}3kD+kAI ^-hʳM&K 6KGg1aJ`Ro]GHcO=0Ji]Y =7.:(|iG_* jx\P?Ndh{}PٽuHuHN_ lk.kBG N;kr|`` nc!c7WZr e0h_j]6ޡ|päˢ`,NNVkmAdq"܂tc}lZk,!QdpMjIݐ8 RMjYaV;":+of2?w$zI5ƠҀGtx=-tOuU1Í>-CȽpFog˓4 S^pI׏ YLu%,{L]iA%Fyṍl'];3})a vMa@w\+K*6BO{?Uy"rq\¹$郡kízM;᦮E5#Y'4LXw\ #B=6Zɦd8-6b9 n ߿o/)pу/Gn0LC~$ *Ri8?%d_=qXB[3oUyM-x\(d]N :3_6qAYze<;82X:w}P~=X=\*̄D]Q?_G3/n5eU&GP} s&-CE1iz_შ'QB7~ZPY۵(=vSJϒa֖+'Z¨"Y!pyxOWD]G®#UV55LǞgP%O:c1]KiZo#~Vz,&w_!pG&xBrj2e%C>AL!a9$g~B|:AYGau);NPХfAw`#ɳ!9#stX3SSK2+ ffxO]g9P‘qn\tCs3ܣnBx:=HʻfvB[V72fa+{Xh:iPf `gQ $k) (egbXuu u-by YeV`GThS7l O<^uB@!5u%DmqkpdYC<sdR^-3ܝu`-t"Fix ײG,J3u% M@K0~G97 5$?F"NZrMޮ99w)sE):w#16ͺJBؕl_ʙbA ~.k'VYK3mHy-bQ#ajvж?SYi15|8]jw~Hkr#! Iǯv\EMh3< >ZN`d(mgy[P~|d8_CVY.,8&H4zn mFZx wn s,M=@ L}mKWuh#J\eg(RU_֜H Js%Oq0t@]^`]~q5YPn9~~ӓZEZQxH /y>wP+eMX{vWAͅSczjGɧ~(Z3AE=&]LSc < m`w~ă?67+m?s(#|!H#ز6cuòc݊ UQ"5ZyA4lC_?|:۸?@6$zŬ"Gj/! ~̦o Ɛ,2\|FgΆ %!-^q"e`=)9%#@] DukbS'T1xK UTyST5k]m2y؂aSYJhoHɜf8GM."aY=.-ihOLg( ǖ-;.>c헥Bll8V`Gb!vŽTIOz3w>cŢ][taCVOY|hQV!_V &8ٚlOdvMmvζ=ir~>yZ}  _PJ@UHTp½(6"܎_ǭ.E"LFxC? Yc'v<昳4s"Ɉ]V$ϰ|_rO^O9&DZsS^+8:Dn=1 4뱑b𞿋ѥ O#| #mlL͔jx&.^P 5CSY[F|T AO -r3=FL>L=- Wh0z˙9C- s~؂S],e B8ƒTg_8fUs<6I N9e}+u.msG["$jv*ƒfF_YYE !07\+ ^b!𳉩7UoHДDPPK* p*/4k@!B=RUU*-jɄ;遈P?hךJ aldXp+'.F\z.[fmhl Qm1\<&31Xf [ЦEC2h2;Cn߅-1hlk.}F-fuj[BPB .9xJdu-3 vX"vڐ4!)n-Ļ^7eOBȅv&I[;/:~+7ɢ㬓N/x{. Y_>FBޮS!wcp~|a;هKlv mMU / Of6eiO"h3C&0^#H3IWycMi' H|M뿚<^Ԙ/7]?&&󯶨@3ϡ(k)n{D!K-/.?;I7SLX8?BR0`r bb`+\B6jQWm~ApED sD;]B@Eh+-gQ=-.nYq,Y0wo@k}]P? S0Yd'v\x[]`۾ zx&MobSBAXq+FYP _U5aڂ Kk6 Z?=Hޏd@]Ʃ?.&fbOCY%/fkl|Qݿ鸂9kOT˯+9뢣*̻9C$gi5&ZGl2;o} erlN۱W/3W!'l_Z8u  ]U0w?'F+L^rDlM@XIF˻4!e )B] 2ir{N߆3qk<#<+l*zhZK!<ߍHx GɧLGJ/oc }@o\j;XVw(mbs|lV갥U16(LXOG>-.. *K?TݎDX%ĦNqu}qv%u޷~8?;grcPA]v0ý$HF3^&>!+ڙE3"To\2w9.hQ'Fo6KƩޫO)K` _+|4`98 X syGr't$=:Pg++6ːuq :6lh/i.W;Gs(]omm| nFeRF{楜9v̀-|8xN]kHX;k&nHxī y-Yit-A$ꛢf#=5>!0n/GfU˳}Sg"lE2~+# lM1n5џa~ -h,gQ(s:!)~D1_)0O8qfz8ѓC-:@%(T(E%1,T*IS w7qwK0? tY+j}FY=Yޠm0h^3es~ӹݿt HF)FШu8ހ'J&}jBi(Jw˥>%P aYwlE# a1۹@Z_f-)zs챫ߙ22[@ tC#In^bXQ$`cɯO^qkVR}`RCWLVLeN(cK {*Ua"CCC [,B9,St@AfQg֘#KUyCL7!XYp}xdVVߎN}#jg._Ȟhp)}mz N@0fGq7bfjH+f\Zo37:rDWv,||#T jܯ"Y1D`1̅Ndߙf43f]P 4k1 _^{'OCKLbDA"0_)>iPJ\ҷ  h 4!k9Yqڪڷp:Ga[lg,fn]l{~3nm7Ms*zq);K$BѸ(Laz e ݻP^R"5msV.tQ'$1[I.`#UT-{XNZ T5˰]=ReuLziTx5rys*'߮7󥲇|G$M6qBW" ڌ5dءMJ|:#A\B*YVQ'wG:)cL(O՘uAJ"lqr8h^-Z QHLHyi)Fu rbx ='\ I4Qm_Ečq|̩G{-Vpxfdt zoW%2v=aU7H _1Q;vḍX.y5$v6()^Z;GR"f .wG</@ lFyULM:}v-XQ,E6銓W;/aqq'N{+5Ίy<5ȭGgm#8B>_ihp 6KUU6U7aQge'C#T4Dnfm\!΁3R⛖<*F}NʗU1 pNcrp-렱Dd3UnY/QHyl<-,'",#@ܝM+^_v1եl41fP +Fz' U◢ٗ++=zΝA0Lbڿ'ًtc<5+i_c)0da  s)(ߥ ?+Nowב.p?@a|9T?f 1-hkN)Kd1oKPT#%yD!P qwZs aKXclbBC#LTWq3N5L!c{XtRi'jj@p\YGV[>] 9(J! _WҰai$eI DWm!)Z}E7a/.{Q`K@@,mx̼$Ne_c+E-*Klw3>TXrIŌ5<ɼi~UdK𯟪^?=Á|T"sx VirS5#f ʹPT!32IhY?E kK!V~URg2/wG\0p>Lֵf,bD4Tv 5U{BB_)Gro 1wM=42_|ZiHRʑ[Vl RlٿVc~>!R@U'/qSo[;3alzϿRqQWQ$hJÊ{8cDb,Xj~T1LٖwGF+=gFj]ȴ%WQe1-/$? ^J>GZrK*ozy,b<0\*p̤@q:yoRqsH#.ޯl0YAtVTtz[̷3^آqn_3~1DGV\RD/,ߐi?@Zg ?Ihi pAE~ v.|֩ .k $:8;] Rq օHc8J@@~UMu 2-gH 3x=L_R&S+&`[+oR@O8=u !r-9YjN[S},aFVtJ|fo$3옻@nP uD)p{e/K^k/=[C @ =@mI|縕(!/ rW4/AZ.> Ay}ߨ?8+gLV }SXhau^&>Z lҕ>(Lsr!aF_>(}h=Rm=L7-\ h?qҋ_&n7ApP%_/(a%Pe?Ir:0q$Q6=?9}IlT_}9O9 TmT 6*ؔY/b! Lqq-; Q&H;OBVF&0\'`5Ѷ08=;r](XԻ4"4~*}(WRY-[NqV~sz26(m}?g1y<>%HQzr{٨,'óCsi;J'4=OL!i"/N@JB)h-w^"{ZW7Sz5Gn.v@X(Hc3qo%wwg=YԚāðgm؝խp:ReTAg H'g ,j(^ͨpNw;bqm?}n'¶ CXڐ$kHXh endstream endobj 93 0 obj << /Length1 2241 /Length2 27295 /Length3 0 /Length 28633 /Filter /FlateDecode >> stream xڴeX[5 A ;w¡%xpww'C}}@19޹VP*2L {0 #3/@N^dgl̠p5v223#RP9V {qc0(r"y)R@{ӛ `<,j㿀`b[XiR@NV?5T-569Xۛ @ -m sP * RQTWRa|+r.bjRq5 P =@A[ty 55m% ?3X@'g?m3R͝@v5P[LLnnn.`F_,n '۫0.for-*L?I9ޤ|Kz؛?5mp_rJJr;c+{0-l vqe{Q  ]NEAoz3wq69)@-{?/3ȃԱgSOD\ `a0-3ĭts `c0I/b0IL2|J#v߈񿈇 d7zg7zgg }v?;Y&![}ss 8[?˿p!K?"lVodo6?J[,u& 1X;h,oL V7 mȁ@Hx?~ w|ϵVsug1V;lVfo7;Y2]8,o (+-* rb`gc0Y\uٽ?w?t"./LSC|% a)xO*qdaӧ;sɀB 9i^}ߤb-`lۗĪk3ec_y_T  F %.2Cٜ|_ǎx:GNb\&V4} `i}lq2Q~(Θ,Co7.dTg {, ߎ+t#;! -T_qqçP! \eŨ>4a,=]yk HؐIydZ %J2e'2o\7t_#3 B1"J&B6(n1Y@cYAp^TI1D#>e͇J*v;I7AThsnrB^ٷj'ҺK~$x_,8}̔pKFm+`I:,VCa dSTVi\NG]bΛ8尭H&"6GN&tPs5UlMFQ)4m}X|M^y|Jxq3SxUM u^+%Xry&Z(v|W;4\dMAB/ X([+EM=q.}.{4R݅_n00mFA;UnKMW<ݴKJDxz{1VVL&lU#A}" sQW]L7ȫI{M!@V,́~c '*e!a  yDzOA KORhcef]%6p~|/a 6^Xw޻>UEͻ.[>BƌSg!"J|Q՘AsL یցP)0r>E֋:of * k9f1; wͿaCW97[Zh!"Y2s{̜WpH=:0 DNlu0>kIKIX-n=|OLK׮zoS B w nIr#K X,1qzP>5ltU1d^?3윅[cgByZO-dG*/,I ⲖjR32n`:PPg Y!]RTw>8ScW )x&qO\\? bs0IK !F"_ìƋ{=g XEpk>ƃ_8\y[7'6 w?@!nwgígy2[|\/WBݧ?ł2"^l!+y >.;Fl4}qqVQ!_B7Κ;#; Lp6ݑEhz3@ |ԍlIg>_xZ:X~W1!! o hL.lE' W9/kĽG̽Ϩoۍkv8n-N1oIu6VIWkYl<>1'=݅ KOoW}uДn!֞_sPmCnITg=.#N}0zv "4Jbf E "T+ŋh~:w*~XuYLA۟}X |` h.Cԩ7hsr4_ Mx/[`T7Y ^P#oi 'e4 DEb=<<# 1|N ~r0! /y0GHHpm~}WK5e݈QI'$?Idcx^̐RyXNn ե^^B-y-lzmӸ4wَq@,Hl/hNWC-v07D9~%x+wZ MPFJ tґݟ,c¹'ӓO2dAآQJ9! Ŀh$'4̅ƭ>!!CDe1pF'12;ilm8#_7(b@h95%m2uHop:vS֭UAe;HҎ- kô;sh"~!ȳvA&ỉt 7EH Rj MYjPUb 4{aduF_⌙վC~OQ#^ kV|óQP_45pXMc Pk t[beY2ZY4[홎ܧXN S&ږ|K,H8\ޣ1t]rƵ-ӉODݥ4+A Yir7ܑ7߼m_y7wDn^7I[" -/:;QDh!VZ8.̡լ"܎5hRX]^_42*]Z]60|L BBuY:ґmeŻ T5f'q|I􂩮"X{ܯ.` EfC>l[n0g/uu X%bĀf,Ώ%A3jd*0XCur:s5bP"`UZhu r`-704- W;.4^uCۮ :?)ׅz5a=U*T5p\AQ/DŸw8?+1QTXƴ E}`)7jƸE 4 ׇ<$`)ty/(PnN0u#JbiA|PJM][-YmK C_u)^2s4Ϻ\x+,VͣoP'4gYLΏZ|'Dˬ/%q}xk)Fx<쏉VS E';a%k CwcJ!)%Dk˯7;#ĉ)e迍^|(tb vM ~p7!i<Ѯ5M9gG>x5^IP i)ރf)7L”NomipGu8ӶZ`NyYg<Ю]w> E ?TU;O _Dn %>G#Vp.mB]~{ZQ \tx=dUUl_Ϲm?w 5a x k\;׻\7'Ł 煈-8Ak.:vq0\yW>NJ*,=ZLzn wz@5zM;Ԩ۝Qii )F)ߏo?Xadp^Ӷ"{=] 7h`UH[0C racQQy/)Dv=t{Lvʹe(%iG﹏n֯R*N$A p7MP[=d_$P>PʉgvѶEM3/s,bQ]@~gu{+p.O-*D4ۼB„M /C U m %ZU6p.yFίN 3e@i"}gpJUv݁)*l;`۾#r"j{H|ly?C5=&_\cl8IU+zFRB Si=.L`Jy!ui# ׽ywMSPt1}G®"A2:Z_8 w?iyq(5@Ρ@цCꪀJV;WQ5!گD\|&!%Si$IC2uwJBF /0's 5`l]DoQg^>4űt3I džWUP/ ;>>u6U{I(O,pB>\Ed:dŸ:B$%mQL.@m~7& 8wO\Y6x9ON (CRH5[)e(,LTG˼]fa/d,)>^ZA0n&=J/baF4%I#Vx#i:|}&=bPe~С:;?8ϞsW*l)FLǎ7Md:2~*Ԉ8r:jRY&SI}uI8ƅLe>wmJ~qe,# @0)Afkot6шCU'`~ Ǫ7梦قRw[|Cf֮/?!D}x332@;|OEl-MsK?5D!dU]=9+9=(A$ꇫ3#1j\ͥyu8E7 MΗJ(/l2&` FMܱYk5SQ.2-,WSK u,V"=&R93̈́&)3r>I1? p!k&0Qq~%q1UK_?f`SJI:ǂH.=LM##DS3RUZ706(i'~2)ـAdم_9H Y؉PRejQf䯒8G~? @NNE?j;1P٬V-7],:{wpGȱø7A dCGuv*t &EáSz$#qI9W=!&˜_~80khPÏ>~ iė3}[\P=pq+&BMny-mh=\N Rge|ʉ,l )?jIPwS?)òFzYPƲ~ j9q8:FPdƶNdިLҾFo)92(fGjH:g#r^‰ OݐET,;׺ڱJlM#qK% i'&}^3ԍ &̙g4bt#Ea=@Х=#?'<9,-.j>QEw䴘)0nU.3cO6![2#S! du &J[cdz }x2q>8gR!)MCE|+L}nV\VBӶ~)%'_0zDc ݎM) ȍ8mtǃcyJaHor[ =7XGG|)&t_FΙrl^UFk5 3K!LE*O΃M`6様Ie:ݭ:.wfL<R^jQG0]'lx}g/!R@j 6a _Pew:AGB4!Alh['w||-~w[1s-ZٶW` )$.LǦzWzU3?G*]cj۩xW秦ypz8 C=, y>ғK*YSfkۺ:e&qY*+u/Z~1ЌB-/!hkЋQ th ^B|YR_. G$3U1ex|#8#04]3?`Xg@Z/S N#.O0P=']BKT,9X8A Ս6T`zv LߕŖ 7=xf<9Fҿl~qߦO8rHS 3.nMq+by3A[AɬF9> ݘXaw?fknWߐ(]]$K~.c7ML< ~߻gM . #C 5&f3݃태:=*m:VO~BP0XXl*zq Mk)Cc%dz Qǔ_\7<\ߢ2]CF6Oke jV _K@lA/.t HAǤ_B<~%ak6#z,O_4`بb?ۡwv, kwccԘ)ʭ ͵Gg߃W"WcZ&z/n F9 n0+0+UWӎpS)G]&0rq: `^A8k ?q=U5ՓªČ ̕ {~(¤o,CJ?fC/_XpV Bkha{N>#gz/?1G'Vbp/%s.|%ھYrݚ?Xo@+'#K| -=*/S&!$V4ȶ!=bث1WS'̈7}t<4/+ ɚ|FYI Ovн7XJDB^EMf;Evyǔ <[{  /V=jU,-'mpPEi4lE&?3&w X;$áȈ؋ YqAvpn|fU.ʗeҐyE+|,HR8peI&X%M@52Asf|a&E߈qL#9}(SkB–2@CSPC !Cl25CgH 11YR rz3o-GsGg )4:xH3 I HXI~zY *͍y8bKZlY7ROm?DV-)WVZw ZMo$)qZ*ɵV$f%-R ].pgW+.+)@c/)UaLKc#M]37|jD}C*v<9$sg !Ɛ!] BN8 DMd"PĿm >BPƇLJ٢o _O''˔T0*oTDzձNJAtKbw!N̰ͤ2/e'z+<GK+\Mj~ 6i?zId9\C((%o9b /y,8zk˱j+iԉu֢w!Cp/Ydh0nI*fuhE0o蓻- yt !Ltr(ԨNKej[vPb̡=yWXKzRRCr;LCV־ء؁ڿ5L!9=mOyN f=d*ZͯGnޅYxg"~o˜~wDt[w@:ً'yci6âw$ ^loY#$%^fHE̵ObD yCC#x~%3L-pJA`n47~|N)P™uE,.yM.owLJKjyFLj7@ }^b;Q\DIw.HfݴsƑ.l(^3MfԵ&dꝗ&7BUlpueׯq* 1.4a/\(i'y<˒$V (Yׁ7 $9/V؝sXBv Bz ẂKk$~\PYt5ӶIab~*=N) gGMv{35[]?41|`B)=VЈ/S-uő=z 1\&㳳h wcSb1uO OeLNzLYy8Jgbտ#ȔJELG[xtl5(R~TnC-+ծʙM-`WPi17K: %mR 1& ?xO.=h]Xλu2(= J!UӰ9A \bVxDGZitԕ9;PgV4!,Yh\iY&4ZZ8~Xao=<eԄ^>fL3r>pYZmlXHܪlhijI|Ԇ:>eϣe2*O` e,֝}=fl4P^3CbN^boIثb̆ ?|np&JuB*(ghδњN8%eG7LK,nNq$яf~!CXv!! BC{с3u2`,JA~sPK%fONTхLu=Bz l9 e~*nz֥9:1ͭd>_6 Oh~EC%!]7Qm'{Ǽ}ּU )+Ⓚy Fy ZwAzf5.Gy Uwbiؽi~pK\F 땈O^aK sD8}TtF$M$]L-"chΧפ-3<:%zzeRu#DOP~k摓ЭV/ =N'glB ;oױ6 YîR7tPF2S5=W6qd<#kx6gX1]۩ͻjp}]Nh7܉\NВ}%GT75jL&M6DUti&:B++8IUz I6[RxWS`?;N|2JܜMO ޙW&dMt<ykgRi0<1}qv:='(xή;6V 1X1_5By We'LKʠ%vN"Z'}ou2_Ɩ S{eWfSweI.PP+3մk&= JH+U 9b9 =#JCIVps#]"KBjqyT}r03m&y`I;Od'mK5ŸZx ~ꓙ)?xTRgIAx6͚і,8Tc/;puix] REl1r ?5".wcەMj2s>q1k[:kYIAV"|NetVxeڍu== P(M+/(SLD)}Y>uHh0kT\h Lo(f3 # P89Dٷy "KwyRG/t{gl㈷z?UFK H0)t9ۺ$3dl)W؈F(G=I'(  ]/h>t 3H2X=UM“N'z)RǀzX|F`@sV?`:8ݴn"oFZ3K& R|3i~W:9R^r9F%4O N>XErONr#/a aS m|B/~Ʀ䳈in Og_ ÄAC|BVƑ,o#Zj"vM\BD 3ŠM}`d]5B+y|sQ b5;ӷN:!l7UWAYA΃4MMVG/}5TMw?re8*[ fҩ`kxf|ދˏz7ZraIsM%ڑ\ʪ_j^9&CW܆gz<9mZ~EYK:yTck1Ao<0aNl6XYB5=$v2v$sX[_`GnwP;kDV0̽C8l0Y<[F&iN-w c`8*&^1 p'\z.]Rkn(E󻚪8Βn#"rٵC$qSsŊn 7J rtm1O6h3cqmc!gQ{A@^[7TC,w?< wE˽"7-qUh [Кf,xH@}#dz/*WGRFO>ք}QgO{p5%vSlPdw1 [6Ы'Z4 e(FZbmbu t {a?9wH %#$LYlmJkOѿgFTu^p> V  WRy^(RmA,c /HD}7!#]*Phr>ˬ>5ORE㒋zōG$0 Yӌp{m=s1*4{_Y~?('|HL9Ry(`8e!Jٶ&=Ϳ3{;Y SF@ {" k-s ΐ>-VlO)' :7"yUzMF g!Qejft,g)QJ7\7cjPH' t"?3j+YeTȑb,WF_!_1?db-&=/ȉ/4 X v4 P?J iëQ2_\dJUVm5UÿrubvA`o)|n27UI'EO~2 oZgXV̆: ;|~\Vi}BS*Dw^l\lL )X|%﷉ ۍ;V~c`ѧ([9N:!ۢ,J<o"mB0̻ahwJʳqz, c~6.K1,oDH"?Mc WC:W% lEq _TU}<"qn}yn@t. Zyc$Hٟi"@F_+#2/`Тr>dOH#4떭,TϾn`)'wLJX.TKs*RM]x]le4=/ ٣}ʍ.SJF,=gt,z=/[g| ,a|j(4?,\׷]rk[IݜBL-c+WE"Mryya1HP@L'yd)2U ]f3 aNPud&_6/ K, @ x.p0޸ϊۂ*s'o ;Cy.SAA%G"vj 4P]W`ICZW<ks0wc#)JW:1@zy(~=)NR*~X iG ޑ:%P1kJ&600e|PKǬ ҾcE[esM^7ZgzڳMfxM=1׵\kkH'y[%6Gj"T7#e]9m[kH(,0mR$3iD+?j0T,(|L>k. "$eE:mͱM obe_(kAw?k5kxNhjҜg)]v{|qXQA>lj<oP`EF0f\LV_;+^).6T'sO9m/TgvU%}RBt3pMݵP8+_]ZU@KҘmg)(M!ӺtRL@Vr4Uص< J3Q xϏAH Cy#4`FMk8;$"ۏ3f!"%WsU <ʐ]$)':;&Ҥ&Q[O$tDVvh>U~-sj`—ì a~  Mg_P1sjvؗdb$SjG٬_A WC*鰚'aXQOInŹ$7ܔg(GBw u%}YީHF(CB}zT,a,HkL@KeHry"Irf7~#h*@˨,pڜˉ,J=S+yCYά  ~QmSv(T88*XRKX.TwX4?RR/>ch{@!~/STĪ=?L&itx}S7S d ;E yjޟ(ӲQ)o^7unJK \wl찌8#6  9'aIv+ھ߀j䉟-mB6R`- at| VzJΚ4Byi}S?FM8iu(D"7j0nl]LaV.V+XTVMQr;mE+s4 ʡz?}l +X9k_s()Hif߭"mo(.w4M[Zsyrc K"ֹNx+eV(C(QfD 23j\Lv Ly|:P_mWxF2R` b chR{Ӏs~>m\Bmbkcd,PK>ĥp^|2A7 rgVV5d\Hht Qsʗ_ur޼ jkX*UHG}ӿq~Z~kT4 ]gJx?iCX0C$.b/(j5bjwFm:E+׳*)V2*\.8 —Wiɩm,£*g\x-3c,~pl37,:oKm x>ս dyCz GD*#>BV_mRkv77JcDmA%O-@떼 (dAY/;m , Hxr BZweSpM%gʬSxOHe=M,Ƀ0kH ]~B=Uz[]gAa-HB˚e 2i.f^eh'8wU,A0T;6d2c".p  e!I\C7{ġ$< kOfb{< mjrAABs\[3kSNUOV1.īV)VxI9{!þ5);(!%:ssW(<ߡqr7k&~>:灨4*uo1Hu鰎f{YRiLM <G};{pQIm}CodL$DU]cCi{4k) r!_^.`ʿf(z:7pGTIl\״yYbَ{+0Mޖ/oJ=1A5 FWUxC `=i9ocAތ#^jZD _{Mx4b(ͷI`]ETSQD 8aQAy? u3e!DP{cy ??lܴE7g?y}^)b/<_N^@zk7xmHh.f@3ȌMccZI듥xORY&&X#/ [Mido5ƉA^PGBa:߬̓I6.\; QVA,fOePK0Hl2~]6p$6uY9ڋ.!B`0ش ~vӍPeM$H;Ԗ[Y]h) Y$ 90 /$:Œ`53gaҿ#zx0)@y 'Q^ Y= ~7RzVJo#6w5J슬&:/|xJ3z/EJ, v\kp4P9v-,vz:!pCA1K:B u59$e]g۝?켺47׸wԛ\pZ@{+@п(Q[Og1|81{V#uɾz8A0 J6m&qGKQ2io /eNl~|>0`qW!YČq?aR┹MݞGΟèl5gJd("'"Kl3Hw'G瓥W ɍ+nNoO> C|%E\Q8SDjagYB3dZb{$b/q% ٵ|);KG|ЍK#z&5x6ʋshlDUy]^Wf޹Ur:vᓚ8Yφ'˚qi-m?_/7!;GSV3ZYA}/\_͚aòG*rF>) Qe 7^Xlm=R76-V.%=4Ӊ P(ozKa spÖa3N-W~_r(vTdnY`KWneID0t5?ҽN"r1A\F8gC u˂VW߻{Kuܩq-~{$P؊oȑY'(%d?d5 ~n= mN{ܶ992qL+@t(4MS&E[^[̈́(SA)JڴƩg;LT>[gD -Am-l6ꉱItOVㇾ,szL _˗& [o(sF)Df".||,(P"d_a^<:vX:YQ*BTU!"沔yzvz7*/gd9@{ǽFuDyĜV%<5F+-Q#`IGb>(}eaRefL-+VJΉG+b ;J*dQB A) P)v2%ؘumV, W -kcd7Y-8^ W1F\ AI?ݿ'FӼHVRUO]`a@*d` p_FX !AX/[]mj RJ{Q׹l5g$,!'4qq \&Q@O Ž3&DhƩBѱgHzIu >Zs,BSri3`nCi"D2- "\,{ͼBtҐV >Ŷ?B"'|-$e2&(|FF*%fY~5HݶJW͛/E^>Bұ}~jĆs%;ؠIֹoʽ˗+?yhq׈̳Da_ԟžжY7v0 AU$^st/BH}ڊ v[@2f;-S5@n|dS|כ瑗hh6's1^~OknE ]B.cmiڎ~:NDˤczR:@mbRme^, j&y> 9&4f2Ck-$ S1 6{-;xJr R6~,h~9q- VDGfAo1?c2~ uEsiFV,ŶRh}|0J Dq1.D  ( èm`@_RSQxScbzKpN+vY/}V2p ?\=Jsc{LZ!f1%KlEhL \@ыqk6Id"Nĵ%^XQ*Y 6h a|LhhܰkuY`2kaLL?>EO2?1 ~\պ4gUv0:.yXGvz]V%\m+pl% :_״%<Ży2Zkg ئ)C]@+h@ GO 604zѽ'=UL^(7̕zcD/Amny̪Ɉ48:U2~ k2*i Ğf},(a?8KG ݤTE@qnfy'IAdAsmL PP]H&aqT.E yoem;OR q-fq a<'Ce@AْK;g}z0Ŷ ~`N{L$;{*R7#'xfc6F0n/CJrUB"V!1t誔 [0"Feh,^#zΏBʆ-=.F:FLThg^>⟠G 34ݥp۝Y.Nu>?e|=TH;]Ƚmy9ԏ4>(xCzAw v ٖ@[%+=7۳*ZR'zaSrkM'){1ˈCÃ\x&@|++6 !  }㦫V,Cc{50PjFǿ&Tvag_L^H6k ꓮd 6Kc- ON`TTGѡqgA¥ )aOk\6$< :LSzs, s!5 9Ua!P)t (/-*0^J t'hr⨝>Uz68{S4"[L槅$QtF ܎e9l}u7<:MjkI| '‡)czLfR@լg\4DU 2d:!چwꓛ%?7`{s'vg$IPk74Zχ׷9J­wע3ۤB] ( O5 ^̂XD^U,ofݓhmǧPY/y`kA҃ ݞ9p 5!V66[Pof'u/B& '/-9z3zGݻvgI4Fk̟=Lw@BQW'v(]Ś$Rݼ+tocB EF?VmxpݙMG H.-%najchZ,En T6׮`vKwNrj7@[%1NЇfO5PiӸ$(9KLՈY$j'.%0P͝ά0R_U ""sU 3zd0A:]w!tIJ>-(벭<.2DXɵ}f_Q|#NaG߿K C䀔#6A-Zo6Iy.υ:˿7|yqMxr8|uA/d uE:|hk꫿H,W Dd% 9۰:GSJ/kBgd`poʡ0旭=L+/)<`@"J}vzgxOcϯv˓{Չe#_9 PrYrv!yPibC0XQY&&.DyGhieG47*ǡf JY\̤E4Bc{x(vQX;pCsh5,XABgFgDy):QQVW*RGpQVFEZ}} !ytLaD0FB'4KmXO"lAIVcd@%mͣsZT{ٙ/Od4>(Vh邌|#f^I_1 MnoT {-Nݲx !miAiFdsfG>hfvig$t1wB5{0'Qj\[ T¹u͉9] aq29G=Ɯ}K"H=1 /Q$P.?ʰWf;md0 b,KxpJ~s0;uSu&^9콾6OA°AVѐWa2El4g]znzE\rܸWjŷa.]JhW+sJהʗbq0iլ{豂hM | ]a$ިԸ&a Rʶ9@GںnMIx/944E]:Lu#J-Fo,=mPjᄨ@^mk~ƌo5  ~Ah-<.GHc-)h$[=,+c3ܿcWdw6pCV;o8^nuW1v}6Bxx#mUwzS_ͦ}5NL$Y ~| 49lw9"MC:ȫx\L; *~X}3h[,x&hXQ-[ O>=kq qOͨAX=~۔2JF^af~ou:`=}RC򅼐G_Tڶğ]u̠)vӳvqHZ^ZXaՋp Mip?piUcJ/;OWQ4'ӌ/Co9~eA^8+bnXrBr/{;ܻYgleNA?6cj9;-+D=1l(x,oq!2]9]}\x蹡.}>ͮqOTV4L+7*f+,w ^1eTMs%lP}0^ʯ Y$ղ_s9oTLu w4{{ݗO~"?#rӪ'AEQPڠ@^18Y6|ɇYo3#[{]w=+{oy!lWP^y|B 1̤) K^6etMn1[@bX2f$$1a851:< Ƨ޲%xGrx~ed==~XJVNd*^URߓqq+n=pQv%ʡL݋ic|y}z=W01=@rzZ0AnCJhc<|>FsFIi x.*:n mjv3x}:d?Lu\*yG.4hh!ئ&ÜF,ֽ~>Zyg|skΙ4nsv=ύMN(4_;dfp3>\iI=/zTN ˖wrb&"[s~7q.͸5-k1k!<'I`bmGd?O4PFEW䡤)W endstream endobj 95 0 obj << /Length1 1809 /Length2 19669 /Length3 0 /Length 20817 /Filter /FlateDecode >> stream xڴuX[߶.w݋KpHŭPݡCq([}={M jxZ[C;`%)E#oӿT;9YZ26bUU@G#{[Z %@o 62 ]}e>cn /Ah [Eg@tJbrRLȤΎ#3|b `gg7;j_$<\XstpV@G [9h9],$]?:kK0`4a[nf~`ejZY_|@Ͽ}X.hhT)prXXZ!(;9ZnʦI:xXJ`j6 Hii r`s|ߓTZmߏߓ 4s|,YE `QWՖdZ7)Gs' 5`j\\Q,̎N`w3x,N(hX9,&rXlGdc3Dv9%KWſys ރ&{`Gl=,Nd o.J`W{_hqq'O&N.6;nwظ}Wγ}[{,-=-͑VBlӛf*`T '¬dtK[dS;):RؿnTM_[2S#Bf VZ&;++nOl'hKu<IJOa\Vz,`cz. "Z=`ǘϛ9|gH/=h6JωCF4W,Atn$0 ho1c\m 2SQ#zUI^M_Qs‰-cy_`8mzO?ghZ^ONoܜoy5b`96|&#= ʹ 2ҭM ?vsM"8pSwbnr*ջ`L ^D6LzwB\RIoE.π &Ǖs*_ i%-USIs:ߎ߬_+Ƽ<(~v}-.4}BT\[ZjN8Q+Rcj!!qφ; T⺦5~#"pCM7SX Jz1Fu:A&| E"+@T<SYIhU ~2@lfA -iy{ #YMRgbvV>@\ĭghgŊ q~׀$(eAg(!6­M/# u1RC}f31u3z_쥑9o"Ҍf.E4% T6 >q.F,5hD}6 /gZ[t١UVK)T۞iN$.- x2Yg̨݄kMvϯHX‰,}SE+N-9qx@ZA9^j'XmEXS ڊZ 2hNˬo.scخl֬`A29u  zd푘o[Y)c1|ӂAy;M6w>qU؞⓳JkF'`0yEuuk.:kd/ʴC%5$&z= ]O,UA@6LaҎq5Î fCap&z!V(~ͳ;71Q) K O A@ wU $h@Tˎf`}d4`Ӹ&_M\(mYw1V Cc_Nшl68SSu (AoѤBK#IGxߺ//j#^u_at5 QȖMjw#%i!W:wؖF.e`󬉹9^ /]{* {@E5_Á[fw|2=oX 2Ϡ#\|m u:Xz1cݎB$3@N(& *9ۏ6r27N^ \4X9B3lekvno(=24qkC'aL6"ؾ/%K2 $ECNqa$lHt3E׵c]jMeXeW~uuЉxQwwn]:@,2ٱ=AQ~E'IJHSHF9İ2fR2\(o+]coGyeȴZr>C$G>ԤGIuXs9Wke=,SڀZ˶'a,ZS8/QTR<2S_8 Qy-$[ۃ{5HRf곒V}D3vuߐ#m@ZlcrV>eُe̷W/j}JАROzV(SU~?O8XQ|m#I|,Udf1ҽr  CKC6ƥ kQ]Dʋ>f ǡ2UOKT =!ɳ|O-4jp̾pJ >]~eLykUlD(*=Q2zHyI}|/V')tϰe81,E[_{TԜOm)j-~{.˕5GJsջd th׺H9h6-m>eDVO]jiCK^c7 / aUτb'9A1H+jFN~6-3"K̍C=_WtU<q0 l=C Ev׋3`7b zoݼ' jm$ ϡѕ$Wu+W"54V*l}vu`,N yej5D"+̂KGQE:m0%5 S9h Dy92 {>K]3&~# 3Z=RJ揹_vJCs1 =ĚLj }T'|\H$E5d䇎UKlempJqP̛fz(( nk X^2R,ԟ3d~,@IC\=]2Qś WwTeuʄPEYkoj(Ap|]r@_6N ׻_[ޢj]H+:G-C!z~k_ 09nz7x5M] ؅ $&k|ފ7*@~ZպD@jMwqIH+CgwELu.ÓHJ!W Gm̥\| xGYnUv0SAOŤd01Ⴙљ)$k" fw]A8s5 '?j0BÉ>j>{ #^p wD`(#bBwQ#qSH<|TW7ɆH741Gi#Mz A0TxCN"J ̀ԻғQ=Tn̓+!絳^C) Szy@Cmjk`J!c~U5}Cx^O޼ \%,0B\^23gp@#zS[NG#!4W1ޏοou,81lm`u.m0ָH ;{R/j x$H8aۚRS;:b-+ nhS\snptfϣ QrDp^w_kŮwt:'9 6֒drǙ:[@9\B+?NU)#%&/s^"ڪV`\ʾIi0$zb;B<٫/,U0'y9MTʮڕy|:Fͥa0ء! !eeh<܊s-ig|^l6tysBnXY`Dx7 T=Yu|zQfCڝJ4>5Je*[6b]0%B݀/Z}ZND=_T9 FzKpþCnj4szot%.eH6;m7Dww! \F|޷ +xE*0fP+"MC@=>Lcw_QM3N(o^/gy +~A]Y;\1dIt^>MV& @'aO]N`ā,8T@Y4Melֱ8W_3C5'y7]O8x^4[wJ~RE*I 8ddNsX9e'fU~cs]0Sbo/H01,DpҳR,p~~l(SM(5x-NL63<1eY({7(8IE&^ ZTl^C9U\?}t]'b7sfܿkebUf*h/<#"E4)Id{F>涟'P|vu(qM”.%DP*6QBqX:>\KHG0[[D_m?" [ve;ۼGݦ"{2T1 l1􁏃dW$FNȚ珏ȥ$( GTJ>|RC-سP?Vk>4ǔ:o90Zz1]=]#B8´z0j#ykNxj\+'=Eg}8鶐 ”c 5%f36X=8$dE$fǟYejzf[mS\BDu[JCl^`T0Ă Bb WZʕd#*|wx/,H3`ՂꢎNTh[8buڬ h_w sX%n_}y_S n K<.31㾒C~c7.竍?9dF~ܾ= U1s^[zQÅ&޼M?H,8Imr#Vgh[ UG4I$@ gՅoeFيR$?[_KujJQ?qo~}{5,J}Sh-`cG?Jllx&;Ĩ*! ə$v9{m}87i#_C$ 3jQV!*'R{ +>O_Cv}vn_̜4X*s+w y- 2@7 ާ awe@I D{Ā% PŻ,EhB&;ľ4nܰrR'V,∛w[/exDiNGH5AI OK4}g=zfy9eoV/4Zۍm^-t1JNU e]Y)i'j/L Iy"$?R١fHð(n7PWk;3X:6|1ݧ|]LwE=B4ս:N H h֛Kϥ,T%a/ 0?JFܸOZT/$h{rkt>=P̊goU)PqbuÁBKLO 'Ƃ̫6Cd~4|eTWn l417g^ 63vTNT~X~\(G}Xɬj3j:y)E3Xޛ/zWL{ ή! \GSo}$Va1-o'xcues9UX}^I#mLǤhV ^0ܘ?k- >ck".vc8}DffGIx^XqI?)EEIz{,gqMkfcd@%!b dBUk2R~U.m>stD}5aj4.)=Ykgvq~,f2[#C+7wTD,(QP^7udĝRPu${qJgO,lN(&9 St$WcLxBA$7Ur- uZ!嗧|'j skP=FL.|8Z,gB\'M+.M}gNBe .bXMQD1@n =wC1 %4:S-P_C zyUI@m4 #BɆQ{*U(|rAE^ hF{ǎҿZu"RhNGyP__ѣ>ܔiE &)rDh2)Q6Sm*C0șK:*jhMQs'&c?x:`%>&}q۠n%=yޖ߁P<+gMFs-׵6 !\%5S@FPqTz5W+ŢZ뢗&2Y0jMP2غI zi1a]& ۟c9k 1;@{wcv{Gf%V|izVzԄOϱQ* j7:rrWR3!irܓ:om/Cj'EPFGstm8AG˱C_َg١/ S{ g-;#4>0{/`¥pj2mqW (r!FmB$iX1!IX>e׭ f[y$r> ΙB,%bc 7T@TSh"3XO ~r*X\r?Ǘ;?/,s>{|،lMlK#Ԅ_؆٣fg$,J}l[F4Xd7hl7ԛ1'S6ڬi^{)%Ocp Qn6\p$*gɑ>hƀHQXnї)n>v6+ܟP؅4nhxv-`w՟*"lP@ikі{mM&(?U-|G֊s pBҷJxL<ȟ-=6@EJmAʾJ!_pF6M(~)'IxOсFf:eKǗ.6*zXRcZaduG%oTE2tSk@>\ؘ(mD.WD]vob[Mwqdݱڷ_<Re !yp* yxL 7ytvO%FD[6AiG$Kf R@2OC A>sE ' ([Ӆefd>U(U 2${7jڱmՖK X4 t EI;A}dk2F8wmJ /JʶŸDEVi پ;qFꉏnGY?,@܉+"թ4"!sg 01׳tOy|7+BC֒MʘөFFvN,${ noL .+@A:i!-!DŽp;[q?a^mE҂Gˀ N+&Ჽ6L Íx|%qxG9>T7_М\܊X`pw넳9Q Gcʡ^M %1}Y01QGxvӶl}L!k8LeSNER`,|}_ef b!*rayɴt 滯O,u}Y/23Q?w)ec_hZwv{!:gIř791nC8?ݓX!QqnȎ0>Z|pMPps  hfUiAz 9;_pIH^P|=R0Es+1M SMes3/@IOi%I.*,jjMw٩&eTY$$S`'a m(4 O0leuoi⸥똆x|`Jd#R?c:kzvwfN-֧]$+?Vgj<7#6 @3ʉO7C3d- ev$Z{LIXY#8Ư+ Zv5憞"sF=Ǝa fjM^h^-Lj淜߭O@%4o¤h̍BRb˭~P"es}2fK.lbPOPe:c>F#H{!u`p0 V)__59FV~;;-u5* =RqsN2 pRprg6RK_lےpg> όvHyV.>=pt{Ib +ƴߺh}50u[k_څ3DVZ-E?QhK:"u['K~$UbQ<~#ڙ@! ȰB<c}ۑj\κTjs}@vVI*YhtY|*мWJl-}'6iHlRWlSVY[|W|^&WT,$^8oTm~35l惧&l3f \xs9ζwP^Oh¡ ލ2`X FU)E;)eЎMdPcQ9Q/ɣgEܖ[?m.`g)8rT&iottB;> \go+"LN0whej$ިh|mdzeu|M&7{~=}u!3Q&(\D3MwP9Ǧa7%?^%,"Pf]w j8'cŏ)>UƠV&"xA[ "}(*M79.  >Rj7Q3,a>e4bF;!QvwהYI@FNےLȺHQ`dŁG g2Qxd3<+5Ev`⍰2uԪ cPHzĕ=愡.SBwl [">md[8o|qCZAiٷ;~%A}e(?9>P'L0`Ko ץx9f-t"-]>TrvM]7G.n@i9t!h_mhãz>U\D~9=hkW61"+wsATMw9/\ 2lZ3:@w#&m<3IJ H&K&:E!Rez0rAzp?l~>.3ڼ+(rd9VǴC$rAV}*Y);6PM葵"{-E]0\w@1 VEC+(>6îV,g&r<? ͫ)&`6Pj/XIz+6S(@z=)g){y v~VﺩE5CK_u~Z\CQIh;|ҍR~xD`ɚ6rtv^N"spfڄ{EOUv}'*'{l| !8IoBlVx7ΣK5&!:W-a›DGu&rms`?[/CEτ``%`=xػ7}I!(2I)+dU~ q}+.iOnlu :i$Gd4t6oH 8Mq6+f>V{l:fZ2>U .2%b>`z^dXy9xk.;Sʱ U'_1:?L\Pepwqg9"l>,=ھ{RF,9-'2 BH`\*ɍl&pG+!4o. 3N&$ qCv3CW__i1t.~ۂ]cQ@Vo^!{1*:z5MR x#,/]{3K>28NNaZ6AgAEZ)2/]xw0s҅k59U0ҀCg`غOKp]DYQ>mڿ}}PĀ!dZCZQĕ4֥#A,Q <+SϾZd#iHtB.%ya&nv k\?ָCp$ 4/ !&]ۯ" T+~d i`VO@9aMxs #8]L)YO vJJzv<k*.,6n+}&hF3,ˤ Ӭd^nkeI_whu #\A} ({pLyG o}os2㰉o3v7f _Whwe,v$M9%>5{YN|.k%U, ψ ;bp,4 AK RsT씹*0I$$idOcpiFY]a&:tլr3k[R4Kc)5^#E/R_GB-ڼ/ O f)o97 !˫A_ȼA!<w"w& ּϊvCq_%b"%7tS`Ub Idld%aBfI#mi*gձ-%m)Odۀ ܳ&. %'H2~R;/=MBOUO#n.Qht[l 6xѭ ]3*3ܣ`a:["iS rɬ A&7xjRK5'xQ6khX֟@ze?ӆZs I!UIn}*eC95Cng ,DdB#~H83x%a@qWXSEB 2?/,X&{7T"܂vK$0Vt&-o_J"4}gxiU \sة??@hhEϟ}Jm:W0VEbHXu!GgciGB~Cрſ~/+ \uMQ2@* =7LVf݋wQ(la`s3nIGnf|+tVŊIypsb$ 2"oF:8qHk%҈r{زpoB孑wZ{5lwY1@0-:lzPgd"ᡓ?#=w@+2 V5ˤƎurڶ'ln/ IF@m;[5L .%'i+mvB\P `Obwww>|Gkh3"ݬ.̹ ,BB~(aC /m](DaDvxB4qQ+I9vߗJ\??M~ob*2#Uop/4Qtsyguf; h<%TaJXϭnjv3N-Y'Yꙡ?eFPDr⡣hːkąqj,Tgj- s4VV =dO ?r/ MViDw;{珍pBfygEY< QJ[KpZ %7ʒ%l6O4Q~, NG5zcrn}WP@HJ'_Qz[LkQsb! Qۓ0q#qX\ ۀv-N-+XH JLP79/]"R\ͨVLDA Hvy7+!T;Qӑg>aA=}QM;Xv{̱47ޥ.oIִw{HX&u Xkk#HYbLТq8 m͓/ u(^ux|Ye3&ӌcQkO^?܍#%Tp^N#-:T>#XO E,ժ.ΣCSYEfKuS!#4I-~gAdw1`D}{(ӵ0ipaQ tN>.3tCo-N(J(Tّ<"G>+TQ댐^U  hk/[K֖ :1zC~$`B3N:ojt1~N%!) vlgR>\bo\>5J[RhA#~`[|3M^OuڀU31)*1g5 rfs5  ]bEd5rA&'R{j#lg _$!}32Rgݥ(dʬ`73*əE tc辴*K̻(1+ ZGx^Sz>mx=4YKeonS$ ѬdJkhIab}u[Z%zôDiS9Խy6Wś@J6J*Yb\?A@vS0IXnz7ަq ,1s9MSEWߑ>"ibHvaA=}TkN&qMw9EñvEp?Vͻ /k v XjȖqZn\e3 7t.Zk_,|)+f-צ0v~mRmL&)C 4Ch _3_3,~Plyp!b"Sk ycu=+sv_^Ou`or)5S܇=BœK4}l|oGDiOs(!C@=D F {dbe*#I"=0gPWܯؤkCVc9g5,uZ̤ΤRhBG ziR*Mr")Do"D` 2˟[&B ~SRQ+P` Zp000r,n<1  a?0H]=1޵_'C&YS!lO8hw- yCgV\@@*6D endstream endobj 106 0 obj << /Author()/Title()/Subject()/Creator(LaTeX with hyperref package)/Producer(pdfTeX-1.40.13)/Keywords() /CreationDate (D:20150211005401+01'00') /ModDate (D:20150211005401+01'00') /Trapped /False /PTEX.Fullbanner (This is pdfTeX, Version 3.1415926-2.4-1.40.13 (TeX Live 2012/Debian) kpathsea version 6.1.0) >> endobj 2 0 obj << /Type /ObjStm /N 92 /First 706 /Length 3004 /Filter /FlateDecode >> stream xZms6_ocx:q|n]ɥUfl]eѕ&,@,N?܌De}v ,gY :&r -0fg*&TOJ&s 3iQLqTc %LS04S̀XӆYZ`siDZ҄43Ù0TnL 1  @B#3$h) 3Af 9wb7g?8C\R bZ(ejZkkAodN,shHC7LTCsꠒx #%І Zb2IoG+{PQkk2&̣(<XCu8I" Ht4@Tp'yL 5?݂\91 h}a@/!Ը[R& aa@ Up# LPu:@P:,|05 8(%` AAދ_1dMA5,Ǚg/_F"St3Jg[tͱb\m5Y55t_s}!A}%'_/_4a_r6SZű弸CA`0DVnM`k\nܲl4H".🯦=Jp8ZG'ETb Jla-@.U x)D#9tnI/--aY8ǯfTNW o5)'sarosMWLd9=,GS4c=~?`Teo[@c1f)N 0g*iZvrOzUMXG/WQqIfۋrH]rϔV$i5tʌVm7-՟~;Dlgv_Xԭ(!m3^ehvoǷńb;'2BDn>[m 3~ Тm+]]j6ݍ3"7 Zm 3-y&4rlNi>)3ZZh7Jۉ˻!}V("_IZɌջunqu`{LfՎ٧L]'c23_iryb۴@ǥ'rWu4)RєHNbCNub^g;.J8'/+UB ?|"<}U=~O,+'wP}/l^KaUH2< ~Kԭ>:7ufd GEsEfEx^zZ$Ft2g+eu||1_ njOHTjSԒ wo}5Et%N_Ne/[Z.7/dkZzi1BKQBF#D*ak`nAn* `WeZ:B|銣Bdth)![#@5 55j6㴍#tCB|ʂM`2|Iʮiu?}{v~Y>"?8*G L'ʣ@`BU }c:hC@?)⳴X ?p|7*XCU<?t^ >c^r\b2,oW|!RO^/۔vGc.*ZJ{Q(J3#U@W>밨'EO%m\x.oޟrn6O6@r5z&@?62J9 Ω@]Ϡ9(Pn_*|s?KH //޽yu q$FCS^F]Vχ1AF ?e?u6z ?~,?&5sG}9DCN! CL9`ᧈ Ya_#Hw,I! t-S?.;wɲ7l:E T7vO_ 5>}5X$ҭ:ӮUtVei3IVDP+ 5?"XaKmX,6M"ޢvn Y4+! #L EX=urUĥor\uH2HB}%mN?>U+AU򮗢<60Beի8/o vZ4^"a`=E endstream endobj 107 0 obj << /Type /XRef /Index [0 108] /Size 108 /W [1 3 1] /Root 105 0 R /Info 106 0 R /ID [<8D7DCD16A87468DB7B60C517B5358BF9> <8D7DCD16A87468DB7B60C517B5358BF9>] /Length 252 /Filter /FlateDecode >> stream xGRQ?(Aň(sV0'%XV9\KrnQnW$R H)z!Ca.!QX AV Ih5)_k8#8vN4d|A7B B`F`r03o&a M"Eo00mz1 `m`LPM؂mةȮӞӾY!ppUx'1|V7m endstream endobj startxref 101714 %%EOF pdf-tools-0.80/test/test.tex000066400000000000000000000031041315513274600160350ustar00rootroot00000000000000\documentclass{article} \usepackage[ngerman]{babel} \usepackage[utf8]{inputenc} \usepackage[T1]{fontenc} \usepackage{lmodern} \usepackage{amsmath,amssymb,amsfonts,amstext,amsthm} \usepackage[rgb]{xcolor} \usepackage[author={PDF Tools}]{pdfcomment} \usepackage{attachfile} \pagestyle{empty} \begin{document} \section{Text} \label{sec:text} PDF Tools is, among other things, a replacement of DocView for PDF files. The key difference is, that pages are not prerendered by e.g. ghostscript and stored in the file-system, but rather created on-demand and stored in memory. PDF Tools is, among other things, a replacement of DocView for PDF files. The key difference is, that pages are not prerendered by e.g. ghostscript and stored in the file-system, but rather created on-demand and stored in memory. \newpage \section{Annotations} \label{sec:annotations} \begin{enumerate} \item \pdfcomment[color=red,icon=Insert]{insert} \\ \item \pdfmarkupcomment[markup=StrikeOut, color=red]{strikeout} \\ \item \pdfmarkupcomment[markup=Highlight, color=red]{highlight} \\ \item \pdfmarkupcomment[markup=Underline, color=red]{underline} \\ \item \pdfmarkupcomment[markup=Squiggly, color=red]{squiggly} \\ \end{enumerate} \newpage \section{Links} \label{sec:links} \begin{enumerate} \item Internal link: \ref{sec:text} \item External link: \url{http://www.gnu.org} \end{enumerate} \newpage \section{Attachments} \label{sec:attachments} \attachfile{test.tex} \newpage \section{Outline} \label{sec:outline} \subsection{subsection} \subsubsection{subsubsection} \newpage \indent % empty page \end{document}