udavdasha
udavdasha

Reputation: 89

How to determine text width and height when using svgwrite for python?

I want to extract pixel size of the text object (svgwrite.Drawing.Text) as it would appear in file after formatting with given style. My font is fixed-width (Courier New).

The reason why I need it is that I want to print to SVG file a string and then map on the resulting text some information: e.g. I have a string "ABCDEF" and external wise man told me that "BCD" portion should be marked. Then I need to know how many pixels (units?) are covered by symbol "A" and symbols "BCD" in both dimensions, and then draw a colored rectangle, or transparent frame, or whatever strictly under the "BCD" portion.

So I have the following code and I would expect to use something like "w = text1.width" to extract width, but it doesn't work this way. Thank you in advance for trying to answer my question.

import svgwrite
my_svg = svgwrite.Drawing(filename = "dasha.svg", size = ("800px", "600px"))
text_style = "font-size:%ipx; font-family:%s" % (12, "Courier New") 
text1 = my_svg.text("HELLO WORLD", insert=(0, 0), fill="black", style=text_style)
my_svg.add(text1)
my_svg.save()

UPD1 [22.06.2014]: Intermediate solution which I use at the moment is to measure height and width of the letter with particular font and size manually in Inkscape. I tried my best, but I am not sure such values are perfect, and now I can't change font size in the program.

Upvotes: 8

Views: 7095

Answers (4)

dlazesz
dlazesz

Reputation: 298

I wanted to determine the width of the text in svgwrite in a specific font. I ended up using the following solution from http://blog.mathieu-leplatre.info/text-extents-with-python-cairo.html:

def textwidth(text, fontsize=14):
    try:
        import cairo
    except:
        return len(text) * fontsize
    surface = cairo.SVGSurface('undefined.svg', 1280, 200)
    cr = cairo.Context(surface)
    cr.select_font_face('Arial', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
    cr.set_font_size(fontsize)
    xbearing, ybearing, width, height, xadvance, yadvance = cr.text_extents(text)
    return width

It brings in cairo as dependency but it is the most clean an platform-independent solution I've found so far.

Upvotes: 4

Fabian
Fabian

Reputation: 626

I ended up using Tkinter:

try:
    # for Python2
    import Tkinter
except ImportError:
    # for Python3
    import tkinter as Tkinter

try:
    import tkFont
except ImportError:
    import tkinter.font as tkFont

def get_text_metrics(family, size, text):
    # initialize Tk so that font metrics will work
    tk_root = Tkinter.Tk()
    font = None
    font = tkFont.Font(family=family, size=size)
    assert font is not None
    (w, h) = (font.measure(text), font.metrics('linespace'))
    return (w, h)

Upvotes: 0

Douglas Kastle
Douglas Kastle

Reputation: 739

I had this exact same problem, but I had a variable width font. I solved it by taking the text element (correct font and content) I wanted, wrote it to a svg file, and I used Inkscape installed on my PC to render the drawing to a temporary png file. I then read back the dimensions of the png file (extracted from the header), removed the temp svg and png files and used the result to place the text where I wanted and elements around it.

I found that rendering to a drawing, using a DPI of 90 seemed to give me the exact numbers I needed, or the native numbers used in svgwrite as a whole. -D is the flag to use so that only the drawable element, i.e. the text, is rendered.

os.cmd(/cygdrive/c/Program\ Files\ \(x86\)/Inkscape/inkscape.exe -f work_temp.svg -e work_temp.png -d 90 -D)

I used these functions to extract the png numbers, found at this link, note mine is corrected slightly for python3 (still working in python2)

def is_png(data):
    return (data[:8] == b'\x89PNG\r\n\x1a\n'and (data[12:16] == b'IHDR'))

def get_image_info(data):
    if is_png(data):
        w, h = struct.unpack('>LL', data[16:24])
        width = int(w)
        height = int(h)
    else:
        raise Exception('not a png image')
    return width, height

if __name__ == '__main__':
    with open('foo.png', 'rb') as f:
        data = f.read()

    print is_png(data)
    print get_image_info(data)

It's clunky, but it worked

Upvotes: 1

punchcard
punchcard

Reputation: 429

I am not an expert in svg but it is my understanding that this is difficult to determine character and text width in svg. That means svgwrite cannot do it either. What I have done is picked a fixed width for the characters and create each character separately a fixed width from the start of the previous character. You may also consider centering string, that is each character, by using text_anchor='middle'. With a character centered on point a box could be created to be centered on that point also.

see also http://www.w3.org/TR/SVG/text.html which says "In many situations, the algorithms for mapping from characters to glyphs are system-dependent, resulting in the possibility that the rendering of text might be (usually slightly) different when viewed in different user environments. If the author of SVG content requires precise selection of fonts and glyphs, then the recommendation is that the necessary fonts (potentially subsetted to include only the glyphs needed for the given document) be available either as SVG fonts embedded within the SVG content or as WebFonts ([CSS2], section 15.1) posted at the same Web location as the SVG content."

Upvotes: -1

Related Questions