Repository navigation
Description
Description of the bug
get_drawings() returns incorrect lineJoin and width values due to two bugs in jm_lineart_stroke_path:
-
lineJoinis multiplied bypathfactor:stroke->linejoinis an enum (0=Miter, 1=Round, 2=Bevel) and should not be scaled. Note thatlineCapis correctly handled as plain integers withoutpathfactormultiplication, so this is inconsistent. -
pathfactoronly handles uniform scaling and 90° rotation: For non-uniform scaling (e.g.2 0 0 3 0 0 cm),pathfactorfalls back to 1, makingwidthincorrect.
This bug is hard to notice because linejoin=0 (Miter, the most common value) always produces 0 * pathfactor = 0.
Affected code:
src/extra.i:jm_lineart_stroke_pathsrc_classic/helper-devices.i: same function
How to reproduce the bug
Bug 1: lineJoin is scaled by pathfactor
import fitz
doc = fitz.open()
page = doc.new_page(width=200, height=200)
shape = page.new_shape()
shape.draw_line((0, 0), (1, 1))
shape.finish(color=(0, 0, 0), width=0.1)
shape.commit()
content = b"q\n0.12 0 0 0.12 0 0 cm\n2 j\n6 w\n100 100 m\n800 100 l\nS\nQ\n"
doc.update_stream(page.get_contents()[0], content)
pdf_bytes = doc.tobytes()
doc.close()
doc2 = fitz.open(stream=pdf_bytes, filetype="pdf")
d = doc2[0].get_drawings()[-1]
print(f"lineJoin={d['lineJoin']}") # Expected: 2, Actual: 0.24
doc2.close()| CTM scale | linejoin (raw) | Expected | Actual |
|---|---|---|---|
| 0.12 | 2 (Bevel) | 2 | 0.24 |
| 2.0 | 2 (Bevel) | 2 | 4.0 |
| 2.83 | 1 (Round) | 1 | 2.83 |
For comparison, lineCap is correctly returned as integers without scaling:
// lineCap - correct (no pathfactor)
Py_BuildValue("iii", stroke->start_cap, stroke->dash_cap, stroke->end_cap)
// lineJoin - incorrect (pathfactor applied)
Py_BuildValue("f", dev->pathfactor * stroke->linejoin)Bug 2: width is wrong with non-uniform scale
doc = fitz.open()
page = doc.new_page(width=200, height=200)
shape = page.new_shape()
shape.draw_line((0, 0), (1, 1))
shape.finish(color=(0, 0, 0), width=0.1)
shape.commit()
content = b"q\n2 0 0 3 0 0 cm\n1 w\n10 10 m\n90 10 l\nS\nQ\n"
doc.update_stream(page.get_contents()[0], content)
pdf_bytes = doc.tobytes()
doc.close()
doc2 = fitz.open(stream=pdf_bytes, filetype="pdf")
d = doc2[0].get_drawings()[-1]
print(f"width={d['width']}") # Expected: 2.0, Actual: 1.0
doc2.close()Suggested fix
lineJoin — use the raw enum value without scaling:
// Before (src/extra.i)
DICT_SETITEMSTR_DROP(dev->pathdict, "lineJoin",
Py_BuildValue("f", dev->pathfactor * stroke->linejoin));
// After
DICT_SETITEMSTR_DROP(dev->pathdict, "lineJoin",
Py_BuildValue("i", stroke->linejoin));pathfactor — use sqrt(a² + b²) to handle arbitrary transforms:
// Before (src/extra.i)
dev->pathfactor = 1;
if (ctm.a != 0 && fz_abs(ctm.a) == fz_abs(ctm.d))
dev->pathfactor = fz_abs(ctm.a);
else if (ctm.b != 0 && fz_abs(ctm.b) == fz_abs(ctm.c))
dev->pathfactor = fz_abs(ctm.b);
// After
float scale = sqrtf(ctm.a * ctm.a + ctm.b * ctm.b);
if (scale < 1e-9f)
scale = sqrtf(ctm.c * ctm.c + ctm.d * ctm.d);
if (scale < 1e-9f)
scale = 1.0f;
dev->pathfactor = scale;Both src/extra.i and src_classic/helper-devices.i have the same bugs.
Note: For non-uniform scaling, stroke width is direction-dependent in general.
This fix approximates it using the length of the transformed unit vector.
PyMuPDF version
1.27.2.2
Operating system
Linux
Python version
3.13
Activity
sakamotch commentedon Mar 26, 2026
I've submitted a PR #4955 for this. The CI test failure in
test_drawings1is because I changedlineJoinfromPy_BuildValue("f", ...)toPy_BuildValue("i", ...), which changes the type from float to int. I can keep it as float if backward compatibility is preferred — happy to adjust based on your preference.JorjMcKie commentedon Mar 26, 2026
In any case thanks for the PR!
Yes, please use floats for backward compatibility. We'll approve the PR as soon as tests go through.
sakamotch commentedon Mar 26, 2026
Thanks! Updated to use float for backward compatibility.
julian-smith-artifex-com commentedon Apr 16, 2026
After merging #4955 i realised that we don't have a test for the new behaviour, so i've added two tests in my tree, based on the original reproducer code you provided above.
For case 1, it changes
d['lineJoin']from 0.24 to 2, as expected.But for case 2, it changes
d['width']from 1.0 to 2.449. Is this ok?Test code is:
sakamotch commentedon Apr 16, 2026
Yes, I think this is OK.
Strictly speaking, with non-uniform scaling there is no single exact stroke width: the effective width depends on the direction of each stroked segment. Since
get_drawings()currently returns a singlewidthvalue for the path, it cannot represent per-segment/per-direction widths.So for the current API, using
sqrt(abs(a*d - b*c))as a representative scalar seems reasonable. In this case that givessqrt(6) = 2.449..., so the proposed test expectation matches the merged implementation.julian-smith-artifex-com commentedon Apr 16, 2026
Marvellous, thanks.
tests/test_drawings.py: New tests for #4954: test_4954_1() test_4954_…
tests/test_drawings.py: New tests for #4954: test_4954_1() test_4954_…
tests/test_drawings.py: New tests for #4954: test_4954_1() test_4954_…
tests/test_drawings.py: New tests for #4954: test_4954_1() test_4954_…
tests/test_drawings.py: New tests for #4954: test_4954_1() test_4954_…
julian-smith-artifex-com commentedon Apr 24, 2026
Fixed in pymupdf-1.27.2.3.