2018年11月3日土曜日

QML での HiDPI 画像保存と Image への読み込み方法


表示状態のビジュアルアイテムに対して Item.grabToImage() を呼び出して表示内容を画像ファイルに保存できます。二つの仮引数があり第一仮引数にはコールバック関数を以下のように指定して画像ファイルを保存します
visualItem.grabToImage(function(result) {
                           if (!result.saveToFile("imageFileName.png")) {
                               // 保存できなかったときの処理
                           }                                         
                       });
コールバック関数の第一仮引数の result の型は ItemGrabResult で、そのオブジェクトの saveToFile() 関数を呼び出してファイルを保存します。保存に成功すれば true が返り、失敗すれば false が返ります。

コールバック関数の第二仮引数には保存する画像ファイルのサイズを指定します。省略するとビジュアルオブジェクトのサイズが使われます。HiDPI の場合には描画内容を縮小して画像ファイルに保存されるため、細い線が欠けたりテキストが粗く表示されてしまいます。コールバック関数の第二仮引数にビジュアルオブジェクトのサイズを Screen.devicePixelRatio 倍したサイズを指定すると HiDPI での表示内容がそのまま画像ファイルに保存されます。Screen.devicePixelRatio の値が 2 の場合には以下のようになります。
visualItem.grabToImage(function(result) {
                           if (!result.saveToFile("imageFIleName@2x.png") {
                               // 保存できなかったときの処理
                           }
                       }, Qt.size(2 * visualItem.width, 2 * visualItem.height));
保存ファイル名に @2x を付加しているのは、以降の説明のように Image で HiDPI 表示するためです。

ビジュアルアイテムの表示内容を HiDPI でファイルに保存するサンプルコードです。
grabtoimage.qml:
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Window 2.12
import QtQuick.Layouts 1.12

ApplicationWindow {
    visible: true

    minimumWidth: 300
    minimumHeight: 300

    Rectangle {
        id: yellowRect

        width: 200
        height: 200

        anchors.centerIn: parent

        color: "yellow"
        border {
            color: "grey"
            width: 1/Screen.devicePixelRatio
            pixelAligned: Screen.devicePixelRatio > 1 ? false : true
        }

        Text {
            id: messageText

            anchors.centerIn: parent

            text: "Yellow"
            font.pointSize: 20
        }

    }

    MouseArea {
        anchors.fill: yellowRect

        onClicked: {
            if (Screen.devicePixelRatio > 1) {
                yellowRect.grabToImage(function(result) {
                                           result.saveToFile(`YellowRectangle@${Screen.devicePixelRatio}x.png`);
                                       }, Qt.size(Screen.devicePixelRatio * yellowRect.width, Screen.devicePixelRatio * yellowRect.height));
            } else {
                yellowRect.grabToImage(function(result) {
                                           result.saveToFile("YellowRectangle.png");
                                       });
            }
        }
    }
}
Rectangle の Qt のドキュメントに記載されているプロパティーの説明には間違いがあり、実際のプロパティーは以下のようになっています。
Rectangle QML タイプのプロパティー
    color: color = "#ffffffff"
    gradient: Gradient = null
    border: QQuickPen(this) readonly constant
        width: real = 1.0
        color: color = "#ff000000"
        pixelAligned: bool = true
        radius: real = 0.0
Rectangle QML タイプのドキュメントには border.width の型が int と記載されていますが実際の型は real です。Rectangle には浮動小数点数値が渡って来て、小数点以下を四捨五入された値を使っています。例えば、0.5 を指定すると 1.0 が縁の細さになります。もしこのプロパティーの型が int ならば小数点以下を切り捨てた値が Rectangle に渡されますが、そうはならずに前述のように処理されます。border.pixelAligned は Rectangle QML タイプのキュメントに記載されていないプロパティーです。border.pixelAligned を false に設定すると Screen.devicePixelRatio の値が 2 の HiDPI ディスプレイでは border.width に 0.5 の場合に、0.5 ピクセルの細さで縁が描画されます。

HiDPI での確認のために Text も表示しています。

Image QML タイプで HiDPI 画像を表示するには以下のように画像ファイルを用意します。前述のサンプルコードでもこのファイル名命名規則に従ってます。
imageFileName.png     Screen.devicePixelRatio が 1 用、通常サイズ
imageFileName@2x.png             〃              2 用、サイズ 2 倍 
imageFileName@3x.png             〃              3 用、サイズ 3 倍
@2x の画像ファイルは表示サイズの二倍サイズで、@3x の画像ファイルは表示サイズの三倍サイズで画像ファイルを作成しておきます。Image の source プロパティーに通常サイズの画像ファイル名を指定しておけば、自動的に実行時の Screen.devicePixelRatio に対応する画像ファイルを読み込み、HiDPI で表示します。
Image {
    source: "YellowRectangle.png"
}
以下のように環境変数を設定すると HiDPI 自動ファイル切り替えを行わないようにできます。スクリーンが HiDPI の場合でも通常サイズの画像が使われるので当然ながら表示は粗くなります。
QT_HIGHDPI_DISABLE_2X_IMAGE_LOADING=1
Qt の Scalability などのドキュメントに、このファイル名命名規則は、iOS と macOS での方法だと記載されています。実際には Image が判断して処理をしているので、Linux や Android、Windows でも同様に扱われます。また、ファイルシステム上のファイルではなく、Qt のリソースに入れた画像ファイルでも同様に動作します。

いくつか制約があります。
  • ビジュアルアイテムが表示状態の時に Item.grabToImage() で画像保存できます。
  • ApplicationWindow の contentItem に設定されているのは Item を継承したビジュアルタイプですが Window が直接そのインスタンスを生成し、QQmlEngine と QQmlContext が紐づけられていないため Item.grabToImage() を使用できません。ApplicationWindow や Window のウィンドウ領域全体に対して Item.grabToImage() を使うにはルートアイテムを contentItem と同じ大きさにします。 
  • QQuickImageProvider で生成した画像は Image で HiDPI 表示できません。Image が QQuickImageProvider の場合には HiDPI 表示できるようにまだ実装されていないためです。
  • ネットワークアクセスした画像は Image で HiDPI 表示できません。