Phân tích kỹ thuật: Cách hoạt động của công cụ tạo con dấu trên trình duyệt web

07 tháng 4, 2026·9 phút đọc

Xây dựng một công cụ thiết kế trên trình duyệt nghe có vẻ đơn giản, nhưng lại chứa đựng nhiều thách thức kỹ thuật phức tạp. Bài viết này đi sâu vào việc lựa chọn giữa Canvas và SVG, giải quyết bài toán chữ cong, xuất file đa định dạng và quản lý trạng thái ứng dụng.

Phân tích kỹ thuật: Cách hoạt động của công cụ tạo con dấu trên trình duyệt web

Việc xây dựng một công cụ thiết kế dựa trên trình duyệt nghe có vẻ đơn giản — cho đến khi bạn nhận ra người dùng mong đợi khả năng kiểm soát như Photoshop, xem trước tức thì và xuất ra nhiều định dạng, tất cả mà không cần cài đặt bất cứ thứ gì. Sau khi dành thời gian phát triển một công cụ tạo con dấu trực tuyến, tôi muốn đi sâu vào các thách thức kỹ thuật thực sự đằng sau loại sản phẩm này và cách chúng tôi giải quyết chúng.

Quyết định kết xuất cốt lõi: Canvas hay SVG

Quyết định kiến trúc đầu tiên trong bất kỳ công cụ thiết kế trên trình duyệt nào là công cụ kết xuất (rendering engine). Bạn có hai tùy chọn chính: HTML5 CanvasSVG DOM. Mỗi cái có một mô hình khác biệt hoàn toàn.

Canvas là một API mệnh lệnh dựa trên pixel. Bạn vẽ lên nó bằng các lệnh:

const ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.arc(200, 200, 180, 0, Math.PI * 2);
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 4;
ctx.stroke();

Sau khi vẽ, canvas không "biết" gì về hình tròn đó. Nó chỉ là các điểm ảnh trên một bitmap. Điều này làm cho nó nhanh khi kết xuất các cảnh phức tạp, nhưng lại kém hiệu quả cho tương tác — bạn phải tự triển khai tính năng phát hiện va chạm (hit detection).

Ngược lại, SVG là định dạng vector dạng chế độ lưu trữ (retained-mode). Mọi hình dạng đều là một nút DOM mà bạn có thể truy vấn, định dạng và sửa đổi:

const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', 200);
circle.setAttribute('cy', 200);
circle.setAttribute('r', 180);
circle.setAttribute('stroke', '#1a1a1a');
circle.setAttribute('stroke-width', '4');
svg.appendChild(circle);

Phần tử này tồn tại liên tục. Bạn có thể thêm addEventListener vào nó, tạo hoạt ảnh cho nó, chuyển đổi nó — DOM sẽ lo phần việc nặng nhọc.

Đối với một công cụ tạo con dấu cụ thể, SVG chiến thắng. Bản chất của con dấu là vector: hình tròn, văn bản, đường viền, logo. Thiết kế phải vẫn có thể chỉnh sửa sau khi định vị. Và SVG là định dạng đầu ra chính cho sản xuất in ấn chuyên nghiệp. Sử dụng Canvas làm bộ kết xuất chính sẽ có nghĩa là phải chuyển đổi ngược lại sang SVG để xuất khẩu — một quy trình vòng vo phức tạp và mất dữ liệu.

Văn bản hình tròn: Vấn đề UI khó khăn nhất

Tính năng đặc trưng của bất kỳ thiết kế con dấu hay tem nào là văn bản được hiển thị dọc theo cung tròn. Nó trông có vẻ đơn giản nhưng lại liên quan đến toán học không hề tầm thường.

SVG có một giải pháp gốc: <textPath> với phần tử <path> làm đường cong tham chiếu.

<svg viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <path id="top-arc"
      d="M 60,200 A 140,140 0 1,1 340,200"
    />
  </defs>
  <text font-family="Futura, sans-serif" font-size="18" fill="#1a1a1a"
        letter-spacing="8">
    <textPath href="#top-arc" startOffset="50%" text-anchor="middle">
      ACME CORPORATION
    </textPath>
  </text>
</svg>

startOffset="50%" kết hợp với text-anchor="middle" sẽ căn giữa văn bản ở đỉnh của cung. Đường dẫn cung sử dụng lệnh SVG Arc (A): tâm x/y, rx/ry, xoay trục x, cờ large-arc, cờ sweep, kết thúc x/y.

Đối với cung dưới (dòng thứ hai của văn bản cong theo hướng ngược lại):

<defs>
  <path id="bottom-arc"
    d="M 60,200 A 140,140 0 0,0 340,200"
  />
</defs>
<text>
  <textPath href="#bottom-arc" startOffset="50%" text-anchor="middle">
    EST. 2020 · NEW YORK
  </textPath>
</text>

Sự khác biệt chính: sweep-flag chuyển từ 1 sang 0, đảo ngược hướng cung để văn bản đọc tự nhiên ở phía dưới.

Vấn đề khoảng cách chữ tại bán kính nhỏ

Đây là nơi lý thuyết gặp thực tế: letter-spacing trong SVG (và CSS) thêm khoảng cách sau mỗi ký tự glyph, được đo bằng đơn vị đường thẳng. Trên một đường cong khép kín, điều này tạo ra khoảng cách trực quan không đồng đều — khoảng trống cảm thấy rộng hơn ở cạnh ngoài của mỗi chữ cái và chật hơn ở cạnh trong.

Không có thuộc tính CSS nào để khắc phục điều này. Giải pháp đúng là đặt thủ công từng glyph bằng textLengthlengthAdjust="spacingAndGlyphs", hoặc tốt hơn là tính toán độ xoay và vị trí cho từng ký tự theo chương trình:

function placeTextOnArc(text, cx, cy, radius, startAngle) {
  const chars = text.split('');
  const angleStep = (2 * Math.PI) / (chars.length * 4); // tinh chinh khoang cach

  return chars.map((char, i) => {
    const angle = startAngle + (i * angleStep);
    const x = cx + radius * Math.cos(angle);
    const y = cy + radius * Math.sin(angle);
    const rotation = (angle * 180 / Math.PI) + 90;

    return `<text x="${x}" y="${y}" 
                  transform="rotate(${rotation}, ${x}, ${y})"
                  text-anchor="middle">${char}</text>`;
  }).join('');
}

Cách tiếp cận này mang lại cho bạn khả năng kiểm soát từng ký tự và khoảng cách trực quan nhất quán bất kể bán kính — rất quan trọng cho đầu ra chất lượng sản xuất.

Kiến trúc xuất đa định dạng

Một công cụ tạo con dấu trực tuyến nghiêm túc cần xuất nhiều định dạng từ một nguồn SVG duy nhất: PNG, PDF, EPS, SVG và DOCX. Mỗi định dạng có các yêu cầu khác nhau.

Xuất file SVG

Đơn giản — tuần tự hóa DOM:

function exportSVG(svgElement) {
  const serializer = new XMLSerializer();
  const svgString = serializer.serializeToString(svgElement);
  const blob = new Blob([svgString], { type: 'image/svg+xml' });
  triggerDownload(blob, 'stamp.svg');
}

Một lưu ý: phông chữ nội tuyến (inline fonts). Nếu SVG của bạn sử dụng phông chữ web tùy chỉnh được tải qua @font-face, SVG được xuất sẽ không nhúng phông chữ đó. Bạn cần thực hiện một trong hai cách:

  • Chuyển đổi văn bản thành đường dẫn (font-to-path conversion), được xử lý phía máy chủ bằng các thư viện như opentype.js.
  • Nhúng phông chữ dưới dạng URI dữ liệu base64 bên trong SVG.

Xuất file PNG

Sử dụng một phần tử <img> làm cầu nối đến Canvas:

async function exportPNG(svgElement, scale = 3) {
  const svgString = new XMLSerializer().serializeToString(svgElement);
  const blob = new Blob([svgString], { type: 'image/svg+xml' });
  const url = URL.createObjectURL(blob);

  const img = new Image();
  img.src = url;

  await new Promise(resolve => img.onload = resolve);

  const canvas = document.createElement('canvas');
  const size = svgElement.viewBox.baseVal;
  canvas.width = size.width * scale;   // 3× để tương đương 300 DPI
  canvas.height = size.height * scale;

  const ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

  canvas.toBlob(blob => triggerDownload(blob, 'stamp.png'), 'image/png');
  URL.revokeObjectURL(url);
}

Hệ số nhân scale = 3 là rất quan trọng — kết xuất ở kích thước gấp 3 lần kích thước hiển thị sẽ mang lại đầu ra ~300 DPI khi in ở kích thước vật lý của con dấu. Nếu không có điều này, PNG được xuất sẽ bị pixelated khi đưa vào tài liệu.

Xuất file PDF

Việc tạo PDF phía trình duyệt được xử lý bởi các thư viện như jsPDF hoặc pdf-lib. Cách tiếp cận sạch nhất là nhúng raster PNG bên trong PDF thay vì cố gắng chuyển đổi vector SVG sang PDF:

import { jsPDF } from 'jspdf';

async function exportPDF(svgElement) {
  const pngDataUrl = await svgToPNGDataURL(svgElement, 4); // tỷ lệ 4×

  const pdf = new jsPDF({
    orientation: 'portrait',
    unit: 'mm',
    format: [50, 50] // trang 50×50mm cho con dấu
  });

  pdf.addImage(pngDataUrl, 'PNG', 0, 0, 50, 50);
  pdf.save('stamp.pdf');
}

Đối với đầu ra PDF vector thực sự (cần thiết cho in ấn chuyên nghiệp), kết xuất phía máy chủ với các công cụ như Puppeteer (headless Chrome) hoặc CairoSVG đáng tin cậy hơn.

Quản lý trạng thái cho trình chỉnh sửa thiết kế

Trình chỉnh sửa con dấu cần theo dõi trạng thái thiết kế đầy đủ: kiểu đường viền, nội dung văn bản, lựa chọn phông chữ, kích thước, màu sắc, hình ảnh logo. Trạng thái này phải được:

  1. Phản ứng (Reactive) — UI cập nhật ngay lập tức khi thuộc tính thay đổi.
  2. Tuần tự hóa (Serializable) — có thể lưu thành JSON để duy trì phiên.
  3. Hoàn tác được (Undoable) — người dùng mong đợi Ctrl+Z.

Một mẫu lệnh đơn giản hoạt động tốt ở đây:

class StampEditor {
  constructor() {
    this.state = {
      outerText: 'COMPANY NAME',
      innerText: 'EST. 2020',
      fontSize: 16,
      letterSpacing: 8,
      borderWidth: 3,
      borderStyle: 'double',
      inkColor: '#1a1a1a'
    };
    this.history = [];
    this.redoStack = [];
  }

  update(patch) {
    this.history.push({ ...this.state });
    this.redoStack = [];
    this.state = { ...this.state, ...patch };
    this.render();
  }

  undo() {
    if (!this.history.length) return;
    this.redoStack.push({ ...this.state });
    this.state = this.history.pop();
    this.render();
  }
}

Mẫu cập nhật bất biến (truyền vào một đối tượng mới) là rất quan trọng — nó đảm bảo mỗi mục nhập lịch sử là một ảnh chụp nhanh sạch sẽ, không phải là tham chiếu đến một đối tượng đã bị đột biến.

Bài học thực tế

Sau khi xây dựng và lặp lại Stampdy như một công cụ tạo con dấu sản xuất thực tế, một vài điều nổi bật:

Sự không nhất quán trong hiển thị phông chữ giữa các trình duyệt là điểm đau lớn nhất. Chrome, Firefox và Safari đều hiển thị cùng một phông chữ SVG một chút khác nhau — đặc biệt là ở kích thước nhỏ. Cách sửa an toàn nhất là kết xuất trước các con dấu nhiều văn bản thành PNG trên máy chủ bằng trình duyệt không đầu (headless browser) và phục vụ nó dưới dạng bản xem trước.

Logo do người dùng tải lên cần được vệ sinh mạnh mẽ. SVG có thể chứa thẻ <script> và tham chiếu tài nguyên bên ngoài. Không bao giờ hiển thị SVG do người dùng tải lên trực tiếp vào tài liệu — hãy phân tích cú pháp nó, loại bỏ các phần tử nguy hiểm, sau đó nhúng.

Hiệu suất trên di đồng giảm nhanh chóng với các SVG phức tạp. Sử dụng Chrome DevTools để kiểm tra và xem thời gian bố cục/vẽ lại cho phần tử SVG. Trong thực tế, con dấu có nhiều hơn 500 nút đường dẫn cần dự phòng Canvas trên thiết bị di động.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗