[Python] Create a linebot that draws any date on a photo

What I made

I created a linebot that returns a date selection action when you send an image and draws the selected date on the image. Basically, I just made it possible to select the date part of this article by myself, but since there were various stumbling points, I will summarize it this time. Reference: [Python] I made a LINE Bot that dates photos

environment

What not to write in this article

--LineBot channel creation --Deploy to heroku

Full text

main.py

main.py


from flask import Flask, request, abort
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import (PostbackEvent, TemplateSendMessage, ButtonsTemplate, DatetimePickerTemplateAction,
                            ImageMessage, ImageSendMessage, MessageEvent, TextMessage, TextSendMessage)

from pathlib import Path
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import datetime
import os
import re

app = Flask(__name__)
app.debug = False

#Get environment variables
YOUR_CHANNEL_ACCESS_TOKEN = os.environ["YOUR_CHANNEL_ACCESS_TOKEN"]
YOUR_CHANNEL_SECRET = os.environ["YOUR_CHANNEL_SECRET"]

line_bot_api = LineBotApi(YOUR_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(YOUR_CHANNEL_SECRET)

#Image referrer path
SRC_IMAGE_PATH = "static/images/{}.jpg "
MAIN_IMAGE_PATH = "static/images/{}_main.jpg "
PREVIEW_IMAGE_PATH = "static/images/{}_preview.jpg "

@app.route("/callback", methods=['POST'])
def callback():
    signature = request.headers['X-Line-Signature']

    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)

    return 'OK'

#Follow event
@handler.add(FollowEvent)
def handle_follow(event):
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=
        "Thank you for registering as a friend. If you send an image and tell me the shooting date, I will write that date on the image"))

#Text parrot return
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=event.message.text))

#Receiving images
@handler.add(MessageEvent, message=ImageMessage)
def get_image(event):
    global message_id

    #message_Get id
    message_id = event.message.id
    
    #File name message_Path converted to id
    src_image_path = Path(SRC_IMAGE_PATH.format(message_id)).absolute()

    #Temporarily save images to heroku
    save_image(message_id, src_image_path)
    
    #Save as an image to display when selecting the date and time
    im = Image.open(src_image_path)
    im.save(src_image_path)
    
    #Selection of shooting date
    date_picker = TemplateSendMessage(
        alt_text='Please select the shooting date',
        template=ButtonsTemplate(
            text='Please select the shooting date',
            thumbnail_image_url=f"https://<heroku app name>.herokuapp.com/{src_image_path}",
            actions=[
                DatetimePickerTemplateAction(
                    label='Choice',
                    data='action=buy&itemid=1',
                    mode='date',
                    initial=str(datetime.date.today()),
                    max=str(datetime.date.today())
                )
            ]
        )
    )
    
    line_bot_api.reply_message(
        event.reply_token,
        date_picker
    )

#Process and send images
@handler.add(PostbackEvent)
def handle_postback(event):
    #File name message_Path converted to id
    src_image_path = Path(SRC_IMAGE_PATH.format(message_id)).absolute()
    main_image_path = MAIN_IMAGE_PATH.format(message_id)
    preview_image_path = PREVIEW_IMAGE_PATH.format(message_id)
    
    #Image processing
    date_the_image(src_image_path, Path(main_image_path).absolute(), event)
    date_the_image(src_image_path, Path(preview_image_path).absolute(), event)

    #Send image
    image_message = ImageSendMessage(
            original_content_url=f"https://<heroku app name>.herokuapp.com/{main_image_path}",
            preview_image_url=f"https://<heroku app name>.herokuapp.com/{preview_image_path}"
    )
    
    #Get log
    app.logger.info(f"https://<heroku app name>.herokuapp.com/{main_image_path}")
    
    line_bot_api.reply_message(event.reply_token, image_message)

#Image storage function
def save_image(message_id: str, save_path: str) -> None:
    # message_Get binary data of image from id
    message_content = line_bot_api.get_message_content(message_id)
    with open(save_path, "wb") as f:
        #Write the acquired binary data
        for chunk in message_content.iter_content():
            f.write(chunk)

#Image processing function
def date_the_image(src: str, desc: str, event) -> None:
    im = Image.open(src)
    draw = ImageDraw.Draw(im)
    font = ImageFont.truetype("./fonts/Helvetica.ttc", 50)
    #Get the date of the date and time selection action
    text = event.postback.params['date']
    #String replacement with regular expression
    text_mod = re.sub("-", "/", text)
    #Text size
    text_width = draw.textsize(text_mod, font=font)[0]
    text_height = draw.textsize(text_mod, font=font)[1]
    
    margin = 10
    x = im.width - text_width
    y = im.height - text_height
    #The size of the rectangle to draw
    rect_size = ((text_width + margin * 6), (text_height + margin * 2))
    #Rectangle drawing
    rect = Image.new("RGB", rect_size, (0, 0, 0))
    #Mask to make the rectangle transparent
    mask = Image.new("L", rect_size, 128)
    
    #Paste the rectangle and mask on the image
    im.paste(rect, (x - margin * 6, y - margin * 3), mask)
    #Writing text
    draw.text((x - margin * 3, y - margin * 2), text_mod, fill=(255, 255, 255), font=font)
    im.save(desc)

if __name__ == "__main__":
    #app.run()
    port = int(os.getenv("PORT", 5000))
    app.run(host="0.0.0.0", port=port)

Commentary

Preparation

from flask import Flask, request, abort
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import (PostbackEvent, TemplateSendMessage, ButtonsTemplate, DatetimePickerTemplateAction,
                            ImageMessage, ImageSendMessage, MessageEvent, TextMessage, TextSendMessage)

from pathlib import Path
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import datetime
import os
import re

app = Flask(__name__)
app.debug = False

#Get environment variables
YOUR_CHANNEL_ACCESS_TOKEN = os.environ["YOUR_CHANNEL_ACCESS_TOKEN"]
YOUR_CHANNEL_SECRET = os.environ["YOUR_CHANNEL_SECRET"]

line_bot_api = LineBotApi(YOUR_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(YOUR_CHANNEL_SECRET)

#Image referrer path
SRC_IMAGE_PATH = "static/images/{}.jpg "
MAIN_IMAGE_PATH = "static/images/{}_main.jpg "
PREVIEW_IMAGE_PATH = "static/images/{}_preview.jpg "

@app.route("/callback", methods=['POST'])
def callback():
    signature = request.headers['X-Line-Signature']

    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)

    return 'OK'

Import the module and get the environment variables set in advance, but I hope you can check the detailed role as appropriate. Set the path of the image reference source described later and make an empty list to store ~~ message_id. ~~ Replace the {} part with the message_id when the image was received.

Send text when following

#Follow event
@handler.add(FollowEvent)
def handle_follow(event):
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=
        "Thank you for registering as a friend. If you send an image and tell me the shooting date, I will write that date on the image"))

A message is sent when a user adds a friend.

Text parrot return

#Text parrot return
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=event.message.text))

I will return the text parrot, but it is not necessary.

Receiving images

#Receiving images
@handler.add(MessageEvent, message=ImageMessage)
def get_image(event):
    global message_id

    #message_Get id
    message_id = event.message.id
    
    #File name message_Path converted to id
    src_image_path = Path(SRC_IMAGE_PATH.format(message_id)).absolute()

    #Temporarily save images to heroku
    save_image(message_id, src_image_path)
    
    #Save as an image to display when selecting the date and time
    im = Image.open(src_image_path)
    im.save(src_image_path)
    
    #Selection of shooting date
    date_picker = TemplateSendMessage(
        alt_text='Please select the shooting date',
        template=ButtonsTemplate(
            text='Please select the shooting date',
            thumbnail_image_url=f"https://<heroku app name>.herokuapp.com/{src_image_path}",
            actions=[
                DatetimePickerTemplateAction(
                    label='Choice',
                    data='action=buy&itemid=1',
                    mode='date',
                    initial=str(datetime.date.today()),
                    max=str(datetime.date.today())
                )
            ]
        )
    )
    
    line_bot_api.reply_message(
        event.reply_token,
        date_picker
    )

ʻEvent.message_idgets the id assigned to each message and sets it globally for use in other events. .. ~~ This id cannot be obtained in other events, so store it in the emptymessage_listcreated in advance. ~~ After temporarily saving the data on heroku, open the saved data for display during the date selection action and save it again as an image. When returning the date selection action withTemplateSendMessage and asking the user to select the shooting date, it will be displayed if you specify the URL of the save destination with thumbnail_image_url`.

Image transmission

#Process and send images
@handler.add(PostbackEvent)
def handle_postback(event):
    #File name message_Path converted to id
    src_image_path = Path(SRC_IMAGE_PATH.format(message_id)).absolute()
    main_image_path = MAIN_IMAGE_PATH.format(message_id)
    preview_image_path = PREVIEW_IMAGE_PATH.format(message_id)
    
    #Image processing
    date_the_image(src_image_path, Path(main_image_path).absolute(), event)
    date_the_image(src_image_path, Path(preview_image_path).absolute(), event)

    #Send image
    image_message = ImageSendMessage(
            original_content_url=f"https://<heroku app name>.herokuapp.com/{main_image_path}",
            preview_image_url=f"https://<heroku app name>.herokuapp.com/{preview_image_path}"
    )
    
    #Get log
    app.logger.info(f"https://<heroku app name>.herokuapp.com/{main_image_path}")
    
    line_bot_api.reply_message(event.reply_token, image_message)

Gets the image received in the MessageEvent (ImageMessage) mentioned above. ~~ Get the id that will be the name of the image from message_list to determine. If images are sent continuously, the new id will be stored before the data is initialized, so specify the last (latest) one with message_list [-1]. Get the date selected by the user with handl_postback (event) and assign it to text of the function data_the_image to write text etc. When the process is finished, save the image and send it back to the user to finish.

Image storage function

#Image storage function
def save_image(message_id: str, save_path: str) -> None:
    # message_Get binary data of image from id
    message_content = line_bot_api.get_message_content(message_id)
    with open(save_path, "wb") as f:
        #Write the acquired binary data
        for chunk in message_content.iter_content():
            f.write(chunk)

Actually, I wanted to get the Exif information of the image and automatically enter the shooting date, but I could not get it with this method, so I took the form of the above date selection action as a painstaking measure. If anyone knows how to do it, please let me know.

Image processing function

#Image processing function
def date_the_image(src: str, desc: str, event) -> None:
    im = Image.open(src)
    draw = ImageDraw.Draw(im)
    font = ImageFont.truetype("./fonts/Helvetica.ttc", 50)
    #Get the date of the date and time selection action
    text = event.postback.params['date']
    #String replacement with regular expression
    text_mod = re.sub("-", "/", text)
    #Text size
    text_width = draw.textsize(text_mod, font=font)[0]
    text_height = draw.textsize(text_mod, font=font)[1]
    
    margin = 10
    x = im.width - text_width
    y = im.height - text_height
    #The size of the rectangle to draw
    rect_size = ((text_width + margin * 6), (text_height + margin * 2))
    #Rectangle drawing
    rect = Image.new("RGB", rect_size, (0, 0, 0))
    #Mask to make the rectangle transparent
    mask = Image.new("L", rect_size, 128)
    
    #Paste the rectangle and mask on the image
    im.paste(rect, (x - margin * 6, y - margin * 3), mask)
    #Writing text
    draw.text((x - margin * 3, y - margin * 2), text_mod, fill=(255, 255, 255), font=font)
    im.save(desc)

I wanted to make it look good to some extent, so I added some processing to it. ʻEvent.postback.params ['date']to get the date selected by the user, andre.sub ("-", "/", text) to change" YYYY-MM-DD "to" YYYY / Convert to the format "MM / DD". Use ((text_width + margin * 6), (text_height + margin * 2))to set the size of the rectangle and mask, and leave a margin of 30px on the left and right and 10px on the top and bottom of the text. Finally, specify the position of the rectangle and mask with ʻim.paste (rect, (x --margin * 6, y --margin * 3), mask)and paste it, anddraw.text ( Write the text with (x --margin * 3, y --margin * 2), text_mod, fill = (255, 255, 255), font = font)and you're done.

Run

if __name__ == "__main__":
    #app.run()
    port = int(os.getenv("PORT", 5000))
    app.run(host="0.0.0.0", port=port)

file organization

Summary

This line bot was the first app I made after self-education, but it took about half a year (about 1 to 2 hours a day on average) to make it so far. About 80% of the production time was spent investigating what I didn't understand, and there were times when it was quite difficult. In fact, there were times when I didn't open my computer for about a week, but I never thought about giving up programming, so I think I was having some fun doing it.

Actually, there were twists and turns from the first thing I wanted to make, and I settled on this shape, but I learned that it is very important to make the whole flow in advance to some extent. However, it is difficult to grasp what kind of work can be done with how much effort with little experience, so in the end I think that there is no choice but to proceed steadily with trial and error.

Currently, I am studying a database, and I am trying to save the name and date of birth of the person in the photo so that I can calculate back from the shooting date and display how old the photo is, so as soon as it is completed I will publish it.

reference

-[Python] I made a LINE Bot that dates photos -Implementation of Datetime picker action using line-bot-sdk-python and implementation sample of Image Carousel -Putalpha to create transparent png image with Python, Pillow

Recommended Posts

[Python] Create a linebot that draws any date on a photo
[Python] Create a LineBot that runs regularly
Create a Python environment on Mac (2017/4)
Create a python environment on centos
Create a python environment on your Mac
[Python] Create a linebot to write a name and age on an image
Create a page that loads infinitely with python
[Venv] Create a python virtual environment on Ubuntu
Python: Create a class that supports unpacked assignment
Create a Python execution environment on IBM i
Create a Python virtual development environment on Windows
Create a Python module
Create a Python environment
Create a comfortable Python 3 (Anaconda) development environment on windows
In Python, create a decorator that dynamically accepts arguments Create a decorator
[python] Create a date array with arbitrary increments with np.arange
Create a decent shell and python environment on Windows
Create a Python development environment on OS X Lion
Create a Wox plugin (Python)
Create a function in Python
Create a dictionary in Python
Create a Python (pyenv / virtualenv) development environment on Mac (Homebrew)
Create a list in Python with all followers on twitter
Loop through a generator that returns a date iterator in Python
Let's create a script that registers with Ideone.com in Python.
[Python] Create a date and time list for a specified period
Create a python numpy array
Create code that outputs "A and pretending B" in python
Create a Python script for Wake on LAN (NAT traversal Wake on LAN [5])
A program that failed when trying to create a linebot with reference to "Dialogue system made with python"
Create a classroom on Jupyterhub
Create a directory with python
Create a virtual environment for python on mac [Very easy]
Create a Python environment for professionals in VS Code on Windows
Easy! Implement a Twitter bot that runs on Heroku in Python
[Python / Django] Create a web API that responds in JSON format
[Ev3dev] Create a program that captures the LCD (screen) using python
[LINE Messaging API] Create a BOT that connects with someone with Python
Let's create a Python directory structure that you won't regret later
Python vba to create a date string for creating a file name
A python script that deletes ._DS_Store and ._ * files created on Mac
Building a Python environment on Mac
I made a Line-bot using Python!
Create a python GUI using tkinter
Create a DI Container in Python
Building a Python environment on Ubuntu
Create a virtual environment with Python!
Create a binary file in Python
Create a SlackBot service on Pepper
Create a Linux environment on Windows 10
Create a Python general-purpose decorator framework
Create a Kubernetes Operator in Python
5 Ways to Create a Python Chatbot
Build a python3 environment on CentOS7
Create a random string in Python
Introducing a library that was not included in pip on Python / Windows
[Python] I made a decorator that doesn't seem to have any use.
Create a Python development environment on Windows (Visual Studio Code remote WSL).
Steps to create a Python virtual environment with VS Code on Windows
Create and edit spreadsheets in any folder on Google Drive with python
Create a Python multi-user platform with JupyterHub + JupyterLab on Rapsberry Pi 3B +!