Zawijanie tekstu SVG: mierzone wiersze z tspan
Zawijaj etykiety SVG, mierząc słowa właściwą czcionką i tworząc tspany. Sprawdź wąskie pola, odrzucaj zbyt długie słowa lub nadmiar wierszy i zapisz wynik.
Na tej stronie
Aby podzielić zmienną etykietę SVG na jawnie zapisane wiersze, zmierz proponowane grupy słów metodą getComputedTextLength(), a następnie zapisz po jednym <tspan> na każdy zaakceptowany wiersz. Najpierw poczekaj na czcionki dokumentu i ustal, co zrobić, gdy jedno słowo jest szersze od etykiety lub wynik przekracza limit wierszy.
Poniższy przykład korzysta ze słów rozdzielonych białymi znakami i odrzuca oba rodzaje przepełnienia bez tworzenia częściowej etykiety. Oryginalna ilustracja pustego rombu pozostaje oddzielona od tekstu, więc zmiana szerokości etykiety nie przerysowuje grafiki.
Ustal zasady zawijania przed rozmieszczeniem wierszy
Element tspan pozwala pozycjonować fragmenty tekstu SVG. Nasza funkcja ustawia jednakowe x dla każdego wiersza oraz przesunięcie dy dla kolejnych wierszy. To funkcja decyduje o podziale; element przechowuje wynik.
Jest to jeden ze sposobów jawnego rozmieszczania tekstu. Specyfikacja tekstu SVG 2 definiuje również automatyczne zawijanie z wykorzystaniem obszaru treści i układu tekstu CSS. Ten artykuł nie testuje tych funkcji ani nie twierdzi, że brakuje ich w każdej przeglądarce.
W przypadku akapitu HTML poza ilustracją oblej tekstem pływającą sylwetkę SVG za pomocą CSS. Taki układ podąża za konturem obrazu, a treść pozostaje zwykłym HTML zamiast stawać się pozycjonowanymi wierszami etykiety SVG.
W tym niewielkim przykładzie angielskiej etykiety obowiązują następujące zasady:
- Sprowadź białe znaki do pojedynczych spacji i sprawdzaj słowa w pierwotnej kolejności.
- Zachowuj proponowany wiersz tylko wtedy, gdy jego zmierzona długość mieści się w dostępnej szerokości.
- Odrzuć słowo, które nie mieści się samodzielnie; nie wymyślaj miejsca podziału wyrazu.
- Odrzuć cały wynik, gdy wymaga więcej niż
maxLines; nie usuwaj po cichu ostatnich słów.
Limit wierszy jest decyzją dotyczącą układu. Dobierz go razem z początkową linią bazową, odstępem między wierszami i dostępną wysokością. Funkcja zawijająca nie oblicza za ciebie pionowego obszaru treści.

Mierz czcionką, która będzie rysować etykietę
getComputedTextLength() mierzy obliczoną długość tekstu SVG. Ten przykład klonuje element tekstowy etykiety jako ukrytą sondę wewnątrz tego samego SVG dołączonego do dokumentu, a po zaplanowaniu wierszy usuwa sondę. Oba elementy mają jawnie ustawione Arial, sans-serif o rozmiarze 22 jednostek.
Kod przed pomiarem czeka na document.fonts.ready. Oznacza to oczekiwanie na wczytanie czcionek dokumentu i związane z nimi przeliczenie układu; nie osadza czcionki w eksportowanym SVG. Inna dostępna czcionka może dać inne długości wierszy. Przelicz układ po zmianie tekstu, ustawień czcionki lub dostępnej szerokości.
Uruchom kompletny przykład bez zależności
Zapisz poniższy kod jako starter.html i otwórz go w przeglądarce. Tworzy sześć przypadków, wypisuje ich pomiary i serializuje każdy wynikowy SVG. Pierwsze dwa przypadki zmieniają tylko szerokość etykiety; trzeci zmienia tekst. Pozostałe sprawdzają odrzucanie wyniku i tekst dosłowny.
<!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>Funkcja tworzy każdy wiersz za pomocą textContent, zamiast wstawiać treść jako znaczniki. W przypadku tekstu dosłownego Cove <g> & field pozostaje zwykłym tekstem: zserializowany SVG zawiera zakodowane nawiasy ostrokątne i znak ampersand, a nie dodatkowy element grupy.
Klon pomiarowy jest dołączony do dokumentu, lecz ukryty, i ma takie same atrybuty czcionki jak widoczna etykieta. Jest usuwany w finally, również przy odrzuceniu wyniku. Wiersze są dodawane dopiero po pomyślnym sprawdzeniu szerokości słów i maksymalnej liczby wierszy. Funkcja najpierw usuwa poprzednią etykietę, więc odrzucona aktualizacja nie pozostawia starego ani częściowego tekstu; obsłuż zwrócony stan w interfejsie aplikacji.
Wyniki sześciu oryginalnych przypadków
Uruchomiliśmy dokładnie ten przykład w Chrome, gdzie czcionki dokumentu zgłaszały stan loaded. Poniższe zmierzone długości są długościami tekstu SVG w tym środowisku, a nie granicami namalowanych glifów ani gwarancją przenośności czcionek.
| Przypadek | Szerokość / maksymalna liczba wierszy | Wynik | Najdłuższy zaobserwowany wiersz |
|---|---|---|---|
| Oryginalna fraza | 200 / 4 | Dwa wiersze: Keep artwork and; labels separate | 172.44 |
| Ta sama fraza, wąskie pole | 110 / 5 | Cztery wiersze: Keep; artwork; and labels; separate | 100.30 |
| Dłuższy tekst | 200 / 4 | Trzy wiersze | 198.13 |
| Zbyt długie słowo | 110 / 5 | overlong-word; zero tspanów | Słowo zmierzone jako 311.82 |
| Wąska fraza, limit dwóch wierszy | 110 / 2 | too-many-lines; wymagane cztery; zero tspanów | Nie utworzono etykiety |
| Dosłowne nawiasy ostrokątne i ampersand | 200 / 4 | Jeden dosłowny wiersz | 162.66 |
Oryginalna fraza to Keep artwork and labels separate. Dłuższa wersja to Keep the original artwork beside every exported label. Każdy zaakceptowany wiersz mieścił się w wybranej szerokości. W odrzuconych przypadkach ilustracja pozostała widoczna, a etykieta została pominięta, co ułatwia sprawdzenie stanu błędu.
Wszystkie sześć zserializowanych plików zachowało identyczną oryginalną ścieżkę rombu, jeden natywny element tekstowy oraz brak obrazu i elementu foreignObject. Zaakceptowane przypadki zawierały odpowiednio dwa, cztery, trzy i jeden tspan; oba odrzucone przypadki zawierały zero. Otworzyliśmy też ponownie sześć zserializowanych SVG jako obrazy w tej samej przeglądarce, gdzie wszystkie wczytały się w rozmiarze 360×240. To sprawdzenie tych plików i tej przeglądarki, a nie każdego edytora lub renderera.
Zapisz SVG z układem i sprawdź plik docelowy
XMLSerializer.serializeToString() tworzy zserializowane znaczniki w raporcie. Zapisz ciąg serialized z udanego przypadku jako plik .svg. Plik zawiera już jawnie zapisane wiersze; nie potrzebuje tej funkcji JavaScript, aby je odtworzyć.
Słowa nadal pozostają edytowalnym tekstem. Zapisany plik zachowuje żądanie rodziny czcionki, a nie plik czcionki lub glify zamienione na krzywe. Środowisko docelowe z inną czcionką może zmienić wygląd liter, mimo zachowania tekstów i pozycji wierszy. Zachowaj oryginalną treść i parametry układu obok wyeksportowanej kopii, a przed przekazaniem sprawdź rzeczywiste środowisko docelowe. Jeśli kształty liter muszą być stałe, przygotuj kopię do przekazania z tekstem zamienionym na krzywe w odpowiednim edytorze źródłowym, zachowując edytowalny tekst w pliku głównym.
Poradnik o tekście foreignObject dotyczy etykiet HTML znikających w innym rendererze. Ten przykład od początku buduje natywny tekst. Poradnik textLength omawia dopasowanie pojedynczego wiersza przez zmianę długości; to inna decyzja niż dodawanie mierzonych wierszy. Skorzystaj z poradnika wyrównania pionowego, jeśli musisz ustawić całą etykietę względem otaczającej grafiki.
Jasno określ ograniczenia funkcji zawijającej
Dzielenie po białych znakach jest celowo prostą zasadą. Sprowadza powtarzane spacje, tabulatory i jawne znaki nowego wiersza do jednego separatora, a nie obsługuje reguł łamania właściwych językom, dzielenia wyrazów, akapitów dwukierunkowych ani bogato formatowanych fragmentów o mieszanych stylach. Wybierz inny system układu lub rozbuduj i przetestuj zasady, gdy twój tekst wymaga tych funkcji.
Sprawdzenie szerokości nie gwarantuje też, że ozdobne części glifów nie wyjdą poza narysowaną ramkę ani że pięć linii bazowych zmieści się w wybranej wysokości. Obejrzyj widoczne litery w rozmiarze docelowym. Nasz przykład używa poziomych etykiet, stałych atrybutów czcionki i kroku wiersza 30 jednostek; nie przenoś jego wyników pomiarów na inny krój bez ponownego mierzenia.
Jeśli ilustracja istnieje tylko jako PNG lub JPG, a potrzebujesz edytowalnej grafiki, przygotuj clipart SVG za pomocą PerfectVector, sprawdź krawędzie i wewnętrzne otwory, a grafikę trzymaj oddzielnie od etykiety. Wpisz treść ponownie jako natywny tekst, zastosuj zasady zawijania i sprawdź złożony eksport. Wektoryzacja przygotowuje ścieżki ilustracji; nie wybiera podziału wierszy ani nie odzyskuje edytowalnych słów z obrysowanych liter. Istniejące dobre ścieżki mogą pozostać bez zmian. Poradnik wektoryzacji wyjaśnia ten etap odzyskiwania źródła.
FAQ
Czy tspan automatycznie decyduje o miejscu podziału wiersza? Ten przykład podejmuje decyzję przez pomiar grup słów, a następnie tworzy po jednym pozycjonowanym tspanie dla każdego zaakceptowanego wiersza. Nie testuje automatycznego zawijania opartego na CSS w SVG 2.
Co dzieje się ze słowem szerszym od etykiety? Funkcja zwraca overlong-word i nie tworzy etykiety. Poszerz obszar, skróć tekst lub zastosuj osobno przetestowaną zasadę dzielenia, zamiast po cichu go przycinać.
Czy maxLines usuwa pozostałe słowa? Nie. Ta zasada odrzuca wynik, gdy wymaga on zbyt wielu wierszy, zgłasza wymaganą liczbę i pozostawia zero tspanów. Aplikacja musi obsłużyć ten stan.
Czy wyeksportowane wiersze będą wyglądać identycznie z inną czcionką? Jawnie zapisane słowa i pozycje pozostają, lecz plik nie osadza czcionki ani nie zamienia glifów na krzywe. Sprawdź dostępną czcionkę i wygląd liter w rzeczywistym środowisku docelowym.
Źródła
- Element tspan w MDN — pozycjonowane fragmenty tekstu i atrybuty przesunięć wierszy.
- Tekst SVG 2 w W3C — jawne pozycjonowanie i odrębny model automatycznego zawijania.
- MDN getComputedTextLength — mierzenie rzeczywistego tekstu SVG.
- MDN FontFaceSet.ready — oczekiwanie na wczytanie czcionek i układ.
- MDN textContent — wstawianie zwykłego tekstu zamiast znaczników.
- MDN XMLSerializer — zapis znaczników SVG z gotowym układem.
Dla własnej ilustracji rastrowej przygotuj edytowalny clipart, sprawdź kształty i zachowaj plik główny. Dodaj tekst oddzielnie, a przed przekazaniem zweryfikuj zaakceptowane wiersze i widoczny wygląd liter w zapisanym SVG.
Więcej z bloga

SVG.js: import grafiki, edycja ścieżki i eksport SVG
Importuj zaufane SVG do SVG.js, zmień kolor nazwanej ścieżki i porównaj eksport całego korzenia z eksportem dzieci. Zachowaj geometrię i sprawdź zapisane pliki.

Znaczniki SVG Leaflet: wyrównaj czubek przez iconAnchor
Pozycjonuj znaczniki SVG w Leaflet, mapując czubek na iconAnchor. Uwzględnij marginesy viewBox, dopasowanie proporcji i zmiany rozmiaru ikony.
