Retour à la ligne SVG : créer des lignes mesurées avec tspan
Répartis les libellés SVG en mesurant les mots avec la police réelle, puis crée des tspans. Teste les libellés étroits, rejette les débordements et enregistre le résultat.
Sur cette page
- Choisis une règle de retour à la ligne avant de placer les lignes
- Mesure avec la police qui dessinera le libellé
- Exécute la base complète sans dépendance
- Ce qu’ont produit les six cas originaux
- Enregistre le SVG mis en page, puis teste la livraison
- Garde les limites du code explicites
- Questions fréquentes
- Sources
Pour répartir un libellé SVG changeant en lignes explicites, mesure les groupes de mots candidats avec getComputedTextLength(), puis écris un <tspan> par ligne acceptée. Attends d’abord les polices du document et choisis ce qui se passe lorsqu’un mot est plus large que le libellé ou que le résultat dépasse ta limite de lignes.
La base ci-dessous utilise des mots séparés par des espaces et rejette les deux débordements sans produire de libellé partiel. Elle garde une illustration originale de losange creux séparée du texte : changer la largeur du libellé ne redessine donc pas l’illustration.
Choisis une règle de retour à la ligne avant de placer les lignes
L’élément tspan permet de positionner des portions de texte SVG. Notre routine définit le même x sur chaque ligne et un décalage dy sur les suivantes. La routine décide du retour à la ligne ; l’élément contient le résultat.
C’est une méthode de mise en page explicite. La spécification du texte SVG 2 définit aussi le retour automatique via une zone de contenu et la mise en page de texte CSS. Cet article ne teste pas ces fonctions et n’affirme pas qu’elles sont absentes de tous les navigateurs.
Pour un paragraphe HTML hors de l’illustration, fais couler le texte autour d’une silhouette SVG flottante avec CSS. Cette mise en page suit le contour d’une image tandis que le texte reste du HTML ordinaire, au lieu de devenir des lignes de libellé SVG positionnées.
Pour ce petit exemple de libellé en anglais, la règle est la suivante :
- Réduire les espaces blancs et essayer les mots dans leur ordre original, séparés par des espaces simples.
- Garder une ligne candidate uniquement tant que sa longueur mesurée tient dans la largeur disponible.
- Rejeter un mot qui ne tient pas seul ; ne pas inventer un point de césure.
- Rejeter le résultat entier s’il nécessite plus de
maxLines; ne pas supprimer silencieusement les derniers mots.
Une limite de lignes est une décision de mise en page. Choisis-la avec la ligne de base initiale, l’interligne et la hauteur disponible. Le code de retour à la ligne ne calcule pas une boîte de contenu verticale pour toi.

Mesure avec la police qui dessinera le libellé
getComputedTextLength() mesure la longueur calculée du texte SVG. Cette base clone l’élément texte du libellé comme sonde cachée dans le même SVG attaché au document, puis retire la sonde après la planification des lignes. Les deux utilisent explicitement Arial, sans-serif à 22 unités.
Le code attend document.fonts.ready avant de mesurer. Cela attend le chargement des polices du document et la mise en page associée ; cela n’intègre pas de police dans le SVG exporté. Une autre police disponible peut donner des longueurs de ligne différentes. Relance la mise en page lorsque le texte, les réglages de police ou la largeur disponible changent.
Exécute la base complète sans dépendance
Enregistre ceci sous starter.html et ouvre-le dans un navigateur. Il crée six cas, affiche leurs mesures et sérialise chaque SVG obtenu. Les deux premiers cas ne changent que la largeur du libellé ; le troisième change le texte. Les autres testent les rejets et le texte littéral.
<!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>La routine crée chaque ligne avec textContent, au lieu d’insérer le texte comme balisage. Dans le cas de texte littéral, Cove <g> & field reste du texte ordinaire : le SVG sérialisé contient des chevrons et une esperluette échappés, pas un élément groupe supplémentaire.
Le clone de mesure est attaché mais caché, avec les mêmes attributs de police que le libellé visible. Il est retiré dans finally, y compris en cas de rejet. Les lignes ne sont ajoutées qu’après validation de la largeur des mots et du nombre maximal de lignes. La routine efface d’abord tout libellé précédent : une mise à jour rejetée ne laisse donc aucun texte ancien ou partiel ; gère le statut renvoyé dans l’interface de ton application.
Ce qu’ont produit les six cas originaux
Nous avons exécuté exactement cette base dans Chrome avec les polices du document au statut loaded. Les longueurs mesurées ci-dessous sont des longueurs de texte SVG pour cet environnement, pas les limites des glyphes peints ni une garantie de portabilité des polices.
| Cas | Largeur / lignes maximales | Résultat | Plus longue ligne observée |
|---|---|---|---|
| Phrase originale | 200 / 4 | Deux lignes : Keep artwork and; labels separate | 172.44 |
| Même phrase, boîte étroite | 110 / 5 | Quatre lignes : Keep; artwork; and labels; separate | 100.30 |
| Texte plus long | 200 / 4 | Trois lignes | 198.13 |
| Mot trop long | 110 / 5 | overlong-word ; aucun tspan | Largeur du mot mesurée à 311.82 |
| Phrase étroite, limite de deux lignes | 110 / 2 | too-many-lines ; quatre nécessaires ; aucun tspan | Aucun libellé produit |
| Chevrons et esperluette littéraux | 200 / 4 | Une ligne littérale | 162.66 |
La phrase originale est Keep artwork and labels separate. La version plus longue est Keep the original artwork beside every exported label. Chaque ligne acceptée était mesurée dans la largeur choisie. Les cas de rejet laissaient l’illustration visible tout en omettant le libellé, ce qui rendait l’état d’échec facile à examiner.
Les six fichiers sérialisés conservaient le même chemin original de losange, un élément texte natif et aucune image ni foreignObject. Les cas acceptés contenaient respectivement deux, quatre, trois et un tspans ; les deux cas rejetés n’en contenaient aucun. Nous avons aussi rouvert les six SVG sérialisés comme images dans le même navigateur, où tous se chargeaient à 360×240. Cela vérifie ces fichiers et ce navigateur, pas tous les éditeurs ou moteurs de rendu.
Enregistre le SVG mis en page, puis teste la livraison
XMLSerializer.serializeToString() crée le balisage sérialisé du rapport. Enregistre la chaîne serialized d’un cas réussi dans un fichier .svg. Ce fichier contient déjà des lignes explicites ; il n’a pas besoin de cette routine JavaScript pour les recréer.
Ses mots restent du texte actif. Le fichier enregistré garde la demande de famille de polices, pas un fichier de police ni des glyphes convertis en chemins. Une destination avec une autre police peut changer le lettrage même si les chaînes de lignes et les positions restent. Garde le texte original et les paramètres de mise en page à côté de la copie exportée, puis examine la destination réelle avant livraison. Pour fixer la forme des lettres, crée une copie de livraison vectorisée dans un éditeur source adapté, tout en préservant le texte actif dans le maître.
Le guide du texte foreignObject traite des libellés HTML qui disparaissent dans un autre moteur de rendu. Cette base construit plutôt du texte natif dès le départ. Le guide de textLength couvre l’ajustement d’une seule ligne en modifiant sa longueur ; c’est une décision différente de l’ajout de lignes mesurées. Utilise le guide d’alignement vertical pour positionner le libellé entier par rapport au dessin environnant.
Garde les limites du code explicites
La séparation par espaces blancs est volontairement une règle limitée. Elle réduit les espaces répétés, les tabulations et les retours explicites à la ligne, et ne gère pas les règles de coupure propres aux langues, la césure, les paragraphes bidirectionnels ni les portions enrichies mélangeant plusieurs styles. Choisis un autre système de mise en page ou étends et teste cette règle lorsque ton texte nécessite ces fonctions.
Le contrôle de largeur ne garantit pas non plus que les débords décoratifs des glyphes restent dans un cadre dessiné, ou que cinq lignes de base tiennent dans la hauteur choisie. Examine le lettrage visible à la taille de livraison. Notre test utilise des libellés horizontaux, des attributs de police fixes et un pas de ligne de 30 unités ; ne transfère pas ses mesures à une autre police sans mesurer à nouveau.
Si ton illustration n’existe qu’en PNG ou JPG et que tu as besoin d’un dessin modifiable, prépare un SVG de clipart avec PerfectVector, examine ses bords et ses ouvertures internes, puis garde ce dessin séparé du libellé. Retape les mots comme texte natif, applique la règle de retour à la ligne et examine l’export assemblé. La vectorisation prépare les chemins de l’illustration ; elle ne choisit pas les retours à la ligne et ne récupère pas de mots modifiables depuis du lettrage vectorisé. De bons chemins existants peuvent rester tels quels. Le guide de vectorisation explique cette récupération de source.
Questions fréquentes
tspan décide-t-il automatiquement où couper chaque ligne ? Cette base prend cette décision en mesurant des groupes de mots, puis crée un tspan positionné pour chaque ligne acceptée. Elle ne teste pas le retour automatique de SVG 2 fondé sur CSS.
Que devient un mot plus large que le libellé ? La routine renvoie overlong-word et ne produit aucun libellé. Élargis la zone, raccourcis le texte ou fournis une règle de coupure testée séparément au lieu de le rogner silencieusement.
maxLines supprime-t-il les mots restants ? Non. Cette règle rejette le résultat s’il nécessite trop de lignes, indique le nombre requis et ne laisse aucun tspan. L’application doit gérer cet état.
Les lignes exportées seront-elles identiques avec une autre police ? Les mots et positions explicites restent, mais le fichier n’intègre pas de police et ne convertit pas les glyphes en chemins. Examine la police disponible et le lettrage dans la destination réelle.
Sources
- MDN, élément tspan — Portions de texte positionnées et attributs de décalage de ligne.
- W3C, texte SVG 2 — Positionnement explicite et modèle distinct de retour automatique.
- MDN getComputedTextLength — Mesure du texte SVG réel.
- MDN FontFaceSet.ready — Attente du chargement des polices et de la mise en page.
- MDN textContent — Insertion de texte brut plutôt que de balisage.
- MDN XMLSerializer — Enregistrement du balisage SVG mis en page.
Pour ton illustration matricielle, prépare un clipart modifiable, examine ses formes et garde le maître. Ajoute le texte séparément, puis vérifie les lignes acceptées et le lettrage visible dans le SVG enregistré avant livraison.
Plus depuis le blog

SVG.js : importer un dessin, modifier un chemin, exporter
Importe un balisage SVG fiable dans SVG.js, recolore un chemin nommé et compare les exports de racine complète ou d’enfants seuls. Garde la géométrie et examine les fichiers enregistrés.

Marqueurs SVG Leaflet : aligne la pointe avec iconAnchor
Positionne des marqueurs SVG dans Leaflet en reliant la pointe visible à iconAnchor. Tiens compte des marges du viewBox, de l’ajustement des proportions et de la taille de l’icône.
