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. Ela usa a API File System Access, a API Async Clipboard, a API File Handling e a personalização de sobreposição de controles de janela.

Se você preferir assistir em vez de ler, este artigo também está disponível como vídeo.

De varredura para vetor

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

Ampliar uma imagem de varredura faz com que ela pareça pixelada.

Por outro lado, 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 rasterizados, porque podem ser dimensionados para qualquer resolução sem pixelização.

Dimensione uma imagem vetorial sem perda de qualidade.

Introdução ao SVGcode

Criei um PWA chamado SVGcode, que pode ajudar a converter imagens rasterizadas em vetores. Crédito no qual o crédito é devido: eu não inventei isso. Com o SVGcode, basta usar uma ferramenta de linha de comando chamada Potrace de Peter Selinger, que converti em Web Assembly, para que possa ser usada em um app da Web.

Captura de tela do aplicativo SVGcode.
O app SVGcode.

Como usar o SVGcode

Primeiro, quero mostrar como usar o aplicativo. Começo com a imagem teaser da Conferência de Desenvolvedores Chrome que eu fiz o download do canal ChromiumDev no Twitter. Essa é uma imagem rasterizada PNG que eu arrastei para o app SVGcode. Quando eu solto o arquivo, o app rastreia a imagem por cor até que uma versão vetorizada da entrada apareça. Agora, posso aplicar zoom à imagem e, como dá para notar, as bordas ficam nítidas. Mas, ao aumentar o zoom no logotipo do Chrome, é possível ver que o rastro não foi perfeito e, especialmente, os contornos do logotipo parecem um pouco salgados. Posso melhorar o resultado reduzindo o rastreio e suprimindo manchas de até cinco pixels, por exemplo.

Converter uma imagem solta em SVG.

Posterização em SVGcode

Uma etapa importante da vetorização, especialmente para imagens fotográficas, é postar um modelo da imagem de entrada para reduzir o número de cores. Com o SVGcode, é possível fazer isso por canal de cor e ver o SVG resultante enquanto faço alterações. Quando estiver satisfeito com o resultado, posso salvar o SVG no meu disco rígido e usá-lo onde quiser.

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

APIs usadas no SVGcode

Agora que você viu do que o app é capaz, vou mostrar algumas das APIs que ajudam a fazer a mágica acontecer.

App Web Progressivo

O SVGcode é um Progressive Web App instalável e, portanto, totalmente off-line. O app é baseado no modelo Vanilla JS para Vite.js e usa o famoso plug-in PWA, que cria um service worker que usa o Workbox.js em segundo plano. O Workbox é um conjunto de bibliotecas que pode alimentar um service worker pronto para produção para Progressive Web Apps. Esse padrão pode não funcionar para todos os apps, mas é ótimo para o caso de uso do SVGcode.

Sobreposição dos 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 cima na área da barra de título. Você pode conferir a ativação no final do fluxo de instalação.

Instalar o SVGcode e ativar a personalização da sobreposição de controles da janela.

API File System Access

Para abrir arquivos de imagem de entrada e salvar os SVGs resultantes, uso a API File System Access. Isso me permite manter uma referência a arquivos abertos anteriormente e continuar de onde parei, mesmo depois de recarregar o app. Sempre que uma imagem é salva, ela é otimizada pela biblioteca svgo, o que pode demorar um pouco, dependendo da complexidade do SVG. A exibição da caixa de diálogo para salvar o arquivo exige um gesto do usuário. Portanto, é importante conseguir o identificador do arquivo antes que a otimização do SVG aconteça, para que o gesto do usuário não seja invalidado no momento em que 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 para abrir arquivos ou, como visto acima, simplesmente arrastar e soltar um arquivo de imagem no app. O recurso para abrir arquivos é bem simples, e mais interessante é o caso de arrastar e soltar. O que é particularmente bom nisso é que você pode receber um identificador do sistema de arquivos no item de transferência de dados pelo método getAsFileSystemHandle(). Como mencionado antes, 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, confira o artigo sobre a API File System Access e, se tiver interesse, estude o código-fonte SVGcode em src/js/filesystem.js (link em inglês).

API Async Clipboard

O SVGcode também é totalmente integrado à área de transferência do sistema operacional por meio da API Async Clipboard. É possível colar imagens do explorador de arquivos do sistema operacional no app clicando no botão "Colar imagem" ou pressionando comando ou control mais v no teclado.

Colando uma imagem do explorador de arquivos no SVGcode.

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

Copiar 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 veja o arquivo src/js/clipboard.js.

Gerenciamento de arquivos

Um dos meus recursos favoritos do SVGcode é a forma como ele se integra ao sistema operacional. Por ser um PWA instalado, ele pode se tornar um gerenciador de arquivos ou até mesmo o gerenciador de arquivos padrão de arquivos de imagem. Isso significa que, quando estiver 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 "Gerenciamento de arquivos" e funciona com base na propriedade file_handlers no manifesto do app da Web e na fila de inicialização, que permite que o app consuma o arquivo transmitido.

Abrir 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 os aplicativos da Web instalados sejam gerenciadores de arquivos e veja o código-fonte em src/js/filehandling.js.

Compartilhamento na Web (arquivos)

Outro exemplo de combinação com o sistema operacional é o recurso de compartilhamento do app. Supondo que eu queira editar um SVG criado com SVGcode, uma maneira de lidar com isso seria salvar o arquivo, iniciar o app de edição de SVG e abrir o arquivo SVG nele. No entanto, um fluxo mais tranquilo é usar a API Web Share, que permite que os arquivos sejam compartilhados diretamente. Portanto, se o app de edição SVG for um alvo de compartilhamento, ele poderá receber diretamente o arquivo 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);
      }
    }
  }
});
Compartilhar uma imagem SVG com o Gmail.

Destino de compartilhamento da Web (arquivos)

Por outro lado, o SVGcode também pode atuar como um alvo de compartilhamento e receber arquivos de outros aplicativos. Para que isso funcione, o app precisa informar ao sistema operacional pela API Web Share Target quais tipos de dados ele pode aceitar. Isso acontece em 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 puramente no gerenciador fetch do service worker, que transmite os arquivos recebidos para o 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 no SVGcode.

Conclusão

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

O SVGcode está disponível em svgco.de. Viu o que eu fiz lá? Revise o código-fonte no GitHub. Como o Potrace é licenciado para GPL, o SVGcode também é. Boa vetorização! Espero que o SVGcode seja útil e alguns dos recursos inspirem seu próximo app.

Agradecimentos

Este artigo foi revisado por Joe Medley.