From 6819ac821be2e31c5396c91cd0c21455025bf2ba Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Mon, 8 Sep 2025 17:35:30 +0100 Subject: [PATCH 1/3] tests/gentle_compare.py:rms(): reduce verbosity. --- tests/gentle_compare.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/gentle_compare.py b/tests/gentle_compare.py index b83fdd5f6..aa155bb7c 100644 --- a/tests/gentle_compare.py +++ b/tests/gentle_compare.py @@ -32,8 +32,6 @@ def rms(a, b, verbose=None, out_prefix=''): ''' Returns RMS diff of raw bytes of two sequences. ''' - if verbose is True: - verbose = 100000 assert len(a) == len(b) e = 0 for i, (aa, bb) in enumerate(zip(a, b)): @@ -62,7 +60,7 @@ def pixmaps_rms(a, b, out_prefix=''): a_mv = a.samples_mv b_mv = b.samples_mv assert len(a_mv) == len(b_mv) - ret = rms(a_mv, b_mv, verbose=True, out_prefix=out_prefix) + ret = rms(a_mv, b_mv, out_prefix=out_prefix) print(f'{out_prefix}pixmaps_rms(): {ret=}.') return ret From 40354e1e70c50e2abdb5eb452de77c0847dcda5d Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Tue, 9 Sep 2025 17:45:20 +0100 Subject: [PATCH 2/3] pipcl.py:git_info(): fix typo. --- pipcl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipcl.py b/pipcl.py index 4fee4fb65..89abc6425 100644 --- a/pipcl.py +++ b/pipcl.py @@ -1990,7 +1990,7 @@ def git_info( directory): ) if not e: branch = out.strip() - log(f'git_info(): directory={directory!r} returning branch={branch!r} sha={sha!r} comment={comment!r}') + log1(f'git_info(): directory={directory!r} returning branch={branch!r} sha={sha!r} comment={comment!r}') return sha, comment, diff, branch From 86ff01e125415a84885a81f3b41dda31759c988b Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Mon, 8 Sep 2025 14:46:45 +0100 Subject: [PATCH 3/3] docs/ src/ tests/: Page.insert_htmlbox(): also down-scale to fit long words. We use mupdf.FZ_PLACE_STORY_FLAG_NO_OVERFLOW with fz_place_story_flags(). Also updated test_htmlbox1() to match new behaviour where Page.insert_htmlbox() returns with a small positive on success. --- docs/page.rst | 18 +++++-- src/__init__.py | 88 +++++++++++++++++++++++----------- tests/resources/test_4613.png | Bin 0 -> 66351 bytes tests/test_textbox.py | 83 +++++++++++++++++++++++++++++++- 4 files changed, 155 insertions(+), 34 deletions(-) create mode 100644 tests/resources/test_4613.png diff --git a/docs/page.rst b/docs/page.rst index 54b4b7e23..12704deba 100644 --- a/docs/page.rst +++ b/docs/page.rst @@ -847,14 +847,24 @@ In a nutshell, this is what you can do with PyMuPDF: :returns: A tuple of floats `(spare_height, scale)`. - - `spare_height`: -1 if content did not fit, else >= 0. It is the height of the unused (still available) rectangle stripe. Positive only if scale = 1 (no down-scaling happened). - - `scale`: down-scaling factor, 0 < scale <= 1. + - spare_height: The (positive) height of the remaining space in `rect` below the + text, or -1 if we failed to fit. + - scale: The scaling required; `0 < scale <= 1`. Will be `scale_low` + if we failed to fit. - Please refer to examples in this section of the recipes: :ref:`RecipesText_I_c`. + Please refer to examples in this section of the recipes: :ref:`RecipesText_I_c`. |history_begin| - * New in v1.23.8; rebased-only. + * New in v1.26.5: + + * do additional scaling to fit long words. + * + If we succeeded and scaled down, the returned `spare_height` is now + generally positive instead of being fixed to zero, because the final + rect's height is usually not an exact multiple of the font line + height. + * New in v1.23.8: rebased-only. * New in v1.23.9: `opacity` parameter. |history_end| diff --git a/src/__init__.py b/src/__init__.py index 16905771b..16182e130 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -12266,7 +12266,9 @@ def insert_htmlbox( oc=0, opacity=1, overlay=True, - ) -> float: + _scale_word_width=True, + _verbose=False, + ) -> tuple: """Insert text with optional HTML tags and stylings into a rectangle. Args: @@ -12282,14 +12284,17 @@ def insert_htmlbox( oc: (int) the xref of an OCG / OCMD (Optional Content). opacity: (float) set opacity of inserted content. overlay: (bool) put text on top of page content. + _scale_word_width: internal, for testing only. + _verbose: internal, for testing only. Returns: A tuple of floats (spare_height, scale). - spare_height: -1 if content did not fit, else >= 0. It is the height of the - unused (still available) rectangle stripe. Positive only if - scale_min = 1 (no down scaling). - scale: downscaling factor, 0 < scale <= 1. Set to 0 if spare_height = -1 (no fit). + spare_height: + The height of the remaining space in below the + text, or -1 if we failed to fit. + scale: + The scaling required; `0 < scale <= 1`. + Will be less than `scale_low` if we failed to fit. """ - # normalize rotation angle if not rotate % 90 == 0: raise ValueError("bad rotation angle") @@ -12320,25 +12325,40 @@ def insert_htmlbox( story = text else: raise ValueError("'text' must be a string or a Story") + # ---------------------------------------------------------------- - # Find a scaling factor that lets our story fit in + # Find a scaling factor that lets our story fit in. Instead of scaling + # the text smaller, we instead look at how much bigger the rect needs + # to be to fit the text, then reverse the scaling to get how much we + # need to scale down the text. # ---------------------------------------------------------------- - scale_max = None if scale_low == 0 else 1 / scale_low - - fit = story.fit_scale(temp_rect, scale_min=1, scale_max=scale_max) + rect_scale_max = None if scale_low == 0 else 1 / scale_low + + fit = story.fit_scale( + temp_rect, + scale_min=1, + scale_max=rect_scale_max, + flags=mupdf.FZ_PLACE_STORY_FLAG_NO_OVERFLOW if _scale_word_width else 0, + verbose=_verbose, + ) + if not fit.big_enough: # there was no fit - return (-1, scale_low) - - filled = fit.filled - scale = 1 / fit.parameter # shrink factor - - spare_height = fit.rect.y1 - filled[3] # unused room at rectangle bottom - # Note: due to MuPDF's logic this may be negative even for successful fits. - if scale != 1 or spare_height < 0: # if scaling occurred, set spare_height to 0 - spare_height = 0 + scale = 1 / fit.parameter + return (-1, scale) + + # fit.filled is a tuple; we convert it in place to a Rect for + # convenience. (fit.rect is already a Rect.) + fit.filled = Rect(fit.filled) + assert (fit.rect.x0, fit.rect.y0) == (0, 0) + assert (fit.filled.x0, fit.filled.y0) == (0, 0) + + scale = 1 / fit.parameter + assert scale >= scale_low, f'{scale_low=} {scale=}' + + spare_height = max((fit.rect.y1 - fit.filled.y1) * scale, 0) def rect_function(*args): - return fit.rect, fit.rect, Identity + return fit.rect, fit.rect, None # draw story on temp PDF page doc = story.write_with_links(rect_function) @@ -15925,10 +15945,13 @@ class Position2: function( position2) mupdf.fz_story_positions( self.this, function2) - def place( self, where): + def place( self, where, flags=0): + ''' + Wrapper for fz_place_story_flags(). + ''' where = JM_rect_from_py( where) filled = mupdf.FzRect() - more = mupdf.fz_place_story( self.this, where, filled) + more = mupdf.fz_place_story_flags( self.this, where, filled, flags) return more, JM_py_from_rect( filled) def reset( self): @@ -16045,7 +16068,9 @@ class FitResult: `big_enough`: `True` if the fit succeeded. `filled`: - From the last call to `Story.place()`. + Tuple (x0, y0, x1, y1) from the last call to `Story.place()`. This + will be wider than .rect if any single word (which we never split) + was too wide for .rect. `more`: `False` if the fit succeeded. `numcalls`: @@ -16053,7 +16078,7 @@ class FitResult: `parameter`: The successful parameter value, or the largest failing value. `rect`: - The rect created from `parameter`. + The pumupdf.Rect created from `parameter`. ''' def __init__(self, big_enough=None, filled=None, more=None, numcalls=None, parameter=None, rect=None): self.big_enough = big_enough @@ -16073,7 +16098,7 @@ def __repr__(self): f' rect={self.rect}' ) - def fit(self, fn, pmin=None, pmax=None, delta=0.001, verbose=False): + def fit(self, fn, pmin=None, pmax=None, delta=0.001, verbose=False, flags=0): ''' Finds optimal rect that contains the story `self`. @@ -16100,6 +16125,9 @@ def fit(self, fn, pmin=None, pmax=None, delta=0.001, verbose=False): Maximum error in returned `parameter`. :arg verbose: If true we output diagnostics. + :arg flags: + Passed to mupdf.fz_place_story_flags(). e.g. + zero or `mupdf.FZ_PLACE_STORY_FLAG_NO_OVERFLOW`. ''' def log(text): assert verbose @@ -16155,7 +16183,7 @@ def update(parameter): if verbose: log(f'update(): not calling self.place() because rect is empty.') else: - more, filled = self.place(rect) + more, filled = self.place(rect, flags) state.numcalls += 1 big_enough = not more result = Story.FitResult( @@ -16224,12 +16252,12 @@ def opposite(p, direction): parameter = (state.pmin + state.pmax) / 2 update(parameter) - def fit_scale(self, rect, scale_min=0, scale_max=None, delta=0.001, verbose=False): + def fit_scale(self, rect, scale_min=0, scale_max=None, delta=0.001, verbose=False, flags=0): ''' Finds smallest value `scale` in range `scale_min..scale_max` where `scale * rect` is large enough to contain the story `self`. - Returns a `Story.FitResult` instance. + Returns a `Story.FitResult` instance with `.parameter` set to `scale`. :arg width: width of rect. @@ -16244,13 +16272,15 @@ def fit_scale(self, rect, scale_min=0, scale_max=None, delta=0.001, verbose=Fals Maximum error in returned scale. :arg verbose: If true we output diagnostics. + :arg flags: + Passed to Story.place(). ''' x0, y0, x1, y1 = rect width = x1 - x0 height = y1 - y0 def fn(scale): return Rect(x0, y0, x0 + scale*width, y0 + scale*height) - return self.fit(fn, scale_min, scale_max, delta, verbose) + return self.fit(fn, scale_min, scale_max, delta, verbose, flags) def fit_height(self, width, height_min=0, height_max=None, origin=(0, 0), delta=0.001, verbose=False): ''' diff --git a/tests/resources/test_4613.png b/tests/resources/test_4613.png new file mode 100644 index 0000000000000000000000000000000000000000..e02b7328d9e30da73fc1b430e6edcff1617a839b GIT binary patch literal 66351 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 nWQY|gXIQWTmH`_=yi$Ui<= literal 0 HcmV?d00001 diff --git a/tests/test_textbox.py b/tests/test_textbox.py index d8003624d..21b253cf0 100644 --- a/tests/test_textbox.py +++ b/tests/test_textbox.py @@ -7,6 +7,11 @@ """ import pymupdf +import gentle_compare + +import os +import textwrap + # codespell:ignore-begin text = """Der Kleine Schwertwal (Pseudorca crassidens), auch bekannt als Unechter oder Schwarzer Schwertwal, ist eine Art der Delfine (Delphinidae) und der einzige rezente Vertreter der Gattung Pseudorca. @@ -182,7 +187,9 @@ def test_htmlbox1(): assert spare_height < 0 assert scale == 1 spare_height, scale = page.insert_htmlbox(rect, text, rotate=rot, scale_low=0) - assert spare_height == 0 + page.draw_rect(rect, (1, 0, 0)) + doc.save(os.path.normpath(f'{__file__}/../../tests/test_htmlbox1.pdf')) + assert abs(spare_height - 3.8507) < 0.001 assert 0 < scale < 1 page = doc.reload_page(page) link = page.get_links()[0] # extracts the links on the page @@ -286,3 +293,77 @@ def test_4400(): text = '111111111' print(f'Calling writer.fill_textbox().', flush=1) writer.fill_textbox(rect=pymupdf.Rect(0, 0, 100, 20), pos=(80, 0), text=text, fontsize=8) + + +def test_4613(): + print() + text = 3 * 'abcdefghijklmnopqrstuvwxyz\nABCDEFGHIJKLMNOPQRSTUVWXYZ\n' + story = pymupdf.Story(text) + rect = pymupdf.Rect(10, 10, 100, 100) + + # Test default operation where we get additional scaling down because of + # the long words in our text. + print(f'test_4613(): ### Testing default operation.') + with pymupdf.open() as doc: + page = doc.new_page() + spare_height, scale = page.insert_htmlbox(rect, story) + print(f'test_4613(): {spare_height=} {scale=}') + # The additional down-scaling from the long word widths results in + # spare vertical space. + page.draw_rect(rect, (1, 0, 0)) + path = os.path.normpath(f'{__file__}/../../tests/test_4613.pdf') + doc.save(path) + + path_pixmap = os.path.normpath(f'{__file__}/../../tests/test_4613.png') + path_pixmap_expected = os.path.normpath(f'{__file__}/../../tests/resources/test_4613.png') + pixmap = page.get_pixmap(dpi=300) + pixmap.save(path_pixmap) + + pixmap_diff = gentle_compare.pixmaps_diff(path_pixmap_expected, pixmap) + pixmap_diff.save(os.path.normpath(f'{__file__}/../../tests/test_4613-diff.png')) + + rms = gentle_compare.pixmaps_rms(pixmap, path_pixmap_expected) + print(f'{rms=}') + assert rms == 0, f'{rms=}' + + assert abs(spare_height - 45.7536) < 0.1 + assert abs(scale - 0.4009) < 0.01 + + new_text = page.get_text('text', clip=rect) + print(f'test_4613(): new_text:') + print(textwrap.indent(new_text, ' ')) + assert new_text == text + + # Check with _scale_word_width=False - ignore too-wide words. + print(f'test_4613(): ### Testing with _scale_word_width=False.') + with pymupdf.open() as doc: + page = doc.new_page() + spare_height, scale = page.insert_htmlbox(rect, story, _scale_word_width=False) + print(f'test_4613(): _scale_word_width=False: {spare_height=} {scale=}') + # With _scale_word_width=False we allow long words to extend beyond the + # rect, so we should have spare_height == 0 and only a small amount of + # down-scaling. + assert spare_height == 0 + assert abs(scale - 0.914) < 0.01 + new_text = page.get_text('text', clip=rect) + print(f'test_4613(): new_text:') + print(textwrap.indent(new_text, ' ')) + assert new_text == textwrap.dedent(''' + abcdefghijklmno + ABCDEFGHIJKLM + abcdefghijklmno + ABCDEFGHIJKLM + abcdefghijklmno + ABCDEFGHIJKLM + ''')[1:] + + + # Check that we get no fit if scale_low is not low enough. + print(f'test_4613(): ### Testing with scale_low too high to allow a fit.') + with pymupdf.open() as doc: + page = doc.new_page() + scale_low=0.6 + spare_height, scale = page.insert_htmlbox(rect, story, scale_low=scale_low) + print(f'test_4613(): {scale_low=}: {spare_height=} {scale=}') + assert spare_height == -1 + assert scale == scale_low