Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ body:
attributes:
label: PyMuPDF version
options:
- 1.28.0
- 1.27.2.3
- 1.27.2.2
- 1.27.2
Expand Down Expand Up @@ -87,6 +88,5 @@ body:
- "3.12"
- "3.11"
- "3.10"
- "3.9"
validations:
required: true
2 changes: 1 addition & 1 deletion .github/workflows/test_quick.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ jobs:
env:
PYMUPDF_test_args: ${{inputs.args}}
run:
python scripts/test.py build test -m 'git:--branch 1.27.x https://github.com/ArtifexSoftware/mupdf.git' -a PYMUPDF_test_args
python scripts/test.py build test -m 'git:--branch 1.28.x https://github.com/ArtifexSoftware/mupdf.git' -a PYMUPDF_test_args
7 changes: 5 additions & 2 deletions changes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ Change Log
==========


**Changes in version 1.28.0**

Fixed issues:

* **Fixed** `4423 <https://github.com/pymupdf/PyMuPDF/issues/4423>`_: pymupdf.mupdf.FzErrorFormat: code=7: cannot find object in xref error encountered after version 1.25.3
* **Fixed** `4114 <https://github.com/pymupdf/PyMuPDF/issues/4114>`_: ComboBox choice_values full of empty strings despite PDF having valid choices.
* **Fixed** `5001 <https://github.com/pymupdf/PyMuPDF/issues/5001>`_: Formulae incorrectly rendered as black boxes
* **Fixed** `4423 <https://github.com/pymupdf/PyMuPDF/issues/4423>`_: pymupdf.mupdf.FzErrorFormat: code=7: cannot find object in xref error encountered after version 1.25.3
* **Fixed** `4950 <https://github.com/pymupdf/PyMuPDF/issues/4950>`_: remove_rotation() raises ValueError on widgets with empty/infinite rects
* **Fixed** `5001 <https://github.com/pymupdf/PyMuPDF/issues/5001>`_: Formulae incorrectly rendered as black boxes

Other:

* Use MuPDF-1.28.0-rc2.
* pymupdf.Document.__init__(): new arg `archive` to support documents with archives.
* pymupdf.Document.convert_to_pdf(): also generate links.
* Fixed MacOS x64 platform tag - used to be macosx_10_9_x86_64, but we actually
Expand Down
196 changes: 196 additions & 0 deletions scripts/autovenv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
'''
Automatic creation/use of a venv.

Example usage:

import autovenv
autovenv.enter(create=3, packages=['foopackage', 'barpackage'])
import foomodule
import barmodule
...
'''

import os
import platform
import shlex
import shutil
import subprocess
import sys
import sysconfig
import time


def log(text, verbose, t0):
debug = False
if debug or verbose:
text = f'autovenv [+{time.time()-t0:.1f}]: {text}'
if verbose:
print(text, flush=1)
if debug:
with open('autoenv-env-log.txt', 'a') as f:
print(text, file=f)


def run(command, verbose, t0, check=1, batch=True):
log(f'Running {batch=}: {command}', verbose, t0)
if batch:
def write_out(text):
for line in text.split('\n'):
log(line, verbose=True, t0=t0)
try: # pylint: disable=no-else-raise
cp = subprocess.run(command, shell=1, check=check, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
except subprocess.SubprocessError as e:
text = e.stdout # pylint: disable=no-member
# Docs say that e.stdout is always bytes, but doesn't seem to be the case?
if isinstance(text, bytes):
text = text.decode('utf8', errors='replace')
write_out(text)
raise
else:
text = cp.stdout
write_out(text)
return cp
else:
cp = subprocess.run(command, shell=1, check=check)
return cp


def gil():
Py_GIL_DISABLED = sysconfig.get_config_var('Py_GIL_DISABLED')
#print(f'{Py_GIL_DISABLED=}')
if Py_GIL_DISABLED==1:
gil_enabled = sys._is_gil_enabled() # pylint:disable=protected-access
#print(f'{gil_enabled=}')
if not gil_enabled:
return False
return True


def bits():
return int.bit_length(sys.maxsize+1)


def shlex_join_windows(argv):
'''
shlex not reliable on Windows.
Use crude quoting with "...". Seems to work.
'''
argv2 = list()
for arg in argv:
if arg.startswith('"') and arg.endswith('"'):
assert '"' not in arg, f'Cannot quote {arg=}.'
argv2.append(arg)
else:
assert '"' not in arg, f'Cannot quote {arg=}.'
argv2.append(f'"{arg}"')
return ' '.join(argv2)


def enter(*,
venv_name=None,
venv_prefix='autovenv',
create=None,
packages=None,
verbose=False,
):
'''
Creates and re-runs inside a venv if we are not already in a venv.

If we are already in a venv, we do nothing.

Otherwise we do the following:

* Create the venv if required.
* In the venv, run `pip install --upgrade` for each item in <packages>.
* Create a child process that runs `python <sys.args>` inside the venv.
* Call `sys.exit()` with termination code of child process.
* We do not return.

Args:

venv_name:
Name of venv. If false, we use:
<venv_prefix>-<python-version><T>-<bits>
Where <T> is '-t' if free-thread else ''.
venv_prefix:
Used if <venv_name> is false.
create:
One of:
1: Only run `python -m venv <venv_name>` and install packages
if the <venv_name> directory does not exist.
2: Always run `python -m venv <venv_name>`.
3: Delete any existing venv and then run `python -m venv <venv_name>`.
If None, we use create=2.
packages:
String or list of packages, passed directly to `pip install`.
verbose:
If true we output diagnostics when running commands.
'''
t0 = time.time()

if create is None:
create = 2
assert create in (1, 2, 3), f'Unrecognised {create=}, should be 1, 2 or 3.'

if sys.prefix != sys.base_prefix:
log(f'Already in a venv, {sys.prefix=}.', verbose, t0)
return

# We are not in a venv.
log(f'Not in a venv.', verbose, t0)

# Set venv name.
if not venv_name:
if not venv_prefix:
venv_prefix = f'autovenv'
venv_name = f'{venv_prefix}-{platform.python_version()}{"" if gil() else "-t"}-{bits()}'

# Create venv.
if create == 3:
# Delete any existing venv.
if os.path.isdir(venv_name):
shutil.rmtree(venv_name, ignore_errors=1)
assert not os.path.exists(venv_name)

if create == 1 and os.path.isdir(venv_name):
# Don't recreate existing venv or install packages.
packages = list()
else:
# Create venv.
if platform.system() == 'Windows':
executable = f'"{sys.executable}"'
else:
executable = shlex.quote(sys.executable)
run(f'{executable} -m venv {venv_name}', verbose, t0)

# Get command to enter venv.
if platform.system() == 'Windows':
# shlex not reliable on Windows.
# Use crude quoting with "...". Seems to work.
venv_enter = f'{venv_name}\\Scripts\\activate'
argv_string = ''
for arg in sys.argv:
if arg.startswith('"') and arg.endswith('"'):
argv_string += f' {arg}'
else:
assert '"' not in arg, f'Cannot handle arg containing double quote on windows: {arg=}'
argv_string += f' "{arg}"'
else:
venv_enter = f'. {venv_name}/bin/activate'
argv_string = shlex.join(sys.argv)

# Install packages.
if isinstance(packages, str):
packages = (packages,)
for package in (packages or list()):
if package:
run(f'{venv_enter} && pip install --upgrade {shlex.quote(package)}', verbose, t0)

# Rerun ourselves in the venv.
if platform.system() == 'Windows':
command = f'{venv_enter} && python {shlex_join_windows(sys.argv)}'
else:
command = f'{venv_enter} && python {shlex.join(sys.argv)}'

cp = run(command, verbose, t0, check=0, batch=0)
sys.exit(cp.returncode)
9 changes: 4 additions & 5 deletions scripts/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,14 +375,13 @@
import sys
import textwrap

import autovenv
autovenv.enter(packages='pipcl')

import pipcl

pymupdf_dir_abs = os.path.abspath( f'{__file__}/../..')

try:
sys.path.insert(0, f'{pymupdf_dir_abs}/src')
import pipcl
finally:
del sys.path[0]

try:
sys.path.insert(0, f'{pymupdf_dir_abs}/scripts')
Expand Down
39 changes: 17 additions & 22 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -720,12 +720,9 @@ def add(flavour, from_, to_):
header_rel = header_abs[len(root)+1:]
add('d', f'{header_abs}', f'{to_dir_d}/include/{header_rel}')

# Add a .py file containing location of MuPDF.
try:
sha, comment, diff, branch = git_info(g_root)
except Exception as e:
log(f'Failed to get git information: {e}')
sha, comment, diff, branch = (None, None, None, None)
# Add a .py file containing build-time information - location of MuPDF,
# pymupdf git info, swig version etc.
#
swig = PYMUPDF_SETUP_SWIG or 'swig'
swig_version_text = run(f'{swig} -version', capture=1)
m = re.search('\nSWIG Version ([^\n]+)', swig_version_text)
Expand All @@ -741,12 +738,10 @@ def int_or_0(text):
version_p_tuple = tuple(int_or_0(i) for i in version_p.split('.'))
log(f'{swig_version=}')
text = ''
text += pipcl.git_info_py(g_root, check=0, prefix = 'pymupdf_git_')
text += f'mupdf_location = {mupdf_location!r}\n'
text += f'pymupdf_version = {version_p!r}\n'
text += f'pymupdf_version_tuple = {version_p_tuple!r}\n'
text += f'pymupdf_git_sha = {sha!r}\n'
text += f'pymupdf_git_diff = {diff!r}\n'
text += f'pymupdf_git_branch = {branch!r}\n'
text += f'swig_version = {swig_version!r}\n'
text += f'swig_version_tuple = {swig_version_tuple!r}\n'
text += f'fake_no_gil = {PYMUPDF_SETUP_FAKE_NOGIL=="1"!r}\n'
Expand Down Expand Up @@ -830,7 +825,7 @@ def build_mupdf_windows(
windows_build_tail += f'-Py_LIMITED_API_{pipcl.current_py_limited_api()}'
if PYMUPDF_SETUP_FAKE_NOGIL == '1':
windows_build_tail += '-nogil'
windows_build_tail += f'-x{wp.cpu.bits}-py{wp.version}'
windows_build_tail += f'-{wp.cpu.windows_name}-py{wp.version}'
pipcl.log(f'{sysconfig.get_config_var("Py_GIL_DISABLED")=}')
if sysconfig.get_config_var('Py_GIL_DISABLED')==1:
# We are building with free-threading python.
Expand Down Expand Up @@ -889,9 +884,8 @@ def build_mupdf_windows(


def _windows_lib_directory(mupdf_local, build_type):
ret = f'{mupdf_local}/platform/win32/'
if _cpu_bits() == 64:
ret += 'x64/'
wc = pipcl.wdev.WindowsCpu()
ret = f'{mupdf_local}/platform/win32/{wc.windows_subdir}'
if build_type == 'release':
ret += 'Release/'
elif build_type == 'debug':
Expand Down Expand Up @@ -1253,6 +1247,7 @@ def clean(all_):
shutil.rmtree(f'{path_mupdf}/platform/win32', ignore_errors=True)
shutil.rmtree(f'{path_mupdf}/platform/win32/Release', ignore_errors=True)
shutil.rmtree(f'{path_mupdf}/platform/win32/x64', ignore_errors=True)
shutil.rmtree(f'{path_mupdf}/platform/win32/arm64', ignore_errors=True)

pipcl.log(f'Returning: {ret=}')
return ret
Expand Down Expand Up @@ -1320,9 +1315,9 @@ def sdist():
#

# PyMuPDF version.
version_p = '1.27.2.3'
version_p = '1.28.0'

version_mupdf = '1.27.2'
version_mupdf = '1.28.0-rc2'

# PyMuPDFb version. This is the PyMuPDF version whose PyMuPDFb wheels we will
# (re)use if generating separate PyMuPDFb wheels. Though as of PyMuPDF-1.24.11
Expand All @@ -1343,7 +1338,7 @@ def get_requires_for_build_wheel(config_settings=None):
return list()

p = pipcl.Package(
'PyMuPDFb',
'pymupdfb',
version_b,
summary = 'Dummy PyMuPDFb wheel',
description = '',
Expand Down Expand Up @@ -1371,27 +1366,27 @@ def get_requires_for_build_wheel(config_settings=None):

if 'p' in PYMUPDF_SETUP_FLAVOUR:
version = version_p
name = 'PyMuPDF'
name = 'pymupdf'
readme = readme_p
summary = 'A high performance Python library for data extraction, analysis, conversion & manipulation of PDF (and other) documents.'
if 'b' not in PYMUPDF_SETUP_FLAVOUR:
requires_dist.append(f'PyMuPDFb =={version_b}')
requires_dist.append(f'pymupdfb =={version_b}')
# Create a `pymupdf` command.
entry_points = textwrap.dedent('''
[console_scripts]
pymupdf = pymupdf.__main__:main
''')
elif 'b' in PYMUPDF_SETUP_FLAVOUR:
version = version_b
name = 'PyMuPDFb'
name = 'pymupdfb'
readme = readme_b
summary = 'MuPDF shared libraries for PyMuPDF.'
summary = 'MuPDF shared libraries for pymupdf.'
tag_python = 'py3'
elif 'd' in PYMUPDF_SETUP_FLAVOUR:
version = version_b
name = 'PyMuPDFd'
name = 'pymupdfd'
readme = readme_d
summary = 'MuPDF build-time files for PyMuPDF.'
summary = 'MuPDF build-time files for pymupdf.'
tag_python = 'py3'
else:
assert 0, f'Unrecognised {PYMUPDF_SETUP_FLAVOUR=}.'
Expand Down
Loading
Loading