Windows Phone 8 - Generating Lockscreen Image in the Background
Asked Answered
M

3

6

I'm trying to create a Windows Phone 8 app (update of my currently published "The Quote") that uses the new Windows Phone 8 Live Lockscreen API. I basically want to choose randomly a image background from the app package and place a textblock on it with a random quote to create the lockscreen image. How can I accomplish that in the background periodic task? There is definitely a way to do it (many current apps including different weather and news apps create live lockscreen locally in the background), but I just don't seem to be able to find out how and so far no internet search got me anything useful.

Any help is very appreciated!

Thank you very much!

EDIT:

I was able to find a way to create a UserControl with my content and take a screenshot of it this way:

var bmp = new WriteableBitmap(768, 1280);
bmp.Render(LayoutRoot, null);

String tempJPEG = "TempJPEG.jpg";

var myStore = IsolatedStorageFile.GetUserStoreForApplication();
if (myStore.FileExists(tempJPEG))
{
    myStore.DeleteFile(tempJPEG);
}
IsolatedStorageFileStream myFileStream = myStore.CreateFile(tempJPEG);

WriteableBitmap wb = new WriteableBitmap(bmp);

wb.SaveJpeg(myFileStream, wb.PixelWidth, wb.PixelHeight, 0, 100);
myFileStream.Close();

This approach got me three different problems:

  1. If I didn't set the WriteableBitmap's size in constructor, it chose it incorrectly and the lockscreen was useless.

  2. If I run the code above, it throws an OutOfMemory error

  3. In the 1 case, there was also a problém with the Control's background (went black, even though I have set the main Grid's Background brush to ImageBrush linking to a local file from the main Appx package.

Is this completely wrong? Is there a better (working) way?

Thank you all so much, I appreciate your help.

Motherhood answered 24/12, 2012 at 4:22 Comment(0)
B
4

You are most likely running into Memory cap limit in your background agent, which is 11 MB on WP8. I would recommend you to render your images on server/Azure and just download it in Background agent, save it into phone and display it on lockscreen, or maybe to use Resource Intesive Task for the rendering?
I use tile rendering in one of my apps and I ran into memory cap when I tried to render just 2 tile images of size 336x336 + 159x159px, so you can imagine rendering 768x1280 image could easily reach this cap as well.

Benkley answered 2/1, 2013 at 16:37 Comment(1)
hi i has tried this method. but i got other problem #20492066Nary
L
2

If I didn't set the WriteableBitmap's size in constructor, it chose it incorrectly and the lockscreen was useless.

Did you try to use Application.Current.Host.ActualHeight and ActualWidth as the size of your generated lock screen images? Essientially adapting the lock screen image to the size currently used by the OS? I'm pretty sure Application.Current might be null in the background, so you'll have to cache it in ApplicationSettings from the main app and use that information in your background agent.

If I run the code above, it throws an OutOfMemory error

Yeah, that's because you're using ImageQuality=100 in the SaveJpeg invocation. Remember, background agents running on WP8 have a memory limitation of 11MB of working set. Drop the ImageQuality down to 70-80 and you should be fine.

In the 1 case, there was also a problém with the Control's background (went black, even though I have set the main Grid's Background brush to ImageBrush linking to a local file from the main Appx package.

Images might take a bit longer to load. First, try calling WriteableBitmap.Invalidate() before saving it down to a file. If that doesn't work (becuase of the image being from a remote source) you'll have to wait until the BitmapImage.ImageOpened event of the image you're trying to capture.

Lancey answered 30/12, 2012 at 0:53 Comment(3)
The OutOfMemory exception occurs on the line before JPEG saving (WriteableBitmap wb = new WriteableBitmap(bmp);). How can I prevent it there?Motherhood
After a little bit of digging, I found out another unusual thing - when I create a brand new clean project and normally generate the image using the code I wrote in my first post, with the difference of not specifying the resolution in the constructor (creating new WriteableBitmap( LayoutRoot, null ) ), the image, that is generated has always 480x800 pixels, even though I'm testing the app on my Lumia 920 (768x1280).Motherhood
Now, when I use this changed code in background agent, I get a new exception - ArgumentException in SaveJpeg call.Motherhood
D
0

You can generate an image but it requires quite a bit of more work. As you pointed out - you can't just save the entire image. So I've changed saving logic to loop through the image and save 60 lines at a time. Notice that I call GC.Collect 3 times to go over all 3 GC generations and get rid of all memory allocations.

static IEnumerable<ExtendedImage> GetImageSegments(LockScreenTile tileControl)
{
    const int segmentMaxHight = 60;
    var aggregatePixelHeight = (int)tileControl.ActualHeight;
    tileControl.LayoutRoot.Height = aggregatePixelHeight;
    for (int row = 0; row < aggregatePixelHeight; row += segmentMaxHight)
    {
        tileControl.LayoutRoot.Margin = new Thickness(0, -row, 0, 0);
        tileControl.Height = Math.Min(segmentMaxHight, aggregatePixelHeight - row);
        yield return tileControl.ToExtendedImage();
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
 }

ExtendedImage is coming from ImageTools library. And once I get the enumerable back I call PNG compression (which I tweaked to persist using image fragments):

var segments = GetImageSegments(tileControl);
double pixelRatio = (template.PixelWidth) / tileControl.ActualWidth;
segments.SaveToPngImage(
    aggregatePixelHeight: (int)(savedActualHeight * pixelRatio),
    pixelWidth: template.PixelWidth,
    densityX: template.DensityX,
    densityY: template.DensityY, 
    directoryName: tileDirectoryName, 
    filePath: filePath);

Finally, SaveToPngImage is using PngEncoder from ImageTools, which I've modified:

    private void WriteDataChunks(IEnumerable<ExtendedImage> verticalImageSegments)
    {
        byte[] compressedBytes;
        MemoryStream underlyingMemoryStream = null;
        DeflaterOutputStream compressionStream = null;
        try
        {
            underlyingMemoryStream = new MemoryStream();
            compressionStream = new DeflaterOutputStream(underlyingMemoryStream);

            foreach (var verticalImageSegment in verticalImageSegments)
            {
                byte[] pixels = verticalImageSegment.Pixels;
                var data = new byte[verticalImageSegment.PixelWidth*verticalImageSegment.PixelHeight*4 + verticalImageSegment.PixelHeight];
                int rowLength = verticalImageSegment.PixelWidth*4 + 1;

                for (int y = 0; y < verticalImageSegment.PixelHeight; y++)
                {
                    byte compression = 0;
                    if (y > 0)
                    {
                        compression = 2;
                    }
                    data[y*rowLength] = compression;

                    for (int x = 0; x < verticalImageSegment.PixelWidth; x++)
                    {
                        // Calculate the offset for the new array.
                        int dataOffset = y*rowLength + x*4 + 1;

                        // Calculate the offset for the original pixel array.
                        int pixelOffset = (y*verticalImageSegment.PixelWidth + x)*4;

                        data[dataOffset + 0] = pixels[pixelOffset + 0];
                        data[dataOffset + 1] = pixels[pixelOffset + 1];
                        data[dataOffset + 2] = pixels[pixelOffset + 2];
                        data[dataOffset + 3] = pixels[pixelOffset + 3];

                        if (y > 0)
                        {
                            int lastOffset = ((y - 1)*verticalImageSegment.PixelWidth + x)*4;

                            data[dataOffset + 0] -= pixels[lastOffset + 0];
                            data[dataOffset + 1] -= pixels[lastOffset + 1];
                            data[dataOffset + 2] -= pixels[lastOffset + 2];
                            data[dataOffset + 3] -= pixels[lastOffset + 3];
                        }
                    }
                }

                compressionStream.Write(data, 0, data.Length);
            }
            compressionStream.Flush();
            compressionStream.Finish();

            compressedBytes = underlyingMemoryStream.GetBuffer();
        }
        finally
        {
            if (compressionStream != null)
            {
                compressionStream.Dispose();
                underlyingMemoryStream = null;
            }
            if (underlyingMemoryStream != null)
            {
                underlyingMemoryStream.Dispose();
            }
        }

        int numChunks = compressedBytes.Length/ MaxBlockSize;
        if (compressedBytes.Length % MaxBlockSize != 0)
        {
            numChunks++;
        }

        for (int i = 0; i < numChunks; i++)
        {
            int length = compressedBytes.Length - i * MaxBlockSize;

            if (length > MaxBlockSize)
            {
                length = MaxBlockSize;
            }

            WriteChunk(PngChunkTypes.Data, compressedBytes, i*MaxBlockSize, length);
        }
    }
Deboer answered 25/5, 2015 at 23:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.