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:
- Story.place() should break the CJK text at character boundaries, or
- Story.place() should return an error/indication that the content cannot fit, or
- 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?
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:
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
<style> .column { width: 100px; } </style>html = """
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:
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:
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?