pax_global_header 0000666 0000000 0000000 00000000064 13407234246 0014517 g ustar 00root root 0000000 0000000 52 comment=af1a5949c2dae59ffcbcf21cc4299fa2fc57ce72
pdf-tools-0.90/ 0000775 0000000 0000000 00000000000 13407234246 0013356 5 ustar 00root root 0000000 0000000 pdf-tools-0.90/.ert-runner 0000664 0000000 0000000 00000000030 13407234246 0015451 0 ustar 00root root 0000000 0000000 --reporter ert+duration
pdf-tools-0.90/.gitignore 0000664 0000000 0000000 00000000131 13407234246 0015341 0 ustar 00root root 0000000 0000000 aux/
dist/
.cask/
pdf-tools-*.tar
pdf-tools-*/
pdf-tools-readme.txt
pdf-tools-0.90.entry
pdf-tools-0.90/.travis.yml 0000664 0000000 0000000 00000001256 13407234246 0015473 0 ustar 00root root 0000000 0000000 language: generic
sudo: false
script:
- emacs --version
- make test
env:
matrix:
- EVM_EMACS=emacs-24.3-travis
- EVM_EMACS=emacs-24.4-travis
- EVM_EMACS=emacs-24.5-travis
- EVM_EMACS=emacs-git-snapshot-travis
before_install:
- curl -fsSkL https://gist.github.com/rejeep/ebcd57c3af83b049833b/raw > x.sh && source ./x.sh || true
- sed -i 's/"https:/"http:/' ~/.cask/*.el
- cask upgrade-cask || true
- evm install $EVM_EMACS --use --skip
- cask
addons:
apt:
packages:
- gcc
- g++
- make
- automake
- autoconf
- libpng-dev
- libz-dev
- libpoppler-glib-dev
- libpoppler-private-dev
notifications:
email: false
pdf-tools-0.90/COPYING 0000664 0000000 0000000 00000104513 13407234246 0014415 0 ustar 00root root 0000000 0000000 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.90/COPYING.SYNCTEX 0000664 0000000 0000000 00000002377 13407234246 0015556 0 ustar 00root root 0000000 0000000 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.
pdf-tools-0.90/Cask 0000664 0000000 0000000 00000000422 13407234246 0014160 0 ustar 00root root 0000000 0000000 (source gnu)
(source melpa)
(package-file "lisp/pdf-tools.el")
(files "lisp/*.el"
"README"
"server/epdfinfo"
"server/epdfinfo.exe")
(development
(depends-on "let-alist")
(depends-on "tablist")
(depends-on "ert-runner")
(depends-on "undercover"))
pdf-tools-0.90/Makefile 0000664 0000000 0000000 00000004442 13407234246 0015022 0 ustar 00root root 0000000 0000000
EMACS ?= emacs
# Handle the mess when inside Emacs.
unexport INSIDE_EMACS #cask not like this.
ifeq ($(EMACS), t)
EMACS = emacs
endif
emacs = $(EMACS)
emacs_version = $(shell $(emacs) --batch --eval \
'(princ (format "%s.%s" emacs-major-version emacs-minor-version))')
$(info Using Emacs $(emacs_version))
version=$(shell sed -ne 's/^;\+ *Version: *\([0-9.]\)/\1/p' lisp/pdf-tools.el)
pkgname=pdf-tools-$(version)
pkgfile=$(pkgname).tar
.PHONY: all clean distclean bytecompile test check melpa cask-install
all: $(pkgfile)
# Create a elpa package including the server
$(pkgfile): .cask/$(emacs_version) server/epdfinfo lisp/*.el
cask package .
# Compile the Lisp sources
bytecompile: .cask/$(emacs_version)
cask exec $(emacs) --batch -L lisp -f batch-byte-compile lisp/*.el
# Run ERT tests
test: all
PACKAGE_TAR=$(pkgfile) cask exec ert-runner
check: test
# Run the autobuild script tests in docker
test-autobuild: server-test
# Run all tests
test-all: test test-autobuild
# Init cask
.cask/$(emacs_version):
cask install
# Run the autobuild script (installing depends and compiling)
autobuild:
cd server && ./autobuild
# Soon to be obsolete targets
melpa-build: autobuild
cp build/epdfinfo .
install-server-deps: ;
# Create a package like melpa would.
melpa-package: $(pkgfile)
cp $(pkgfile) $(pkgname)-melpa.tar
tar -u --transform='s/server/$(pkgname)\/build\/server/' \
-f $(pkgname)-melpa.tar \
$$(git ls-files server)
tar -u --transform='s/Makefile/$(pkgname)\/build\/Makefile/' \
-f $(pkgname)-melpa.tar \
Makefile
tar -u --transform='s/README\.org/$(pkgname)\/README/' \
-f $(pkgname)-melpa.tar \
README.org
-tar --delete $(pkgname)/epdfinfo \
-f $(pkgname)-melpa.tar
# Various clean targets
clean: server-clean
rm -f -- $(pkgfile)
rm -f -- lisp/*.elc
rm -f -- pdf-tools-readme.txt
rm -f -- pdf-tools-$(version).entry
distclean: clean server-distclean
rm -rf -- .cask
# Server targets
server/epdfinfo: server/Makefile server/*.[ch]
$(MAKE) -C server
server/Makefile: server/configure
cd server && ./configure -q
server/configure: server/configure.ac
cd server && ./autogen.sh
server-test: server/Makefile
$(MAKE) -C server check
server-clean:
! [ -f server/Makefile ] || $(MAKE) -C server clean
server-distclean:
! [ -f server/Makefile ] || $(MAKE) -C server distclean
pdf-tools-0.90/NEWS 0000664 0000000 0000000 00000013276 13407234246 0014066 0 ustar 00root root 0000000 0000000 -*- org -*-
* Version 1.0
* Version 0.90
u** The displayed columns when listing annotations is now customizable
See variable pdf-annot-list-format and
pdf-annot-list-highlight-type.
** Improved handling of default annotation properties
A new variable pdf-annot-default-annotation-properties was
introduced, subsuming and obsoleting
pdf-annot-default-text-annotation-properties and
pdf-annot-default-markup-annotation-properties. The new variable
let's the user choose default properties, e.g. a color, for all
supported annotations separately.
** Provide a faster "boot-loader"
The autoloaded function pdf-loader-install acts as a replacement
for pdf-tools-install and makes Emacs load and use PDF Tools as
soon as a PDF file is opened, but not sooner.
** Improved the process of (re)compiling the server
This obsoletes the variable pdf-tools-handle-upgrades, which does
nothing anymore.
* 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.90/README 0000777 0000000 0000000 00000000000 13407234246 0015677 2README.org ustar 00root root 0000000 0000000 pdf-tools-0.90/README.org 0000664 0000000 0000000 00000040441 13407234246 0015027 0 ustar 00root root 0000000 0000000 #+TITLE: PDF Tools README
#+AUTHOR: Andreas Politz
#+EMAIL: politza@fh-trier.de
[[https://travis-ci.org/politza/pdf-tools.svg?branch%3Dmaster][https://travis-ci.org/politza/pdf-tools.svg?branch=master]]
[[http://stable.melpa.org/#/pdf-tools][http://stable.melpa.org/packages/pdf-tools-badge.svg]]
[[http://melpa.org/#/pdf-tools][http://melpa.org/packages/pdf-tools-badge.svg]]
** 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 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 were 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 its 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.
**** FreeBSD
Although not officially supported, it has been reported that
pdf-tools work well on FreeBSD. Instead of building pdf-tools, you
can install one of the OS packages with, e.g.
#+BEGIN_SRC sh
$ pkg install pdf-tools-emacs25
#+END_SRC
To see the current list of pdf-tools packages for FreeBSD visit
[[https://repology.org/metapackages/?search=pdf-tools&inrepo=freebsd][the Repology list]].
To build pdf-tools from either melpa or directly from the source
repository, 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 Alpine Linux
#+BEGIN_SRC sh
$ apk add build-base g++ gcc automake autoconf libpng-dev glib-dev poppler-dev
#+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][Compilation and installation on Windows]], below.
PDF Tools will successfully compile using Cygwin, but it will not be
able to open PDFs properly due to the way binaries compiled with Cygwin
handle file paths.
*** 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 (*Note:* You must use mingw64.exe and not msys2.exe)
5. Compile pdf-tools
#+BEGIN_SRC sh
$ cd /path/to/pdf-tools
$ 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~. Alternatively, and if you care about
start-up time, you may want to use
#+begin_src elisp
(pdf-loader-install)
#+end_src
instead. 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-p~ |
| 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~ / ~N~ |
| 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.90/TODO 0000664 0000000 0000000 00000002050 13407234246 0014043 0 ustar 00root root 0000000 0000000 -*- 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.90/appveyor.yml 0000664 0000000 0000000 00000000767 13407234246 0015760 0 ustar 00root root 0000000 0000000 version: 1.0.{build}
environment:
matrix:
- COMPILER: msys2
PLATFORM: x64
MSYS2_ARCH: x86_64
MSYS2_DIR: msys64
MSYSTEM: MINGW64
BIT: 64
install:
# running under CI
- '%APPVEYOR_BUILD_FOLDER%\ci\appveyor\install.bat'
- 'echo End intall at: & time /t'
build_script:
- 'pushd %APPVEYOR_BUILD_FOLDER%'
- 'make autobuild -f %APPVEYOR_BUILD_FOLDER%\Makefile'
after_build:
- '%APPVEYOR_BUILD_FOLDER%\ci\appveyor\pack.bat'
artifacts:
- path: '*.zip'
- path: '*.tar'
pdf-tools-0.90/ci/ 0000775 0000000 0000000 00000000000 13407234246 0013751 5 ustar 00root root 0000000 0000000 pdf-tools-0.90/ci/appveyor/ 0000775 0000000 0000000 00000000000 13407234246 0015616 5 ustar 00root root 0000000 0000000 pdf-tools-0.90/ci/appveyor/install.bat 0000664 0000000 0000000 00000001576 13407234246 0017765 0 ustar 00root root 0000000 0000000 @echo off
cd %APPVEYOR_BUILD_FOLDER%
echo Compiler: %COMPILER%
echo Architecture: %MSYS2_ARCH%
echo Platform: %PLATFORM%
echo MSYS2 directory: %MSYS2_DIR%
echo MSYS2 system: %MSYSTEM%
echo Bits: %BIT%
REM Create a writeable TMPDIR
mkdir %APPVEYOR_BUILD_FOLDER%\tmp
set TMPDIR=%APPVEYOR_BUILD_FOLDER%\tmp
IF %COMPILER%==msys2 (
@echo on
SET "PATH=C:\%MSYS2_DIR%\%MSYSTEM%\bin;C:\%MSYS2_DIR%\usr\bin;C:\%MSYS2_DIR%\home\appveyor\.cask\bin;%PATH%"
bash -lc "pacman -S --needed --noconfirm git"
REM dependencies
bash -lc "pacman -S --needed --noconfirm mingw-w64-x86_64-zlib mingw-w64-x86_64-libpng mingw-w64-x86_64-poppler mingw-w64-x86_64-imagemagick openssl mingw-w64-x86_64-openssl"
REM Set up emacs
bash -lc "pacman -S --needed --noconfirm mingw-w64-x86_64-emacs"
REM Set up Cask
bash -lc "curl -fsSL https://raw.githubusercontent.com/cask/cask/master/go | python"
)
pdf-tools-0.90/ci/appveyor/pack.bat 0000664 0000000 0000000 00000001146 13407234246 0017226 0 ustar 00root root 0000000 0000000 @echo off
cd %APPVEYOR_BUILD_FOLDER%
REM Create a writeable TMPDIR
mkdir %APPVEYOR_BUILD_FOLDER%\pack
set PACKDIR=%APPVEYOR_BUILD_FOLDER%\pack
IF %COMPILER%==msys2 (
@echo on
SET "PATH=C:\%MSYS2_DIR%\%MSYSTEM%\bin;C:\%MSYS2_DIR%\usr\bin;C:\%MSYS2_DIR%\home\appveyor\.cask\bin;%PATH%"
REM Copy epdfinfo.exe and all dependencies
bash -lc "pushd /c/projects/pdf-tools; ldd server/epdfinfo.exe | grep mingw | cut -d' ' -f 3 | xargs -I {} cp {} ./pack/; cp server/epdfinfo.exe ./pack/; cp /mingw64/bin/*eay32.dll ./pack/"
REM Package epdfinfo.exe and all dependencies
7z a epdfinfo.zip %PACKDIR%\*.*
)
pdf-tools-0.90/lisp/ 0000775 0000000 0000000 00000000000 13407234246 0014325 5 ustar 00root root 0000000 0000000 pdf-tools-0.90/lisp/.gitignore 0000664 0000000 0000000 00000000025 13407234246 0016312 0 ustar 00root root 0000000 0000000 *.elc
.dir-locals.el
pdf-tools-0.90/lisp/pdf-annot.el 0000664 0000000 0000000 00000203206 13407234246 0016540 0 ustar 00root root 0000000 0000000 ;;; 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 nil
"Alist of initial properties for new text annotations."
:group 'pdf-annot
:type '(alist :key-type symbol :value-type sexp))
(defcustom pdf-annot-default-markup-annotation-properties nil
"Alist of initial properties for new markup annotations."
:group 'pdf-annot
:type '(alist :key-type symbol :value-type sexp))
(make-obsolete-variable 'pdf-annot-default-text-annotation-properties
'pdf-annot-default-annotation-properties
"0.90")
(make-obsolete-variable 'pdf-annot-default-markup-annotation-properties
'pdf-annot-default-annotation-properties
"0.90")
(defcustom pdf-annot-default-annotation-properties
`((t (label . ,user-full-name))
(text (icon . "Note")
(color . "#ff0000"))
(highlight (color . "yellow"))
(squiggly (color . "orange"))
(strike-out(color . "red"))
(underline (color . "blue")))
"An alist of initial properties for new annotations.
The alist contains a sub-alist for each of the currently available
annotation types, i.e. text, highlight, squiggly, strike-out and
underline. Additionally a sub-alist with a key of t acts as a default
entry.
Each of these sub-alists contain default property-values of newly
added annotations of its respective type.
Some of the most important properties and their types are label
\(a string\), contents \(a string\), color \(a color\) and, for
text-annotations only, icon \(one of the standard icon-types, see
`pdf-annot-standard-text-icons'\).
For example a value of
\(\(t \(color . \"red\"\)
\(label . \"Joe\"\)
\(highlight \(color . \"green\"\)\)
would use a green color for highlight and a red one for other
annotations. Additionally the label for all annotations is set
to \"Joe\"."
:group 'pdf-annot
:type (let* ((label '(cons :tag "Label" (const label) string))
(contents '(cons :tag "Contents" (const contents) string))
(color '(cons :tag "Color" (const color) color))
(icon `(cons :tag "Icon"
(const icon)
(choice
,@(mapcar (lambda (icon)
`(const ,icon))
'("Note" "Comment" "Key" "Help" "NewParagraph"
"Paragraph" "Insert" "Cross" "Circle")))))
(other '(repeat
:tag "Other properties"
(cons :tag "Property"
(symbol :tag "Key ")
(sexp :tag "Value"))))
(text-properties
`(set ,label ,contents ,color ,icon ,other))
(markup-properties
`(set ,label ,contents ,color))
(all-properties
`(set ,label ,contents ,color ,icon ,other)))
`(set
(cons :tag "All Annotations" (const t) ,all-properties)
(cons :tag "Text Annotations" (const text) ,text-properties)
(cons :tag "Highlight Annotations" (const highlight) ,markup-properties)
(cons :tag "Underline Annotations" (const underline) ,markup-properties)
(cons :tag "Squiggly Annotations" (const squiggly) ,markup-properties)
(cons :tag "Strike-out Annotations" (const strike-out) ,markup-properties))))
(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
:type 'boolean)
(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)
(apply #'pdf-util-edges-union
(apply #'append
(mapcar
(lambda (e)
(pdf-info-getselection page e))
edges))))
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
(cdr (assq 'text pdf-annot-default-annotation-properties))
(cdr (assq t pdf-annot-default-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
(cdr (assq type pdf-annot-default-annotation-properties))
(cdr (assq t pdf-annot-default-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
(let ((inhibit-read-only t))
(erase-buffer)
(save-excursion (insert (pdf-annot-get a 'contents)))
(set-buffer-modified-p nil))
(setq pdf-annot-edit-contents--annotation a)
(funcall pdf-annot-edit-contents-setup-function a)
(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
:type display-buffer--action-custom-type)
(defcustom pdf-annot-list-format
'((page . 3)
(type . 10)
(label . 24)
(date . 24))
"Annotation properties visible in the annotation list.
It should be a list of \(PROPERTIZE. WIDTH\), where PROPERTY is a
symbol naming one of supported properties to list and WIDTH its
desired column-width.
Currently supported properties are page, type, label, date and contents."
:type '(alist :key-type (symbol))
:options '((page (integer :value 3 :tag "Column Width"))
(type (integer :value 10 :tag "Column Width" ))
(label (integer :value 24 :tag "Column Width"))
(date (integer :value 24 :tag "Column Width"))
(contents (integer :value 56 :tag "Column Width")))
:group 'pdf-annot)
(defcustom pdf-annot-list-highlight-type nil
"Whether to highlight \"Type\" column annotation list with annotation color."
:group 'pdf-annot
:type 'boolean)
(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--make-entry-formatter (a)
(lambda (fmt)
(let ((entry-type (car fmt))
(entry-width (cdr fmt))
;; Taken from css-mode.el
(contrasty-color
(lambda (name)
(if (> (color-distance name "black") 292485)
"black" "white")))
(prune-newlines
(lambda (str)
(replace-regexp-in-string "\n" " " str t t))))
(cl-ecase entry-type
(date (pdf-annot-print-property a 'modified))
(page (pdf-annot-print-property a 'page))
(label (funcall prune-newlines
(pdf-annot-print-property a 'label)))
(contents
(truncate-string-to-width
(funcall prune-newlines
(pdf-annot-print-property a 'contents))
entry-width))
(type
(let ((color (pdf-annot-get a 'color))
(type (pdf-annot-print-property a 'type)))
(if pdf-annot-list-highlight-type
(propertize
type 'face
`(:background ,color
:foreground ,(funcall contrasty-color color)))
type)))))))
(defun pdf-annot-list-create-entry (a)
"Create a `tabulated-list-entries' entry for annotation A."
(list (pdf-annot-get-id a)
(vconcat
(mapcar (pdf-annot--make-entry-formatter a)
pdf-annot-list-format))))
(define-derived-mode pdf-annot-list-mode tablist-mode "Annots"
(let* ((page-sorter
(lambda (a b)
(< (string-to-number (aref (cadr a) 0))
(string-to-number (aref (cadr b) 0)))))
(format-generator
(lambda (format)
(let ((field (car format))
(width (cdr format)))
(cl-case field
(page `("Pg." 3 ,page-sorter :read-only t :right-alight t))
(t (list
(capitalize (symbol-name field))
width t :read-only t)))))))
(setq tabulated-list-entries 'pdf-annot-list-entries
tabulated-list-format (vconcat
(mapcar
format-generator
pdf-annot-list-format))
tabulated-list-padding 2))
(set-keymap-parent pdf-annot-list-mode-map tablist-mode-map)
(use-local-map pdf-annot-list-mode-map)
(when (assq 'type pdf-annot-list-format)
(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.90/lisp/pdf-cache.el 0000664 0000000 0000000 00000037750 13407234246 0016475 0 ustar 00root root 0000000 0000000 ;;; 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)
;; FIXME: Disable the time when the buffer is killed or it's
;; major-mode changes.
(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.90/lisp/pdf-dev.el 0000664 0000000 0000000 00000005505 13407234246 0016201 0 ustar 00root root 0000000 0000000 ;;; 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.90/lisp/pdf-history.el 0000664 0000000 0000000 00000011776 13407234246 0017133 0 ustar 00root root 0000000 0000000 ;;; 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.90/lisp/pdf-info.el 0000664 0000000 0000000 00000174271 13407234246 0016365 0 ustar 00root root 0000000 0000000 ;;; 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"))
(default-directory
(or (and load-file-name
(file-name-directory load-file-name))
default-directory)))
(cl-labels ((try-directory (directory)
(and (file-directory-p directory)
(file-executable-p (expand-file-name executable directory))
(expand-file-name executable directory))))
(or (executable-find executable)
;; This works if epdfinfo is in the same place as emacs and
;; the editor was started with an absolute path, i.e. it is
;; ment for Windows/Msys2.
(and (stringp (car-safe command-line-args))
(file-name-directory (car command-line-args))
(try-directory
(file-name-directory (car command-line-args))))
;; If we are running directly from the git repo.
(try-directory (expand-file-name "../server"))
;; Fall back to epdfinfo in the directory of this file.
(expand-file-name executable))))
"Filename of the epdfinfo executable."
:group 'pdf-info
:type 'file)
(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."
:group 'pdf-info
:type 'hook)
;; * ================================================================== *
;; * 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
;; * ================================================================== *
(defconst pdf-info-empty-page-data
(eval-when-compile
(concat
"%PDF-1.0\n1 0 obj<>endobj 2 0"
" obj<>endobj 3 0 obj<"
"Type/Page/MediaBox[0 0 3 3]>>endobj\nxref\n0 4\n00000000"
"0065535 f\n0000000010 00000 n\n0000000053 00000 n\n00000"
"00102 00000 n\ntrailer<>\nstartxref\n149\n%EOF"))
"PDF data of an empty page.")
(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)
"Check if the server should be working properly.
Signal an error if some problem was found. Message a
confirmation, if INTERACTIVE-P is non-nil and no problems were
found.
Returns nil."
(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 (and (stringp pdf-info-epdfinfo-error-filename)
(file-writable-p pdf-info-epdfinfo-error-filename))
(error "pdf-info-epdfinfo-error-filename should contain writable filename")))
(let* ((default-directory (expand-file-name "~/"))
(cmdfile (make-temp-file "commands"))
(pdffile (make-temp-file "empty.pdf"))
(tempdir (make-temp-file "tmpdir" t))
(process-environment (cons (concat "TMPDIR=" tempdir)
process-environment)))
(unwind-protect
(with-temp-buffer
(with-temp-file pdffile
(set-buffer-multibyte nil)
(insert pdf-info-empty-page-data))
(with-temp-file cmdfile
(insert (format "renderpage:%s:1:100\nquit\n"
(pdf-info-query--escape pdffile))))
(unless (= 0 (apply #'call-process
executable cmdfile (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 cmdfile)
(delete-file cmdfile))
(when (file-exists-p pdffile)
(delete-file pdffile))
(when (file-exists-p tempdir)
(delete-directory tempdir t)))))
(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))
(not (eq cmd 'quit)))
(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.90/lisp/pdf-isearch.el 0000664 0000000 0000000 00000074553 13407234246 0017052 0 ustar 00root root 0000000 0000000 ;;; 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.90/lisp/pdf-links.el 0000664 0000000 0000000 00000032456 13407234246 0016550 0 ustar 00root root 0000000 0000000 ;;; 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)
(require 'org)
;;; 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
'pdf-links-browse-uri-default
"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)))
(defun pdf-links-browse-uri-default (uri)
"Open the string URI using Org.
Wraps the URI in \[\[ ... \]\] and calls `org-open-link-from-string'
on the resulting string."
(cl-check-type uri string)
(message "Opening `%s' with Org" uri)
(org-open-link-from-string (format "[[%s]]" uri)))
(provide 'pdf-links)
;;; pdf-links.el ends here
pdf-tools-0.90/lisp/pdf-loader.el 0000664 0000000 0000000 00000005050 13407234246 0016664 0 ustar 00root root 0000000 0000000 ;;; pdf-loader.el --- Minimal PDF Tools loader -*- lexical-binding: t; -*-
;; Copyright (C) 2017 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:
;;
;;; Code:
(defconst pdf-loader--auto-mode-alist-item
(copy-sequence "\\.[pP][dD][fF]\\'")
"The item used in `auto-mode-alist'.")
(defconst pdf-loader--magic-mode-alist-item
(copy-sequence "%PDF")
"The item used in`magic-mode-alist'.")
(declare-function pdf-tools-install "pdf-tools.el")
;;;###autoload
(defun pdf-loader-install (&optional no-query-p skip-dependencies-p
no-error-p force-dependencies-p)
"Prepare Emacs for using PDF Tools.
This function acts as a replacement for `pdf-tools-install' and
makes Emacs load and use PDF Tools as soon as a PDF file is
opened, but not sooner.
The arguments are passed verbatim to `pdf-tools-install', which
see."
(let ((args (list no-query-p skip-dependencies-p
no-error-p force-dependencies-p)))
(if (featurep 'pdf-tools)
(apply #'pdf-tools-install args)
(pdf-loader--install
(lambda ()
(apply #'pdf-loader--load args))))))
(defun pdf-loader--load (&rest args)
(pdf-loader--uninstall)
(save-selected-window
(pdf-tools-install args)))
(defun pdf-loader--install (loader)
(pdf-loader--uninstall)
(push (cons pdf-loader--auto-mode-alist-item loader)
auto-mode-alist)
(push (cons pdf-loader--magic-mode-alist-item loader)
magic-mode-alist))
(defun pdf-loader--uninstall ()
(let ((elt (assoc pdf-loader--auto-mode-alist-item
auto-mode-alist)))
(when elt
(setq auto-mode-alist (remove elt auto-mode-alist))))
(let ((elt (assoc pdf-loader--magic-mode-alist-item
magic-mode-alist)))
(when elt
(setq magic-mode-alist (remove elt magic-mode-alist)))))
(provide 'pdf-loader)
;;; pdf-loader.el ends here
pdf-tools-0.90/lisp/pdf-misc.el 0000664 0000000 0000000 00000024301 13407234246 0016351 0 ustar 00root root 0000000 0000000 ;;; 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.90/lisp/pdf-occur.el 0000664 0000000 0000000 00000073253 13407234246 0016543 0 ustar 00root root 0000000 0000000 ;;; 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.90/lisp/pdf-outline.el 0000664 0000000 0000000 00000046660 13407234246 0017111 0 ustar 00root root 0000000 0000000 ;;; 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-local outline-regexp "\\( *\\).")
(setq-local 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.90/lisp/pdf-sync.el 0000664 0000000 0000000 00000072734 13407234246 0016407 0 ustar 00root root 0000000 0000000 ;;; 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
:type '(repeat function))
;;;###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)))
(jka-compr-verbose nil))
(when (and synctex
(file-readable-p synctex))
(with-current-buffer (find-file-noselect synctex :nowarn)
(unless (or (verify-visited-file-modtime)
(buffer-modified-p))
(revert-buffer :ignore-auto :noconfirm)
(goto-char (point-min)))
;; 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.90/lisp/pdf-tools.el 0000664 0000000 0000000 00000043553 13407234246 0016570 0 ustar 00root root 0000000 0000000 ;;; 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.90
;; 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)
(require 'cl-lib)
(require 'package)
;; * ================================================================== *
;; * Customizables
;; * ================================================================== *
(defgroup pdf-tools nil
"Support library for PDF documents."
:group 'data)
(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'.")
(defconst pdf-tools-magic-mode-alist-entry
'("%PDF" . pdf-view-mode)
"The entry to use for `magic-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))))
;; * ================================================================== *
;; * Installation
;; * ================================================================== *
;;;###autoload
(defcustom pdf-tools-handle-upgrades t
"Whether PDF Tools should handle upgrading itself."
:group 'pdf-tools
:type 'boolean)
(make-obsolete-variable 'pdf-tools-handle-upgrades
"Not used anymore" "0.90")
(defconst pdf-tools-directory
(or (and load-file-name
(file-name-directory load-file-name))
default-directory)
"The directory from where this library was first loaded.")
(defvar pdf-tools-msys2-directory nil)
(defun pdf-tools-identify-build-directory (directory)
"Return non-nil, if DIRECTORY appears to contain the epdfinfo source.
Returns the expanded directory-name of DIRECTORY or nil."
(setq directory (file-name-as-directory
(expand-file-name directory)))
(and (file-exists-p (expand-file-name "autobuild" directory))
(file-exists-p (expand-file-name "epdfinfo.c" directory))
directory))
(defun pdf-tools-locate-build-directory ()
"Attempt to locate a source directory.
Returns a appropriate directory or nil. See also
`pdf-tools-identify-build-directory'."
(cl-some #'pdf-tools-identify-build-directory
(list default-directory
(expand-file-name "build/server" pdf-tools-directory)
(expand-file-name "server")
(expand-file-name "../server" pdf-tools-directory))))
(defun pdf-tools-msys2-directory (&optional noninteractive-p)
"Locate the Msys2 installation directory.
Ask the user if necessary and NONINTERACTIVE-P is nil.
Returns always nil, unless `system-type' equals windows-nt."
(cl-labels ((if-msys2-directory (directory)
(and (stringp directory)
(file-directory-p directory)
(file-exists-p
(expand-file-name "usr/bin/bash.exe" directory))
directory)))
(when (eq system-type 'windows-nt)
(setq pdf-tools-msys2-directory
(or pdf-tools-msys2-directory
(cl-some #'if-msys2-directory
(cl-mapcan
(lambda (drive)
(list (format "%c:/msys64" drive)
(format "%c:/msys32" drive)))
(number-sequence ?c ?z)))
(unless (or noninteractive-p
(not (y-or-n-p "Do you have Msys2 installed ? ")))
(if-msys2-directory
(read-directory-name
"Please enter Msys2 installation directory: " nil nil t))))))))
(defun pdf-tools-msys2-mingw-bin ()
"Return the location of /mingw*/bin."
(when (pdf-tools-msys2-directory)
(let ((arch (intern (car (split-string system-configuration "-" t)))))
(expand-file-name
(format "./mingw%s/bin" (if (eq arch 'x86_64) "64" "32"))
(pdf-tools-msys2-directory)))))
(defun pdf-tools-find-bourne-shell ()
"Locate a usable sh."
(or (and (eq system-type 'windows-nt)
(let* ((directory (pdf-tools-msys2-directory)))
(when directory
(expand-file-name "usr/bin/bash.exe" directory))))
(executable-find "sh")))
(defun pdf-tools-build-server (target-directory
&optional
skip-dependencies-p
force-dependencies-p
callback
build-directory)
"Build the epdfinfo program in the background.
Install into TARGET-DIRECTORY, which should be a directory.
If CALLBACK is non-nil, it should be a function. It is called
with the compiled executable as the single argument or nil, if
the build falied.
Expect sources to be in BUILD-DIRECTORY. If nil, search for it
using `pdf-tools-locate-build-directory'.
See `pdf-tools-install' for the SKIP-DEPENDENCIES-P and
FORCE-DEPENDENCIES-P arguments.
Returns the buffer of the compilation process."
(unless callback (setq callback #'ignore))
(unless build-directory
(setq build-directory (pdf-tools-locate-build-directory)))
(cl-check-type target-directory file-directory)
(setq target-directory (file-name-as-directory
(expand-file-name target-directory)))
(cl-check-type build-directory (and (not null) file-directory))
(when (and skip-dependencies-p force-dependencies-p)
(error "Can't simultaneously skip and force dependencies"))
(let* ((compilation-auto-jump-to-first-error nil)
(compilation-scroll-output t)
(shell-file-name (pdf-tools-find-bourne-shell))
(shell-command-switch "-c")
(process-environment process-environment)
(default-directory build-directory)
(autobuild (shell-quote-argument
(expand-file-name "autobuild" build-directory)))
(msys2-p (equal "bash.exe" (file-name-nondirectory shell-file-name))))
(unless shell-file-name
(error "No suitable shell found"))
(when msys2-p
(push "BASH_ENV=/etc/profile" process-environment))
(let ((executable
(expand-file-name
(concat "epdfinfo" (and (eq system-type 'windows-nt) ".exe"))
target-directory))
(compilation-buffer
(compilation-start
(format "%s -i %s%s"
autobuild
(shell-quote-argument target-directory)
(cond
(skip-dependencies-p " -D")
(force-dependencies-p " -d")
(t "")))
t)))
;; In most cases user-input is required, so select the window.
(if (get-buffer-window compilation-buffer)
(select-window (get-buffer-window compilation-buffer))
(pop-to-buffer compilation-buffer))
(with-current-buffer compilation-buffer
(setq-local compilation-error-regexp-alist nil)
(add-hook 'compilation-finish-functions
(lambda (_buffer status)
(funcall callback
(and (equal status "finished\n")
executable)))
nil t)
(current-buffer)))))
;; * ================================================================== *
;; * Initialization
;; * ================================================================== *
;;;###autoload
(defun pdf-tools-install (&optional no-query-p skip-dependencies-p
no-error-p force-dependencies-p)
"Install PDF-Tools in all current and future PDF buffers.
If the `pdf-info-epdfinfo-program' is not running or does not
appear to be working, attempt to rebuild it. If this build
succeeded, continue with the activation of the package.
Otherwise fail silently, i.e. no error is signaled.
Build the program (if necessary) without asking first, if
NO-QUERY-P is non-nil.
Don't attempt to install system packages, if SKIP-DEPENDENCIES-P
is non-nil.
Do not signal an error in case the build failed, if NO-ERROR-P is
non-nil.
Attempt to install system packages (even if it is deemed
unnecessary), if FORCE-DEPENDENCIES-P is non-nil.
Note that SKIP-DEPENDENCIES-P and FORCE-DEPENDENCIES-P are
mutually exclusive.
Note further, that you can influence the installation directory
by setting `pdf-info-epdfinfo-program' to an appropriate
value (e.g. ~/bin/epdfinfo) before calling this function.
See `pdf-view-mode' and `pdf-tools-enabled-modes'."
(interactive)
(if (or (pdf-info-running-p)
(ignore-errors (pdf-info-check-epdfinfo) t))
(pdf-tools-install-noverify)
(let ((target-directory
(or (and (stringp pdf-info-epdfinfo-program)
(file-name-directory
pdf-info-epdfinfo-program))
pdf-tools-directory)))
(if (or no-query-p
(y-or-n-p "Need to (re)build the epdfinfo program, do it now ?"))
(pdf-tools-build-server
target-directory
skip-dependencies-p
force-dependencies-p
(lambda (executable)
(let ((msg (format
"Building the PDF Tools server %s"
(if executable "succeeded" "failed"))))
(if (not executable)
(funcall (if no-error-p #'message #'error) "%s" msg)
(message "%s" msg)
(setq pdf-info-epdfinfo-program executable)
(let ((pdf-info-restart-process-p t))
(pdf-tools-install-noverify))))))
(message "PDF Tools not activated")))))
(defun pdf-tools-install-noverify ()
"Like `pdf-tools-install', but skip checking `pdf-info-epdfinfo-program'."
(add-to-list 'auto-mode-alist pdf-tools-auto-mode-alist-entry)
(add-to-list 'magic-mode-alist pdf-tools-magic-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))
(setq-default magic-mode-alist
(remove pdf-tools-magic-mode-alist-entry magic-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)))))
(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-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.90/lisp/pdf-util.el 0000664 0000000 0000000 00000141375 13407234246 0016406 0 ustar 00root root 0000000 0000000 ;;; 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
;; Avoid using the MS Windows command convert.exe .
(unless (memq system-type '(ms-dos windows-nt))
(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
:type '(cons symbol string))
(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)
(set-buffer-file-coding-system 'binary)
(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.90/lisp/pdf-view.el 0000664 0000000 0000000 00000170667 13407234246 0016411 0 ustar 00root root 0000000 0000000 ;;; 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)
(declare-function cua-copy-region "cua-base")
;; * ================================================================== *
;; * 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)))
;; Decryption needs to be done before any other function calls into
;; pdf-info.el (e.g. from the mode-line during redisplay during
;; waiting for process output).
(pdf-view-decrypt-document)
;; 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)))
;; Avoid errors during redisplay.
"/" (:eval (or (ignore-errors
(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)
;; In Emacs >= 24.4, `cua-copy-region' should have been advised when
;; loading pdf-view.el so as to make it work with
;; pdf-view-mode. Disable cua-mode if that is not the case.
;; FIXME: cua-mode is a global minor-mode, but setting cua-mode to
;; nil seems to do the trick.
(when (and (bound-and-true-p cua-mode)
(version< emacs-version "24.4"))
(setq-local cua-mode nil))
(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)
;; Issue a warning in the future about incompatible modes.
(run-with-timer 1 nil (lambda (buffer)
(when (buffer-live-p buffer)
(pdf-view-check-incompatible-modes buffer)))
(current-buffer))
;; Setup initial page and start display
(pdf-view-goto-page (or (pdf-view-current-page) 1)))
(unless (version< emacs-version "24.4")
(defun cua-copy-region--pdf-view-advice (&rest _)
"If the current buffer is in `pdf-view' mode, call
`pdf-view-kill-ring-save'."
(when (eq major-mode 'pdf-view-mode)
(pdf-view-kill-ring-save)
t))
(advice-add 'cua-copy-region
:before-until
#'cua-copy-region--pdf-view-advice))
(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.
This function always suceeds. See also `pdf-info-close', which
does not return immediately."
(when (pdf-info-running-p)
(let ((pdf-info-asynchronous 'ignore))
(ignore-errors
(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.90/lisp/pdf-virtual.el 0000664 0000000 0000000 00000116744 13407234246 0017121 0 ustar 00root root 0000000 0000000 ;;; 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)))
(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)))
(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.90/server/ 0000775 0000000 0000000 00000000000 13407234246 0014664 5 ustar 00root root 0000000 0000000 pdf-tools-0.90/server/.gitignore 0000664 0000000 0000000 00000000406 13407234246 0016654 0 ustar 00root root 0000000 0000000 *.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.90/server/Makefile.am 0000664 0000000 0000000 00000002443 13407234246 0016723 0 ustar 00root root 0000000 0000000 bin_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)
if HAVE_W32
epdfinfo_LDADD += -lshlwapi
endif
SYNCTEX_UPSTREAM = svn://tug.org/texlive/tags/texlive-2017.1/Build/source/texk/web2c/synctexdir
SYNCTEX_FILES = synctex_parser.c \
synctex_parser.h \
synctex_parser_local.h \
synctex_parser_readme.txt \
synctex_parser_utils.c \
synctex_parser_utils.h \
synctex_parser_version.txt
check-local:
@if $(MAKE) --version 2>&1 | grep -q GNU; then \
cd test && $(MAKE) $(AM_MAKEFLAGS); \
else \
echo "Skipping tests in server/test (requires GNU make)"; \
fi
synctex-pull:
@if [ -n "$$(git status --porcelain)" ]; then \
git status; \
echo "Not checking-out files into a dirty work-directory"; \
false; \
fi
for file in $(SYNCTEX_FILES); do \
svn export --force $(SYNCTEX_UPSTREAM)/$$file; \
done
pdf-tools-0.90/server/autobuild 0000775 0000000 0000000 00000027110 13407234246 0016603 0 ustar 00root root 0000000 0000000 #!/bin/sh
##
## Installs package dependencies and builds the application.
##
# Don't exit if some command fails.
set +e
# Disalbe file globbing.
set -f
# Boolean variables are true if non-empty and false otherwise.
# Command to install packages.
PKGCMD=
# Args to pass to $PKGCMD.
PKGARGS=
# Required packages.
PACKAGES=
# Whether package installation requires root permissions.
PKG_INSTALL_AS_ROOT=true
# Whether to skip package installation altogether.
PKG_INSTALL_SKIP=
# Whether to force package installation, even if it does not seem
# necessary.
PKG_INSTALL_FORCE=
# Only test if the OS is handled by this script.
DRY_RUN=
# If and where to install the program.
INSTALL_DIR=
# Whether we can install packages.
OS_IS_HANDLED=true
## +-----------------------------------------------------------+
## * Utility Functions
## +-----------------------------------------------------------+
usage()
{
cat <()$\`"'\'' ]/\\&/g')
if [ -z "$quoted" ]; then
quoted=$qarg
else
quoted="$quoted $qarg"
fi
done
printf "%s" "$quoted"
}
# Attempt to exec $@ as root.
exec_privileged() {
if [ -z "$1" ]; then
echo "internal error: command is empty"
exit 2
fi
if [ -w / ]; then
"$@"
elif which sudo >/dev/null 2>&1; then
sudo -- "$@"
retval=$?
sudo -k
return $retval
elif which su >/dev/null 2>&1; then
su -c "$(quote "$@")"
else
echo "No such program: sudo or su"
exit 1
fi
}
# Test if $1 is in PATH or exit with a failure status.
assert_program()
{
if ! which "$1" >/dev/null 2>&1; then
echo "No such program: $1"
exit 1
fi
}
# Source filename $1 and echo variable $2.
source_var()
{
if ! [ -f "$1" ] || ! [ -r "$1" ] || [ -z "$2" ]; then
return 1
fi
# shellcheck source=/dev/null
. "$1"
eval "printf '%s\n' \$$2"
return 0
}
exit_success()
{
echo "==========================="
echo " Build succeeded. :O) "
echo "==========================="
exit 0
}
exit_fail()
{
echo "==========================="
echo " Build failed. ;o( "
echo "==========================="
if [ -z "$PKG_INSTALL_FORCE" ]; then
echo "Note: maybe try the '-d' option."
fi
exit 1
}
# Return 0, if all required packages seem to be installed.
have_packages_installed()
{
{
which pkg-config || return 1
if ! [ -f configure ];then
which autoreconf || return 1
which automake || return 1
fi
for lib in libpng glib-2.0 poppler poppler-glib zlib; do
pkg-config --exists $lib || return 1
done
which make || return 1
which gcc || which cc || return 1
which g++ || which c++ || return 1
cc $(pkg-config --cflags poppler) -o /dev/null -E - 2>/dev/null <
EOF
[ $? -eq 0 ] || return 1
return 0
} >/dev/null 2>&1
}
handle_options()
{
while [ $# -gt 0 ]; do
case $1 in
--help) usage 0;;
-n) DRY_RUN=true;;
-d) PKG_INSTALL_FORCE=true ;;
-D) PKG_INSTALL_SKIP=true ;;
-i)
shift
[ $# -gt 0 ] || usage 1
if [ "${1%%/}" != "${PWD%%/}" ]; then
INSTALL_DIR=$1
fi ;;
*) usage 1 ;;
esac
shift
done
if [ -n "$PKG_INSTALL_SKIP" ] && [ -n "$PKG_INSTALL_FORCE" ]; then
usage 1
fi
}
## +-----------------------------------------------------------+
## * OS Functions
## +-----------------------------------------------------------+
# Archlinux
os_arch() {
if ! [ -e "/etc/arch-release" ]; then
return 1;
fi
PKGCMD=pacman
PKGARGS="-S --needed"
PACKAGES="base-devel libpng zlib poppler-glib"
return 0;
}
# CentOS
os_centos() {
if ! [ -e "/etc/centos-release" ]; then
return 1
fi
PKGCMD=yum
if yum help install-n >/dev/null 2>&1; then
PKGARGS=install-n
else
PKGARGS=install
fi
PACKAGES="autoconf
automake
gcc
gcc-c++
libpng-devel
make
pkgconfig
poppler-devel
poppler-glib-devel
zlib-devel"
return 0
}
# FreeBSD
os_freebsd() {
if ! which uname >/dev/null 2>&1 || [ "$(uname -s)" != "FreeBSD" ]; then
return 1
fi
PKGCMD=pkg
PKGARGS=install
PACKAGES="autotools poppler-glib png pkgconf"
return 0
}
# OpenBSD
os_openbsd() {
if ! which uname >/dev/null 2>&1 || [ "$(uname -s)" != "OpenBSD" ]; then
return 1
fi
PKGCMD=pkg_add
PKGARGS="-uU"
PACKAGES="autoconf-2.69p2 automake-1.15.1 poppler poppler-utils png"
export AUTOCONF_VERSION=2.69
export AUTOMAKE_VERSION=1.15
if whereis clang++ ;then
export CXX=clang++
elif whereis eg++ ;then
export CXX=eg++
else
export CXX=eg++
PACKAGES="${PACKAGES} g++"
fi
export CXXFLAGS="-std=c++11 -I/usr/local/include/poppler -I/usr/local/include"
return 0
}
# Fedora
os_fedora() {
if ! [ -e "/etc/fedora-release" ]; then
return 1
fi
PKGCMD=dnf
PKGARGS=install
PACKAGES="autoconf
automake
gcc
gcc-c++
libpng-devel
make
poppler-devel
poppler-glib-devel
zlib-devel"
VERSION=$(source_var /etc/os-release VERSION_ID)
if [ -n "$VERSION" ] && [ "$VERSION" -ge 26 ]; then
PACKAGES="$PACKAGES pkgconf"
else
PACKAGES="$PACKAGES pkgconfig"
fi
return 0
}
# Debian/Ubuntu
os_debian() {
if ! [ -e "/etc/debian_version" ]; then
return 1
fi
PACKAGES="autoconf
automake
g++
gcc
libpng-dev
libpoppler-dev
libpoppler-glib-dev
libpoppler-private-dev
libz-dev
make
pkg-config"
PKGCMD=apt-get
PKGARGS=install
return 0
}
# Msys2
os_msys2() {
if [ -z "$MSYSTEM" ] || ! [ -r "/etc/profile" ]; then
return 1
fi
case $MSYSTEM in
MINGW64)
PACKAGES="base-devel
mingw-w64-x86_64-libpng
mingw-w64-x86_64-poppler
mingw-w64-x86_64-toolchain
mingw-w64-x86_64-zlib" ;;
MINGW32)
PACKAGES="base-devel
mingw-w64-i686-libpng
mingw-w64-i686-poppler
mingw-w64-i686-toolchain
mingw-w64-i686-zlib" ;;
MSYS)
case $(uname -m) in
x86_64)
MSYSTEM=MINGW64 ;;
*)
MSYSTEM=MINGW32 ;;
esac
export MSYSTEM
# shellcheck source=/dev/null
. /etc/profile
eval "exec $(quote "$0" "$@")" ;;
*)
echo "Unrecognized MSYSTEM value: $MSYSTEM"
exit 1 ;;
esac
PKGCMD=pacman
PKGARGS="-S --needed"
PKG_INSTALL_AS_ROOT=
return 0
}
# MacOS
os_macos() {
if ! which uname >/dev/null 2>&1 || [ "$(uname -s)" != "Darwin" ]; then
return 1
fi
PKGCMD=brew
PKGARGS=install
PACKAGES="pkg-config poppler automake"
PKG_INSTALL_AS_ROOT=
return 0
}
# NixOS
os_nixos() {
# Already in the nix-shell.
if [ -n "$AUTOBUILD_NIX_SHELL" ]; then
return 0
fi
if ! which nix-shell >/dev/null 2>&1; then
return 1
fi
if [ -n "$DRY_RUN" ]; then
return 0
fi
command="AUTOBUILD_NIX_SHELL=true"
command="$command;export AUTOBUILD_NIX_SHELL"
command="$command;$(quote "$0" "$@")"
exec nix-shell --pure --command "$command" \
-p gcc gnumake automake autoconf pkgconfig libpng zlib poppler
}
# Gentoo
os_gentoo() {
if ! [ -e "/etc/gentoo-release" ]; then
return 1
fi
PKGCMD=emerge
PKGARGS=--noreplace
PACKAGES="app-text/poppler
dev-util/pkgconfig
media-libs/libpng
sys-devel/autoconf
sys-devel/automake
sys-devel/gcc
sys-devel/make
sys-libs/zlib"
return 0
}
## +-----------------------------------------------------------+
## * Figure out were we are, install deps and build the program
## +-----------------------------------------------------------+
handle_options "$@"
os_macos "$@" || \
os_freebsd "$@" || \
os_arch "$@" || \
os_centos "$@" || \
os_openbsd "$@" || \
os_fedora "$@" || \
os_debian "$@" || \
os_gentoo "$@" || \
os_msys2 "$@" || \
os_nixos "$@" || \
{
OS_IS_HANDLED=
if [ -z "$DRY_RUN" ]; then
echo "Failed to recognize this system, trying to continue."
fi
}
if [ -n "$DRY_RUN" ]; then
[ -n "$OS_IS_HANDLED" ]
exit $?
fi
if [ -n "$PKGCMD" ];then
echo "---------------------------"
echo " Installing packages "
echo "---------------------------"
if [ -n "$PKG_INSTALL_SKIP" ]; then
echo "Skipping package installation (as requested)"
elif [ -z "$PKG_INSTALL_FORCE" ] && have_packages_installed; then
echo "Skipping package installation (already installed)"
else
assert_program "$PKGCMD"
echo "$PKGCMD $PKGARGS $PACKAGES"
if [ -n "$PKG_INSTALL_AS_ROOT" ]; then
exec_privileged $PKGCMD $PKGARGS $PACKAGES
else
$PKGCMD $PKGARGS $PACKAGES
fi
fi
echo
fi
echo "---------------------------"
echo " Configuring and compiling "
echo "---------------------------"
# Try to be in the correct directory.
if which dirname >/dev/null 2>&1; then
cd "$(dirname "$0")" || {
echo "Failed to change into the source directory"
exit 1
}
fi
# Create the configure script.
if ! [ -f ./configure ]; then
assert_program autoreconf
echo "autoreconf -i"
autoreconf -i
[ -f ./configure ] || exit_fail
fi
# Build the program.
if [ -n "$INSTALL_DIR" ]; then
prefix=--bindir=$INSTALL_DIR
fi
echo "./configure -q $prefix && make -s"
eval "./configure -q $(quote "$prefix") && make -s || exit_fail"
echo
if [ -n "$INSTALL_DIR" ]; then
echo "---------------------------"
echo " Installing "
echo "---------------------------"
echo make -s install
if mkdir -p -- "$INSTALL_DIR" && [ -w "$INSTALL_DIR" ]; then
make install || exit_fail
else
exec_privileged make install || exit_fail
fi
# Copy dynamic libraries on windows.
if [ -f epdfinfo.exe ]; then
assert_program awk
assert_program ldd
for dll in $(ldd epdfinfo.exe | awk '/\/mingw/ {print $3}'); do
cp -u "$dll" "$INSTALL_DIR"
done
fi
echo
fi
exit_success
# Local Variables:
# compile-command: "shellcheck autobuild"
# End:
pdf-tools-0.90/server/autogen.sh 0000775 0000000 0000000 00000000067 13407234246 0016670 0 ustar 00root root 0000000 0000000 #!/bin/sh
echo "Running autoreconf..."
autoreconf -i
pdf-tools-0.90/server/configure.ac 0000664 0000000 0000000 00000006720 13407234246 0017157 0 ustar 00root root 0000000 0000000 # -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.67])
AC_INIT([epdfinfo], 0.90, [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])
AC_COMPILE_IFELSE(
[AC_LANG_PROGRAM([[
#ifndef _WIN32
error
#endif
]])], [have_w32=true], [have_w32=false])
AM_CONDITIONAL(HAVE_W32, [test "$have_w32" = true])
if test "$have_w32" = true; then
if test "$MSYSTEM" = MINGW32 -o "$MSYSTEM" = MINGW64; then
# glib won't work properly on msys2 without it.
CFLAGS="-D__USE_MINGW_ANSI_STDIO=1 $CFLAGS"
fi
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.90/server/epdfinfo.c 0000664 0000000 0000000 00000303404 13407234246 0016626 0 ustar 00root root 0000000 0000000 /* 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.90/server/epdfinfo.h 0000664 0000000 0000000 00000017102 13407234246 0016630 0 ustar 00root root 0000000 0000000 // 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.90/server/m4/ 0000775 0000000 0000000 00000000000 13407234246 0015204 5 ustar 00root root 0000000 0000000 pdf-tools-0.90/server/m4/ax_check_compile_flag.m4 0000664 0000000 0000000 00000006403 13407234246 0021717 0 ustar 00root root 0000000 0000000 # ===========================================================================
# 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.90/server/poppler-hack.cc 0000664 0000000 0000000 00000006765 13407234246 0017576 0 ustar 00root root 0000000 0000000 // 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
#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 ())
#if POPPLER_CHECK_VERSION(0,72,0)
#define GET_CSTR c_str
#else
#define GET_CSTR getCString
#endif
struct PopplerAnnot
{
GObject parent_instance;
Annot *annot;
};
struct PopplerAnnotMarkup
{
GObject parent_instance;
};
struct PopplerRectangle
{
double x1;
double y1;
double x2;
double y2;
};
// This function does not modify its argument s, but for
// compatibility reasons (e.g. getLength in GooString.h before 2015)
// with older poppler code, it can't be declared as such.
char *_xpoppler_goo_string_to_utf8(/* const */ GooString *s)
{
char *result;
if (! s)
return NULL;
if (s->hasUnicodeMarker()) {
result = g_convert (s->GET_CSTR () + 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 = (GooString*) 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 = (GooString*) annot->getDate ();
return text ? _xpoppler_goo_string_to_utf8 (text) : NULL;
}
}
pdf-tools-0.90/server/poppler-versions 0000664 0000000 0000000 00000000542 13407234246 0020137 0 ustar 00root root 0000000 0000000 HAVE_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.90/server/synctex_parser.c 0000664 0000000 0000000 00000503227 13407234246 0020112 0 ustar 00root root 0000000 0000000 /*
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.18
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;
# if defined(SYNCTEX_USE_CHARINDEX)
# define SYNCTEX_DECLARE_CHARINDEX synctex_charindex_t char_index;
# define SYNCTEX_CHARINDEX(NODE) (NODE->char_index)
# define SYNCTEX_PRINT_CHARINDEX printf("#%i\n",SYNCTEX_CHARINDEX(node))
# define SYNCTEX_DECLARE_CHAR_OFFSET synctex_charindex_t charindex_offset
# define SYNCTEX_IMPLEMENT_CHARINDEX(NODE,CORRECTION) NODE->char_index = (synctex_charindex_t)(scanner->charindex_offset+SYNCTEX_CUR-SYNCTEX_START+(CORRECTION));
# else
# define SYNCTEX_DECLARE_CHARINDEX
# define SYNCTEX_CHARINDEX(NODE) 0
# define SYNCTEX_PRINT_CHARINDEX printf("\n")
# define SYNCTEX_DECLARE_CHAR_OFFSET synctex_charindex_t charindex_offset
# define SYNCTEX_IMPLEMENT_CHARINDEX(NODE,CORRECTION)
# endif
struct _synctex_node {
SYNCTEX_DECLARE_CHARINDEX
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_hbox;
_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_hbox(NODE) SYNCTEX_GET(NODE,next_hbox)
# define SYNCTEX_SET_NEXT_hbox(NODE,NEXT_HBOX) if (NODE && NEXT_HBOX){\
SYNCTEX_GETTER(NODE,next_hbox)[0]=NEXT_HBOX;\
}
/* 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.
*/
static 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.
* The first sheet is onwned by the scanner.
*/
static 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 */
SYNCTEX_DECLARE_CHAR_OFFSET;
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)\
static 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_DECLARE_CHARINDEX
synctex_class_t class;
synctex_info_t implementation[3+SYNCTEX_PAGE_IDX+1];/* child, sibling, next box,
* SYNCTEX_PAGE_IDX */
} synctex_node_sheet_t;
/* sheet node creator */
#define DEFINE_synctex_new_NODE(NAME)\
static synctex_node_t _synctex_new_##NAME(synctex_scanner_t scanner) {\
if (scanner) {\
synctex_node_t node = _synctex_malloc(sizeof(synctex_node_##NAME##_t));\
if (node) {\
SYNCTEX_IMPLEMENT_CHARINDEX(node,0);\
++SYNCTEX_CUR;\
node->class = scanner->class+synctex_node_type_##NAME;\
}\
return node;\
}\
return NULL;\
}
DEFINE_synctex_new_NODE(sheet)
static void _synctex_display_sheet(synctex_node_t node);
static void _synctex_log_sheet(synctex_node_t node);
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 hbox */
(_synctex_info_getter_t)&_synctex_implementation_3 /* info */
};
/* 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_DECLARE_CHARINDEX
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_node_vbox_t;
/* vertical box node creator */
DEFINE_synctex_new_NODE(vbox)
static void _synctex_log_vbox(synctex_node_t node);
static 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_vbox, /* 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 hbox */
(_synctex_info_getter_t)&_synctex_implementation_5
};
/* Horizontal boxes must contain visible size, because 0 width does not mean emptiness.
* They also contain an average of the line numbers of the containing nodes. */
# define SYNCTEX_MEAN_LINE_IDX (SYNCTEX_DEPTH_IDX+1)
# define SYNCTEX_NODE_WEIGHT_IDX (SYNCTEX_MEAN_LINE_IDX+1)
# define SYNCTEX_HORIZ_V_IDX (SYNCTEX_NODE_WEIGHT_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_MEAN_LINE(NODE) SYNCTEX_INFO(NODE)[SYNCTEX_MEAN_LINE_IDX].INT
# define SYNCTEX_NODE_WEIGHT(NODE) SYNCTEX_INFO(NODE)[SYNCTEX_NODE_WEIGHT_IDX].INT
# 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)))
typedef struct {
SYNCTEX_DECLARE_CHARINDEX
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_MEAN_LINE,SYNCTEX_NODE_WEIGHT,
* SYNCTEX_HORIZ_V,SYNCTEX_VERT_V,SYNCTEX_WIDTH_V,SYNCTEX_HEIGHT_V,SYNCTEX_DEPTH_V*/
} synctex_node_hbox_t;
/* horizontal box node creator */
DEFINE_synctex_new_NODE(hbox)
static void _synctex_display_hbox(synctex_node_t node);
static void _synctex_log_hbox(synctex_node_t node);
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_hbox, /* 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 hbox */
(_synctex_info_getter_t)&_synctex_implementation_5
};
/* This void box node implementation is either horizontal or vertical
* It does not contain a child field.
*/
typedef struct {
SYNCTEX_DECLARE_CHARINDEX
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_node_void_vbox_t;
/* vertical void box node creator */
DEFINE_synctex_new_NODE(void_vbox)
static void _synctex_log_void_box(synctex_node_t node);
static 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 hbox */
(_synctex_info_getter_t)&_synctex_implementation_3
};
typedef synctex_node_void_vbox_t synctex_node_void_hbox_t;
/* horizontal void box node creator */
DEFINE_synctex_new_NODE(void_hbox)
static 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 hbox */
(_synctex_info_getter_t)&_synctex_implementation_3
};
/* The medium nodes correspond to kern, glue, penalty and math nodes.
* In LuaTeX, the size of the nodes may have changed. */
typedef struct {
SYNCTEX_DECLARE_CHARINDEX
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_node_medium_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))
static void _synctex_log_medium_node(synctex_node_t node);
typedef synctex_node_medium_t synctex_node_math_t;
/* math node creator */
DEFINE_synctex_new_NODE(math)
static 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 hbox */
(_synctex_info_getter_t)&_synctex_implementation_3
};
typedef synctex_node_medium_t synctex_node_kern_t;
/* kern node creator */
DEFINE_synctex_new_NODE(kern)
static 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 hbox */
(_synctex_info_getter_t)&_synctex_implementation_3
};
/* The small nodes correspond to glue and boundary nodes. */
typedef struct {
SYNCTEX_DECLARE_CHARINDEX
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_node_small_t;
static void _synctex_log_small_node(synctex_node_t node);
/* glue node creator */
typedef synctex_node_small_t synctex_node_glue_t;
DEFINE_synctex_new_NODE(glue)
static 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 hbox */
(_synctex_info_getter_t)&_synctex_implementation_3
};
/* boundary node creator */
typedef synctex_node_small_t synctex_node_boundary_t;
DEFINE_synctex_new_NODE(boundary)
static 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 hbox */
(_synctex_info_getter_t)&_synctex_implementation_3
};
# 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_DECLARE_CHARINDEX
synctex_class_t class;
synctex_info_t implementation[1+SYNCTEX_NAME_IDX+1]; /* sibling,
* SYNCTEX_TAG,SYNCTEX_NAME */
} synctex_input_t;
# define SYNCTEX_INPUT_MARK "Input:"
static synctex_node_t _synctex_new_input(synctex_scanner_t scanner) {
if (scanner) {
synctex_node_t node = _synctex_malloc(sizeof(synctex_input_t));
if (node) {
SYNCTEX_IMPLEMENT_CHARINDEX(node,strlen(SYNCTEX_INPUT_MARK));
node->class = scanner->class+synctex_node_type_input;
}
return node;
}
return NULL;
}
static void _synctex_free_input(synctex_node_t node){
if (node) {
SYNCTEX_FREE(SYNCTEX_SIBLING(node));
free(SYNCTEX_NAME(node));
free(node);
}
}
static void _synctex_display_input(synctex_node_t node);
static void _synctex_log_input(synctex_node_t node);
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 hbox */
(_synctex_info_getter_t)&_synctex_implementation_1
};
# 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);
}
static void _synctex_log_input(synctex_node_t node) {
if (node) {
printf("%s:%i,%s",synctex_node_isa(node),SYNCTEX_TAG(node),SYNCTEX_NAME(node));
printf(" SYNCTEX_SIBLING:%p",(void *)SYNCTEX_SIBLING(node));
}
}
static void _synctex_log_sheet(synctex_node_t node) {
if (node) {
printf("%s:%i",synctex_node_isa(node),SYNCTEX_PAGE(node));
SYNCTEX_PRINT_CHARINDEX;
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",(void *)SYNCTEX_FRIEND(node));
printf(" SYNCTEX_NEXT_hbox:%p\n",(void *)SYNCTEX_NEXT_hbox(node));
}
}
static void _synctex_log_small_node(synctex_node_t node) {
if (node) {
printf("%s:%i,%i:%i,%i",
synctex_node_isa(node),
SYNCTEX_TAG(node),
SYNCTEX_LINE(node),
SYNCTEX_HORIZ(node),
SYNCTEX_VERT(node));
SYNCTEX_PRINT_CHARINDEX;
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));
}
}
static void _synctex_log_medium_node(synctex_node_t node) {
if (node) {
printf("%s:%i,%i:%i,%i:%i",
synctex_node_isa(node),
SYNCTEX_TAG(node),
SYNCTEX_LINE(node),
SYNCTEX_HORIZ(node),
SYNCTEX_VERT(node),
SYNCTEX_WIDTH(node));
SYNCTEX_PRINT_CHARINDEX;
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));
}
}
static void _synctex_log_void_box(synctex_node_t node) {
if (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));
SYNCTEX_PRINT_CHARINDEX;
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));
}
}
static void _synctex_log_vbox(synctex_node_t node) {
if (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));
SYNCTEX_PRINT_CHARINDEX;
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",(void *)SYNCTEX_FRIEND(node));
printf(" SYNCTEX_NEXT_hbox:%p\n",(void *)SYNCTEX_NEXT_hbox(node));
}
}
static void _synctex_log_hbox(synctex_node_t node) {
if (node) {
printf("%s",synctex_node_isa(node));
printf(":%i",SYNCTEX_TAG(node));
printf(",%i~%i*%i",SYNCTEX_LINE(node),SYNCTEX_MEAN_LINE(node),SYNCTEX_NODE_WEIGHT(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));
SYNCTEX_PRINT_CHARINDEX;
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",(void *)SYNCTEX_FRIEND(node));
printf(" SYNCTEX_NEXT_hbox:%p\n",(void *)SYNCTEX_NEXT_hbox(node));
}
}
# define SYNCTEX_DISPLAY(NODE) SYNCTEX_MSG_SEND(NODE,display)
void synctex_node_display(synctex_node_t node) {
SYNCTEX_DISPLAY(node);
}
static void _synctex_display_input(synctex_node_t node) {
if (node) {
printf("....Input:%i:%s",
SYNCTEX_TAG(node),
SYNCTEX_NAME(node));
SYNCTEX_PRINT_CHARINDEX;
SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node));
}
}
static void _synctex_display_sheet(synctex_node_t node) {
if (node) {
printf("....{%i",SYNCTEX_PAGE(node));
SYNCTEX_PRINT_CHARINDEX;
SYNCTEX_DISPLAY(SYNCTEX_CHILD(node));
printf("....}\n");
SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node));
}
}
static void _synctex_display_vbox(synctex_node_t node) {
if (node) {
printf("....[%i,%i:%i,%i:%i,%i,%i",
SYNCTEX_TAG(node),
SYNCTEX_LINE(node),
SYNCTEX_HORIZ(node),
SYNCTEX_VERT(node),
SYNCTEX_WIDTH(node),
SYNCTEX_HEIGHT(node),
SYNCTEX_DEPTH(node));
SYNCTEX_PRINT_CHARINDEX;
SYNCTEX_DISPLAY(SYNCTEX_CHILD(node));
printf("....]\n");
SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node));
}
}
static void _synctex_display_hbox(synctex_node_t node) {
if (node) {
printf("....(%i,%i~%i*%i:%i,%i:%i,%i,%i",
SYNCTEX_TAG(node),
SYNCTEX_LINE(node),
SYNCTEX_MEAN_LINE(node),
SYNCTEX_NODE_WEIGHT(node),
SYNCTEX_HORIZ(node),
SYNCTEX_VERT(node),
SYNCTEX_WIDTH(node),
SYNCTEX_HEIGHT(node),
SYNCTEX_DEPTH(node));
SYNCTEX_PRINT_CHARINDEX;
SYNCTEX_DISPLAY(SYNCTEX_CHILD(node));
printf("....)\n");
SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node));
}
}
static void _synctex_display_void_vbox(synctex_node_t node) {
if (node) {
printf("....v%i,%i;%i,%i:%i,%i,%i",
SYNCTEX_TAG(node),
SYNCTEX_LINE(node),
SYNCTEX_HORIZ(node),
SYNCTEX_VERT(node),
SYNCTEX_WIDTH(node),
SYNCTEX_HEIGHT(node),
SYNCTEX_DEPTH(node));
SYNCTEX_PRINT_CHARINDEX;
SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node));
}
}
static void _synctex_display_void_hbox(synctex_node_t node) {
if (node) {
printf("....h%i,%i:%i,%i:%i,%i,%i",
SYNCTEX_TAG(node),
SYNCTEX_LINE(node),
SYNCTEX_HORIZ(node),
SYNCTEX_VERT(node),
SYNCTEX_WIDTH(node),
SYNCTEX_HEIGHT(node),
SYNCTEX_DEPTH(node));
SYNCTEX_PRINT_CHARINDEX;
SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node));
}
}
static void _synctex_display_glue(synctex_node_t node) {
if (node) {
printf("....glue:%i,%i:%i,%i",
SYNCTEX_TAG(node),
SYNCTEX_LINE(node),
SYNCTEX_HORIZ(node),
SYNCTEX_VERT(node));
SYNCTEX_PRINT_CHARINDEX;
SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node));
}
}
static void _synctex_display_math(synctex_node_t node) {
if (node) {
printf("....math:%i,%i:%i,%i",
SYNCTEX_TAG(node),
SYNCTEX_LINE(node),
SYNCTEX_HORIZ(node),
SYNCTEX_VERT(node));
SYNCTEX_PRINT_CHARINDEX;
SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node));
}
}
static void _synctex_display_kern(synctex_node_t node) {
if (node) {
printf("....kern:%i,%i:%i,%i:%i",
SYNCTEX_TAG(node),
SYNCTEX_LINE(node),
SYNCTEX_HORIZ(node),
SYNCTEX_VERT(node),
SYNCTEX_WIDTH(node));
SYNCTEX_PRINT_CHARINDEX;
SYNCTEX_DISPLAY(SYNCTEX_SIBLING(node));
}
}
static void _synctex_display_boundary(synctex_node_t node) {
if (node) {
printf("....boundary:%i,%i:%i,%i",
SYNCTEX_TAG(node),
SYNCTEX_LINE(node),
SYNCTEX_HORIZ(node),
SYNCTEX_VERT(node));
SYNCTEX_PRINT_CHARINDEX;
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
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);
synctex_status_t _synctex_scan_postamble(synctex_scanner_t scanner);
synctex_status_t _synctex_setup_visible_box(synctex_node_t box);
synctex_status_t _synctex_hbox_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);
/* 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 defined(SYNCTEX_USE_CHARINDEX)
scanner->charindex_offset += SYNCTEX_CUR - SYNCTEX_START;
# endif
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,(int)(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 go 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,SYNCTEX_INPUT_MARK);
if (statusinput);
scanner->input = input;
# if SYNCTEX_VERBOSE
synctex_node_log(input);
# endif
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_hbox_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);
}
goto prepare_loop;
}
/* The child loop means that we go down 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) {
/* Update the mean line number */
synctex_node_t node = SYNCTEX_CHILD(parent);
if (node) {
unsigned int node_weight = 0;
unsigned int cumulated_line_numbers = 0;
do {
if (synctex_node_type(node)==synctex_node_type_hbox) {
if (SYNCTEX_NODE_WEIGHT(node)) {
node_weight += SYNCTEX_NODE_WEIGHT(node);
cumulated_line_numbers += SYNCTEX_MEAN_LINE(node)*SYNCTEX_NODE_WEIGHT(node);
} else {
++node_weight;
cumulated_line_numbers += SYNCTEX_MEAN_LINE(node);
}
} else {
++node_weight;
cumulated_line_numbers += SYNCTEX_LINE(node);
}
} while ((node = SYNCTEX_SIBLING(node)));
SYNCTEX_MEAN_LINE(parent)=(cumulated_line_numbers + node_weight/2)/node_weight;
SYNCTEX_NODE_WEIGHT(parent)=node_weight;
} else {
SYNCTEX_MEAN_LINE(parent)=SYNCTEX_LINE(parent);
SYNCTEX_NODE_WEIGHT(parent)=1;
}
if (NULL == child) {
/* Only boxes with no children are friends,
* boxes with children are indirectly friends through one of their contained nodes. */
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_hbox(box,parent);
box = parent;
child = parent;
parent = SYNCTEX_PARENT(child);
} else {
_synctex_error("Unexpected end 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);
# if SYNCTEX_VERBOSE
synctex_node_log(child);
# endif
goto sibling_loop;
} else {
_synctex_error("Can't create vbox record.");
SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
}
} else if (*SYNCTEX_CUR == SYNCTEX_CHAR_VOID_HBOX) {
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;
}
}
/* 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
}
# define SYNCTEX_APPEND_SHEET(SCANNER,SHEET) if (SCANNER->sheet) {\
synctex_node_t last_sheet = SCANNER->sheet;\
synctex_node_t next_sheet = NULL;\
while ((next_sheet = SYNCTEX_SIBLING(last_sheet))) {\
last_sheet = next_sheet;\
}\
SYNCTEX_SET_SIBLING(last_sheet,SHEET);\
} else {\
SCANNER->sheet = SHEET;\
}
/* 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 (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("Internal inconsistency, bad SYNCTEX_BUFFER_SIZE (1)");
return NULL;
}
/* for integers: */
if (SYNCTEX_BUFFER_SIZE < SYNCTEX_BUFFER_MIN_SIZE) {
_synctex_error("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("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;
}
/* 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. */
static 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("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("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("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("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("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)) {
free(build_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,"/")) {
free(build_output);
return -2;
}
}
/* Append the last path component of the output. */
if (build_output != strcat(build_output,lpc)) {
free(build_output);
return -3;
}
result = __synctex_open(build_output,synctex_name_ref,file_ref,add_quotes,io_mode_ref);
free(build_output);
return result;
}
free(build_output);
}
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;
# define DEFINE_synctex_scanner_class(NAME)\
scanner->class[synctex_node_type_##NAME] = synctex_class_##NAME;\
(scanner->class[synctex_node_type_##NAME]).scanner = scanner
DEFINE_synctex_scanner_class(sheet);
DEFINE_synctex_scanner_class(input);
DEFINE_synctex_scanner_class(hbox);
DEFINE_synctex_scanner_class(void_hbox);
DEFINE_synctex_scanner_class(vbox);
DEFINE_synctex_scanner_class(void_vbox);
DEFINE_synctex_scanner_class(kern);
DEFINE_synctex_scanner_class(glue);
DEFINE_synctex_scanner_class(math);
DEFINE_synctex_scanner_class(boundary);
SYNCTEX_START = (char *)malloc(SYNCTEX_BUFFER_SIZE+1); /* one more character for null termination */
if (NULL == SYNCTEX_START) {
_synctex_error("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;
# if defined(SYNCTEX_USE_CHARINDEX)
scanner->charindex_offset = -SYNCTEX_BUFFER_SIZE;
# endif
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);
// 2011 version
name = _synctex_base_name(name);
input = scanner->input;
do {
if (_synctex_is_equivalent_file_name(name,_synctex_base_name(SYNCTEX_NAME(input)))) {
synctex_node_t other_input = input;
while((other_input = SYNCTEX_SIBLING(other_input)) != NULL) {
if (_synctex_is_equivalent_file_name(name,_synctex_base_name(SYNCTEX_NAME(other_input)))
&& (strlen(SYNCTEX_NAME(input))!=strlen(SYNCTEX_NAME(other_input))
|| strncmp(SYNCTEX_NAME(other_input),SYNCTEX_NAME(input),strlen(SYNCTEX_NAME(input))))) {
// There is a second possible candidate
return 0;
}
}
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;
}
synctex_charindex_t synctex_node_charindex(synctex_node_t node) {
return node?SYNCTEX_CHARINDEX(node):0;
}
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_mean_line(synctex_node_t node) {
return node?(node->class->type==synctex_node_type_hbox?SYNCTEX_MEAN_LINE(node):SYNCTEX_LINE(node)):-1;
}
int synctex_node_child_count(synctex_node_t node) {
return node?(node->class->type==synctex_node_type_hbox?SYNCTEX_NODE_WEIGHT(node):0):-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(synctex_scanner_t scanner,int page) {
if (scanner) {
synctex_node_t sheet = scanner->sheet;
while(sheet) {
if (page == SYNCTEX_PAGE(sheet)) {
return sheet;
}
sheet = SYNCTEX_SIBLING(sheet);
}
}
return NULL;
}
synctex_node_t synctex_sheet_content(synctex_scanner_t scanner,int page) {
if (scanner) {
return SYNCTEX_CHILD(synctex_sheet(scanner,page));
}
return NULL;
}
# ifdef SYNCTEX_NOTHING
# pragma mark -
# pragma mark Query
# endif
synctex_status_t 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 then keep just a few nodes.
* Order first the best node. */
if ((SYNCTEX_START) && (SYNCTEX_END)) {
unsigned int best_match = -1;
unsigned int next_match = -1;
unsigned int best_weight = 0;
synctex_node_t * best_ref = NULL;
synctex_node_t * start_ref = (synctex_node_t *)SYNCTEX_START;
synctex_node_t * end_ref = (synctex_node_t *)SYNCTEX_END;
--end_ref;
while (start_ref < end_ref) {
node = *start_ref;
*start_ref = *end_ref;
*end_ref = node;
++start_ref;
--end_ref;
}
/* Now reorder the nodes to put first the one which fits best.
* The idea is to walk along the list of nodes and pick up the first one
* which line info is exactly the mean line of its parent, or at least very close.
* Then we choose among all such node the one with the maximum number of child nodes.
* Then we switch with the first node.
*/
best_ref = start_ref = (synctex_node_t *)SYNCTEX_START;
node = *start_ref;
best_match = abs(SYNCTEX_LINE(node)-SYNCTEX_MEAN_LINE(SYNCTEX_PARENT(node)));
end_ref = (synctex_node_t *)SYNCTEX_END;
while (++start_refbest_weight)) {
best_match = next_match;
best_ref = start_ref;
best_weight = SYNCTEX_NODE_WEIGHT(parent);
}
}
node = *best_ref;
*best_ref = *(synctex_node_t *)SYNCTEX_START;
*(synctex_node_t *)SYNCTEX_START = node;
/* Basically, we keep the first node for each parent.
* More precisely, we keep only nodes that are not children 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_hbox(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_hbox(node))) {
do {
if (_synctex_point_in_box(hitPoint,other_node,synctex_YES)) {
node = _synctex_smallest_container(other_node,node);
}
} while((other_node = SYNCTEX_NEXT_hbox(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_hbox(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
/* 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 {
gzFile 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.90/server/synctex_parser.h 0000664 0000000 0000000 00000037114 13407234246 0020114 0 ustar 00root root 0000000 0000000 /*
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.18
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
# define SYNCTEX_VERSION_STRING "1.18"
/* 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.
*/
typedef long synctex_status_t;
synctex_status_t synctex_display_query(synctex_scanner_t scanner,const char * name,int line,int column);
synctex_status_t 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(synctex_scanner_t scanner,int page);
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 the location in the synctex file where it is defined.
*/
typedef unsigned int synctex_charindex_t;
synctex_charindex_t synctex_node_charindex(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);
/* In order to enhance forward synchronization,
* non void horizontal boxes have supplemental cached information.
* The mean line is the average of the line numbers of the included nodes.
* The child count is the number of chidren.
*/
int synctex_node_mean_line(synctex_node_t node);
int synctex_node_child_count(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.90/server/synctex_parser_local.h 0000664 0000000 0000000 00000003312 13407234246 0021257 0 ustar 00root root 0000000 0000000 /*
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.18
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.90/server/synctex_parser_readme.txt 0000664 0000000 0000000 00000020572 13407234246 0022021 0 ustar 00root root 0000000 0000000 This 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
1.17: Fri Oct 14 08:15:16 UTC 2011
This concerns the synctex command line tool and 3rd party developers.
TeX and friends are not concerned by these changes.
- synctex_parser.c: cosmetic changes to enhance code readability
- Better forward synchronization.
The problem occurs for example with LaTeX \item command.
The fact is that this command creates nodes at parse time but these nodes are used only
after the text material of the \item is displayed on the page. The consequence is that sometimes,
forward synchronization spots an irrelevant point from the point of view of the editing process.
This was due to some very basic filtering policy, where a somehow arbitrary choice was made when
many different possibilities where offered for synchronisation.
Now, forward synchronization prefers nodes inside an hbox with as many acceptable spots as possible.
This is achieved with the notion of mean line and node weight.
- Adding support for the new file naming convention with './'
+ function synctex_ignore_leading_dot_slash_in_path replaces synctex_ignore_leading_dot_slash
+ function _synctex_is_equivalent_file_name is more permissive
Previously, the function synctex_scanner_get_tag would give an answer only when
the given file name was EXACTLY one of the file names listed in the synctex file.
The we added some changes accepting for example 'foo.tex' instead of './foo.tex'.
Now we have an even looser policy for dealing with file names.
If the given file name does not match exactly one the file names of the synctex file,
then we try to match the base names. If there is only one match of the base names,
then it is taken as a match for the whole names.
The base name is defined as following:
./foo => foo
/my///.////foo => foo
/foo => /foo
/my//.foo => /my//.foo
1.17: Tue Mar 13 10:10:03 UTC 2012
- minor changes, no version changes
- syntax man pages are fixed as suggested by M. Shimata
see mail to tex-live@tug.org titled "syntax.5 has many warnings from groff" and "syntax.1 use invalid macro for mdoc"
1.17: Tue Jan 14 09:55:00 UTC 2014
- fixed a segfault, from Sebastian Ramacher
1.17: Mon Aug 04
- fixed a memory leak
1.18: Thu Jun 25 11:36:05 UTC 2015
- nested sheets now fully supported (does it make sense in TeX)
- cosmetic changes: uniform indentation
- suppression of warnings, mainly long/int ones. In short, zlib likes ints when size_t likes longs.
- CLI synctex tool can build out of TeXLive (modulo appropriate options passed to the compiler)
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-2014 jerome DOT laurens AT u-bourgogne DOT fr
pdf-tools-0.90/server/synctex_parser_utils.c 0000664 0000000 0000000 00000042244 13407234246 0021327 0 ustar 00root root 0000000 0000000 /*
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.18
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
#if defined(__OS2__)
#define SYNCTEX_OS2 1
#endif
#ifdef _WIN32_WINNT_WINXP
#define SYNCTEX_RECENT_WINDOWS 1
#endif
#ifdef SYNCTEX_WINDOWS
#include
#include /* Use shlwapi.lib */
#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;
# if defined(SYNCTEX_WINDOWS)
last_component = PathFindFileName(string);
last_extension = PathFindExtension(string);
if(last_extension == NULL)return;
if(last_component == NULL)last_component = string;
if(last_extension>last_component){/* filter out paths like "my/dir/.hidden" */
last_extension[0] = '\0';
}
# else
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;
}
}
# if defined(SYNCTEX_OS2)
/* On OS2, the '\' is also a path separator. */
while((next = strstr(last_component,"\\"))){
last_component = next+1;
}
# endif /* SYNCTEX_OS2 */
/* 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';
}
}
# endif /* SYNCTEX_WINDOWS */
}
}
synctex_bool_t synctex_ignore_leading_dot_slash_in_path(const char ** name_ref)
{
if (SYNCTEX_IS_DOT((*name_ref)[0]) && SYNCTEX_IS_PATH_SEPARATOR((*name_ref)[1])) {
do {
(*name_ref) += 2;
while (SYNCTEX_IS_PATH_SEPARATOR((*name_ref)[0])) {
++(*name_ref);
}
} while(SYNCTEX_IS_DOT((*name_ref)[0]) && SYNCTEX_IS_PATH_SEPARATOR((*name_ref)[1]));
return synctex_YES;
}
return synctex_NO;
}
/* The base name is necessary to deal with the 2011 file naming convention...
* path is a '\0' terminated string
* The return value is the trailing part of the argument,
* just following the first occurrence of the regexp pattern "[^|/|\].[\|/]+".*/
const char * _synctex_base_name(const char *path) {
const char * ptr = path;
do {
if (synctex_ignore_leading_dot_slash_in_path(&ptr)) {
return ptr;
}
do {
if (!*(++ptr)) {
return path;
}
} while (!SYNCTEX_IS_PATH_SEPARATOR(*ptr));
} while (*(++ptr));
return path;
}
/* 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 */
synctex_ignore_leading_dot_slash_in_path(&lhs);
synctex_ignore_leading_dot_slash_in_path(&rhs);
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;
}
++lhs;
++rhs;
synctex_ignore_leading_dot_slash_in_path(&lhs);
synctex_ignore_leading_dot_slash_in_path(&rhs);
goto next_character;
} else if (SYNCTEX_IS_PATH_SEPARATOR(*rhs)) {/* rhs points to a path separator but not lhs */
return synctex_NO;
} else if (SYNCTEX_ARE_PATH_CHARACTERS_EQUAL(*lhs,*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;
}
synctex_bool_t _synctex_path_is_absolute(const char * name) {
if(!strlen(name)) {
return synctex_NO;
}
# if defined(SYNCTEX_WINDOWS) || defined(SYNCTEX_OS2)
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.90/server/synctex_parser_utils.h 0000664 0000000 0000000 00000013215 13407234246 0021330 0 ustar 00root root 0000000 0000000 /*
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.18
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 defined(_WIN32) || defined(__OS2__)
# define SYNCTEX_CASE_SENSITIVE_PATH 0
# define SYNCTEX_IS_PATH_SEPARATOR(c) ('/' == c || '\\' == c)
# else
# define SYNCTEX_CASE_SENSITIVE_PATH 1
# define SYNCTEX_IS_PATH_SEPARATOR(c) ('/' == c)
# endif
# if defined(_WIN32) || defined(__OS2__)
# define SYNCTEX_IS_DOT(c) ('.' == c)
# else
# define SYNCTEX_IS_DOT(c) ('.' == c)
# endif
# if SYNCTEX_CASE_SENSITIVE_PATH
# define SYNCTEX_ARE_PATH_CHARACTERS_EQUAL(left,right) (left != right)
# else
# define SYNCTEX_ARE_PATH_CHARACTERS_EQUAL(left,right) (toupper(left) != toupper(right))
# 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);
/* Description forthcoming...*/
const char * _synctex_base_name(const char *path);
/* 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);
synctex_bool_t synctex_ignore_leading_dot_slash_in_path(const char ** name);
#ifdef __cplusplus
}
#endif
#endif
pdf-tools-0.90/server/synctex_parser_version.txt 0000664 0000000 0000000 00000000004 13407234246 0022235 0 ustar 00root root 0000000 0000000 1.18 pdf-tools-0.90/server/test/ 0000775 0000000 0000000 00000000000 13407234246 0015643 5 ustar 00root root 0000000 0000000 pdf-tools-0.90/server/test/Makefile 0000664 0000000 0000000 00000002443 13407234246 0017306 0 ustar 00root root 0000000 0000000 # +-----------------------------------------------------------+
# * Docker tests for the autobuild script
# +-----------------------------------------------------------+
# List of available OS.
DOCKER_OS = $(patsubst %.Dockerfile.in, %, \
$(notdir $(wildcard docker/templates/*.Dockerfile.in)))
# Arguments to pass to docker build .
DOCKER_BUILD_ARGS = -q
# Advice make not to delete these "intermediate" files.
.PRECIOUS: docker/%.Dockerfile docker/.%.build
.PHONY: all test check docker/build
all: docker/test
test: docker/test
check: docker/test
# Create the Dockerfile
docker/%.Dockerfile: docker/templates/%.Dockerfile.in \
docker/templates/Dockerfile.in
@echo Creating Dockerfile for target $*
cat $^ > $@
# Build the Dockerfile
docker/.%.build: docker/%.Dockerfile ../autobuild docker/lib
@echo Building target $*
docker build $(DOCKER_BUILD_ARGS) -t epdfinfo/$* -f $< ../
touch $@
# Run the Dockerfile
docker/%: docker/.%.build
@echo Running tests on target $*
docker run epdfinfo/$*
# Run all Dockerfiles
docker/test: docker/build $(patsubst %, docker/%, $(DOCKER_OS))
# Build all Dockerfiles
docker/build: $(patsubst %, docker/.%.build, $(DOCKER_OS))
clean:
rm -f -- docker/.[^.]*.build
rm -f -- docker/*.Dockerfile
print:
@for os in $(DOCKER_OS); do echo $$os; done | sort
pdf-tools-0.90/server/test/docker/ 0000775 0000000 0000000 00000000000 13407234246 0017112 5 ustar 00root root 0000000 0000000 pdf-tools-0.90/server/test/docker/.gitignore 0000664 0000000 0000000 00000000025 13407234246 0021077 0 ustar 00root root 0000000 0000000 *.Dockerfile
*.build
pdf-tools-0.90/server/test/docker/lib/ 0000775 0000000 0000000 00000000000 13407234246 0017660 5 ustar 00root root 0000000 0000000 pdf-tools-0.90/server/test/docker/lib/run-tests 0000775 0000000 0000000 00000000277 13407234246 0021560 0 ustar 00root root 0000000 0000000 #!/bin/sh
PATH="$(dirname "$0")":$PATH
set -e
yes-or-enter | ./autobuild -i /bin
yes-or-enter | ./autobuild -i /usr/bin | \
grep -q "Skipping package installation (already installed)"
pdf-tools-0.90/server/test/docker/lib/yes-or-enter 0000775 0000000 0000000 00000000176 13407234246 0022143 0 ustar 00root root 0000000 0000000 #!/bin/bash
# Step over prompts from the package-manager.
if [ -f /etc/arch-release ]; then
yes ''
else
yes
fi
pdf-tools-0.90/server/test/docker/templates/ 0000775 0000000 0000000 00000000000 13407234246 0021110 5 ustar 00root root 0000000 0000000 pdf-tools-0.90/server/test/docker/templates/Dockerfile.in 0000664 0000000 0000000 00000000152 13407234246 0023505 0 ustar 00root root 0000000 0000000 ADD . /epdfinfo
WORKDIR /epdfinfo
RUN make -s distclean || true
CMD ["sh", "./test/docker/lib/run-tests"]
pdf-tools-0.90/server/test/docker/templates/arch.Dockerfile.in 0000664 0000000 0000000 00000000236 13407234246 0024424 0 ustar 00root root 0000000 0000000 # -*- dockerfile -*-
FROM base/archlinux
RUN pacman -Syu --noconfirm --noprogressbar && \
pacman -S --noconfirm --noprogressbar poppler-glib base-devel
pdf-tools-0.90/server/test/docker/templates/centos-7.Dockerfile.in 0000664 0000000 0000000 00000000146 13407234246 0025146 0 ustar 00root root 0000000 0000000 # -*- dockerfile -*-
FROM centos:7
RUN yum update -y && yum install -y gcc gcc-c++ poppler-glib-devel
pdf-tools-0.90/server/test/docker/templates/debian-8.Dockerfile.in 0000664 0000000 0000000 00000000154 13407234246 0025075 0 ustar 00root root 0000000 0000000 # -*- dockerfile -*-
FROM debian:8
RUN apt-get update -y && apt-get install -y gcc g++ libpoppler-glib-dev
pdf-tools-0.90/server/test/docker/templates/debian-9.Dockerfile.in 0000664 0000000 0000000 00000000154 13407234246 0025076 0 ustar 00root root 0000000 0000000 # -*- dockerfile -*-
FROM debian:9
RUN apt-get update -y && apt-get install -y gcc g++ libpoppler-glib-dev
pdf-tools-0.90/server/test/docker/templates/fedora-24.Dockerfile.in 0000664 0000000 0000000 00000000151 13407234246 0025166 0 ustar 00root root 0000000 0000000 # -*- dockerfile -*-
FROM fedora:24
RUN dnf update -y && dnf install -y gcc gcc-c++ poppler-glib-devel
pdf-tools-0.90/server/test/docker/templates/fedora-25.Dockerfile.in 0000664 0000000 0000000 00000000151 13407234246 0025167 0 ustar 00root root 0000000 0000000 # -*- dockerfile -*-
FROM fedora:25
RUN dnf update -y && dnf install -y gcc gcc-c++ poppler-glib-devel
pdf-tools-0.90/server/test/docker/templates/fedora-26.Dockerfile.in 0000664 0000000 0000000 00000000151 13407234246 0025170 0 ustar 00root root 0000000 0000000 # -*- dockerfile -*-
FROM fedora:26
RUN dnf update -y && dnf install -y gcc gcc-c++ poppler-glib-devel
pdf-tools-0.90/server/test/docker/templates/gentoo.Dockerfile.in 0000664 0000000 0000000 00000000153 13407234246 0025000 0 ustar 00root root 0000000 0000000 # -*- dockerfile -*-
FROM gentoo/stage3-amd64
RUN emerge --sync && emerge sys-devel/gcc app-text/poppler
pdf-tools-0.90/server/test/docker/templates/ubuntu-14.Dockerfile.in 0000664 0000000 0000000 00000000161 13407234246 0025250 0 ustar 00root root 0000000 0000000 # -*- dockerfile -*-
FROM ubuntu:trusty
RUN apt-get update -y && apt-get install -y gcc g++ libpoppler-glib-dev
pdf-tools-0.90/server/test/docker/templates/ubuntu-16.Dockerfile.in 0000664 0000000 0000000 00000000161 13407234246 0025252 0 ustar 00root root 0000000 0000000 # -*- dockerfile -*-
FROM ubuntu:xenial
RUN apt-get update -y && apt-get install -y gcc g++ libpoppler-glib-dev
pdf-tools-0.90/server/test/docker/templates/ubuntu-17.Dockerfile.in 0000664 0000000 0000000 00000000161 13407234246 0025253 0 ustar 00root root 0000000 0000000 # -*- dockerfile -*-
FROM ubuntu:artful
RUN apt-get update -y && apt-get install -y gcc g++ libpoppler-glib-dev
pdf-tools-0.90/test/ 0000775 0000000 0000000 00000000000 13407234246 0014335 5 ustar 00root root 0000000 0000000 pdf-tools-0.90/test/encrypted.pdf 0000664 0000000 0000000 00000333364 13407234246 0017041 0 ustar 00root root 0000000 0000000 %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
MF2gvpVsBxʦ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_+&Ecu뢒,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 (͎NfV4=)
/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إxz