84

Box size known. Text string length unknown. Fit text to box without ruining its aspect ratio.

enter image description here

After an evening of googling and reading the SVG spec, I'm pretty sure this isn't possible without JavaScript. The closest I could get was using the textLength and lengthAdjust text attributes, but that stretches the text along one axis only.

<svg width="436" height="180"
    style="border:solid 6px"
    xmlns="http://www.w3.org/2000/svg">
    <text y="50%" textLength="436" lengthAdjust="spacingAndGlyphs">UGLY TEXT</text>
</svg>

enter image description here

I am aware of SVG Scaling Text to fit container and fitting text into the box

Yves M.
  • 29,855
  • 23
  • 108
  • 144
Bemmu
  • 17,849
  • 16
  • 76
  • 93
  • 2
    I ended up making a loop in javascript that increases the font size until getBBox shows it would no longer fit. Ugly, so still hoping there would be some other way. – Bemmu Mar 19 '13 at 03:03
  • I have also been trying to get this same functionality to work. The best method I have found was the same one you did. Loop through JS and change font until it fits. But even in fonts there is still some whitespace above and below so you can't seem to get it just right. – Chad Mar 06 '14 at 23:35
  • This sucks, it seems like such a basic thing, but the spec is clear that it only stretches it in one direction. After playing around, I was able to get it close by modifying only the Y scale using transform, aka: `transform="scale(0,5)"` - http://jsfiddle.net/G5L8W/ – streetlogics Mar 22 '14 at 00:07
  • I am trying to think if there was a way to calculate the amount of scale being applied to the text on the x axis and apply it to the y axis, but that would then also mean adding in checks to make sure you aren't over scaling the Y, and then you're back at using JS. I'll bet you could use transform/scale with JS better though. – streetlogics Mar 22 '14 at 00:08
  • built-in support for this in SVG (or any text elements in a webpage) would be fantastic – Andy Jun 12 '15 at 18:13
  • 1
    http://tavmjong.free.fr/SVG/TEXT_IN_A_BOX/index.html – Mohsen Feb 12 '21 at 09:24

5 Answers5

53

I didn't find a way to do it directly without Javascript, but I found a JS quite easy solution, without for loops and without modify the font-size and fits well in all dimensions, that is, the text grows until the limit of the shortest side.

Basically, I use the transform property, calculating the right proportion between the desired size and the current one.

This is the code:

<?xml version="1.0" encoding="UTF-8" ?>
<svg version="1.2" viewBox="0 0 1000 1000" width="1000" height="1000" xmlns="http://www.w3.org/2000/svg" >
 <text id="t1" y="50" >MY UGLY TEXT</text>
 <script type="application/ecmascript"> 

    var width=500, height=500;

    var textNode = document.getElementById("t1");
    var bb = textNode.getBBox();
    var widthTransform = width / bb.width;
    var heightTransform = height / bb.height;
    var value = widthTransform < heightTransform ? widthTransform : heightTransform;
    textNode.setAttribute("transform", "matrix("+value+", 0, 0, "+value+", 0,0)");

 </script>
</svg>

In the previous example the text grows until the width == 500, but if I use a box size of width = 500 and height = 30, then the text grows until height == 30.

JoKalliauer
  • 1,648
  • 1
  • 13
  • 17
Roberto
  • 8,586
  • 3
  • 42
  • 53
  • 10
    Or simply... textNode.setAttribute("transform", "scale("+value+")"); – Charlie Martin Feb 27 '15 at 19:55
  • 4
    Quick note- the scale attribute also affects the coordinate system of the current item, so if you want the element to be in the same position, will need to divide both x and y positions by the scalar to get the same relative position – DragonJawad Jun 13 '16 at 21:09
  • 2
    Or even more simply... set the text `font-size` to 1 (`1em` in our case); then once you have the scale factor (`value` in this answer) just set the `font-size` to it. See example below. – Izhaki Jun 04 '18 at 21:42
  • I also wanted the text to be verticaly-centered, so I changed the end (after `var value = ...`) to `textNode.style.transform = "translateY(50%) scale(" + value + ")"; textNode.style.transformOrigin = '0px 0px'; textNode.style.dominantBaseline = 'central';` – jave.web Oct 03 '19 at 18:12
  • I am converting an svg to png using imagemagick and as I thought, this js is not working there. Is there any recommendation to get it work when I am converting SVG to PNG? – Krunal Dec 12 '19 at 11:40
  • 1
    @Krunal, I'd try using Inkscape CLI: https://stackoverflow.com/a/34725966/661140 – Roberto Dec 12 '19 at 17:18
  • For vertical and horizontal centering the parameters should be `matrix(value, 0, 0, value, width/2, height/2)` and with `x="0" y="0"` – user3187724 Mar 04 '21 at 16:36
  • when I put this script into my svg Chrome things that less than operator at "widthTransform < heightTransform " is the opening of a new tag and gives "StartTag: invalid element name" error. Is there a way we should be escaping it or something? – Josh P Jun 13 '21 at 21:07
  • I figured out my above problem by wrapping the contents of the – Josh P Jun 13 '21 at 21:19
19

first of all: just saw that the answer doesn't precisely address your need - it might still be an option, so here we go:

you are rightly observing that svg doesn't support word-wrapping directly. however, you might benefit from foreignObject elements serving as a wrapper for xhtml fragments where word-wrapping is available.

have a look at this self-contained demo (available online):

<?xml version="1.0" encoding="utf-8"?>
<!-- SO: http://stackoverflow.com/questions/15430189/pure-svg-way-to-fit-text-to-a-box  -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
   xmlns:xhtml="http://www.w3.org/1999/xhtml"
   xmlns:xlink="http://www.w3.org/1999/xlink"
   version="1.1"
   width="20cm" height="20cm"
   viewBox="0 0 500 500"
   preserveAspectRatio="xMinYMin"
   style="background-color:white; border: solid 1px black;"
>
  <title>simulated wrapping in svg</title>
  <desc>A foreignObject container</desc>

   <!-- Text-Elemente -->
   <foreignObject
      x="100" y="100" width="200" height="150"
      transform="translate(0,0)"
   >
      <xhtml:div style="display: table; height: 150px; overflow: hidden;">
         <xhtml:div style="display: table-cell; vertical-align: middle;">
            <xhtml:div style="color:black; text-align:center;">Demo test that is supposed to be word-wrapped somewhere along the line to show that it is indeed possible to simulate ordinary text containers in svg.</xhtml:div>
         </xhtml:div>
      </xhtml:div>
   </foreignObject>

  <rect x="100" y="100" width="200" height="150" fill="transparent" stroke="red" stroke-width="3"/>
</svg>
1j01
  • 3,714
  • 2
  • 29
  • 30
collapsar
  • 17,010
  • 4
  • 35
  • 61
6

I've developed @Roberto answer, but instead of transforming (scaling) the textNode, we simply:

  • give it font-size of 1em to begin with
  • calculate the scale based on getBBox
  • set the font-size to that scale

(You can also use 1px etc.)

Here's the React HOC that does this:

import React from 'react';
import TextBox from './TextBox';

const AutoFitTextBox = TextBoxComponent =>
  class extends React.Component {
    constructor(props) {
      super(props);
      this.svgTextNode = React.createRef();
      this.state = { scale: 1 };
    }

    componentDidMount() {
      const { width, height } = this.props;
      const textBBox = this.getTextBBox();
      const widthScale = width / textBBox.width;
      const heightScale = height / textBBox.height;
      const scale = Math.min(widthScale, heightScale);

      this.setState({ scale });
    }

    getTextBBox() {
      const svgTextNode = this.svgTextNode.current;
      return svgTextNode.getBBox();
    }

    render() {
      const { scale } = this.state;
      return (
        <TextBoxComponent
          forwardRef={this.svgTextNode}
          fontSize={`${scale}em`}
          {...this.props}
        />
      );
    }
  };

export default AutoFitTextBox(TextBox);
Izhaki
  • 23,372
  • 9
  • 69
  • 107
3

I don't think its the solution for what you want to do but you can use textLength with percentage ="100%" for full width.

<svg width="436" height="180"
    style="border:solid 6px"
    xmlns="http://www.w3.org/2000/svg">
    <text x="0%" y="50%" textLength="100%">blabla</text>
</svg>

you can also add text-anchor="middle" and change the x position to center perfectly your text

this will not change the fontsize and you will have weird space letterspacing...

JSFIDDLE DEMO

Zach Saucier
  • 24,871
  • 12
  • 85
  • 147
Kuro Kumo
  • 69
  • 1
2

This is still an issue in 2022. There is no way to define bounds and get text to scale in a pure scalable vector graphic. Adjusting the font size manually is still the only solution it seems, and the examples given are quite buggy. Has anybody figured out a clean solution that works? Judging by the svg spec it looks like a pure solution doesn't exist.

And to provide some sort of answer myself, this resource is the best I've found, is hacky, but works much more robustly: fitrsvgtext - storybook | fitrsvgtext - GitHub

Eben
  • 41
  • 1
  • 5