[reportlab-users] Circular text (was: iCards...)

Dinu Gherman reportlab-users@reportlab.com
Wed, 29 Jan 2003 18:30:04 +0100

Content-Transfer-Encoding: 7bit
Content-Type: text/plain;

I wrote:

> I'm attaching an experimental function which does circular text,
> using Drawings, mostly as I like.

Ok, it now is a class CircularString, which I first wanted to sub-
class from shapes.String, but that won't work as you'd need to tell
the renderers how to handle it...

So I need to make it a subclass of shapes.UsewrNode, which seems to
be the canonical way of doing it, but I still don't like it that
much, because the base attributes come from a different class (i.e.
shapes.String) and you need to copy some methods you might need
from the Shape base class as I did with setProperties()...

Anyway, it kind of works, but might need some finetuning when using
text anchors like 'start' and 'end'... Also the angle parameter
should probably become mandatory, isn't it?

Please folks, test it! If it's useful let's check it into CVS!


Content-Disposition: attachment;
Content-Transfer-Encoding: 7bit
Content-Type: application/text;

#! /usr/bin/env python

import time
from math import pi, sqrt, sin, cos

from reportlab.lib.validators import isNumber, OneOf
from reportlab.lib.attrmap import AttrMap, AttrMapValue
from reportlab.lib import colors
from reportlab.graphics import shapes
from reportlab.graphics import renderPDF, renderPM
from reportlab.pdfbase.pdfmetrics import stringWidth

class CircularString(shapes.UserNode):
    "A circular string class."
    _attrMap = AttrMap(BASE=shapes.String,
        radius = AttrMapValue(isNumber),
        angle = AttrMapValue(isNumber),
        counterClockWise = AttrMapValue(OneOf(0, 1)),

    def __init__(self, x, y, radius, text, **kw):
        # set mandatory values
        self.x = x
        self.y = y
        self.text = text
        self.radius = radius

        # provide default values
        self.fillColor = colors.black
        self.fontName = 'Helvetica-Bold'
        self.fontSize = 12
        self.textAnchor = 'middle'
        self.angle = 0
        self.counterClockWise = 0

        # overwrite defaults, if needed

    def setProperties(self, props):
        ## self.verify()

    def provideNode(self):
        "Return a group with displaced strings for each letter." 
        angle = self.angle
        radius = self.radius
        ccw = self.counterClockWise
        fn, fs, = self.fontName, self.fontSize
        width = stringWidth(self.text, fn, fs)
        alpha = width / self.radius / pi*180

        g = shapes.Group()
        b = {0: -angle, 1: 180-angle}[ccw]

        # apply global textAnchor (might need some finetuning...)
        b = b + {'start':alpha/2, 'middle':0, 'end':-alpha/2}[self.textAnchor]

        for letter in self.text:
            # create a group containing one letter
            h = shapes.Group()
            ## h.add(shapes.Circle(0, 0, 3, fillColor=colors.red))
            s = shapes.String(0, 0, letter)
            s.textAnchor = 'middle' # really needs to be 'middle'!
            s.fillColor = self.fillColor
            s.fontName = fn
            s.fontSize = fs
            # translate it to new origin: x,y
            h.translate(self.x, self.y)

            # translate to circle border: dx,dy
            width = stringWidth(letter, fn, fs)
            beta = width / radius / pi*180
            phi = alpha/2-b-beta/2
            if ccw: 
                phi = 180 - phi
            dx = -sin(phi*pi/180)*radius
            dy = cos(phi*pi/180)*radius
            h.translate(dx, dy)        

            # rotate as needed
            if ccw:
                phi = phi - 180

            # increment used letter widths in degrees
            b = b + beta

        return g

def main() :
    col = colors.blue
    w, h = 200, 200
    x, y = 100, 100
    r = 50
    fn, fs = "Helvetica-Bold", 28
    d = shapes.Drawing(w, h)
    g = shapes.Group()

    # add a couple of helping widgets
    g.add(shapes.Rect(0, 0, w, h, fillColor=None))
    g.add(shapes.Circle(x, y, 3, fillColor=colors.black))
    g.add(shapes.Circle(x, y, r, fillColor=None))
    g.add(shapes.Circle(x, y, r+fs, fillColor=None))
    # add two circular texts
    # add two circular texts
    for text, angle, ccw in [("Python", 0, 0), ("p o w e r e d", 180, 1)]:
        cs = CircularString(x, y, r+ccw*fs/2, text)
        cs.angle = angle
        cs.textAnchor = 'middle'
        cs.fontName = fn
        cs.fontSize = fs
        cs.fillColor = col
        cs.counterClockWise = ccw


    # renderPM.drawToFile(d, "circularText.jpg", "JPG")
    renderPDF.drawToFile(d, "circularText.pdf")

if __name__ == "__main__" :

Content-Transfer-Encoding: 7bit
Content-Type: text/plain;

Dinu C. Gherman
"America is the only country that went from barbarism to decadence
without civilization in between." (Oscar Wilde)