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 changes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Change Log
==========


**Changes in version 1.28.0**
**Changes in version 1.28.0** (2026-06-29)

Fixed issues:

Expand Down
7 changes: 2 additions & 5 deletions src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4967,11 +4967,8 @@ def get_ocgs(self):
o = mupdf.pdf_array_get( intent, j)
if mupdf.pdf_is_name( o):
intents.append( mupdf.pdf_to_name( o))
if mupdf_version_tuple >= (1, 26, 11):
resource_stack = mupdf.PdfResourceStack()
hidden = mupdf.pdf_is_ocg_hidden( pdf, resource_stack, usage, ocg)
else:
hidden = mupdf.pdf_is_ocg_hidden( pdf, mupdf.PdfObj(), usage, ocg)
resource_stack = mupdf.PdfResourceStack()
hidden = mupdf.pdf_is_ocg_hidden( pdf, resource_stack, usage, ocg)
item = {
"name": name,
"intent": intents,
Expand Down
2 changes: 2 additions & 0 deletions src/fitz___init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@
from pymupdf import __doc__
from pymupdf import _globals
from pymupdf import _g_out_message

message_warning('The `fitz` API is deprecated and will be removed in future. Use `import pymupdf` instead.')
22 changes: 22 additions & 0 deletions tests/resources/test_markdown_bad_unicode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Title

## A Table

|**Boiling Points °C**|**min**|**max**|**avg**|
|---|---|---|---|
|**Noble gases**|-269|-62|-170.5|
|**Nonmetals**|-253|4827|414.1|
|**Metalloids**|335|3900|741.5|
|**Metals**|357|>5000|2755.9|

## A List

* Comment 1
* Comment 2
* Comment 3 with a link to [Find out more](https://pymupdf.readthedocs.io)

## My TO-DOs

* [x] Done!
* [x] Also done!
* [ ] Still open
9 changes: 1 addition & 8 deletions tests/test_font.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,14 +323,7 @@ def test_4457():

assert text_before == text
assert rms_before == 0

if pymupdf.mupdf_version_tuple >= (1, 26, 6):
assert rms_after == 0
else:
# 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 abs(rms_after - rms_old_after_max) < 2
assert rms_after == 0

# Avoid test failure caused by mupdf warnings.
wt = pymupdf.TOOLS.mupdf_warnings()
Expand Down
24 changes: 2 additions & 22 deletions tests/test_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ def test_iswrapped():
doc = pymupdf.open(filename)
page = doc[0]
assert page.is_wrapped
if (1, 26, 0) <= pymupdf.mupdf_version_tuple < (1, 27):
assert pymupdf.TOOLS.mupdf_warnings() == 'bogus font ascent/descent values (0 / 0)'


def test_wrapcontents():
Expand All @@ -87,10 +85,7 @@ def test_wrapcontents():
assert len(page.get_contents()) == 1
page.clean_contents()
wt = pymupdf.TOOLS.mupdf_warnings()
if (1, 26, 0) <= pymupdf.mupdf_version_tuple < (1, 27):
assert wt == 'bogus font ascent/descent values (0 / 0)\nPDF stream Length incorrect'
else:
assert wt == 'PDF stream Length incorrect'
assert wt == 'PDF stream Length incorrect'


def test_page_clean_contents():
Expand Down Expand Up @@ -251,8 +246,6 @@ def test_get_text_dict():
blocks=page.get_text("dict")["blocks"]
# Check no opaque types in `blocks`.
json.dumps( blocks, indent=4)
if (1, 26, 0) <= pymupdf.mupdf_version_tuple < (1, 27):
assert pymupdf.TOOLS.mupdf_warnings() == 'bogus font ascent/descent values (0 / 0)'

def test_font():
font = pymupdf.Font()
Expand Down Expand Up @@ -608,9 +601,6 @@ def test_2596():
page = doc.reload_page(page)
pix1 = page.get_pixmap()
assert pix1.samples == pix0.samples
if pymupdf.mupdf_version_tuple < (1, 26, 6):
wt = pymupdf.TOOLS.mupdf_warnings()
assert wt == 'too many indirections (possible indirection cycle involving 24 0 R)'


def test_2730():
Expand Down Expand Up @@ -1966,10 +1956,7 @@ def test_4533():
cp = subprocess.run(command, shell=1, check=0)
e = cp.returncode
print(f'{e=}')
if pymupdf.mupdf_version_tuple >= (1, 26, 6):
assert e == 0
else:
assert e != 0
assert e == 0


def test_4564():
Expand Down Expand Up @@ -2147,9 +2134,6 @@ def test_4712():
'''
Crash with "corrupted double-linked list
'''
if pymupdf.mupdf_version_tuple < (1, 26, 11):
print(f'test_4712m(): Not running because known to fail on mupdf < 1.26.11: {pymupdf.mupdf_version=}.')
return
path_a = os.path.normpath(f'{__file__}/../../tests/resources/test_4712_a.pdf')
path_b = os.path.normpath(f'{__file__}/../../tests/resources/test_4712_b.pdf')
doc1 = pymupdf.open(path_a)
Expand All @@ -2161,10 +2145,6 @@ def test_4712():


def test_4712m():
if pymupdf.mupdf_version_tuple < (1, 26, 11):
print(f'test_4712m(): Not running because known to fail on mupdf < 1.26.11: {pymupdf.mupdf_version=}.')
return

path_a = os.path.normpath(f'{__file__}/../../tests/resources/test_4712_a.pdf')
path_b = os.path.normpath(f'{__file__}/../../tests/resources/test_4712_b.pdf')

Expand Down
7 changes: 1 addition & 6 deletions tests/test_insertpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,12 +326,7 @@ def test_4571():
print(f'Have saved to: {path_out=}')
with open(path_out, 'rb') as f:
content = f.read()
if pymupdf.mupdf_version_tuple >= (1, 26, 6):
# Correct.
assert b'<</Type/Pages/Count 6/Kids[4 0 R 6 0 R 12 0 R 13 0 R 14 0 R 15 0 R]>>' in content
else:
# Incorrect.
assert b'<</Type/Pages/Count 6/Kids[4 0 R 6 0 R 12 0 R 4 0 R 6 0 R 12 0 R]>>' in content
assert b'<</Type/Pages/Count 6/Kids[4 0 R 6 0 R 12 0 R 13 0 R 14 0 R 15 0 R]>>' in content


def test_4958():
Expand Down
77 changes: 77 additions & 0 deletions tests/test_markdown_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,80 @@ def test_markdown_save():
with pymupdf.open(stream=md.encode(), filetype='md') as document_md:
out_pdf = os.path.normpath(f'{__file__}/../../tests/test_markdown_save.pdf')
document_md.save(out_pdf)


def test_markdown_bad_unicode():
path_md = os.path.normpath(f'{__file__}/../../tests/resources/test_markdown_bad_unicode.md')
path_md_pdf = os.path.normpath(f'{__file__}/../../tests/test_markdown_bad_unicode.md.pdf')
with pymupdf.open(path_md) as md_doc:
md_doc.save(path_md_pdf)

# To check that pymupdf has done the right thing, we extract text from our
# generated pdf and assert that it is as expected.
with pymupdf.open(path_md_pdf) as document:
text = document[0].get_text()
textb = text.encode('utf8')
print()
print(textwrap.indent(text, ' '))
print(f'textb:')
for line in textb.split(b'\n'):
print(f' {line}')
print(f'{pymupdf.mupdf_version_tuple=}')
if pymupdf.mupdf_version_tuple > (1, 28, 0):
textb_expected = (
b'Title',
b'A Table',
b'Boiling Points \xc8\xb9C',
b'min',
b'max',
b'avg',
b'Noble gases',
b'-269',
b'-62',
b'-170.5',
b'Nonmetals',
b'-253',
b'4827',
b'414.1',
b'Metalloids',
b'335',
b'3900',
b'741.5',
b'Metals',
b'357',
b'>5000',
b'2755.9',
b'A List',
b'\xe2\x80\xa2 Comment 1',
b'\xe2\x80\xa2 Comment 2',
b'\xe2\x80\xa2 Comment 3 with a link to Find out more',
b'My TO-DOs',
b'\xe2\x80\xa2 \xe2\x98\x92 Done!',
b'\xe2\x80\xa2 \xe2\x98\x92 Also done!',
b'\xe2\x80\xa2 \xe2\x98\x90 Still open',
b'',
)
else:
# Table is not recognised because it contains illegal utf8 sequence.
textb_expected = (
b'Title',
b'A Table',
b'|Boiling Points \xc8\xb9C|min|max|avg| |---|---|---|---| |Noble',
b'gases|-269|-62|-170.5| |Nonmetals|-253|4827|414.1| |Metalloids|',
b'335|3900|741.5| |Metals|357|>5000|2755.9|',
b'A List',
b'\xe2\x80\xa2 Comment 1',
b'\xe2\x80\xa2 Comment 2',
b'\xe2\x80\xa2 Comment 3 with a link to Find out more',
b'My TO-DOs',
b'\xe2\x80\xa2 \xe2\x98\x92 Done!',
b'\xe2\x80\xa2 \xe2\x98\x92 Also done!',
b'\xe2\x80\xa2 \xe2\x98\x90 Still open',
b'',
)
textb_expected = b'\n'.join(textb_expected)
if textb != textb_expected:
print(f'textb_expected:')
for line in textb_expected.split(b'\n'):
print(f' {line}')
assert textb == textb_expected
22 changes: 4 additions & 18 deletions tests/test_pixmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,7 @@ def product(x, y):
print(f'{rms=}')
assert rms == 0
wt = pymupdf.TOOLS.mupdf_warnings()
if (1, 26, 0) <= pymupdf.mupdf_version_tuple < (1, 27):
assert wt == 'bogus font ascent/descent values (0 / 0)\nPDF stream Length incorrect'
else:
assert wt == 'PDF stream Length incorrect'
assert wt == 'PDF stream Length incorrect'

def test_3058():
doc = pymupdf.Document(os.path.abspath(f'{__file__}/../../tests/resources/test_3058.pdf'))
Expand Down Expand Up @@ -616,10 +613,7 @@ def test_3806():
pixmap.save(path_png)
rms = gentle_compare.pixmaps_rms(path_png_expected, pixmap)
print(f'{rms=}')
if pymupdf.mupdf_version_tuple >= (1, 26, 6):
assert rms < 0.1
else:
assert rms > 50
assert rms < 0.1


def test_4388():
Expand All @@ -644,10 +638,7 @@ def test_4388():

rms = gentle_compare.pixmaps_rms(pixmap_correct, pixmap_test)
print(f'{rms=}')
if pymupdf.mupdf_version_tuple >= (1, 26, 6):
assert rms == 0
else:
assert rms >= 10
assert rms == 0


def test_4699():
Expand All @@ -661,12 +652,7 @@ def test_4699():
print(f'Have saved to {path_png_actual=}.')
rms = gentle_compare.pixmaps_rms(path_png_expected, pixmap)
print(f'test_4699(): {rms=}')
if pymupdf.mupdf_version_tuple >= (1, 26, 11):
assert rms == 0
else:
wt = pymupdf.TOOLS.mupdf_warnings()
assert 'syntax error: cannot find ExtGState resource' in wt
assert rms > 20
assert rms == 0


def test_5001():
Expand Down
27 changes: 8 additions & 19 deletions tests/test_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,25 +305,14 @@ def test_markdown():
doc = pymupdf.open(filename)
page = doc[0]
tab = page.find_tables(strategy="lines_strict")[0]
if pymupdf.mupdf_version_tuple < (1, 26, 3):
md_expected = textwrap.dedent('''
|Header1|Header2|Header3|
|---|---|---|
|Col11<br>Col12|~~Col21~~<br>~~Col22~~|Col31<br>Col32<br>Col33|
|Col13|~~Col23~~|Col34<br>Col35|
|Col14|~~Col24~~|Col36|
|Col15|~~Col25~~<br>~~Col26~~||

''').lstrip()
else:
md_expected = (
"|Header1|Header2|Header3|\n"
"|---|---|---|\n"
"|Col11<br>Col12|Col21<br>Col22|Col31<br>Col32<br>Col33|\n"
"|Col13|Col23|Col34<br>Col35|\n"
"|Col14|Col24|Col36|\n"
"|Col15|Col25<br>Col26||\n\n"
)
md_expected = (
"|Header1|Header2|Header3|\n"
"|---|---|---|\n"
"|Col11<br>Col12|Col21<br>Col22|Col31<br>Col32<br>Col33|\n"
"|Col13|Col23|Col34<br>Col35|\n"
"|Col14|Col24|Col36|\n"
"|Col15|Col25<br>Col26||\n\n"
)


md = tab.to_markdown()
Expand Down
45 changes: 8 additions & 37 deletions tests/test_textextract.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,9 +354,7 @@ def test_3594():
print(f' {line!r}')
print('='*40)
wt = pymupdf.TOOLS.mupdf_warnings()
if pymupdf.mupdf_version_tuple < (1, 26, 8):
assert not wt
elif pymupdf.mupdf_version_tuple < (1, 28):
if pymupdf.mupdf_version_tuple < (1, 28):
assert wt == 'Actualtext with no position. Text may be lost or mispositioned.\n... repeated 2 times...'
else:
assert wt == 'ActualText with no position. Text may be lost or mispositioned.\n... repeated 2 times...'
Expand Down Expand Up @@ -407,14 +405,10 @@ def get_all_page_from_pdf(document, last_page=None):
if pymupdf.mupdf_version_tuple >= (1, 27, 1):
expected = ''
assert wt == expected
elif pymupdf.mupdf_version_tuple >= (1, 27):
else:
expected = 'format error: No common ancestor in structure tree\nstructure tree broken, assume tree is missing'
expected = '\n'.join([expected] * 56)
assert wt == expected
elif pymupdf.mupdf_version_tuple >= (1, 26, 8):
assert wt == 'Actualtext with no position. Text may be lost or mispositioned.\n... repeated 7684 times...'
else:
assert wt == 'Actualtext with no position. Text may be lost or mispositioned.\n... repeated 434 times...'

def test_3650():
path = os.path.normpath(f'{__file__}/../../tests/resources/test_3650.pdf')
Expand Down Expand Up @@ -910,18 +904,11 @@ def test_4546():
print(f'{text.encode()=}')

wt = pymupdf.TOOLS.mupdf_warnings()
if pymupdf.mupdf_version_tuple >= (1, 26, 8):
assert text == expected_mupdf_1_27_0
if pymupdf.mupdf_version_tuple >= (1, 28):
assert wt == 'ActualText with no position. Text may be lost or mispositioned.\n... repeated 120 times...'
else:
assert wt == 'Actualtext with no position. Text may be lost or mispositioned.\n... repeated 120 times...'
elif pymupdf.mupdf_version_tuple >= (1, 26, 1):
assert text == expected_mupdf_1_26_1
assert not wt
assert text == expected_mupdf_1_27_0
if pymupdf.mupdf_version_tuple >= (1, 28):
assert wt == 'ActualText with no position. Text may be lost or mispositioned.\n... repeated 120 times...'
else:
print(f'No expected output for {pymupdf.mupdf_version_tuple=}')
assert not wt
assert wt == 'Actualtext with no position. Text may be lost or mispositioned.\n... repeated 120 times...'


def test_4503():
Expand Down Expand Up @@ -959,21 +946,5 @@ def test_4503():
strikeout = span_0['char_flags'] & pymupdf.mupdf.FZ_STEXT_STRIKEOUT
print(f'{strikeout=}')

if pymupdf.mupdf_version_tuple >= (1, 26, 3):
assert strikeout, f'Expected bit 0 (FZ_STEXT_STRIKEOUT) to be set in {span_0["char_flags"]=:#x}.'
assert text_0 == 'the right to request the state to review and, if appropriate,'
elif pymupdf.mupdf_version_tuple >= (1, 26, 2):
# 2025-06-09: This is still incorrect - the span should include the
# following text 'and, if appropriate,'. It looks like following spans
# are:
# strikeout=0: 'and, '
# strikeout=1: 'if '
# strikeout=0: 'appropri'
# strikeout=1: 'ate,'
#
assert strikeout, f'Expected bit 0 (FZ_STEXT_STRIKEOUT) to be set in {span_0["char_flags"]=:#x}.'
assert text_0 == 'the right to request the state to review '
else:
# Expecting the bug.
assert not strikeout, f'Expected bit 0 (FZ_STEXT_STRIKEOUT) to be unset in {span_0["char_flags"]=:#x}.'
assert text_0 == 'notice the right to request the state to review and, if appropriate,'
assert strikeout, f'Expected bit 0 (FZ_STEXT_STRIKEOUT) to be set in {span_0["char_flags"]=:#x}.'
assert text_0 == 'the right to request the state to review and, if appropriate,'
Loading