SVGcode: 래스터 이미지를 SVG 벡터 그래픽으로 변환하는 PWA

SVGcode는 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를 사용하면 웹 앱에서 사용할 수 있도록 Web Assembly로 변환피터 셀링거Potrace라는 명령줄 도구를 활용할 수 있습니다.

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

SVGcode 사용

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

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

SVGcode의 포스터화

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

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

SVGcode에서 사용되는 API

이제 앱의 기능을 살펴봤습니다. 이제 마법 같은 기능을 제공하는 데 도움이 되는 몇 가지 API를 보여드리겠습니다.

프로그레시브 웹 앱

SVGcode는 설치 가능한 프로그레시브 웹 앱이므로 완전히 오프라인에서 사용할 수 있습니다. 이 앱은 Vite.jsVanilla JS 템플릿을 기반으로 하며, Workbox.js를 내부적으로 사용하는 서비스 워커를 만드는 인기 있는 Vite 플러그인 PWA를 사용합니다. Workbox는 Progressive Web App용 프로덕션 준비가 완료된 서비스 워커를 지원할 수 있는 라이브러리 모음입니다. 이 패턴은 모든 앱에 반드시 작동하지는 않지만 SVGcode의 사용 사례에는 적합합니다.

Window Controls Overlay

SVGcode는 사용 가능한 화면 공간을 최대화하기 위해 기본 메뉴를 제목 표시줄 영역 위로 이동하여 창 컨트롤 오버레이 맞춤설정을 사용합니다. 설치 흐름이 끝날 때 활성화되는 것을 확인할 수 있습니다.

SVG 코드를 설치하고 창 컨트롤 오버레이 맞춤설정을 활성화합니다.

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

자세한 내용은 파일 시스템 액세스 API에 관한 도움말을 참고하고 관심이 있는 경우 src/js/filesystem.js의 SVGcode 소스 코드를 살펴보세요.

Async Clipboard API

SVGcode는 Async Clipboard API를 통해 운영체제의 클립보드와도 완전히 통합됩니다. 이미지 붙여넣기 버튼을 클릭하거나 키보드에서 ⌘ 또는 Ctrl + 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로 열 수 있습니다. 이 기능을 파일 처리라고 하며 웹 앱 매니페스트 및 실행 대기열의 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가 유용하고 일부 기능이 다음 앱에 영감을 줄 수 있기를 바랍니다.

감사의 말씀

이 도움말은 조 미들리님이 검토했습니다.