> [Site index](https://perfectvector.com/llms.txt) — available pages and descriptions.

# ตัดบรรทัดข้อความ SVG ด้วยการวัดและ tspan

> ตัดบรรทัดป้าย SVG โดยวัดคำด้วยฟอนต์จริงแล้วสร้าง tspan ทดสอบป้ายแคบ ปฏิเสธคำที่ล้นหรือบรรทัดเกินกำหนด แล้วบันทึกผลลัพธ์

Source: https://perfectvector.com/th/blog/svg-text-wrap-tspan

Language: th
Author: Irene Kim
Published: 2026-10-06T08:00:00+09:00
Updated: 2026-10-06T08:00:00+09:00

หากต้องการแบ่งป้ายข้อความ SVG ที่เปลี่ยนแปลงได้เป็นบรรทัดที่ระบุไว้อย่างชัดเจน ให้วัดกลุ่มคำที่เสนอด้วย `getComputedTextLength()` แล้วเขียน `<tspan>` หนึ่งตัวต่อบรรทัดที่ยอมรับ รอให้ฟอนต์ของเอกสารพร้อมก่อน และกำหนดว่าจะทำอย่างไรเมื่อคำหนึ่งกว้างกว่าป้ายหรือผลลัพธ์เกินจำนวนบรรทัดที่อนุญาต

ตัวอย่างเริ่มต้นด้านล่างใช้คำที่คั่นด้วยช่องว่าง และปฏิเสธการล้นทั้งสองแบบโดยไม่สร้างป้ายเพียงบางส่วน โดยแยกภาพรูปสี่เหลี่ยมข้าวหลามตัดกลวงต้นฉบับออกจากข้อความ การเปลี่ยนความกว้างป้ายจึงไม่วาดภาพใหม่

## เลือกกติกาการตัดบรรทัดก่อนวางตำแหน่ง

[องค์ประกอบ `tspan`](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/tspan) ช่วยวางตำแหน่งส่วนต่าง ๆ ของข้อความ SVG ฟังก์ชันของเราตั้ง `x` เท่ากันในทุกบรรทัด และตั้งระยะเลื่อน `dy` ในบรรทัดถัดไป ฟังก์ชันเป็นผู้ตัดสินใจว่าจะขึ้นบรรทัดตรงไหน ส่วนองค์ประกอบเก็บผลลัพธ์นั้น

นี่เป็นวิธีจัดวางแบบระบุตำแหน่งอย่างชัดเจนวิธีหนึ่ง [ข้อกำหนดข้อความ SVG 2](https://www.w3.org/TR/SVG2/text.html) ยังระบุการตัดบรรทัดอัตโนมัติผ่านพื้นที่เนื้อหาและการจัดข้อความด้วย CSS บทความนี้ไม่ได้ทดสอบความสามารถเหล่านั้นหรืออ้างว่าทุกเบราว์เซอร์ไม่มีให้ใช้

สำหรับย่อหน้า HTML ที่อยู่นอกภาพ ให้[ใช้ CSS จัดข้อความล้อมเงารูปร่าง SVG แบบ float](https://perfectvector.com/th/blog/css-text-around-svg-shape) รูปแบบนั้นตามแนวขอบภาพ โดยข้อความยังเป็น HTML ธรรมดา แทนที่จะกลายเป็นบรรทัดป้าย SVG ที่กำหนดตำแหน่ง

สำหรับตัวอย่างป้ายภาษาอังกฤษขนาดเล็กนี้ กติกาคือ:

* ยุบช่องว่างและลองคำตามลำดับเดิม โดยคั่นด้วยช่องว่างเดียว
* เก็บบรรทัดที่เสนอไว้เฉพาะเมื่อความยาวที่วัดได้พอดีกับความกว้างที่มี
* ปฏิเสธคำที่วางเดี่ยว ๆ แล้วยังไม่พอดี ไม่กำหนดจุดแบ่งคำด้วยยัติภังค์ขึ้นเอง
* ปฏิเสธผลลัพธ์ทั้งหมดเมื่อต้องใช้มากกว่า `maxLines` ไม่ตัดคำท้ายทิ้งโดยไม่แจ้ง

จำนวนบรรทัดสูงสุดเป็นการตัดสินใจด้านการจัดวาง เลือกพร้อมกับเส้นฐานเริ่มต้น ระยะระหว่างบรรทัด และความสูงที่มี ฟังก์ชันตัดบรรทัดไม่ได้คำนวณกรอบเนื้อหาแนวตั้งให้คุณ

**เปลี่ยนความกว้างป้ายโดยคงภาพเดิม**

![ภาพเปรียบเทียบแนวคิดรูปสี่เหลี่ยมข้าวหลามตัดกลวงข้างบรรทัดยาวในป้ายกว้าง และบรรทัดสั้นในป้ายแคบ](https://perfectvector.com/blog/svg-text-wrap-tspan/measured-lines.webp)

*ภาพประกอบ: ป้ายที่แคบลงอาจต้องใช้บรรทัดมากขึ้น แท่งนามธรรมและกรอบเส้นประใช้สื่อแนวคิด ส่วนข้อความจริง ค่าที่วัด และสถานะปฏิเสธด้านล่างมาจากตัวอย่างเบราว์เซอร์ที่ทำงานแยกต่างหาก*

## วัดด้วยฟอนต์ที่จะใช้แสดงป้าย

[`getComputedTextLength()`](https://developer.mozilla.org/en-US/docs/Web/API/SVGTextContentElement/getComputedTextLength) วัดความยาวข้อความ SVG ที่คำนวณแล้ว ตัวอย่างนี้โคลนองค์ประกอบข้อความของป้ายเป็นตัววัดที่ซ่อนไว้ภายใน SVG เดียวกันซึ่งแนบกับเอกสาร แล้วลบตัววัดหลังจากวางแผนบรรทัด ทั้งสองใช้ `Arial, sans-serif` ขนาด 22 หน่วยที่กำหนดไว้อย่างชัดเจน

โค้ดรอ [`document.fonts.ready`](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready) ก่อนวัด ซึ่งรอการโหลดฟอนต์ของเอกสารและการจัดวางที่เกี่ยวข้อง ไม่ได้ฝังฟอนต์ลงใน SVG ที่ส่งออก ฟอนต์ที่ใช้ได้ต่างกันอาจทำให้ความยาวบรรทัดต่างกัน คำนวณการจัดวางใหม่เมื่อข้อความ การตั้งค่าฟอนต์ หรือความกว้างที่มีเปลี่ยนไป

## เรียกใช้ตัวอย่างครบชุดโดยไม่ต้องมีไลบรารีเพิ่ม

บันทึกเป็น `starter.html` แล้วเปิดในเบราว์เซอร์ โค้ดสร้างหกกรณี แสดงค่าที่วัด และแปลง SVG ที่ได้แต่ละอันเป็นข้อความ สองกรณีแรกเปลี่ยนเฉพาะความกว้างป้าย กรณีที่สามเปลี่ยนข้อความ ส่วนที่เหลือทดสอบการปฏิเสธและข้อความตามตัวอักษร

```html
<!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>
```

ฟังก์ชันสร้างแต่ละบรรทัดด้วย [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) แทนการแทรกข้อความเป็นมาร์กอัป ในกรณีข้อความตามตัวอักษร `Cove <g> & field` ยังคงเป็นข้อความธรรมดา SVG ที่แปลงเป็นข้อความมีวงเล็บมุมและเครื่องหมายแอมเพอร์แซนด์ที่ escape แล้ว ไม่ได้มีองค์ประกอบกลุ่มเพิ่มมา

โคลนสำหรับวัดถูกแนบไว้แต่ซ่อนอยู่ และมีแอตทริบิวต์ฟอนต์เหมือนป้ายที่มองเห็น โคลนจะถูกลบใน `finally` รวมถึงเมื่อปฏิเสธผลลัพธ์ บรรทัดจะถูกเพิ่มต่อเมื่อผ่านการตรวจความกว้างคำและจำนวนบรรทัดสูงสุดแล้ว ฟังก์ชันล้างป้ายเดิมก่อน การอัปเดตที่ถูกปฏิเสธจึงไม่เหลือข้อความเก่าหรือข้อความบางส่วน ให้จัดการสถานะที่คืนมาในอินเทอร์เฟซแอปพลิเคชัน

## ผลจากกรณีต้นฉบับทั้งหก

เราเรียกใช้ตัวอย่างนี้ตามที่แสดงใน Chrome โดยฟอนต์ของเอกสารรายงานสถานะ `loaded` ความยาวด้านล่างเป็นความยาวข้อความ SVG ในสภาพแวดล้อมนั้น ไม่ใช่ขอบเขตรูปร่างตัวอักษรที่วาดจริงหรือการรับประกันว่าฟอนต์จะให้ผลเหมือนกันทุกระบบ

| กรณี                                 | ความกว้าง / บรรทัดสูงสุด | ผลลัพธ์                                        | บรรทัดยาวที่สุดที่พบ |
| ------------------------------------ | ------------------------ | ---------------------------------------------- | -------------------- |
| วลีต้นฉบับ                           | 200 / 4                  | สองบรรทัด: Keep artwork and; labels separate   | 172.44               |
| วลีเดิมในกรอบแคบ                     | 110 / 5                  | สี่บรรทัด: Keep; artwork; and labels; separate | 100.30               |
| ข้อความยาวขึ้น                       | 200 / 4                  | สามบรรทัด                                      | 198.13               |
| คำยาวเกินไป                          | 110 / 5                  | overlong-word; ไม่มี tspan                     | คำวัดได้ 311.82      |
| วลีในกรอบแคบ จำกัดสองบรรทัด          | 110 / 2                  | too-many-lines; ต้องใช้สี่; ไม่มี tspan        | ไม่สร้างป้าย         |
| วงเล็บมุมและแอมเพอร์แซนด์ตามตัวอักษร | 200 / 4                  | หนึ่งบรรทัดตามตัวอักษร                         | 162.66               |

วลีต้นฉบับคือ `Keep artwork and labels separate` เวอร์ชันที่ยาวขึ้นคือ `Keep the original artwork beside every exported label` ทุกบรรทัดที่ยอมรับมีความยาวที่วัดได้ไม่เกินความกว้างที่เลือก กรณีที่ปฏิเสธยังแสดงภาพแต่ไม่ใส่ป้าย จึงตรวจสอบสถานะล้มเหลวได้ง่าย

ไฟล์ที่แปลงเป็นข้อความทั้งหกยังมีพาธรูปสี่เหลี่ยมข้าวหลามตัดต้นฉบับเหมือนกัน มีองค์ประกอบข้อความเนทีฟหนึ่งตัว และไม่มีรูปภาพหรือ `foreignObject` กรณีที่ยอมรับมี tspan สอง สี่ สาม และหนึ่งตัวตามลำดับ ส่วนสองกรณีที่ปฏิเสธไม่มีเลย เรายังเปิด SVG ทั้งหกที่แปลงเป็นข้อความแล้วกลับมาเป็นภาพในเบราว์เซอร์เดิม โดยทั้งหมดโหลดที่ 360×240 นี่เป็นการตรวจไฟล์เหล่านี้กับเบราว์เซอร์นี้ ไม่ใช่ทุกโปรแกรมแก้ไขหรือเอนจินแสดงผล

## บันทึก SVG ที่จัดวางแล้วและทดสอบไฟล์ส่งมอบ

[`XMLSerializer.serializeToString()`](https://developer.mozilla.org/en-US/docs/Web/API/XMLSerializer/serializeToString) สร้างมาร์กอัปที่แปลงเป็นข้อความในรายงาน บันทึกสตริง `serialized` ของกรณีที่สำเร็จเป็นไฟล์ `.svg` ไฟล์มีบรรทัดที่ระบุไว้อย่างชัดเจนอยู่แล้ว จึงไม่ต้องใช้ฟังก์ชัน JavaScript นี้สร้างบรรทัดซ้ำ

คำยังคงเป็นข้อความที่แก้ไขได้ ไฟล์ที่บันทึกเก็บคำขอใช้ตระกูลฟอนต์ ไม่ได้เก็บไฟล์ฟอนต์หรือรูปร่างตัวอักษรที่แปลงเป็นเส้นขอบ ปลายทางที่มีฟอนต์อื่นอาจทำให้ตัวอักษรเปลี่ยน แม้สตริงและตำแหน่งบรรทัดยังเหมือนเดิม เก็บข้อความและข้อมูลการจัดวางต้นฉบับไว้ข้างสำเนาที่ส่งออก และตรวจปลายทางจริงก่อนส่งมอบ หากต้องการรูปร่างตัวอักษรคงที่ ให้สร้างสำเนาส่งมอบที่แปลงตัวอักษรเป็นเส้นขอบด้วยโปรแกรมต้นทางที่เหมาะสม พร้อมเก็บข้อความที่แก้ไขได้ไว้ในไฟล์หลัก

[คู่มือข้อความ foreignObject](https://perfectvector.com/th/blog/svg-foreignobject-text) อธิบายป้าย HTML ที่หายไปในเอนจินอื่น ตัวอย่างนี้สร้างข้อความเนทีฟตั้งแต่แรก [คู่มือ textLength](https://perfectvector.com/th/blog/svg-textlength-fit) กล่าวถึงการทำให้บรรทัดเดียวพอดีด้วยการปรับความยาว ซึ่งเป็นการตัดสินใจคนละแบบกับการเพิ่มบรรทัดจากการวัด ใช้[คู่มือการจัดแนวตั้ง](https://perfectvector.com/th/blog/svg-text-vertical-alignment) เมื่อต้องวางตำแหน่งป้ายทั้งชุดเทียบกับภาพรอบข้าง

## ระบุข้อจำกัดของฟังก์ชันให้ชัดเจน

การแยกคำด้วยช่องว่างเป็นกติกาที่ตั้งใจให้เรียบง่าย โดยยุบช่องว่างซ้ำ แท็บ และการขึ้นบรรทัดใหม่ที่ระบุไว้ และไม่จัดการกฎตัดบรรทัดเฉพาะภาษา การแบ่งคำด้วยยัติภังค์ ย่อหน้าสองทิศทาง หรือข้อความหลายช่วงที่มีรูปแบบผสม หากข้อความต้องการความสามารถเหล่านั้น ให้เลือกระบบจัดวางอื่นหรือขยายกติกาแล้วทดสอบ

การตรวจความกว้างยังไม่รับประกันว่าส่วนตกแต่งของตัวอักษรที่ยื่นออกมาจะอยู่ในกรอบที่วาด หรือเส้นฐานห้าเส้นจะพอดีกับความสูงที่เลือก ตรวจตัวอักษรที่มองเห็นในขนาดส่งมอบ ชุดทดสอบของเราใช้ป้ายแนวนอน แอตทริบิวต์ฟอนต์คงที่ และระยะบรรทัด 30 หน่วย อย่านำค่าที่วัดไปใช้กับฟอนต์อื่นโดยไม่วัดใหม่

หากภาพมีเพียง PNG หรือ JPG และต้องการงานที่แก้ไขได้ ให้[เตรียม SVG คลิปอาร์ตด้วย PerfectVector](https://perfectvector.com/th/clipart-to-svg) ตรวจขอบและช่องว่างด้านใน แล้วแยกภาพออกจากป้าย พิมพ์คำใหม่เป็นข้อความเนทีฟ เรียกใช้กติกาตัดบรรทัด และตรวจไฟล์ส่งออกที่ประกอบแล้ว การแปลงเป็นเวกเตอร์เตรียมพาธของภาพ ไม่ได้เลือกจุดตัดบรรทัดหรือกู้คำที่แก้ไขได้จากตัวอักษรที่ลากเส้นตาม หากมีพาธที่ดีอยู่แล้วก็ใช้ต่อได้ [คู่มือการแปลงเป็นเวกเตอร์](https://perfectvector.com/th/blog/what-is-image-vectorization) อธิบายขั้นตอนกู้คืนต้นทางนี้

## คำถามที่พบบ่อย

**tspan ตัดสินใจจุดขึ้นบรรทัดเองหรือไม่?**
ตัวอย่างนี้ตัดสินใจด้วยการวัดกลุ่มคำ แล้วสร้าง tspan ที่กำหนดตำแหน่งหนึ่งตัวต่อบรรทัดที่ยอมรับ ไม่ได้ทดสอบการตัดบรรทัดอัตโนมัติของ SVG 2 ที่อาศัย CSS

**จะเกิดอะไรขึ้นกับคำที่กว้างกว่าป้าย?**
ฟังก์ชันคืนค่า overlong-word และไม่สร้างป้าย ให้ขยายพื้นที่ ย่อข้อความ หรือใช้กติกาแบ่งคำที่ทดสอบแยกแล้ว แทนการตัดข้อความที่ล้นทิ้งโดยไม่แจ้ง

**maxLines ลบคำที่เหลือหรือไม่?**
ไม่ กติกานี้ปฏิเสธผลลัพธ์เมื่อต้องใช้บรรทัดมากเกินไป รายงานจำนวนที่ต้องใช้ และไม่เหลือ tspan แอปพลิเคชันต้องจัดการสถานะนั้น

**บรรทัดที่ส่งออกจะดูเหมือนเดิมเมื่อใช้ฟอนต์อื่นหรือไม่?**
คำและตำแหน่งที่ระบุชัดเจนยังอยู่ แต่ไฟล์ไม่ได้ฝังฟอนต์หรือแปลงตัวอักษรเป็นเส้นขอบ ตรวจฟอนต์ที่มีและรูปร่างตัวอักษรในปลายทางจริง

## แหล่งข้อมูล

1. [องค์ประกอบ tspan ของ MDN](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/tspan) — ส่วนข้อความที่กำหนดตำแหน่งและแอตทริบิวต์เลื่อนบรรทัด
2. [ข้อความ SVG 2 ของ W3C](https://www.w3.org/TR/SVG2/text.html) — การกำหนดตำแหน่งชัดเจนและโมเดลตัดบรรทัดอัตโนมัติที่แยกต่างหาก
3. [MDN getComputedTextLength](https://developer.mozilla.org/en-US/docs/Web/API/SVGTextContentElement/getComputedTextLength) — การวัดข้อความ SVG จริง
4. [MDN FontFaceSet.ready](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready) — การรอโหลดฟอนต์และจัดวาง
5. [MDN textContent](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) — การใส่ข้อความธรรมดาแทนมาร์กอัป
6. [MDN XMLSerializer](https://developer.mozilla.org/en-US/docs/Web/API/XMLSerializer/serializeToString) — การบันทึกมาร์กอัป SVG ที่จัดวางแล้ว

***

สำหรับภาพราสเตอร์ของคุณ ให้[เตรียมคลิปอาร์ตที่แก้ไขได้](https://perfectvector.com/th/clipart-to-svg) ตรวจรูปร่างและเก็บไฟล์หลัก เพิ่มข้อความแยกต่างหาก แล้วตรวจบรรทัดที่ยอมรับและตัวอักษรที่มองเห็นใน SVG ที่บันทึกก่อนส่งมอบ

## Explore

- [Read the HTML article](https://perfectvector.com/th/blog/svg-text-wrap-tspan)
- [SVG.js: นำเข้างาน แก้พาธ และส่งออก SVG](https://perfectvector.com/th/blog/svgjs-import-export-artwork.md)
- [หมุด SVG ใน Leaflet: จัดปลายด้วย iconAnchor](https://perfectvector.com/th/blog/leaflet-svg-marker-anchor.md)
- [Product facts](https://perfectvector.com/product-facts.md)
- [FAQ](https://perfectvector.com/faq.md)
- [Pricing and credits](https://perfectvector.com/pricing.md)
