tmytのらくがき

個人の日記レベルです

透過PNGでライブタイルを更新したかった

追記ここから

PNGにするところのBitmapAlphaModeをPremultipliedにするとめんどくさいことしなくても一発で保存できました。こんな感じ。

encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied, (uint)bmp.PixelWidth, (uint)bmp.PixelHeight, 96, 96, pixels);

というわけでこの記事はいらない子になったのでここから下は読まなくて大丈夫です。RenderTargetBitmapで作ったα付き画像をPNGで保存するときにはBitmapAlphaModeをPremultipliedにしましょう。というのがまとめです。

ここまで。


Mimosa作ってて困ったんですけど、ほんとにこれこういう実装でいいんですか?っていうエントリです。

WinRTだとRenderTargetBitmapというのに、UIElementを投げつけるといい感じに描画してくれます。でもって、XamlRenderingBackgroundTaskから派生したバックグラウンドタスクだと、UI要素を適当にnewしてRenderAsync呼ぶといい感じに描画して、バックグラウンドでタイル画像作ったりできます。

というのが前置き。自由にデザインしたタイルでライブタイルを更新するときだいたいこんなコードを書きますよね。

まずこういうUserControlを用意して

<UserControl
    x:Class="Background.Controls.TileTemplate"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="175"
    d:DesignWidth="175">

    <Grid Width="175" Height="175">
        <Grid.Background>
            <SolidColorBrush Opacity="0.5" Color="{ThemeResource SystemColorControlAccentColor}" 
                             x:Name="BackgroundBrush"/>
        </Grid.Background>
        <TextBlock Text="Hello!" Margin="11.666" FontSize="32" />
    </Grid>
</UserControl>

こんな感じにバックグラウンドタスクで更新します。

namespace Background
{
    public sealed class TileUpdateTask : XamlRenderingBackgroundTask
    {
        protected override async void OnRun(IBackgroundTaskInstance taskInstance)
        {
            var deferral = taskInstance.GetDeferral();

            // Update all tiles
            var tiles = await SecondaryTile.FindAllAsync();
            foreach (var tile in tiles)
            {
                await UpdateTile(tile.TileId);
            }

            // notify complete
            deferral.Complete();
        }

        private static async Task UpdateTile(string tileId)
        {
            // タイル画像を作る
            var template = new TileTemplate();
            var bmp = new RenderTargetBitmap();
            await bmp.RenderAsync(template, 175, 175);
            // PNGで保存
            var pixels = await bmp.GetPixelsAsync();
            var folder = ApplicationData.Current.LocalFolder;
            var file = await folder.CreateFileAsync(tileId + ".png", CreationCollisionOption.ReplaceExisting);
            using (var writeStream = await file.OpenAsync(FileAccessMode.ReadWrite))
            {
                var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, writeStream);
                encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Straight, (uint)bmp.PixelWidth,
                    (uint)bmp.PixelHeight, 96, 96, pixels);
                await encoder.FlushAsync();
            }
            // 作ったPNGでタイルを更新
            var builder = TileContentFactory.CreateTileSquare150x150Image();
            builder.Branding = TileBranding.None;
            builder.Image.Src = "ms-appdata:///local/" + filename;
            builder.Branding = TileBranding.None;
            var manager = TileUpdateManager.CreateTileUpdaterForSecondaryTile(tileId);
            var notification = new TileNotification(builder.GetXml());
            manager.Clear();
            manager.Update(notification);
        }
    }
}

ここで、UserControlにα値が0xFFまたは0x00でないピクセルがあるとなぜかRenderTargetBitmapで描画したときに期待していない結果となります。それがこの図。

f:id:tmyt:20150712145231p:plain

そして、背景を半透明にせずに描画したものがこちら。

f:id:tmyt:20150712145421p:plain

ちなみに期待した結果はこちら。

f:id:tmyt:20150712145859p:plain

1枚目の画像の濃いブルーで描画されているところの値を確認してみると、RGBA=(1f,32,80,80)となっています。透明度を追加する前の2枚目の画像はRGBA=(3e,65,ff,ff) であるので、透明度だけでなくRGB値も変わってしまっていることがわかります。

半透明画素の計算はR = R1 * a + R2 * (1 - a) で求まるので計算すると、透明度を除いた色と、RGB=(0,0,0)をピクセルごとの透明度で合成したものに、さらに透明度を付加したものが描画されていることがわかります。

これを期待した結果にするにはこんな感じにします。

// 色がおかしくなるけど透明度のために描画する
var maskBmp = new RenderTargetBitmap();
template.IsTransparent = true;
await maskBmp.RenderAsync(template, 175, 175);
// 透明度を削除して正しい色で描画する
var baseBmp = new RenderTargetBitmap();
template.IsTransparent = false;
await baseBmp.RenderAsync(template, 175, 175);
// 正しい色の画像に、正しい透明度を反映させる
var baseBytes = (await baseBmp.GetPixelsAsync()).ToArray();
var maskBytes = (await maskBmp.GetPixelsAsync()).ToArray();
for (var i = 0; i < baseBytes.Length / 4; ++i)
{
    baseBytes[i * 4 + 3] = maskBytes[i * 4 + 3];
}

こうすると期待した透明度できれいに描画できます。がめんどくさいので正しい方法があったら教えてください…