From 925c13f988a011ddd78581416795e2c4fc845e89 Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Fri, 24 Jul 2026 14:20:01 +0100 Subject: [PATCH 1/9] tests/: added test_5054(). Currently expects incorrect behaviour for all mupdf versions; will update the test when mupdf is fixed. --- tests/resources/test_5054.pdf | Bin 0 -> 603 bytes tests/test_general.py | 16 ++++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 tests/resources/test_5054.pdf diff --git a/tests/resources/test_5054.pdf b/tests/resources/test_5054.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c907982f3259275c01d2ada4ba6bb492e367abd3 GIT binary patch literal 603 zcmZWnO>e?5487-9_=2<@wj}-FLqZ%FRFk%82y#O^1aMgqG*cTUwqHNHAA^mkmFnkb zzgIh**(ABs_dTIOfm`jwa0t?TeIUpO>+RlcLC)+JO9+TChtWu2zF{KAe+z1!e9AT@ zEJLQhQ5Sd2J?M)<&RiF=Oo2qLb>s_fGCOuBSSsR&NNm&I9g8&?VyB6tit0l{ z%!;JL56R}9x|I+$Xlt9}R{a_Ct$&Aj!0t+4pgoAO5i-#bL}1ctD2mK$lJC)L4&o-g zH9Ntc%!C4qWQCEb5iHSkX=cQhTCsYlm0`I!u6#9P1~tgBEwRo#;{FqTwzd}oI~ACc zVppi&1Kl_dV@TnQ4WP% W$7^lwMConeE1yxIM5i--O~em?mYa6~ literal 0 HcmV?d00001 diff --git a/tests/test_general.py b/tests/test_general.py index d7f5a2ee2..efe461092 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -2256,3 +2256,19 @@ def test_5056(): assert text_normal in data_normal assert text_normal not in data_repro assert text_repro in data_repro + + +def test_5054(): + path = os.path.normpath(f'{__file__}/../../tests/resources/test_5054.pdf') + with pymupdf.open(path) as document: + page = document[0] + text1 = page.get_text() + page.clean_contents(sanitize=True) + text2 = page.get_text() + print(f'{text1=}') + print(f'{text2=}') + if 1: + # Currently fails for all mupdf versions. + assert text2 != text1 + else: + assert text2 == text1 From d97c797151ba1586e89ee4a316824e9fde6f60c2 Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Tue, 4 Aug 2026 12:05:15 +0100 Subject: [PATCH 2/9] setup.py: misc cleanups. * Use pipcl functions instead of local code, e.g. _fs_remove() => pipl.fs_remove(), pipcl.windows() etc. * Removed unused functions: _fs_remove() _git_get_branch() git_info() git_patch() _cpu_bits() _fs_update. * Removed old PYMUPDF_SETUP_FLAVOUR code fragment. * Removed `if 1:` for top-level block of code that creates pipcl.Package(). --- setup.py | 411 +++++++++++++++++++------------------------------------ 1 file changed, 137 insertions(+), 274 deletions(-) diff --git a/setup.py b/setup.py index f0d7a9f83..21aad9681 100755 --- a/setup.py +++ b/setup.py @@ -181,21 +181,11 @@ run = pipcl.run - -if 1: - # For debugging. - log(f'### Starting.') - pipcl.show_system() - - -PYMUPDF_SETUP_FLAVOUR = os.environ.get( 'PYMUPDF_SETUP_FLAVOUR', 'pbd') -for i in PYMUPDF_SETUP_FLAVOUR: - assert i in 'pbd', f'Unrecognised flag "{i} in {PYMUPDF_SETUP_FLAVOUR=}. Should be one of "p", "b", "d"' +log(f'### Starting.') +pipcl.show_system() g_root = os.path.abspath( f'{__file__}/..') -python_version_tuple = tuple(int(x) for x in platform.python_version_tuple()[:2]) - PYMUPDF_SETUP_PY_LIMITED_API = os.environ.get('PYMUPDF_SETUP_PY_LIMITED_API') assert PYMUPDF_SETUP_PY_LIMITED_API in (None, '', '0', '1'), \ f'Should be "", "0", "1" or undefined: {PYMUPDF_SETUP_PY_LIMITED_API=}.' @@ -225,50 +215,6 @@ def mupdf_win32_infix(): return 'win32' -def _fs_remove(path): - ''' - Removes file or directory, without raising exception if it doesn't exist. - - We assert-fail if the path still exists when we return, in case of - permission problems etc. - ''' - # First try deleting `path` as a file. - try: - os.remove( path) - except Exception as e: - pass - - if os.path.exists(path): - # Try deleting `path` as a directory. Need to use - # shutil.rmtree() callback to handle permission problems; see: - # https://docs.python.org/3/library/shutil.html#rmtree-example - # - def error_fn(fn, path, excinfo): - # Clear the readonly bit and reattempt the removal. - os.chmod(path, stat.S_IWRITE) - fn(path) - shutil.rmtree( path, onerror=error_fn) - - assert not os.path.exists( path) - - -def _git_get_branch( directory): - command = f'cd {directory} && git branch --show-current' - log( f'Running: {command}') - p = subprocess.run( - command, - shell=True, - check=False, - text=True, - stdout=subprocess.PIPE, - ) - ret = None - if p.returncode == 0: - ret = p.stdout.strip() - log( f'Have found MuPDF git branch: ret={ret!r}') - return ret - - def tar_check(path, mode='r:gz', prefix=None, remove=False): ''' Checks items in tar file have same , or if not None. @@ -340,68 +286,6 @@ def tar_extract(path, mode='r:gz', prefix=None, exists='raise'): return prefix_actual -def git_info( directory): - ''' - Returns `(sha, comment, diff, branch)`, all items are str or None if not - available. - - directory: - Root of git checkout. - ''' - sha, comment, diff, branch = '', '', '', '' - cp = subprocess.run( - f'cd {directory} && (PAGER= git show --pretty=oneline|head -n 1 && git diff)', - capture_output=1, - shell=1, - text=1, - ) - if cp.returncode == 0: - sha, _ = cp.stdout.split(' ', 1) - comment, diff = _.split('\n', 1) - cp = subprocess.run( - f'cd {directory} && git rev-parse --abbrev-ref HEAD', - capture_output=1, - shell=1, - text=1, - ) - if cp.returncode == 0: - branch = cp.stdout.strip() - log(f'git_info(): directory={directory!r} returning branch={branch!r} sha={sha!r} comment={comment!r}') - return sha, comment, diff, branch - - -def git_patch(directory, patch, hard=False): - ''' - Applies string with `git patch` in . - - If is true we clean the tree with `git checkout .` and then apply - the patch. - - Otherwise we apply patch only if it is not already applied; this might fail - if there are conflicting changes in the tree. - ''' - log(f'Applying patch in {directory}:\n{textwrap.indent(patch, " ")}') - if not patch: - return - # Carriage returns break `git apply` so we use `newline='\n'` in open(). - path = os.path.abspath(f'{directory}/pymupdf_patch.txt') - with open(path, 'w', newline='\n') as f: - f.write(patch) - log(f'Using patch file: {path}') - if hard: - run(f'cd {directory} && git checkout .') - run(f'cd {directory} && git apply {path}') - log(f'Have applied patch in {directory}.') - else: - e = run( f'cd {directory} && git apply --check --reverse {path}', check=0) - if e == 0: - log(f'Not patching {directory} because already patched.') - else: - run(f'cd {directory} && git apply {path}') - log(f'Have applied patch in {directory}.') - run(f'cd {directory} && git diff') - - mupdf_tgz = os.path.abspath( f'{__file__}/../mupdf.tgz') def get_mupdf_internal(out, location=None, local_tgz=None): @@ -463,7 +347,7 @@ def get_mupdf_internal(out, location=None, local_tgz=None): tar_check(local_tgz, 'r:gz', prefix=f'{name}/') except Exception as e: log(f'Not using existing file {local_tgz} because invalid tar data: {e}') - _fs_remove( local_tgz) + pipcl.fs_remove( local_tgz) if os.path.exists(local_tgz): log(f'Not downloading from {location} because already present: {local_tgz!r}') else: @@ -491,7 +375,7 @@ def get_mupdf_internal(out, location=None, local_tgz=None): top = os.path.basename(local_dir) local_tgz = f'{local_dir}.tgz' log( f'Creating .tgz from git files. {top=} {local_dir=} {local_tgz=}') - _fs_remove( local_tgz) + pipcl.fs_remove( local_tgz) with tarfile.open( local_tgz, 'w:gz') as f: for name in pipcl.git_items( local_dir, submodules=True): path = os.path.join( local_dir, name) @@ -538,19 +422,17 @@ def get_mupdf(path=None, sha=None): return get_mupdf_internal('dir', m) -linux = sys.platform.startswith( 'linux') or 'gnu' in sys.platform -openbsd = sys.platform.startswith( 'openbsd') -freebsd = sys.platform.startswith( 'freebsd') -darwin = sys.platform.startswith( 'darwin') -windows = platform.system() == 'Windows' or platform.system().startswith('CYGWIN') -msys2 = platform.system().startswith('MSYS_NT-') +def msys2(): + return platform.system().startswith('MSYS_NT-') if os.environ.get('PYODIDE') == '1': if os.environ.get('OS') != 'pyodide': log('PYODIDE=1, setting OS=pyodide.') os.environ['OS'] = 'pyodide' -pyodide = os.environ.get('OS') == 'pyodide' +def pyodide(): + return os.environ.get('OS') == 'pyodide' + def build(): ''' @@ -580,7 +462,7 @@ def build(): # Build MuPDF shared libraries. # - if windows: + if pipcl.windows(): mupdf_build_dir = build_mupdf_windows( mupdf_local, build_type, @@ -644,7 +526,7 @@ def build(): ret.append( (f'{mupdf_build_dir}/mupdf.py', to_dir) ) # Add MuPDF shared libraries. - if windows: + if pipcl.windows(): wp = pipcl.wdev.WindowsPython() ret.append( (f'{mupdf_build_dir}/_mupdf.pyd', to_dir) ) ret.append( (f'{mupdf_build_dir}/mupdfcpp{wp.cpu.windows_suffix}.dll', to_dir) ) @@ -654,12 +536,12 @@ def build(): ret.append( (f'{mupdf_build_dir2}/mupdfcpp{wp.cpu.windows_suffix}.lib', f'{to_dir_d}/lib/') ) # MuPDF-1.25+ language bindings build also builds libmuthreads. ret.append( (f'{mupdf_build_dir2}/libmuthreads.lib', f'{to_dir_d}/lib/') ) - elif darwin: + elif pipcl.darwin(): ret.append( (f'{mupdf_build_dir}/_mupdf.so', to_dir) ) ret.append( (f'{mupdf_build_dir}/libmupdfcpp.so', to_dir) ) ret.append( (f'{mupdf_build_dir}/libmupdf.dylib', to_dir) ) ret.append( (f'{mupdf_build_dir}/libmupdf-threads.a', f'{to_dir_d}/lib/') ) - elif pyodide: + elif pyodide(): ret.append( (f'{mupdf_build_dir}/_mupdf.so', to_dir) ) ret.append( (f'{mupdf_build_dir}/libmupdfcpp.so', to_dir) ) ret.append( (f'{mupdf_build_dir}/libmupdf.so', to_dir) ) @@ -856,12 +738,6 @@ def _windows_lib_directory(mupdf_local, build_type): return ret -def _cpu_bits(): - if sys.maxsize == 2**31 - 1: - return 32 - return 64 - - def build_mupdf_unix( mupdf_local, build_type, @@ -895,10 +771,10 @@ def build_mupdf_unix( env_add(env, 'XCFLAGS', '-DTOFU_CJK_EXT') env_add(env, 'XCXXFLAGS', '-DTOFU_CJK_EXT') - if openbsd or freebsd: + if pipcl.openbsd() or platform.system() == 'FreeBSD': env_add(env, 'CXX', 'c++', ' ') - if darwin and os.environ.get('GITHUB_ACTIONS') == 'true': + if pipcl.darwin() and os.environ.get('GITHUB_ACTIONS') == 'true': if os.environ.get('ImageOS') == 'macos13': # On Github macos13 we need to use Clang/LLVM (Homebrew) 15.0.7, # otherwise mupdf:thirdparty/tesseract/src/api/baseapi.cpp fails to @@ -956,7 +832,7 @@ def build_mupdf_unix( # # Avoid link command length problems seen on musllinux. build_prefix = '' - if pyodide: + if pyodide(): build_prefix += 'pyodide-' else: build_prefix += f'{platform.machine()}-' @@ -964,7 +840,7 @@ def build_mupdf_unix( if build_prefix_extra: build_prefix += f'{build_prefix_extra}-' build_prefix += 'shared-' - if msys2: + if msys2(): # Error in mupdf/scripts/tesseract/endianness.h: # #error "I don't know what architecture this is!" log(f'msys2: building MuPDF without tesseract.') @@ -973,7 +849,7 @@ def build_mupdf_unix( else: build_prefix += 'tesseract-' if ( - linux + pipcl.linux() and os.environ.get('PYMUPDF_SETUP_MUPDF_BSYMBOLIC', '1') == '1' ): log(f'Appending `bsymbolic-` to MuPDF build path.') @@ -1008,10 +884,7 @@ def build_mupdf_unix( command += f' --refcheck-if "{PYMUPDF_SETUP_MUPDF_REFCHECK_IF}"' if PYMUPDF_SETUP_MUPDF_TRACE_IF: command += f' --trace-if "{PYMUPDF_SETUP_MUPDF_TRACE_IF}"' - if 'p' in PYMUPDF_SETUP_FLAVOUR: - command += ' all' - else: - command += ' m01' # No need for C++/Python bindings. + command += ' all' command += f' && echo {unix_build_dir}:' command += f' && ls -l {unix_build_dir}' @@ -1038,17 +911,6 @@ def get_mupdf_version(mupdf_dir): v2 = int(v2.group(1)) return v0, v1, v2 -def _fs_update(text, path): - try: - with open( path) as f: - text0 = f.read() - except OSError: - text0 = None - print(f'path={path!r} text==text0={text==text0!r}') - if text != text0: - with open( path, 'w') as f: - f.write( text) - def _build_extension( mupdf_local, mupdf_build_dir, build_type, g_py_limited_api): ''' @@ -1067,7 +929,7 @@ def _build_extension( mupdf_local, mupdf_build_dir, build_type, g_py_limited_api log('Building PyMuPDF extension.') compile_extra_cpp = '' - if darwin: + if pipcl.darwin(): # Avoids `error: cannot pass object of non-POD type # 'std::nullptr_t' through variadic function; call will abort at # runtime` when compiling `mupdf::pdf_dict_getl(..., nullptr)`. @@ -1075,7 +937,7 @@ def _build_extension( mupdf_local, mupdf_build_dir, build_type, g_py_limited_api # Avoid errors caused by mupdf's C++ bindings' exception classes # not having `nothrow` to match the base exception class. compile_extra_cpp += ' -std=c++14' - if windows: + if pipcl.windows(): wp = pipcl.wdev.WindowsPython() libs = f'mupdfcpp{wp.cpu.windows_suffix}.lib' else: @@ -1124,7 +986,7 @@ def _extension_flags( mupdf_local, mupdf_build_dir, build_type): debug = 'debug' in mupdf_build_dir_flags r_extra = '' defines = list() - if windows: + if pipcl.windows(): defines.append('FZ_DLL_CLIENT') wp = pipcl.wdev.WindowsPython() build_type_infix = 'Debug' if debug else 'Release' @@ -1148,7 +1010,7 @@ def _extension_flags( mupdf_local, mupdf_build_dir, build_type): if mupdf_local: libpaths = (mupdf_build_dir,) libraries = f'{mupdf_build_dir}/{libs[0]}' - if openbsd: + if pipcl.openbsd(): compiler_extra += ' -Wno-deprecated-declarations' else: libpaths = os.environ.get('PYMUPDF_MUPDF_LIB') @@ -1215,6 +1077,7 @@ def sdist(): return ret for p in pipcl.git_items( g_root): + pipcl.log(f'{p=}') if p.startswith( ( 'docs/', @@ -1223,6 +1086,7 @@ def sdist(): ) ): pass + pipcl.log(f'Omiting {p=}') else: ret.append(p) if 0: @@ -1239,119 +1103,118 @@ def sdist(): version_mupdf = '1.28.0' -if 1: - # A normal PyMuPDF package. - - requires_dist = list() - if os.environ.get('PYODIDE_ROOT'): - # We can't pip install pytest on pyodide, so specify it here. - requires_dist.append('pytest') - requires_dist.append('pipcl') - - p = pipcl.Package( - 'pymupdf', - version_p, - summary = 'A high performance Python library for data extraction, analysis, conversion & manipulation of PDF (and other) documents.', - description = 'README.md', - description_content_type = 'text/markdown', - classifier = [ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Intended Audience :: Information Technology', - 'Operating System :: MacOS', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: C', - 'Programming Language :: C++', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: Implementation :: CPython', - 'Topic :: Utilities', - 'Topic :: Multimedia :: Graphics', - 'Topic :: Software Development :: Libraries', - ], - - author = 'Artifex', - author_email = 'support@artifex.com', - requires_dist = requires_dist, - requires_python = '>=3.10', - license = 'Dual Licensed - GNU AFFERO GPL 3.0 or Artifex Commercial License', - project_url = [ - ('Documentation, https://pymupdf.readthedocs.io/'), - ('Source, https://github.com/pymupdf/pymupdf'), - ('Tracker, https://github.com/pymupdf/PyMuPDF/issues'), - ('Changelog, https://pymupdf.readthedocs.io/en/latest/changes.html'), - ], - - # We create a `pymupdf` command. - entry_points = textwrap.dedent(''' - [console_scripts] - pymupdf = pymupdf.__main__:main - '''), - - fn_build=build, - fn_clean=clean, - fn_sdist=sdist, - - py_limited_api=g_py_limited_api, - - # 30MB: 9 ZIP_DEFLATED - # 28MB: 9 ZIP_BZIP2 - # 23MB: 9 ZIP_LZMA - #wheel_compression = zipfile.ZIP_DEFLATED if (darwin or pyodide) else zipfile.ZIP_LZMA, - wheel_compresslevel = 9, - ) - - # Patch up macos platform tag - we require at least 10.15 because otherwise - # std::filesystem appears to be not available. - if pipcl.darwin(): - pt = p.tag_platform() - #pipcl.log(f'{pt=}') - m = re.match('^(macosx_)(([0-9]+)_([0-9]+))(.*)', pt) - #pipcl.log(f'{m=}') - if m: - v = int(m.group(3)), int(m.group(4)) - #pipcl.log(f'{v=}') - if v < (10, 15): - pt2 = f'{m.group(1)}10_15{m.group(5)}' - pipcl.log(f'Changing tag_platform from {pt!r} to {pt2!r}') - p.tag_platform_ = pt2 - - def get_requires_for_build_wheel(config_settings=None): - ''' - Adds to pyproject.toml:[build-system]:requires, allowing programmatic - control over what packages we require. - ''' - def platform_release_tuple(): - r = platform.release() - r = r.split('.') - r = tuple(int(i) for i in r) - log(f'platform_release_tuple() returning {r=}.') - return r - - ret = list() - libclang = os.environ.get('PYMUPDF_SETUP_LIBCLANG') - if libclang: - print(f'Overriding to use {libclang=}.') - ret.append(libclang) - elif openbsd: - print(f'OpenBSD: libclang not available via pip; assuming `pkg_add py3-llvm`.') - elif darwin and platform_release_tuple() < (18,): - # There are still of problems when building on old macos. - ret.append('libclang==14.0.6') - else: - ret.append('libclang') - if msys2: - print(f'msys2: pip install of swig does not build; assuming `pacman -S swig`.') - elif openbsd: - print(f'OpenBSD: pip install of swig does not build; assuming `pkg_add swig`.') - elif PYMUPDF_SETUP_SWIG: - pass - elif darwin and python_version_tuple < (3, 13): - # Latest swig-4.4.1 gives director errors on macos with python<3.13. - ret.append('swig==4.3.1') - else: - ret.append('swig') - return ret +# A normal PyMuPDF package. + +requires_dist = list() +if os.environ.get('PYODIDE_ROOT'): + # We can't pip install pytest on pyodide, so specify it here. + requires_dist.append('pytest') + requires_dist.append('pipcl') + +p = pipcl.Package( + 'pymupdf', + version_p, + summary = 'A high performance Python library for data extraction, analysis, conversion & manipulation of PDF (and other) documents.', + description = 'README.md', + description_content_type = 'text/markdown', + classifier = [ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Intended Audience :: Information Technology', + 'Operating System :: MacOS', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: C', + 'Programming Language :: C++', + 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: Implementation :: CPython', + 'Topic :: Utilities', + 'Topic :: Multimedia :: Graphics', + 'Topic :: Software Development :: Libraries', + ], + + author = 'Artifex', + author_email = 'support@artifex.com', + requires_dist = requires_dist, + requires_python = '>=3.10', + license = 'Dual Licensed - GNU AFFERO GPL 3.0 or Artifex Commercial License', + project_url = [ + ('Documentation, https://pymupdf.readthedocs.io/'), + ('Source, https://github.com/pymupdf/pymupdf'), + ('Tracker, https://github.com/pymupdf/PyMuPDF/issues'), + ('Changelog, https://pymupdf.readthedocs.io/en/latest/changes.html'), + ], + + # We create a `pymupdf` command. + entry_points = textwrap.dedent(''' + [console_scripts] + pymupdf = pymupdf.__main__:main + '''), + + fn_build=build, + fn_clean=clean, + fn_sdist=sdist, + + py_limited_api=g_py_limited_api, + + # 30MB: 9 ZIP_DEFLATED + # 28MB: 9 ZIP_BZIP2 + # 23MB: 9 ZIP_LZMA + #wheel_compression = zipfile.ZIP_DEFLATED if (darwin or pyodide) else zipfile.ZIP_LZMA, + wheel_compresslevel = 9, + ) + +# Patch up macos platform tag - we require at least 10.15 because otherwise +# std::filesystem appears to be not available. +if pipcl.darwin(): + pt = p.tag_platform() + #pipcl.log(f'{pt=}') + m = re.match('^(macosx_)(([0-9]+)_([0-9]+))(.*)', pt) + #pipcl.log(f'{m=}') + if m: + v = int(m.group(3)), int(m.group(4)) + #pipcl.log(f'{v=}') + if v < (10, 15): + pt2 = f'{m.group(1)}10_15{m.group(5)}' + pipcl.log(f'Changing tag_platform from {pt!r} to {pt2!r}') + p.tag_platform_ = pt2 + +def get_requires_for_build_wheel(config_settings=None): + ''' + Adds to pyproject.toml:[build-system]:requires, allowing programmatic + control over what packages we require. + ''' + def platform_release_tuple(): + r = platform.release() + r = r.split('.') + r = tuple(int(i) for i in r) + log(f'platform_release_tuple() returning {r=}.') + return r + + ret = list() + libclang = os.environ.get('PYMUPDF_SETUP_LIBCLANG') + if libclang: + print(f'Overriding to use {libclang=}.') + ret.append(libclang) + elif pipcl.openbsd(): + print(f'OpenBSD: libclang not available via pip; assuming `pkg_add py3-llvm`.') + elif pipcl.darwin() and platform_release_tuple() < (18,): + # There are still of problems when building on old macos. + ret.append('libclang==14.0.6') + else: + ret.append('libclang') + if msys2(): + print(f'msys2: pip install of swig does not build; assuming `pacman -S swig`.') + elif pipcl.openbsd(): + print(f'OpenBSD: pip install of swig does not build; assuming `pkg_add swig`.') + elif PYMUPDF_SETUP_SWIG: + pass + elif pipcl.darwin() and pipcl.python_version_tuple() < (3, 13): + # Latest swig-4.4.1 gives director errors on macos with python<3.13. + ret.append('swig==4.3.1') + else: + ret.append('swig') + return ret if PYMUPDF_SETUP_URL_WHEEL: From 3656c7fa77e9f65749776a23f1f991785fcfe4ca Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Tue, 4 Aug 2026 12:10:24 +0100 Subject: [PATCH 3/9] tests/test_font.py:test_5049(): don't overwrite input file. --- tests/test_font.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_font.py b/tests/test_font.py index ccbe12d3e..0e9f4665e 100644 --- a/tests/test_font.py +++ b/tests/test_font.py @@ -393,5 +393,5 @@ def add_name(page: pymupdf.Page, name: str) -> None: exam.subset_fonts() path_out = os.path.normpath(f'{__file__}/../../tests/test_5049_out.pdf') - exam.save(path, garbage=4, deflate=True, clean=True) + exam.save(path_out, garbage=4, deflate=True, clean=True) exam.close() From 4576d245990b1c9219f5e787d40590896fafbbf5 Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Tue, 4 Aug 2026 22:44:35 +0100 Subject: [PATCH 4/9] setup.py: increment version to 1.28.2. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 21aad9681..af830b2b0 100755 --- a/setup.py +++ b/setup.py @@ -1099,9 +1099,9 @@ def sdist(): # PyMuPDF version. -version_p = '1.28.0' +version_p = '1.28.2' -version_mupdf = '1.28.0' +version_mupdf = '1.28.2' # A normal PyMuPDF package. From 27befe0d7603b85b073db3dcd5b6e1622199dc88 Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Tue, 4 Aug 2026 23:53:12 +0100 Subject: [PATCH 5/9] .github/ISSUE_TEMPLATE/bug_report.yml: added 1.28.2. --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index be916e2d7..8c5355e7e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -46,6 +46,7 @@ body: attributes: label: PyMuPDF version options: + - 1.28.2 - 1.28.0 - 1.27.2.3 - 1.27.2.2 From b88752c41cd27fa683aa56870791c840f75e2f5c Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Wed, 5 Aug 2026 09:55:16 +0100 Subject: [PATCH 6/9] tests/test_pynone.py: renamed test to test_5042() to match github issue. --- tests/test_pynone.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_pynone.py b/tests/test_pynone.py index 629d9ae3b..a76b95611 100644 --- a/tests/test_pynone.py +++ b/tests/test_pynone.py @@ -1,6 +1,6 @@ import sys, pymupdf -def test_none_refcount(): +def test_5042(): doc = pymupdf.open() page = doc.new_page() for i in range(10): @@ -10,4 +10,4 @@ def test_none_refcount(): for _ in range(200): page.get_texttrace() after = sys.getrefcount(None) - assert before == after, f"refcount of None changed: {before} -> {after}" \ No newline at end of file + assert before == after, f"refcount of None changed: {before} -> {after}" From 20a5b86c849b9e28da849d01406eb16d026ff967 Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Tue, 4 Aug 2026 23:53:26 +0100 Subject: [PATCH 7/9] changes.txt: updates for 1.28.2. --- changes.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/changes.txt b/changes.txt index 996c43f10..bc95eed84 100644 --- a/changes.txt +++ b/changes.txt @@ -2,24 +2,28 @@ Change Log ========== +**Changes in version 1.28.2** () + Fixed issues: * **Fixed** `4670 `_: scrub fails to remove hidden text after clean_contents stopped including line breaks (\u2265 1.24.0) * **Fixed** `4943 `_: enh: applying redactions with image cropping for currently unsupported colorspaces * **Fixed** `5030 `_: find_tables() with layout enabled can return a zero-cell Table, and Table.bbox then raises "ValueError: min() iterable argument is empty" +* **Fixed** `5042 `_: Page.get_texttrace() leaks None references \u2014 Fatal Python error (none_dealloc) in long-running processes (1.27.2.3 & 1.28.0) * **Fixed** `5044 `_: Outline (TOC) parsing bug * **Fixed** `5049 `_: font subsetting segfaults in 1.28.0, regression from 1.27.2.2 #5049 -* **Fixed** `5042 `_: Page.get_texttrace() leaks None references \u2014 Fatal Python error (none_dealloc) in long-running processes (1.27.2.3 & 1.28.0) Other: +* Use MuPDF-1.28.2. * Output warning when legacy `fitz` module is imported. * Cope better with markdown containing illegal utf8 sequences. * Fixed building with PYMUPDF_SETUP_MUPDF_VS_UPGRADE. * pymupdf.Page.find_tables(): * new args `use_layout: bool = True` `union: bool = False` `refine: bool = False`. * Improved speed. +* Retrospectively added fix for #4936 in release 1.28.0 below. **Changes in version 1.28.0** (2026-06-29) @@ -30,6 +34,7 @@ Fixed issues: * **Fixed** `4950 `_: remove_rotation() raises ValueError on widgets with empty/infinite rects * **Fixed** `5001 `_: Formulae incorrectly rendered as black boxes * **Fixed** `5033 `_: Annot.set_rotation(0) followed by Annot.update() throws AttributeError +* **Fixed** `4936 `_: bug: incomplete redaction of vector graphics (line art) Other: From 7b7ccdd8d39b5e43be0715b6e6166f48afc7807e Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Thu, 6 Aug 2026 05:49:02 +0100 Subject: [PATCH 8/9] src/__init__.py:JM_get_font(): cope with mupdf master fz_lookup_noto_font()'s new args. --- src/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/__init__.py b/src/__init__.py index 000c2a23c..f5d73e531 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -19981,7 +19981,10 @@ def fertig(font): # Check for NOTO font #have_noto:; - data, size, index = mupdf.fz_lookup_noto_font( script, lang) + if mupdf_version_tuple >= (1, 29): + data, size, index, attr, noto_index = mupdf.fz_lookup_noto_font(script, lang) + else: + data, size, index = mupdf.fz_lookup_noto_font(script, lang) font = None if data: font = mupdf.fz_new_font_from_memory( None, data, size, index, 0) From 5e30e5a9257377430e68c0f4a7b0f1c04fd83044 Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Thu, 6 Aug 2026 10:43:49 +0100 Subject: [PATCH 9/9] tests/test_general.py:test_5054(): expect success after recent fix in mupdf master. --- tests/test_general.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_general.py b/tests/test_general.py index efe461092..11af8f598 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -2267,8 +2267,7 @@ def test_5054(): text2 = page.get_text() print(f'{text1=}') print(f'{text2=}') - if 1: - # Currently fails for all mupdf versions. + if pymupdf.mupdf_version_tuple < (1, 29): assert text2 != text1 else: assert text2 == text1