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

# Перенос текста SVG: создайте измеренные строки с tspan

> Переносите подписи SVG, измеряя слова настоящим шрифтом, затем создавайте tspan. Проверьте узкие подписи, отклоняйте слишком широкие слова и лишние строки, сохраняйте результат.

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

Language: ru
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-абзаца вне иллюстрации используйте [обтекание плавающего силуэта SVG текстом через CSS](https://perfectvector.com/ru/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 содержит экранированные угловые скобки и амперсанд, а не дополнительный элемент группы.

Измерительный клон присоединён, но скрыт и имеет те же шрифтовые атрибуты, что видимая подпись. Он удаляется в `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`. Каждая принятая строка по измерению помещалась в выбранную ширину. Случаи отклонения оставляли иллюстрацию видимой без подписи, что упрощало проверку состояния ошибки.

Все шесть сериализованных файлов сохранили одинаковый оригинальный контур ромба, один собственный текстовый элемент и отсутствие image или `foreignObject`. Принятые случаи содержали соответственно два, четыре, три и один tspan; оба отклонённых — ноль. Мы также снова открыли шесть сериализованных SVG как изображения в том же браузере: все загрузились в 360×240. Это проверка данных файлов и браузера, а не каждого редактора или средства отрисовки.

## Сохраните скомпонованный SVG, затем проверьте передачу

[`XMLSerializer.serializeToString()`](https://developer.mozilla.org/en-US/docs/Web/API/XMLSerializer/serializeToString) создаёт сериализованную разметку в отчёте. Сохраните строку `serialized` успешного случая как файл `.svg`. В файле уже есть явные строки; эта процедура JavaScript для их воссоздания не нужна.

Слова всё ещё остаются живым текстом. Сохранённый файл хранит запрос font-family, а не файл шрифта или контуры глифов. Целевая среда с другим шрифтом может изменить надпись, хотя строки и позиции сохраняются. Держите исходную формулировку и входы компоновки рядом с экспортированной копией и проверяйте реальную среду перед передачей. Для фиксированной формы букв создайте передаваемую копию с контурами в подходящем редакторе, сохранив живой текст в мастере.

[Руководство по тексту foreignObject](https://perfectvector.com/ru/blog/svg-foreignobject-text) описывает HTML-подписи, исчезающие в другом средстве отрисовки. Этот пример сразу создаёт собственный текст SVG. [Руководство по textLength](https://perfectvector.com/ru/blog/svg-textlength-fit) описывает вписывание одной строки изменением длины — другое решение, чем добавление измеренных строк. Используйте [руководство по вертикальному выравниванию](https://perfectvector.com/ru/blog/svg-text-vertical-alignment), если нужно расположить всю подпись относительно окружающей графики.

## Явно обозначайте ограничения обёртки

Разделение по пробельным символам — намеренно небольшой набор правил. Он сворачивает повторяющиеся пробелы, табуляцию и явные переводы строк и не обрабатывает языковые правила разрыва, переносы с дефисами, двунаправленные абзацы или форматированные фрагменты со смешанными стилями. Выбирайте другую систему компоновки либо расширяйте и проверяйте правила, если тексту нужны эти функции.

Проверка ширины также не гарантирует, что выступы декоративных глифов останутся внутри нарисованной рамки или что пять базовых линий поместятся в выбранную высоту. Проверяйте видимые буквы в передаваемом размере. Наш пример использует горизонтальные подписи, фиксированные атрибуты шрифта и шаг строки в 30 единиц; не переносите его числа на другую гарнитуру без повторного измерения.

Если иллюстрация существует только в PNG или JPG, а нужна редактируемая графика, [подготовьте SVG-клипарт с PerfectVector](https://perfectvector.com/ru/clipart-to-svg), проверьте края и внутренние отверстия и храните графику отдельно от подписи. Наберите формулировку заново как собственный текст, выполните правила переноса и проверьте собранный экспорт. Векторизация готовит контуры иллюстрации; она не выбирает разрывы строк и не восстанавливает редактируемые слова из трассированных букв. Уже хорошие контуры можно оставить как есть. [Руководство по векторизации](https://perfectvector.com/ru/blog/what-is-image-vectorization) объясняет этот этап восстановления исходника.

## Частые вопросы

**Решает ли tspan автоматически, где переносить каждую строку?**
Этот пример решает это измерением групп слов, затем создаёт один позиционированный tspan для каждой принятой строки. Он не проверяет автоматический перенос SVG 2 на основе CSS.

**Что происходит со словом шире подписи?**
Процедура возвращает overlong-word и не создаёт подпись. Расширьте область, сократите формулировку или предоставьте отдельно проверенные правила разрыва вместо незаметной обрезки.

**Удаляет ли maxLines оставшиеся слова?**
Нет. Правила отклоняют результат, если нужно слишком много строк, сообщают требуемое число и оставляют ноль tspan. Приложение должно обработать этот статус.

**Будут ли экспортированные строки выглядеть одинаково с другим шрифтом?**
Явные слова и позиции сохраняются, но файл не встраивает шрифт и не создаёт контуры глифов. Проверяйте доступный шрифт и надпись в настоящей целевой среде.

## Источники

1. [MDN: Элемент tspan](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/tspan) — позиционированные части текста и атрибуты смещения строк.
2. [W3C: Текст SVG 2](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/ru/clipart-to-svg), проверьте фигуры и сохраните мастер. Добавляйте формулировку отдельно, затем перед передачей проверьте принятые строки и видимые буквы в сохранённом SVG.

## Explore

- [Read the HTML article](https://perfectvector.com/ru/blog/svg-text-wrap-tspan)
- [SVG.js: импорт графики, правка контура и экспорт SVG](https://perfectvector.com/ru/blog/svgjs-import-export-artwork.md)
- [Маркеры SVG в Leaflet: совместите кончик с iconAnchor](https://perfectvector.com/ru/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)
