1#! python
2
3project = dict(
4    name = 'python-poppler-qt5',
5    version = '0.75.0',
6    description = 'A Python binding to Poppler-Qt5',
7    long_description = (
8        'A Python binding to Poppler-Qt5 that aims for '
9        'completeness and for being actively maintained. '
10        'Using this module you can access the contents of PDF files '
11        'inside PyQt5 applications.'
12    ),
13    maintainer = 'Wilbert Berendsen',
14    maintainer_email = 'wbsoft@xs4all.nl',
15    url = 'https://github.com/frescobaldi/python-poppler-qt5',
16    license = 'LGPL',
17    classifiers = [
18        'Development Status :: 5 - Production/Stable',
19        'Intended Audience :: Developers',
20        'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
21        'Operating System :: MacOS :: MacOS X',
22        'Operating System :: Microsoft :: Windows',
23        'Operating System :: POSIX',
24        'Programming Language :: Python :: 3',
25        'Topic :: Multimedia :: Graphics :: Viewers',
26    ],
27    cmdclass={}
28)
29
30import os
31import re
32import shlex
33import subprocess
34import sys
35import platform
36
37try:
38   from setuptools import setup, Extension
39except ImportError:
40   from distutils.core import setup, Extension
41
42import sipdistutils
43
44### this circumvents a bug in sip < 4.14.2, where the file() builtin is used
45### instead of open()
46try:
47    import builtins
48    try:
49        builtins.file
50    except AttributeError:
51        builtins.file = open
52except ImportError:
53    pass
54### end
55
56
57def check_qtxml():
58    """Return True if PyQt5.QtXml can be imported.
59
60    in some early releases of PyQt5, QtXml was missing because it was
61    thought QtXml was deprecated.
62
63    """
64    import PyQt5
65    try:
66        import PyQt5.QtXml
67    except ImportError:
68        return False
69    return True
70
71
72def pkg_config(package, attrs=None, include_only=False):
73    """parse the output of pkg-config for a package.
74
75    returns the given or a new dictionary with one or more of these keys
76    'include_dirs', 'library_dirs', 'libraries'. Every key is a list of paths,
77    so that it can be used with distutils Extension objects.
78
79    """
80    if attrs is None:
81        attrs = {}
82    cmd = ['pkg-config']
83    if include_only:
84        cmd += ['--cflags-only-I']
85    else:
86        cmd += ['--cflags', '--libs']
87    cmd.append(package)
88    try:
89        output = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
90    except OSError:
91        return attrs
92    flag_map = {'-I': 'include_dirs', '-L': 'library_dirs', '-l': 'libraries'}
93    # for python3 turn bytes back to string
94    if sys.version_info[0] > 2:
95        output = output.decode('utf-8')
96    for flag in shlex.split(output):
97        option, path = flag[:2], flag[2:]
98        if option in flag_map:
99            l = attrs.setdefault(flag_map[option], [])
100            if path not in l:
101                l.append(path)
102    return attrs
103
104def pkg_config_version(package):
105    """Returns the version of the given package as a tuple of ints."""
106    cmd = ['pkg-config', '--modversion', package]
107    try:
108        output = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
109        # for python3 turn bytes back to string
110        if sys.version_info[0] > 2:
111            output = output.decode('utf-8')
112        return tuple(map(int, re.findall(r'\d+', output)))
113    except OSError:
114        sys.stderr.write("Can't determine version of %s\n" % package)
115
116ext_args = {}
117ext_args['extra_compile_args'] = ['-std=c++11']
118pkg_config('poppler-qt5', ext_args)
119
120if 'libraries' not in ext_args:
121    ext_args['libraries'] = ['poppler-qt5']
122
123# hack to provide our options to sip on its invocation:
124build_ext_base = sipdistutils.build_ext
125class build_ext(build_ext_base):
126
127    description = "Builds the popplerqt5 module."
128
129    user_options = build_ext_base.user_options + [
130        ('poppler-version=', None, "version of the poppler library"),
131        ('qmake-bin=', None, "Path to qmake binary"),
132        ('sip-bin=', None, "Path to sip binary"),
133        ('qt-include-dir=', None, "Path to Qt headers"),
134        ('pyqt-sip-dir=', None, "Path to PyQt's SIP files"),
135        ('pyqt-sip-flags=', None, "SIP flags used to generate PyQt bindings")
136    ]
137
138    def initialize_options (self):
139        build_ext_base.initialize_options(self)
140        self.poppler_version = None
141        self.qmake_bin = 'qmake'
142        self.sip_bin = None
143        self.qt_include_dir = None
144        self.pyqt_sip_dir = None
145        self.pyqt_sip_flags = None
146
147    def finalize_options (self):
148        build_ext_base.finalize_options(self)
149
150        if not self.qt_include_dir:
151            self.qt_include_dir = self.__find_qt_include_dir()
152
153        if not self.pyqt_sip_dir:
154            self.pyqt_sip_dir = self.__find_pyqt_sip_dir()
155
156        if not self.pyqt_sip_flags:
157            self.pyqt_sip_flags = self.__find_pyqt_sip_flags()
158
159        if not self.qt_include_dir:
160            raise SystemExit('Could not find Qt5 headers. '
161                             'Please specify via --qt-include-dir=')
162
163        if not self.pyqt_sip_dir:
164            raise SystemExit('Could not find PyQt SIP files. '
165                             'Please specify containing directory via '
166                             '--pyqt-sip-dir=')
167
168        if not self.pyqt_sip_flags:
169            raise SystemExit('Could not find PyQt SIP flags. '
170                             'Please specify via --pyqt-sip-flags=')
171
172        self.include_dirs += (self.qt_include_dir,
173                              os.path.join(self.qt_include_dir, 'QtCore'),
174                              os.path.join(self.qt_include_dir, 'QtGui'),
175                              os.path.join(self.qt_include_dir, 'QtXml'))
176
177        if self.poppler_version is not None:
178            self.poppler_version = tuple(map(int, re.findall(r'\d+', self.poppler_version)))
179
180    def __find_qt_include_dir(self):
181        if self.pyqtconfig:
182            return self.pyqtconfig.qt_inc_dir
183
184        try:
185            qt_version = subprocess.check_output([self.qmake_bin,
186                                                  '-query',
187                                                  'QT_VERSION'])
188            qt_version = qt_version.strip().decode("ascii")
189        except (OSError, subprocess.CalledProcessError) as e:
190            raise SystemExit('Failed to determine Qt version (%s).' % e)
191
192        if not qt_version.startswith("5."):
193            raise SystemExit('Unsupported Qt version (%s). '
194                             'Try specifying the path to qmake manually via '
195                             '--qmake-bin=' % qt_version)
196
197        try:
198            result =  subprocess.check_output([self.qmake_bin,
199                                               '-query',
200                                               'QT_INSTALL_HEADERS'])
201            return result.strip().decode(sys.getfilesystemencoding())
202        except (OSError, subprocess.CalledProcessError) as e:
203            raise SystemExit('Failed to determine location of Qt headers (%s).' % e)
204
205    def __find_pyqt_sip_dir(self):
206        if self.pyqtconfig:
207            return self.pyqtconfig.pyqt_sip_dir
208
209        import sipconfig
210
211        return os.path.join(sipconfig.Configuration().default_sip_dir, 'PyQt5')
212
213    def __find_pyqt_sip_flags(self):
214        if self.pyqtconfig:
215            return self.pyqtconfig.pyqt_sip_flags
216
217        from PyQt5 import QtCore
218
219        return QtCore.PYQT_CONFIGURATION.get('sip_flags', '')
220
221    @property
222    def pyqtconfig(self):
223        if not hasattr(self, '_pyqtconfig'):
224            try:
225                from PyQt5 import pyqtconfig
226
227                self._pyqtconfig = pyqtconfig.Configuration()
228            except ImportError:
229                self._pyqtconfig = None
230
231        return self._pyqtconfig
232
233    def write_version_sip(self, poppler_qt5_version, python_poppler_qt5_version):
234        """Write a version.sip file.
235
236        The file contains code to make version information accessible from
237        the popplerqt5 Python module.
238
239        """
240        with open('version.sip', 'w') as f:
241            f.write(version_sip_template.format(
242                vlen = 'i' * len(python_poppler_qt5_version),
243                vargs = ', '.join(map(format, python_poppler_qt5_version)),
244                pvlen = 'i' * len(poppler_qt5_version),
245                pvargs = ', '.join(map(format, poppler_qt5_version))))
246
247    def _find_sip(self):
248        """override _find_sip to allow for manually speficied sip path."""
249        return self.sip_bin or build_ext_base._find_sip(self)
250
251    def _sip_compile(self, sip_bin, source, sbf):
252
253        # First check manually specified poppler version
254        ver = self.poppler_version or pkg_config_version('poppler-qt5') or ()
255
256        # our own version:
257        version = tuple(map(int, re.findall(r'\d+', project['version'])))
258
259        # make those accessible from the popplerqt5 module:
260        self.write_version_sip(ver, version)
261
262        # Disable features if older poppler-qt5 version is found.
263        # See the defined tags in %Timeline{} in timeline.sip.
264        tag = 'POPPLER_V0_20_0'
265        if ver:
266            with open("timeline.sip", "r") as f:
267                for m in re.finditer(r'POPPLER_V(\d+)_(\d+)_(\d+)', f.read()):
268                    if ver < tuple(map(int, m.group(1, 2, 3))):
269                        break
270                    tag = m.group()
271
272        cmd = [sip_bin]
273        if hasattr(self, 'sip_opts'):
274            cmd += self.sip_opts
275        if hasattr(self, '_sip_sipfiles_dir'):
276            cmd += ['-I', self._sip_sipfiles_dir()]
277        if tag:
278            cmd += ['-t', tag]
279        if not check_qtxml():
280            cmd += ["-x", "QTXML_AVAILABLE"]     # mark QtXml not supported
281        cmd += [
282            "-c", self.build_temp,
283            "-b", sbf,
284            "-I", self.pyqt_sip_dir]             # find the PyQt5 stuff
285        cmd += shlex.split(self.pyqt_sip_flags)  # use same SIP flags as for PyQt5
286        cmd.append(source)
287        self.spawn(cmd)
288
289if platform.system() == 'Windows':
290   # Enforce libraries to link against on Windows
291   ext_args['libraries'] = ['poppler-qt5', 'Qt5Core', 'Qt5Gui', 'Qt5Xml']
292
293   class bdist_support():
294       def __find_poppler_dll(self):
295           paths = os.environ['PATH'].split(";")
296           poppler_dll = None
297
298           for path in paths:
299               dll_path_candidate = os.path.join(path, "poppler-qt5.dll")
300               if os.path.exists(dll_path_candidate):
301                   return dll_path_candidate
302
303           return None
304
305       def _copy_poppler_dll(self):
306           poppler_dll = self.__find_poppler_dll()
307           if poppler_dll is None:
308               self.warn("Could not find poppler-qt5.dll in any of the folders listed in the PATH environment variable.")
309               return False
310
311           self.mkpath(self.bdist_dir)
312           self.copy_file(poppler_dll, os.path.join(self.bdist_dir, "python-poppler5.dll"))
313
314           return True
315
316   import distutils.command.bdist_msi
317   class bdist_msi(distutils.command.bdist_msi.bdist_msi, bdist_support):
318       def run(self):
319           if not self._copy_poppler_dll():
320               return
321           distutils.command.bdist_msi.bdist_msi.run(self)
322
323   project['cmdclass']['bdist_msi'] = bdist_msi
324
325   import distutils.command.bdist_wininst
326   class bdist_wininst(distutils.command.bdist_wininst.bdist_wininst, bdist_support):
327       def run(self):
328           if not self._copy_poppler_dll():
329               return
330           distutils.command.bdist_wininst.bdist_wininst.run(self)
331   project['cmdclass']['bdist_wininst'] = bdist_wininst
332
333   import distutils.command.bdist_dumb
334   class bdist_dumb(distutils.command.bdist_dumb.bdist_dumb, bdist_support):
335       def run(self):
336           if not self._copy_poppler_dll():
337               return
338           distutils.command.bdist_dumb.bdist_dumb.run(self)
339   project['cmdclass']['bdist_dumb'] = bdist_dumb
340
341   try:
342       # Attempt to patch bdist_egg if the setuptools/distribute extension is installed
343       import setuptools.command.bdist_egg
344       class bdist_egg(setuptools.command.bdist_egg.bdist_egg, bdist_support):
345           def run(self):
346               if not self._copy_poppler_dll():
347                   return
348               setuptools.command.bdist_egg.bdist_egg.run(self)
349       project['cmdclass']['bdist_egg'] = bdist_egg
350   except ImportError:
351       pass
352
353
354version_sip_template = r"""// Generated by setup.py -- Do not edit
355
356PyObject *version();
357%Docstring
358The version of the popplerqt5 python module.
359%End
360
361PyObject *poppler_version();
362%Docstring
363The version of the Poppler library.
364%End
365
366%ModuleCode
367
368PyObject *version()
369{{ return Py_BuildValue("({vlen})", {vargs}); }};
370
371PyObject *poppler_version()
372{{ return Py_BuildValue("({pvlen})", {pvargs}); }};
373
374%End
375"""
376
377### use full README.rst as long description
378with open('README.rst', 'rb') as f:
379    project["long_description"] = f.read().decode('utf-8')
380
381
382
383project['cmdclass']['build_ext'] = build_ext
384setup(
385    ext_modules = [Extension("popplerqt5", ["poppler-qt5.sip"], **ext_args)],
386    **project
387)
388