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,7 +46,7 @@ body:
attributes:
label: PyMuPDF version
options:
-
- 1.26.0
- 1.25.5
- 1.25.4
- 1.25.3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build_wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
flavours:
description: 'If set, we build separate PyMuPDF and PyMuPDFb wheels.'
type: boolean
default: true
default: false

sdist:
type: boolean
Expand Down
7 changes: 7 additions & 0 deletions changes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@ Change Log
* Fixed issues:

* **Fixed** `4324 <https://github.com/pymupdf/PyMuPDF/issues/4324>`_: cluster_drawings() fails to cluster horizontal and vertical thin lines
* **Fixed** `4363 <https://github.com/pymupdf/PyMuPDF/issues/4363>`_: Trouble with searching
* **Fixed** `4404 <https://github.com/pymupdf/PyMuPDF/issues/4404>`_: IndexError in page.get_links()
* **Fixed** `4412 <https://github.com/pymupdf/PyMuPDF/issues/4412>`_: Regression? Spurious error? in insert_pdf in v1.25.4
* **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** `4435 <https://github.com/pymupdf/PyMuPDF/issues/4435>`_: get_pixmap method stuck on one page
* **Fixed** `4439 <https://github.com/pymupdf/PyMuPDF/issues/4439>`_: New Xml class from data does not work - bug in code
* **Fixed** `4445 <https://github.com/pymupdf/PyMuPDF/issues/4445>`_: Broken XREF table incorrectly repaired
* **Fixed** `4447 <https://github.com/pymupdf/PyMuPDF/issues/4447>`_: Stroke color of annotations cannot be correctly set
* **Fixed** `4479 <https://github.com/pymupdf/PyMuPDF/issues/4479>`_: set_layer_ui_config() toggles all layers rather than just one
* **Fixed** `4505 <https://github.com/pymupdf/PyMuPDF/issues/4505>`_: Follow Widget flag values up its parent structure

* Other:

* Partial fixed for `4457 <https://github.com/pymupdf/PyMuPDF/issues/4457>`_: Wrong characters displayed after font subsetting (w/ native method)
* Support image stamp annotations.
* Support recoloring pages.
* Added example of using Django's file storage API to open files with pymupdf.
Expand Down
2 changes: 1 addition & 1 deletion docs/version.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
----

This documentation covers **PyMuPDF v1.25.5** features as of **2025-03-31 00:00:01**.
This documentation covers **PyMuPDF v1.26.0** features as of **2025-03-31 00:00:01**.

The major and minor versions of |PyMuPDF| and |MuPDF| will always be the same. Only the third qualifier (patch level) may deviate from that of |MuPDF|.

Expand Down
2 changes: 1 addition & 1 deletion scripts/gh_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ def set_cibuild_test():
# Include MuPDF build-time files.
flavour += 'd'
env_set( 'PYMUPDF_SETUP_FLAVOUR', flavour, pass_=1)
run( f'cibuildwheel{platform_arg}', env_extra)
run( f'cibuildwheel{platform_arg}', env_extra=env_extra)
run( 'echo after {flavour=}')
run( 'ls -l wheelhouse')

Expand Down
4 changes: 2 additions & 2 deletions scripts/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
./PyMuPDF/scripts/test.py --mupdf 'git:https://git.ghostscript.com/mupdf.git' buildtest
Build and test with internal checkout of mupdf master.

./PyMuPDF/scripts/test.py --mupdf 'git:--branch 1.25.x https://github.com/ArtifexSoftware/mupdf.git' buildtest
Build and test using internal checkout of mupdf 1.25.x branch from Github.
./PyMuPDF/scripts/test.py --mupdf 'git:--branch 1.26.x https://github.com/ArtifexSoftware/mupdf.git' buildtest
Build and test using internal checkout of mupdf 1.26.x branch from Github.

Usage:
scripts/test.py <options> <command(s)>
Expand Down
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1229,15 +1229,15 @@ def sdist():
#

# PyMuPDF version.
version_p = '1.25.5'
version_p = '1.26.0'

# 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
# (2024-10-03) we no longer use PyMuPDFb wheels so this is actually unused.
#
version_b = '1.25.3'
version_b = '1.26.0'

version_mupdf = '1.25.6'
version_mupdf = '1.26.1'

if os.path.exists(f'{g_root}/{g_pymupdfb_sdist_marker}'):

Expand Down
4 changes: 2 additions & 2 deletions src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ def _int_rc(text):

# Basic version information.
#
pymupdf_version = "1.25.5"
pymupdf_version = "1.26.0"
mupdf_version = mupdf.FZ_VERSION
pymupdf_date = "2025-03-31 00:00:01"

Expand Down Expand Up @@ -5809,7 +5809,7 @@ def update_object(self, xref, text, page=None):
pdf = _as_pdf_document(self)
xreflen = mupdf.pdf_xref_len(pdf)
if not _INRANGE(xref, 1, xreflen-1):
RAISEPY("bad xref", MSG_BAD_XREF, PyExc_ValueError)
RAISEPY("bad xref", MSG_BAD_XREF)
ENSURE_OPERATION(pdf)
# create new object with passed-in string
new_obj = JM_pdf_obj_from_str(pdf, text)
Expand Down
Binary file added tests/resources/test_4423.pdf
Binary file not shown.
Binary file added tests/resources/test_4435.pdf
Binary file not shown.
Binary file added tests/resources/test_4479.pdf
Binary file not shown.
103 changes: 102 additions & 1 deletion tests/test_font.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
"""
Tests for the Font class.
"""
import pymupdf
import os
import platform
import pymupdf
import subprocess
import textwrap

import util


def test_font1():
text = "PyMuPDF"
Expand Down Expand Up @@ -218,3 +224,98 @@ def test_3887():
print(f'Have saved to: {path_pixmap=}')
assert set(output)==set(text)


def test_4457():
print()
files = (
('https://arxiv.org/pdf/2504.13180', 'test_4457_a.pdf', None, 4),
('https://arxiv.org/pdf/2504.13181', 'test_4457_b.pdf', None, 9),
)
for url, name, size, rms_after_max in files:
path = util.download(url, name, size)

with pymupdf.open(path) as document:
page = document[0]

pixmap = document[0].get_pixmap()
path_pixmap = f'{path}.png'
pixmap.save(path_pixmap)
print(f'Have created: {path_pixmap=}')

text = page.get_text()
path_before = f'{path}.before.pdf'
path_after = f'{path}.after.pdf'
document.ez_save(path_before, garbage=4)
print(f'Have created {path_before=}')

document.subset_fonts()
document.ez_save(path_after, garbage=4)
print(f'Have created {path_after=}')

with pymupdf.open(path_before) as document:
text_before = document[0].get_text()
pixmap_before = document[0].get_pixmap()
path_pixmap_before = f'{path_before}.png'
pixmap_before.save(path_pixmap_before)
print(f'Have created: {path_pixmap_before=}')

with pymupdf.open(path_after) as document:
text_after = document[0].get_text()
pixmap_after = document[0].get_pixmap()
path_pixmap_after = f'{path_after}.png'
pixmap_after.save(path_pixmap_after)
print(f'Have created: {path_pixmap_after=}')

import gentle_compare
rms_before = gentle_compare.pixmaps_rms(pixmap, pixmap_before)
rms_after = gentle_compare.pixmaps_rms(pixmap, pixmap_after)
print(f'{rms_before=}')
print(f'{rms_after=}')

# Create .png file showing differences between <path> and <path_after>.
path_pixmap_after_diff = f'{path_after}.diff.png'
pixmap_after_diff = gentle_compare.pixmaps_diff(pixmap, pixmap_after)
pixmap_after_diff.save(path_pixmap_after_diff)
print(f'Have created: {path_pixmap_after_diff}')

# Extract text from <path>, <path_before> and <path_after> and write to
# files so we can show differences with `diff`.
path_text = os.path.normpath(f'{__file__}/../../tests/test_4457.txt')
path_text_before = f'{path_text}.before.txt'
path_text_after = f'{path_text}.after.txt'
with open(path_text, 'w', encoding='utf8') as f:
f.write(text)
with open(path_text_before, 'w', encoding='utf8') as f:
f.write(text_before)
with open(path_text_after, 'w', encoding='utf8') as f:
f.write(text_after)

# Can't write text to stdout on Windows because of encoding errors.
if platform.system() != 'Windows':
print(f'text:\n{textwrap.indent(text, " ")}')
print(f'text_before:\n{textwrap.indent(text_before, " ")}')
print(f'text_after:\n{textwrap.indent(text_after, " ")}')
print(f'{path_text=}')
print(f'{path_text_before=}')
print(f'{path_text_after=}')

command = f'diff -u {path_text} {path_text_before}'
print(f'Running: {command}', flush=1)
subprocess.run(command, shell=1)

command = f'diff -u {path_text} {path_text_after}'
print(f'Running: {command}', flush=1)
subprocess.run(command, shell=1)

assert text_before == text
assert rms_before == 0

# As of 2025-05-20 there are some differences in some characters, e.g.
# the non-ascii characters in `Philipp Krahenbuhl`.
# See <path_pixmap> and <path_pixmap_after>.
assert rms_after < rms_after_max

# Avoid test failure caused by mupdf warnings.
wt = pymupdf.TOOLS.mupdf_warnings()
print(f'{wt=}')
assert wt == 'bogus font ascent/descent values (0 / 0)\n... repeated 5 times...'
40 changes: 40 additions & 0 deletions tests/test_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -1834,3 +1834,43 @@ def test_4466():
pixmap = page.get_pixmap(clip=(0, 0, 10, 10))
print(f'{pixmap.n=} {pixmap.size=} {pixmap.stride=} {pixmap.width=} {pixmap.height=} {pixmap.x=} {pixmap.y=}', flush=1)
pixmap.is_unicolor # Used to crash.


def test_4479():
# This passes with pymupdf-1.24.14, fails with pymupdf==1.25.*, passes with
# pymupdf-1.26.0.
print()
path = os.path.normpath(f'{__file__}/../../tests/resources/test_4479.pdf')
with pymupdf.open(path) as document:

def show(items):
for item in items:
print(f' {repr(item)}')

items = document.layer_ui_configs()
show(items)
assert items == [
{'depth': 0, 'locked': 0, 'number': 0, 'on': 1, 'text': 'layer_0', 'type': 'checkbox'},
{'depth': 0, 'locked': 0, 'number': 1, 'on': 1, 'text': 'layer_1', 'type': 'checkbox'},
{'depth': 0, 'locked': 0, 'number': 2, 'on': 0, 'text': 'layer_2', 'type': 'checkbox'},
{'depth': 0, 'locked': 0, 'number': 3, 'on': 1, 'text': 'layer_3', 'type': 'checkbox'},
{'depth': 0, 'locked': 0, 'number': 4, 'on': 1, 'text': 'layer_4', 'type': 'checkbox'},
{'depth': 0, 'locked': 0, 'number': 5, 'on': 1, 'text': 'layer_5', 'type': 'checkbox'},
{'depth': 0, 'locked': 0, 'number': 6, 'on': 1, 'text': 'layer_6', 'type': 'checkbox'},
{'depth': 0, 'locked': 0, 'number': 7, 'on': 1, 'text': 'layer_7', 'type': 'checkbox'},
]

document.set_layer_ui_config(0, pymupdf.PDF_OC_OFF)
items = document.layer_ui_configs()
show(items)
assert items == [
{'depth': 0, 'locked': 0, 'number': 0, 'on': 0, 'text': 'layer_0', 'type': 'checkbox'},
{'depth': 0, 'locked': 0, 'number': 1, 'on': 1, 'text': 'layer_1', 'type': 'checkbox'},
{'depth': 0, 'locked': 0, 'number': 2, 'on': 0, 'text': 'layer_2', 'type': 'checkbox'},
{'depth': 0, 'locked': 0, 'number': 3, 'on': 1, 'text': 'layer_3', 'type': 'checkbox'},
{'depth': 0, 'locked': 0, 'number': 4, 'on': 1, 'text': 'layer_4', 'type': 'checkbox'},
{'depth': 0, 'locked': 0, 'number': 5, 'on': 1, 'text': 'layer_5', 'type': 'checkbox'},
{'depth': 0, 'locked': 0, 'number': 6, 'on': 1, 'text': 'layer_6', 'type': 'checkbox'},
{'depth': 0, 'locked': 0, 'number': 7, 'on': 1, 'text': 'layer_7', 'type': 'checkbox'},
]

69 changes: 69 additions & 0 deletions tests/test_pixmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,3 +510,72 @@ def test_4336():

if cc_old:
assert cc == cc_old


def test_4435():
print(f'{pymupdf.version=}')
path = os.path.normpath(f'{__file__}/../../tests/resources/test_4435.pdf')
with pymupdf.open(path) as document:
page = document[2]
print(f'Calling page.get_pixmap().', flush=1)
pixmap = page.get_pixmap(alpha=False, dpi=120)
print(f'Called page.get_pixmap().', flush=1)
wt = pymupdf.TOOLS.mupdf_warnings()
assert wt == 'bogus font ascent/descent values (0 / 0)\n... repeated 9 times...'


def test_4423():
path = os.path.normpath(f'{__file__}/../../tests/resources/test_4423.pdf')
with pymupdf.open(path) as document:
path2 = f'{path}.pdf'
ee = None
try:
document.save(
path2,
garbage=4,
expand=1,
deflate=True,
pretty=True,
no_new_id=True,
)
except Exception as e:
print(f'Exception: {e}')
ee = e

if (1, 25, 5) <= pymupdf.mupdf_version_tuple < (1, 26):
assert ee, f'Did not receive the expected exception.'
wt = pymupdf.TOOLS.mupdf_warnings()
assert wt == 'dropping unclosed output'
else:
assert not ee, f'Received unexpected exception: {e}'
wt = pymupdf.TOOLS.mupdf_warnings()
assert wt == 'format error: cannot find object in xref (56 0 R)\nformat error: cannot find object in xref (68 0 R)'


def test_4445():
print()
# Test case is large so we download it instead of having it in PyMuPDF
# git. We put it in `cache/` directory do it is not removed by `git clean`
# (unless `-d` is specified).
import util
path = util.download(
'https://github.com/user-attachments/files/19738242/ss.pdf',
'test_4445.pdf',
size=2671185,
)
with pymupdf.open(path) as document:
page = document[0]
pixmap = page.get_pixmap()
print(f'{pixmap.width=}')
print(f'{pixmap.height=}')
if pymupdf.mupdf_version_tuple >= (1, 26):
assert (pixmap.width, pixmap.height) == (792, 612)
else:
assert (pixmap.width, pixmap.height) == (612, 792)
if 0:
path_pixmap = f'{path}.png'
pixmap.save(path_pixmap)
print(f'Have created {path_pixmap=}')
wt = pymupdf.TOOLS.mupdf_warnings()
print(f'{wt=}')
assert wt == 'broken xref subsection, proceeding anyway.\nTrailer Size is off-by-one. Ignoring.'
28 changes: 28 additions & 0 deletions tests/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import os
import subprocess


def download(url, name, size=None):
'''
Downloads from <url> to a local file and returns its path.

If file already exists and matches <size> we do not re-download it.

We put local files within a `cache/` directory so that it is not deleted by
`git clean` (unless `-d` is specified).
'''
path = os.path.normpath(f'{__file__}/../../tests/cache/{name}')
if os.path.isfile(path) and (not size or os.stat(path).st_size == size):
print(f'Using existing file {path=}.')
else:
print(f'Downloading from {url=}.')
subprocess.run(f'pip install -U requests', check=1, shell=1)
import requests
r = requests.get(url, path, timeout=10)
r.raise_for_status()
if size is not None:
assert len(r.content) == size
os.makedirs(os.path.dirname(path), exist_ok=1)
with open(path, 'wb') as f:
f.write(r.content)
return path