SVGcode: um PWA para converter imagens rasterizadas em gráficos vetoriais SVG

O SVGcode é um Progressive Web App que permite converter imagens rasterizadas, como JPG, PNG, GIF, WebP, AVIF etc., em gráficos vetoriais no formato SVG. Ele usa a API File System Access, a API Async Clipboard, a API File Handling e a personalização da Window Controls Overlay.

Se você prefere assistir a ler, este artigo também está disponível como um vídeo.

De raster para vetor

Você já dimensionou uma imagem e o resultado ficou pixelado e insatisfatório? Nesse caso, você provavelmente lidou com um formato de imagem raster, como WebP, PNG ou JPG.

A ampliação de uma imagem raster faz com que ela pareça pixelada.

Em contraste, os gráficos vetoriais são imagens definidas por pontos em um sistema de coordenadas. Esses pontos são conectados por linhas e curvas para formar polígonos e outras formas. Os gráficos vetoriais têm uma vantagem em relação aos gráficos raster, já que podem ser dimensionados para cima ou para baixo em qualquer resolução sem pixelização.

Aumentar o tamanho de uma imagem vetorial sem perda de qualidade.

Introdução ao SVGcode

Eu criei um PWA chamado SVGcode que pode ajudar você a converter imagens raster em vetores. Crédito onde é devido: eu não inventei isso. Com o SVGcode, eu apenas uso uma ferramenta de linha de comando chamada Potrace, de Peter Selinger, que converti para o Web Assembly, para que possa ser usada em um app da Web.

Captura de tela do aplicativo SVGcode.
O app SVGcode.

Usando o SVGcode

Primeiro, quero mostrar como usar o app. Vou começar com a imagem de apresentação do Chrome Dev Summit que baixei do canal do ChromiumDev no Twitter. Esta é uma imagem raster PNG que arrasto para o app SVGcode. Quando deixo o arquivo, o app rastreia a imagem cor por cor, até que uma versão vetorizada da entrada apareça. Agora posso aplicar zoom na imagem e, como você pode ver, as bordas continuam nítidas. Mas, ao ampliar o logotipo do Chrome, você pode notar que o rastreamento não foi perfeito, e os contornos do logotipo parecem um pouco pontilhados. Posso melhorar o resultado removendo as manchas do rastreamento, suprimindo manchas de até cinco pixels.

Conversão de uma imagem inserida em SVG.

Posterização em SVGcode

Uma etapa importante para a vetorização, especialmente para imagens fotográficas, é posterizar a imagem de entrada para reduzir o número de cores. O SVGcode permite fazer isso por canal de cor e conferir o SVG resultante conforme faço as mudanças. Quando estou satisfeito com o resultado, posso salvar o SVG no meu disco rígido e usá-lo onde quiser.

Posterizar uma imagem para reduzir o número de cores.

APIs usadas no SVGcode

Agora que você já sabe o que o app pode fazer, vamos mostrar algumas das APIs que ajudam a criar a magia.

App Web Progressivo

O SVGcode é um App Web Progressivo instalável e, portanto, totalmente compatível com o modo off-line. O app é baseado no modelo Vanilla JS para Vite.js e usa o popular plug-in do Vite PWA, que cria um worker de serviço que usa Workbox.js. O Workbox é um conjunto de bibliotecas que podem acionar um service worker pronto para produção em Progressive Web Apps. Esse padrão não funciona necessariamente para todos os apps, mas é ótimo para o caso de uso do SVGcode.

Sobreposição de controles da janela

Para maximizar o espaço disponível na tela, o SVGcode usa a personalização da sobreposição de controles de janela, movendo o menu principal para a área da barra de título. Você pode conferir a ativação no final do fluxo de instalação.

Instalando o SVGcode e ativando a personalização da sobreposição de controles de janela.

API File System Access

Para abrir arquivos de imagem de entrada e salvar os SVGs resultantes, uso a API File System Access. Isso permite que eu mantenha uma referência aos arquivos abertos anteriormente e continue de onde parei, mesmo após uma recarga do app. Sempre que uma imagem é salva, ela é otimizada pela biblioteca svgo, o que pode levar alguns instantes, dependendo da complexidade do SVG. Mostrar a caixa de diálogo de salvamento de arquivo requer um gesto do usuário. Portanto, é importante conseguir o identificador de arquivo antes da otimização do SVG, para que o gesto do usuário não seja invalidado quando o SVG otimizado estiver pronto.

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);
}

Arrastar e soltar

Para abrir uma imagem de entrada, posso usar o recurso de abertura de arquivo ou, como você viu acima, arrastar e soltar um arquivo de imagem no app. O recurso de abertura de arquivo é bastante simples, mas o caso de arrastar e soltar é mais interessante. O bom disso é que você pode receber um identificador de sistema de arquivos do item de transferência de dados pelo método getAsFileSystemHandle(). Como mencionado anteriormente, posso manter esse identificador para que ele esteja pronto quando o app for recarregado.

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);
  }
});

Para mais detalhes, consulte o artigo sobre a API File System Access e, se você tiver interesse, estude o código-fonte do SVGcode em src/js/filesystem.js.

API Async Clipboard

O SVGcode também é totalmente integrado à área de transferência do sistema operacional pela API Async Clipboard. É possível colar imagens do explorador de arquivos do sistema operacional no app clicando no botão "Colar imagem" ou pressionando Command ou Control e V no teclado.

Colar uma imagem do File Explorer no SVGcode.

Recentemente, a API Async Clipboard ganhou a capacidade de lidar com imagens SVG. Assim, você também pode copiar uma imagem SVG e colá-la em outro aplicativo para processamento adicional.

Copiando uma imagem do SVGcode para o 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'));
});

Para saber mais, leia o artigo Área de transferência assíncrona ou consulte o arquivo src/js/clipboard.js.

Processamento de arquivos

Um dos meus recursos favoritos do SVGcode é como ele se integra ao sistema operacional. Como um PWA instalado, ele pode se tornar um gerenciador de arquivos ou até mesmo o gerenciador de arquivos padrão para arquivos de imagem. Isso significa que, quando estou no Finder na minha máquina macOS, posso clicar com o botão direito do mouse em uma imagem e abri-la com SVGcode. Esse recurso é chamado de Processamento de arquivos e funciona com base na propriedade file_handlers no manifesto do app da Web e na fila de inicialização, o que permite que o app consuma o arquivo transmitido.

Abertura de um arquivo no computador com o app SVGcode instalado.
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;
    }
  }
});

Para mais informações, consulte Permitir que aplicativos da Web instalados sejam gerenciadores de arquivos e confira o código-fonte em src/js/filehandling.js.

Compartilhamento na Web (arquivos)

Outro exemplo de integração com o sistema operacional é o recurso de compartilhamento do app. Supondo que eu queira fazer edições em um SVG criado com o SVGcode, uma maneira de lidar com isso seria salvar o arquivo, iniciar o app de edição de SVG e abrir o arquivo SVG a partir dele. Um fluxo mais suave, no entanto, é usar a API Web Share, que permite que os arquivos sejam compartilhados diretamente. Portanto, se o app de edição de SVG for um destino de compartilhamento, ele poderá receber o arquivo diretamente sem desvio.

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);
      }
    }
  }
});
Compartilhando uma imagem SVG no Gmail.

Destino de compartilhamento da Web (arquivos)

Por outro lado, o SVGcode também pode atuar como um destino de compartilhamento e receber arquivos de outros apps. Para fazer isso, o app precisa informar ao sistema operacional, pela API de destino de compartilhamento da Web, quais tipos de dados ele pode aceitar. Isso acontece por meio de um campo dedicado no manifesto do app da 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"]
        }
      ]
    }
  }
}

A rota action não existe, mas é processada apenas no gerenciador fetch do worker do serviço, que transmite os arquivos recebidos para processamento real no app.

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);
      })(),
    );
  }
});
Compartilhando uma captura de tela para o SVGcode.

Conclusão

Certo, este foi um tour rápido sobre alguns dos recursos avançados de apps em SVGcode. Espero que esse app se torne uma ferramenta essencial para suas necessidades de processamento de imagens, junto com outros apps incríveis, como Squoosh ou SVGOMG.

O SVGcode está disponível em svgco.de. Entendeu? Confira o código-fonte no GitHub. Como o Potrace tem licença GPL, o SVGcode também tem. Agora é só vetorizar! Espero que o SVGcode seja útil e que alguns de seus recursos possam inspirar seu próximo aplicativo.

Agradecimentos

Este artigo foi revisado por Joe Medley.