ตัดบรรทัดข้อความ SVG ด้วยการวัดและ tspan
ตัดบรรทัดป้าย SVG โดยวัดคำด้วยฟอนต์จริงแล้วสร้าง tspan ทดสอบป้ายแคบ ปฏิเสธคำที่ล้นหรือบรรทัดเกินกำหนด แล้วบันทึกผลลัพธ์
ในหน้านี้
หากต้องการแบ่งป้ายข้อความ SVG ที่เปลี่ยนแปลงได้เป็นบรรทัดที่ระบุไว้อย่างชัดเจน ให้วัดกลุ่มคำที่เสนอด้วย getComputedTextLength() แล้วเขียน <tspan> หนึ่งตัวต่อบรรทัดที่ยอมรับ รอให้ฟอนต์ของเอกสารพร้อมก่อน และกำหนดว่าจะทำอย่างไรเมื่อคำหนึ่งกว้างกว่าป้ายหรือผลลัพธ์เกินจำนวนบรรทัดที่อนุญาต
ตัวอย่างเริ่มต้นด้านล่างใช้คำที่คั่นด้วยช่องว่าง และปฏิเสธการล้นทั้งสองแบบโดยไม่สร้างป้ายเพียงบางส่วน โดยแยกภาพรูปสี่เหลี่ยมข้าวหลามตัดกลวงต้นฉบับออกจากข้อความ การเปลี่ยนความกว้างป้ายจึงไม่วาดภาพใหม่
เลือกกติกาการตัดบรรทัดก่อนวางตำแหน่ง
องค์ประกอบ tspan ช่วยวางตำแหน่งส่วนต่าง ๆ ของข้อความ SVG ฟังก์ชันของเราตั้ง x เท่ากันในทุกบรรทัด และตั้งระยะเลื่อน dy ในบรรทัดถัดไป ฟังก์ชันเป็นผู้ตัดสินใจว่าจะขึ้นบรรทัดตรงไหน ส่วนองค์ประกอบเก็บผลลัพธ์นั้น
นี่เป็นวิธีจัดวางแบบระบุตำแหน่งอย่างชัดเจนวิธีหนึ่ง ข้อกำหนดข้อความ SVG 2 ยังระบุการตัดบรรทัดอัตโนมัติผ่านพื้นที่เนื้อหาและการจัดข้อความด้วย CSS บทความนี้ไม่ได้ทดสอบความสามารถเหล่านั้นหรืออ้างว่าทุกเบราว์เซอร์ไม่มีให้ใช้
สำหรับย่อหน้า HTML ที่อยู่นอกภาพ ให้ใช้ CSS จัดข้อความล้อมเงารูปร่าง SVG แบบ float รูปแบบนั้นตามแนวขอบภาพ โดยข้อความยังเป็น HTML ธรรมดา แทนที่จะกลายเป็นบรรทัดป้าย SVG ที่กำหนดตำแหน่ง
สำหรับตัวอย่างป้ายภาษาอังกฤษขนาดเล็กนี้ กติกาคือ:
- ยุบช่องว่างและลองคำตามลำดับเดิม โดยคั่นด้วยช่องว่างเดียว
- เก็บบรรทัดที่เสนอไว้เฉพาะเมื่อความยาวที่วัดได้พอดีกับความกว้างที่มี
- ปฏิเสธคำที่วางเดี่ยว ๆ แล้วยังไม่พอดี ไม่กำหนดจุดแบ่งคำด้วยยัติภังค์ขึ้นเอง
- ปฏิเสธผลลัพธ์ทั้งหมดเมื่อต้องใช้มากกว่า
maxLinesไม่ตัดคำท้ายทิ้งโดยไม่แจ้ง
จำนวนบรรทัดสูงสุดเป็นการตัดสินใจด้านการจัดวาง เลือกพร้อมกับเส้นฐานเริ่มต้น ระยะระหว่างบรรทัด และความสูงที่มี ฟังก์ชันตัดบรรทัดไม่ได้คำนวณกรอบเนื้อหาแนวตั้งให้คุณ

วัดด้วยฟอนต์ที่จะใช้แสดงป้าย
getComputedTextLength() วัดความยาวข้อความ SVG ที่คำนวณแล้ว ตัวอย่างนี้โคลนองค์ประกอบข้อความของป้ายเป็นตัววัดที่ซ่อนไว้ภายใน SVG เดียวกันซึ่งแนบกับเอกสาร แล้วลบตัววัดหลังจากวางแผนบรรทัด ทั้งสองใช้ Arial, sans-serif ขนาด 22 หน่วยที่กำหนดไว้อย่างชัดเจน
โค้ดรอ document.fonts.ready ก่อนวัด ซึ่งรอการโหลดฟอนต์ของเอกสารและการจัดวางที่เกี่ยวข้อง ไม่ได้ฝังฟอนต์ลงใน SVG ที่ส่งออก ฟอนต์ที่ใช้ได้ต่างกันอาจทำให้ความยาวบรรทัดต่างกัน คำนวณการจัดวางใหม่เมื่อข้อความ การตั้งค่าฟอนต์ หรือความกว้างที่มีเปลี่ยนไป
เรียกใช้ตัวอย่างครบชุดโดยไม่ต้องมีไลบรารีเพิ่ม
บันทึกเป็น starter.html แล้วเปิดในเบราว์เซอร์ โค้ดสร้างหกกรณี แสดงค่าที่วัด และแปลง SVG ที่ได้แต่ละอันเป็นข้อความ สองกรณีแรกเปลี่ยนเฉพาะความกว้างป้าย กรณีที่สามเปลี่ยนข้อความ ส่วนที่เหลือทดสอบการปฏิเสธและข้อความตามตัวอักษร
<!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 แทนการแทรกข้อความเป็นมาร์กอัป ในกรณีข้อความตามตัวอักษร 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() สร้างมาร์กอัปที่แปลงเป็นข้อความในรายงาน บันทึกสตริง serialized ของกรณีที่สำเร็จเป็นไฟล์ .svg ไฟล์มีบรรทัดที่ระบุไว้อย่างชัดเจนอยู่แล้ว จึงไม่ต้องใช้ฟังก์ชัน JavaScript นี้สร้างบรรทัดซ้ำ
คำยังคงเป็นข้อความที่แก้ไขได้ ไฟล์ที่บันทึกเก็บคำขอใช้ตระกูลฟอนต์ ไม่ได้เก็บไฟล์ฟอนต์หรือรูปร่างตัวอักษรที่แปลงเป็นเส้นขอบ ปลายทางที่มีฟอนต์อื่นอาจทำให้ตัวอักษรเปลี่ยน แม้สตริงและตำแหน่งบรรทัดยังเหมือนเดิม เก็บข้อความและข้อมูลการจัดวางต้นฉบับไว้ข้างสำเนาที่ส่งออก และตรวจปลายทางจริงก่อนส่งมอบ หากต้องการรูปร่างตัวอักษรคงที่ ให้สร้างสำเนาส่งมอบที่แปลงตัวอักษรเป็นเส้นขอบด้วยโปรแกรมต้นทางที่เหมาะสม พร้อมเก็บข้อความที่แก้ไขได้ไว้ในไฟล์หลัก
คู่มือข้อความ foreignObject อธิบายป้าย HTML ที่หายไปในเอนจินอื่น ตัวอย่างนี้สร้างข้อความเนทีฟตั้งแต่แรก คู่มือ textLength กล่าวถึงการทำให้บรรทัดเดียวพอดีด้วยการปรับความยาว ซึ่งเป็นการตัดสินใจคนละแบบกับการเพิ่มบรรทัดจากการวัด ใช้คู่มือการจัดแนวตั้ง เมื่อต้องวางตำแหน่งป้ายทั้งชุดเทียบกับภาพรอบข้าง
ระบุข้อจำกัดของฟังก์ชันให้ชัดเจน
การแยกคำด้วยช่องว่างเป็นกติกาที่ตั้งใจให้เรียบง่าย โดยยุบช่องว่างซ้ำ แท็บ และการขึ้นบรรทัดใหม่ที่ระบุไว้ และไม่จัดการกฎตัดบรรทัดเฉพาะภาษา การแบ่งคำด้วยยัติภังค์ ย่อหน้าสองทิศทาง หรือข้อความหลายช่วงที่มีรูปแบบผสม หากข้อความต้องการความสามารถเหล่านั้น ให้เลือกระบบจัดวางอื่นหรือขยายกติกาแล้วทดสอบ
การตรวจความกว้างยังไม่รับประกันว่าส่วนตกแต่งของตัวอักษรที่ยื่นออกมาจะอยู่ในกรอบที่วาด หรือเส้นฐานห้าเส้นจะพอดีกับความสูงที่เลือก ตรวจตัวอักษรที่มองเห็นในขนาดส่งมอบ ชุดทดสอบของเราใช้ป้ายแนวนอน แอตทริบิวต์ฟอนต์คงที่ และระยะบรรทัด 30 หน่วย อย่านำค่าที่วัดไปใช้กับฟอนต์อื่นโดยไม่วัดใหม่
หากภาพมีเพียง PNG หรือ JPG และต้องการงานที่แก้ไขได้ ให้เตรียม SVG คลิปอาร์ตด้วย PerfectVector ตรวจขอบและช่องว่างด้านใน แล้วแยกภาพออกจากป้าย พิมพ์คำใหม่เป็นข้อความเนทีฟ เรียกใช้กติกาตัดบรรทัด และตรวจไฟล์ส่งออกที่ประกอบแล้ว การแปลงเป็นเวกเตอร์เตรียมพาธของภาพ ไม่ได้เลือกจุดตัดบรรทัดหรือกู้คำที่แก้ไขได้จากตัวอักษรที่ลากเส้นตาม หากมีพาธที่ดีอยู่แล้วก็ใช้ต่อได้ คู่มือการแปลงเป็นเวกเตอร์ อธิบายขั้นตอนกู้คืนต้นทางนี้
คำถามที่พบบ่อย
tspan ตัดสินใจจุดขึ้นบรรทัดเองหรือไม่? ตัวอย่างนี้ตัดสินใจด้วยการวัดกลุ่มคำ แล้วสร้าง tspan ที่กำหนดตำแหน่งหนึ่งตัวต่อบรรทัดที่ยอมรับ ไม่ได้ทดสอบการตัดบรรทัดอัตโนมัติของ SVG 2 ที่อาศัย CSS
จะเกิดอะไรขึ้นกับคำที่กว้างกว่าป้าย? ฟังก์ชันคืนค่า overlong-word และไม่สร้างป้าย ให้ขยายพื้นที่ ย่อข้อความ หรือใช้กติกาแบ่งคำที่ทดสอบแยกแล้ว แทนการตัดข้อความที่ล้นทิ้งโดยไม่แจ้ง
maxLines ลบคำที่เหลือหรือไม่? ไม่ กติกานี้ปฏิเสธผลลัพธ์เมื่อต้องใช้บรรทัดมากเกินไป รายงานจำนวนที่ต้องใช้ และไม่เหลือ tspan แอปพลิเคชันต้องจัดการสถานะนั้น
บรรทัดที่ส่งออกจะดูเหมือนเดิมเมื่อใช้ฟอนต์อื่นหรือไม่? คำและตำแหน่งที่ระบุชัดเจนยังอยู่ แต่ไฟล์ไม่ได้ฝังฟอนต์หรือแปลงตัวอักษรเป็นเส้นขอบ ตรวจฟอนต์ที่มีและรูปร่างตัวอักษรในปลายทางจริง
แหล่งข้อมูล
- องค์ประกอบ tspan ของ MDN — ส่วนข้อความที่กำหนดตำแหน่งและแอตทริบิวต์เลื่อนบรรทัด
- ข้อความ SVG 2 ของ W3C — การกำหนดตำแหน่งชัดเจนและโมเดลตัดบรรทัดอัตโนมัติที่แยกต่างหาก
- MDN getComputedTextLength — การวัดข้อความ SVG จริง
- MDN FontFaceSet.ready — การรอโหลดฟอนต์และจัดวาง
- MDN textContent — การใส่ข้อความธรรมดาแทนมาร์กอัป
- MDN XMLSerializer — การบันทึกมาร์กอัป SVG ที่จัดวางแล้ว
สำหรับภาพราสเตอร์ของคุณ ให้เตรียมคลิปอาร์ตที่แก้ไขได้ ตรวจรูปร่างและเก็บไฟล์หลัก เพิ่มข้อความแยกต่างหาก แล้วตรวจบรรทัดที่ยอมรับและตัวอักษรที่มองเห็นใน SVG ที่บันทึกก่อนส่งมอบ
เพิ่มเติมจากบล็อก

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

หมุด SVG ใน Leaflet: จัดปลายด้วย iconAnchor
วางหมุด SVG ใน Leaflet โดยแมปปลายที่เห็นกับ iconAnchor คำนึงถึงขอบว่าง viewBox การปรับสัดส่วน และการเปลี่ยนขนาดไอคอน
