Description of the bug
get_drawings() returns incorrect lineJoin and width values due to two bugs in jm_lineart_stroke_path:
-
lineJoin is multiplied by pathfactor: stroke->linejoin is an enum (0=Miter, 1=Round, 2=Bevel) and should not be scaled. Note that lineCap is correctly handled as plain integers without pathfactor multiplication, so this is inconsistent.
-
pathfactor only handles uniform scaling and 90° rotation: For non-uniform scaling (e.g. 2 0 0 3 0 0 cm), pathfactor falls back to 1, making width incorrect.
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_path
src_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
Description of the bug
get_drawings()returns incorrectlineJoinandwidthvalues due to two bugs injm_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 produces0 * pathfactor = 0.Affected code:
src/extra.i:jm_lineart_stroke_pathsrc_classic/helper-devices.i: same functionHow to reproduce the bug
Bug 1: lineJoin is scaled by pathfactor
For comparison,
lineCapis correctly returned as integers without scaling:Bug 2: width is wrong with non-uniform scale
Suggested fix
lineJoin — use the raw enum value without scaling:
pathfactor — use
sqrt(a² + b²)to handle arbitrary transforms:Both
src/extra.iandsrc_classic/helper-devices.ihave 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