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
- It is very complex problem, implemented many times, improved many times still its a very huge research topic.
- 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.
- 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
Use Win32API in python to use windows rendering engine in python to
obtain almost same results or at least nearest.
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