Skip to content

Instantly share code, notes, and snippets.

@arthurk
Created March 21, 2010 16:15
Show Gist options
  • Save arthurk/339383 to your computer and use it in GitHub Desktop.
Save arthurk/339383 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python
# encoding: utf-8
"""
Text Labels as png with Pango/Cairo
"""
import os
import cairo
import pango
import pangocairo
class BaseImage(object):
def __init__(self, text, directory, filename=None, width=0, height=0,
font="Helvetica 20px", remove_width_margin=False):
self.text = text
self.font = font
# calculate width and height
if width == 0 or height == 0:
size = self._get_width_height()
if width == 0:
width = size[0]
if height == 0:
height = size[1]
# Cairo adds an unnecessary margin of 1px to the left and the right
# side of the image. To get rid of this margin, the width is reduced
# by 2px and the text is moved 1px to the left.
if remove_width_margin:
width -= 2
self.width = width
self.height = height
#self.box_width = self.box_width or 0
self.directory = directory
if not filename:
filename = self._get_output_filename()
self.filename = filename
self.filepath = os.path.join(directory, filename)
self.compressed_filepath = os.path.join(directory, 'c_' + filename)
def _get_output_filename(self, random=False):
"""
Returns a generated filename.
"""
if random:
import tempfile
fd, filename = tempfile.mkstemp('.png', '', self.directory)
filename = os.path.split(filename)[1]
else:
from hashlib import sha1
filename_args = ''.join([self.text, self.__class__.__name__])
filename = sha1(filename_args).hexdigest() + '.png'
return filename
def _get_width_height(self):
"""
Create a 0x0 surface and draw text on it, to measure how large the final
surface needs to be.
"""
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 0, 0)
context = cairo.Context(surface)
pc = pangocairo.CairoContext(context)
layout = pc.create_layout()
self.draw_text(surface, context, self.text, pc, layout, self.font)
#box_width=self.box_width)
return layout.get_pixel_size()
def draw_text(self, surface, context, text, pc=None, layout=None,
font="Helvetica 20px", position=None, color=None,
box_width=None, alignment=pango.ALIGN_LEFT,
line_spacing=None, letter_spacing=None, extra_kerning=None):
if color is None:
color = (0.0, 0.0, 0.0)
elif color.startswith('#'):
color = self._convert_color(color)
context.set_source_rgba(*color)
if not pc:
pc = pangocairo.CairoContext(context)
if not layout:
layout = pc.create_layout()
layout.set_text(text)
layout.set_font_description(pango.FontDescription(font))
if box_width: layout.set_width(box_width)
layout.set_alignment(alignment)
if line_spacing: layout.set_spacing(line_spacing)
alist = pango.AttrList()
if letter_spacing:
alist.insert(pango.AttrLetterSpacing(letter_spacing, 0, len(text)))
if extra_kerning:
for pos, kern in extra_kerning.iteritems():
alist.insert(pango.AttrLetterSpacing(kern, pos, pos+1))
layout.set_attributes(alist)
if position is None:
width, height = surface.get_width(), surface.get_height()
w, h = layout.get_pixel_size()
position = (width/2.0 - w/2.0, height/2.0 - h/2.0)
context.move_to(*position)
pc.show_layout(layout)
def _convert_color(self, s):
"""
Convert a HEX color to the Cairo compliant format.
"""
if not isinstance(s, basestring):
return s
if not s.startswith('#'):
s = _colors[s]
l = len(s)
if l in (4, 5):
c = [int(x*2, 16)/255.0 for x in s[1:]]
elif l in (7, 9):
c = [int(s[i:i+2], 16)/255.0 for i in range(1, l, 2)]
else:
raise ValueError('color %r has invalid length' % s)
if len(c) < 4:
c.append(1)
return tuple(c)
def pngcrush(self):
"""
Compress image with pngcrush.
"""
os.system('pngcrush %s %s > /dev/null 2>&1' % (
self.filepath,
self.compressed_filepath)
)
def draw(self):
pass
def save(self, force_overwrite=False):
"""
Saves the current image.
The 'force_overwrite' parameter can be used to insist
that an image must be overwritten.
"""
# only draw and save if the image file doesn't exist
if not os.path.exists(self.filepath) or force_overwrite:
self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.width, self.height)
self.context = cairo.Context(self.surface)
self.draw()
self.surface.write_to_png(self.filepath)
self.pngcrush()
class NavigationImage(BaseImage):
"""
A navigation image is a sprite which consists of an active and
an inactive version of the text. The height of a navigation image is
always 2*34px=68px (inactive+active).
"""
def __init__(self, *args, **kwargs):
super(NavigationImage, self).__init__(font="Helvetica 15px",
remove_width_margin=True,
height=68,
*args, **kwargs)
def draw(self):
"""
Draws the text to the navigation image.
"""
# inactive text
self.draw_text(self.surface, self.context, self.text, color='#004165',
font=self.font, position=(-1, (34-self.height)/2))
# active text
self.draw_text(self.surface, self.context, self.text, color='#7AB800',
font=self.font, position=(-1, ((34-self.height)/2)+34))
class ArticleImage(BaseImage):
"""
An article image is the title image of an article. The height is 25px.
"""
def __init__(self, *args, **kwargs):
self.color = self._convert_color('#7AB800')
self.font = "Helvetica 20px"
self.width, self.height = self._get_width_height()
# Cairo adds an unnecessary margin of 1px to the left and the right
# side of the image. To get rid of this margin, the width is reduced
# by 2px and the text is moved 1px to the left.
self.width -= 2
self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.width, 25)
self.context = cairo.Context(self.surface)
super(ArticleImage, self).__init__(*args, **kwargs)
def draw(self):
"""
Draw the article image.
"""
self.draw_text(self.surface, self.context, self.text, color=self.color,
font=self.font, position=(-1, (25-self.height)/2))
class PageImage(BaseImage):
"""
A page image is displayed at the top of each page. It consists of a
text and a subtext. The subtext is optional. The image has a fixed width
of 660x135px.
"""
def __init__(self, subtext='', *args, **kwargs):
self.subtext = subtext
super(PageImage, self).__init__(*args, **kwargs)
def save(self):
"""
Returns the filename of a generated page image.
"""
color_text = self._convert_color('#FFFFFF')
color_subtext = self._convert_color('#004165')
font = "Helvetica 24px"
box_width = 660*pango.SCALE
width, height = self._get_width_height(self.text, font, box_width)
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 660, 135)
context = cairo.Context(surface)
# text
self.draw_text(surface, context, self.text, color=color_text,
font=font, position=(-1, 0), box_width=box_width)
# subtext
self.draw_text(surface, context, self.subtext, color=color_subtext,
font=font, position=(-1, height), box_width=box_width)
surface.write_to_png(self.output_filename)
return self.pngcrush(self.output_filename)
img = NavigationImage('Foobar', os.getcwd())
img.save()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment