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 SelingerPotrace というコマンドライン ツールをベースにしています。このツールは Web Assembly に変換して、ウェブアプリで使用できるようにしています。

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

SVGcode を使用する

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

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

SVGcode のポスター化

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

画像をポスター化して色数を減らす

SVGcode で使用される API

ここまででアプリの機能を確認できました。次は、この便利な API をいくつか紹介します。

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

SVGcode はインストール可能なプログレッシブ ウェブアプリであるため、完全にオフラインで動作します。このアプリは Vite.jsVanilla JS テンプレートに基づいており、一般的な Vite プラグイン PWA を使用します。このプラグインは、Workbox.js を内部で使用する Service Worker を作成します。Workbox は、Progressive Web App 向けの製品版 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 キーを押します。

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

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 の高度なアプリ機能について簡単に説明しました。このアプリが、SquooshSVGOMG などの優れたアプリとともに、画像処理のニーズに不可欠なツールになることを願っています。

SVGcode は svgco.de で入手できます。何をしていたのか見てみる?GitHub でソースコードを確認できます。Potrace は GPL ライセンスであるため、SVGcode も GPL ライセンスです。これで、ベクトル化ができるようになりました。SVGcode がお役に立てば幸いです。その機能のいくつかが、次のアプリのヒントになるかもしれません。

謝辞

この記事は、Joe Medley がレビューしました。