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를 사용하면 웹 어셈블리로 변환Peter SelingerPotrace라는 명령줄 도구를 웹 앱에서 사용할 수 있습니다.

SVGcode 애플리케이션 스크린샷
SVGcode

SVGcode 사용

먼저 앱 사용 방법을 보여드리겠습니다. ChromiumDev Twitter 채널에서 다운로드한 Chrome Dev Summit의 티저 이미지부터 살펴보겠습니다. 이 이미지는 PNG 래스터 이미지로, SVGcode 앱으로 드래그합니다. 파일을 드롭하면 앱에서 벡터화된 버전의 입력이 나타날 때까지 이미지 색상을 색상별로 추적합니다. 이제 이미지를 확대할 수 있으며 보시다시피 가장자리가 선명하게 유지됩니다. 하지만 Chrome 로고를 확대하면 트레이싱이 완벽하지 않으며 특히 로고의 윤곽선이 약간 얼룩져 있는 것을 확인할 수 있습니다. 최대 5픽셀의 반점을 억제하여 추적의 스펙클링을 제거함으로써 결과를 개선할 수 있습니다.

드롭된 이미지를 SVG로 변환합니다.

SVGcode의 포스터

특히 사진 이미지의 경우 벡터화에서 중요한 단계는 입력 이미지를 포스터화하여 색상 수를 줄이는 것입니다. SVGcode를 사용하면 색상 채널별로 이 작업을 수행하고 변경할 때 결과 SVG를 볼 수 있습니다. 결과가 만족스러우면 SVG를 하드 디스크에 저장하여 어디서나 사용할 수 있습니다.

이미지를 포스터 처리하여 색상 수를 줄입니다.

SVGcode에서 사용되는 API

지금까지 앱의 기능을 살펴보았으니 이제 놀라운 결과를 얻는 데 도움이 되는 몇 가지 API를 살펴보겠습니다.

프로그레시브 웹 앱

SVGcode는 설치 가능한 프로그레시브 웹 앱이므로 완전히 오프라인으로 사용할 수 있습니다. 이 앱은 Vite.jsVanilla JS 템플릿을 기반으로 하며, 내부적으로 Workbox.js를 사용하는 서비스 워커를 만드는 인기 있는 Vite 플러그인 PWA를 사용합니다. Workbox는 프로그레시브 웹 앱의 프로덕션에 즉시 사용 가능한 서비스 워커를 지원하는 라이브러리 세트입니다. 이 패턴이 모든 앱에서 반드시 작동하는 것은 아니지만 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'));
});

자세한 내용은 비동기 클립보드 문서 또는 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를 수정하려는 경우, 이를 처리하는 한 가지 방법은 파일을 저장하고 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);
      }
    }
  }
});
Gmail에 SVG 이미지 공유

웹 공유 타겟 (파일)

반대로 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 경로는 실제로 존재하지 않지만 서비스 워커의 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의 고급 앱 기능을 간단히 둘러보았습니다. 이 앱이 Squoosh 또는 SVGOMG 같은 다른 멋진 앱과 함께 이미지 처리 요구사항에 필수적인 도구가 되기를 바랍니다.

SVGcode는 svgco.de에서 제공됩니다. 거기서 무슨 일을 했는지 봐요? GitHub에서 소스 코드를 검토할 수 있습니다. Potrace는 GPL 라이선스를 받으므로 SVGcode에도 적용됩니다. 이제 벡터화 작업을 즐겁게 수행할 수 있습니다. SVGcode가 유용하기를 바라며 일부 기능이 다음 앱에 영감을 줄 수 있기를 바랍니다

감사의 말

Joe Medley가 이 문서를 검토했습니다.