[Python]331 PyQt6 icnsファイル作成コード修正

[M1 Mac, Big Sur 11.6.5, Python 3.10.0]

[Python]329のコードを修正しました。以下の通りになります。

修正前のコードでも動作しますが、jpgファイルへの変換により透過部分が黒くなる上に、不可逆圧縮することで図形周辺画素の色データにズレが生じています。pngのままでも解像度を上げられるのでその方が良いでしょう。

前回330の記事でnumpyを使った透過部分黒色化対策方法を紹介しましたが、pngのままicnsファイルを作れるようになったためnumpyは不要になりました。Qt5への移植もスムーズに進みそうです。

from PIL import Image
import subprocess, os
from PyQt6.QtWidgets import QDialog,QPushButton,QLabel
from PyQt6.QtCore import Qt

class MakeIcns():
    def make(self, filepath0, window):
        img = Image.open(filepath0)
        print(str(img.size))
        if str(img.size)=="(2048, 2048)": # 2048*2048 dpi=72
            filepath1 = ".".join(filepath0.split(".")[:-1]) + "1.png" # 1024*1024 dpi=144
            filepath2 = ".".join(filepath0.split(".")[:-1]) + "2.png" # 512*512 dpi=72
            filedir = "/".join(filepath0.split("/")[:-1]) + "/" + (filepath0.split("/")[-1]).split(".")[-2] + ".iconset/"
            
            os.mkdir(filedir)

            img = Image.open(filepath0)
            
            # 1024*1024 dpi=144
            img_resize2 = img.resize((1024,1024))
            img_resize2.save(filepath1,dpi = (144, 144))
            
            # 512*512 dpi=72
            img_resize = img.resize((512,512))
            img_resize.save(filepath2,dpi = (72, 72))

            pixels = [32, 64, 256, 512, 1024]
            pixels2 = [16, 32, 128, 256, 512]
            filepaths = ['icon_16x16@2x.png','icon_32x32@2x.png','icon_128x128@2x.png','icon_256x256@2x.png','icon_512x512@2x.png']
            filepaths2 = ['icon_16x16.png','icon_32x32.png','icon_128x128.png','icon_256x256.png','icon_512x512.png']

            # dpi=144の各種pngファイル作成
            for pixel,file in zip(pixels,filepaths):
                img = Image.open(filepath1)
                img_resize = img.resize((pixel,pixel))
                img_resize.save(filedir + file, dpi = (144, 144))
                img.close()

            # dpi=72の各種pngファイル作成    
            for pixel,file in zip(pixels2,filepaths2):
                img = Image.open(filepath2)
                img_resize = img.resize((pixel,pixel))
                img_resize.save(filedir + file, dpi = (72, 72))
                img.close()

            # icnsファイル作成
            dir = "/".join(filepath0.split("/")[:-1])
            iconset = (filepath0.split("/")[-1]).split(".")[-2] + ".iconset"
            cmd = f'iconutil -c icns {iconset}'
            subprocess.run(cmd, cwd=dir,shell=True)
            
            os.remove(filepath1)
            os.remove(filepath2)
        else:
            MakeIcns.showDialog(self,window)
            
    def showDialog(self,window):
        dlg = QDialog(window)
        dlg.setFixedSize(250,100)
        label = QLabel('This file is invalid.\nPNG file[2048*2048,72dpi] required',dlg)
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        label.move(15,20)
        label.setStyleSheet('font-size:14px')
        btn = QPushButton("OK",dlg)
        btn.move(90,60)
        def action():
            dlg.close()
        btn.released.connect(action)
        dlg.setWindowTitle("Attention")
        dlg.exec()

[Python]330 pngファイル透過部黒色化への対策

[M1 Mac, Big Sur 11.6.5, Python 3.10.0]

pngファイルをjpgファイルに変換すると透過部が黒色になります。pngファイルの色情報から透明度が削除されたためです。

pngファイル RGBA(0, 0, 0, 0) 透明な黒[見た目は無色透明]

jpgファイル RGB(0, 0, 0) = RGBA(0, 0, 0, 255) 不透明な黒

対策としてpngファイルに再変換された画像の黒っぽい画素についてアルファ値を255から0に置き換えました。自分としては会心の出来ですが、コードにしてみるとあっさりしたものです。ちなみに画像はAdobe Illustratorで作ってみたアプリのアイコンです。フォントは自製しました。無料期間が終わったらどうするか悩みどころです。

jpgの場合は黒を白に変換するなどします。下記コード該当部分をnp.put(pixel,0,255)などに置き換えればできるはずです。

ただ上記の方法では元画像の絵の部分に黒が含まれていればそこも透過してしまいます。その場合は元画像の画素でアルファ値が0のものについて行列インデックスを変数化する、あるいは透過部分を絵にはない色に変換しておく、などの処置が必要です。

numpyと同じことがC++のライブラリでできないか、あるいはC++からnumpyを使えないか調査を進めています。

from PIL import Image
import numpy as np

# pngファイルの色情報を読み込む [red, green, blue, alpha]
img_array = np.array(Image.open('ImageInspector1.png'))

# 黒に近い画素のアルファ値を0にして透過させる
for row in img_array:
    for pixel in row:
        if pixel[0] <= 70 and pixel[1] <= 70 and pixel[2] <= 70 :
            np.put(pixel,3,0)
            
img = Image.fromarray(img_array)

file = "ImageInspector2.png"
img.save(file)
対策前
対策後

[C++] 12 Qt5 GUIアプリ作成 QMainWindow

[M1 Mac, Big Sur 11.6.5]

Qt5でデプロイの目処が立ったので、ようやくPyQt6からの移植に着手しました。

さすが古株のウィジェットツールキットだけあってかなり前からの記事が検索でヒットしました。ただ記事の日付やバージョンを明記していないため、陳腐化した情報に振り回されることもありました。

自分もそうですがせめて記事の日付やバージョン情報をきちんと書いておかないと後進の役に立たないです。

まずはQMainWindowの座標とサイズ、背景色を設定しました。

#include <QtWidgets/QApplication>
#include <QtWidgets/QLabel>
#include <QtWidgets/QWidget>
#include <QMainWindow>

int main(int argc, char *argv[]){
    QApplication a(argc, argv);

    QMainWindow *mainWin = new QMainWindow();
    mainWin->setGeometry(100,100,360,220);
    mainWin->setWindowTitle("IMAGE INSPECTOR");
    mainWin->setStyleSheet("QMainWindow {background: '#708090';}");
    
    mainWin->show();
    return a.exec();
}

[C++] 11 Qt5 appファイル作成&デプロイ qmake

[M1 Mac, Big Sur 11.6.5]

Qt6のCMakeがまだよく分からないので、Qt5にダウングレードしてデプロイしました。Hello WorldレベルであればApple Siliconでも問題は発生しませんでした。

Java(Oracle)はノンIDE派にも優しくてユーザーへの負担が少なくなるように配慮されている印象ですが、最新のQt6については高度化が著しい上に情報が少なく、専用IDEであるQt Creatorを使わないと厳しい感じがします。いずれノンIDEで挑戦したいです。

手順は以下の通りです。

1.Qtの公式サイトに登録し、Qt5.15.2(27.2GB)をダウンロード&インストールする。

2.Qtのバイナリがあるところへパスを通す。今回は優先順位を最上位にしました。

export PATH=[HOME]/Qt/5.15.2/clang_64/bin:$PATH

3.プロジェクトディレクトリをカレントディレクトリにして、以下コマンドでMakefileとproファイルを作成する。

qmake -project && qmake

4.アイコンを設定するため、proファイルに以下内容を追記する。

# Qt5以降用
QT+=widgets

# アイコン設定
ICON = images/ImageInspector.icns
RESOURCE_FILES.files = $$ICON
RESOURCE_FILES.path = Contents/Resources
QMAKE_BUNDLE_DATA += RESOURCE_FILES

5.makeコマンドでappファイルを作成する。

6.以下コマンドでアプリをデプロイする。
macdeployqtコマンドへのパスは通っているはずなのにエラーになり、仕方なく絶対パスを使いました。サイズはHello Worldでも40MBになりました。PyQt6よりもかなり小さくなったものの、まだ大きいです。

[HOME]/Qt/5.15.2/clang_64/bin/macdeployqt ImageInspector.app/

[C++] 10 Qt6 CMakeによるビルド

[M1 Mac, Big Sur 11.6.5]

リリース用appファイルの作成方法を調べていくうちに、Qt6ではqmakeではなくCmakeというビルド自動化ソフトの使用が推奨されていることが判明しました。

せっかくMakefileを使えるようになったのにまた難物が登場しました。

とりあえず実行ファイルを作成するところまでできたので記録しておきます。

1.CMakeを公式サイトからダウンロードしてインストールする。CMakeはGUIソフトですが、Contents内にあるバイナリを直接使います。

2.CMakeバイナリのあるところにパスを通す。

export PATH=$PATH:/Applications/CMake.app/Contents/bin

3.CMakeLists.txtを作成しプロジェクト直下に置く。

cmake_minimum_required(VERSION 3.16)

project(ImageInspector VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)

find_package(Qt6 REQUIRED COMPONENTS Widgets)

add_executable(ImageInspector
    ImageInspector.cpp
)

target_link_libraries(ImageInspector PRIVATE Qt6::Widgets)

4.プロジェクト内にbuildディレクトリを作成し、カレントディレクトリをそこに移す。

5.以下コマンドでMakefileを作成する。

cmake ..

6.makeコマンドで実行ファイルを作成する。

[C++] 09 Qt6のappファイル作成 qmake

[M1 Mac, Big Sur 11.6.5]

Qt6アプリのappファイルをテスト作成しました。複雑なアプリは作らないので専用IDEのQt Creatorは使わないつもりです。

基本的にネット情報を参考に進めました。方法は以下の通りです。デフォルトでアイコンの設定がないのは意外でした。

1. HomebrewでQt6をインストールし、バイナリのあるところにパスを通す。

export PATH=$PATH:/opt/homebrew/Cellar/qt/6.2.3_1/bin

2.プロジェクトを作成する。icnsファイルはimagesディレクトリに置く。

3.プロジェクトディレクトリをカレントディレクトリにして、以下コマンドでMakefileとproファイルを作成する。

qmake -project && qmake

4.アイコンを設定するため、proファイルに以下内容を追記する。

# Qt6用
QT+=widgets

# アイコン設定
ICON = images/ImageInspector.icns
RESOURCE_FILES.files = $$ICON
RESOURCE_FILES.path = Contents/Resources
QMAKE_BUNDLE_DATA += RESOURCE_FILES

5.makeコマンドでappファイルを作成する。

6.appファイル内Contents直下のInfo.plistに以下内容を追記する。

<key>CFBundleIconFile</key>
<string>ImageInspector.icns</string>

[Python]329 PyQt6 ボタン動作の実装

[M1 Mac, Big Sur 11.6.5, Python 3.10.0]

前回のコードにボタン動作を追加しPyQt6アプリを完成させました。icnsファイル作成箇所はクラスファイルとして別にしました。このアプリではドラッグ&ドロップでファイルパスを取得することもできます。

appファイルにすると260MBの巨大サイズになりました。さくっと作れますがやはりファイルの大きさが問題です。

アプリはとりあえず自サイトにアップしました。ここからC++の本家Qtに移植の予定です。

PyQt6はこれまで見てきたウィジェットツールキットの中では最も扱いやすかったです。並べると、PyQt6 > Swing >>> Tkinter >>FLTK といったところでしょうか。上位2つは企業開発であり、商品としてそれなりに洗練されているということでしょう。

FLTKは外観・文法・ウィジェットの名称など、クセが強すぎて開発意欲が低いままです。ラジオボタンがFl_Round_Button、ラベルがFl_Boxなどなどネーミングがピンとこなくて困っています。

2022/03/25追記
pngからjpgに変換しなくても解像度を上げられましたので後日コードをアップします。

https://www.tampiweb.site/?page_id=453
import sys
from PyQt6.QtWidgets import QLabel,QWidget,QApplication,QTextEdit,QLineEdit,QPushButton,QButtonGroup,QRadioButton
from PyQt6.QtCore import Qt
from pathlib import Path
from PIL import Image
import makeIcns

class ImageInspector(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("IMAGE INSPECTOR")
        self.setGeometry(100,100,360,220)
        self.setStyleSheet('background-color: #708090')
        self.setAcceptDrops(True)
        
        file = QLabel('File',self)
        file.setGeometry(15,15,26,16)
        file.setAlignment(Qt.AlignmentFlag.AlignCenter)
        
        self.input= QLineEdit('',self)
        self.input.setGeometry(50,10,220,25)
        self.input.setAcceptDrops(True)
        
        execution = QPushButton('実行',self)
        execution.setGeometry(290,10,50,30)
        execution.released.connect(self.execute)
        
        clear = QPushButton('クリア',self)
        clear.setGeometry(290,50,50,30)
        clear.released.connect(self.clear)
        
        self.rbtns = QButtonGroup()
        self.inspect = QRadioButton("Inspect",self)
        self.inspect.setGeometry(50,40,90,20)
        self.inspect.setChecked(True)
        self.rbtns.addButton(self.inspect)
        
        self.resize_img = QRadioButton("Resize",self)
        self.resize_img.setGeometry(50,65,90,20)
        self.rbtns.addButton(self.resize_img)
        
        width_label = QLabel('W',self)
        width_label.setGeometry(135,70,15,10)
        width_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        width_label.setStyleSheet('font-size:10px')
        
        self.width = QLineEdit('',self)
        self.width.setGeometry(155,65,45,20)
        
        height_label = QLabel('H',self)
        height_label.setGeometry(205,70,15,10)
        height_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        height_label.setStyleSheet('font-size:10px')
        
        self.height = QLineEdit('',self)
        self.height.setGeometry(220,65,45,20)
        
        self.icns = QRadioButton("icns作成",self)
        self.icns.setGeometry(50,90,90,20)
        self.icns.setToolTip('PNG file[2048*2048,72px] required')  
        self.rbtns.addButton(self.icns)
        
        self.output = QTextEdit('',self)
        self.output.setGeometry(50,115,240,100)
        self.output.setAcceptDrops(False)
        
    def dragEnterEvent(self, e):
        if(e.mimeData().hasUrls()):
            e.accept()

    def dropEvent(self, e):
        urls = e.mimeData().urls()
        path = Path(urls[0].toLocalFile())
        print(f"Drop_path {path}")
        self.input.setText(str(path))
        
    def execute(self):
        image_path = self.input.text()
        print(f"Exe_path {image_path}")
        img = Image.open(image_path)
        
        if self.inspect.isChecked():
            self.output.append("Size " + str(img.size) + "\n"+ "dpi " + str(img.info.get('dpi')))
        elif self.resize_img.isChecked():
            width = int(self.width.text())
            height = int(self.height.text())
            resize_img= img.resize((width,height))
            resize_img_path = ".".join(self.input.text().split(".")[:-1]) + "_resized." + (self.input.text()).split(".")[-1]
            print(f"Resize_path {resize_img_path}")
            resize_img.save(resize_img_path)
        else: # icns作成
            makeIcns.MakeIcns.make(None,image_path,self)
        
    def clear(self):
        self.input.clear()
from PIL import Image
import subprocess, os
from PyQt6.QtWidgets import QDialog,QPushButton,QLabel
from PyQt6.QtCore import Qt

class MakeIcns():
    def make(self, filepath0, window):
        img = Image.open(filepath0)
        print(str(img.size))
        if str(img.size)=="(2048, 2048)": # 2048*2048
            filepath0_jpg = ".".join(filepath0.split(".")[:-1]) + ".jpg" # 1024*1024 dpi=144
            filepath = ".".join(filepath0.split(".")[:-1]) + "1.png" # 1024*1024 dpi=144
            filepath2 = ".".join(filepath0.split(".")[:-1]) + "2.png" # 512*512 dpi=72
            filedir = "/".join(filepath0.split("/")[:-1]) + "/" + (filepath0.split("/")[-1]).split(".")[-2] + ".iconset/"
            
            print(f"filepath0_jpg {filepath0_jpg}")
            print(f"filepath {filepath}")
            print(f"filepath2 {filepath2}")
            print(f"filedir {filedir}")
            
            os.mkdir(filedir)

            img1 = Image.open(filepath0).convert("RGB")
            # 512*512 dpi=72
            img1_resize = img1.resize((512,512))
            img1_resize.save(filepath2)

            # dpi=144にするため一旦jpgへ変換
            img1_resize2 = img1.resize((1024,1024))
            img1_resize2.save(filepath0_jpg,dpi=(144,144))
            # jpgをpngへ変換
            img3 = Image.open(filepath0_jpg).convert("RGBA")
            img3.save(filepath,dpi=(144,144))

            pixels = [32, 64, 256, 512, 1024]
            pixels2 = [16, 32, 128, 256, 512]
            filepaths = ['icon_16x16@2x.png','icon_32x32@2x.png','icon_128x128@2x.png','icon_256x256@2x.png','icon_512x512@2x.png']
            filepaths2 = ['icon_16x16.png','icon_32x32.png','icon_128x128.png','icon_256x256.png','icon_512x512.png']

            # dpi=144の各種pngファイル作成
            for pixel,file in zip(pixels,filepaths):
                img = Image.open(filepath)
                img_resize = img.resize((pixel,pixel))
                img_resize.save(filedir + file)
                img.close()

            # dpi=72の各種pngファイル作成    
            for pixel,file in zip(pixels2,filepaths2):
                img = Image.open(filepath2)
                img_resize = img.resize((pixel,pixel))
                img_resize.save(filedir + file)
                img.close()

            # icnsファイル作成
            dir = "/".join(filepath0.split("/")[:-1])
            iconset = (filepath0.split("/")[-1]).split(".")[-2] + ".iconset"
            cmd = f'iconutil -c icns {iconset}'
            subprocess.run(cmd, cwd=dir,shell=True)
            
            os.remove(filepath0_jpg)
            os.remove(filepath)
            os.remove(filepath2)
        else:
            MakeIcns.showDialog(self,window)
            
    def showDialog(self,window):
        dlg = QDialog(window)
        dlg.setFixedSize(250,100)
        label = QLabel('This file is invalid.\nPNG file[2048*2048,72dpi] required',dlg)
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        label.move(15,20)
        label.setStyleSheet('font-size:14px')
        btn = QPushButton("OK",dlg)
        btn.move(90,60)
        def action():
            dlg.close()
        btn.released.connect(action)
        dlg.setWindowTitle("Attention")
        dlg.exec()

[Python]328 PyQt6 UIデザインの実装

[M1 Mac, Big Sur 11.6.5, Python 3.10.0]

PyQt6ツールのUIデザインを実装しました。特に引っかかるところはなかったです。

あらかじめAdobeXDなどで設計しておくと座標やサイズはコピーするだけでいいので楽です。

import sys
from PyQt6.QtWidgets import QLabel,QWidget,QApplication,QTextEdit,QLineEdit,QPushButton,QButtonGroup,QRadioButton
from PyQt6.QtCore import Qt

class ImageInspector(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("IMAGE INSPECTOR")
        self.setGeometry(100,100,360,220)
        self.setStyleSheet('background-color: #708090')
        
        file = QLabel('File',self)
        file.setGeometry(15,15,26,16)
        file.setAlignment(Qt.AlignmentFlag.AlignCenter)
        
        input = QLineEdit('',self)
        input.setGeometry(50,10,220,25)
        
        execution = QPushButton('実行',self)
        execution.setGeometry(290,10,50,30)
        
        clear = QPushButton('クリア',self)
        clear.setGeometry(290,50,50,30)
        
        self.rbtns = QButtonGroup()
        inspect = QRadioButton("Inspect",self)
        inspect.setGeometry(50,40,90,20)
        self.rbtns.addButton(inspect)
        
        resize = QRadioButton("Resize",self)
        resize.setGeometry(50,65,90,20)
        self.rbtns.addButton(resize)
        
        width_label = QLabel('W',self)
        width_label.setGeometry(135,70,15,10)
        width_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        width_label.setStyleSheet('font-size:10px')
        
        width = QLineEdit('',self)
        width.setGeometry(155,65,45,20)
        
        height_label = QLabel('H',self)
        height_label.setGeometry(205,70,15,10)
        height_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        height_label.setStyleSheet('font-size:10px')
        
        height = QLineEdit('',self)
        height.setGeometry(220,65,45,20)
        
        icns = QRadioButton("icns作成",self)
        icns.setGeometry(50,90,90,20)
        self.rbtns.addButton(icns)
        
        output = QTextEdit('',self)
        output.setGeometry(50,115,220,100)

app = QApplication(sys.argv)
window = ImageInspector()
window.show()
app.exec()

[Python]327 appファイルのApple公証

[M1 Mac, Big Sur 11.6.5, Python 3.10.0]

py2appにより作成したappファイルをAppleに提出しApple公証をパスさせました。

appファイルはインタプリタやPyQtを丸ごとパッケージにしているので中にあるプログラムに署名する必要があります。今回は268個のファイルに署名しました。

署名対象ファイルはappファイル作成時にログ出力されるsignリストで確認できます。このリストを取り出して以下のコードで一括署名しました。

これでGUIのXCodeでは扱っていないJava, C++, PythonアプリのApple公証を成功させたことになります。

次の目標はC++とPythonアプリのMicrosoft Storeアプリ登録(コマンドでappxファイル作成)ですが、ハードルはかなり高そうです。特に多言語対応に不安があります。xmlが使えるのであれば問題ないのですが。

import subprocess

signs = [ログ出力から取得したファイルパス + Contents/MacOS内実行ファイル]

for sign in signs:
    cmd = f'codesign --force --verify --verbose --sign [mac-signing-key-user-name] {sign} --deep --options runtime --entitlements entitlements.plist --timestamp'
    subprocess.run(cmd,shell=True)

[C++] 08 FLTKアプリのApple公証

[M1 Mac, Big Sur 11.6.5, FLTK 1.3.8]

FLTKアプリのApple公証はJavaアプリとは異なりappファイルへの署名だけでパスしました。署名時の–options runtimeオプションは必須でした。

Javaアプリではappファイルの中にある実行ファイルや動的ライブラリにも署名が必要だったのでこれはありがたいです。

# 署名コマンド
codesign --force --verify --verbose \
    --sign [mac-signing-key-user-name] \
    "test.app" \
    --deep \
    --options runtime \
    --entitlements entitlements.plist \
    --timestamp

# 提出コマンド
xcrun altool --notarize-app -t osx -f "test.zip" \
    --primary-bundle-id [ID] \
    -u [登録メールアドレス] \
    -p [パスワード]