SVGcode: ラスター画像を SVG ベクター グラフィックに変換する PWA

SVGcode は、JPG、PNG、GIF、WebP、AVIF などのラスター画像を SVG 形式のベクター グラフィックに変換できるプログレッシブ ウェブアプリです。File System Access API、Async Clipboard API、File Handling API、Window Controls Overlay のカスタマイズを使用します。

(この記事は、動画で視聴することもできます。動画)

ラスターからベクターへ

画像を拡大した結果、画質が粗くなって満足できなかったという経験はありませんか?これまでに画像を扱ったことがある場合は、WebP、PNG、JPG などのラスター画像形式を扱ったことがあるはずです。

ラスター画像を拡大すると、画像が粗く見えます。

一方、ベクター グラフィックは、座標系内の点によって定義される画像です。これらのポイントは線や曲線で接続され、ポリゴンなどの形状を形成します。ベクター グラフィックは、ラスター グラフィックに比べて、ピクセル化することなく任意の解像度に拡大または縮小できるという利点があります。

品質を損なうことなくベクター画像を拡大します。

SVGcode のご紹介

ラスター画像をベクターに変換できる SVGcode という PWA を作成しました。功績は功績に帰す: この方法は私が考案したものではありません。SVGcode では、Peter Selinger の Potrace というコマンドライン ツールをベースにしています。このツールは Web Assembly に変換して、ウェブアプリで使用できるようにしています。

SVGcode アプリのスクリーンショット。
SVGcode アプリ。

SVGcode を使用する

まず、アプリの使用方法を説明します。最初は、ChromiumDev の Twitter チャンネルからダウンロードした Chrome Dev Summit のティーザー画像を使用します。これは PNG ラスター画像で、SVGcode アプリにドラッグします。ファイルをドロップすると、入力のベクター化されたバージョンが表示されるまで、アプリが画像を色ごとにトレースします。画像をズームしても、エッジはシャープなままです。ただし、Chrome のロゴを拡大すると、トレース結果は完璧ではなく、特にロゴの輪郭が少し斑点状に見えます。たとえば、最大 5 ピクセルのスペックを抑制してトレースからスペックを除去することで、結果を改善できます。

ドロップされた画像を SVG に変換します。

SVGcode のポスト処理

ベクトル化の重要なステップは、特に写真画像の場合、入力画像をポスターライズして色数を減らすことです。SVGcode では、色チャンネルごとにこの操作を行い、変更を加えるたびに結果の SVG を確認できます。結果に満足したら、SVG をハードディスクに保存して、好きな場所で使用できます。

画像をポスターライズして色の数を減らします。

SVGcode で使用される API

アプリの機能を確認したので、魔法の実現に役立つ API をいくつか紹介します。

プログレッシブ ウェブアプリ

SVGcode はインストール可能なプログレッシブ ウェブアプリであるため、完全にオフラインで動作します。このアプリは Vite.js の Vanilla JS テンプレートに基づいており、一般的な Vite プラグイン PWA を使用します。このプラグインは、Workbox.js を内部で使用する Service Worker を作成します。Workbox は、プログレッシブ ウェブアプリ向けの製品版 Service Worker を強化できるライブラリのセットです。このパターンはすべてのアプリで機能するとは限りません。しかし、SVGcode のユースケースでは非常に便利です。

ウィンドウ コントロール オーバーレイ

利用可能な画面領域を最大限に活用するため、SVGcode はWindow Controls Overlay のカスタマイズを使用して、メインメニューをタイトルバー領域に移動しています。この機能は、インストール フローの最後に有効になります。

SVGcode のインストールと、ウィンドウ コントロール オーバーレイのカスタマイズの有効化。

File System Access API

入力画像ファイルを開いて、生成された SVG を保存するには、File System Access API を使用します。これにより、以前に開いたファイルへの参照を保持し、アプリの再読み込み後も中断したところから続行できます。画像が保存されるたびに、svgo ライブラリを介して最適化されます。SVG の複雑さによっては、時間がかかることがあります。ファイル保存ダイアログを表示するには、ユーザーの操作が必要です。そのため、最適化された SVG の準備が整うまでにユーザー操作が無効にならないように、SVG の最適化の前にファイルハンドルを取得することが重要です。

try {
  let svg = svgOutput.innerHTML;
  let handle = null;
  // To not consume the user gesture obtain the handle before preparing the
  // blob, which may take longer.
  if (supported) {
    handle = await showSaveFilePicker({
      types: [{description: 'SVG file', accept: {'image/svg+xml': ['.svg']}}],
    });
  }
  showToast(i18n.t('optimizingSVG'), Infinity);
  svg = await optimizeSVG(svg);
  showToast(i18n.t('savedSVG'));
  const blob = new Blob([svg], {type: 'image/svg+xml'});
  await fileSave(blob, {description: 'SVG file'}, handle);
} catch (err) {
  console.error(err.name, err.message);
  showToast(err.message);
}

ドラッグ&ドロップ

入力画像を開くには、ファイルのオープン機能を使用するか、上記のように画像ファイルをアプリにドラッグ&ドロップします。ファイルのオープン機能は非常に簡単ですが、ドラッグ&ドロップの場合はさらに興味深いことが起こります。特に便利なのは、getAsFileSystemHandle() メソッドを介してデータ転送アイテムからファイル システム ハンドルを取得できることです。前述のように、このハンドルを保持できるため、アプリが再読み込みされたときにすぐに使用できます。

document.addEventListener('drop', async (event) => {
  event.preventDefault();
  dropContainer.classList.remove('dropenter');
  const item = event.dataTransfer.items[0];
  if (item.kind === 'file') {
    inputImage.addEventListener(
      'load',
      () => {
        URL.revokeObjectURL(blobURL);
      },
      {once: true},
    );
    const handle = await item.getAsFileSystemHandle();
    if (handle.kind !== 'file') {
      return;
    }
    const file = await handle.getFile();
    const blobURL = URL.createObjectURL(file);
    inputImage.src = blobURL;
    await set(FILE_HANDLE, handle);
  }
});

詳細については、File System Access API に関する記事をご覧ください。また、興味がある場合は、src/js/filesystem.js の SVGcode ソースコードをご覧ください。

Async Clipboard API

SVGcode は、Async Clipboard API を介してオペレーティング システムのクリップボードと完全に統合されています。オペレーティング システムのファイル エクスプローラからアプリに画像を貼り付けるには、画像の貼り付けボタンをクリックするか、キーボードの Ctrl+V キーを押します。

ファイル エクスプローラから SVG コードに画像を貼り付けます。

Async Clipboard API は最近、SVG 画像も処理できるようになりました。そのため、SVG 画像をコピーして別のアプリケーションに貼り付け、さらに処理することもできます。

SVGcode から SVGOMG に画像をコピー。
copyButton.addEventListener('click', async () => {
  let svg = svgOutput.innerHTML;
  showToast(i18n.t('optimizingSVG'), Infinity);
  svg = await optimizeSVG(svg);
  const textBlob = new Blob([svg], {type: 'text/plain'});
  const svgBlob = new Blob([svg], {type: 'image/svg+xml'});
  navigator.clipboard.write([
    new ClipboardItem({
      [svgBlob.type]: svgBlob,
      [textBlob.type]: textBlob,
    }),
  ]);
  showToast(i18n.t('copiedSVG'));
});

詳細については、非同期クリップボードの記事を読むか、ファイル src/js/clipboard.js をご覧ください。

ファイル処理

SVGcode の機能の中で、私が特に気に入っているものの一つが、オペレーティング システムにうまく溶け込む点です。インストールされた PWA は、画像ファイルのファイル ハンドラ、またはデフォルトのファイル ハンドラになることができます。つまり、macOS マシンの Finder で画像を右クリックして、SVGcode で開くことができます。この機能はファイル処理と呼ばれ、ウェブアプリ マニフェストの file_handlers プロパティと起動キューに基づいて動作します。これにより、アプリは渡されたファイルを使用できます。

SVGcode アプリがインストールされたデスクトップからファイルを開く。
window.launchQueue.setConsumer(async (launchParams) => {
  if (!launchParams.files.length) {
    return;
  }
  for (const handle of launchParams.files) {
    const file = await handle.getFile();
    if (file.type.startsWith('image/')) {
      const blobURL = URL.createObjectURL(file);
      inputImage.addEventListener(
        'load',
        () => {
          URL.revokeObjectURL(blobURL);
        },
        {once: true},
      );
      inputImage.src = blobURL;
      await set(FILE_HANDLE, handle);
      return;
    }
  }
});

詳細については、インストール済みのウェブ アプリケーションをファイル ハンドラにするをご覧ください。また、src/js/filehandling.js のソースコードをご覧ください。

ウェブ共有(ファイル)

オペレーティング システムに溶け込むもう 1 つの例として、アプリの共有機能があります。SVGcode で作成した SVG を編集する場合、ファイルを保存して SVG 編集アプリを起動し、そこから SVG ファイルを開く方法があります。ただし、Web Share API を使用すると、ファイルを直接共有できるため、よりスムーズなフローになります。そのため、SVG 編集アプリが共有先である場合、そのアプリは逸脱することなくファイルを直接受け取ることができます。

shareSVGButton.addEventListener('click', async () => {
  let svg = svgOutput.innerHTML;
  svg = await optimizeSVG(svg);
  const suggestedFileName =
    getSuggestedFileName(await get(FILE_HANDLE)) || 'Untitled.svg';
  const file = new File([svg], suggestedFileName, { type: 'image/svg+xml' });
  const data = {
    files: [file],
  };
  if (navigator.canShare(data)) {
    try {
      await navigator.share(data);
    } catch (err) {
      if (err.name !== 'AbortError') {
        console.error(err.name, err.message);
      }
    }
  }
});
SVG 画像を Gmail と共有。

ウェブ共有の対象(ファイル)

逆に、SVGcode は共有先として機能し、他のアプリからファイルを受信することもできます。これを実現するには、アプリが受け入れることができるデータの種類を Web Share Target API を介してオペレーティング システムに通知する必要があります。これは、ウェブアプリ マニフェストの専用フィールドで指定します。

{
  "share_target": {
    "action": "https://svgco.de/share-target/",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "files": [
        {
          "name": "image",
          "accept": ["image/jpeg", "image/png", "image/webp", "image/gif"]
        }
      ]
    }
  }
}

action ルートは実際には存在しませんが、サービス ワーカーの fetch ハンドラでのみ処理されます。このハンドラは、受信したファイルをアプリで実際に処理するために渡します。

self.addEventListener('fetch', (fetchEvent) => {
  if (
    fetchEvent.request.url.endsWith('/share-target/') &&
    fetchEvent.request.method === 'POST'
  ) {
    return fetchEvent.respondWith(
      (async () => {
        const formData = await fetchEvent.request.formData();
        const image = formData.get('image');
        const keys = await caches.keys();
        const mediaCache = await caches.open(
          keys.filter((key) => key.startsWith('media'))[0],
        );
        await mediaCache.put('shared-image', new Response(image));
        return Response.redirect('./?share-target', 303);
      })(),
    );
  }
});
SVGcode にスクリーンショットを共有します。

まとめ

以上、SVGcode の高度なアプリ機能の一部について簡単に説明しました。このアプリが、Squoosh や SVGOMG などの優れたアプリとともに、画像処理のニーズに不可欠なツールになることを願っています。

SVGcode は svgco.de で入手できます。なるほど、そういうことね。GitHub でソースコードを確認できます。Potrace は GPL ライセンスであるため、SVGcode も GPL ライセンスです。以上で、ベクトル化は完了です。SVGcode が役に立ち、その機能の一部が次のアプリのヒントになることを願っています。

謝辞

この記事は Joe Medley さんが確認しました。