Using User-Provided Images for Secondary Tiles

Often when you are creating a secondary tile in Windows 8, it will be based on images coming from the internet.  However a requirement of secondary tile images are that they need to be stored locally. I initially had some problems getting this working right and the streams closed correctly for this to work, so here’s the code for other to use and save the hazzle:

public async static Task CreateSecondaryTileFromWebImage(
    string tileId, Uri imageUri, string shortName, string displayName,
    string arguments, Rect selection)
{
    //Download image to LocalFolder and use the tileId as the identifier
    string filename = string.Format("{0}.png", tileId);
    HttpClient httpClient = new HttpClient();
    var response = await httpClient.GetAsync(imageUri);
    var imageFile = await ApplicationData.Current.LocalFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);
    using (var fs = await imageFile.OpenAsync(FileAccessMode.ReadWrite))
    {
        using (var outStream = fs.GetOutputStreamAt(0))
        {
            DataWriter writer = new DataWriter(outStream);
            writer.WriteBytes(await response.Content.ReadAsByteArrayAsync());
            await writer.StoreAsync();
            writer.DetachStream();
            await outStream.FlushAsync();
        }
    }
    //Create tile
    Uri image = new Uri(string.Format("ms-appdata:///local/{0}", filename));
    SecondaryTile secondaryTile = new SecondaryTile(tileId, shortName, displayName, arguments, TileOptions.ShowNameOnLogo, image);
    secondaryTile.ForegroundText = ForegroundText.Light;
    await secondaryTile.RequestCreateForSelectionAsync(selection, Windows.UI.Popups.Placement.Above);
}

Often this is enough, but there is still a small problem. What if the image is very light, and the text you display on top of it is white, thus drowning in the background image? You could set the tile text to black, but then what happens if the image is dark? And if it has a lot of texture in it, the text still gets very unreadable. Since you might not know up front what the image looks like, whether it’s dark or bright, or textures, we will need a way to ensure the text will still look good on top of the image.

image

Unfortunately full WriteableBitmap support in WinRT isn’t there to help us out modifying the image, but we do get low-level access to the pixel buffer of the image, so we could fairly simple darken or brighten the bottom a bit to ensure the image looks good as a backdrop for the text.

I wrote a little utility that loads the image, gradually darkens the bottom 40% of the image before saving it back. I’ve found that doing a slight graduated darkening on photos isn’t too noticeably, while making the text in front of it much more readable. So with out further ado, here’s my simple image pixelbuffer modifier:

private async static Task DarkenImageBottom(string filename, string outfilename)
{
    var file = await ApplicationData.Current.LocalFolder.GetFileAsync(filename);
    BitmapDecoder decoder = null;
    byte[] sourcePixels = null;
    using (IRandomAccessStream fileStream = await file.OpenReadAsync())
    {
        decoder = await BitmapDecoder.CreateAsync(fileStream);
        // Scale image to appropriate size 
        BitmapTransform transform = new BitmapTransform();
        PixelDataProvider pixelData = await decoder.GetPixelDataAsync(
            BitmapPixelFormat.Bgra8,
            BitmapAlphaMode.Straight,
            transform,
            ExifOrientationMode.IgnoreExifOrientation, // This sample ignores Exif orientation 
            ColorManagementMode.DoNotColorManage
        );
        // An array containing the decoded image data, which could be modified before being displayed 
        sourcePixels = pixelData.DetachPixelData();
        fileStream.Dispose();
    }
    if (decoder != null && sourcePixels != null)
    {
        for (uint col = 0; col < decoder.PixelWidth; col++)
        {
            for (uint row = (uint)(decoder.PixelHeight * .6); row < decoder.PixelHeight; row++)
            {
                uint idx = (row * decoder.PixelWidth + col) * 4;
                if (decoder.BitmapPixelFormat == BitmapPixelFormat.Bgra8 ||
                    decoder.BitmapPixelFormat == BitmapPixelFormat.Rgba8)
                {
                    var frac = 1 - Math.Sin(((row / (double)decoder.PixelHeight) - .6) * (1 / .4));
                    byte b = sourcePixels[idx];
                    byte g = sourcePixels[idx + 1];
                    byte r = sourcePixels[idx + 2];
                    sourcePixels[idx] = (byte)(b * frac);
                    sourcePixels[idx + 1] = (byte)(g * frac);
                    sourcePixels[idx + 2] = (byte)(r * frac);
                }
            }
        }

        var file2 = await ApplicationData.Current.LocalFolder.CreateFileAsync(outfilename, CreationCollisionOption.ReplaceExisting);

        var str = await file2.OpenAsync(Windows.Storage.FileAccessMode.ReadWrite);
        BitmapEncoder enc = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, str);
        enc.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Ignore, decoder.PixelWidth, decoder.PixelHeight,
            decoder.DpiX, decoder.DpiY, sourcePixels);
        await enc.FlushAsync();
        str.Dispose();
    }
}

So we can call this utility prior to creating the secondary tile request. Compare the before(left) and after (right) here:

imageimage

The image difference is barely noticeable, but the text is much more readable.

You can download the tile utility class and sample app here.

Comments (6) -

  • Pretty clever work around for the text color issue. Me likey.
  • I can haz code: Well yes you can haz the code. It's right there above in plain text. Or use the download link at the end of the article to grab the sample app.
  • First, thanks a LOT for this!

    Second, the darken image doesn't seem to work with "wide logo" image files.  Is there something to be tweaked with that method?

    Thanks again!
  • First, thanks for this!  Great work!

    How do I tweak the darken image method to increase the amount of the photo being shaded from 40% and how do I control how light or dark I would like to make it?

    Thanks again!
  • Richard: The devil is in the numbers Smile Change the .6 and .4 numbers in the faction calculation (the two numbers have to add up to 1). So if you want 50%, make it .5 and .5. 70% make it .3 and .7 etc
  • Richard: Regarding wide images: Although I actually didn't test that, the code should be invariant to image size (but I might be wrong). What problem are you seeing? Also note that using a very large image as a tile image doesn't work - it has to be fairly small to work.

Pingbacks and trackbacks (1)+

Add comment