SVGcode:一个将光栅图像转换为 SVG 矢量图形的 PWA

SVGcode 是一款渐进式 Web 应用,可让您将 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,我只是站在名为 Potrace 的命令行工具(由 Peter Selinger 开发)的肩膀上,我已将其转换为 Web Assembly,因此可以在 Web 应用中使用。

SVGcode application screenshot.
SVGcode 应用。

使用 SVGcode

首先,我想向您展示如何使用该应用。我从 Chrome Dev Summit 的宣传图片开始,该图片是我从 ChromiumDev Twitter 频道下载的。这是一个 PNG 光栅图像,然后我将其拖到 SVGcode 应用上。当我放下文件时,该应用会逐个颜色地追踪图像,直到出现输入图像的矢量化版本。我现在可以放大图像,正如您所见,边缘保持清晰。但是放大 Chrome 徽标,您可以看到追踪并不完美,尤其是徽标的轮廓看起来有点斑点。我可以通过消除斑点来改进结果,例如抑制最多五个像素的斑点。

将拖放的图像转换为 SVG。

SVGcode 中的色调分离

矢量化中的一个重要步骤,尤其是对于摄影图像,是对输入图像进行色调分离以减少颜色数量。SVGcode 允许我按颜色通道执行此操作,并在我进行更改时查看生成的 SVG。当我对结果感到满意时,我可以将 SVG 保存到我的硬盘并随处使用它。

对图像进行色调分离以减少颜色数量。

SVGcode 中使用的 API

现在您已经了解了该应用的功能,让我向您展示一些帮助实现神奇效果的 API。

渐进式 Web 应用

SVGcode 是一款可安装的渐进式 Web 应用,因此完全支持离线使用。该应用基于 Vanilla JS 模板,用于 Vite.js,并使用流行的 Vite plugin PWA,后者创建了一个使用 Workbox.js 的 Service Worker。Workbox 是一组库,可以为渐进式 Web 应用提供生产就绪的 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 或 Control 加 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'));
});

要了解更多信息,请阅读 Async Clipboard 文章,或查看文件 src/js/clipboard.js

File Handling

SVGcode 最受欢迎的功能之一是它与操作系统的融合程度。作为已安装的 PWA,它可以成为图像文件的文件处理程序,甚至成为默认文件处理程序。这意味着当我在 macOS 计算机上的 Finder 中时,我可以右键单击图像并使用 SVGcode 打开它。此功能称为文件处理,它基于 Web App Manifest 中的 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;
    }
  }
});

有关更多信息,请参阅让已安装的 Web 应用程序成为文件处理程序,并查看 src/js/filehandling.js 中的源代码。

Web Share (Files)

与操作系统融合的另一个示例是该应用的共享功能。假设我想对使用 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。

Web Share Target (Files)

反过来,SVGcode 也可以充当共享目标并接收来自其他应用的文件。为了实现这一点,应用需要通过 Web Share Target API 让操作系统知道它可以接受哪些类型的数据。这通过 Web App Manifest 中的专用字段来实现。

{
  "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 审阅。