SVGcode: một PWA để chuyển đổi hình ảnh đường quét thành đồ họa vectơ SVG

SVGcode là một Ứng dụng web tăng tiến cho phép bạn chuyển đổi hình ảnh đường quét như JPG, PNG, GIF, WebP, AVIF, v.v. thành đồ hoạ vectơ ở định dạng SVG. Ứng dụng này sử dụng API Truy cập hệ thống tệp, API Bảng nhớ tạm không đồng bộ, API Xử lý tệp và tuỳ chỉnh Lớp phủ chế độ điều khiển cửa sổ.

(Nếu bạn thích xem hơn là đọc, bài viết này cũng có sẵn dưới dạng video.)

Từ đường quét thành vectơ

Bạn đã từng điều chỉnh tỷ lệ hình ảnh và kết quả là hình ảnh bị mờ và không đạt yêu cầu chưa? Nếu vậy, có thể bạn đã xử lý các định dạng hình ảnh đường quét như WebP, PNG hoặc JPG.

Việc mở rộng hình ảnh đường quét sẽ khiến hình ảnh đó có dạng pixel.

Ngược lại, đồ hoạ vectơ là hình ảnh được xác định bằng các điểm trong hệ toạ độ. Các điểm này được kết nối bằng các đường thẳng và đường cong để tạo thành đa giác và các hình dạng khác. Đồ hoạ vectơ có ưu điểm hơn đồ hoạ đường quét ở chỗ bạn có thể tăng hoặc giảm tỷ lệ theo bất kỳ độ phân giải nào mà không bị tạo pixel.

Điều chỉnh tỷ lệ hình ảnh vectơ mà không làm giảm chất lượng.

Giới thiệu về SVGcode

Tôi đã tạo một PWA có tên SVGcode có thể giúp bạn chuyển đổi hình ảnh đường quét thành vectơ. Xin ghi công đúng người: Tôi không phải là người phát minh ra điều này. Với SVGcode, tôi chỉ dựa vào một công cụ dòng lệnh có tên là Potrace của Peter Selinger mà tôi đã chuyển đổi thành Web Assembly để có thể sử dụng trong ứng dụng Web.

Ảnh chụp màn hình ứng dụng SVGcode.
Ứng dụng SVGcode.

Sử dụng mã SVG

Trước tiên, tôi muốn hướng dẫn bạn cách sử dụng ứng dụng. Tôi bắt đầu bằng hình ảnh giới thiệu cho Chrome Dev Summit mà tôi đã tải xuống từ kênh Twitter ChromiumDev. Đây là hình ảnh đường quét PNG mà sau đó tôi kéo vào ứng dụng SVGcode. Khi tôi thả tệp, ứng dụng sẽ theo dõi màu hình ảnh theo màu, cho đến khi phiên bản vectơ của dữ liệu đầu vào xuất hiện. Giờ đây, tôi có thể phóng to hình ảnh và như bạn có thể thấy, các cạnh vẫn sắc nét. Tuy nhiên, khi phóng to biểu trưng Chrome, bạn có thể thấy rằng bản vẽ theo dõi không hoàn hảo, đặc biệt là đường viền của biểu trưng trông hơi có đốm. Tôi có thể cải thiện kết quả bằng cách loại bỏ các hạt trên đường vẽ bằng cách loại bỏ các hạt có kích thước tối đa là 5 pixel.

Quá trình chuyển đổi hình ảnh đã thả thành SVG.

Posterization trong SVGcode

Một bước quan trọng để vectơ hoá, đặc biệt là đối với hình ảnh chụp ảnh, là tạo ảnh in cho hình ảnh đầu vào để giảm số lượng màu. SVGcode cho phép tôi thực hiện việc này cho mỗi kênh màu và xem SVG thu được khi tôi thực hiện các thay đổi. Khi hài lòng với kết quả, tôi có thể lưu tệp SVG vào ổ đĩa cứng và sử dụng tệp đó ở bất cứ đâu tôi muốn.

Tạo ảnh áp phích để giảm số lượng màu.

Các API được dùng trong SVGcode

Giờ bạn đã thấy ứng dụng có thể làm được những gì, hãy để tôi giới thiệu cho bạn một số API giúp tạo ra điều kỳ diệu.

Ứng dụng web tiến bộ

SVGcode là một Ứng dụng web tiến bộ có thể cài đặt và do đó có thể hoạt động hoàn toàn khi không có mạng. Ứng dụng này dựa trên mẫu Vanilla JS cho Vite.js và sử dụng trình bổ trợ PWA Vite phổ biến. Trình bổ trợ này tạo một worker dịch vụ sử dụng Workbox.js. Workbox là một nhóm thư viện có thể hỗ trợ trình chạy dịch vụ sẵn sàng phát hành cho Ứng dụng web tiến bộ. Mẫu này có thể không hoạt động cho tất cả ứng dụng, nhưng rất phù hợp với trường hợp sử dụng của SVGcode.

Lớp phủ chế độ điều khiển cửa sổ

Để tối đa hoá không gian màn hình có sẵn, SVGcode sử dụng tuỳ chỉnh Lớp phủ điều khiển cửa sổ bằng cách di chuyển trình đơn chính lên khu vực thanh tiêu đề. Bạn có thể thấy tính năng này được kích hoạt ở cuối quy trình cài đặt.

Cài đặt mã SVG và kích hoạt chế độ tuỳ chỉnh Lớp phủ điều khiển cửa sổ.

API Truy cập hệ thống tệp

Để mở tệp hình ảnh đầu vào và lưu SVG thu được, tôi sử dụng File System Access API (API Truy cập hệ thống tệp). Điều này cho phép tôi giữ lại tệp tham chiếu đã mở trước đó và tiếp tục từ nơi tôi đã dừng lại, ngay cả sau khi tải lại ứng dụng. Bất cứ khi nào một hình ảnh được lưu, hình ảnh đó sẽ được tối ưu hoá thông qua thư viện svgo. Quá trình này có thể mất chút thời gian, tuỳ thuộc vào độ phức tạp của SVG. Cần có cử chỉ của người dùng để hiển thị hộp thoại lưu tệp. Do đó, điều quan trọng là phải lấy handle tệp trước khi quá trình tối ưu hoá SVG diễn ra, để cử chỉ của người dùng không bị vô hiệu hoá khi SVG được tối ưu hoá đã sẵn sàng.

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

Kéo và thả

Để mở hình ảnh đầu vào, tôi có thể sử dụng tính năng mở tệp hoặc như bạn đã thấy ở trên, chỉ cần kéo và thả tệp hình ảnh vào ứng dụng. Tính năng mở tệp khá đơn giản, điều thú vị hơn là trường hợp kéo và thả. Điều đặc biệt thú vị là bạn có thể lấy một tay điều khiển hệ thống tệp từ mục chuyển dữ liệu thông qua phương thức getAsFileSystemHandle(). Như đã đề cập trước đó, tôi có thể duy trì tay cầm này để sẵn sàng khi ứng dụng được tải lại.

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

Để biết thêm thông tin, hãy xem bài viết về File System Access API (API Truy cập hệ thống tệp) và nếu bạn quan tâm, hãy nghiên cứu mã nguồn của mã SVG trong src/js/filesystem.js.

API Bảng nhớ tạm không đồng bộ

SVGcode cũng được tích hợp đầy đủ với bảng nhớ tạm của hệ điều hành thông qua API Bảng nhớ tạm không đồng bộ. Bạn có thể dán hình ảnh từ trình khám phá tệp của hệ điều hành vào ứng dụng bằng cách nhấp vào nút dán hình ảnh hoặc nhấn phím Command hoặc Control + v trên bàn phím.

Dán một hình ảnh trong trình khám phá tệp vào mã SVGcode.

API Bảng nhớ tạm không đồng bộ gần đây cũng có khả năng xử lý hình ảnh SVG, vì vậy, bạn cũng có thể sao chép hình ảnh SVG rồi dán vào một ứng dụng khác để xử lý thêm.

Sao chép hình ảnh từ SVGcode và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'));
});

Để tìm hiểu thêm, hãy đọc bài viết Bảng nhớ tạm không đồng bộ hoặc xem tệp src/js/clipboard.js.

Xử lý tệp

Một trong những tính năng mà tôi yêu thích nhất của SVGcode là khả năng kết hợp hài hòa với hệ điều hành. Là một PWA đã cài đặt, ứng dụng này có thể trở thành trình xử lý tệp hoặc thậm chí là trình xử lý tệp mặc định cho các tệp hình ảnh. Điều này có nghĩa là khi đang ở trong Finder trên máy macOS, tôi có thể nhấp chuột phải vào một hình ảnh và mở hình ảnh đó bằng SVGcode. Tính năng này có tên là Xử lý tệp và hoạt động dựa trên thuộc tính file_handlers trong Tệp kê khai ứng dụng web và hàng đợi khởi chạy, cho phép ứng dụng sử dụng tệp đã truyền.

Việc mở tệp trên máy tính có ứng dụng SVGcode đã cài đặt.
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;
    }
  }
});

Để biết thêm thông tin, hãy xem phần Cho phép ứng dụng web đã cài đặt làm trình xử lý tệp và xem mã nguồn trong src/js/filehandling.js.

Chia sẻ trên web (tệp)

Một ví dụ khác về việc kết hợp với hệ điều hành là tính năng chia sẻ của ứng dụng. Giả sử tôi muốn chỉnh sửa một tệp SVG được tạo bằng SVGcode, một cách để xử lý việc này là lưu tệp, khởi chạy ứng dụng chỉnh sửa SVG, sau đó mở tệp SVG từ đó. Tuy nhiên, bạn có thể sử dụng Web Share API (API Chia sẻ trên web) để chia sẻ tệp trực tiếp. Vì vậy, nếu ứng dụng chỉnh sửa SVG là mục tiêu chia sẻ, thì ứng dụng đó có thể trực tiếp nhận tệp mà không bị lệch.

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);
      }
    }
  }
});
Chia sẻ hình ảnh SVG vào Gmail.

Mục tiêu chia sẻ trên web (Tệp)

Ngược lại, SVGcode cũng có thể đóng vai trò là mục tiêu chia sẻ và nhận tệp từ các ứng dụng khác. Để làm được việc này, ứng dụng cần cho hệ điều hành biết thông qua API Mục tiêu chia sẻ trên web những loại dữ liệu mà ứng dụng có thể chấp nhận. Quá trình này xảy ra thông qua một trường chuyên biệt trong Tệp kê khai ứng dụng 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"]
        }
      ]
    }
  }
}

Tuyến action thực sự không tồn tại, nhưng được xử lý hoàn toàn trong trình xử lý fetch của worker dịch vụ, sau đó chuyển các tệp đã nhận để xử lý thực tế trong ứng dụng.

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);
      })(),
    );
  }
});
Chia sẻ ảnh chụp màn hình với SVGcode.

Kết luận

Vậy là bạn đã có một chuyến tham quan nhanh qua một số tính năng nâng cao của ứng dụng trong SVGcode. Tôi hy vọng ứng dụng này có thể trở thành một công cụ thiết yếu cho nhu cầu xử lý hình ảnh của bạn cùng với các ứng dụng tuyệt vời khác như Squoosh hoặc SVGOMG.

Bạn có thể truy cập SVGcode tại svgco.de. Bạn thấy tôi đã làm gì ở đó không? Bạn có thể xem lại mã nguồn của ứng dụng này trên GitHub. Xin lưu ý rằng vì Potrace được cấp phép GPL nên mã SVGcode cũng vậy. Chúc mừng bạn đã tạo vectơ! Tôi hy vọng SVGcode sẽ hữu ích và một số tính năng của SVGcode có thể truyền cảm hứng cho ứng dụng tiếp theo của bạn.

Lời cảm ơn

Bài viết này được Joe Medley xem xét.