Created
March 21, 2010 16:15
-
-
Save arthurk/339383 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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