841-biborokuWebフロントエンドの備忘録

Node.jsのSVGOを使ってSVGの最適化とBase64変換したCSSプロパティをコピーするツールを作ってみた

SVGアイコンをCSSのbackgroundやmaskで使用するときに、パスを最適化したり不要な要素を削除したり・・何かと手間がかかるのでSVGパスを最適化した状態でCSSプロパティとして出力するツールを作りました。

SVG ICON Convert to CSS
https://svg-icon-convert-to-css.pages.dev/

ツールでできること

  • SVGの複数<path>を結合するかしないか
  • パスやアンカーポイントの位置の小数点の桁数変更
  • CSSプロパティ設定
    • background か mask
    • cover, contain, カスタム設定
  • SVGパスをクリップボードにコピー
    • 最適化する前
    • 最適化した後
  • Base64に変換したSVGパスをCSSプロパティにセットしクリップボードにコピー
    • 最適化する前
    • 最適化した後
  • 最適化前と最適化後のSVGプレビュー
  • ダークモード

使用したライブラリ・フレームワーク

SVG最適化

SVGO

クリップボードコピー

react-copy-to-clipboard

CSSフレームワーク

MUI

SVGパスの貼り付けとプレビュー表示

textareaは<TextField>コンポーネントを使用しています。

<TextField
  multiline
  rows={12}
  size="small"
  value={originalSvgPath.value}
  tabIndex={0}
  autoFocus={true}
  focused
  label="元のSVG"
  placeholder="Paste SVG Code!!!"
  inputRef={inputRef}
  color="primary"
  onChange={svgPasteChange}
  sx={{ ...textareaStyles, backgroundColor: "primary.light" }}
/>

貼り付けたSVGパスを正規表現でパターンチェックをしています。

コンテンツ内には<Snackbar><Alert>でエラー表示用の要素を配置していて、SVGパスでないとき<Snackbar>のプロパティopentrueを渡してエラーを表示させます。

SVGパスのパターンに一致した場合は、originalSvgPathのstateを更新しています。

const [originalSvgPath, setOriginalSvgPath] = useState(defaultPath);

const pattern = /^<svg[\s\S]*?<\/svg>\s*$/;  //SVGパスの正規表現

const svgPasteChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  if (!event.target) return;
  const newValue = event.target.value;
  if (!pattern.test(newValue)) {  //SVGのパスかパターンチェック
    setStateError({
      open: true,
      vertical: "bottom",
      horizontal: "center",
    });
    return;
  }
  setOriginalSvgPath({
    value: newValue,
    copied: true,
  });
};

最適化されたSVGを表示するだけでもいいのですが、貼り付けたSVGと最適化されたSVGに違いがあるか視覚的に比較したかったのでプレビュー表示のエリアを作りました。

貼り付けたSVGは、そのままSVGパスをdangerouslySetInnerHTMLで表示させています。

<Box sx={previewStyles}>
{originalSvgPath.value && (
  <div
    dangerouslySetInnerHTML={{
      __html: originalSvgPath.value,
    }}
  ></div>
)}
</Box>

SVGパスの最適化

SVGOを使ってパスの最適化を行いますが、SVGOにはプロパティがたくさんあります。自分の好みの設定ができますが、SVGアイコンの最適化には多機能すぎる気がしたので最低限の設定をユーザーに委ねる設計にしました。

1. mergePaths

複数のパスを1つにマージするか

//貼り付けたSVG
<svg width="278" height="192" viewBox="0 0 278 192" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="133" height="133" fill="#D9D9D9"/>
<rect x="145" y="59" width="133" height="133" fill="#D9D9D9"/>
</svg>

//パスをマージしない場合
<svg xmlns="http://www.w3.org/2000/svg" width="278" height="192" fill="none" viewBox="0 0 278 192">
<path fill="#D9D9D9" d="M0 0h133v133H0z"/>
<path fill="#D9D9D9" d="M145 59h133v133H145z"/>
</svg>

//パスをマージした場合
<svg xmlns="http://www.w3.org/2000/svg" width="278" height="192" fill="none" viewBox="0 0 278 192">
<path fill="#D9D9D9" d="M0 0h133v133H0zm145 59h133v133H145z"/>
</svg>

mergePathsを有効にすると<path>が1つになっていることが分かります。
パス自体を合体させるオプションではないため、<path>と<path>が重なっている場合、パスはマージされません。パスが重なっていない場合に有効です。

2. convertPathDataのfloatPrecision

SVGパスの不要な区切り文字や小数点以下の丸め処理をして軽量化をはかる

//貼り付けたSVG
<svg width="33" height="30" viewBox="0 0 33 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.5 0L20.2045 11.4012H32.1924L22.494 18.4476L26.1985 29.8488L16.5 22.8024L6.80154 29.8488L10.506 18.4476L0.807568 11.4012H12.7955L16.5 0Z" fill="#D9D9D9"/>
</svg>

//丸め処理:デフォルト(小数第三位)
<svg xmlns="http://www.w3.org/2000/svg" width="33" height="30" fill="none" viewBox="0 0 33 30">
<path fill="#D9D9D9" d="m16.5 0 3.704 11.401h11.988l-9.698 7.047 3.704 11.4-9.698-7.046-9.698 7.047 3.704-11.401L.808 11.4h11.988L16.5 0Z"/>
</svg>

//丸め処理:小数第一位
<svg xmlns="http://www.w3.org/2000/svg" width="33" height="30" fill="none" viewBox="0 0 33 30">
<path fill="#D9D9D9" d="m16.5 0 3.7 11.4h12l-9.7 7 3.7 11.4-9.7-7-9.7 7 3.7-11.4-9.7-7h12L16.5 0Z"/>
</svg>

小数第三位では3.704だった値が、小数第一位にすると3.7になっていることが分かります。値が小さくなるほどパスは軽量化されますが、微妙にパスの位置が変わるため見た目に影響が出ます。

軽量化できてもアイコンがいびつな形になっては意味がありません。プレビュー機能を付けた理由は、この小数点の丸め処理によって変化したSVGを確認するためです。

ユーザー操作なしの設定

removeRasterImagesを有効にして、あとはSVGOのデフォルト設定を利用しました。
https://www.npmjs.com/package/svgo#built-in-plugins

クリップボードへのコピー

冒頭でも書いたように4種類のSVGパスをコピーできるツールです。

  • SVGパスをクリップボードにコピー
    • 最適化する前
    • 最適化した後
  • Base64に変換したSVGパスをCSSプロパティにセットしクリップボードにコピー
    • 最適化する前
    • 最適化した後

SVGパスは最適化前・後の値を使用し、CSS用のSVGは最適化前・後のSVGパスをSVGOのdatauri: encでエンコードしています。

Base64にエンコードした値は、CSSにそのまま貼り付けられるようツール上でプロパティの組み合わせを選択しセットした状態でクリップボードにコピーできるようにしました。

  • background または mask
  • url(Base64エンコードのパス)
  • no-repeat
  • left top / cover 、left top contain、カスタマイズ

あとは<Button>コンポーネントをコピーしたい値をセットした<CopyToClipboard>でラップするだけで、ボタンをクリックするとクリップボードに値がコピーされます。

<CopyToClipboard
  text={クリップボードにコピーする値}
  onCopy={コールバック関数}
>
  <Button
    variant="outlined"
    sx={{ textTransform: "capitalize" }}
    startIcon={<CodeIcon />}
  >
    SVG
  </Button>
</CopyToClipboard>

おまけ:ダークモードの切り替え

せっかくなのでMUIのドキュメントにダークモードのサンプルがあったので組み込みました。

モード切替後、リロードしても状態を保持するようにしましたが、はじめは一瞬ライトモードが表示されるちらつきに悩まされましたが、MUIの公式で「CSS theme variables」による解決策が掲載されていました。

https://mui.com/material-ui/guides/next-js-app-router/
https://mui.com/material-ui/experimental-api/css-theme-variables/migration/

📝参考
How to install material UI in the Next.js with the app router?
[Theme Mode] CssBaseline not working with Next.js App Router (with ThemeRegistry)

ちらつきが無くなりました😊