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

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

(テキストではなく動画をご覧になる場合は、この記事を動画でご覧いただくこともできます。)

ラスターからベクターへ

画像を拡大縮小した結果、モザイク状に満足のいく仕上がりになったことはありませんか?使用したことがある場合、おそらく WebP、PNG、JPG などのラスター画像形式を扱うことになります。

ラスター画像をスケールアップすると、モザイク状になります。

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

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

SVGcode の概要

ラスター画像をベクターに変換する際に役立つ SVGcode という PWA を作成しました。クレジットが必要なクレジット: これは私が開発したものではありません。SVGcode を使用すると、Web Assembly に変換した Peter SelingerPotrace というコマンドライン ツールをウェブアプリで使用するだけです。

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 では、利用可能な画面領域を最大化するために、メインメニューをタイトルバー領域まで移動して、ウィンドウ コントロール オーバーレイのカスタマイズを使用しています。これが、インストール フローの最後に有効化されたことを確認できます。

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 を介してオペレーティング システムのクリップボードと完全に統合されています。オペレーティング システムのファイル エクスプローラからアプリに画像を貼り付けるには、[画像を貼り付ける] ボタンをクリックするか、キーボードの Command または 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 でソースコードを表示できます。

ウェブ共有(ファイル)

オペレーティング システムと融合するもう一つの例として、アプリの共有機能があります。SVGcode を使用して作成された SVG を編集する場合、対処法の 1 つは、ファイルを保存し、SVG 編集アプリを起動して、そこから SVG ファイルを開くことです。Web Share API を使用すると、ファイルを直接共有できます。この 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 ルートは実際には存在しませんが、Service Worker の 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 も同様です。それでは、ベクトル化をお楽しみください。SVGcode がお役に立てば幸いです

謝辞

この記事は Joe Medley によってレビューされました。