How do I access path points that form a hole within a subpath?

I’m trying to access the path points circled below using the API, but can only access the points on the outside rim.

I seem to be able to access them by manually deleting the outside points in Photoshop, and then programatically accessing the inside points, but then the follow-up question would be: How would I programatically delete the outside points of an existing path while retaining the inner points?

Do you need to delete the outside path or do you just need to access the info for the inside points? Are those two separate paths or a single “compound path”? If they are the latter you can access each path inside of the subPathItems property.

In the example file shown below, the donut shape is a compound path and if I run this code, you can see where that particular path has 2 subPathItems.

const doc = app.activeDocument;

for (let i = 0; i < doc.pathItems.length; i++) {
    const pathItem = doc.pathItems[i];
    console.log(
        `Path '${pathItem.name}' has ${pathItem.subPathItems.length} sub paths.`
    );
    for (let j = 0; j < pathItem.subPathItems.length; j++) {
        const p = pathItem.subPathItems[i];
        console.log(
            `Sub path ${j} has ${pathItem.subPathItems[j].pathPoints.length} points`
        );
    }
}

Console log:

main.js:43 Path 'Test Path 1' has 2 sub paths.
main.js:48 Sub path 0 has 6 points
main.js:48 Sub path 1 has 3 points
main.js:43 Path 'Test Path 2' has 1 sub paths.
main.js:48 Sub path 0 has 4 points

I’m not sure how Photoshop builds compound paths so you may need to do a little math to determine which subPaths are enclosed inside of the other(s), but that isn’t too hard.

Let me know if this helps?

2 Likes

Thank you for your help. So I tested this out by creating a custom path and it works.

However, my use case is a path that has been converted from a text layer. For some reason the inner points don’t get included in subPathItems if the path originates from a text layer.


Okay, this is my first attempt working with path points in Photoshop but I have done something very similar in Illustrator. I have commented the code to help explain the process.

The result is an array of arrays, where the first item of the sub-array is the “parent” (outer) path and all other items are “children” paths that are fully contained within the bounds of the “parent”.

Please note, there are some edge cased this would not work perfect for, but if you are working with just fonts, it should work just fine.

I’m not sure what the end result you are seeking is, but hopefully this gets you closer to your goal. Let me know if you have any questions or issues?

function getPathBounds(path) {
    // iterate over every point in a path to get the outer bounds
    return path.pathPoints.reduce(
        (bounds, point) => {
            const [x, y] = point.anchor;
            return {
                left: Math.min(bounds.left, x),
                top: Math.min(bounds.top, y),
                right: Math.max(bounds.right, x),
                bottom: Math.max(bounds.bottom, y),
            };
        },
        {
            left: Infinity,
            top: Infinity,
            right: -Infinity,
            bottom: -Infinity,
        }
    );
}

function groupContainedPaths(paths) {
    // check if a path is inside of another path
    function isInside(inner, outer) {
        return (
            inner.left >= outer.left &&
            inner.right <= outer.right &&
            inner.top >= outer.top &&
            inner.bottom <= outer.bottom
        );
    }

    // get the outer bounds for every path
    const boundsList = paths.map((path) => ({
        path,
        bounds: getPathBounds(path),
    }));

    const result = [];
    for (let i = 0; i < boundsList.length; i++) {
        const outer = boundsList[i];
        let added = false;

        for (let j = 0; j < result.length; j++) {
            const container = result[j][0]; // first path in the group is the container

            if (isInside(outer.bounds, getPathBounds(container))) {
                result[j].push(outer.path);
                added = true;
                break;
            }
        }

        if (!added) {
            result.push([outer.path]);
        }
    }

    return result;
}

const doc = app.activeDocument;

// if you are sure none of the converted letters will have true subPaths then
// we can just extract the first (0-index) subPathItem from each path
const paths = doc.pathItems.map((pathItem) => pathItem.subPathItems[0]);

// now we need to group the paths by "parent" paths and any "childen"
// paths that are fully contained inside of the "parent" path
const groupedPaths = groupContainedPaths(paths);

console.log(groupedPaths);
1 Like

I think there’s a misunderstanding. When I call:

const paths = doc.pathItems.map((pathItem) => pathItem.subPathItems[0]);

… the inner points are not included in paths (at least for me). This only applies for paths that were created from text layers.

The problem isn’t grouping the inner points apart from the outer points, but that the inner points aren’t provided at all from the Photoshop UXP API. Have you successfully made it work in Photoshop?

Yes, it seems you have found a bug. If I create a letter “A” (like pictured) and then create a path using “Create Work Path” from the type layer flyout menu, the inner triangle shaped path points are not accessible (like you describe). Only the 8 points from the outer edge of the letter are listed when I query the path points.

If, I duplicate the path, I still can’t access those points but if I delete all of the outside points using the Direct Selection Tool the inside points then become available.

Also, if I create the work path from a selection of the letter pixels, I can access the inner points. Definitely weird behavior.

1 Like

I think in Photoshop there are in reality 3 levels of of paths.
Path → Subpath → Closed paths (DOM gets you only first closed path in subpath here because closed paths are not in DOM)

The text of one layer is usually all within one subpath divided across multiple closed paths, with few exceptions. I think underline style and maybe strikethrough would get their own subpaths.

I think there were some “corners cutting” during making of DOM to make it easier and simpler.

You should be still able to see whole path correctly if you would use low-level batchPlay. You could use Alchemist to read all points in path. Also then you could also move/upgrade closed paths into subpaths in JS object. I mean like upgrade them using batchPlay and then resume in DOM if need to.

3 Likes

From my usage, if I have “ABC”, I’ll get 3 subpaths (one for each character assuming each character only has one closed shape). Your hypothesis on corner cutting is likely correct.

How would I read the points in Alchemist?

Unfortunately, the selection method isn’t sufficient for my use-case, but great find!