From 7d535bd565040cb5cc027c3abf2ab2ad0f0181fa Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Thu, 31 Oct 2024 11:57:25 +0000 Subject: [PATCH 1/4] pipcl.py: run(): added arg. --- pipcl.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pipcl.py b/pipcl.py index 051d31731..53e5d8aa8 100644 --- a/pipcl.py +++ b/pipcl.py @@ -1899,7 +1899,16 @@ def git_items( directory, submodules=False): return ret -def run( command, capture=False, check=1, verbose=1, env_extra=None, caller=1): +def run( + command, + *, + capture=False, + check=1, + verbose=1, + env_extra=None, + timeout=None, + caller=1, + ): ''' Runs a command using `subprocess.run()`. @@ -1923,6 +1932,10 @@ def run( command, capture=False, check=1, verbose=1, env_extra=None, caller=1): If true we show the command. env_extra: None or dict to add to environ. + timeout: + If not None, timeout in seconds; passed directly to + subprocess.run(). Note that on MacOS subprocess.run() seems to + leave processes running if timeout expires. Returns: check capture Return -------------------------- @@ -1949,6 +1962,7 @@ def run( command, capture=False, check=1, verbose=1, env_extra=None, caller=1): check=check, encoding='utf8', env=env, + timeout=timeout, ) if check: return cp.stdout if capture else None From f06f1c7549d508d63eacff947978bece188721ed Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Thu, 31 Oct 2024 11:58:00 +0000 Subject: [PATCH 2/4] scripts/: use pipcl.py's run() and log() functions. Avoids code duplication, and shows file:line in diagnostics. --- scripts/gh_release.py | 51 ++++++------------------------------- scripts/sysinstall.py | 59 ++++++++++++++++++++++--------------------- scripts/test.py | 35 +++++++++++++------------ 3 files changed, 57 insertions(+), 88 deletions(-) diff --git a/scripts/gh_release.py b/scripts/gh_release.py index b9453cd5b..b834df4c0 100755 --- a/scripts/gh_release.py +++ b/scripts/gh_release.py @@ -89,6 +89,14 @@ pymupdf_dir = os.path.abspath( f'{__file__}/../..') +sys.path.insert(0, pymupdf_dir) +import pipcl +del sys.path[0] + +log = pipcl.log0 +run = pipcl.run + + def main(): log( '### main():') @@ -586,49 +594,6 @@ def relpath(path, start=None): return os.path.relpath(path, start) -def log(text, caller=0): - ''' - Writes `text` to stdout with prefix showing caller path relative to - pymupdf_dir and fn name. - ''' - frame_record = inspect.stack( context=0)[ caller+1] - filename = frame_record.filename - line = frame_record.lineno - function = frame_record.function - prefix = f'{relpath(filename, pymupdf_dir)}:{line}:{function}(): ' - print(textwrap.indent(text, prefix), flush=1) - - -def run(command, env_extra=None, check=1, timeout=None): - ''' - Runs a command using subprocess.run(). - Args: - command: - The command to run. - env_extra: - None or dict containing extra environment variable settings to add - to os.environ. - check: - Whether to raise exception if command fails. - timeout: - If not None, timeout in seconds; passed directory to - subprocess.run(). Note that on MacOS subprocess.run() seems to - leave processes running if timeout expires. - ''' - env = None - message = 'Running: ' - if env_extra: - env = os.environ.copy() - env.update(env_extra) - message += '\n[environment:\n' - for n, v in env_extra.items(): - message += f' {n}={shlex.quote(v)}\n' - message += ']\n' - message += f'{command}' - log(message, caller=1) - return subprocess.run(command, check=check, shell=1, env=env, timeout=timeout) - - def platform_tag(): bits = cpu_bits() if platform.system() == 'Windows': diff --git a/scripts/sysinstall.py b/scripts/sysinstall.py index 0c2a7f697..65132b6e1 100755 --- a/scripts/sysinstall.py +++ b/scripts/sysinstall.py @@ -86,6 +86,14 @@ import test as test_py +pymupdf_dir = os.path.abspath( f'{__file__}/../..') + +sys.path.insert(0, pymupdf_dir) +import pipcl +del sys.path[0] + +log = pipcl.log0 + # Requirements for a system build and install: # # system packages (Debian names): @@ -106,12 +114,12 @@ def main(): if 1: - print(f'## {__file__}: Starting.') - print(f'{sys.executable=}') - print(f'{platform.python_version()=}') - print(f'{__file__=}') - print(f'{sys.argv=}') - print(f'{sysconfig.get_path("platlib")=}') + log(f'## {__file__}: Starting.') + log(f'{sys.executable=}') + log(f'{platform.python_version()=}') + log(f'{__file__=}') + log(f'{sys.argv=}') + log(f'{sysconfig.get_path("platlib")=}') run_command(f'python -V', check=0) run_command(f'python3 -V', check=0) run_command(f'sudo python -V', check=0) @@ -132,7 +140,6 @@ def main(): packages = True prefix = '/usr/local' pymupdf_do = True - pymupdf_dir = os.path.abspath( f'{__file__}/../..') root = 'sysinstall_test' tesseract5 = True pytest_args = None @@ -152,7 +159,7 @@ def main(): except StopIteration: break if arg in ('-h', '--help'): - print(__doc__) + log(__doc__) return elif arg == '--mupdf-do': mupdf_do = int(next(args)) elif arg == '--mupdf-dir': mupdf_dir = next(args) @@ -161,7 +168,6 @@ def main(): elif arg == '--packages': packages = int(next(args)) elif arg == '--prefix': prefix = next(args) elif arg == '--pymupdf-do': pymupdf_do = int(next(args)) - elif arg == '--pymupdf-dir': pymupdf_dir = next(args) elif arg == '--root': root = next(args) elif arg == '--tesseract5': tesseract5 = int(next(args)) elif arg == '--pytest-do': pytest_do = int(next(args)) @@ -191,23 +197,23 @@ def run(command): if mupdf_git: # Update existing checkout or do `git clone`. if os.path.exists(mupdf_dir): - print(f'## Update MuPDF checkout {mupdf_dir}.') + log(f'## Update MuPDF checkout {mupdf_dir}.') run(f'cd {mupdf_dir} && git pull && git submodule update --init') else: # No existing git checkout, so do a fresh clone. - print(f'## Clone MuPDF into {mupdf_dir}.') + log(f'## Clone MuPDF into {mupdf_dir}.') run(f'git clone --recursive --depth 1 --shallow-submodules {mupdf_git} {mupdf_dir}') if packages: # Install required system packages. We assume a Debian package system. # - print('## Install system packages required by MuPDF.') + log('## Install system packages required by MuPDF.') run(f'sudo apt update') run(f'sudo apt install {" ".join(g_sys_packages)}') # Ubuntu-22.04 has freeglut3-dev, not libglut-dev. run(f'sudo apt install libglut-dev | sudo apt install freeglut3-dev') if tesseract5: - print(f'## Force installation of libtesseract-dev version 5.') + log(f'## Force installation of libtesseract-dev version 5.') # https://stackoverflow.com/questions/76834972/how-can-i-run-pytesseract-python-library-in-ubuntu-22-04 # run('sudo apt install -y software-properties-common') @@ -220,12 +226,12 @@ def run(command): # Build+install MuPDF. We use mupd:Makefile's install-shared-python target. # if pip == 'sudo': - print('## Installing Python packages required for building MuPDF and PyMuPDF.') + log('## Installing Python packages required for building MuPDF and PyMuPDF.') run(f'sudo pip install --upgrade pip') names = test_py.wrap_get_requires_for_build_wheel(f'{__file__}/../..') run(f'sudo pip install {names}') - print('## Build and install MuPDF.') + log('## Build and install MuPDF.') command = f'cd {mupdf_dir}' command += f' && {sudo}make' command += f' -j {multiprocessing.cpu_count()}' @@ -246,10 +252,10 @@ def run(command): # Build+install PyMuPDF. # - print('## Build and install PyMuPDF.') + log('## Build and install PyMuPDF.') def run(command): return run_command(command, doit=pymupdf_do) - flags_freetype2 = run_command('pkg-config --cflags freetype2', capture_output=1).stdout.strip() + flags_freetype2 = run_command('pkg-config --cflags freetype2', capture=1) compile_flags = f'-I {root_prefix}/include {flags_freetype2}' link_flags = f'-L {root_prefix}/lib' env = '' @@ -258,7 +264,7 @@ def run(command): env += f'LDFLAGS="-L {root}/{prefix}/lib" ' env += f'PYMUPDF_SETUP_MUPDF_BUILD= ' # Use system MuPDF. if use_installer: - print(f'## Building wheel.') + log(f'## Building wheel.') if pip == 'venv': venv_name = 'venv-pymupdf-sysinstall' run(f'pwd') @@ -277,7 +283,7 @@ def run(command): wheel = glob.glob(f'dist/*') assert len(wheel) == 1, f'{wheel=}' wheel = wheel[0] - print(f'## Installing wheel using `installer`.') + log(f'## Installing wheel using `installer`.') pv = '.'.join(platform.python_version_tuple()[:2]) p = f'{root_prefix}/lib/python{pv}' # `python -m installer` fails to overwrite existing files. @@ -315,13 +321,10 @@ def run(command): if leaf in dirnames: pythonpath.append(os.path.join(dirpath, leaf)) pythonpath = ':'.join(pythonpath) - print(f'{pythonpath=}') + log(f'{pythonpath=}') else: command = f'{env} pip install -vv --root {root} {os.path.abspath(pymupdf_dir)}' run( command) - sys.path.insert(0, pymupdf_dir) - import pipcl - del sys.path[0] pythonpath = pipcl.install_dir(root) # Show contents of installation directory. This is very slow on github, @@ -330,7 +333,7 @@ def run(command): # Run pytest tests. # - print('## Run PyMuPDF pytest tests.') + log('## Run PyMuPDF pytest tests.') def run(command): return run_command(command, doit=pytest_do) import gh_release @@ -387,13 +390,11 @@ def run(command): run(command) -def run_command(command, capture_output=False, check=True, doit=True): +def run_command(command, capture=False, check=True, doit=True): if doit: - print(f'## Running: {command}') - sys.stdout.flush() - return subprocess.run(command, shell=1, check=check, text=1, capture_output=capture_output) + return pipcl.run(command, capture=capture, check=check, caller=2) else: - print(f'## Would have run: {command}') + log(f'## Would have run: {command}', caller=2) if __name__ == '__main__': diff --git a/scripts/test.py b/scripts/test.py index 04c0196cd..b08f74b60 100755 --- a/scripts/test.py +++ b/scripts/test.py @@ -135,6 +135,13 @@ pymupdf_dir = os.path.abspath( f'{__file__}/../..') +sys.path.insert(0, pymupdf_dir) +import pipcl +del sys.path[0] + +log = pipcl.log0 +run = pipcl.run + def main(argv): @@ -402,7 +409,7 @@ def build( if venv_quick: log(f'{venv_quick=}: Not installing packages with pip: {names}') else: - gh_release.run( f'python -m pip install --upgrade {names}') + run( f'python -m pip install --upgrade {names}') build_isolation_text = ' --no-build-isolation' env_extra = dict() @@ -413,9 +420,9 @@ def build( if build_flavour: env_extra['PYMUPDF_SETUP_FLAVOUR'] = build_flavour if wheel: - gh_release.run(f'pip wheel{build_isolation_text} -v {pymupdf_dir}', env_extra=env_extra) + run(f'pip wheel{build_isolation_text} -v {pymupdf_dir}', env_extra=env_extra) else: - gh_release.run(f'pip install{build_isolation_text} -v {pymupdf_dir}', env_extra=env_extra) + run(f'pip install{build_isolation_text} -v {pymupdf_dir}', env_extra=env_extra) def build_pyodide_wheel(pyodide_build_version=None): @@ -452,14 +459,14 @@ def build_pyodide_wheel(pyodide_build_version=None): env_extra['PYMUPDF_SETUP_MUPDF_TESSERACT'] = '0' setup = pyodide_setup(pymupdf_dir, pyodide_build_version=pyodide_build_version) command = f'{setup} && pyodide build --exports pyinit' - gh_release.run(command, env_extra=env_extra) + run(command, env_extra=env_extra) # Copy wheel into `wheelhouse/` so it is picked up as a workflow # artifact. # - gh_release.run(f'ls -l {pymupdf_dir}/dist/') - gh_release.run(f'mkdir -p {pymupdf_dir}/wheelhouse && cp -p {pymupdf_dir}/dist/* {pymupdf_dir}/wheelhouse/') - gh_release.run(f'ls -l {pymupdf_dir}/wheelhouse/') + run(f'ls -l {pymupdf_dir}/dist/') + run(f'mkdir -p {pymupdf_dir}/wheelhouse && cp -p {pymupdf_dir}/dist/* {pymupdf_dir}/wheelhouse/') + run(f'ls -l {pymupdf_dir}/wheelhouse/') def pyodide_setup( @@ -655,7 +662,7 @@ def test( if venv_quick: log(f'{venv_quick=}: Not installing test packages: {gh_release.test_packages}') else: - gh_release.run(f'pip install --upgrade {gh_release.test_packages}') + run(f'pip install --upgrade {gh_release.test_packages}') run_compound_args = '' if implementations: run_compound_args += f' -i {implementations}' @@ -664,9 +671,9 @@ def test( env_extra = None if valgrind: log('Installing valgrind.') - gh_release.run(f'sudo apt update') - gh_release.run(f'sudo apt install --upgrade valgrind') - gh_release.run(f'valgrind --version') + run(f'sudo apt update') + run(f'sudo apt install --upgrade valgrind') + run(f'valgrind --version') log('Running PyMuPDF tests under valgrind.') command = ( @@ -714,7 +721,7 @@ def test( f.write(text2) log(f'Running tests with tests/run_compound.py and pytest.') - gh_release.run(command, env_extra=env_extra, timeout=timeout) + run(command, env_extra=env_extra, timeout=timeout) except subprocess.TimeoutExpired as e: log(f'Timeout when running tests.') @@ -768,10 +775,6 @@ def wrap_get_requires_for_build_wheel(dir_): return ' '.join(ret) -def log(text): - gh_release.log(text, caller=1) - - if __name__ == '__main__': try: sys.exit(main(sys.argv)) From fc803921ba8ffdada4c4b2dcc450a35d4386f7f2 Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Thu, 31 Oct 2024 23:26:17 +0000 Subject: [PATCH 3/4] tests/test_story.py: added test for #3813. This is addressed in mupdf master. --- tests/test_story.py | 65 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/test_story.py b/tests/test_story.py index 48643be25..5eb2628db 100644 --- a/tests/test_story.py +++ b/tests/test_story.py @@ -227,3 +227,68 @@ def contentfn(positions): def test_archive_creation(): s = pymupdf.Story(archive=pymupdf.Archive('.')) s = pymupdf.Story(archive='.') + + +def test_3813(): + import pymupdf + + HTML = """ +

Count is fine:

+
    +
  1. Lorem +
      +
    1. Sub Lorem
    2. +
    3. Sub Lorem
    4. +
    +
  2. +
  3. Lorem
  4. +
  5. Lorem
  6. +
+ +

Broken count:

+
    +
  1. Lorem +
      +
    • Sub Lorem
    • +
    • Sub Lorem
    • +
    +
  2. +
  3. Lorem
  4. +
  5. Lorem
  6. +
+ """ + MEDIABOX = pymupdf.paper_rect("A4") + WHERE = MEDIABOX + (36, 36, -36, -36) + + story = pymupdf.Story(html=HTML) + path = os.path.normpath(f'{__file__}/../../tests/test_3813_out.pdf') + writer = pymupdf.DocumentWriter(path) + + more = 1 + + while more: + device = writer.begin_page(MEDIABOX) + more, _ = story.place(WHERE) + story.draw(device) + writer.end_page() + + writer.close() + + with pymupdf.open(path) as document: + page = document[0] + text = page.get_text() + text_utf8 = text.encode() + + if pymupdf.mupdf_version_tuple < (1, 25): + # MuPDF gets things wrong. + text_expected_utf8 = b'Count is \xef\xac\x81ne:\n1. Lorem\n1. Sub Lorem\n2. Sub Lorem\n2. Lorem\n3. Lorem\nBroken count:\n1. Lorem\n\xe2\x80\xa2 Sub Lorem\n\xe2\x80\xa2 Sub Lorem\n4. Lorem\n5. Lorem\n' + else: + text_expected_utf8 = b'Count is \xef\xac\x81ne:\n1. Lorem\n1. Sub Lorem\n2. Sub Lorem\n2. Lorem\n3. Lorem\nBroken count:\n1. Lorem\n\xe2\x80\xa2 Sub Lorem\n\xe2\x80\xa2 Sub Lorem\n2. Lorem\n3. Lorem\n' + text_expected = text_expected_utf8.decode() + + print(f'text_utf8:\n {text_utf8!r}') + print(f'text_expected_utf8:\n {text_expected_utf8!r}') + print(f'text:\n {textwrap.indent(text, " ")}') + print(f'text_expected:\n {textwrap.indent(text_expected, " ")}') + + assert text == text_expected From 35be90eb7fe3cddc7aedd06a1bdb741f0bbb1288 Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Fri, 1 Nov 2024 11:03:07 +0000 Subject: [PATCH 4/4] tests/: added test_3448(). Checks issue #3448. --- tests/resources/test_3448.pdf | Bin 0 -> 47464 bytes tests/resources/test_3448.pdf-expected.png | Bin 0 -> 18457 bytes tests/test_pixmap.py | 19 +++++++++++++++++++ 3 files changed, 19 insertions(+) create mode 100644 tests/resources/test_3448.pdf create mode 100644 tests/resources/test_3448.pdf-expected.png diff --git a/tests/resources/test_3448.pdf b/tests/resources/test_3448.pdf new file mode 100644 index 0000000000000000000000000000000000000000..dae9e9c1a66cdbb172d5b5a86ba54a15c201aa0f GIT binary patch literal 47464 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