6.파이썬을 이용한 디스코드 봇 만들기: PIL Image 만들기

4 분 소요

이미지를 다루는 Pillow 라이브러리

안녕하세요! 이번 글은 파이썬을 이용한 디스코드 봇 만들기의 마지막 글입니다. 이번 글에서는 Pillow 라이브러리를 이용하여 이미지를 다루고 결과물을 봇으로 전송해보겠습니다.

1. Pillow 라이브러리

파이썬에서 이미지를 처리하려면 외부 라이브러리를 이용해야 합니다. 저희는 Pillow 라이브러리를 이용하여 이미지를 직접 처리해보겠습니다.

pip install Pillow

이제 라이브러리를 설치했으니 간단한 예제로 이미지를 생성해보겠습니다. Pillow 라이브러리는 PIL 을 import 하여 사용합니다.

from PIL import Image
# 50x50 픽셀 사이즈의 검정색 RGB 이미지
im = Image.new(mode="RGB", size=(50, 50), color=(0, 0, 0))

im.show() # 이미지 출력

모든 이미지는 PIL.Image 클래스를 통해 표현합니다. 이때 Image.open 으로 이미지를 열거나 Image.save 를 통해 이미지를 저장할 수 있습니다.

이번에는 생성한 이미지에 다른 이미지나 글씨를 추가해보겠습니다. 먼저 해당 작업을 하려면 PIL.Image, PIL.ImageDraw, PIL.ImageFont 라이브러리가 필요합니다. 이때 ImageFont 클래스를 통하여 시스템에 있는 폰트를 불러올 수 있는데, 사용자마다 시스템에 설치되어 있는 폰트의 종류가 다를 수 있기 때문에 설치된 폰트를 정확히 알아야합니다.

from PIL import Image, ImageDraw, ImageFont

im = Image.new(mode="RGB", size=(50, 50), color=(0, 0, 0))
# 추가할 다른 이미지 생성
im2 = Image.new(mode="RGB", size=(25, 25), color=(255, 255, 255))
# (0, 0) 좌표에 이미지 붙여넣기
im.paste(im2, (0, 0))

# 텍스트 출력을 위한 ImageDraw.Draw
draw = ImageDraw.Draw(im)
font = ImageFont.truetype("Menlo.ttc", 20) #트루타입 폰트
# (1, 1) 좌표에 'A' 텍스트 생성
draw.text((1, 1), "A", font=font, fill=(0, 0, 0))

im.show()
  • 실행 결과

im1

2. Riot API 데이터 이미지 프로세싱

그러면 이전 글에서 다루었던 Riot API의 데이터를 이미지로 표현해보겠습니다.

  • 배경 이미지 생성
from PIL import Image, ImageDraw, ImageFont

im = Image.new("RGB", (400, 300), (255, 255, 255))
blue_image = Image.new("RGB", (200, 50), (0, 0, 255))
red_image = Image.new("RGB", (200, 50), (255, 0, 0))
line = Image.new("RGB", (400, 50), (230, 230, 230))
blank = Image.new("RGB", (2, 300), (0, 0, 0))

im.paste(blue_image, (0, 0))
im.paste(red_image, (200, 0))

for i in range(6):
    if i % 2 == 1:
        im.paste(line, (0, i * 50))

im.paste(blank, (199, 0))  # 구분선
  • 실행 결과

im2

  • 테스트 데이터 출력
font = ImageFont.truetype("Menlo.ttc", 20)
d = ImageDraw.Draw(im)
d.text((10, 10), "블루 팀", font=font, fill=(0, 0, 0))
d.text((210, 10), "레드 팀", font=font, fill=(0, 0, 0))
  • 실행 결과

im3

아쉽게도 한글 폰트가 깨지는 현상이 발생했습니다. 이는 불러온 트루타입 폰트가 한글 유니코드를 지원하지 않았기 때문이므로 한글을 지원하는 폰트를 불러와야 합니다. 저는 MacOS의 기본 한글 폰트인 애플고딕체를 사용했습니다.

  • 기존 코드에 이미지 추가
@bot.command()
async def 게임(ctx, *args):

        ...생략...
    # 배경 이미지 생성
    im = Image.new("RGB", (400, 300), (255, 255, 255))
    blue_image = Image.new("RGB", (200, 50), (0, 0, 255))
    red_image = Image.new("RGB", (200, 50), (255, 0, 0))
    line = Image.new("RGB", (400, 50), (230, 230, 230))
    blank = Image.new("RGB", (2, 300), (0, 0, 0))

    im.paste(blue_image, (0, 0))
    im.paste(red_image, (200, 0))

    for i in range(6):
        if i % 2 == 1:
            im.paste(line, (0, i * 50))

    im.paste(blank, (199, 0))  # 구분선
    # 이미지에 텍스트 추가
    font = ImageFont.truetype("AppleSDGothicNeo.ttc", 20)
    d = ImageDraw.Draw(im)
    d.text((10, 10), "블루 팀", font=font, fill=(0, 0, 0))
    d.text((210, 10), "레드 팀", font=font, fill=(0, 0, 0))

    font = ImageFont.truetype("AppleSDGothicNeo.ttc", 13)
    for i, data in zip(range(1, 11), participants):
        if i < 6:
            d.text((10, i * 50 + 20), data["championId"], font=font, fill=(0, 0, 0))
            d.text((100, i * 50 + 20), data["summonerName"], font=font, fill=(0, 0, 0))
        else:
            d.text((210, (i - 5) * 50 + 20), data["championId"], font=font, fill=(0, 0, 0))
            d.text((300, (i - 5) * 50 + 20), data["summonerName"], font=font, fill=(0, 0, 0))
  • 실행 결과

im4

하지만 챔피언을 텍스트로 출력한다면 기존의 discord.Embed에서 이미지로 변환한 의미가 없겠죠? 이번에는 라이엇의 DataDragon에서 제공하는 챔피언 썸네일 이미지를 불러와서 적용해봅시다.

  • 챔피언 이미지 불러오기

이전에 사용했었던 requests라이브러리를 통하여 이미지를 불러와봅시다. DataDragon 에서 (챔피언이름).png 파일 형식을 통해 쉽게 이미지 파일을 가져올 수 있습니다. 하지만 requests 라이브러리에서는 이미지파일 또한 byte 포맷으로 데이터를 가져오기 때문에 이를 변환하기 위한 BytesIO 클래스가 필요합니다.

import requests
from io import BytesIO

url = ("https://ddragon.leagueoflegends.com/cdn/11.4.1/img/champion/Teemo.png")
req = requests.get(url).content

im = Image.open(BytesIO(res))

저희가 기존에 작성한 코드에서는 실시간 매치 데이터를 불러올 때 championId 값을 챔피언의 한글 명으로 교체했었습니다. 하지만 DataDragon에서 이미지 파일을 불러오려면 챔피언의 한글 이름이 아닌 영문 이름이 필요합니다.

이는 간단히 기존의 코드를 수정해서 영문 챔피언 이름을 가져올 수 있습니다. 그리고 주의 할 점은 DataDragonchampion.json에서 챔피언의 이름을 가져올 때 name이 아니라 id를 가져와야 한다는 것 입니다. 리그 오브 레전드 챔피언들중에는 띄어쓰기가 포함된 이름들이 존재하는데 이러한 챔피언들의 이미지 파일은 띄어쓰기가 포함되지 않은 이름으로 되어있기 때문입니다.

/img/champion/Twisted Fate.png (X)
/img/champion/TwistedFate.png (O)

  • 코드 수정
# 기존의 ko_KR -> en_US 로 변경
static_champ_list = requests.get("http://ddragon.leagueoflegends.com/cdn/11.4.1/data/en_US/champion.json").json()
# row["name"] -> row["id"] 로 변경
champ_dict = {}
    for champ in static_champ_list["data"]:
        row = static_champ_list["data"][champ]
        champ_dict[row["key"]] = row["id"]

이제 모든 준비가 끝났으니 영문 명으로 된 챔피언 이름의 이미지 파일을 불러옵시다.

from io import BytesIO # 라이브러리 추가

# 간단한 메소드 추가
def getChampionImage(name):
    url = "https://ddragon.leagueoflegends.com/cdn/11.4.1/img/champion/"+name+".png"
    res = requests.get(url).content
    im = Image.open(BytesIO(res))
    return im.resize((40, 40))

# 기존 이미지 코드 수정
    ...생략...

font = ImageFont.truetype("AppleSDGothicNeo.ttc", 13)
    for i, data in zip(range(1, 11), participants):
        if i < 6:
            im.paste(getChampionImage(data['championId']), (10, i * 50 + 5))
            d.text((100, i * 50 + 20), data["summonerName"], font=font, fill=(0, 0, 0))
        else:
            im.paste(getChampionImage(data['championId']), (210, (i - 5) * 50 + 5))
            d.text((300, (i - 5) * 50 + 20), data["summonerName"], font=font, fill=(0, 0, 0))
  • 실행 결과

im5

3. 이미지 전송

이제 전송할 이미지를 완성했으니 디스코드 봇을 통해 직접 이미지 파일을 전송해봅시다. 디스코드에서 파일을 전송해야 하므로 discord.py 라이브러리의 discord.File 클래스를 이용해봅시다.

class discord.File(fp, filename=None, *, spoiler=False)

If the file-like object passed is opened via open then the modes ‘rb’ should be used. To pass binary data, consider usage of io.BytesIO.

rb 모드로 열어진 파일 오브젝트를 보낼 수 있다고 하네요. 저희는 이미지를 전송해야하므로 BytesIO를 통해 바이너리 데이터로 변환한 후, discord.File 인스턴스를 생성하여 디스코드 봇으로 전송해봅시다.

@bot.command()
async def 게임(ctx, *args):

        ...생략...

    with BytesIO() as image_binary:
        # 이미지를 BytesIO 스트림에 저장
        im.save(image_binary, "png")
        # BytesIO 스트림의 0바이트(처음)로 이동
        image_binary.seek(0)
        # discord.File 인스턴스 생성
        out = discord.File(fp=image_binary, filename="image.png")
        await ctx.send(file=out)
    
  • 실행 결과

im6

마무리하며..

Riot API를 이용하여 리그 오브 레전드 실시간 매치 데이터를 가져오는 간단한 디스코드 봇을 만들어보았습니다. 글에서 다룬 여러가지 방법들을 응용하여 자신만의 특별한 봇을 만들어보셨으면 좋겠습니다. 궁금한 점 있으시면 언제든지 질문해주시고 제가 만든 디스코드 봇의 코드는 저의 GitHub repo에서 확인하실 수 있습니다. 감사합니다!

References

댓글남기기