title: Multicolored text with PIL
body: <p>I'm creating a web-app that serves a dynamic image, with text.
  Each string drawn may be in multiple colors. </p>
  
  <p>So far I've created a parse method, and a render method. The parse method just takes the string, and parses colors from it, they are in format like this: "§aThis is green§rthis is white" (Yeah, it is Minecraft).
  So this is how my font module looks like:</p>
  
  <pre><code># Imports from pillow
  from PIL import Image, ImageDraw, ImageFont
  
  # Load the fonts
  font_regular = ImageFont.truetype("static/font/regular.ttf", 24)
  font_bold = ImageFont.truetype("static/font/bold.ttf", 24)
  font_italics = ImageFont.truetype("static/font/italics.ttf", 24)
  font_bold_italics = ImageFont.truetype("static/font/bold-italics.ttf", 24)
  
  max_height = 21 # 9, from FONT_HEIGHT in FontRederer in MC source, multiplied by
                  # 3, because each virtual pixel in the font is 3 real pixels
                  # This number is also returned by:
                  # font_regular.getsize("ABCDEFGHIJKLMNOPQRSTUVWXYZ")[1] 
  
  # Create the color codes
  colorCodes = [0] * 32 # Empty array, 32 slots
  # This is ported from the original MC java source:
  for i in range(0, 32):
      j = int((i &gt;&gt; 3 &amp; 1) * 85)
      k = int((i &gt;&gt; 2 &amp; 1) * 170 + j)
      l = int((i &gt;&gt; 1 &amp; 1) * 170 + j)
      i1 = int((i &gt;&gt; 0 &amp; 1) * 170 + j)
      if i == 6:
          k += 85
      if i &gt;= 16:
          k = int(k/4)
          l = int(l/4)
          i1 = int(i1/4)
      colorCodes[i] = (k &amp; 255) &lt;&lt; 16 | (l &amp; 255) &lt;&lt; 8 | i1 &amp; 255
  
  def _get_colour(c):
      ''' Get the RGB-tuple for the color
      Color can be a string, one of the chars in: 0123456789abcdef
      or an int in range 0 to 15, including 15
      '''
      if type(c) == str:
          if c == 'r':
              c = int('f', 16)
          else:
              c = int(c, 16)
      c = colorCodes[c]
      return ( c &gt;&gt; 16 , c &gt;&gt; 8 &amp; 255 , c &amp; 255 )
  
  def _get_shadow(c):
      ''' Get the shadow RGB-tuple for the color
      Color can be a string, one of the chars in: 0123456789abcdefr
      or an int in range 0 to 15, including 15
      '''
      if type(c) == str:
          if c == 'r':
              c = int('f', 16)
          else:
              c = int(c, 16)
      return _get_colour(c+16)
  
  def _get_font(bold, italics):
      font = font_regular
      if bold and italics:
          font = font_bold_italics
      elif bold:
          font = font_bold
      elif italics:
          font = font_italics
      return font
  
  def parse(message):
      ''' Parse the message in a format readable by render
      this will return a touple like this:
      [((int,int),str,str)]
      so if you where to send it directly to the rederer you have to do this:
      render(pos, parse(message), drawer)
      '''
      result = []
      lastColour = 'r'
      total_width = 0
      bold = False
      italics = False
      for i in range(0,len(message)):
          if message[i] == '§':
              continue
          elif message[i-1] == '§':
              if message[i] in "01234567890abcdef":
                  lastColour = message[i]
              if message[i] == 'l':
                  bold = True
              if message[i] == 'o':
                  italics = True
              if message[i] == 'r':
                  bold = False
                  italics = False
                  lastColour = message[i]  
              continue
          width, height = _get_font(bold, italics).getsize(message[i])
          total_width += width
          result.append(((width, height), lastColour, bold, italics, message[i]))
      return result
  
  def get_width(message):
      ''' Calculate the width of the message
      The message has to be in the format returned by the parse function
      '''
      return sum([i[0][0] for i in message])
  
  
  def render(pos, message, drawer):
      ''' Render the message to the drawer
      The message has to be in the format returned by the parse function
      '''
      x = pos[0]
      y = pos[1]
      for i in message:
          (width, height), colour, bold, italics, char = i
          font = _get_font(bold, italics)
          drawer.text((x+3, y+3+(max_height-height)), char, fill=_get_shadow(colour), font=font)
          drawer.text((x, y+(max_height-height)), char, fill=_get_colour(colour), font=font)
          x += width
  </code></pre>
  
  <p>And it does work, but characters who are supposed to go below the ground line of the font, like g, y and q, are rendered on the ground line, so it looks strange, Here's an example:
  <img src="https://bildr.no/image/NXFlay9G.jpeg" alt=""></p>
  
  <p>Any ideas on how I can make them display corectly? Or do I have to make my own offset table, where I manually put them?</p>