iouvxz
iouvxz

Reputation: 163

How to render font exactly like what it appears?

I found a script in a game that's definitely using Microsoft YaHei font (when I replace the font file I found in the game folder with my own font file, the script font changes as well):

enter image description here

But even after changing the size and position parameter, the render result is always a bit different.

from PIL import Image, ImageDraw, ImageFont
import numpy as np

i = 12
text = '我是王中王'

font = ImageFont.truetype('mysh.ttf', i)
PIL_image = Image.new('RGB', (100, 100), color=0xffffff)
draw = ImageDraw.Draw(PIL_image)
draw.text((31, 11), text, font=font, fill=10, anchor='mm')
Image.fromarray(np.array(PIL_image)).save('out.png')

This is the piece of code I'm using, by changing the font_size argument, the most close result is this:

enter image description here

Tried with windows paint, get the same font like PIL:

enter image description here

There are slight differences ,you should be able to locate it:

enter image description here

I realize it could be the position parameter is decimal, but pillow text method seems to truncate decimal position parameter to integers, changing the position parameter to decimal makes no difference. What should I do?

Upvotes: 3

Views: 1002

Answers (2)

Red
Red

Reputation: 27577

This is the cause of Pillow's aliasing. Unfortunately, Pillow doesn't support anti-aliasing, but you might get some information at this post: Python Imaging Library - Text rendering.

One of the answers there stated:

I've never used PIL, but a quick review of the documentation for the Draw method indicates that PIL provides a way to render simple graphics.

So it indicates that PIL simplifies the text rendered on its images. The OP of the question eventually posted an answer that I find very helpful:

I came up with my own solution that I find acceptable.

What I did was render the text large, like 3x the size it needs to be then scale it resize it down with antialiasing, it's not 100% perfect, but it's a hell of a lot better than default, and doesn't require cairo or pango.

For example,


 image = Image.new("RGBA", (600, 150), (255, 255, 255)) 
 draw = ImageDraw.Draw(image) 
 font = ImageFont.truetype("resources/HelveticaNeueLight.ttf", fontsize) 
 
 draw.text((10, 0), txt, (0,0,0), font=font) 
 img_resized = image.resize((188,45), Image.ANTIALIAS) 

and you end up with this result:

enter image description here

which is a lot better than what I was getting before with the same font.

Upvotes: 0

Zain Ul Abidin
Zain Ul Abidin

Reputation: 2710

The problem is neither in the PIL nor in your code you are doing it right, but bear in mind while working with font rendering in images.

Your Problem

  1. It is very complex problem, implemented many times, improved many times still its a very huge research topic.
  2. PIL uses its own font rendering engine, and "Microsoft YaHei" means you are running code in windows most probably & it has it's own rendering engine its just an assumption.
  3. This change of implementation in both PIL & Windows will always makes some difference does not matter if you provide same arguments to both of these.

The Solution

  1. Use Win32API in python to use windows rendering engine in python to obtain almost same results or at least nearest.

  2. Or try to experiment with gamming library pygame, as it is made to develop games so it will handle font rendering more precisely and using a sophisticated way to achieve reliability.

Using Windows API

import ctypes
import struct
import win32con
import win32gui
import win32ui

from PIL import Image


def RGB(r, g, b):    
    return r | (g << 8) | (b << 16)

def native_bmp_to_pil(hdc, bitmap_handle, width, height):
    bmpheader = struct.pack("LHHHH", struct.calcsize("LHHHH"),
                            width, height, 1, 24) #w,h, planes=1, bitcount)
    c_bmpheader = ctypes.c_buffer(bmpheader)

    #3 bytes per pixel, pad lines to 4 bytes    
    c_bits = ctypes.c_buffer(" " * (height * ((width*3 + 3) & -4)))

    res = ctypes.windll.gdi32.GetDIBits(
        hdc, bitmap_handle, 0, height,
        c_bits, c_bmpheader,
        win32con.DIB_RGB_COLORS)
    if not res:
        raise IOError("native_bmp_to_pil failed: GetDIBits")

    im = Image.frombuffer(
        "RGB", (width, height), c_bits,
        "raw", "BGR", (width*3 + 3) & -4, -1)
    return im    


class Win32Font:
    def __init__(self, name, height, weight=win32con.FW_NORMAL,
                 italic=False, underline=False):
        self.font = win32ui.CreateFont({
            'name': name, 'height': height,
            'weight': weight, 'italic': italic, 'underline': underline})

        #create a compatible DC we can use to draw:
        self.desktopHwnd = win32gui.GetDesktopWindow()
        self.desktopDC = win32gui.GetWindowDC(self.desktopHwnd)
        self.mfcDC = win32ui.CreateDCFromHandle(self.desktopDC)         
        self.drawDC = self.mfcDC.CreateCompatibleDC()

        #initialize it
        self.drawDC.SelectObject(self.font)

    def renderText(self, text):
        """render text to a PIL image using the windows API."""
        self.drawDC.SetTextColor(RGB(255,0,0))

        #create the compatible bitmap:
        w,h = self.drawDC.GetTextExtent(text)
        
        saveBitMap = win32ui.CreateBitmap()
        saveBitMap.CreateCompatibleBitmap(self.mfcDC, w, h)        
        self.drawDC.SelectObject(saveBitMap)

        #draw it
        self.drawDC.DrawText(text, (0, 0, w, h), win32con.DT_LEFT)

        #convert to PIL image
        im = native_bmp_to_pil(self.drawDC.GetSafeHdc(), saveBitMap.GetHandle(), w, h)

        #clean-up
        win32gui.DeleteObject(saveBitMap.GetHandle())

        return im        

    def __del__(self):
        self.mfcDC.DeleteDC()
        self.drawDC.DeleteDC()
        win32gui.ReleaseDC(self.desktopHwnd, self.desktopDC)
        win32gui.DeleteObject(self.font.GetSafeHandle())

    def __del__(self):
        win32gui.DeleteObject(self.font.GetSafeHandle())

and to use that

f = Win32Font("Your Font Name e.g. Microsoft YaHei", 15) #to use your font file install the font in windows
im = f.renderText("your text") #render your text
im.save("/path/to/image") #save your image if needed

Using Pygame

pygfont = pygame.font.Font(r"c:\windows\fonts\yourcustomfont.ttf", 15)
surf = pygfont.render("your text to render", False, (0,0,0), (255,255,255)) #False means anti aliasing disabled, you can experiment with enabled flag also
pygame.image.save(surf, r"/path/to/your/image")

To install pygame run pip install pygame for python2 or pip3 install pygame for python3

Upvotes: 3

Related Questions