How do I calculate a String's width in Ruby?
Asked Answered
M

6

8

String.length will only tell me how many characters are in the String. (In fact, before Ruby 1.9, it will only tell me how many bytes, which is even less useful.)

I'd really like to be able to find out how many 'en' wide a String is. For example:

'foo'.width
# => 3

'moo'.width
# => 3.5          # m's, w's, etc. are wide

'foi'.width
# => 2.5          # i's, j's, etc. are narrow

'foo bar'.width
# => 6.25         # spaces are very narrow

Even better would be if I could get the first n en of a String:

'foo'[0, 2.en]
# => "fo"

'filial'[0, 3.en]
# => "fili"

'foo bar baz'[0, 4.5en]
# => "foo b"

And better still would be if I could strategize the whole thing. Some people think a space should be 0.25en, some think it should be 0.33, etc.

Mariahmariam answered 18/12, 2008 at 19:17 Comment(4)
This has to depend heavily on the font used to render the string, no?Pituitary
And you want that independantly of the font? I don't think it is possibleWeidar
I agree with the other commenters, unless you know the font this is impossible.Septum
Option 1 would be to assume that if you were using a fixed-width font then you'd use .legnth instead of .width. Option 2 would be to have a Font object. AFAIK, there's no Font class in Ruby.Mariahmariam
M
15

You should use the RMagick gem to render a "Draw" object using the font you want (you can load .ttf files and such)

The code would look something like this:

   the_text = "TheTextYouWantTheWidthOf"
   label = Draw.new
   label.font = "Vera" #you can also specify a file name... check the rmagick docs to be sure
   label.text_antialias(true)
   label.font_style=Magick::NormalStyle
   label.font_weight=Magick::BoldWeight
   label.gravity=Magick::CenterGravity
   label.text(0,0,the_text)
   metrics = label.get_type_metrics(the_text)
   width = metrics.width
   height = metrics.height

You can see it in action in my button maker here: http://risingcode.com/button/everybodywangchungtonite

Mchenry answered 19/12, 2008 at 12:11 Comment(2)
just be sure to set the font size with "label.pointsize = THE_SIZE", otherwise RMagick will default to a font size of 12.Rafael
the button maker link appears to be deadDisorient
A
7

Use the ttfunk gem to read the metrics from the font file. You can then get the width of a string of text in em. Here's my pull request to get this example added to the gem.

require 'rubygems'
require 'ttfunk'
require 'valuable'
# Everything you never wanted to know about glyphs:
# http://chanae.walon.org/pub/ttf/ttf_glyphs.htm

# this code is a substantial reworking of:
# https://github.com/prawnpdf/ttfunk/blob/master/examples/metrics.rb

class Font
  attr_reader :file

  def initialize(path_to_file)
    @file = TTFunk::File.open(path_to_file)
  end

  def width_of( string )
    string.split('').map{|char| character_width( char )}.inject{|sum, x| sum + x}
  end

  def character_width( character )
    width_in_units = ( horizontal_metrics.for( glyph_id( character )).advance_width )
    width_in_units.to_f / units_per_em
  end

  def units_per_em
    @u_per_em ||= file.header.units_per_em
  end

  def horizontal_metrics
    @hm = file.horizontal_metrics
  end

  def glyph_id(character)
    character_code = character.unpack("U*").first
    file.cmap.unicode.first[character_code]
  end
end

Here it is in action:

>> din = Font.new("#{File.dirname(__FILE__)}/../../fonts/DIN/DINPro-Light.ttf")
>> din.width_of("Hypertension")
=> 5.832
# which is correct! Hypertension in that font takes up about 5.832 em! It's over by maybe ... 0.015.
Amelina answered 25/2, 2015 at 17:24 Comment(0)
M
4

You could attempt to create a standarized "width proportion table" to calculate an aproximation, basically you need to store the width of each character and then traverse the string adding up the widths.

I found this table here:

Left, Width, Advance values for ArialBD16 'c' through 'm'
Letter  Left    Width   Advance
c        1       7       9
d        1       8       10
e        1       8       9
f        0       6       5
g        0       9       10
h        1       8       10
i        1       2       4
j       -1       4       4
k        1       8       9
l        1       2       4
m        1       12      14

If you want to get serious, I'd start by looking at webkit, gecko, and OO.org, but I guess the algorithms for kerning and size calculation are not trivial.

Mystical answered 18/12, 2008 at 22:47 Comment(0)
O
4

If you have ImageMagick installed you can access this information from the command line.

$ convert xc: -font ./.fonts/HelveticaRoundedLTStd-Bd.otf  -pointsize 24 -debug annotate -annotate 0 'MyTestString' null: 2>&1
2010-11-02T19:17:48+00:00 0:00.010 0.010u 6.6.5 Annotate convert[22496]: annotate.c/RenderFreetype/1155/Annotate
  Font ./.fonts/HelveticaRoundedLTStd-Bd.otf; font-encoding none; text-encoding none; pointsize 24
2010-11-02T19:17:48+00:00 0:00.010 0.010u 6.6.5 Annotate convert[22496]: annotate.c/GetTypeMetrics/736/Annotate
  Metrics: text: MyTestString; width: 157; height: 29; ascent: 18; descent: -7; max advance: 24; bounds: 0,-5  20,17; origin: 158,0; pixels per em: 24,24; underline position: -1.5625; underline thickness: 0.78125
2010-11-02T19:17:48+00:00 0:00.010 0.010u 6.6.5 Annotate convert[22496]: annotate.c/RenderFreetype/1155/Annotate
  Font ./.fonts/HelveticaRoundedLTStd-Bd.otf; font-encoding none; text-encoding none; pointsize 24

To do it from Ruby, use backticks:

result = `convert xc: -font #{path_to_font} -pointsize #{size} -debug annotate -annotate 0 '#{string}' null: 2>&1`
if result =~ /width: (\d+);/
  $1
end
Oink answered 2/11, 2010 at 19:27 Comment(1)
Since the rmagick gem is currently unmaintained, this is an attracitve option.Wavemeter
R
2

This is a good problem!

I'm trying to solve it using pango/cairo in ruby for SVG output. I am probably going to use pango to calculate the width and then use a simple svg element.

I use the following code:

require "cairo"
require "pango"

paper = Cairo::Paper::A4_LANDSCAPE
TEXT = "Don't you love me anymore?"
def pac(surface)
        cr = Cairo::Context.new(surface)
        cr.select_font_face("Calibri",
                              Cairo::FONT_SLANT_NORMAL,
                              Cairo::FONT_WEIGHT_NORMAL)
    cr.set_font_size(12)
    extents = cr.text_extents(TEXT)
    puts extents
end

Cairo::ImageSurface.new(*paper.size("pt")) do |surface|
  cr = pac(surface)
end
Rabblerousing answered 5/6, 2011 at 11:23 Comment(1)
what did you end up using for this? Trying to do something similar, but without RMagick (and lightweight).Commonable
S
0

Once I had to display a string array (containing the coming world days, current namedays, etc) in two lines, putting the linebreak after the appropriate string I had to determine the cumulative widths of the strings, printed in Arial. I opened my word editor, typed the alphabet, and I classified the characters into two classes, based on their width in the given font:

w="023456789AÁBCDEFGHJKLMNOÓÖŐPQRSTUÚÜŰWZYaábcdeghksoóöőpqwuúüűzymn".chars.yield_self{|z| z.zip(Array.new(z.size){1.5})}.to_h.merge("1rfiíjltIÍ ".chars.yield_self{|z| z.zip(Array.new(z.size){1})}.to_h)
w.default=1
nntd=["01-21:A vallások világnapja", "01-19:Kanut", "Kenéz", "Margaréta", "Márió", "Máriusz", "Megyer", "Sára", "Szultána", "Vázsony"]
nntd.sort_by!{|z| z.chars.map{|q| w[q]}.sum}.reverse

Then I was able to determine the position of the linebreak:

ind=nntd.collect.with_index.find_index{|z,i| nntd[0..i].join.chars.map{|q| w[q]}.sum >=nntd.join.chars.map{|q| w[q]}.sum/2}

t=[nntd[0..ind],nntd[ind+1..-1]].map{|z| z.join(",")}.join("\n")

After all I got a nice, balanced output, divided into two lines:

01-21:A vallások világnapja,01-19:Margaréta,Szultána Vázsony,Máriusz,Megyer,Kenéz,Kanut,Márió,Sára

enter image description here

This way I can check with an eyeblink the incoming world days, and current namedays.

Symphony answered 19/1, 2021 at 7:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.