-
Notifications
You must be signed in to change notification settings - Fork 699
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