Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add plugin simplifyPaths to reduce the number of points in paths and polylines while maintaining approximately the same overall shape #1698

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

MarcelRobitaille
Copy link

Fixes #411

This pull request adds a new plugin that can be used to approximate paths and polylines to further optimize SVGs. This takes advantage of Paper.js, as suggested by @jeromew.

I not find a construct in Paper.js to represent <polyline>s, but I found a way to convert these into paths. Then, the same path.simplify() method can be used.

The available parameters are as follows:

  floatPrecision: 2,  // precision to use when printing floats
  path: {
    simplifyThreshold: 2.5,  // the allowed maximum error when fitting the curves through the segment points
  },
  polyline: {
    transform: true, // Whether to convert <polyline> into <path>
    simplifyThreshold: 2.5,
  },

This plugin can have big savings even if path.simplify() is not use simplifyThreshold = null. This is because <path> is often more compact than <polyline> and because Paper.js prints the d attribute very compactly.

With this plugin, I was able to get one of my personal SVGs from 188KiB down to 40KiB. I also added some tests in the test/plugins folder.

I did notice that this plugin must come before mergePaths. Even the following code with totally mess up an SVG if it comes after mergePaths. All this code is doing is parsing a <path>'s d attribute with Paper.js, and printing it in Paper.js's format. It seems like that format is not compatible with mergePaths. To be honest, I'm not entirely sure what's going on here.

node.attributes.d = new paper.Path(node.attributes.d).exportSVG().attributes.d.value

This plugin also depends on paper and paper-jsdom. I'm not sure how to handle that either.

plugins/simplifyPaths.js Outdated Show resolved Hide resolved
@MarcelRobitaille
Copy link
Author

Thanks for reviewing @jguddas! I resolved that suggestion and rebased onto main

Copy link

@jguddas jguddas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@dtbaker
Copy link

dtbaker commented Mar 18, 2024

this would be cool to try out, any ideas if this will reach main?

@MarcelRobitaille
Copy link
Author

I resolved the merge conflicts again. Let's see where this goes.

Otherwise, it will strip out the move commands and make paths be continuous even tho they shouldn't be.

Co-authored-by: Jakob Guddas <github@jguddas.de>
@dtbaker
Copy link

dtbaker commented Mar 19, 2024

Hey @MarcelRobitaille I ended up borrowing your code and rewriting it a little. I couldn't get your one working in typescript.

The plugin file was this (I know, really rough, I have no idea how plugins works. I hard coded values instead of supporting params because I couldn't figure that out):

// contents of simplifyPaths.ts

import paper from 'paper-jsdom'
paper.setup();

const processNode = node => {
    if(node?.attributes?.d){
        var path = new paper.Path({pathData: node.attributes.d })
        path.closePath();
        path.flatten(0.5)
        path.simplify(0.001);
        var svg = path.exportSVG({asString: true})
        var matches = svg.match(/d="([^"]+)"/)
        if(matches){
            node.attributes.d = matches[1]
        }
    }
    node.children.forEach(child => {
        processNode(child)
    })
}
const fn = () => ({
    root: {
        enter: node => {
            processNode(node)
        }
    }
})


export default {
    name,
    fn
};

then I used it like so:

import {optimize} from "svgo";
import simplifyPaths from "./simplifyPaths.ts";
const result = optimize(originalSvgString, {
        multipass: true,
        plugins: [
            'preset-default',
            simplifyPaths
        ]
});
const optimizedSvgString = result.data;

however, in the end I found it was twice as fast to optimise the SVG outside of the svgo pipeline, so now I'm using paper.js as a separate post processing script after svgo does its thing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Simplify curves like Paper.js or vectorscribes "smart remove brush tool" does.
3 participants