ハンコを押す技術 (Pythonで画像生成)


こんにちは。京セラコミュニケーションシステムの徳丸です。( @kccs_masataka-tokumaru )

無くなりそうで無くならない、ハンコありますよね。

昨今だと、書類もデータでのやり取りが増えてきましたが、それでも付きまとうハンコ。

仕方がないので、ハンコの画像を生成してペーストするハンコ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3809400/dce92474-e692-d323-38d8-feef28019807.png)

こういうタイプは1回作っておけばいいですが、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3809400/6ff955db-450e-585a-c37d-641987b80d82.png)


以外と作るのがめんどくさい、データ印。

今回は、Windows上でPythonを使って、データ印の画像を生成・クリップボードにコピーしてくれるツールを作ってみようと思います。

Pythonで画像を扱えるようになっておくと、他にも応用が効きそうです。

# この記事の狙い・対象者

独力でPythonのコードが書ける方。

ざっくり出力する画像の設計図を作成して、ハンコ画像を生成する。

ハンコ画像を生成を題材に、Pythonによる画像生成・加工の実例を示す。

ハンコ画像生成を例にPythonによる画像生成・加工の経験を積み、様々なことに応用できるようになる。

# この記事で説明しないこと

Pillowなどライブラリの細かい使い方。引数の説明。

# 作る図(ハンコ)のレイアウトを考える

それでは、ハンコ画像を作っていきます。

プログラミングするより先に、ある程度のレイアウトを図にしておくと楽そうです。

まずは、レイアウトを考えてみようと思います。

一旦、ハンコのサイズは100px x 100pxで仮定して、後で調整できるるようにします。

大きな〇があって、大体その円を三等分する形で、2本の線を引きたいです。

ピッタリ三等分だと、ちょっと上の領域と下の領域が狭いので上下の領域を若干広めにとります。


<img width="500px" alt="データ印の設計図画像1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3809400/88063efc-3a99-276c-62b4-db3243c2815c.png">

大体こんな感じでしょうか。

<img width="500px" alt="データ印の画像2" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3809400/f5b85624-598f-ebca-0e15-959272547351.png">

これに、上の文字、中央の文字、下の文字の3つを中央ぞろえで重ねてやると良さそうです。

# 入力値を考える

今回は、上面・下面に最大4文字ずつ受け取れるようにして、

中面は、日付を入れる(YYYY/MM/DD) 想定で最大10文字受け取れるようにします。

その入力を用いて判子画像を出力します。

どういうものを作るのかは、先にこの記事の「動作確認」のセクションを読むと分かりやすいと思います。

# 実装する

さて本題の実装です。

今回は対象をWindows限定とします。

今回は、`Pillow` ライブラリを使用してPythonで作図していきます。

このライブラリを使用することで、キャンバスを作成し、線を引いたり、文字を書いたり、円などの図形を画像として描画することができます。

https://pypi.org/project/pillow/


また、生成した画像は最終的にクリップボードにコピーするようにしたいと思います。

動作環境がWindows限定になってしまいますが、`pywin32` パッケージの `win32clipboard` モジュールを使ってクリップボードにコピーできるようにします。

pywin32は、Excelを自動操作したり、その他さまざまななWindowsのAPIを実行できますが、今回はクリップボードにコピーする機能のみを使います。

https://pypi.org/project/pywin32/

下記のコマンドで、ライブラリをインストールします。

```bash
pip install pillow pywin32
```

ライブラリを読み込み、設計図に従って実装していきます。

1行ずつ説明すると見づらく大変なので、コメントでそれぞれ説明していきます。

```python
from PIL import Image, ImageDraw, ImageFont
import win32clipboard
import io

# カッコイイタイトルロゴ
TITLE_LOGO = """
=====================================================
 ____  _
/ ___|| |_ __ _ _ __ ___  _ __
\___ \| __/ _` | '_ ` _ \| '_ \\
 ___) | || (_| | | | | | | |_) |
|____/ \__\__,_|_| |_| |_| .__/
                         |_|
                                 _
  __ _  ___ _ __   ___ _ __ __ _| |_ ___  _ __
 / _` |/ _ \ '_ \ / _ \ '__/ _` | __/ _ \| '__|
| (_| |  __/ | | |  __/ | | (_| | || (_) | |
 \__, |\___|_| |_|\___|_|  \__,_|\__\___/|_|
 |___/
=====================================================
"""

def copy_clipboard(data):
    """
    クリップボードにビットマップデータをコピーする
    """
    win32clipboard.OpenClipboard()
    win32clipboard.EmptyClipboard()
    win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
    win32clipboard.CloseClipboard()

def input_prompt(prompt_text, min_len, max_len):
    """
    入力を受け取りざっくりバリデーションする。
    """

    while True:
        input_text = input(prompt_text+"\n")
        if min_len <= len(input_text) and len(input_text) <= max_len:
            return input_text
        else:
            print("入力文字数が上限を超えています。")

def get_font_size(canvas_size, length):
    """
    入力文字数に基づいてフォントサイズを決定する。
    """

    if 0 <= length and length <= 1:
        return int(canvas_size * 0.2)
    if 2 <= length and length <= 3:
        return int(canvas_size * 0.18)
    if 4 <= length and length <= 5:
        return int(canvas_size * 0.15)
    return int(canvas_size * 0.13)

def main():
    # 最終的に出力されるハンコの画像サイズ(正方形の1辺の長さpx)
    OUTPUT_CANVAS_SIZE = 100

    # 内部的にはデカイ画像で作成しておいて、
    # 出力時に縮小した方が、疑似アンチエイリアスが効いて綺麗に出力できる。
    # そのため、一旦出力より10倍大きいキャンバスに作図する。
    INTERNAL_CANVAS_SIZE = OUTPUT_CANVAS_SIZE * 10

    # 作図する際の線の太さ100pxのキャンパスなら3pxで出力される。
    LINE_WIDTH = int(INTERNAL_CANVAS_SIZE*0.03)

    # カッコイイタイトルロゴを表示して、使い方を説明。
    print(TITLE_LOGO)
    print("ハンコ画像を作成し、クリップボードへコピーします。\n 中断する場合は「Ctrl + C」を押してください。\n")

    # 入力を受け取る
    upper_text = input_prompt("データ印の上に表示する文字を入力してください(最大4文字)", 0, 4)
    under_text = input_prompt("データ印の下に表示する文字を入力してください(最大4文字)", 0, 4)
    center_text = input_prompt("データ印の真ん中に表示する文字を入力してください(最大10文字)", 0, 10)

    # 描画する文字(フォントとフォントサイズ)の設定
    FONT_PATH = 'C:/Windows/Fonts/meiryob.ttc'
    upper_text_font = ImageFont.truetype(FONT_PATH, get_font_size(INTERNAL_CANVAS_SIZE, len(upper_text)))
    center_text_font = ImageFont.truetype(FONT_PATH, get_font_size(INTERNAL_CANVAS_SIZE, len(center_text)))
    under_text_font = ImageFont.truetype(FONT_PATH, get_font_size(INTERNAL_CANVAS_SIZE, len(under_text)))

    # (R,G,B)で色を指定
    WHITE = (255, 255, 255)
    RED = (255, 0, 0)

    # 描画するキャンバスを作成
    im = Image.new('RGB', (INTERNAL_CANVAS_SIZE, INTERNAL_CANVAS_SIZE), WHITE)
    draw = ImageDraw.Draw(im)

    # キャンバスサイズ(100px x 100px)の円を描画する。
    # 外周は3px (キャンバスサイズの3%) の赤線で、内部は白で塗りつぶす。
    draw.ellipse((0, 0, INTERNAL_CANVAS_SIZE, INTERNAL_CANVAS_SIZE), fill=WHITE, outline=RED, width=LINE_WIDTH)

    # 円を分割する赤線を3px(キャンバスサイズの3%)で引く(1本目)
    # (線の始点x, 線の始点y, 線の終点x, 線の終点y) 
    # ※ xを0～100にしてしまうと円を突き抜けてしまうので5～95(キャンバスサイズの5%～95%)
    upper_line_xy=(INTERNAL_CANVAS_SIZE*0.05, INTERNAL_CANVAS_SIZE*0.35, INTERNAL_CANVAS_SIZE*0.95, INTERNAL_CANVAS_SIZE*0.35)
    draw.line(upper_line_xy, fill=RED, width=LINE_WIDTH)

    # 円を分割する赤線を3pxで引く(2本目)
    # 中面は30px(キャンバスサイズの30%)なので、キャンバスサイズの3割を占めるように引く
    under_line_xy=(upper_line_xy[0], upper_line_xy[1]+(INTERNAL_CANVAS_SIZE*0.3), upper_line_xy[2], upper_line_xy[3]+(INTERNAL_CANVAS_SIZE*0.3))
    draw.line(under_line_xy, fill=RED, width=LINE_WIDTH)

    # 上の文字を描画する。上面は0px～35x(キャンバスサイズの35%)の範囲。その間に文字を配置したいので35/2=17.5。
    # 実際は円の上が狭く、ちょっと下に配置した方が見栄えがいいので、少し下に補正(offsetの値)。
    offset = 0.3
    draw.text((INTERNAL_CANVAS_SIZE/2.0, upper_line_xy[1]/(2.0-offset)), upper_text, fill=RED, font=upper_text_font, anchor='mm')

    # 下の文字を描画する下面。は65px～100(キャンバスサイズの65%～100%の範囲)
    # 同様には円の下が狭く、ちょっと上に配置した方が見栄えがいいので、少し上に補正(offsetの値)。
    draw.text((INTERNAL_CANVAS_SIZE/2.0, under_line_xy[1] + (INTERNAL_CANVAS_SIZE - under_line_xy[1])/(2.0+offset)), under_text, fill=RED, font=under_text_font, anchor='mm')

    # 中面の文字を描画する。キャンバスの真ん中に配置したいので、キャンバスサイズの半分。
    draw.text((INTERNAL_CANVAS_SIZE/2.0, INTERNAL_CANVAS_SIZE/2.0), center_text, fill=RED, font=center_text_font, anchor='mm')

    # 最終的に出力したいサイズに縮小
    im = im.resize((OUTPUT_CANVAS_SIZE, OUTPUT_CANVAS_SIZE))

    # Bitmapとして出力
    output = io.BytesIO()
    im.convert('RGB').save(output, 'BMP')
    data = output.getvalue()[14:]
    output.close()

    # クリップボードに画像ファイルをコピーする
    copy_clipboard(data)
    print("データ印をクリップボードにコピーしました")
    input("終了するにはEnterキーを押してください")

if __name__ == "__main__":
    main()
```

# 動作確認

実行してみます。

```bash
python stamp_generator.py
```

```
=====================================================
 ____  _
/ ___|| |_ __ _ _ __ ___  _ __
\___ \| __/ _` | '_ ` _ \| '_ \
 ___) | || (_| | | | | | | |_) |
|____/ \__\__,_|_| |_| |_| .__/
                         |_|
                                 _
  __ _  ___ _ __   ___ _ __ __ _| |_ ___  _ __
 / _` |/ _ \ '_ \ / _ \ '__/ _` | __/ _ \| '__|
| (_| |  __/ | | |  __/ | | (_| | || (_) | |
 \__, |\___|_| |_|\___|_|  \__,_|\__\___/|_|
 |___/
=====================================================

ハンコ画像を作成し、クリップボードへコピーします。
 中断する場合は「Ctrl + C」を押してください。

データ印の上に表示する文字を入力してください(最大4文字)
山田
データ印の下に表示する文字を入力してください(最大4文字)
太郎
データ印の真ん中に表示する文字を入力してください(最大10文字)
2024/06/28
データ印をクリップボードにコピーしました
終了するにはEnterキーを押してください
```

クリップボードにハンコ画像がコピーされたようなので、ペイントに張り付けてみます。

![生成されたハンコの画像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3809400/3df9f02e-9539-13c0-96a7-5ab9f5a3c641.png)

無事ハンコが生成されました！これでハンコ画像が欲しくなっても困らないですね。

# 応用編

今回はハンコ画像を作りましたが、このようにPythonで画像編集・生成をした経験があるといろいろなことに応用できそうですね。

下の画像のような、字幕のテロップを一括作成したり。

<img width="500px" alt="テロップ画像" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3809400/667fe934-cf31-e7d2-d8cd-374fd1d8ac99.png">

よくあるタイトルロゴジェネレータなども作れそうです。

是非是非いろんな作業の自動化に応用してみてください！

ここまでご覧くださいましてありがとうございました。