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,我只需依托 Peter Selinger 开发的命令行工具 Potrace(我已将其转换为 Web Assembly,以便在 Web 应用中使用)即可。

SVGcode 应用屏幕截图。
SVGcode 应用。

使用 SVGcode

首先,我想向您展示如何使用该应用。我先从 ChromiumDev Twitter 频道下载了 Chrome 开发者峰会的预告图片。这是 PNG 光栅图片,我将其拖动到 SVGcode 应用中。当我放下该文件时,应用会逐一跟踪图片的颜色,直到显示矢量化版本的输入内容。现在,我可以放大图片,如您所见,边缘保持清晰。不过,如果您放大 Chrome 徽标,就会发现描边并不完美,尤其是徽标的轮廓看起来有点斑点。我可以通过抑制最多 5 个像素的斑点来去除轨迹的斑点,从而改进结果。

将拖放的图片转换为 SVG。

SVGcode 中的海报效果

矢量化的一个重要步骤(尤其是对于摄影图片)是将输入图片做成海报图片,以减少颜色数量。借助 SVGcode,我可以按颜色通道执行此操作,并在进行更改时查看生成的 SVG。当我对结果满意后,可以将 SVG 保存到硬盘,并在任何地方使用。

将图片做成海报效果,以减少颜色数量。

SVGcode 中使用的 API

现在,您已经了解了该应用的功能,接下来我将向您介绍一些有助于实现这些功能的 API。

渐进式 Web 应用

SVGcode 是一款可安装的渐进式 Web 应用,因此完全支持离线功能。该应用基于 Vite.jsVanilla JS 模板,并使用流行的 Vite 插件 PWA,该插件会创建一个服务工件,该服务工件在后台使用 Workbox.js。Workbox 是一组可为 Progressive Web App 提供可用于生产环境的服务工作线程的库。此模式可能并不适用于所有应用,但对于 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 与操作系统的剪贴板完全集成。您可以通过点击“粘贴图片”按钮或按键盘上的“Command”或“Control”键加“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 打开它。此功能称为“文件处理”,基于 Web 应用清单和启动队列中的 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 中的源代码。

网页分享(文件)

与操作系统融为一体的另一个示例是应用的分享功能。假设我想修改使用 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 告知操作系统它可以接受哪些类型的数据。这通过 Web 应用清单中的专用字段实现。

{
  "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 也采用 GPL 许可。祝您使用愉快!希望 SVGcode 对您有所帮助,并希望它的一些功能能为您构建下一款应用提供灵感。

致谢

本文由 Joe Medley 审核。