Skip to content

Commit f877d19

Browse files
src/ tests/ docs/: avoid crash if samples memoryview is used after Pixmap destruction.
This addresses #4155. src/__init__.py: With .samples_mv, remember the memoryview and call its .release() method in Pixmap.__del__(). docs/pixmap.rst: Document improved behaviour of Pixmap.samples_mv. Also warn that Pixmap.samples_ptr is unsafe after destruction of the pixmap. tests/test_pixmap.py: Added test_4155(), check we get ValueError when accessing memoryview after Pixmap is destroyed.
1 parent 7aeb3ff commit f877d19

File tree

3 files changed

+38
-1
lines changed

3 files changed

+38
-1
lines changed

docs/pixmap.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,9 @@ Have a look at the :ref:`FAQ` section to see some pixmap usage "at work".
546546
367 ns ± 1.75 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
547547
In [4]: %timeit len(pix.samples)
548548
3.52 ms ± 57.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
549+
550+
After the Pixmap has been destroyed, any attempt to use the memoryview
551+
will fail with ValueError.
549552

550553
:type: memoryview
551554

@@ -559,6 +562,9 @@ Have a look at the :ref:`FAQ` section to see some pixmap usage "at work".
559562
img = QtGui.QImage(pix.samples_ptr, pix.width, pix.height, format) # (2)
560563

561564
Both of the above lead to the same Qt image, but (2) can be **many hundred times faster**, because it avoids an additional copy of the pixel area.
565+
566+
Warning: after the Pixmap has been destroyed, the Python pointer will be
567+
invalid and attempting to use it may crash the Python interpreter.
562568

563569
:type: int
564570

src/__init__.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10059,6 +10059,9 @@ def __init__(self, *args):
1005910059
# data. Doesn't seem to make much difference to Pixmap.set_pixel() so
1006010060
# not currently used.
1006110061
self._memory_view = None
10062+
10063+
# Cache for property `self.samples_mv`.
10064+
self._samples_mv = None
1006210065

1006310066
def __len__(self):
1006410067
return self.size
@@ -10339,7 +10342,13 @@ def samples_mv(self):
1033910342
'''
1034010343
Pixmap samples memoryview.
1034110344
'''
10342-
return mupdf.fz_pixmap_samples_memoryview(self.this)
10345+
# We remember the returned memoryview so that our `__del__()` can
10346+
# release it; otherwise accessing it after we have been destructed will
10347+
# fail, possibly crashing Python; this is #4155.
10348+
#
10349+
if self._samples_mv is None:
10350+
self._samples_mv = mupdf.fz_pixmap_samples_memoryview(self.this)
10351+
return self._samples_mv
1034310352

1034410353
@property
1034510354
def samples_ptr(self):
@@ -10625,6 +10634,10 @@ def yres(self):
1062510634

1062610635
width = w
1062710636
height = h
10637+
10638+
def __del__(self):
10639+
if self._samples_mv:
10640+
self._samples_mv.release()
1062810641

1062910642

1063010643
del Point

tests/test_pixmap.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,3 +428,21 @@ def test_3854():
428428
assert rms < 1
429429
else:
430430
assert rms == 0
431+
432+
433+
def test_4155():
434+
path = os.path.normpath(f'{__file__}/../../tests/resources/test_3854.pdf')
435+
with pymupdf.open(path) as document:
436+
page = document[0]
437+
pixmap = page.get_pixmap()
438+
mv = pixmap.samples_mv
439+
mvb1 = mv.tobytes()
440+
del page
441+
del pixmap
442+
try:
443+
mvb2 = mv.tobytes()
444+
except ValueError as e:
445+
print(f'Received exception: {e}')
446+
assert 'operation forbidden on released memoryview object' in str(e)
447+
else:
448+
assert 0, f'Did not receive expected exception when using defunct memoryview.'

0 commit comments

Comments
 (0)