--- /dev/null
+#+TITLE: PDF Tools README
+#+AUTHOR: Andreas Politz
+#+EMAIL: politzaNOSPAMfh-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-emacs26
+#+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:
--- /dev/null
+
+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
--- /dev/null
+*.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
--- /dev/null
+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
--- /dev/null
+#!/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
+# Which OSs installer to use
+OS=
+
+## +-----------------------------------------------------------+
+## * Utility Functions
+## +-----------------------------------------------------------+
+
+usage()
+{
+ cat <<EOF
+usage:$(basename "$0") [--help|-n|-i DIR|[-d -D]|[--os OS]]
+
+ -n Don't do anything, but check if this OS is handled.
+
+ -i DIR Install the program in the given directory.
+
+ -d Force dependency installattion.
+
+ -D Skip dependency installattion.
+
+ --os OS Use the given OS's installer
+
+ --help Display this message.
+
+EOF
+ exit "$1"
+}
+
+# Search for command $1 in PATH. Print its absolute filename.
+which()
+{
+ if [ -z "$1" ]; then
+ return 1
+ fi
+ IFS=:
+ for dir in $PATH; do
+ if [ -x "$dir/$1" ]; then
+ printf "%s" "$dir/$1"
+ unset IFS
+ return 0
+ fi
+ done
+ unset IFS
+ return 1
+}
+
+# Quote $@ for the shell.
+quote()
+{
+ quoted=
+ for arg; do
+ qarg=$(printf "%s" "$arg" | sed -e 's/[|&;<>()$\`"'\'' ]/\\&/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
+ #include <PDFDocEncoding.h>
+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 ;;
+ --os)
+ shift
+ [ $# -gt 0 ] || usage 1
+ OS="$1"
+ ;;
+ *) 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
+ elif which brew >/dev/null 2>&1; then
+ PKGCMD=brew
+ PKGARGS=install
+ PACKAGES="pkg-config poppler automake"
+ PKG_INSTALL_AS_ROOT=
+ # homebrew install libffi as keg-only, meaning we need to set
+ # PKG_CONFIG_PATH manually so configure can find it
+ export PKG_CONFIG_PATH="$(brew --prefix libffi)/lib/pkgconfig/"
+ elif which port >/dev/null 2>&1; then
+ PKGCMD=port
+ PKGARGS=install
+ PACKAGES="pkgconfig poppler automake libpng"
+ else
+ return 1
+ fi
+ 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
+}
+
+# By Parameter --os
+os_argument() {
+ [ -z "$OS" ] && return 1
+ case $OS in
+ macos) os_macos "$@";;
+ freebsd) os_freebsd "$@";;
+ arch) os_arch "$@";;
+ centos) os_centos "$@";;
+ openbsd) os_openbsd "$@";;
+ fedora) os_fedora "$@";;
+ debian) os_debian "$@";;
+ gentoo) os_gentoo "$@";;
+ msys2) os_msys2 "$@";;
+ nixos) os_nixos "$@";;
+ *) echo "Invalid --os argument: $OS"
+ exit 1
+ esac || {
+ echo "Unable to install on this system as $OS"
+ exit 1
+ }
+}
+
+## +-----------------------------------------------------------+
+## * Figure out were we are, install deps and build the program
+## +-----------------------------------------------------------+
+
+handle_options "$@"
+
+os_argument "$@" || \
+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:
--- /dev/null
+#!/bin/sh
+
+echo "Running autoreconf..."
+
+autoreconf -i
--- /dev/null
+# -*- Autoconf -*-
+# Process this file with autoconf to produce a configure script.
+
+AC_PREREQ([2.67])
+AC_INIT([epdfinfo], 1.0, [politzaNOSPAMfh-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.h>
+ ],[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
--- /dev/null
+/* Copyright (C) 2013, 2014 Andreas Politz
+ *
+ * Author: Andreas Politz <politzaNOSPAMfh-trier.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#include <assert.h>
+#ifdef HAVE_ERR_H
+# include <err.h>
+#endif
+#ifdef HAVE_ERROR_H
+# include <error.h>
+#endif
+#include <glib.h>
+#include <poppler.h>
+#include <cairo.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <png.h>
+#include <math.h>
+#include <regex.h>
+#include "synctex_parser.h"
+#include "epdfinfo.h"
+
+\f
+/* ================================================================== *
+ * 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;
+ }
+}
+
+\f
+/* ------------------------------------------------------------------ *
+ * 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:
+ ;
+ }
+}
+
+\f
+/* ------------------------------------------------------------------ *
+ * 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');
+}
+
+
+\f
+/* ================================================================== *
+ * 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 ();
+}
+
+\f
+/* ================================================================== *
+ * 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);
+}
--- /dev/null
+// Copyright (C) 2013, 2014 Andreas Politz
+
+// Author: Andreas Politz <politzaNOSPAMfh-trier.de>
+
+// 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 <http://www.gnu.org/licenses/>.
+
+#ifndef _EPDF_H_
+#define _EPDF_H_ _EPDF_H_
+#include "config.h"
+#include <glib.h>
+#include <poppler.h>
+#include <png.h>
+
+/* 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_ */
--- /dev/null
+# ===========================================================================
+# 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 <guidodNOSPAMgmx.de>
+# Copyright (c) 2011 Maarten Bosmans <mkbosmansNOSPAMgmail.com>
+#
+# 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 <https://www.gnu.org/licenses/>.
+#
+# 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
--- /dev/null
+// 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 <http://www.gnu.org/licenses/>.
+
+#include <config.h>
+#include <PDFDocEncoding.h>
+#include <Annot.h>
+#include <glib.h>
+#include <glib-object.h>
+#include <poppler-features.h>
+
+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<AnnotMarkup *>(POPPLER_ANNOT (poppler_annot)->annot);
+ text = (GooString*) annot->getDate ();
+
+ return text ? _xpoppler_goo_string_to_utf8 (text) : NULL;
+ }
+}
--- /dev/null
+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}
--- /dev/null
+/*
+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 <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <limits.h>
+
+#if defined(HAVE_LOCALE_H)
+#include <locale.h>
+#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 <zlib.h>
+# 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.
+ * status<SYNCTEX_STATUS_EOF means an error
+ */
+
+/* When the end of the synctex file has been reached: */
+# define SYNCTEX_STATUS_EOF 0
+/* When the function could not return the value it was asked for: */
+# define SYNCTEX_STATUS_NOT_OK (SYNCTEX_STATUS_EOF+1)
+/* When the function returns the value it was asked for: */
+# define SYNCTEX_STATUS_OK (SYNCTEX_STATUS_NOT_OK+1)
+/* Generic error: */
+# define SYNCTEX_STATUS_ERROR -1
+/* Parameter error: */
+# define SYNCTEX_STATUS_BAD_ARGUMENT -2
+
+# define SYNCTEX_FILE (scanner->file)
+
+/* 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 0<already_read<=SYNCTEX_BUFFER_SIZE - available, such that
+ * SYNCTEX_CUR + already_read = SYNCTEX_START + available + already_read <= SYNCTEX_START + SYNCTEX_BUFFER_SIZE */
+ SYNCTEX_END = SYNCTEX_CUR + already_read;
+ /* If the end of the file was reached, all the required SYNCTEX_BUFFER_SIZE - available
+ * may not be filled with values from the file.
+ * In that case, the buffer should stop properly after already_read characters. */
+ * SYNCTEX_END = '\0';
+ SYNCTEX_CUR = SYNCTEX_START;
+ size = SYNCTEX_END - SYNCTEX_CUR; /* == old available + already_read*/
+ return SYNCTEX_STATUS_OK; /* May be available is less than size, the caller will have to test. */
+ } else if (0>already_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<SYNCTEX_END) {
+ if (*SYNCTEX_CUR == '\n') {
+ ++SYNCTEX_CUR;
+ available = 1;
+ return _synctex_buffer_get_available_size(scanner, &available);
+ }
+ ++SYNCTEX_CUR;
+ }
+ /* Here, we have SYNCTEX_CUR == SYNCTEX_END, such that the next call to _synctex_buffer_get_available_size
+ * will read another bunch of synctex file. Little by little, we advance to the end of the file. */
+ available = 1;
+ status = _synctex_buffer_get_available_size(scanner, &available);
+ if (status<=0) {
+ return status;
+ }
+ goto infinite_loop;
+}
+
+/* Scan the given string.
+ * Both scanner and the_string must not be NULL, and the_string must not be 0 length.
+ * SYNCTEX_STATUS_OK is returned if the string is found,
+ * SYNCTEX_STATUS_EOF is returned when the EOF is reached,
+ * SYNCTEX_STATUS_NOT_OK is returned is the string is not found,
+ * an error status is returned otherwise.
+ * This is a critical method because buffering renders things more difficult.
+ * The given string might be as long as the maximum size_t value.
+ * As side effect, the buffer state may have changed if the given argument string can't fit into the buffer.
+ */
+synctex_status_t _synctex_match_string(synctex_scanner_t scanner, const char * the_string) {
+ size_t tested_len = 0; /* the number of characters at the beginning of the_string that match */
+ size_t remaining_len = 0; /* the number of remaining characters of the_string that should match */
+ size_t available = 0;
+ synctex_status_t status = 0;
+ if (NULL == scanner || NULL == the_string) {
+ return SYNCTEX_STATUS_BAD_ARGUMENT;
+ }
+ remaining_len = strlen(the_string); /* All the_string should match */
+ if (0 == remaining_len) {
+ return SYNCTEX_STATUS_BAD_ARGUMENT;
+ }
+ /* How many characters available in the buffer? */
+ available = remaining_len;
+ status = _synctex_buffer_get_available_size(scanner,&available);
+ if (status<SYNCTEX_STATUS_EOF) {
+ return status;
+ }
+ /* Maybe we have less characters than expected because the buffer is too small. */
+ if (available>=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 (status<SYNCTEX_STATUS_EOF) {
+ return status; /* This is an error, no need to go further. */
+ }
+ if (available==0) {
+ /* Missing characters: recover the initial state of the file and return. */
+return_NOT_OK:
+ if (offset != gzseek(SYNCTEX_FILE,offset,SEEK_SET)) {
+ /* This is a critical error, we could not recover the previous state. */
+ _synctex_error("can't seek file");
+ return SYNCTEX_STATUS_ERROR;
+ }
+ /* Next time we are asked to fill the buffer,
+ * we will read a complete bunch of text from the file. */
+ SYNCTEX_CUR = SYNCTEX_END;
+ return SYNCTEX_STATUS_NOT_OK;
+ }
+ if (available<remaining_len) {
+ /* We'll have to loop one more time. */
+ if (strncmp((char *)SYNCTEX_CUR,the_string,available)) {
+ /* This is not the expected string, recover the previous state and return. */
+ goto return_NOT_OK;
+ }
+ /* Advance the_string to the first 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. */
+ return SYNCTEX_STATUS_OK;
+ }
+ goto more_characters;
+ }
+ /* This is the last step. */
+ if (strncmp((char *)SYNCTEX_CUR,the_string,remaining_len)) {
+ /* This is not the expected string, recover the previous state and return. */
+ goto return_NOT_OK;
+ }
+ goto return_OK;
+ } else {
+ /* The buffer can't contain the given string argument, and the EOF was reached */
+ return SYNCTEX_STATUS_EOF;
+ }
+}
+
+/* Used when parsing the synctex file.
+ * Decode an integer.
+ * First, field separators, namely ':' and ',' characters are skipped
+ * The returned value is negative if there is an unrecoverable error.
+ * It is SYNCTEX_STATUS_NOT_OK if an integer could not be parsed, for example
+ * if the characters at the current cursor position are not digits or
+ * if the end of the file has been reached.
+ * It is SYNCTEX_STATUS_OK if an int has been successfully parsed.
+ * The given scanner argument must not be NULL, on the contrary, value_ref may be NULL.
+ */
+synctex_status_t _synctex_decode_int(synctex_scanner_t scanner, int* value_ref) {
+ char * ptr = NULL;
+ char * end = NULL;
+ int result = 0;
+ size_t available = 0;
+ synctex_status_t status = 0;
+ if (NULL == scanner) {
+ return SYNCTEX_STATUS_BAD_ARGUMENT;
+ }
+ available = SYNCTEX_BUFFER_MIN_SIZE;
+ status = _synctex_buffer_get_available_size(scanner, &available);
+ if (status<SYNCTEX_STATUS_EOF) {
+ return status;/* Forward error. */
+ }
+ if (available==0) {
+ return SYNCTEX_STATUS_EOF;/* it is the end of file. */
+ }
+ ptr = SYNCTEX_CUR;
+ if (*ptr==':' || *ptr==',') {
+ ++ptr;
+ --available;
+ if (available==0) {
+ return SYNCTEX_STATUS_NOT_OK;/* It is not possible to scan an int */
+ }
+ }
+ result = (int)strtol(ptr, &end, 10);
+ if (end>ptr) {
+ 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 (end<SYNCTEX_END) {
+ if (*end == '\n') {
+ /* OK, we found where to stop */
+ len = end - SYNCTEX_CUR;
+ if (current_size>UINT_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_size<UINT_MAX,
+ * where we have assumed that len<UINT_MAX */
+ 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 += len;/* Advance to the terminating '\n' */
+ return SYNCTEX_STATUS_OK;
+ }
+ free(* value_ref);
+ * value_ref = NULL;
+ _synctex_error("could not copy memory (1).");
+ return SYNCTEX_STATUS_ERROR;
+ }
+ _synctex_error("could not allocate memory (1).");
+ return SYNCTEX_STATUS_ERROR;
+ } else {
+ ++end;
+ goto next_character;
+ }
+ } else {
+ /* end == SYNCTEX_END */
+ len = SYNCTEX_END - SYNCTEX_CUR;
+ if (current_size>UINT_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 (status<SYNCTEX_STATUS_OK) {
+ return status;
+ }
+ /* Create a node */
+ input = _synctex_new_input(scanner);
+ if (NULL == input) {
+ _synctex_error("could not create an input node.");
+ return SYNCTEX_STATUS_ERROR;
+ }
+ /* Decode the synctag */
+ status = _synctex_decode_int(scanner,&(SYNCTEX_TAG(input)));
+ if (status<SYNCTEX_STATUS_OK) {
+ _synctex_error("bad format of input node.");
+ SYNCTEX_FREE(input);
+ return status;
+ }
+ /* The next character is a field separator, we expect one character in the buffer. */
+ available = 1;
+ status = _synctex_buffer_get_available_size(scanner, &available);
+ if (status<=SYNCTEX_STATUS_ERROR) {
+ return status;
+ }
+ if (0 == available) {
+ return SYNCTEX_STATUS_EOF;
+ }
+ /* We can now safely advance to the next character, stepping over the field separator. */
+ ++SYNCTEX_CUR;
+ --available;
+ /* Then we scan the file name */
+ status = _synctex_decode_string(scanner,&(SYNCTEX_NAME(input)));
+ if (status<SYNCTEX_STATUS_OK) {
+ SYNCTEX_FREE(input);
+ return status;
+ }
+ /* Prepend this input node to the input linked list of the scanner */
+ SYNCTEX_SET_SIBLING(input,scanner->input);
+ 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 (status<SYNCTEX_STATUS_NOT_OK) {
+ return status;
+ } else if (status == SYNCTEX_STATUS_NOT_OK) {
+ status = _synctex_next_line(scanner);
+ if (status<SYNCTEX_STATUS_OK) {
+ return status;
+ }
+ goto not_found;
+ }
+ /* A line is found, scan the value */
+ return (*decoder)(scanner,value_ref);
+}
+
+/* Used when parsing the synctex file.
+ * Read the preamble.
+ */
+synctex_status_t _synctex_scan_preamble(synctex_scanner_t scanner) {
+ synctex_status_t status = 0;
+ if (NULL == scanner) {
+ return SYNCTEX_STATUS_BAD_ARGUMENT;
+ }
+ status = _synctex_scan_named(scanner,"SyncTeX Version:",&(scanner->version),(synctex_decoder_t)&_synctex_decode_int);
+ if (status<SYNCTEX_STATUS_OK) {
+ return status;
+ }
+ status = _synctex_next_line(scanner);
+ if (status<SYNCTEX_STATUS_OK) {
+ return status;
+ }
+ /* Read all the input records */
+ do {
+ status = _synctex_scan_input(scanner);
+ if (status<SYNCTEX_STATUS_NOT_OK) {
+ return status;
+ }
+ } while(status == SYNCTEX_STATUS_OK);
+ /* the loop exits when status == SYNCTEX_STATUS_NOT_OK */
+ /* Now read all the required settings. */
+ status = _synctex_scan_named(scanner,"Output:",&(scanner->output_fmt),(synctex_decoder_t)&_synctex_decode_string);
+ if (status<SYNCTEX_STATUS_NOT_OK) {
+ return status;
+ }
+ status = _synctex_next_line(scanner);
+ if (status<SYNCTEX_STATUS_OK) {
+ return status;
+ }
+ status = _synctex_scan_named(scanner,"Magnification:",&(scanner->pre_magnification),(synctex_decoder_t)&_synctex_decode_int);
+ if (status<SYNCTEX_STATUS_OK) {
+ return status;
+ }
+ status = _synctex_next_line(scanner);
+ if (status<SYNCTEX_STATUS_OK) {
+ return status;
+ }
+ status = _synctex_scan_named(scanner,"Unit:",&(scanner->pre_unit),(synctex_decoder_t)&_synctex_decode_int);
+ if (status<SYNCTEX_STATUS_OK) {
+ return status;
+ }
+ status = _synctex_next_line(scanner);
+ if (status<SYNCTEX_STATUS_OK) {
+ return status;
+ }
+ status = _synctex_scan_named(scanner,"X Offset:",&(scanner->pre_x_offset),(synctex_decoder_t)&_synctex_decode_int);
+ if (status<SYNCTEX_STATUS_OK) {
+ return status;
+ }
+ status = _synctex_next_line(scanner);
+ if (status<SYNCTEX_STATUS_OK) {
+ return status;
+ }
+ status = _synctex_scan_named(scanner,"Y Offset:",&(scanner->pre_y_offset),(synctex_decoder_t)&_synctex_decode_int);
+ if (status<SYNCTEX_STATUS_OK) {
+ return status;
+ }
+ return _synctex_next_line(scanner);
+}
+
+/* parse a float with a dimension */
+synctex_status_t _synctex_scan_float_and_dimension(synctex_scanner_t scanner, float * value_ref) {
+ synctex_status_t status = 0;
+ char * endptr = NULL;
+ float f = 0;
+#ifdef HAVE_SETLOCALE
+ char * loc = setlocale(LC_NUMERIC, NULL);
+#endif
+ size_t available = 0;
+ if (NULL == scanner || NULL == value_ref) {
+ return SYNCTEX_STATUS_BAD_ARGUMENT;
+ }
+ available = SYNCTEX_BUFFER_MIN_SIZE;
+ status = _synctex_buffer_get_available_size(scanner, &available);
+ if (status<SYNCTEX_STATUS_EOF) {
+ _synctex_error("problem with float.");
+ return status;
+ }
+#ifdef HAVE_SETLOCALE
+ setlocale(LC_NUMERIC, "C");
+#endif
+ f = strtod(SYNCTEX_CUR,&endptr);
+#ifdef HAVE_SETLOCALE
+ setlocale(LC_NUMERIC, loc);
+#endif
+ if (endptr == SYNCTEX_CUR) {
+ _synctex_error("a float was expected.");
+ return SYNCTEX_STATUS_ERROR;
+ }
+ SYNCTEX_CUR = endptr;
+ if ((status = _synctex_match_string(scanner,"in")) >= SYNCTEX_STATUS_OK) {
+ f *= 72.27f*65536;
+ } else if (status<SYNCTEX_STATUS_EOF) {
+report_unit_error:
+ _synctex_error("problem with unit.");
+ return status;
+ } else if ((status = _synctex_match_string(scanner,"cm")) >= 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 (status<SYNCTEX_STATUS_NOT_OK) {
+ return status;
+ }
+ if (status == SYNCTEX_STATUS_NOT_OK) {
+ status = _synctex_next_line(scanner);
+ if (status<SYNCTEX_STATUS_EOF) {
+ return status;
+ } else if (status<SYNCTEX_STATUS_OK) {
+ return SYNCTEX_STATUS_OK;/* The EOF is found, we have properly scanned the file */
+ }
+ goto post_scriptum_not_found;
+ }
+ /* We found the name, advance to the next line. */
+next_line:
+ status = _synctex_next_line(scanner);
+ if (status<SYNCTEX_STATUS_EOF) {
+ return status;
+ } else if (status<SYNCTEX_STATUS_OK) {
+ return SYNCTEX_STATUS_OK;/* The EOF is found, we have properly scanned the file */
+ }
+ /* Scanning the information */
+ status = _synctex_match_string(scanner,"Magnification:");
+ if (status == SYNCTEX_STATUS_OK ) {
+#ifdef HAVE_SETLOCALE
+ setlocale(LC_NUMERIC, "C");
+#endif
+ scanner->unit = 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 (status<SYNCTEX_STATUS_EOF){
+report_record_problem:
+ _synctex_error("Problem reading the Post Scriptum records");
+ return status; /* echo the error. */
+ }
+ status = _synctex_match_string(scanner,"X Offset:");
+ if (status == SYNCTEX_STATUS_OK) {
+ status = _synctex_scan_float_and_dimension(scanner, &(scanner->x_offset));
+ if (status<SYNCTEX_STATUS_OK) {
+ _synctex_error("problem with X offset in the Post Scriptum.");
+ return status;
+ }
+ goto next_line;
+ } else if (status<SYNCTEX_STATUS_EOF){
+ goto report_record_problem;
+ }
+ status = _synctex_match_string(scanner,"Y Offset:");
+ if (status==SYNCTEX_STATUS_OK) {
+ status = _synctex_scan_float_and_dimension(scanner, &(scanner->y_offset));
+ if (status<SYNCTEX_STATUS_OK) {
+ _synctex_error("problem with Y offset in the Post Scriptum.");
+ return status;
+ }
+ goto next_line;
+ } else if (status<SYNCTEX_STATUS_EOF){
+ goto report_record_problem;
+ }
+ goto next_line;
+}
+
+/* SYNCTEX_STATUS_OK is returned if the postamble is read
+ * SYNCTEX_STATUS_NOT_OK is returned if the postamble is not at the current location
+ * a negative error otherwise
+ * The postamble comprises the post scriptum section.
+ */
+synctex_status_t _synctex_scan_postamble(synctex_scanner_t scanner) {
+ synctex_status_t status = 0;
+ if (NULL == scanner) {
+ return SYNCTEX_STATUS_BAD_ARGUMENT;
+ }
+ status = _synctex_match_string(scanner,"Postamble:");
+ if (status < SYNCTEX_STATUS_OK) {
+ return status;
+ }
+count_again:
+ status = _synctex_next_line(scanner);
+ if (status < SYNCTEX_STATUS_OK) {
+ return status;
+ }
+ status = _synctex_scan_named(scanner,"Count:",&(scanner->count),(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 (status<SYNCTEX_STATUS_OK) {
+ return status;
+ }
+ goto count_again;
+ }
+ /* Now we scan the last part of the SyncTeX file: the Post Scriptum section. */
+ return _synctex_scan_post_scriptum(scanner);
+}
+
+/* Horizontal boxes also have visible size.
+ * Visible size are bigger than real size.
+ * For example 0 width boxes may contain text.
+ * At creation time, the visible size is set to the values of the real size.
+ */
+synctex_status_t _synctex_setup_visible_box(synctex_node_t box) {
+ if (box) {
+ switch(box->class->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 (h<itsBtm) {
+ SYNCTEX_HORIZ_V(node) = h;
+ SYNCTEX_WIDTH_V(node) = SYNCTEX_HORIZ_V(node) - itsTop;
+ } else if (h>itsTop) {
+ 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 (h<itsBtm) {
+ SYNCTEX_HORIZ_V(node) = h;
+ SYNCTEX_WIDTH_V(node) = itsTop - SYNCTEX_HORIZ_V(node);
+ } else if (h>itsTop) {
+ 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)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Unexpected end of nested sheet (1).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+scan_next_line:
+ if (SYNCTEX_CUR<SYNCTEX_END) {
+ if (*SYNCTEX_CUR == SYNCTEX_CHAR_END_SHEET) {
+ ++SYNCTEX_CUR;
+ if (_synctex_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Unexpected end of nested sheet (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ if (--depth>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)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Unexpected end of nested sheet (3).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ }
+ _synctex_error("Unexpected end of nested sheet (4).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+}
+
+/* Used when parsing the synctex file.
+ * The sheet argument is a newly created sheet node that will hold the contents.
+ * Something is returned in case of error.
+ */
+synctex_status_t _synctex_scan_sheet(synctex_scanner_t scanner, synctex_node_t sheet) {
+ synctex_node_t parent = sheet;
+ synctex_node_t child = NULL;
+ synctex_node_t sibling = NULL;
+ synctex_node_t box = sheet;
+ int friend_index = 0;
+ synctex_info_t * info = NULL;
+ synctex_status_t status = 0;
+ size_t available = 0;
+ if ((NULL == scanner) || (NULL == sheet)) {
+ return SYNCTEX_STATUS_BAD_ARGUMENT;
+ }
+ /* We MUST start with a box, so at this level, the unique possibility is '[', '(' or "}". */
+prepare_loop:
+ if (SYNCTEX_CUR<SYNCTEX_END) {
+ if (*SYNCTEX_CUR == SYNCTEX_CHAR_BEGIN_VBOX) {
+scan_vbox:
+ if ((child = _synctex_new_vbox(scanner)) && (info = SYNCTEX_INFO(child))) {
+# define SYNCTEX_DECODE_FAILED(WHAT) \
+ (_synctex_decode_int(scanner,&(info[WHAT].INT))<SYNCTEX_STATUS_OK)
+ 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)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad vbox record.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ SYNCTEX_SET_CHILD(parent,child);
+ parent = child;
+ child = NULL;
+ goto child_loop;/* next created node will be a child */
+ } else {
+ _synctex_error("Can't create vbox record.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_BEGIN_HBOX) {
+scan_hbox:
+ if ((child = _synctex_new_hbox(scanner)) && (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_setup_visible_box(child)<SYNCTEX_STATUS_OK
+ || _synctex_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad hbox record.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child),SYNCTEX_VERT(child));
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child)+SYNCTEX_ABS_WIDTH(child),SYNCTEX_VERT(child));
+ SYNCTEX_SET_CHILD(parent,child);
+ parent = child;
+ child = NULL;
+ goto child_loop;/* next created node will be a child */
+ } else {
+ _synctex_error("Can't create hbox record.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_END_SHEET) {
+scan_teehs:
+ ++SYNCTEX_CUR;
+ if (NULL == parent || parent->class->type != synctex_node_type_sheet
+ || _synctex_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Unexpected end of sheet.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+# if SYNCTEX_VERBOSE
+ synctex_node_log(parent);
+# endif
+ SYNCTEX_RETURN(SYNCTEX_STATUS_OK);
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_BEGIN_SHEET) {
+ /* Addendum to version 1.10 to manage nested sheets */
+ ++SYNCTEX_CUR;
+ if (_synctex_scan_nested_sheet(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Unexpected nested sheet.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ goto prepare_loop;
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_ANCHOR) {
+scan_anchor:
+ ++SYNCTEX_CUR;
+ if (_synctex_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Missing anchor.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ goto prepare_loop;
+ } else {
+ /* _synctex_error("Ignored record %c\n",*SYNCTEX_CUR); */
+ ++SYNCTEX_CUR;
+ if (_synctex_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Unexpected end.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ goto prepare_loop;
+ }
+ } else {
+ available = 1;
+ status = _synctex_buffer_get_available_size(scanner,&available);
+ if (status<SYNCTEX_STATUS_OK && available>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_CUR<SYNCTEX_END) {
+ if (*SYNCTEX_CUR == SYNCTEX_CHAR_BEGIN_VBOX) {
+ goto scan_vbox;
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_END_VBOX) {
+scan_xobv:
+ ++SYNCTEX_CUR;
+ if (NULL != parent && parent->class->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)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Uncomplete sheet.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+# if SYNCTEX_VERBOSE
+ synctex_node_log(child);
+# endif
+ goto sibling_loop;
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_BEGIN_HBOX) {
+ goto scan_hbox;
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_END_HBOX) {
+scan_xobh:
+ ++SYNCTEX_CUR;
+ if ((parent) && parent->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)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Uncomplete sheet.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+# if SYNCTEX_VERBOSE
+ synctex_node_log(child);
+# endif
+ goto sibling_loop;
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_VOID_VBOX) {
+ if (NULL != (child = _synctex_new_void_vbox(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)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad void vbox record.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ SYNCTEX_SET_CHILD(parent,child);
+ #define SYNCTEX_UPDATE_FRIEND(NODE)\
+ friend_index = (info[SYNCTEX_TAG_IDX].INT+info[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;
+ 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)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad void hbox record.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ SYNCTEX_SET_CHILD(parent,child);
+ SYNCTEX_UPDATE_FRIEND(child);
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child),SYNCTEX_VERT(child));
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child)+SYNCTEX_ABS_WIDTH(child),SYNCTEX_VERT(child));
+# if SYNCTEX_VERBOSE
+ synctex_node_log(child);
+# endif
+ goto sibling_loop;
+ } else {
+ _synctex_error("Can't create void hbox record.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_KERN) {
+ if (NULL != (child = _synctex_new_kern(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_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad kern record.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ SYNCTEX_SET_CHILD(parent,child);
+ SYNCTEX_UPDATE_FRIEND(child);
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child),SYNCTEX_VERT(child));
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child)-SYNCTEX_WIDTH(child),SYNCTEX_VERT(child));
+# if SYNCTEX_VERBOSE
+ synctex_node_log(child);
+# endif
+ goto sibling_loop;
+ } else {
+ _synctex_error("Can't create kern record.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_GLUE) {
+ if (NULL != (child = _synctex_new_glue(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_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad glue record.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ SYNCTEX_SET_CHILD(parent,child);
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child),SYNCTEX_VERT(child));
+ SYNCTEX_UPDATE_FRIEND(child);
+# if SYNCTEX_VERBOSE
+ synctex_node_log(child);
+# endif
+ goto sibling_loop;
+ } else {
+ _synctex_error("Can't create glue record.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_MATH) {
+ if (NULL != (child = _synctex_new_math(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_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad math record.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ SYNCTEX_SET_CHILD(parent,child);
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child),SYNCTEX_VERT(child));
+ SYNCTEX_UPDATE_FRIEND(child);
+# if SYNCTEX_VERBOSE
+ synctex_node_log(child);
+# endif
+ goto sibling_loop;
+ } else {
+ _synctex_error("Can't create math record.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_BOUNDARY) {
+ if (NULL != (child = _synctex_new_boundary(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_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad boundary record.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ SYNCTEX_SET_CHILD(parent,child);
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child),SYNCTEX_VERT(child));
+ SYNCTEX_UPDATE_FRIEND(child);
+# if SYNCTEX_VERBOSE
+ synctex_node_log(child);
+# endif
+ goto sibling_loop;
+ } else {
+ _synctex_error("Can't create math record.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_END_SHEET) {
+ goto scan_teehs;
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_BEGIN_SHEET) {
+ /* Addendum to version 1.10 to manage nested sheets */
+ ++SYNCTEX_CUR;
+ if (_synctex_scan_nested_sheet(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Unexpected nested sheet.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ goto child_loop;
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_ANCHOR) {
+ goto scan_anchor;
+ } else {
+ /* _synctex_error("Ignored record %c\n",*SYNCTEX_CUR); */
+ ++SYNCTEX_CUR;
+ if (_synctex_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Unexpected end.");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ goto child_loop;
+ }
+ } else {
+ available = 1;
+ status = _synctex_buffer_get_available_size(scanner,&available);
+ if (status<SYNCTEX_STATUS_OK && available>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_CUR<SYNCTEX_END) {
+ if (*SYNCTEX_CUR == SYNCTEX_CHAR_BEGIN_VBOX) {
+ if (NULL != (sibling = _synctex_new_vbox(scanner))
+ && NULL != (info = SYNCTEX_INFO(sibling))) {
+ 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)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad vbox record (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ SYNCTEX_SET_SIBLING(child,sibling);
+ parent = sibling;
+ child = NULL;
+ goto child_loop;
+ } else {
+ _synctex_error("Can't create vbox record (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_END_VBOX) {
+ goto scan_xobv;
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_BEGIN_HBOX) {
+ if (NULL != (sibling = _synctex_new_hbox(scanner)) &&
+ NULL != (info = SYNCTEX_INFO(sibling))) {
+ 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_setup_visible_box(sibling)<SYNCTEX_STATUS_OK
+ || _synctex_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad hbox record (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ SYNCTEX_SET_SIBLING(child,sibling);
+ child = sibling;
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child),SYNCTEX_VERT(child));
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child)+SYNCTEX_ABS_WIDTH(child),SYNCTEX_VERT(child));
+# if SYNCTEX_VERBOSE
+ synctex_node_log(child);
+# endif
+ parent = child;
+ child = NULL;
+ goto child_loop;
+ } else {
+ _synctex_error("Can't create hbox record (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_END_HBOX) {
+ goto scan_xobh;
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_VOID_VBOX) {
+ if (NULL != (sibling = _synctex_new_void_vbox(scanner)) &&
+ NULL != (info = SYNCTEX_INFO(sibling))) {
+ 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)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad void vbox record (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ SYNCTEX_SET_SIBLING(child,sibling);
+ child = sibling;
+# if SYNCTEX_VERBOSE
+ synctex_node_log(child);
+# endif
+ SYNCTEX_UPDATE_FRIEND(child);
+ goto sibling_loop;
+ } else {
+ _synctex_error("can't create void vbox record (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_VOID_HBOX) {
+ if (NULL != (sibling = _synctex_new_void_hbox(scanner)) &&
+ NULL != (info = SYNCTEX_INFO(sibling))) {
+ 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)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad void hbox record (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ SYNCTEX_SET_SIBLING(child,sibling);
+ child = sibling;
+# if SYNCTEX_VERBOSE
+ synctex_node_log(child);
+# endif
+ SYNCTEX_UPDATE_FRIEND(child);
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child),SYNCTEX_VERT(child));
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child)+SYNCTEX_ABS_WIDTH(child),SYNCTEX_VERT(child));
+ goto sibling_loop;
+ } else {
+ _synctex_error("can't create void hbox record (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_KERN) {
+ if (NULL != (sibling = _synctex_new_kern(scanner))
+ && NULL != (info = SYNCTEX_INFO(sibling))) {
+ 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_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad kern record (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ SYNCTEX_SET_SIBLING(child,sibling);
+ child = sibling;
+# if SYNCTEX_VERBOSE
+ synctex_node_log(child);
+# endif
+ SYNCTEX_UPDATE_FRIEND(child);
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child),SYNCTEX_VERT(child));
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child)-SYNCTEX_WIDTH(child),SYNCTEX_VERT(child));
+ goto sibling_loop;
+ } else {
+ _synctex_error("Can't create kern record (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_GLUE) {
+ if (NULL != (sibling = _synctex_new_glue(scanner))
+ && NULL != (info = SYNCTEX_INFO(sibling))) {
+ 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_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad glue record (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ SYNCTEX_SET_SIBLING(child,sibling);
+ child = sibling;
+# if SYNCTEX_VERBOSE
+ synctex_node_log(child);
+# endif
+ SYNCTEX_UPDATE_FRIEND(child);
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child),SYNCTEX_VERT(child));
+ goto sibling_loop;
+ } else {
+ _synctex_error("Can't create glue record (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_MATH) {
+ if (NULL != (sibling = _synctex_new_math(scanner))
+ && NULL != (info = SYNCTEX_INFO(sibling))) {
+ 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_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad math record (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ SYNCTEX_SET_SIBLING(child,sibling);
+ child = sibling;
+# if SYNCTEX_VERBOSE
+ synctex_node_log(child);
+# endif
+ SYNCTEX_UPDATE_FRIEND(child);
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child),SYNCTEX_VERT(child));
+ goto sibling_loop;
+ } else {
+ _synctex_error("Can't create math record (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_BOUNDARY) {
+ if (NULL != (sibling = _synctex_new_boundary(scanner))
+ && NULL != (info = SYNCTEX_INFO(sibling))) {
+ 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_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad boundary record (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ SYNCTEX_SET_SIBLING(child,sibling);
+ child = sibling;
+# if SYNCTEX_VERBOSE
+ synctex_node_log(child);
+# endif
+ SYNCTEX_UPDATE_FRIEND(child);
+ _synctex_hbox_setup_visible(parent,SYNCTEX_HORIZ(child),SYNCTEX_VERT(child));
+ goto sibling_loop;
+ } else {
+ _synctex_error("Can't create boundary record (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_END_SHEET) {
+ goto scan_teehs;
+ } else if (*SYNCTEX_CUR == SYNCTEX_CHAR_ANCHOR) {
+ ++SYNCTEX_CUR;
+ if (_synctex_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Missing anchor (2).");
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ goto sibling_loop;
+ } else {
+ ++SYNCTEX_CUR;
+ /* _synctex_error("Ignored record %c(2)\n",*SYNCTEX_CUR); */
+ if (_synctex_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ SYNCTEX_RETURN(SYNCTEX_STATUS_ERROR);
+ }
+ goto sibling_loop;
+ }
+ } else {
+ available = 1;
+ status = _synctex_buffer_get_available_size(scanner,&available);
+ if (status<SYNCTEX_STATUS_OK && available>0){
+ 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_EOF) {
+ return status;
+ }
+ if (_synctex_next_line(scanner)<SYNCTEX_STATUS_OK) {
+ _synctex_error("Uncomplete Content.");
+ return SYNCTEX_STATUS_ERROR;
+ }
+ if (status == SYNCTEX_STATUS_NOT_OK) {
+ goto content_not_found;
+ }
+next_sheet:
+ if (*SYNCTEX_CUR != SYNCTEX_CHAR_BEGIN_SHEET) {
+ status = _synctex_scan_postamble(scanner);
+ if (status < SYNCTEX_STATUS_EOF) {
+ _synctex_error("Bad content.");
+ return status;
+ }
+ if (status<SYNCTEX_STATUS_OK) {
+ status = _synctex_next_line(scanner);
+ if (status < SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad content.");
+ return status;
+ }
+ goto next_sheet;
+ }
+ return SYNCTEX_STATUS_OK;
+ }
+ /* Create a new sheet node */
+ sheet = _synctex_new_sheet(scanner);
+ status = _synctex_decode_int(scanner,&(SYNCTEX_PAGE(sheet)));
+ if (status<SYNCTEX_STATUS_OK) {
+ _synctex_error("Missing sheet number.");
+bail:
+ SYNCTEX_FREE(sheet);
+ return SYNCTEX_STATUS_ERROR;
+ }
+ status = _synctex_next_line(scanner);
+ if (status<SYNCTEX_STATUS_OK) {
+ _synctex_error("Uncomplete file.");
+ goto bail;
+ }
+ status = _synctex_scan_sheet(scanner,sheet);
+ if (status<SYNCTEX_STATUS_OK) {
+ _synctex_error("Bad sheet content.");
+ goto bail;
+ }
+ SYNCTEX_APPEND_SHEET(scanner,sheet);
+ sheet = NULL;
+ /* Now read the list of Inputs between 2 sheets. */
+ do {
+ status = _synctex_scan_input(scanner);
+ if (status<SYNCTEX_STATUS_EOF) {
+ _synctex_error("Bad input section.");
+ goto bail;
+ }
+ }
+ while(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 (status<SYNCTEX_STATUS_OK) {
+ _synctex_error("SyncTeX Error: Bad preamble\n");
+bailey:
+ synctex_scanner_free(scanner);
+ return NULL;
+ }
+ status = _synctex_scan_content(scanner);
+ if (status<SYNCTEX_STATUS_OK) {
+ _synctex_error("SyncTeX Error: Bad content\n");
+ goto bailey;
+ }
+ /* Everything is finished, free the buffer, close the file */
+ free((void *)SYNCTEX_START);
+ SYNCTEX_START = SYNCTEX_CUR = SYNCTEX_END = NULL;
+ gzclose(SYNCTEX_FILE);
+ SYNCTEX_FILE = NULL;
+ /* Final tuning: set the default values for various parameters */
+ /* 1 pre_unit = (scanner->pre_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(0<char_index) {
+ char_index -= 1;
+ if (SYNCTEX_IS_PATH_SEPARATOR(name[char_index])
+ && (result = _synctex_scanner_get_tag(scanner,name+char_index+1))) {
+ return result;
+ }
+ }
+ }
+ }
+ return result;
+ }
+ }
+ return 0;
+}
+synctex_node_t synctex_scanner_input(synctex_scanner_t scanner) {
+ return scanner?scanner->input: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(line<max_line) {
+ /* This loop will only be performed once for advanced viewers */
+ friend_index = (tag+line)%(scanner->number_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_ref<end_ref) {
+ synctex_node_t parent = NULL;
+ node = *start_ref;
+ parent = SYNCTEX_PARENT(node);
+ next_match = abs(SYNCTEX_LINE(node)-SYNCTEX_MEAN_LINE(parent));
+ if (next_match < best_match
+ || (next_match == best_match && SYNCTEX_NODE_WEIGHT(parent)>best_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<SYNCTEX_END) {
+ return *(synctex_node_t*)SYNCTEX_CUR;
+ } else {
+ return NULL;
+ }
+}
+
+/* This struct records a point in TeX coordinates.*/
+typedef struct {
+ int h;
+ int v;
+} synctex_point_t;
+
+/* This struct records distances, the left one is positive or 0 and the right one is negative or 0.
+ * When comparing the locations of 2 different graphical objects on the page, we will have to also record the
+ * horizontal distance as signed to keep track of the typesetting order.*/
+typedef struct {
+ int left;
+ int right;
+} synctex_distances_t;
+
+typedef struct {
+ synctex_point_t left;
+ synctex_point_t right;
+} synctex_offsets_t;
+
+
+typedef struct {
+ synctex_node_t left;
+ synctex_node_t right;
+} synctex_node_set_t;
+
+/* The smallest container between two has the smallest width or height.
+ * This comparison is used when there are 2 overlapping boxes that contain the hit point.
+ * For ConTeXt, the problem appears at each page.
+ * The chosen box is the one with the smallest height, then the smallest width. */
+SYNCTEX_INLINE static synctex_node_t _synctex_smallest_container(synctex_node_t node, synctex_node_t other_node);
+
+/* Returns the distance between the hit point hitPoint=(H,V) and the given node. */
+synctex_bool_t _synctex_point_in_box(synctex_point_t hitPoint, synctex_node_t node, synctex_bool_t visible);
+int _synctex_node_distance_to_point(synctex_point_t hitPoint, synctex_node_t node, synctex_bool_t visible);
+
+/* The best container is the deeper box that contains the hit point (H,V).
+ * _synctex_eq_deepest_container starts with node whereas
+ * _synctex_box_child_deepest starts with node's children, if any
+ * if node is not a box, or a void box, NULL is returned.
+ * We traverse the node tree in a deep first manner and stop as soon as a result is found. */
+static synctex_node_t _synctex_eq_deepest_container(synctex_point_t hitPoint,synctex_node_t node, synctex_bool_t visible);
+
+/* Once a best container is found, the closest children are the closest nodes to the left or right of the hit point.
+ * Only horizontal and vertical offsets are used to compare the positions of the nodes. */
+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);
+
+/* The closest container is the box that is the one closest to the given point.
+ * The "visible" version takes into account the visible dimensions instead of the real ones given by TeX. */
+SYNCTEX_INLINE static synctex_node_t _synctex_eq_closest_child(synctex_point_t hitPoint,synctex_node_t node, synctex_bool_t visible);
+
+#define SYNCTEX_MASK_LEFT 1
+#define SYNCTEX_MASK_RIGHT 2
+
+synctex_status_t synctex_edit_query(synctex_scanner_t scanner,int page,float h,float v) {
+ synctex_node_t sheet = NULL;
+ synctex_node_t node = NULL; /* placeholder */
+ synctex_node_t other_node = NULL; /* placeholder */
+ synctex_point_t hitPoint = {0,0}; /* placeholder */
+ synctex_node_set_t bestNodes = {NULL,NULL}; /* holds the best node */
+ synctex_distances_t bestDistances = {INT_MAX,INT_MAX}; /* holds the best distances for the best node */
+ synctex_node_t bestContainer = NULL; /* placeholder */
+ if (NULL == (scanner = synctex_scanner_parse(scanner)) || 0 >= 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<min) {
+ return min - hitPoint.h; /* regions 1+4+7, result is > 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<min) {
+ return min - hitPoint.h; /* regions 1+4+7, result is > 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<<<<med<<<<max
+ * .................................
+ * Actually, we do not take into account negative widths.
+ * There is a problem for such situation when there is efectively overlapping text.
+ * But this should be extremely rare. I guess that in that case, many different choices
+ * could be made, one being in contradiction of the other.
+ * It means that the best choice should be made according to the situation that occurs
+ * most frequently.
+ */
+ if (hitPoint.h<min) {
+ return min - hitPoint.h + 1; /* penalty to ensure other nodes are chosen first in case of overlapping ones */
+ } else if (hitPoint.h>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<min) {
+ return min - hitPoint.v; /* regions 1+2+3, result is > 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<min) {
+ return min - hitPoint.v; /* regions 1+2+3, result is > 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 node;
+ }
+ 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 (height<other_height) {
+ return node;
+ }
+ if (height>other_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.v<minV) {
+ if (hitPoint.h<minH) {
+ /* This is region 1. The distance to the box is the L1 distance PA. */
+ result = minV - hitPoint.v + minH - hitPoint.h;/* Integer overflow? probability epsilon */
+ } else if (hitPoint.h<=maxH) {
+ /* This is region 2. The distance to the box is the geometrical distance to the top edge. */
+ result = minV - hitPoint.v;
+ } else {
+ /* This is region 3. The distance to the box is the L1 distance PB. */
+ result = minV - hitPoint.v + hitPoint.h - maxH;
+ }
+ } else if (hitPoint.v<=maxV) {
+ if (hitPoint.h<minH) {
+ /* This is region 4. The distance to the box is the geometrical distance to the left edge. */
+ result = minH - hitPoint.h;
+ } else if (hitPoint.h<=maxH) {
+ /* This is region 4. We are inside the box. */
+ result = 0;
+ } else {
+ /* This is region 6. The distance to the box is the geometrical distance to the right edge. */
+ result = hitPoint.h - maxH;
+ }
+ } else {
+ if (hitPoint.h<minH) {
+ /* This is region 7. The distance to the box is the L1 distance PC. */
+ result = hitPoint.v - maxV + minH - hitPoint.h;
+ } else if (hitPoint.h<=maxH) {
+ /* This is region 8. The distance to the box is the geometrical distance to the top edge. */
+ result = hitPoint.v - maxV;
+ } else {
+ /* This is region 9. The distance to the box is the L1 distance PD. */
+ result = hitPoint.v - maxV + hitPoint.h - maxH;
+ }
+ }
+ break;
+ case synctex_node_type_kern:
+ maxH = SYNCTEX_WIDTH(node);
+ if (maxH<0) {
+ minH = SYNCTEX_HORIZ(node);
+ maxH = minH - maxH;
+ } else {
+ minH = -maxH;
+ maxH = SYNCTEX_HORIZ(node);
+ minH += maxH;
+ }
+ minV = SYNCTEX_VERT(node);
+ if (hitPoint.h<minH) {
+ if (hitPoint.v>minV) {
+ 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.h<minH) {
+ if (hitPoint.v>minV) {
+ 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;
+}
--- /dev/null
+/*
+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
--- /dev/null
+/*
+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 <w2c/c-auto.h> /* for inline && HAVE_xxx */
+/* No inlining for synctex tool in texlive. */
+# define SYNCTEX_INLINE
--- /dev/null
+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-liveNOSPAMtug.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
+
--- /dev/null
+/*
+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 <synctex_parser_utils.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+#include <stdio.h>
+
+#include <limits.h>
+#include <ctype.h>
+#include <string.h>
+
+#include <sys/stat.h>
+
+#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 <windows.h>
+#include <shlwapi.h> /* 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)+2<size (see below) */
+ if(strlen(src)<size) {
+ if((dest = (char *)malloc(size+2))) {
+ char * dpc = dest + (lpc-src); /* dpc is the last path component of dest. */
+ if(dest != strncpy(dest,src,size)) {
+ _synctex_error("! _synctex_copy_with_quoting_last_path_component: Copy problem");
+ free(dest);
+ dest = NULL;/* Don't forget to reinitialize. */
+ return -2;
+ }
+ memmove(dpc+1,dpc,strlen(dpc)+1); /* Also move the null terminating character. */
+ dpc[0]='"';
+ dpc[strlen(dpc)+1]='\0';/* Consistency test */
+ dpc[strlen(dpc)]='"';
+ return 0; /* Success. */
+ }
+ return -1; /* Memory allocation error. */
+ }
+ _synctex_error("! _synctex_copy_with_quoting_last_path_component: Internal inconsistency");
+ return -3;
+ }
+ return 0; /* Success. */
+ }
+ return 0; /* No last path component. */
+# undef dest
+ }
+ return 1; /* Bad parameter, this value is subject to changes. */
+}
+
+/* The client is responsible of the management of the returned string, if any. */
+char * _synctex_merge_strings(const char * first,...);
+
+char * _synctex_merge_strings(const char * first,...) {
+ va_list arg;
+ size_t size = 0;
+ const char * temp;
+ /* First retrieve the size necessary to store the merged string */
+ va_start (arg, first);
+ temp = first;
+ do {
+ size_t len = strlen(temp);
+ if(UINT_MAX-len<size) {
+ _synctex_error("! _synctex_merge_strings: Capacity exceeded.");
+ return NULL;
+ }
+ size+=len;
+ } while( (temp = va_arg(arg, const char *)) != NULL);
+ va_end(arg);
+ if(size>0) {
+ 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];
+}
--- /dev/null
+/*
+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 <stdlib.h>
+
+#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
--- /dev/null
+1.18
\ No newline at end of file
--- /dev/null
+*.Dockerfile
+*.build
--- /dev/null
+#!/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)"
--- /dev/null
+#!/bin/bash
+
+# Step over prompts from the package-manager.
+if [ -f /etc/arch-release ]; then
+ yes ''
+else
+ yes
+fi
+
--- /dev/null
+ADD . /epdfinfo
+WORKDIR /epdfinfo
+RUN make -s distclean || true
+CMD ["sh", "./test/docker/lib/run-tests"]
--- /dev/null
+# -*- dockerfile -*-
+FROM base/archlinux
+RUN pacman -Syu --noconfirm --noprogressbar && \
+ pacman -S --noconfirm --noprogressbar poppler-glib base-devel
+
+
--- /dev/null
+# -*- dockerfile -*-
+FROM centos:7
+RUN yum update -y && yum install -y gcc gcc-c++ poppler-glib-devel
--- /dev/null
+# -*- dockerfile -*-
+FROM debian:8
+RUN apt-get update -y && apt-get install -y gcc g++ libpoppler-glib-dev
+
--- /dev/null
+# -*- dockerfile -*-
+FROM debian:9
+RUN apt-get update -y && apt-get install -y gcc g++ libpoppler-glib-dev
+
--- /dev/null
+# -*- dockerfile -*-
+FROM fedora:24
+RUN dnf update -y && dnf install -y gcc gcc-c++ poppler-glib-devel
+
+
--- /dev/null
+# -*- dockerfile -*-
+FROM fedora:25
+RUN dnf update -y && dnf install -y gcc gcc-c++ poppler-glib-devel
+
+
--- /dev/null
+# -*- dockerfile -*-
+FROM fedora:26
+RUN dnf update -y && dnf install -y gcc gcc-c++ poppler-glib-devel
+
+
--- /dev/null
+# -*- dockerfile -*-
+FROM gentoo/stage3-amd64
+RUN emerge --sync && emerge sys-devel/gcc app-text/poppler
+
+
--- /dev/null
+# -*- dockerfile -*-
+FROM ubuntu:trusty
+RUN apt-get update -y && apt-get install -y gcc g++ libpoppler-glib-dev
+
--- /dev/null
+# -*- dockerfile -*-
+FROM ubuntu:xenial
+RUN apt-get update -y && apt-get install -y gcc g++ libpoppler-glib-dev
+
--- /dev/null
+# -*- dockerfile -*-
+FROM ubuntu:artful
+RUN apt-get update -y && apt-get install -y gcc g++ libpoppler-glib-dev
+
--- /dev/null
+;;; pdf-annot.el --- Annotation support for PDF files. -*- lexical-binding: t -*-
+
+;; Copyright (C) 2013, 2014 Andreas Politz
+
+;; Author: Andreas Politz <politzaNOSPAMfh-trier.de>
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; 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)
+
+\f
+;; * ================================================================== *
+;; * 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)))
+
+\f
+;; * ================================================================== *
+;; * 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)))
+
+\f
+;; * ================================================================== *
+;; * 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))
+\f
+;; * ================================================================== *
+;; * 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))))
+
+\f
+;; * ================================================================== *
+;; * 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))
+
+\f
+;; * ================================================================== *
+;; * 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"))))
+
+\f
+;; * ================================================================== *
+;; * 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)))
+
+
+\f
+;; * ================================================================== *
+;; * 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)))))
+
+\f
+;; * ================================================================== *
+;; * 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)))
+
+
+\f
+;; * ================================================================== *
+;; * 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))))
+ (delay-mode-hooks
+ (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))))
+ (run-mode-hooks)
+ (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
--- /dev/null
+;;; pdf-cache.el --- Cache time-critical or frequent epdfinfo queries. -*- lexical-binding:t -*-
+
+;; Copyright (C) 2013 Andreas Politz
+
+;; Author: Andreas Politz <politzaNOSPAMfh-trier.de>
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;;; Code:
+;;
+
+(require 'pdf-info)
+(require 'pdf-util)
+
+\f
+;; * ================================================================== *
+;; * 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)
+
+\f
+;; * ================================================================== *
+;; * 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)
+
+\f
+;; * ================================================================== *
+;; * 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 pdf-cache--image-cache)
+ image)
+ ;; Find it in the cache.
+ (while (and (setq image (pop cache))
+ (not (pdf-cache--image-match
+ image page min-width max-width hash))))
+ ;; Remove it and push it to the front.
+ (when image
+ (setq pdf-cache--image-cache
+ (cons image (delq 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)))))
+
+\f
+;; * ================================================================== *
+;; * 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 nil
+ (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
--- /dev/null
+;;; pdf-dev.el --- Mother's little development helper -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2015 Andreas Politz
+
+;; Author: Andreas Politz <politzaNOSPAMhochschule-trier.de>
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; 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
--- /dev/null
+;;; pdf-history.el --- A simple stack-based history in PDF buffers. -*- lexical-binding: t -*-
+
+;; Copyright (C) 2013, 2014 Andreas Politz
+
+;; Author: Andreas Politz <politzaNOSPAMfh-trier.de>
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; 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
--- /dev/null
+;;; pdf-info.el --- Extract info from pdf-files via a helper process. -*- lexical-binding: t -*-
+
+;; Copyright (C) 2013, 2014 Andreas Politz
+
+;; Author: Andreas Politz <politzaNOSPAMfh-trier.de>
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; 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)
+
+
+\f
+;; * ================================================================== *
+;; * 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)
+
+
+\f
+;; * ================================================================== *
+;; * 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.")
+
+\f
+;; * ================================================================== *
+;; * Process handling
+;; * ================================================================== *
+
+(defconst pdf-info-empty-page-data
+ (eval-when-compile
+ (concat
+ "%PDF-1.0\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0"
+ " obj<</Type/Pages/Kids[3 0 R]/Count 1>>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<</Size 4/Root 1 0 R>>\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)))))))))))))
+
+\f
+;; * ================================================================== *
+;; * 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)))))))
+
+
+\f
+;; * ================================================================== *
+;; * 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))))
+
+\f
+;; * ================================================================== *
+;; * 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)))
+
+
+\f
+;; * ================================================================== *
+;; * 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 (pdf-util-frame-scale-factor))
+ (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
--- /dev/null
+;;; pdf-isearch.el --- Isearch in pdf buffers. -*- lexical-binding: t -*-
+
+;; Copyright (C) 2013, 2014 Andreas Politz
+
+;; Author: Andreas Politz <politzaNOSPAMfh-trier.de>
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; 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:
+
+
+\f
+;; * ================================================================== *
+;; * 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'.")
+
+\f
+;; * ================================================================== *
+;; * 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\).")
+
+\f
+;; * ================================================================== *
+;; * 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"))))
+
+
+\f
+;; * ================================================================== *
+;; * 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)))
+
+\f
+;; * ================================================================== *
+;; * 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))
+
+
+\f
+;; * ================================================================== *
+;; * 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))))))))
+
+
+\f
+;; * ================================================================== *
+;; * 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))))))))
+
+\f
+;; * ================================================================== *
+;; * 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:
--- /dev/null
+;;; pdf-links.el --- Handle PDF links. -*- lexical-binding: t -*-
+
+;; Copyright (C) 2013, 2014 Andreas Politz
+
+;; Author: Andreas Politz <politzaNOSPAMfh-trier.de>
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+
+(require 'pdf-info)
+(require 'pdf-util)
+(require 'pdf-misc)
+(require 'pdf-cache)
+(require 'pdf-isearch)
+(require 'let-alist)
+(require 'org)
+
+;;; Code:
+
+
+\f
+;; * ================================================================== *
+;; * 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)
+
+\f
+;; * ================================================================== *
+;; * 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.\\<pdf-links-minor-mode-map>
+
+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
--- /dev/null
+;;; pdf-loader.el --- Minimal PDF Tools loader -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2017 Andreas Politz
+
+;; Author: Andreas Politz <politzaNOSPAMhochschule-trier.de>
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; 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
--- /dev/null
+;;; pdf-misc.el --- Miscellaneous commands for PDF buffer.
+
+;; Copyright (C) 2013, 2014 Andreas Politz
+
+;; Author: Andreas Politz <politzaNOSPAMfh-trier.de>
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; 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
--- /dev/null
+;;; pdf-occur.el --- Display matching lines of PDF documents. -*- lexical-binding: t -*-
+
+;; Copyright (C) 2013, 2014 Andreas Politz
+
+;; Author: Andreas Politz <politzaNOSPAMfh-trier.de>
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; 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:
+
+
+
+\f
+;; * ================================================================== *
+;; * 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'.")
+
+\f
+;; * ================================================================== *
+;; * High level functions
+;; * ================================================================== *
+
+(define-derived-mode pdf-occur-buffer-mode tablist-mode "PDFOccur"
+ "Major mode for output from `pdf-occur`. \\<pdf-occur-buffer-mode-map>
+
+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))
+
+\f
+;; * ================================================================== *
+;; * 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)))
+
+\f
+;; * ================================================================== *
+;; * 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)))))
+
+
+\f
+;; * ================================================================== *
+;; * 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)))))
+
+
+\f
+;; * ================================================================== *
+;; * 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]"))))))
+
+\f
+;; * ================================================================== *
+;; * 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
--- /dev/null
+;;; pdf-outline.el --- Outline for PDF buffer -*- lexical-binding: t -*-
+
+;; Copyright (C) 2013, 2014 Andreas Politz
+
+;; Author: Andreas Politz <politzaNOSPAMfh-trier.de>
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; 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))))
+
+\f
+;;
+;; 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))))
+
+
+\f
+;;
+;; 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:
--- /dev/null
+;;; pdf-sync.el --- Use synctex to correlate LaTeX-Sources with PDF positions. -*- lexical-binding:t -*-
+;; Copyright (C) 2013, 2014 Andreas Politz
+
+;; Author: Andreas Politz <politzaNOSPAMfh-trier.de>
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; 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)
+
+(make-obsolete-variable
+ 'pdf-sync-forward-display-pdf-key
+ "Bound in Auctex's to C-c C-v, if TeX-source-correlate-mode is activate." "1.0")
+
+(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.
+\\<pdf-sync-minor-mode-map>
+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))
+
+\f
+;; * ================================================================== *
+;; * 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))))))
+
+\f
+;; * ================================================================== *
+;; * 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))))))
+
+
+\f
+;; * ================================================================== *
+;; * 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)))))))))
+
+(provide 'pdf-sync)
+;;; pdf-sync.el ends here
--- /dev/null
+;;; pdf-tools-autoloads.el --- automatically extracted autoloads
+;;
+;;; Code:
+(add-to-list 'load-path (directory-file-name (or (file-name-directory #$) (car load-path))))
+\f
+;;;### (autoloads nil "pdf-annot" "pdf-annot.el" (23994 46397 260173
+;;;;;; 770000))
+;;; Generated autoloads from pdf-annot.el
+
+(autoload 'pdf-annot-minor-mode "pdf-annot" "\
+Support for PDF Annotations.
+
+\\{pdf-annot-minor-mode-map}
+
+\(fn &optional ARG)" t nil)
+
+;;;***
+\f
+;;;### (autoloads nil "pdf-history" "pdf-history.el" (23994 46397
+;;;;;; 259193 628000))
+;;; Generated autoloads from pdf-history.el
+
+(autoload 'pdf-history-minor-mode "pdf-history" "\
+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}
+
+\(fn &optional ARG)" t nil)
+
+;;;***
+\f
+;;;### (autoloads nil "pdf-isearch" "pdf-isearch.el" (23994 46397
+;;;;;; 249007 18000))
+;;; Generated autoloads from pdf-isearch.el
+
+(autoload 'pdf-isearch-minor-mode "pdf-isearch" "\
+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}
+
+\(fn &optional ARG)" t nil)
+
+;;;***
+\f
+;;;### (autoloads nil "pdf-links" "pdf-links.el" (23994 46397 251008
+;;;;;; 846000))
+;;; Generated autoloads from pdf-links.el
+
+(autoload 'pdf-links-minor-mode "pdf-links" "\
+Handle links in PDF documents.\\<pdf-links-minor-mode-map>
+
+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}
+
+\(fn &optional ARG)" t nil)
+
+(autoload 'pdf-links-action-perform "pdf-links" "\
+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.
+
+\(fn LINK)" t nil)
+
+;;;***
+\f
+;;;### (autoloads nil "pdf-loader" "pdf-loader.el" (23994 46397 258169
+;;;;;; 584000))
+;;; Generated autoloads from pdf-loader.el
+
+(autoload 'pdf-loader-install "pdf-loader" "\
+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.
+
+\(fn &optional NO-QUERY-P SKIP-DEPENDENCIES-P NO-ERROR-P FORCE-DEPENDENCIES-P)" nil nil)
+
+;;;***
+\f
+;;;### (autoloads nil "pdf-misc" "pdf-misc.el" (23994 46397 253136
+;;;;;; 199000))
+;;; Generated autoloads from pdf-misc.el
+
+(autoload 'pdf-misc-minor-mode "pdf-misc" "\
+FIXME: Not documented.
+
+\(fn &optional ARG)" t nil)
+
+(autoload 'pdf-misc-size-indication-minor-mode "pdf-misc" "\
+Provide a working size indication in the mode-line.
+
+\(fn &optional ARG)" t nil)
+
+(autoload 'pdf-misc-menu-bar-minor-mode "pdf-misc" "\
+Display a PDF Tools menu in the menu-bar.
+
+\(fn &optional ARG)" t nil)
+
+(autoload 'pdf-misc-context-menu-minor-mode "pdf-misc" "\
+Provide a right-click context menu in PDF buffers.
+
+\\{pdf-misc-context-menu-minor-mode-map}
+
+\(fn &optional ARG)" t nil)
+
+;;;***
+\f
+;;;### (autoloads nil "pdf-occur" "pdf-occur.el" (23994 46397 248001
+;;;;;; 265000))
+;;; Generated autoloads from pdf-occur.el
+
+(autoload 'pdf-occur "pdf-occur" "\
+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.
+
+\(fn STRING &optional REGEXP-P)" t nil)
+
+(autoload 'pdf-occur-multi-command "pdf-occur" "\
+Perform `pdf-occur' on multiple buffer.
+
+For a programmatic search of multiple documents see
+`pdf-occur-search'.
+
+\(fn)" t nil)
+
+(defvar pdf-occur-global-minor-mode nil "\
+Non-nil if Pdf-Occur-Global minor mode is enabled.
+See the `pdf-occur-global-minor-mode' command
+for a description of this minor mode.
+Setting this variable directly does not take effect;
+either customize it (see the info node `Easy Customization')
+or call the function `pdf-occur-global-minor-mode'.")
+
+(custom-autoload 'pdf-occur-global-minor-mode "pdf-occur" nil)
+
+(autoload 'pdf-occur-global-minor-mode "pdf-occur" "\
+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.
+
+\(fn &optional ARG)" t nil)
+
+(autoload 'pdf-occur-ibuffer-minor-mode "pdf-occur" "\
+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'.
+
+\(fn &optional ARG)" t nil)
+
+(autoload 'pdf-occur-dired-minor-mode "pdf-occur" "\
+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'.
+
+\(fn &optional ARG)" t nil)
+
+;;;***
+\f
+;;;### (autoloads nil "pdf-outline" "pdf-outline.el" (23994 46397
+;;;;;; 261141 728000))
+;;; Generated autoloads from pdf-outline.el
+
+(autoload 'pdf-outline-minor-mode "pdf-outline" "\
+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}
+
+\(fn &optional ARG)" t nil)
+
+(autoload 'pdf-outline "pdf-outline" "\
+Display an PDF outline of BUFFER.
+
+BUFFER defaults to the current buffer. Select the outline
+buffer, unless NO-SELECT-WINDOW-P is non-nil.
+
+\(fn &optional BUFFER NO-SELECT-WINDOW-P)" t nil)
+
+(autoload 'pdf-outline-imenu-enable "pdf-outline" "\
+Enable imenu in the current PDF buffer.
+
+\(fn)" t nil)
+
+;;;***
+\f
+;;;### (autoloads nil "pdf-sync" "pdf-sync.el" (23994 46397 262109
+;;;;;; 503000))
+;;; Generated autoloads from pdf-sync.el
+
+(autoload 'pdf-sync-minor-mode "pdf-sync" "\
+Correlate a PDF position with the TeX file.
+\\<pdf-sync-minor-mode-map>
+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.
+
+\(fn &optional ARG)" t nil)
+
+;;;***
+\f
+;;;### (autoloads nil "pdf-tools" "pdf-tools.el" (23994 46397 247044
+;;;;;; 318000))
+;;; Generated autoloads from pdf-tools.el
+
+(defvar pdf-tools-handle-upgrades t "\
+Whether PDF Tools should handle upgrading itself.")
+
+(custom-autoload 'pdf-tools-handle-upgrades "pdf-tools" t)
+
+(autoload 'pdf-tools-install "pdf-tools" "\
+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'.
+
+\(fn &optional NO-QUERY-P SKIP-DEPENDENCIES-P NO-ERROR-P FORCE-DEPENDENCIES-P)" t nil)
+
+(autoload 'pdf-tools-enable-minor-modes "pdf-tools" "\
+Enable MODES in the current buffer.
+
+MODES defaults to `pdf-tools-enabled-modes'.
+
+\(fn &optional MODES)" t nil)
+
+(autoload 'pdf-tools-help "pdf-tools" "\
+
+
+\(fn)" t nil)
+
+;;;***
+\f
+;;;### (autoloads nil "pdf-view" "pdf-view.el" (23994 46397 250000
+;;;;;; 807000))
+;;; Generated autoloads from pdf-view.el
+
+(autoload 'pdf-view-bookmark-jump-handler "pdf-view" "\
+The bookmark handler-function interface for bookmark BMK.
+
+See also `pdf-view-bookmark-make-record'.
+
+\(fn BMK)" nil nil)
+
+;;;***
+\f
+;;;### (autoloads nil "pdf-virtual" "pdf-virtual.el" (23994 46397
+;;;;;; 256216 424000))
+;;; Generated autoloads from pdf-virtual.el
+
+(autoload 'pdf-virtual-edit-mode "pdf-virtual" "\
+Major mode when editing a virtual PDF buffer.
+
+\(fn)" t nil)
+
+(autoload 'pdf-virtual-view-mode "pdf-virtual" "\
+Major mode in virtual PDF buffers.
+
+\(fn)" t nil)
+
+(defvar pdf-virtual-global-minor-mode nil "\
+Non-nil if Pdf-Virtual-Global minor mode is enabled.
+See the `pdf-virtual-global-minor-mode' command
+for a description of this minor mode.
+Setting this variable directly does not take effect;
+either customize it (see the info node `Easy Customization')
+or call the function `pdf-virtual-global-minor-mode'.")
+
+(custom-autoload 'pdf-virtual-global-minor-mode "pdf-virtual" nil)
+
+(autoload 'pdf-virtual-global-minor-mode "pdf-virtual" "\
+Enable recognition and handling of VPDF files.
+
+\(fn &optional ARG)" t nil)
+
+(autoload 'pdf-virtual-buffer-create "pdf-virtual" "\
+
+
+\(fn &optional FILENAMES BUFFER-NAME DISPLAY-P)" t nil)
+
+;;;***
+\f
+;;;### (autoloads nil nil ("pdf-cache.el" "pdf-dev.el" "pdf-info.el"
+;;;;;; "pdf-tools-pkg.el" "pdf-util.el") (23994 46397 257150 14000))
+
+;;;***
+\f
+;; Local Variables:
+;; version-control: never
+;; no-byte-compile: t
+;; no-update-autoloads: t
+;; End:
+;;; pdf-tools-autoloads.el ends here
--- /dev/null
+(define-package "pdf-tools" "20191031.642" "Support library for PDF documents."
+ '((emacs "24.3")
+ (tablist "1.0")
+ (let-alist "1.0.4"))
+ :keywords
+ '("files" "multimedia")
+ :authors
+ '(("Andreas Politz" . "politzaNOSPAMfh-trier.de"))
+ :maintainer
+ '("Andreas Politz" . "politzaNOSPAMfh-trier.de"))
+;; Local Variables:
+;; no-byte-compile: t
+;; End:
--- /dev/null
+;;; pdf-tools.el --- Support library for PDF documents. -*- lexical-binding:t -*-
+
+;; Copyright (C) 2013, 2014 Andreas Politz
+
+;; Author: Andreas Politz <politzaNOSPAMfh-trier.de>
+;; Keywords: files, multimedia
+;; Package: pdf-tools
+;; Version: 1.0
+;; Package-Requires: ((emacs "24.3") (tablist "1.0") (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 <http://www.gnu.org/licenses/>.
+
+;;; 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)
+
+
+\f
+;; * ================================================================== *
+;; * 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))))
+
+\f
+;; * ================================================================== *
+;; * 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)
+
+(defcustom pdf-tools-installer-os nil
+ "Specifies which installer to use.
+
+If nil the installer is chosen automatically. This variable is
+useful if you have multiple installers present on your
+system (e.g. nix on arch linux)"
+ :group 'pdf-tools
+ :type 'string)
+
+(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%s"
+ autobuild
+ (shell-quote-argument target-directory)
+ (cond
+ (skip-dependencies-p " -D")
+ (force-dependencies-p " -d")
+ (t ""))
+ (if pdf-tools-installer-os (concat " --os " pdf-tools-installer-os) ""))
+ 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)))))
+
+\f
+;; * ================================================================== *
+;; * 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 "\f~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\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 "\f~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"))))
+
+\f
+;; * ================================================================== *
+;; * 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
--- /dev/null
+;;; pdf-util.el --- PDF Utility functions. -*- lexical-binding: t -*-
+
+;; Copyright (C) 2013, 2014 Andreas Politz
+
+;; Author: Andreas Politz <politzaNOSPAMfh-trier.de>
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; 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")
+
+(declare-function pdf-view-image-type "pdf-view")
+
+
+\f
+;; * ================================================================== *
+;; * 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)))))
+
+
+\f
+;; * ================================================================== *
+;; * 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))))
+
+\f
+;; * ================================================================== *
+;; * 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))))
+
+
+\f
+;; * ================================================================== *
+;; * 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 (convert-standard-filename
+ (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 (convert-standard-filename prefix) dir-flag suffix)))
+
+\f
+;; * ================================================================== *
+;; * 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))
+ (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))))
+
+(defun pdf-util-frame-ppi ()
+ "Return the PPI of the current frame."
+ (let* ((props (frame-monitor-attributes))
+ (px (nthcdr 2 (alist-get 'geometry props)))
+ (mm (alist-get 'mm-size props))
+ (dp (sqrt (+ (expt (nth 0 px) 2)
+ (expt (nth 1 px) 2))))
+ (di (sqrt (+ (expt (/ (nth 0 mm) 25.4) 2)
+ (expt (/ (nth 1 mm) 25.4) 2)))))
+ (/ dp di)))
+
+(defvar pdf-view-use-scaling)
+
+(defun pdf-util-frame-scale-factor ()
+ "Return the frame scale factor depending on the image type used for display.
+When `pdf-view-use-scaling' is non-nil and imagemagick or
+image-io are used as the image type for display, return the
+backing-scale-factor of the frame if available. If a
+backing-scale-factor attribute isn't available, return 2 if the
+frame's PPI is larger than 180. Otherwise, return 1."
+ (if (and pdf-view-use-scaling
+ (memq (pdf-view-image-type) '(imagemagick image-io))
+ (fboundp 'frame-monitor-attributes))
+ (or (cdr (assq 'backing-scale-factor (frame-monitor-attributes)))
+ (if (>= (pdf-util-frame-ppi) 180)
+ 2
+ 1))
+ 1))
+
+\f
+;; * ================================================================== *
+;; * 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
--- /dev/null
+;;; pdf-view.el --- View PDF documents. -*- lexical-binding:t -*-
+
+;; Copyright (C) 2013 Andreas Politz
+
+;; Author: Andreas Politz <politzaNOSPAMfh-trier.de>
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; 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")
+
+\f
+;; * ================================================================== *
+;; * 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 for rendering.
+
+This variable affects both the reuse of higher-resolution images
+as lower-resolution ones by down-scaling the image. As well as
+the rendering of higher-resolution for high-resolution displays,
+if available.
+
+It has no effect, unless either the imagemagick or image-io
+image-format is available."
+ :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))
+
+\f
+;; * ================================================================== *
+;; * 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))
+
+(defmacro pdf-view-window-needs-redisplay (&optional window)
+ `(image-mode-window-get 'needs-redisplay ,window))
+
+(defun pdf-view-current-pagelabel (&optional window)
+ (nth (1- (pdf-view-current-page window)) (pdf-info-pagelabels)))
+
+(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")))
+
+(defconst pdf-view-have-image-mode-pixel-vscroll
+ (>= emacs-major-version 27)
+ "Whether image-mode scrolls vertically by pixels.")
+
+\f
+;; * ================================================================== *
+;; * 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 "<next>") 'forward-page)
+ (define-key map (kbd "<prior>") '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 "<down>") 'pdf-view-next-line-or-next-page)
+ (define-key map (kbd "C-p") 'pdf-view-previous-line-or-previous-page)
+ (define-key map (kbd "<up>") '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)
+ ;; Disable image rescaling.
+ (when (boundp 'image-scaling-factor)
+ (setq-local image-scaling-factor 1))
+ ;; 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-redisplay-some-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)))
+
+(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)))))
+
+\f
+;; * ================================================================== *
+;; * 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))
+
+
+\f
+;; * ================================================================== *
+;; * 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 nil pdf-view-have-image-mode-pixel-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 nil pdf-view-have-image-mode-pixel-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 nil pdf-view-have-image-mode-pixel-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 nil pdf-view-have-image-mode-pixel-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))))
+
+\f
+;; * ================================================================== *
+;; * 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))))
+
+\f
+;; * ================================================================== *
+;; * 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 'image-io)
+ 'image-io)
+ ((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 (memq (pdf-view-image-type)
+ '(imagemagick image-io))
+ 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))
+ (let ((image-data (make-symbol "data")))
+ `(let ((,image-data ,data))
+ (apply #'create-image ,image-data (pdf-view-image-type) t ,@props
+ (cl-list*
+ :relief (or pdf-view-image-relief 0)
+ (when (and (eq (framep-on-display) 'mac)
+ (= (pdf-util-frame-scale-factor) 2))
+ (list :data-2x ,image-data)))))))
+
+(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."
+ (setf (pdf-view-window-needs-redisplay window) nil)
+ (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 pdf-view-have-image-mode-pixel-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))
+ (when (consp image-mode-winprops-alist)
+ (dolist (window (mapcar #'car image-mode-winprops-alist))
+ (unless (or (not (window-live-p window))
+ (eq (current-buffer)
+ (window-buffer window)))
+ (setf (pdf-view-window-needs-redisplay window) t)))))
+ (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-redisplay-some-windows ()
+ (pdf-view-maybe-redisplay-resized-windows)
+ (dolist (window (get-buffer-window-list nil nil t))
+ (when (pdf-view-window-needs-redisplay window)
+ (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 " 🌙" ))))
+
+\f
+;; * ================================================================== *
+;; * 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))))))
+
+\f
+;; * ================================================================== *
+;; * 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.05)
+ (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))))))
+\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))
+ (if pdf-view-have-image-mode-pixel-vscroll
+ 1
+ (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
--- /dev/null
+;;; pdf-virtual.el --- Virtual PDF documents -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2015 Andreas Politz
+
+;; Author: Andreas Politz <politzaNOSPAMhochschule-trier.de>
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; 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)
+
+\f
+;; * ================================================================== *
+;; * 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.")
+
+\f
+;; * ================================================================== *
+;; * 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)))))
+
+\f
+;; * ================================================================== *
+;; * 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))))
+
+\f
+;; * ================================================================== *
+;; * 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))
+
+\f
+;; * ================================================================== *
+;; * 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))))))
+
+\f
+;; * ================================================================== *
+;; * 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