Skip to content

fitz.Story.place() can loop indefinitely with long unbreakable CJK text #5119

Description

@montasirm8b

I encountered a case where fitz.Story can cause an application to enter an effectively infinite page-generation loop when rendering a long Chinese text block.

The problematic combination appears to be:

  • A long continuous Chinese/CJK text run with no spaces
  • A narrow fixed-width container/column
  • No CSS rule allowing word breaking
  • fitz.Story.place() used in the standard while more: pagination loop

The Story.place() call continues returning more != 0, but apparently makes no progress on the unbreakable text. As a result, the caller keeps generating pages indefinitely.

In our production case, this resulted in a PDF-generation worker running until our 20-minute timeout. The same operation was retried 3 times.

Minimal reproduction

import fitz
html = """

<style> .column { width: 100px; } </style>
[LONG_CONTINUOUS_CHINESE_TEXT_WITH_NO_SPACES]
""" story = fitz.Story(html=html) mediabox = fitz.paper_rect("a4") where = mediabox + (36, 36, -36, -36) writer = fitz.DocumentWriter("output.pdf") more = 1 page_count = 0 while more: page_count += 1 # Safety limit added only to demonstrate the problem. if page_count > 100: raise RuntimeError("Story.place() did not make progress") device = writer.begin_page(mediabox) more, filled = story.place(where) print( f"page={page_count}, more={more}, filled={filled}" ) story.draw(device) writer.end_page() writer.close()

With sufficiently long continuous Chinese text, story.place() repeatedly returns more without successfully consuming the remaining content, causing the pagination loop to continue indefinitely.

Expected behavior

One of the following would be preferable:

  1. Story.place() should break the CJK text at character boundaries, or
  2. Story.place() should return an error/indication that the content cannot fit, or
  3. Story.place() should make progress rather than repeatedly returning more for the same content.

It should not be possible for the normal documented pagination pattern:

while more:
more, filled = story.place(where)

to continue indefinitely without consuming any content.

Actual behavior

story.place() repeatedly returns more != 0.

The caller therefore creates page after page while the same unbreakable CJK content remains pending.

In our production environment this manifested as:

PDF generation

Story.place()

more = 1

new page

Story.place()

more = 1

new page

...

20 minute timeout

Why this is particularly problematic

The documented Story examples use the same while more: pagination pattern, so an application following the standard API usage can accidentally create an unbounded loop when encountering this input.

A guard such as:

previous_filled = filled
if more and filled == previous_filled:
# no progress

may be necessary as an application-level workaround, but it would be preferable for Story.place() to expose a reliable way to detect this condition.

Environment

Please let me know which additional information would be useful. I can provide:

  • PyMuPDF version
  • Python version
  • OS
  • Minimal HTML/CSS reproduction
  • The exact Chinese text pattern
  • A complete reproducible script
  • PDF/output information

Related behavior

I found existing PyMuPDF discussions/issues around CJK text and line breaking. In particular, the documentation/community discussion indicates that Story primarily considers spaces/line breaks as word boundaries, which makes continuous CJK text problematic.

There also appears to be an existing enhancement request for character-wise line breaking for CJK.

However, I did not find a report describing the specific failure mode where this results in Story.place() continuously returning more and allowing the standard pagination loop to run indefinitely.

Is this expected behavior, or should Story.place() detect that no progress is being made?

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions