From 0f8fe915625a821ae229bc58a1f91510c4dd3f72 Mon Sep 17 00:00:00 2001 From: Abelardo Ramirez Date: Mon, 3 Aug 2026 09:18:33 -0600 Subject: [PATCH 1/2] Fix several small correctness bugs - Archive.add(): fix broken assert (missing comma turned the error message into part of the boolean expression, disabling the empty-name check for binary content). - Annot.file_info: avoid UnboundLocalError when a crafted/malformed attachment's /FS dict has neither /UF nor /F. - Page.set_language(): call pdfpage.obj() instead of passing the bound method itself, which broke every call that actually sets a language. - Widget.on_state(): skip None entries from button_states() instead of raising TypeError. - Pixmap.tobytes(): add missing f-string prefix so the alpha error message actually includes the format name. - JM_matrix_from_py(): fix copy-paste bug returning FzRect() instead of FzMatrix() on the float-conversion failure path. - utils.get_text(): remove a bare assert that made the documented "misspelled option falls back to text" behavior unreachable. - utils.get_label_pno(): return "" (as documented) instead of raising IndexError when no label rule applies to the given page number. - utils.get_sorted_text(): guard against ZeroDivisionError for zero-width glyph bboxes. - _apply_pages.py (method='fork'): send the actual page numbers to workers instead of plain list indices, which silently processed the wrong pages for any non-contiguous/non-zero-based `pages` list. - table.chars_in_rect(): compare against c["top"]/c["bottom"] (top-down, matching the rect from page.get_drawings()) instead of c["y0"]/c["y1"] (PDF-native bottom-up space), fixing table detection for frames drawn as filled rectangles. --- src/__init__.py | 11 +++++++---- src/_apply_pages.py | 25 +++++++++++++------------ src/table.py | 7 +++++-- src/utils.py | 8 +++++--- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 000c2a23c..d8be4e9b2 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -943,6 +943,7 @@ def file_info(self): fs = mupdf.pdf_dict_get(annot_obj, PDF_NAME('FS')) + filename = None o = mupdf.pdf_dict_get(fs, PDF_NAME('UF')) if o.m_internal: filename = mupdf.pdf_to_text_string(o) @@ -2053,7 +2054,7 @@ def make_subarch(entries, mount, fmt): raise ValueError(f'Not a file or directory: {content!r}') elif is_binary_data(content): - assert isinstance(path, str) and path != '' \ + assert isinstance(path, str) and path != '', \ f'Need name for binary content, but {path=}.' self._add_treeitem(content, path) return make_subarch([path], None, 'tree') @@ -9482,6 +9483,8 @@ def on_state(self): if bstate is None: bstate = dict() for k in bstate.keys(): + if bstate[k] is None: + continue for v in bstate[k]: if v != "Off": return v @@ -13140,7 +13143,7 @@ def set_language(self, language=None): lang = mupdf.fz_text_language_from_string(language) assert hasattr(mupdf, 'fz_string_from_text_language2') mupdf.pdf_dict_put_text_string( - pdfpage.obj, + pdfpage.obj(), PDF_NAME('Lang'), mupdf.fz_string_from_text_language2(lang) ) @@ -14179,7 +14182,7 @@ def tobytes(self, output="png", jpg_quality=95): if idx is None: raise ValueError(f"Image format {output} not in {tuple(valid_formats.keys())}") if self.alpha and idx in (2, 6, 7): - raise ValueError("'{output}' cannot have alpha") + raise ValueError(f"'{output}' cannot have alpha") if self.colorspace and self.colorspace.n > 3 and idx in (1, 2, 4): raise ValueError(f"unsupported colorspace for '{output}'") if idx == 7: @@ -20996,7 +20999,7 @@ def JM_matrix_from_py(m): for i in range(6): a[i] = JM_FLOAT_ITEM(m, i) if a[i] is None: - return mupdf.FzRect() + return mupdf.FzMatrix() return mupdf.FzMatrix(a[0], a[1], a[2], a[3], a[4], a[5]) diff --git a/src/_apply_pages.py b/src/_apply_pages.py index 5d473db4c..0af40652b 100644 --- a/src/_apply_pages.py +++ b/src/_apply_pages.py @@ -137,11 +137,12 @@ def childfn(): while 1: if verbose: pymupdf.log(f'{os.getpid()=}: calling get().') - page_num = queue_down.get() + item = queue_down.get() if verbose: - pymupdf.log(f'{os.getpid()=}: {page_num=}.') - if page_num is None: + pymupdf.log(f'{os.getpid()=}: {item=}.') + if item is None: break + index, page_num = item try: if not document: if stats: @@ -172,9 +173,9 @@ def childfn(): if verbose: pymupdf.log(f'{os.getpid()=}: exception {e=}') ret = e if verbose: - pymupdf.log(f'{os.getpid()=}: sending {page_num=} {ret=}') - - queue_up.put( (page_num, ret) ) + pymupdf.log(f'{os.getpid()=}: sending {index=} {ret=}') + + queue_up.put( (index, ret) ) error = None @@ -206,8 +207,8 @@ def childfn(): t = time.time() if verbose: pymupdf.log(f'Sending page numbers.') - for page_num in range(len(pages)): - queue_down.put(page_num) + for index, page_num in enumerate(pages): + queue_down.put((index, page_num)) if stats: _stats_write(t, 'Send page numbers') @@ -215,15 +216,15 @@ def childfn(): # of text, but this hasn't been tested. ret = [None] * len(pages) for i in range(len(pages)): - page_num, text = queue_up.get() + index, text = queue_up.get() if verbose: - pymupdf.log(f'{page_num=} {len(text)=}') - assert ret[page_num] is None + pymupdf.log(f'{index=} {len(text)=}') + assert ret[index] is None if isinstance(text, Exception): if not error: error = text break - ret[page_num] = text + ret[index] = text # Close queue. This should cause exception in workers and terminate # them, but on macos-arm64 this does not seem to happen, so we also diff --git a/src/table.py b/src/table.py index 7b8e2c19c..8bbd17b2b 100644 --- a/src/table.py +++ b/src/table.py @@ -196,12 +196,15 @@ def rect_in_rect(inner, outer): def chars_in_rect(CHARS, rect): """Check whether any of the chars in CHAR are inside rectangle 'rect'.""" + # NB: 'rect' (e.g. from page.get_drawings()) and c["x0"]/c["x1"] are in + # top-down page space; c["top"]/c["bottom"] are the top-down counterparts + # of c["y0"]/c["y1"], which are in PDF-native (bottom-up) space instead. return any( 1 and rect[0] <= c["x0"] and c["x1"] <= rect[2] - and rect[1] <= c["y0"] - and rect[3] >= c["y1"] + and rect[1] <= c["top"] + and rect[3] >= c["bottom"] for c in CHARS ) diff --git a/src/utils.py b/src/utils.py index 7d3271ea8..23a90a82b 100644 --- a/src/utils.py +++ b/src/utils.py @@ -210,7 +210,7 @@ def line_text(clip, line): lrect |= r # update line bbox # convert distance to previous word to multiple spaces dist = max( - int(round((r.x0 - x1) / r.width * len(t))), + int(round((r.x0 - x1) / r.width * len(t))) if r.width else 0, 0 if (x1 == clip.x0 or r.x0 <= x1) else 1, ) # number of space characters @@ -497,7 +497,6 @@ def get_text( "blocks": pymupdf.TEXTFLAGS_BLOCKS, } option = option.lower() - assert option in formats if option not in formats: option = "text" if flags is None: @@ -962,7 +961,10 @@ def get_label_pno(pgNo, labels): """ # Jorj McKie, 2021-01-06 - item = [x for x in labels if x[0] <= pgNo][-1] + candidates = [x for x in labels if x[0] <= pgNo] + if not candidates: + return "" + item = candidates[-1] rule = rule_dict(item) prefix = rule.get("prefix", "") style = rule.get("style", "") From fe2798b486e9dcc6a519bc1861f807ccd8b2f06a Mon Sep 17 00:00:00 2001 From: Abelardo Ramirez Date: Tue, 4 Aug 2026 09:10:05 -0600 Subject: [PATCH 2/2] get_text: fail on unrecognized option instead of silently defaulting Per review feedback from julian-smith-artifex-com: the intent was always to raise on an unrecognized option, not fall back to "text" silently. Removes the fallback and keeps the assert (which the earlier fix in this PR had already de-duplicated from its own missing-comma bug elsewhere). --- src/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/utils.py b/src/utils.py index 23a90a82b..1686ebbba 100644 --- a/src/utils.py +++ b/src/utils.py @@ -497,8 +497,7 @@ def get_text( "blocks": pymupdf.TEXTFLAGS_BLOCKS, } option = option.lower() - if option not in formats: - option = "text" + assert option in formats if flags is None: flags = formats[option]