diff --git a/bridge_subpaths.inx b/bridge_subpaths.inx
new file mode 100644
index 0000000000000000000000000000000000000000..20cf07f8ad464cca75b19ee1d285d0acac70f262
--- /dev/null
+++ b/bridge_subpaths.inx
@@ -0,0 +1,43 @@
+
+
+
+
+
+ Bridge Subpaths
+ org.inkscape.path.bridge_subpaths
+
+
+ true
+ false
+
+
+
+
+
+
+ path
+
+
+
+
+
+
+
+
diff --git a/bridge_subpaths.py b/bridge_subpaths.py
new file mode 100644
index 0000000000000000000000000000000000000000..7f26a332f6d862a8bb1dae91f91ef1a4c82f7795
--- /dev/null
+++ b/bridge_subpaths.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python
+"""
+Inkscape extension to join the selected paths.
+Tested with Inkscape version 1.4
+
+Based on https://github.com/Shriinivas/inkscapejoinpaths/tree/520ccf3d7100cbfa0c15c660b4d109672b35e187
+by Shrinivas Kulkarni (khemadeva@gmail.com)
+
+SPDX-FileCopyrightText:
+SPDX-FileCopyrightText: 2025 Intevation GmbH
+SPDX-License-Identifier: GPL-2.0-or-later
+"""
+
+import logging
+import inkex
+from inkex import PathElement, Path, Boolean
+from inkex.paths import ZoneClose
+from inkex.bezier import pointdistance
+from inkex.localization import inkex_gettext as _
+
+logger = logging.getLogger(__file__)
+
+
+class BridgeSubpaths(inkex.EffectExtension):
+ def add_arguments(self, pars):
+ pars.add_argument("--delete_orig", type=Boolean, default=False)
+ pars.add_argument("--sort_subpaths", type=Boolean, default=True)
+ pars.add_argument("--tab", help="Select Options")
+
+ def effect(self):
+ paths = self.svg.selection.get(PathElement)
+ if len(paths) != 1:
+ inkex.errormsg(_("Please only select one path (with several subpaths)."))
+ return
+
+ delete_orig = self.options.delete_orig
+ sort_subpaths = self.options.sort_subpaths
+
+ ordered_paths = self.order_paths(paths, sort_subpaths)
+ bridged_path = self.bridge_paths(ordered_paths)
+
+ new_path = PathElement()
+ self.copy_style(paths[0], new_path)
+
+ layer = self.svg.get_current_layer()
+ bridged_path.transform(-layer.composed_transform()) # Just in case
+ new_path.path = bridged_path
+
+ layer.add(new_path)
+
+ if delete_orig:
+ for p in paths:
+ p.getparent().remove(p)
+
+ def copy_style(self, source_path, target_path):
+ if source_path.style:
+ target_path.style = source_path.style
+
+ style_attributes = [
+ "fill",
+ "stroke",
+ "stroke-width",
+ "opacity",
+ "fill-opacity",
+ "stroke-opacity",
+ ]
+ for attr in style_attributes:
+ if attr in source_path.attrib:
+ target_path.set(attr, source_path.get(attr))
+
+ def closest_nodes(self, subpath1, subpath2):
+ """Return the points from each subpaths which are nearest.
+
+ Using a brute force algorithm, as it works all the time. See
+ https://www.crunchydata.com/blog/inside-postgis-calculating-distance
+ for potentially more clever algorithms (that work on edges not
+ on nodes like this one).
+ """
+
+ def min_dist_to_path(basis_node, path):
+ """Return tuple (point, distance) where distance is smallest."""
+ node_distances = map(
+ lambda node: (
+ node,
+ pointdistance(basis_node.end_point(0, 0), node.end_point(0, 0)),
+ ),
+ path,
+ )
+
+ return min(node_distances, key=lambda t: t[1])
+
+ nodes_distances = list(
+ map(lambda node: (node,) + min_dist_to_path(node, subpath2), subpath1)
+ )
+
+ logger.debug(f"{nodes_distances=}")
+ # two subpath that are closed will get a zero distance ZoneClose
+ # to ZoneClose, so we eliminate it
+ nodes_distances_no_z = filter(
+ lambda t: not isinstance(t[0], ZoneClose), nodes_distances
+ )
+
+ return min(nodes_distances_no_z, key=lambda t: t[2])
+
+ def order_paths(self, pathElems, sort_subpaths):
+ """Prepare pathElems as subpaths for bridging.
+
+ Internally transfer pathElems to_absolute() so that point operations
+ like distance calculations work the same way.
+ """
+ sub_paths = [
+ sub.transform(p.composed_transform())
+ for p in pathElems
+ for sub in p.path.to_absolute().break_apart()
+ ]
+
+ if len(sub_paths) == 1:
+ inkex.errormsg(_("Only found one subpath, nothing to bridge."))
+
+ if sort_subpaths:
+ ordered = [sub_paths[0]]
+ remaining = sub_paths[1:]
+ while remaining:
+ end_point = list(ordered[-1].end_points)[-1]
+ cmp_paths = []
+ for ppath in remaining:
+ pts = list(ppath.end_points)
+ cmp_paths.append([ppath, pts[0], True])
+ cmp_paths.append([ppath, pts[-1], False])
+ nearest = min(cmp_paths, key=lambda p: pointdistance(end_point, p[1]))
+ next_path = nearest[0] if nearest[2] else nearest[0].reverse()
+ ordered.append(next_path)
+ remaining.remove(nearest[0])
+ return ordered
+
+ return sub_paths
+
+ def bridge_paths(self, paths) -> Path:
+ bridged = paths[0]
+ for i, subpath in enumerate(paths[1:]):
+ n1, n2, d = self.closest_nodes(paths[i], subpath)
+ p1 = n1.end_point(0, 0)
+ p2 = n2.end_point(0, 0)
+ bridged.extend(Path(f"M {p1[0]},{p1[1]} L {p2[0]},{p2[1]}"))
+ logger.debug(f"added line with {p1=} {p2=} {d=} ")
+ bridged.extend(subpath)
+
+ return bridged
+
+
+if __name__ == "__main__":
+ # comment the next 2 lines to enable writing a debuging log for analysis:
+ # logging.basicConfig(
+ # filename='inkscape_extension.log', level=logging.DEBUG)
+ logger.info("logging started")
+ BridgeSubpaths().run()
diff --git a/icons/org.inkscape.path.bridge_subpaths.svg b/icons/org.inkscape.path.bridge_subpaths.svg
new file mode 100644
index 0000000000000000000000000000000000000000..17f2a5a1383b50e6c1592cda01c3604f8081f86c
--- /dev/null
+++ b/icons/org.inkscape.path.bridge_subpaths.svg
@@ -0,0 +1,58 @@
+
+
+
+
+
diff --git a/tests/data/refs/bridge_subpaths__--sort_subpaths__False__--delete_orig__True__--id__path4-7.out b/tests/data/refs/bridge_subpaths__--sort_subpaths__False__--delete_orig__True__--id__path4-7.out
new file mode 100644
index 0000000000000000000000000000000000000000..6854851fa55916716c8a0a13c73c6b80b7f32f42
--- /dev/null
+++ b/tests/data/refs/bridge_subpaths__--sort_subpaths__False__--delete_orig__True__--id__path4-7.out
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/tests/data/refs/bridge_subpaths__--sort_subpaths__True__--delete_orig__False__--id__path4-7.out b/tests/data/refs/bridge_subpaths__--sort_subpaths__True__--delete_orig__False__--id__path4-7.out
new file mode 100644
index 0000000000000000000000000000000000000000..efa7bea100434f3e3f9c6d28d0ea1182ef161009
--- /dev/null
+++ b/tests/data/refs/bridge_subpaths__--sort_subpaths__True__--delete_orig__False__--id__path4-7.out
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/tests/data/svg/bridge_subpaths.svg b/tests/data/svg/bridge_subpaths.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f58d70c68eca641dfef04aaae6da79aeb4605336
--- /dev/null
+++ b/tests/data/svg/bridge_subpaths.svg
@@ -0,0 +1,56 @@
+
+
+
+
diff --git a/tests/test_bridge_subpaths.py b/tests/test_bridge_subpaths.py
new file mode 100644
index 0000000000000000000000000000000000000000..2e865874c7eb0a8ae2da4d1a34de6724336b9c6c
--- /dev/null
+++ b/tests/test_bridge_subpaths.py
@@ -0,0 +1,16 @@
+from patternalongpath import PatternAlongPath
+from inkex.tester import ComparisonMixin, TestCase
+from inkex.tester.filters import CompareNumericFuzzy, CompareWithPathSpace
+
+from bridge_subpaths import BridgeSubpaths
+from inkex.tester import ComparisonMixin, TestCase
+
+
+class TestBridgeSubpaths(ComparisonMixin, TestCase):
+ effect_class = BridgeSubpaths
+ compare_file = "svg/bridge_subpaths.svg"
+
+ comparisons = [
+ ("--sort_subpaths=True", "--delete_orig=False", "--id=path4-7"),
+ ("--sort_subpaths=False", "--delete_orig=True", "--id=path4-7"),
+ ]