プログラムでおかえしできるかな

定年を過ぎて何かの役に立てないかなと始めた元SEのブログです

画像サイズを変更し文字透かしを入れるアプリの作り方【Python】

このエントリーをはてなブックマークに追加

Python の pillow を使用して画像サイズを変更するアプリを作成しました。

次の特徴があります。
👍アスペクト比固定で縦か横のサイズを指定してサイズ変更します。
👍画像は exe にドラッグアンドドロップするかファイルダイアログで選択します。
👍Exif 情報を残すか消すか選択できます。
👍画像に文字で透かしを付加できます。

アプリの作り方をサンプルコードも交えて説明します。

アプリはお使いいただけます アプリ(バイナリ)を使ってみたい方は、別記事から取得できます。
 📔 画像サイズを変更し文字透かしを入れるアプリ【フリー】 🔗
目次

◆動作時の画像

検索結果

◆機能・特長

  • アスペクト比固定で縦か横のサイズを指定してサイズ変更します。
  • 画像は exe にドラッグアンドドロップするかファイルダイアログで選択します。
  • Exif情報を残すか消すか選択できます。
  • 画像に文字で透かしを付加できます。

◆考え方

指定された画像を pillow の Image.resize() メソッドでサイズ変換します。
必要なら Exif 情報を読み込んで保存用オプションに設定します。
文字透かしを施す場合、ImageDraw オブジェクトを作成し、文字列やフォントなどを設定して、text() メソッドで書き込みます。

具体的には、次の通りです。

  1. 対象画像ファイルパスの取得

    1. コマンドライン引数があったら、パスのタプルとする
    2. なかったら、ファイルダイアログを表示
    3. パスがなかったら終了
  2. 保存先フォルダの作成
    保存先フォルダがなかったら作成

  3. Exif 情報の取得

    • Exif 情報を書き込む場合、Exif 情報を読み込んで書き込みオプションを準備
    • Exif 情報を書き込まない場合、Exif 情報を空にするオプションを準備
  4. 画像サイズ変更

    • 幅が指定されていたら、幅を元にアスペクト比を維持して高さを計算してサイズ変更
    • 高さが指定されていたら、高さを元にアスペクト比を維持して高さを計算してサイズ変更
  5. 文字透かしの描画

    1. サイズ変換した画像を元に ImageDraw オブジェクトを作成
    2. フォントファイル名から ImageFont.truetype() メソッドで フォントオブジェクトを作成
    3. フォントオブジェクトの getsize() メソッドで書き込む文字列の長さを計測
    4. 画像の右下の位置から文字列を書き込む位置を計算
    5. 文字書き込み用オプションの作成
      fill, stroke_width, stroke_fillの設定値を辞書で作成
    6. 文字の書き込み
      ImageDrqwオブジェクト.text() メソッド
  6. 保存

    1. 画像のファイル名にサフィックスを付加した名前のファイルの存在確認
    2. Imageオブジェクト.save() メソッドで保存

◆pillow で画像サイズを変更する

pillow を使用して画像サイズを変更する手順は、次の通りです。

  1. 画像を開く Image.open(パス)
  2. サイズ変更 imageオブジェクト.resize((幅, 高さ))
  3. 保存 imageオブジェクト.save(パス)

◇画像サイズ変更

  • 【構文】imageオブジェクト.resize(size, resample=None, box=None, reducing_gap=None)
  • 主な引数
    • size:(幅, 高さ)のタプル
    • resample:サンプリングフィルター
      • フィルター(品質順)
        PIL.Image.NEAREST, PIL.Image.BOX, PIL.Image.BILINEAR, PIL.Image.HAMMING, PIL.Image.BICUBIC, PIL.Image.LANCZOS
        デフォルトはPIL.Image.BICUBIC
    • box:切り抜き範囲(x, y, 幅, 高さ)のタプル、無指定は全体
  • 戻り値
    • 新しい image オブジェクトが返ります

◇保存

  • 【構文】imageオブジェクト.save(fp, format=None, **params)
  • 主な引数
    • fp:ファイル名(文字列、pathlib.Pathオブジェクト、ファイルオブジェクト)
    • format:画像の種類
      Noneの場合、ファイル名の拡張子で判断
    • **params:ファイルの種類によって有効なオプションが異なる
      ただし、無効なオプションを指定してもエラーにはならない
      ⇒拡張子で判断してオプションを用意しなくても良い
  • 注意事項

    • 同じファイルが存在しても上書きする
    • 例外:ValueError(拡張子エラー)、OSError(書き込みエラー)
  • 拡張子ごとに異なるオプション

    • JPEG
      • quality:品質。0(悪い)~95(良い)。デフォルト:75
        ⇒画質を保持するなら95を設定すると良い
         元の画像と同じ品質で保存するには、
        img.save('test.jpg', quality=100, subsampling=0)

◎フォーマット変換

読み込み画像のフォーマットと異なる拡張子で保存するとフォーマット変換ができます。
ただし、画像のフォーマットによってはそのままでは保存できません。

  • PNG(rbga)⇒JPEG(rbg)の場合
    モードrbgaからrbgへの変換ができない
    ⇒convert()メソッドでモードの変換をすると保存できるようになる  例:convert("RBG")

アスペクト比の維持

ブログなどで使用する画像のサイズ変更は、ほとんどがアスペクト比を維持したままのサイズ変更と思われます。
ここでは、幅か高さのどちらかを指定して、もう一方は計算します。

計算式

  • 幅が指定された場合
    高さ = round(元の高さ * 幅 / 元の幅)
  • 高さが指定された場合
    幅 = round(元の幅 * 高さ / 元の高さ)

【コード】

    def scale_to_width(self, img, width):
        """
        アスペクト比を固定して、幅が指定した値になるようリサイズする。
        """
        height = round(img.height * width / img.width)
        return img.resize((width, height))

    def scale_to_height(self, img, height):
        """
        アスペクト比を固定して、高さが指定した値になるようリサイズする。
        """
        width = round(img.width * height / img.height)
        return img.resize((width, height))

◆pillow で画像のEixf情報を出力する

pillow で画像の Exif 情報を出力するには、読み込んだ画像から Exif 情報を取得して、保存時にその Exif 情報を設定する必要があります。

注意 pillow 6.0.0 から PNG でも Exif 情報がサポートされました。
PNG の場合、保存時に何も指定しないと Exif 情報が出力されます。
したがって、明示的に Exif 情報を保存しない設定が必要です。

Exif情報の取得

  • 【構文】Imageオブジェクト.getexif()
  • 戻り値
    PIL.Image.Exifクラス
    ※collections.abc.MutableMapping (辞書のようなもの) を継承

Exif情報の保存

読み込んだ画像をデフォルトで保存するとExif情報は書かれない。 Exif情報を書き込むには、画像からExif情報を読み込んで保存時に指定する必要がある。

  • 【構文】imageオブジェクト.save(fp, format=None, **params)
    -よく使う使い方(サンプル)
    【構文】imageオブジェクト.save(fp, exif=exif)
    ▽読み取りと保存のサンプル
    image1 = Image.open(file_name)
    exif = image1.getexif()
    image1.save(new_file_name, exif=exif)

PNGExif オプションを省略すると元の画像のまま Exif 情報が残ります。
消すためには、exif = bytes(b"") とします。

  • exif = None だと JPEG でエラーになってしまう

【コード】

        save_kwarg = {}

        # Exif情報を書き込む場合、読んだ画像のExif情報を保存時に設定
        if settings_img_conv.exif:
            save_kwarg["exif"] = img.getexif()
        else:
            save_kwarg["exif"] = bytes(b"") 
            # JPEGはオプションなしかこの指定でないとExif無しにできない
            # PNGはオプションなしだとExifを出力するのでNoneかこの指定。

        # 画像を保存    
        new_path = img_conv.save_image(
            dst_img, file_name, name_suffix, dst_path, overwrite=settings_img_conv.overwrite, **save_kwarg)

◆pillow で文字透かしを付加する

画像に文字透かしを付加するのは、単純に画像に文字を書き込むことです。
また、文字を書く機能に縁取りを付けるオプションがあります。

【手順】

  1. 読み込んだ Image オブジェクトから、書き込み用のImageDrawオブジェクトを作成
  2. 使用するフォントオブジェクトを作成
  3. 文字の書き込み

◇ImageDrawオブジェクトを作成

  • 【構文】ImageDraw.Draw(imgae))
    • 主な引数
      • imgae:Imageオブジェクト。このオブジェクトに書き込む

◇フォントの指定

  • 【構文】ImageFont.truetype( font=None, size=10, index=0, encoding='', layout_engine=None)
    -よく使う使い方(サンプル)
    【構文】ImageFont.truetype( "msgothic.ttc", 10)
    • 引数
      • font:ファイル名、ファイルライクなオブジェクト
      • size:フォントサイズ(ポイント)
      • index:フォントフェイス
      • encoding:デフォルトはUnicode フォントをロードするイメージ
    • フォントの指定方法
      • フォント名 tkinter.font.families()で確認できる(import tkinter.font)
      • フォントファイル(フォントファイルのパス) windowsの場合、c:\window\fontsフォルダにある

◇文字の書き込み

  • 【構文】ImageDrawオブジェクト.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align='left', direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False))
  • よく使う使い方(サンプル)
    【構文】draw_img.text((0, 0), "signetue", font=font, fill="gold", stroke_width=2, stroke_fill="black")
  • 引数
    • xy:文字の左上の座標のタプル
    • text:文字列(改行を含む場合はmultiline_text()を通す)
    • fill:文字色
    • anchor:アンカーアラインメント(デフォルトはtop,left)
    • font:ImageFontオブジェクト
  • 縁取り文字用の引数
    • stroke_width:縁取りの幅(px)
    • stroke_fill:縁取りの色
    • 例外エラー:フォントによって例外が出る(たぶんビットマップフォントの場合)

◎色の指定

こちらのサイト『Color Names — HTML Color Codes』のNAME列の英語名で指定できます
#FFFFFFという記述方法でも可能です

【コード】

            # 透かし
            if settings_img_conv.water_mark:
                kwargs = {"fill":settings_img_conv.wm_f_color,
                            "stroke_width":settings_img_conv.wm_stroke,
                            "stroke_fill":settings_img_conv.wm_b_color}
                img_conv.set_watermark_by_str(dst_img, settings_img_conv.water_mark,
                                                settings_img_conv.wm_size,
                                                settings_img_conv.wm_font_name,
                                                settings_img_conv.wm_padx,
                                                settings_img_conv.wm_pady, kwargs)

    def set_watermark_by_str(self, img:Image, water_mark:str, size:int, font_name:str, padx:int, pady:int, kwarges):
        """
        透かしを文字で入れる
        """
        sig = water_mark
        draw_img = ImageDraw.Draw(img)
        try:
            font = ImageFont.truetype(font_name, size) # フォント名とサイズ(px)
        except:
            print(f"【エラー】フォントファイル名を確認してください({font_name})")
            sys.exit()
        sig_size = font.getsize(sig)
        sig_nw = (img.size[0] - sig_size[0] - padx, img.size[1] - sig_size[1] - pady)
        try:
            draw_img.text(sig_nw, sig, font=font, **kwarges)
        except:
            print("【エラー】透かしが書けませんでした。フォントを変更してみてください。")

◆必要なパッケージ

  • pillow
    【インストール】pip install Pillow
    【インポート】 from PIL import Image, ImageDraw, ImageFont

◆全体のソース

全体のソースはこちらから取得できます。

◆バイナリ作成(pyinstaller)

バイナリ( exe ) ファイルは、pyinstaller で作成します。

  • 作成コマンド: pyinstaller -F --exclude-module settings_img_conv ファイル名

◇設定ファイルがある時のpyinstaller

設定ファイル(仮にsettings.py)をバイナリでも使えるようにします。
考え方の基本は、バイナリを作るソースから設定ファイルを除外して、exe と同じパスにある設定ファイルを読めるようにします。

  1. import settingsを記述しているソースの変更
    pyinstallerで作成したexeファイルがあるディレクトリから import できるように修正
    sys.executableが該当ディレクトリなので sys.path に追加
    例:sys.path.append( os.path.dirname( sys.executable))
      import settings
  2. settings.pyはexeに含めないようにpyinstallerを実行
    --exclude-moduleオプションを指定
    例:--exclude-module settings
    ※拡張子は指定しないので注意
  3. 配布はexeとsettings.pyを渡し、同じディレクトリにおいて起動

❖ライブラリとして使用する場合の仕様

  • 【クラス】ImageUI クラス
    画像変換クラスを使用して画像変換する操作クラス
  • コンストラクタ:ImageUI()
  • メソッド
    • convert_image_from_dialog_or_args(book:dict, paths:list=None): 画像変換する
      リサイズ、回転、反転、Exif除去、文字透かしを処理する
      対象の画像はパスで指定、複数指定可
      • 引数
        • book:設定項目の辞書
          • do_resize:サイズ変更するか(bool)
          • dest_path:保存用フォルダ 相対パス(読んだ画像のフォルダから見て)、絶対パスとも可(str)
          • width:新しい画像の幅(int)
          • height:新しい画像の高さ 幅が0の時に有効(int)
          • overwrite:新しい画像のファイル名(_w300などが付加される)(bool)
            True:上書き、False:末尾に数字を付加して別名保存
          • exifExif情報の書き込み True:書き込む、False:書かない(bool)
          • mirror:ミラー反転するか(bool)
          • rotate270:右回転するか(bool)
          • rotate90:左回転するか(bool)
          • is_water_mark:透かし文字付加するか(bool)
          • water_mark:透かしの文字(str)
          • wm_font_name:フォントファイル名(str)
          • wm_size:フォントサイズ(int)
          • wm_f_color:文字色(str)
          • wm_b_color:縁取りの色(str)
          • wm_stroke:縁取りの幅(px)(int)
          • wm_padx:右下からの隙間(横)(int)
          • wm_pady:右下からの隙間(縦)(int)
        • paths:画像のパスのリスト
      • 戻り値
        • set:変換後の画像のパスのセット
        • str:エラーメッセージ

  • 【クラス】ImageConversion クラス
    画像変換クラス
  • コンストラクタ:ImageConversion()
  • メソッド
    • scale_to_width(img, width)アスペクト比を固定して、指定した幅になるようリサイズする
      • 引数
        • img:元の画像
        • width:幅
      • 戻り値
        • 変換した画像
    • scale_to_height(img, height)アスペクト比を固定して、指定した高さになるようリサイズする
      • 引数
        • img:元の画像
        • height:高さ
      • 戻り値
        • 変換した画像
    • get_image(file_path):画像を読み込む
      • 引数
        • file_path:画像のパス
      • 戻り値
        • ロードした画像
    • save_image(img:Image, file_path:str, suffix:str, dst_path:str, overwrite:bool=True, **save_kwargs):画像の保存
      保存パスはdst_path\ベース名suffix.拡張子
      • 引数
        • img:保存する画像
        • file_path:元画像のパス
        • suffix:元のファイル名に付加するサフィックス
        • dst_path:保存先フォルダのパス
        • overwrite:上書きフラグ、False の場合、サフィックス_n を付加
        • **save_kwargs:Image クラスの save() メソッドで使える引数
      • 戻り値
        • 保存した画像のパス
    • make_dir(target_path:str, base_path:str):フォルダの作成
      指定フォルダが存在しない場合に作成する
      • 引数
        • target_path:指定されたフォルダ
        • base_path:基準にするフォルダ。相対パスの場合に使用
      • 戻り値
        • 作成したフォルダのパス
    • set_watermark_by_str(img:Image, water_mark:str, size:int, font_name:str, padx:int, pady:int, kwarges):透かしを文字で入れる
      • 引数
        • img:保存する画像
        • water_mark:透かし文字列
        • size:フォントサイズ
        • font_name:フォントファイル名
        • padx:画像の右下からの移動距離(水平方向)
        • pady:画像の右下からの移動距離(垂直方向)
        • kwarges:Draw クラスの text() メソッドで使える引数
      • 戻り値
        • エラーメッセージ

更新:2022-10-22

◆さいごに🦉

本記事で紹介しているアプリと同じようなアプリは他にも存在します。
紹介しませんが、アプリを作ってブログの記事を書くのに似たようなものがないか調べたら、当然のようにありました。

本記事のアプリは、機能的には他のものと変わりませんが、むしろ低い、ブロガー向けに特化していると思っています。

作成するに当たり、画像のサイズ変更は特に難しくありませんでした。

Exit 情報を書かないようにするのに PNGJPEG で挙動が違っていて分かるまでに時間が掛かりました。
仕様書もなかなかぴしっと書かれていないのですね。

文字の描画はフォントでてこずりました。
ただ描画するだけならすぐできたのですが、フォントを指定してもらうための方法を提供するのに迷いました。
フォントについては、後日、少し書かせていただきます。

あわせて読みたい - pillow 関連記事 📔 シンプル画像ビューアの作り方(マウスホイール対応)【Python】
📔 画像ビューアの作り方(Treeviewに画像と疑似チェックボックス)【Python】

◇免責事項

ご利用に際しては、『免責事項』をご確認ください。
お気づきの点がございましたら『お問い合わせ』からお問い合わせください。

◆参考

投稿: 、更新: