Repository: svg/svgo
Pull request #1005 (2018-07-23 23:23:06)

SVGO: Add plugin "reusePaths".
Find duplicated <path> elements and replace them with <use> links. (disabled by default)

tigt: This maybe should check for other styling attributes than `stroke` and `fill` — it could change rendering if one of the paths has `stroke-dasharray`, for example. It probably shouldn't clone paths unless the differences fall under the attributes where it's safe for the `<use>` to differ:

* `id` and `class`
* `transform`
* Maybe some others

It also doesn't have to move the "source" `<path>` into a `<defs>` block — referencing an already-visible element with `<use> ` is perfectly okay.

Patch:
diff --git a/.svgo.yml b/.svgo.yml
index f4bd0c9..eb02b2d 100644
--- a/.svgo.yml
+++ b/.svgo.yml
@@ -62,6 +62,7 @@ plugins:
   - removeScriptElement
   - addAttributesToSVGElement
   - removeOffCanvasPaths
+  - reusePaths
 
 # configure the indent (default 4 spaces) used by `--pretty` here:
 #
diff --git a/README.md b/README.md
index 4dfcefc..5ae2482 100644
--- a/README.md
+++ b/README.md
@@ -64,6 +64,7 @@ Today we have:
 | [addAttributesToSVGElement](https://github.com/svg/svgo/blob/master/plugins/addAttributesToSVGElement.js) | adds attributes to an outer `<svg>` element (disabled by default) |
 | [removeStyleElement](https://github.com/svg/svgo/blob/master/plugins/removeStyleElement.js) | remove `<style>` elements (disabled by default) |
 | [removeScriptElement](https://github.com/svg/svgo/blob/master/plugins/removeScriptElement.js) | remove `<script>` elements (disabled by default) |
+| [reusePaths](https://github.com/svg/svgo/blob/master/plugins/reusePaths.js) | Find duplicated <path> elements and replace them with <use> links. (disabled by default) |
 
 Want to know how it works and how to write your own plugin? [Of course you want to](https://github.com/svg/svgo/blob/master/docs/how-it-works/en.md). ([동작방법](https://github.com/svg/svgo/blob/master/docs/how-it-works/ko.md))
 
diff --git a/plugins/reusePaths.js b/plugins/reusePaths.js
new file mode 100644
index 0000000..ba097da
--- /dev/null
+++ b/plugins/reusePaths.js
@@ -0,0 +1,168 @@
+/**
+ * @license
+ * The MIT License
+ *
+ * Copyright © 2012–2016 Kir Belevich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ *
+ * Лицензия MIT
+ *
+ * Copyright © 2012–2016 Кир Белевич
+ *
+ * Данная лицензия разрешает лицам, получившим копию
+ * данного
+ * программного обеспечения и сопутствующей
+ * документации
+ * (в дальнейшем именуемыми «Программное Обеспечение»),
+ * безвозмездно
+ * использовать Программное Обеспечение без
+ * ограничений, включая
+ * неограниченное право на использование, копирование,
+ * изменение,
+ * добавление, публикацию, распространение,
+ * сублицензирование
+ * и/или продажу копий Программного Обеспечения, также
+ * как и лицам,
+ * которым предоставляется данное Программное
+ * Обеспечение,
+ * при соблюдении следующих условий:
+ *
+ * Указанное выше уведомление об авторском праве и
+ * данные условия
+ * должны быть включены во все копии или значимые части
+ * данного
+ * Программного Обеспечения.
+ *
+ * ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК
+ * ЕСТЬ»,
+ * БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ
+ * ПОДРАЗУМЕВАЕМЫХ,
+ * ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ГАРАНТИЯМИ ТОВАРНОЙ
+ * ПРИГОДНОСТИ,
+ * СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И
+ * ОТСУТСТВИЯ НАРУШЕНИЙ
+ * ПРАВ. НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ
+ * НЕСУТ
+ * ОТВЕТСТВЕННОСТИ ПО ИСКАМ О ВОЗМЕЩЕНИИ УЩЕРБА, УБЫТКОВ
+ * ИЛИ ДРУГИХ
+ * ТРЕБОВАНИЙ ПО ДЕЙСТВУЮЩИМ КОНТРАКТАМ, ДЕЛИКТАМ ИЛИ
+ * ИНОМУ,
+ * ВОЗНИКШИМ ИЗ, ИМЕЮЩИМ ПРИЧИНОЙ ИЛИ СВЯЗАННЫМ С
+ * ПРОГРАММНЫМ
+ * ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ ПРОГРАММНОГО
+ * ОБЕСПЕЧЕНИЯ
+ * ИЛИ ИНЫМИ ДЕЙСТВИЯМИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ.
+ */
+
+'use strict';
+
+var JSAPI = require('../lib/svgo/jsAPI');
+
+exports.type = 'full';
+
+exports.active = false;
+
+exports.description = 'Finds <path> elements with the same d, fill, and ' +
+                      'stroke, and converts them to <use> elements ' +
+                      'referencing a single <path> def.';
+
+/**
+ * Finds <path> elements with the same d, fill, and stroke, and converts them to
+ * <use> elements referencing a single <path> def.
+ *
+ * @author Jacob Howcroft
+ */
+exports.fn = function(data) {
+  const seen = new Map();
+  let count = 0;
+  const defs = [];
+  traverse(data, item => {
+    if (!item.isElem('path') || !item.hasAttr('d')) {
+      return;
+    }
+    const d = item.attr('d').value;
+    const fill = (item.hasAttr('fill') && item.attr('fill').value) || '';
+    const stroke = (item.hasAttr('stroke') && item.attr('stroke').value) || '';
+    const key = d + ';s:' + stroke + ';f:' + fill;
+    const hasSeen = seen.get(key);
+    if (!hasSeen) {
+      seen.set(key, {elem: item, reused: false});
+      return;
+    }
+    if (!hasSeen.reused) {
+      hasSeen.reused = true;
+      if (!hasSeen.elem.hasAttr('id')) {
+        hasSeen.elem.addAttr({name: 'id', local: 'id',
+                              prefix: '', value: 'reuse-' + (count++)});
+      }
+      defs.push(hasSeen.elem);
+    }
+    item = convertToUse(item, hasSeen.elem.attr('id').value);
+  });
+  const defsTag = new JSAPI({
+    elem: 'defs', prefix: '', local: 'defs', content: [], attrs: []}, data);
+  data.content[0].spliceContent(0, 0, defsTag);
+  for (let def of defs) {
+    // Remove class and style before copying to avoid circular refs in
+    // JSON.stringify. This is fine because we don't actually want class or
+    // style information to be copied.
+    const style = def.style;
+    const defClass = def.class;
+    delete def.style;
+    delete def.class;
+    const defClone = def.clone();
+    def.style = style;
+    def.class = defClass;
+    defClone.removeAttr('transform');
+    defsTag.spliceContent(0, 0, defClone);
+    // Convert the original def to a use so the first usage isn't duplicated.
+    def = convertToUse(def, defClone.attr('id').value);
+    def.removeAttr('id');
+  }
+  return data;
+};
+
+/** */
+function convertToUse(item, href) {
+  item.renameElem('use');
+  item.removeAttr('d');
+  item.removeAttr('stroke');
+  item.removeAttr('fill');
+  item.addAttr({name: 'xlink:href', local: 'xlink:href',
+                prefix: 'none', value: '#' + href});
+  delete item.pathJS;
+  return item;
+}
+
+/** */
+function traverse(parent, callback) {
+  if (parent.isEmpty()) {
+    return;
+  }
+  for (let child of parent.content) {
+    callback(child);
+    traverse(child, callback);
+  }
+}


Test patch:
diff --git a/test/plugins/reusePaths.01.svg b/test/plugins/reusePaths.01.svg
new file mode 100644
index 0000000..3630385
--- /dev/null
+++ b/test/plugins/reusePaths.01.svg
@@ -0,0 +1,35 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+    <path id="test0" d="M 10,50 l 20,30 L 20,30"/>
+    <path transform="translate(10, 10)"
+          d="M 10,50 c 20,30 40,50 60,70 C 20,30 40,50 60,70"/>
+    <path transform="translate(20, 20)"
+          d="M 10,50 c 20,30 40,50 60,70 C 20,30 40,50 60,70"/>
+    <path d="M 10,50 c 20,30 40,50 60,70 C 20,30 40,50 60,70"/>
+    <path id="test1" d="M 10,50 l 20,30 L 20,30"/>
+    <path d="M 10,50 a 20,60 45 0,1 40,70 A 20,60 45 0,1 40,70"/>
+    <path d="M 20,30 a 20,60 45 0,1 40,70 A 20,60 45 0,1 40,70"/>
+    <g>
+      <path id="test2" d="M 10,50 l 20,30 L 20,30"/>
+    </g>
+    <path d="M 10,50 c 20,30 40,50 60,70 C 20,30 40,50 60,70"/>
+</svg>
+
+@@@
+
+<svg xmlns="http://www.w3.org/2000/svg">
+    <defs>
+        <path id="test0" d="M 10,50 l 20,30 L 20,30"/>
+        <path d="M 10,50 c 20,30 40,50 60,70 C 20,30 40,50 60,70" id="reuse-0"/>
+    </defs>
+    <use xlink:href="#test0"/>
+    <use transform="translate(10, 10)" xlink:href="#reuse-0"/>
+    <use transform="translate(20, 20)" xlink:href="#reuse-0"/>
+    <use xlink:href="#reuse-0"/>
+    <use id="test1" xlink:href="#test0"/>
+    <path d="M 10,50 a 20,60 45 0,1 40,70 A 20,60 45 0,1 40,70"/>
+    <path d="M 20,30 a 20,60 45 0,1 40,70 A 20,60 45 0,1 40,70"/>
+    <g>
+        <use id="test2" xlink:href="#test0"/>
+    </g>
+    <use xlink:href="#reuse-0"/>
+</svg>
diff --git a/test/plugins/reusePaths.02.svg b/test/plugins/reusePaths.02.svg
new file mode 100644
index 0000000..45bfefd
--- /dev/null
+++ b/test/plugins/reusePaths.02.svg
@@ -0,0 +1,23 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+    <path id="test0" d="M 10,50 l 20,30 L 20,30"/>
+    <path id="test1" stroke="red" d="M 10,50 l 20,30 L 20,30"/>
+    <path id="test2" stroke="blue" d="M 10,50 l 20,30 L 20,30"/>
+    <path id="test3" d="M 10,50 l 20,30 L 20,30"/>
+    <path id="test4" stroke="blue" d="M 10,50 l 20,30 L 20,30"/>
+    <path id="test1" stroke="red" fill="green" d="M 10,50 l 20,30 L 20,30"/>
+</svg>
+
+@@@
+
+<svg xmlns="http://www.w3.org/2000/svg">
+    <defs>
+        <path id="test2" stroke="blue" d="M 10,50 l 20,30 L 20,30"/>
+        <path id="test0" d="M 10,50 l 20,30 L 20,30"/>
+    </defs>
+    <use xlink:href="#test0"/>
+    <path id="test1" stroke="red" d="M 10,50 l 20,30 L 20,30"/>
+    <use xlink:href="#test2"/>
+    <use id="test3" xlink:href="#test0"/>
+    <use id="test4" xlink:href="#test2"/>
+    <path id="test1" stroke="red" fill="green" d="M 10,50 l 20,30 L 20,30"/>
+</svg>