Skip to content

get_drawings() returns incorrect lineJoin and width #4954

@sakamotch

Description

@sakamotch

Description of the bug

get_drawings() returns incorrect lineJoin and width values due to two bugs in jm_lineart_stroke_path:

  1. 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.

  2. 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

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