From df8534c3060b547c116acbc958d418ccc794caef Mon Sep 17 00:00:00 2001 From: sakamotch Date: Thu, 26 Mar 2026 22:34:15 +0900 Subject: [PATCH 1/4] Fix incorrect lineJoin and pathfactor in get_drawings() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1: lineJoin was multiplied by pathfactor. stroke->linejoin is an enum (0=Miter, 1=Round, 2=Bevel) and should not be scaled. Note that lineCap is already correctly handled as plain integers without pathfactor multiplication. Bug 2: pathfactor calculation only handled uniform scaling (|a|==|d|) and 90-degree rotation (|b|==|c|). For non-uniform scaling, pathfactor fell back to 1, making width incorrect. Use sqrt(a²+b²) to handle arbitrary transforms. Fixed in: - src/extra.i (rebased build) - src_classic/helper-devices.i (classic build) - src/__init__.py (Python fallback) --- src/__init__.py | 13 +++++++------ src/extra.i | 16 ++++++++-------- src_classic/helper-devices.i | 12 ++++++++---- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index e3845575b..4472de089 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -23194,11 +23194,12 @@ def jm_lineart_stroke_path( dev, ctx, path, stroke, ctm, colorspace, color, alph #log(f'{dev.pathdict=} {dev.clips=}') try: assert isinstance( ctm, mupdf.fz_matrix) - dev.pathfactor = 1 - if ctm.a != 0 and abs(ctm.a) == abs(ctm.d): - dev.pathfactor = abs(ctm.a) - elif ctm.b != 0 and abs(ctm.b) == abs(ctm.c): - dev.pathfactor = abs(ctm.b) + scale = math.sqrt(ctm.a ** 2 + ctm.b ** 2) + if scale < 1e-9: + scale = math.sqrt(ctm.c ** 2 + ctm.d ** 2) + if scale < 1e-9: + scale = 1.0 + dev.pathfactor = scale dev.ctm = mupdf.FzMatrix( ctm) # fz_concat(ctm, dev_ptm); dev.path_type = trace_device_STROKE_PATH @@ -23214,7 +23215,7 @@ def jm_lineart_stroke_path( dev, ctx, path, stroke, ctm, colorspace, color, alph stroke.dash_cap, stroke.end_cap, ) - dev.pathdict[ 'lineJoin'] = dev.pathfactor * stroke.linejoin + dev.pathdict[ 'lineJoin'] = float(stroke.linejoin) if 'closePath' not in dev.pathdict: #log('setting dev.pathdict["closePath"] to false') dev.pathdict['closePath'] = False diff --git a/src/extra.i b/src/extra.i index d2d96811a..1b14899e6 100644 --- a/src/extra.i +++ b/src/extra.i @@ -2719,13 +2719,13 @@ jm_lineart_stroke_path(fz_context *ctx, fz_device *dev_, const fz_path *path, jm_lineart_device *dev = (jm_lineart_device *)dev_; //printf("extra.jm_lineart_stroke_path(): dev->seqno=%zi\n", dev->seqno); int 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); - } + { + 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; } dev->ctm = ctm; // fz_concat(ctm, trace_device_ptm); dev->path_type = STROKE_PATH; @@ -2739,7 +2739,7 @@ jm_lineart_stroke_path(fz_context *ctx, fz_device *dev_, const fz_path *path, DICT_SETITEMSTR_DROP(dev->pathdict, "color", jm_lineart_color(colorspace, color)); DICT_SETITEM_DROP(dev->pathdict, dictkey_width, Py_BuildValue("f", dev->pathfactor * stroke->linewidth)); DICT_SETITEMSTR_DROP(dev->pathdict, "lineCap", Py_BuildValue("iii", stroke->start_cap, stroke->dash_cap, stroke->end_cap)); - DICT_SETITEMSTR_DROP(dev->pathdict, "lineJoin", Py_BuildValue("f", dev->pathfactor * stroke->linejoin)); + DICT_SETITEMSTR_DROP(dev->pathdict, "lineJoin", Py_BuildValue("i", stroke->linejoin)); if (!PyDict_GetItemString(dev->pathdict, "closePath")) { DICT_SETITEMSTR_DROP(dev->pathdict, "closePath", JM_BOOL(0)); } diff --git a/src_classic/helper-devices.i b/src_classic/helper-devices.i index fb194f23e..95902ebcf 100644 --- a/src_classic/helper-devices.i +++ b/src_classic/helper-devices.i @@ -433,9 +433,13 @@ jm_lineart_stroke_path(fz_context *ctx, fz_device *dev_, const fz_path *path, jm_lineart_device *dev = (jm_lineart_device *)dev_; PyObject *out = dev->out; int i; - dev_pathfactor = 1; - if (fz_abs(ctm.a) == fz_abs(ctm.d)) { - dev_pathfactor = fz_abs(ctm.a); + { + 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; } trace_device_ctm = ctm; // fz_concat(ctm, trace_device_ptm); path_type = STROKE_PATH; @@ -449,7 +453,7 @@ jm_lineart_stroke_path(fz_context *ctx, fz_device *dev_, const fz_path *path, DICT_SETITEMSTR_DROP(dev_pathdict, "color", jm_lineart_color(ctx, colorspace, color)); DICT_SETITEM_DROP(dev_pathdict, dictkey_width, Py_BuildValue("f", dev_pathfactor * stroke->linewidth)); DICT_SETITEMSTR_DROP(dev_pathdict, "lineCap", Py_BuildValue("iii", stroke->start_cap, stroke->dash_cap, stroke->end_cap)); - DICT_SETITEMSTR_DROP(dev_pathdict, "lineJoin", Py_BuildValue("f", dev_pathfactor * stroke->linejoin)); + DICT_SETITEMSTR_DROP(dev_pathdict, "lineJoin", Py_BuildValue("i", stroke->linejoin)); if (!PyDict_GetItemString(dev_pathdict, "closePath")) { DICT_SETITEMSTR_DROP(dev_pathdict, "closePath", JM_BOOL(0)); } From f606c6a2f5f8b29be763dd050890ee9adeab67a0 Mon Sep 17 00:00:00 2001 From: sakamotch Date: Thu, 26 Mar 2026 23:57:16 +0900 Subject: [PATCH 2/4] Use float for lineJoin to maintain backward compatibility --- src/extra.i | 2 +- src_classic/helper-devices.i | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extra.i b/src/extra.i index 1b14899e6..b19aa83f4 100644 --- a/src/extra.i +++ b/src/extra.i @@ -2739,7 +2739,7 @@ jm_lineart_stroke_path(fz_context *ctx, fz_device *dev_, const fz_path *path, DICT_SETITEMSTR_DROP(dev->pathdict, "color", jm_lineart_color(colorspace, color)); DICT_SETITEM_DROP(dev->pathdict, dictkey_width, Py_BuildValue("f", dev->pathfactor * stroke->linewidth)); DICT_SETITEMSTR_DROP(dev->pathdict, "lineCap", Py_BuildValue("iii", stroke->start_cap, stroke->dash_cap, stroke->end_cap)); - DICT_SETITEMSTR_DROP(dev->pathdict, "lineJoin", Py_BuildValue("i", stroke->linejoin)); + DICT_SETITEMSTR_DROP(dev->pathdict, "lineJoin", Py_BuildValue("f", (float)stroke->linejoin)); if (!PyDict_GetItemString(dev->pathdict, "closePath")) { DICT_SETITEMSTR_DROP(dev->pathdict, "closePath", JM_BOOL(0)); } diff --git a/src_classic/helper-devices.i b/src_classic/helper-devices.i index 95902ebcf..911f0e742 100644 --- a/src_classic/helper-devices.i +++ b/src_classic/helper-devices.i @@ -453,7 +453,7 @@ jm_lineart_stroke_path(fz_context *ctx, fz_device *dev_, const fz_path *path, DICT_SETITEMSTR_DROP(dev_pathdict, "color", jm_lineart_color(ctx, colorspace, color)); DICT_SETITEM_DROP(dev_pathdict, dictkey_width, Py_BuildValue("f", dev_pathfactor * stroke->linewidth)); DICT_SETITEMSTR_DROP(dev_pathdict, "lineCap", Py_BuildValue("iii", stroke->start_cap, stroke->dash_cap, stroke->end_cap)); - DICT_SETITEMSTR_DROP(dev_pathdict, "lineJoin", Py_BuildValue("i", stroke->linejoin)); + DICT_SETITEMSTR_DROP(dev_pathdict, "lineJoin", Py_BuildValue("f", (float)stroke->linejoin)); if (!PyDict_GetItemString(dev_pathdict, "closePath")) { DICT_SETITEMSTR_DROP(dev_pathdict, "closePath", JM_BOOL(0)); } From f0edd28c9ee55533e41760578e7a54efa4f96e51 Mon Sep 17 00:00:00 2001 From: sakamotch Date: Fri, 27 Mar 2026 01:38:04 +0900 Subject: [PATCH 3/4] Use determinant-based pathfactor and remove src_classic changes Use sqrt(abs(a*d - b*c)) for pathfactor calculation as suggested in review. The determinant correctly represents how the matrix scales areas, so its square root gives a meaningful scalar scale. Remove src_classic/helper-devices.i changes since src_classic/ is no longer used. --- src/__init__.py | 7 +------ src/extra.i | 9 +-------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 4472de089..ace57d1d0 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -23194,12 +23194,7 @@ def jm_lineart_stroke_path( dev, ctx, path, stroke, ctm, colorspace, color, alph #log(f'{dev.pathdict=} {dev.clips=}') try: assert isinstance( ctm, mupdf.fz_matrix) - scale = math.sqrt(ctm.a ** 2 + ctm.b ** 2) - if scale < 1e-9: - scale = math.sqrt(ctm.c ** 2 + ctm.d ** 2) - if scale < 1e-9: - scale = 1.0 - dev.pathfactor = scale + dev.pathfactor = math.sqrt(abs(ctm.a * ctm.d - ctm.b * ctm.c)) dev.ctm = mupdf.FzMatrix( ctm) # fz_concat(ctm, dev_ptm); dev.path_type = trace_device_STROKE_PATH diff --git a/src/extra.i b/src/extra.i index b19aa83f4..6ce80c481 100644 --- a/src/extra.i +++ b/src/extra.i @@ -2719,14 +2719,7 @@ jm_lineart_stroke_path(fz_context *ctx, fz_device *dev_, const fz_path *path, jm_lineart_device *dev = (jm_lineart_device *)dev_; //printf("extra.jm_lineart_stroke_path(): dev->seqno=%zi\n", dev->seqno); int i; - { - 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; - } + dev->pathfactor = sqrtf(fabsf(ctm.a * ctm.d - ctm.b * ctm.c)); dev->ctm = ctm; // fz_concat(ctm, trace_device_ptm); dev->path_type = STROKE_PATH; From 3cf53a99bc7394723083e85470ae403d600ab1c7 Mon Sep 17 00:00:00 2001 From: sakamotch Date: Fri, 27 Mar 2026 02:26:02 +0900 Subject: [PATCH 4/4] Revert src_classic/helper-devices.i changes --- src_classic/helper-devices.i | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src_classic/helper-devices.i b/src_classic/helper-devices.i index 911f0e742..fb194f23e 100644 --- a/src_classic/helper-devices.i +++ b/src_classic/helper-devices.i @@ -433,13 +433,9 @@ jm_lineart_stroke_path(fz_context *ctx, fz_device *dev_, const fz_path *path, jm_lineart_device *dev = (jm_lineart_device *)dev_; PyObject *out = dev->out; int i; - { - 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; + dev_pathfactor = 1; + if (fz_abs(ctm.a) == fz_abs(ctm.d)) { + dev_pathfactor = fz_abs(ctm.a); } trace_device_ctm = ctm; // fz_concat(ctm, trace_device_ptm); path_type = STROKE_PATH; @@ -453,7 +449,7 @@ jm_lineart_stroke_path(fz_context *ctx, fz_device *dev_, const fz_path *path, DICT_SETITEMSTR_DROP(dev_pathdict, "color", jm_lineart_color(ctx, colorspace, color)); DICT_SETITEM_DROP(dev_pathdict, dictkey_width, Py_BuildValue("f", dev_pathfactor * stroke->linewidth)); DICT_SETITEMSTR_DROP(dev_pathdict, "lineCap", Py_BuildValue("iii", stroke->start_cap, stroke->dash_cap, stroke->end_cap)); - DICT_SETITEMSTR_DROP(dev_pathdict, "lineJoin", Py_BuildValue("f", (float)stroke->linejoin)); + DICT_SETITEMSTR_DROP(dev_pathdict, "lineJoin", Py_BuildValue("f", dev_pathfactor * stroke->linejoin)); if (!PyDict_GetItemString(dev_pathdict, "closePath")) { DICT_SETITEMSTR_DROP(dev_pathdict, "closePath", JM_BOOL(0)); }