How to minimize "very long vector path" in android vector drawable files
Asked Answered
C

1

7

How to minimize very long vector path in vector drawable files using avocado optimizer tool.

I have tried using svg editor, but vector path is still long, so it's getting warnings as very long vector path:

"Very long vector path (7985 characters), which is bad for performance. Considering reducing precision, removing minor details or rasterizing vector." in the layout resource file.

<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="200dp" android:viewportHeight="500">
    <path android:fillColor="#e0e0e0" android:pathData="M304,247.48c0.43,-0.67 -1.2,-2.29 -1.21,-3s0.14,-0.3 0.36,-1.46 -1.26,-0.61 -2,-1.72 0.22,-0.64 -0.3,-1.39 -2.34,-0.15 -2.82,-0.53 0,-1.2 -1.08,-1.88 -2.48,0.86 -3.4,0.53 -0.91,-1.06 -1.74,-0.74 -1.65,1.57 -3.38,1.64 -2.34,0.41 -2.34,1.28 -0.07,1.48 -1.67,2.62 -0.89,2.65 -0.13,3.86 -0.83,1.65 -1.06,2.55 1.89,2 2,2.89 -1,2.08 0.82,3.43 1.81,0 2.64,0.75 2.86,2.34 3.59,2a4.68,4.68 0,0 1,2.35 -0.51c0.53,0.15 2.83,0.06 3,-1.06s1.48,-1.57 2.3,-1.47 0.9,-2.13 1.07,-2.81 1.34,-0.38 2.05,-1.3 -0.33,-1.79 -0.23,-2.23S303.52,248.15 304,247.48ZM298.79,248.74c-0.84,0.39 -0.75,0.8 -0.8,1.72s-1.4,1.79 -1.7,1.51 -0.76,0.1 -1.17,0.78 -1.77,1.3 -2.22,0.53 -0.9,-0.69 -1.8,-0.76a1.48,1.48 0,0 1,-1.43 -1.47c0,-0.71 0.3,-0.52 -1.06,-1.52s-1.13,-1.47 -0.75,-2a2.19,2.19 0,0 0,0.38 -2c-0.23,-1 0.27,-1.67 1.25,-1.62s0.65,-1.1 1.11,-1.5 0.65,0.2 1.7,-0.25 1,0.74 2.11,0.75 1.21,-0.88 1.88,-0.44 0,1.33 1.11,1.75 1.31,0.31 1.38,1.08 -0.45,1.7 0,2.11S299.58,248.34 298.74,248.74Z"/>
</vector>

Can anyone help me about installation of avocado vector optimizer tool.

Cosmorama answered 3/11, 2022 at 5:58 Comment(3)
Can you share a code example or link? I guess by "long" you mean a long/complex pathdata d attribute? This post might be helpful “Very Long Vector Path” issues… and where to find them.Upanchor
#74287406Cosmorama
Thanks for your reply... i have added specific tags, but its didn't take that specific tag, since its need more reputations. hence i have added another related tag options. Then, i have used svg-path-editor. But it didn't minify my pathdata outputsCosmorama
U
7

Provided, your vector drawable markup is correct and valid you can try this
java script based svg optimizer:

/**
 * decompose shorthands to "longhand" commands:
 * H, V, S, T => L, L, C, Q
 * reversed method: pathDataToShorthands()
 */
function pathDataToLonghands(pathData) {
  pathData = pathDataToAbsolute(pathData);
  let pathDataLonghand = [];
  let comPrev = {
    type: "M",
    values: pathData[0].values
  };
  pathDataLonghand.push(comPrev);

  for (let i = 1; i < pathData.length; i++) {
    let com = pathData[i];
    let {
      type,
      values
    } = com;
    let valuesL = values.length;
    let valuesPrev = comPrev.values;
    let valuesPrevL = valuesPrev.length;
    let [x, y] = [values[valuesL - 2], values[valuesL - 1]];
    let cp1X, cp1Y, cp2X, cp2Y;
    let [prevX, prevY] = [
      valuesPrev[valuesPrevL - 2],
      valuesPrev[valuesPrevL - 1]
    ];
    switch (type) {
      case "H":
        comPrev = {
          type: "L",
          values: [values[0], prevY]
        };
        break;
      case "V":
        comPrev = {
          type: "L",
          values: [prevX, values[0]]
        };
        break;
      case "T":
        [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
        [prevX, prevY] = [
          valuesPrev[valuesPrevL - 2],
          valuesPrev[valuesPrevL - 1]
        ];
        // new control point
        cpN1X = prevX + (prevX - cp1X);
        cpN1Y = prevY + (prevY - cp1Y);
        comPrev = {
          type: "Q",
          values: [cpN1X, cpN1Y, x, y]
        };
        break;
      case "S":
        [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
        [cp2X, cp2Y] =
        valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];
        [prevX, prevY] = [
          valuesPrev[valuesPrevL - 2],
          valuesPrev[valuesPrevL - 1]
        ];
        // new control points
        cpN1X = 2 * prevX - cp2X;
        cpN1Y = 2 * prevY - cp2Y;
        cpN2X = values[0];
        cpN2Y = values[1];
        comPrev = {
          type: "C",
          values: [cpN1X, cpN1Y, cpN2X, cpN2Y, x, y]
        };

        break;
      default:
        comPrev = {
          type: type,
          values: values
        };
    }
    pathDataLonghand.push(comPrev);
  }
  return pathDataLonghand;
}

/**
 * apply shorthand commands if possible
 * L, L, C, Q => H, V, S, T
 */
function pathDataToShorthands(pathData) {
  pathData = pathDataToAbsolute(pathData);
  let comShort = {
    type: "M",
    values: pathData[0].values
  };
  let pathDataShorts = [comShort];
  for (let i = 1; i < pathData.length; i++) {
    let com = pathData[i];
    let comPrev = pathData[i - 1];
    let {
      type,
      values
    } = com;
    let valuesL = values.length;
    let valuesPrev = comPrev.values;
    let valuesPrevL = valuesPrev.length;
    let [x, y] = [values[valuesL - 2], values[valuesL - 1]];
    let cp1X, cp1Y, cp2X, cp2Y;
    let [prevX, prevY] = [
      valuesPrev[valuesPrevL - 2],
      valuesPrev[valuesPrevL - 1]
    ];
    let val0R, cpN1XR, val1R, cpN1YR, cpN1X, cpN1Y, cpN2X, cpN2Y, prevXR, prevYR;

    switch (type) {
      case "L":
        [val0R, prevXR, val1R, prevYR] = [
          values[0],
          prevX,
          values[1],
          prevY
        ].map((val) => {
          return +(val * 2).toFixed(1);
        });

        if (prevYR == val1R && prevXR !== val0R) {
          comShort = {
            type: "H",
            values: [values[0]]
          };
        } else if (prevXR == val0R && prevYR !== val1R) {
          comShort = {
            type: "V",
            values: [values[1]]
          };
        } else {
          comShort = com;
        }
        break;
      case "Q":
        [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
        [prevX, prevY] = [
          valuesPrev[valuesPrevL - 2],
          valuesPrev[valuesPrevL - 1]
        ];
        // Q control point
        cpN1X = prevX + (prevX - cp1X);
        cpN1Y = prevY + (prevY - cp1Y);

        /**
         * control points can be reflected
         */
        [val0R, cpN1XR, val1R, cpN1YR] = [
          values[0],
          cpN1X,
          values[1],
          cpN1Y
        ].map((val) => {
          return +val.toFixed(1);
        });

        if (val0R == cpN1XR && val1R == cpN1YR) {
          comShort = {
            type: "T",
            values: [x, y]
          };
        } else {
          comShort = com;
        }
        break;
      case "C":
        [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
        [cp2X, cp2Y] =
        valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];
        [prevX, prevY] = [
          valuesPrev[valuesPrevL - 2],
          valuesPrev[valuesPrevL - 1]
        ];
        // C control points
        cpN1X = 2 * prevX - cp2X;
        cpN1Y = 2 * prevY - cp2Y;
        cpN2X = values[2];
        cpN2Y = values[3];

        /**
         * control points can be reflected
         */
        [val0R, cpN1XR, val1R, cpN1YR] = [
          values[0],
          cpN1X,
          values[1],
          cpN1Y
        ].map((val) => {
          return +val.toFixed(1);
        });

        if (val0R == cpN1XR && val1R == cpN1YR) {
          comShort = {
            type: "S",
            values: [cpN2X, cpN2Y, x, y]
          };
        } else {
          comShort = com;
        }
        break;
      default:
        comShort = {
          type: type,
          values: values
        };
    }
    pathDataShorts.push(comShort);
  }
  return pathDataShorts;
}

/**
 * dependancy: Jarek Foks's pathdata polyfill
 * github: https://github.com/jarek-foksa/path-data-polyfill
 */
function pathDataToRelative(pathData, decimals = -1) {
  let M = pathData[0].values;
  let x = M[0],
    y = M[1],
    mx = x,
    my = y;
  for (let i = 1; i < pathData.length; i++) {
    let cmd = pathData[i];
    let type = cmd.type;
    let typeRel = type.toLowerCase();
    let values = cmd.values;

    // is absolute
    if (type != typeRel) {
      type = typeRel;
      cmd.type = type;
      // check current command types
      switch (typeRel) {
        case "a":
          values[5] = +(values[5] - x);
          values[6] = +(values[6] - y);
          break;
        case "v":
          values[0] = +(values[0] - y);
          break;
        case "m":
          mx = values[0];
          my = values[1];
        default:
          // other commands
          if (values.length) {
            for (let v = 0; v < values.length; v++) {
              // even value indices are y coordinates
              values[v] = values[v] - (v % 2 ? y : x);
            }
          }
      }
    }
    // is already relative
    else {
      if (cmd.type == "m") {
        mx = values[0] + x;
        my = values[1] + y;
      }
    }
    let vLen = values.length;
    switch (type) {
      case "z":
        x = mx;
        y = my;
        break;
      case "h":
        x += values[vLen - 1];
        break;
      case "v":
        y += values[vLen - 1];
        break;
      default:
        x += values[vLen - 2];
        y += values[vLen - 1];
    }

    // round coordinates
    if (decimals >= 0) {
      cmd.values = values.map((val) => {
        return +val.toFixed(decimals);
      });
    }
  }
  // round M (starting point)
  if (decimals >= 0) {
    [M[0], M[1]] = [+M[0].toFixed(decimals), +M[1].toFixed(decimals)];
  }
  return pathData;
}

function pathDataToAbsolute(pathData, decimals = -1) {
  let M = pathData[0].values;
  let x = M[0],
    y = M[1],
    mx = x,
    my = y;

  for (let i = 1; i < pathData.length; i++) {
    let cmd = pathData[i];
    let type = cmd.type;
    let typeAbs = type.toUpperCase();
    let values = cmd.values;

    if (type != typeAbs) {
      type = typeAbs;
      cmd.type = type;

      switch (typeAbs) {
        case "A":
          values[5] = +(values[5] + x);
          values[6] = +(values[6] + y);
          break;

        case "V":
          values[0] = +(values[0] + y);
          break;

        case "H":
          values[0] = +(values[0] + x);
          break;

        case "M":
          mx = +values[0] + x;
          my = +values[1] + y;

        default:
          // other commands
          if (values.length) {
            for (let v = 0; v < values.length; v++) {
              // even value = y coordinates
              values[v] = values[v] + (v % 2 ? y : x);
            }
          }
      }
    }
    // is already absolute
    let vLen = values.length;
    switch (type) {
      case "Z":
        x = +mx;
        y = +my;
        break;
      case "H":
        x = values[0];
        break;
      case "V":
        y = values[0];
        break;
      case "M":
        mx = values[vLen - 2];
        my = values[vLen - 1];

      default:
        x = values[vLen - 2];
        y = values[vLen - 1];
    }

    // round coordinates
    if (decimals >= 0) {
      cmd.values = values.map((val) => {
        return +val.toFixed(decimals);
      });
    }
  }
  // round M (starting point)
  if (decimals >= 0) {
    [M[0], M[1]] = [+M[0].toFixed(decimals), +M[1].toFixed(decimals)];
  }
  return pathData;
}

function setPathDataOpt(path, pathData, decimals) {
  let d = "";
  pathData.forEach((com, c) => {
    let type = com["type"];
    let values = com["values"];

    if (decimals >= 0) {
      values.forEach(function(val, v) {
        pathData[c]["values"][v] = +val.toFixed(decimals);
      });
    }
    d += `${type}${values.join(" ")}`;
  });
  d = d.replace(/\s\s+/g, " ", "").replaceAll(",", " ").replaceAll(" -", "-");
  path.setAttribute("d", d);
}

function roundPathData(pathData, decimals = -1) {
  pathData.forEach((com, c) => {
    if (decimals >= 0) {
      com.values.forEach((val, v) => {
        pathData[c].values[v] = +val.toFixed(decimals);
      });
    }
  });
  return pathData;
}

function pathDataToQuadratic(pathData, width, height, precision = 0.1) {
  let newPathData = [pathData[0]];
  for (let i = 1; i < pathData.length; i++) {
    let comPrev = pathData[i - 1];
    let com = pathData[i];
    let [type, values] = [com.type, com.values];
    let [typePrev, valuesPrev] = [comPrev.type, comPrev.values];
    let valuesPrevL = valuesPrev.length;
    let [xPrev, yPrev] = [
      valuesPrev[valuesPrevL - 2],
      valuesPrev[valuesPrevL - 1]
    ];

    // convert C to Q
    if (type == "C") {
      let q = cubicToQuad(
        xPrev,
        yPrev,
        values[0],
        values[1],
        values[2],
        values[3],
        values[4],
        values[5],
        precision
      );

      let p0 = {
          x: xPrev,
          y: yPrev
        },
        cp1 = {
          x: values[0],
          y: values[1]
        },
        cp2 = {
          x: values[2],
          y: values[3]
        },
        p = {
          x: values[4],
          y: values[5]
        };

      /**
       * convert to quadratic
       * if curve is rather flat
       * or shorter than 1/20 width/height of path
       */
      let angle = getAngleABC(cp1, cp2, p);
      let segLength = getDistance(p0, p);

      if (segLength < (width + height) / 20 || angle < 20) {
        for (let j = 2; j < q.length; j += 4) {
          newPathData.push({
            type: "Q",
            values: [q[j], q[j + 1], q[j + 2], q[j + 3]].map((val) => {
              return +val.toFixed(9);
            })
          });
        }
      } else {
        newPathData.push(com);
      }
    } else {
      newPathData.push(com);
    }
  }
  return newPathData;
}

// get distance
function getDistance(p1, p2) {
  if (Array.isArray(p1)) {
    p1.x = p1[0];
    p1.y = p1[1];
  }
  if (Array.isArray(p2)) {
    p2.x = p2[0];
    p2.y = p2[1];
  }
  let [x1, y1, x2, y2] = [p1.x, p1.y, p2.x, p2.y];
  let y = x2 - x1;
  let x = y2 - y1;
  return Math.sqrt(x * x + y * y);
}

// get angle helper
function getAngle(p1, p2) {
  let angle = (Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180) / Math.PI;
  //console.log(angle);
  return angle;
}

// get angle between 3 points helper
function getAngleABC(A, B, C) {
  let BA = Math.sqrt(Math.pow(A.x - B.x, 2) + Math.pow(A.y - B.y, 2));
  let AC = Math.sqrt(Math.pow(A.x - C.x, 2) + Math.pow(A.y - C.y, 2));
  let BC = Math.sqrt(Math.pow(C.x - B.x, 2) + Math.pow(C.y - B.y, 2));
  let angle =
    (Math.acos((AC * AC + BA * BA - BC * BC) / (2 * AC * BA)) * 180) / Math.PI;
  return angle;
}
* {
  box-sizing: border-box;
}

body {
  font-family: sans-serif;
}

svg {
  height: 20em;
  width: auto;
  max-width: 100%;
  margin: 0.3em;
  overflow: visible;
  border: 1px solid #ccc;
}

textarea {
  width: 100%;
  min-height: 30em;
  font-family: monospace;
  white-space: pre-wrap;
}

.flex {
  display: flex;
  gap: 1em;
}

.col {
  flex: 1 1 auto;
  width: 100%;
}
<label>Round to n decimals<input id="round" class="inputs" type="number" min="-1" max="8" value="1"></label>
<label><input name="absoluteRelative" class="inputs absoluteRelative" type="radio" min="-1" max="8" value="absolute">
  Absolute </label>
<label><input name="absoluteRelative" class="inputs absoluteRelative" type="radio" min="-1" max="8" value="relative" checked> Relative </label>
<label> <input id="shorthands" class="inputs" type="checkbox" value="1" checked> Apply shorthands</label>
<label> <input id="toQuadratic" class="inputs" type="checkbox" value="1">small or flat curves to quadratic</label>
<label> <input id="crop" class="inputs" type="checkbox" value="1" checked> Crop and center</label>

<div class="flex">
  <div class="col">
    <h3>Vector drawable input</h3>
    <textarea id="svgIn" class="inputs filesize">
        <vector android:height="24dp" android:tint="#FFFFFF"
        android:viewportHeight="24" android:viewportWidth="24"
        android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
        <path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
    </vector>
            </textarea>
    <svg id="svgPreview">
      <path id="pathPrev" />
    </svg>
  </div>
  <div class="col">
    <h3>Vector drawable: relative, shorthands, rounded</h3>
    <textarea id="svgOut" class="filesize"></textarea>
    <p id="pathSize"></p>
    <svg id="svgNew">
      <path id="pathNew" />
    </svg>
  </div>
  <div id="vectorDrawable"></div>
</div>

<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@latest/path-data-polyfill.min.js">
</script>
<script src="https://cdn.jsdelivr.net/gh/herrstrietzel/svgHelpers@main/js/pathData.cubic2quad.js">
</script>



<script>
  let decimals = +round.value;
  let inputs = document.querySelectorAll('.inputs');
  inputs.forEach(input => {
    input.addEventListener('input', e => {
      updateSVG()
    })
  })
  window.addEventListener('DOMContentLoaded', e => {
    updateSVG();
  })

  function updateSVG() {
    let useRelative = document.querySelector('.absoluteRelative:checked').value;
    decimals = +round.value;
    let svgCode = cleanStr(svgIn.value);
    //drawable to svg
    let ns = 'http://schemas.android.com/apk/res/android';
    let xml = new DOMParser();
    let doc = xml.parseFromString(svgCode, "application/xml");
    let vector = doc.querySelector('vector');
    vectorDrawable.appendChild(vector);
    let vectorPath = vector.querySelector('path');
    let vectorD = vectorPath.getAttribute('android:pathData');
    // apply path data to preview svg
    pathPrev.setAttribute('d', vectorD);
    let pathData = pathPrev.getPathData();
    let viewportWidth = vector.getAttribute('android:viewportWidth');
    let viewportHeight = vector.getAttribute('android:viewportHeight');
    svgPreview.setAttribute('viewBox', [0, 0, viewportWidth, viewportHeight].join(' '));
    let {
      x,
      y,
      width,
      height
    } = pathPrev.getBBox();
    // crop and center icon
    let cropAndCenter = crop.checked ? true : false;
    if (cropAndCenter) {
      // apply rounded viewport height divisable by 4
      viewportHeight = Math.ceil(height / 4) * 4;
      viewportWidth = Math.ceil(width / 4) * 4;
      // add necessary attributes
      vector.setAttributeNS(ns, 'android:viewportWidth', viewportWidth);
      vector.setAttributeNS(ns, 'android:viewportHeight', viewportHeight);
      vector.setAttributeNS(ns, 'android:width', viewportWidth + 'dp');
      vector.setAttributeNS(ns, 'android:height', viewportHeight + 'dp');
      pathData = pathDataToRelative(pathData);
      let xN = pathData[0].values[0] - x;
      let yN = pathData[0].values[1] - y;
      let offsetX = (viewportWidth - width) / 2;
      let offsetY = (viewportHeight - height) / 2;
      pathData[0].values = [xN + offsetX, yN + offsetY]
    }
    // pre rounding for low precision
    if (decimals < 2) {
      pathData = roundPathData(pathData, decimals)
    }
    /**
     * cubic curves to quadratic
     */
    let convertToQuad = toQuadratic.checked ? true : false;
    if (convertToQuad) {
      pathData = pathDataToQuadratic(pathDataToLonghands(pathData), width, height);
    }
    let useShorthands = shorthands.checked ? true : false;
    if (useShorthands) {
      pathData = pathDataToShorthands(pathData);
    } else {
      pathData = pathDataToLonghands(pathData);
    }
    if (useRelative === 'relative') {
      pathData = pathDataToRelative(pathData);
    } else {
      pathData = pathDataToAbsolute(pathData);
    }
    setPathDataOpt(pathNew, pathData, decimals)
    let dMin = pathNew.getAttribute('d');
    // update drawable
    vectorPath.setAttributeNS(ns, 'android:pathData', dMin);
    let serializer = new XMLSerializer();
    let xmlStr = serializer.serializeToString(vector).replace(/>\s+/g, '>').replaceAll('><', '>\n<');
    pathSize.textContent = filesize(dMin) + ' Bytes (pathdata)';
    svgOut.value = xmlStr;
    svgNew.setAttribute('viewBox', [0, 0, viewportWidth, viewportHeight].join(' '));
    showFileSize();
  }

  function showFileSize() {
    let textareas = document.querySelectorAll('.filesize');
    textareas.forEach(text => {
      let pSize = text.nextElementSibling;
      if (!pSize.classList.contains('pFileSize')) {
        pSize = document.createElement('p');
        pSize.classList.add('pFileSize');
        text.parentNode.insertBefore(pSize, text.nextElementSibling);
      }
      let size = new Blob([text.value]).size;
      size = +size.toFixed(3);
      pSize.textContent = size + ' Bytes'
    })
  }

  function filesize(str) {
    let size = new Blob([str]).size;
    return +size.toFixed(3);
  }

  function cleanStr(str) {
    str = str.
    replace(/[(\t\r\n]+/g, '').
    replace(/\s{2,}/g, ' ').
    trim();
    return str;
  }

  function adjustViewBox(svg) {
    let bb = svg.getBBox();
    let bbVals = [bb.x, bb.y, bb.width, bb.height].map((val) => {
      return +val.toFixed(2);
    });
    let maxBB = Math.max(...bbVals);
    let [x, y, width, height] = bbVals;
    svg.setAttribute("viewBox", [x, y, width, height].join(" "));
  }
</script>

Codepen example

How it works

  1. vector drawable is appended to DOM via DOMParser()
  2. <vector> is converted to a <svg>
  3. svg pathData is parsed using getPathData() (requires Jarek Foksa's polyfill)
  4. pathdata is minified by:
    4.1 rounding coordinates to n decimal precision
    4.2 commands are converted to relative (custom helper method pathDataToRelative(pathData))
    4.3 commands are converted to shorthand commands (if applicable via pathDataToShorthands() helper)
  5. optional cropping and centering to a suitable viewPort
  6. optimized <svg><path> data is applied to <vector><path> element

Apparently the max byteSize for a vector drawable element in Android Studio is 800 bytes as mentioned in this article: “Very Long Vector Path” issues… and where to find them.

If you can't reduce it to <=800 Bytes by the aforementioned optimisations: You need to refactor your <path> in a vector editor like inkscape, Adobe Illusrator etc. E.g by using a simplifying function or by manually deleting unnecessary points.

Upanchor answered 5/11, 2022 at 19:11 Comment(5)
AWESOME interactive tool; got mysettings icon pathdata 905 -> 648, while looking basically the same. Did get one visual bug with "apply shortcuts" though, caused a single node to jump position, which I'm not sure is supposed to happen? (would have saved another 50 bytes)Ectosarc
My pleasure. Feel free to post/link an example pathdata code where the optimization failed as a comment. In fact shorthand calculations as well as coordinate rounding can be a bit tricky – in case of lower floating point precision a pre-rounding of coordinates before "toRelative" or "shorthand" recalculations can improve the result. Worth noting: there are limitation shrinking the precision to integers as explained hereUpanchor
Thanks! Here's the commit where I replaced the old path with 'your' optimised result: github.com/nicolasbrailo/PianOli/commit/… It's a gear icon, and the artifact happens on the gear tooth at the 11-o-clock position. Note that the "after" result also includes some manual path tweaking in inkscape (converted some corner nodes to smooth), before I fed it into your codepen.Ectosarc
I've improved the optimizer: the shorthand detection was too aggressive. Besides, you can further minify the output by converting small or short curves to quadratic beziersUpanchor
Heroic! You fixed the artifact AND added a new feature! 560 bytes now (at precision 1) and I can't tell a visual difference from the original! You truly are a vector-hacker! I'll have to look up "quadratic beziers", but enabling it saves 110 bytes, so I can completely forego manual tweaking. MASSIVE timesaver! I wish I could do more than upvote, but I can't hand out bounties at my replevel..Ectosarc

© 2022 - 2025 — McMap. All rights reserved.