PerfectVector
Bởi Irene Kim

Xuống dòng văn bản SVG: tạo dòng đã đo bằng tspan

Xuống dòng nhãn SVG bằng cách đo từ với phông chữ thực, rồi tạo tspan. Thử nhãn hẹp, từ chối từ vượt chiều rộng hoặc quá số dòng và lưu kết quả.

13 phút đọc
Trên trang này

Để chia nhãn SVG có nội dung thay đổi thành các dòng tường minh, đo các nhóm từ ứng viên bằng getComputedTextLength(), rồi ghi một <tspan> cho mỗi dòng được chấp nhận. Trước tiên chờ phông chữ của tài liệu và chọn cách xử lý khi một từ rộng hơn nhãn hoặc kết quả vượt giới hạn dòng.

Mã khởi đầu bên dưới dùng các từ phân tách bằng khoảng trắng và từ chối cả hai kiểu vượt giới hạn mà không tạo nhãn một phần. Nó giữ hình minh họa hình thoi rỗng gốc riêng với nội dung chữ, nên đổi chiều rộng nhãn không vẽ lại hình.

Chọn chính sách xuống dòng trước khi đặt các dòng

Phần tử tspan cho phép đặt vị trí các phần văn bản SVG. Quy trình của chúng tôi đặt cùng x cho mỗi dòng và độ lệch dy cho các dòng sau. Quy trình quyết định chỗ ngắt; phần tử giữ kết quả.

Đây là một cách bố cục tường minh. Đặc tả văn bản SVG 2 cũng định nghĩa xuống dòng tự động qua vùng nội dung và bố cục văn bản CSS. Bài này không kiểm thử các tính năng đó hay khẳng định mọi trình duyệt đều thiếu chúng.

Với đoạn HTML ngoài hình minh họa, hãy dùng CSS để văn bản bao quanh dáng SVG nổi. Bố cục đó đi theo đường bao ảnh trong khi chữ vẫn là HTML thông thường, thay vì trở thành các dòng nhãn SVG được định vị.

Với ví dụ nhãn tiếng Anh nhỏ này, chính sách là:

  • Gộp khoảng trắng và thử các từ theo thứ tự gốc, phân cách bằng một dấu cách.
  • Chỉ giữ dòng ứng viên khi độ dài đo được còn nằm trong chiều rộng khả dụng.
  • Từ chối từ không vừa khi đứng riêng; không tự đặt điểm ngắt từ có gạch nối.
  • Từ chối toàn bộ kết quả khi cần nhiều hơn maxLines; không âm thầm bỏ các từ cuối.

Giới hạn dòng là quyết định bố cục. Chọn nó cùng đường cơ sở bắt đầu, khoảng cách dòng và chiều cao khả dụng. Hàm bao dòng không tính hộp nội dung dọc thay bạn.

Đổi chiều rộng nhãn, giữ hình
So sánh khái niệm hình thoi rỗng bên cạnh các dòng dài trong nhãn rộng và các dòng ngắn trong nhãn hẹp
Minh họa: nhãn hẹp hơn có thể cần nhiều dòng hơn. Thanh trừu tượng và khung chấm là gợi ý khái niệm; nội dung chữ, phép đo và trạng thái từ chối thực bên dưới đến từ mã khởi đầu trình duyệt độc lập.

Đo bằng phông chữ sẽ vẽ nhãn

getComputedTextLength() đo độ dài tính toán của văn bản SVG. Mã khởi đầu nhân bản phần tử chữ của nhãn làm phần tử đo ẩn bên trong cùng SVG đã gắn, rồi xóa sau khi lập các dòng. Cả hai dùng Arial, sans-serif tường minh ở 22 đơn vị.

Mã chờ document.fonts.ready trước khi đo. Điều này chờ tải phông chữ và bố cục liên quan của tài liệu; nó không nhúng phông chữ vào SVG đã xuất. Phông chữ khả dụng khác có thể cho độ dài dòng khác. Chạy lại bố cục khi nội dung chữ, thiết lập phông chữ hoặc chiều rộng khả dụng thay đổi.

Chạy mã khởi đầu hoàn chỉnh không có phụ thuộc

Lưu thành starter.html và mở trong trình duyệt. Nó tạo sáu trường hợp, in các phép đo và tuần tự hóa từng SVG kết quả. Hai trường hợp đầu chỉ đổi chiều rộng nhãn; trường hợp thứ ba đổi nội dung chữ. Các trường hợp còn lại thử từ chối và văn bản nguyên dạng.

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Measured SVG label lines</title>
<style>
  body { margin: 24px; font: 16px system-ui; background: #f6f2e8; }
  #cases { display: grid; grid-template-columns: repeat(2, 360px); gap: 20px; }
  figure { margin: 0; }
  svg { display: block; background: white; }
  figcaption { margin: 6px 0; }
  pre { white-space: pre-wrap; font-size: 12px; }
</style>
<h1>Keep artwork and label layout separate</h1>
<div id="cases"></div>
<pre id="report"></pre>
<script>
  const ns = 'http://www.w3.org/2000/svg';
  const emblem = 'M60 32 L100 72 L60 112 L20 72 Z M60 54 L78 72 L60 90 L42 72 Z';
  function element(name, attributes) {
    const node = document.createElementNS(ns, name);
    for (const [key, value] of Object.entries(attributes)) node.setAttribute(key, value);
    return node;
  }
  function wrapWords(text, source, { width, maxLines, lineHeight }) {
    text.replaceChildren();
    if (!(width > 0 && lineHeight > 0 && Number.isInteger(maxLines) && maxLines > 0)) {
      return { status: 'invalid-options', lines: [] };
    }
    const words = source.trim().split(/\s+/u).filter(Boolean);
    const probe = text.cloneNode(false);
    probe.setAttribute('visibility', 'hidden');
    text.parentNode.append(probe);
    const measure = value => { probe.textContent = value; return probe.getComputedTextLength(); };
    const lines = [];
    let line = '';
    try {
      for (const word of words) {
        const wordWidth = measure(word);
        if (wordWidth > width) {
          return { status: 'overlong-word', word, measuredWidth: wordWidth, lines: [] };
        }
        const candidate = line ? line + ' ' + word : word;
        if (line && measure(candidate) > width) {
          lines.push(line);
          line = word;
        } else line = candidate;
      }
      if (line) lines.push(line);
      if (lines.length > maxLines) {
        return { status: 'too-many-lines', requiredLines: lines.length, lines: [] };
      }
      const x = text.getAttribute('x');
      lines.forEach((value, index) => {
        const span = element('tspan', { x, dy: index ? lineHeight : 0 });
        span.textContent = value;
        text.append(span);
      });
      return { status: 'ok', lines,
        lengths: [...text.children].map(span => span.getComputedTextLength()) };
    } finally { probe.remove(); }
  }
  const cases = [
    { id: 'normal', source: 'Keep artwork and labels separate', width: 200, maxLines: 4 },
    { id: 'narrow', source: 'Keep artwork and labels separate', width: 110, maxLines: 5 },
    { id: 'changed', source: 'Keep the original artwork beside every exported label', width: 200, maxLines: 4 },
    { id: 'long-word', source: 'Artwork supercalifragilisticexpialidocious', width: 110, maxLines: 5 },
    { id: 'line-limit', source: 'Keep artwork and labels separate', width: 110, maxLines: 2 },
    { id: 'literal', source: 'Cove <g> & field', width: 200, maxLines: 4 },
  ];
  const entries = cases.map(test => {
    const figure = document.createElement('figure');
    const svg = element('svg', { width: 360, height: 240, viewBox: '0 0 360 240' });
    svg.append(element('path', { d: emblem, fill: '#176b5b', 'fill-rule': 'evenodd' }));
    svg.append(element('rect', { x: 140, y: 34, width: test.width, height: 170,
      fill: 'none', stroke: '#ce7b55', 'stroke-dasharray': '4 4' }));
    const text = element('text', { x: 140, y: 60, 'font-family': 'Arial, sans-serif',
      'font-size': 22, fill: '#172b34' });
    svg.append(text);
    const caption = document.createElement('figcaption');
    figure.append(svg, caption);
    document.getElementById('cases').append(figure);
    return { test, svg, text, caption };
  });
  async function run() {
    await document.fonts.ready;
    const results = entries.map(({ test, svg, text, caption }) => {
      const result = wrapWords(text, test.source, { ...test, lineHeight: 30 });
      caption.textContent = test.id + ': ' + result.status;
      return { ...test, ...result, fontFamily: getComputedStyle(text).fontFamily,
        fontSize: getComputedStyle(text).fontSize, tspanCount: text.children.length,
        pathD: svg.querySelector('path').getAttribute('d'),
        pathCount: svg.querySelectorAll('path').length,
        imageCount: svg.querySelectorAll('image').length,
        foreignObjectCount: svg.querySelectorAll('foreignObject').length,
        serialized: new XMLSerializer().serializeToString(svg) };
    });
    document.getElementById('report').textContent = JSON.stringify({
      fontStatus: document.fonts.status, policy: 'Whitespace words; reject a long word or too many lines without partial output', results,
    }, null, 2);
  }
  run();
</script>
</html>

Quy trình tạo mỗi dòng bằng textContent, thay vì chèn nội dung như mã đánh dấu. Trong trường hợp văn bản nguyên dạng, Cove <g> & field vẫn là chữ thông thường: SVG tuần tự hóa chứa ngoặc nhọn và dấu và đã mã hóa thoát, không phải phần tử nhóm bổ sung.

Bản sao đo được gắn nhưng ẩn, có cùng thuộc tính phông chữ với nhãn hiển thị. Nó bị xóa trong finally, kể cả khi bị từ chối. Chỉ thêm dòng sau khi vượt qua kiểm tra chiều rộng từ và số dòng tối đa. Quy trình xóa nhãn cũ trước, nên cập nhật bị từ chối không để lại chữ cũ hoặc một phần; hãy xử lý trạng thái trả về trong giao diện ứng dụng.

Sáu trường hợp gốc tạo ra gì

Chúng tôi chạy đúng mã khởi đầu trong Chrome khi phông chữ tài liệu báo loaded. Các độ dài bên dưới là độ dài văn bản SVG trong môi trường đó, không phải phạm vi nét chữ được vẽ hay bảo đảm tính di động phông chữ.

Trường hợpChiều rộng / số dòng tối đaKết quảDòng dài nhất quan sát được
Cụm từ gốc200 / 4Hai dòng: Keep artwork and; labels separate172.44
Cùng cụm từ, hộp hẹp110 / 5Bốn dòng: Keep; artwork; and labels; separate100.30
Nội dung dài hơn200 / 4Ba dòng198.13
Từ quá dài110 / 5overlong-word; không có tspanTừ đo được 311.82
Cụm từ hẹp, giới hạn hai dòng110 / 2too-many-lines; cần bốn; không có tspanKhông tạo nhãn
Ngoặc nhọn và dấu và nguyên dạng200 / 4Một dòng nguyên dạng162.66

Cụm từ gốc là Keep artwork and labels separate. Bản dài hơn là Keep the original artwork beside every exported label. Mọi dòng được chấp nhận đều có độ dài đo nằm trong chiều rộng đã chọn. Các trường hợp từ chối giữ hình minh họa hiển thị nhưng bỏ nhãn, giúp dễ kiểm tra trạng thái lỗi.

Cả sáu tệp tuần tự hóa giữ đường dẫn hình thoi gốc giống hệt, một phần tử văn bản SVG gốc và không có image hay foreignObject. Các trường hợp được chấp nhận lần lượt chứa hai, bốn, ba và một tspan; cả hai trường hợp từ chối không có tspan. Chúng tôi cũng mở lại sáu SVG tuần tự hóa làm ảnh trong cùng trình duyệt, tất cả tải ở 360×240. Đây là kiểm tra các tệp và trình duyệt này, không phải mọi trình chỉnh sửa hay bộ kết xuất.

Lưu SVG đã bố cục rồi kiểm thử bàn giao

XMLSerializer.serializeToString() tạo mã đánh dấu tuần tự hóa trong báo cáo. Lưu chuỗi serialized của trường hợp thành công thành tệp .svg. Tệp đã chứa các dòng tường minh; nó không cần quy trình JavaScript này để tái tạo chúng.

Nội dung vẫn là văn bản có thể sửa. Tệp lưu giữ yêu cầu font-family, không phải tệp phông chữ hay nét chữ đã chuyển thành đường bao. Đích có phông chữ khác có thể đổi hình thức chữ dù chuỗi dòng và vị trí giữ nguyên. Giữ nội dung gốc cùng đầu vào bố cục cạnh bản xuất, kiểm tra đích thực trước khi bàn giao. Với hình chữ cố định, tạo bản sao bàn giao có đường bao trong trình chỉnh sửa nguồn phù hợp, đồng thời giữ văn bản chỉnh sửa được trong bản chính.

Hướng dẫn văn bản foreignObject xử lý nhãn HTML biến mất trong bộ kết xuất khác. Mã khởi đầu này tạo văn bản SVG gốc ngay từ đầu. Hướng dẫn textLength trình bày cách co vừa một dòng bằng điều chỉnh độ dài; đó là quyết định khác với thêm các dòng đã đo. Dùng hướng dẫn căn dọc khi cần đặt toàn bộ nhãn tương đối với hình xung quanh.

Nêu rõ giới hạn của hàm bao dòng

Tách theo khoảng trắng là chính sách chủ đích nhỏ. Nó gộp các dấu cách lặp, tab và xuống dòng tường minh, và không xử lý quy tắc ngắt theo ngôn ngữ, ngắt từ có gạch nối, đoạn hai chiều hay các span định dạng có kiểu trộn. Chọn hệ thống bố cục khác hoặc mở rộng và kiểm thử chính sách khi văn bản cần những tính năng đó.

Kiểm tra chiều rộng cũng không bảo đảm phần nét chữ trang trí nhô ra vẫn nằm trong khung đã vẽ hay năm đường cơ sở vừa chiều cao bạn chọn. Kiểm tra chữ nhìn thấy ở kích thước bàn giao. Mẫu của chúng tôi dùng nhãn ngang, thuộc tính phông chữ cố định và bước dòng 30 đơn vị; đừng áp dụng số đo cho kiểu chữ khác mà không đo lại.

Nếu hình minh họa chỉ có dưới dạng PNG hoặc JPG và bạn cần hình chỉnh sửa được, hãy chuẩn bị SVG clipart bằng PerfectVector, kiểm tra cạnh và khoảng trống bên trong, rồi giữ hình riêng với nhãn. Gõ lại nội dung thành văn bản SVG gốc, chạy chính sách xuống dòng và kiểm tra bản xuất đã ghép. Vector hóa chuẩn bị đường dẫn minh họa; nó không chọn chỗ ngắt dòng hay khôi phục từ có thể sửa từ chữ được đồ lại. Đường dẫn tốt có sẵn có thể giữ nguyên. Hướng dẫn vector hóa giải thích bước khôi phục nguồn đó.

Câu hỏi thường gặp

tspan có tự quyết định chỗ ngắt từng dòng không? Mã khởi đầu quyết định bằng cách đo nhóm từ, rồi tạo một tspan được định vị cho mỗi dòng được chấp nhận. Nó không thử xuống dòng tự động dựa trên CSS của SVG 2.

Điều gì xảy ra với từ rộng hơn nhãn? Quy trình trả về overlong-word và không tạo nhãn. Mở rộng vùng, rút gọn nội dung hoặc cung cấp chính sách ngắt được kiểm thử riêng thay vì âm thầm cắt chữ.

maxLines có bỏ các từ còn lại không? Không. Chính sách từ chối kết quả khi cần quá nhiều dòng, báo số lượng cần và để lại không tspan. Ứng dụng phải xử lý trạng thái đó.

Các dòng đã xuất có trông giống hệt với phông chữ khác không? Các từ và vị trí tường minh giữ nguyên, nhưng tệp không nhúng phông chữ hay tạo đường bao nét chữ. Kiểm tra phông chữ khả dụng và hình thức chữ ở đích thực.

Nguồn tham khảo

  1. MDN phần tử tspan — phần chữ được định vị và thuộc tính lệch dòng.
  2. W3C văn bản SVG 2 — định vị tường minh và mô hình xuống dòng tự động riêng.
  3. MDN getComputedTextLength — đo văn bản SVG thực.
  4. MDN FontFaceSet.ready — chờ tải phông chữ và bố cục.
  5. MDN textContent — chèn văn bản thuần thay mã đánh dấu.
  6. MDN XMLSerializer — lưu mã đánh dấu SVG đã bố cục.

Với hình minh họa raster của bạn, hãy chuẩn bị clipart có thể chỉnh sửa, kiểm tra hình dạng và giữ bản chính. Thêm nội dung chữ riêng, rồi xác minh các dòng được chấp nhận và chữ nhìn thấy trong SVG đã lưu trước khi bàn giao.

Thêm từ blog

PerfectVector

Bắt đầu với SVG gọn sạch hơn
và dễ chỉnh sửa hơn

Không cần thẻ tín dụng