poniedziałek, 24 grudnia 2012

Bitmapowy zawrót głowy

Dziś bez zbędnych wprowadzeń i opisów - po prostu fragmenty kodu odpowiedzialne za konwersję pomiędzy trzema klasami odpowiedzialnymi za grafikę w .NET: Bitmap, BitmapImage oraz BitmapSource. Natomiast w ramach bonusu świątecznego konwersja z i do Byte[], która z pewnością w przypadku trzymania obrazów w plikach/bazach danych przyda się "od kopa". Przedstawiony kod jest w postaci metod rozszerzających (ang. extension methods), więc umożliwi przejrzyste i proste wykorzystanie.

Bitmap

using System;
using System.Drawing;
using System.Windows.Media.Imaging;
using System.IO;
using System.Drawing.Imaging;
using System.Windows.Interop;

namespace Runaurufu.Extension
{
    public static class ExtensionBitmap
    {
        public static BitmapImage ToBitmapImage(this Bitmap A)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                A.Save(ms, ImageFormat.Bmp);
                ms.Position = 0;
                BitmapImage bi = new BitmapImage();
                bi.BeginInit();
                bi.StreamSource = ms;
                bi.CacheOption = BitmapCacheOption.OnLoad;
                bi.EndInit();
                return bi;
            }
        }

        [System.Runtime.InteropServices.DllImport("gdi32.dll")]
        private static extern bool DeleteObject(IntPtr hObject);

        public static BitmapSource ToBitmapSource(this Bitmap A)
        {
            IntPtr hBitmap = A.GetHbitmap();
            BitmapSource result;
            try
            {
                result = Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, System.Windows.Int32Rect.Empty, System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());
            }
            finally
            {
                DeleteObject(hBitmap);
            }

            return result;
        }

        public static Byte[] ToByteArray(this Bitmap A)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                A.Save(ms, ImageFormat.Bmp);
                ms.Position = 0;
                return ms.ToArray();
            }
        }
    }
}

BitmapSource

using System;
using System.Drawing;
using System.IO;
using System.Windows.Media.Imaging;

namespace Runaurufu.Extension
{
    public static class ExtensionBitmapSource
    {
        public static BitmapImage ToBitmapImage(this BitmapSource A)
        {
            return A.ToBitmapImage(new BmpBitmapEncoder());
        }

        public static BitmapImage ToBitmapImage(this BitmapSource A, BitmapEncoder encoder)
        {
            BitmapImage bi = new BitmapImage();

            using (MemoryStream ms = new MemoryStream())
            {
                encoder.Frames.Add(BitmapFrame.Create(A));
                encoder.Save(ms);
                ms.Position = 0;

                bi.BeginInit();
                bi.StreamSource = ms;
                bi.CacheOption = BitmapCacheOption.OnLoad;
                bi.EndInit();
            }

            return bi;
        }

        public static Bitmap ToBitmap(this BitmapSource A)
        {
            return A.ToBitmap(new BmpBitmapEncoder());
        }

        public static Bitmap ToBitmap(this BitmapSource A, BitmapEncoder encoder)
        {
            System.Drawing.Bitmap bitmap;
            using (MemoryStream outStream = new MemoryStream())
            {
                encoder.Frames.Add(BitmapFrame.Create(A));
                encoder.Save(outStream);
                bitmap = new System.Drawing.Bitmap(outStream);
            }
            return bitmap;
        }

        public static Byte[] ToByteArray(this BitmapSource A)
        {
            return A.ToByteArray(new BmpBitmapEncoder());
        }

        public static Byte[] ToByteArray(this BitmapSource A, BitmapEncoder encoder)
        {
            using (MemoryStream memoryStream = new MemoryStream())
            {
                encoder.Frames.Add(BitmapFrame.Create(A));
                encoder.Save(memoryStream);
                return memoryStream.GetBuffer();
            }
        }
    }
}

BitmapImage

using System;
using System.Drawing;
using System.IO;
using System.Windows.Media.Imaging;

namespace Runaurufu.Extension
{
    public static class ExtensionBitmapImage
    {
        public static Bitmap ToBitmap(this BitmapImage A)
        {
            return A.ToBitmap(new BmpBitmapEncoder());
        }

        public static Bitmap ToBitmap(this BitmapImage A, BitmapEncoder encoder)
        {
            Bitmap bitmap;
            using (MemoryStream outStream = new MemoryStream())
            {
                encoder.Frames.Add(BitmapFrame.Create(A));
                encoder.Save(outStream);
                bitmap = new System.Drawing.Bitmap(outStream);
            }
            return bitmap;
        }

        public static Byte[] ToByteArray(this BitmapImage A)
        {
            return A.ToByteArray(new BmpBitmapEncoder());
        }

        public static Byte[] ToByteArray(this BitmapImage A, BitmapEncoder encoder)
        {
            Byte[] byteArr;
            using (MemoryStream ms = new MemoryStream())
            {
                encoder.Frames.Add(BitmapFrame.Create(A));
                encoder.Save(ms);
                ms.Position = 0;
                byteArr = ms.GetBuffer();
            }
            return byteArr;
        }
    }
}

Byte[]

using System;
using System.Drawing;
using System.IO;
using System.Windows.Media.Imaging;

namespace Runaurufu.Extension
{
    public static class ExtensionByte
    {
        public static Bitmap ToBitmap(this Byte[] A)
        {
            using (MemoryStream ms = new MemoryStream(A))
            {
                return new Bitmap(ms);
            }
        }

        public static BitmapImage ToBitmapImage(this Byte[] A)
        {
            using (MemoryStream ms = new MemoryStream(A))
            {
                ms.Position = 0;
                BitmapImage bi = new BitmapImage();
                bi.BeginInit();
                bi.StreamSource = ms;
                bi.CacheOption = BitmapCacheOption.OnLoad;
                bi.EndInit();
                return bi;
            }
        }
    }
}

Warto zwrócić uwagę, że w przypadku BitmapSource czyli obrazach rodem z WPF/Silverlighta konieczne jest podanie odpowiedniego enkodera - wybór np. JpegEncoder na obrazie z kanałem alfa (przeźroczystość) sprawi, że ów kanał alfa trafi szlag... :) Dodatkowo z oczywistych przyczyn nie ma metody do konwersji BitmapImage -> BitmapSource - a jeśli ktoś szuka bo z jakiś powodów potrzebuje to może wykorzystać ten fragment:
        public static BitmapSource ToBitmapSource (this BitmapImage A)
        {
            return (BitmapSource)A;
        }

A wszystkim, którzy choć od czasu do czasu zaglądają w me skromne progi życzę Wesołych Świąt :)

Keywords: C#, . NET, Bitmap, BitmapImage, BitmapSource, Bitmap conversions, Silverlight, WPF, WinForm, Image, Picture, Transparent BitmapImage.

niedziela, 4 listopada 2012

Ree7 Tile Toolkit i obrazy na kafelkach

Jeśli kiedykolwiek poczujecie chęć stworzenia własnych "kafelek" do swojej aplikacji to zapewne odnajdziecie w czeluściach Internetu bibliotekę "Ree7 Tile Toolkit for Windows Phone 7" umożliwiającą tworzenie całkowicie dowolnych kafelek. I żeby była jasność - poprzez "całkowitą dowolność" mam na myśli tworzenie zawartości Tile w ten sam sposób co zwykłego User Control (oczywiście Tile musi być statyczny, więc animacji w nim nie odtworzymy) - czyli tak za pomocą XAML jak i bezpośrednio przy użyciu kodu w VB czy C#.

Niestety przy tworzeniu własnych kafelek napotkać możemy jeden podstawowy problem wynikający ze sposobu obsługi obrazów w Silverlight. Obrazy generalnie nie są ładowane do pamięci aż do momentu, w którym są wyświetlane, tak więc tworzone kafelki po prostu renderowane są bez elementów pochodzących z plików graficznych. Dodatkowo w przykładowej aplikacji biblioteki ten problem jest nie dostrzegalny, gdyż tam kafelki są uprzednio wyświetlane w drzewie aplikacji:
SimpleTileContainer.Children.Add(myCustomTile1);
a tworzone są dopiero po kliknięciu w odpowiedni przycisk.
ShellTile.Create(new Uri("/MainPage.xaml?src=SampleTile", UriKind.Relative), myCustomTile1.GetShellTileData());

wtorek, 16 października 2012

Syndykacja czyli jak wyświetlić treść na swojej stronie?

Kanały treści, feedy lub po prostu RSS/Atom*. Istnieje wiele określeń na nośnik informacji używany w syndykacji treści stron internetowych, które to nieraz są stosowane wymiennie. W zasadzie dziś mało kto zwraca uwagę na to co kryje się pod magicznym guzikiem symbolizującym właśnie kanał treści danej witryny. Ale czym jest owa syndykacja, której próżno szukać w słownikach języka polskiego (link | link)? W uproszczeniu jest to publikowanie treści podmiotu przez inny podmiot, który tej treści nie utworzył. Definicja może i krótka, ale pewnie nie do końca zrozumiała, dlatego posłużę się przykładem nie związanym z internetem:
Stacja telewizyjna A nakręciła materiał z koncertu super gwiazdy i udostępnia go na swoim kanale. Jednocześnie stacja ta pozwala innym stacjom na używanie tego materiału na swoich kanałach (zwykle za opłatą licencyjną). I właśnie takie udostępnianie innym nazywamy syndykacją.
W świecie internetu gro procesu syndykacji zostało uproszczone do generowania przez właściciela treści pliku z jej zawartością, który to umieszcza na swoim serwerze. Rzadko też spotykamy się z odpłatnymi kanałami, a jeśli już to są one konsekwencją płatnego dostępu do całego serwisu. Dość po słowie, że świat idzie na przód i syndykacja w obecnym obliczu jest jednym z elementów, które powodują odejście starej wizji mediów w niebyt.

JavaScript i AJAX

 Ale wracając do tematu wpisu, czyli wyświetlania kanału treści na swojej witrynie. Niestety najprostsze i najkorzystniejsze dla właściciela hostingu (transfer, czas procesora) podejście wykorzystujące JavaScript w wielu przypadkach nie sprawdzi się z racji polityki pochodzenia z tej samej domeny. Oczywiście jeśli chcemy agregować treści znajdujące się w serwisach tej samej domeny ten problem nas nie dotyczy i bez problemu możemy wykorzystać AJAX (Tak kanały w standardzie RSS jak i w Atom korzystają z XML) choćby z poniższym kodem:
function GetXHR()
{
 var xmlHttp = null;
 try
 {   // Firefox, Opera 8.0+, Safari
  xmlHttp=new XMLHttpRequest();
 }
 catch (e)
 {   // Internet Explorer
  try
  {
   xmlHttp=new ActiveXObject("Msxml2.XMLHTTP");
  }
  catch (e)
  {
   xmlHttp=new ActiveXObject("Microsoft.XMLHTTP");
  }
 }
 if(xmlHttp == null)
  alert("Browser does not support HTTP Request");
 return xmlHttp;
}

window.addEventListener('load', function()
{
 var chk=setInterval(function()
 {  
  doc = document.getElementsByTagName("body");
  if(doc.length == 0 || doc[0].innerHTML.length < 500)
  { return; }
  clearInterval(chk);
  
  var xhr = GetXHR();
  xhr.onreadystatechange=function()
  {
   if(this.readyState == 4 && this.status == 200)
   {
    document.getElementById("articleStuffShowRSSHere").innerHTML = "<a href='"+
     this.responseXML.getElementsByTagName("entry")[0].getElementsByTagName("link")[4].getAttribute("href")+"'>"+
     this.responseXML.getElementsByTagName("entry")[0].getElementsByTagName("title")[0].textContent+
     "</a>";
   }
   
  };
  xhr.open("GET" , '/feeds/posts/default', true);
  xhr.send();  
 },100);
}, false);
Przykład ten wyświetla obok link do najnowszego artykułu z tego bloga:

PHP i cache po stronie serwera

Jednak jeśli nasz serwis znajduje się w innej domenie to z usług JavaScript skorzystać nie będzie nam dane. Oczywiście możemy próbować korzystać z różnego rodzaju sztuczek lub zewnętrznych serwisów pobierających pliki syndykacji za nas... ale raczej nie o półśrodki tu chodzi, a o mniej lub bardziej profesjonalne i autonomiczne rozwiązanie. PHP na szczęście umożliwia nam pobranie plików z zewnętrznych serwisów i swobodną manipulację na nich, tak więc problem wspólnej domeny odpada. Pojawia się za to inny problem, a mianowicie problem z wydajnością - w przypadku JavaScript cały proces de facto zrzucaliśmy na użytkownika i z punktu widzenia serwera nie odczulibyśmy różnicy przy syndykacji z 2 kanałów lub z 200 kanałów (Choć użytkownik by zapewne odczuł). Przy korzystaniu z PHP natomiast jeśli każda wizyta użytkownika miałaby uruchamiać mechanizm pobierania plików, ich parsowania i transformacji do finalnej postaci prezentowanej użytkownikowi to nie potrzeba agregacji z 200 kanałów, ale wystarczy 1 kanał i 200 użytkowników naszego serwisu byśmy odczuli ten sam efekt. Co więcej istnieje realna groźba, że właściciel serwisu, z którego dane pobieramy uzna nas za źródło ataku DoS (ang. Denial-of-service), - bo wszak wszystkie żądania pobrania będą pochodzić z naszego serwera - i zablokuje nam dostęp do kanału.
Stąd też konieczny jest mechanizm zapisu pobranych danych w taki sposób aby odwiedziny nowych użytkowników nie generowały transferu danych pomiędzy serwisami, a korzystały z już ściągniętych danych. Tu należy się zastanowić na którym etapie procesu przygotowywania do prezentacji należy dane zapisać, bo w zależności od tego jak chcemy je wykorzystać inny moment tego procesu może okazać się najefektywniejszy. W poniższym przykładzie zapis do pliku na serwerze odbywa się po przygotowaniu całości prezentowanego komponentu, a podobny mechanizm można z powodzeniem wykorzystać do zapisu całych stron, których wygenerowanie wymaga wielu obliczeń lub zapytań do baz danych, a których treści nie ulegają częstej zmianie.
Generalnie kod składa się z dwóch części, pierwsza - dłuższa to funkcja przeprowadzająca pobranie, obróbkę i zapis treści na dysk serwera:
<?php
function prepare($ident)
{
 // download data and parse them with SimleXmlElement
 $content = file_get_contents('http://runaurufu.blogspot.com/feeds/posts/default');
 $x = new SimpleXmlElement($content);

 // get data which we want to use
 $entryName = $x->entry[0]->title."";
 $entryUrl = "";
 foreach ($x->entry[0]->link as $value)
 {
  $aux = $value->attributes();
  if($aux['rel']."" == 'alternate')
  {
   $entryUrl = $aux['href']."";
  }
 }

 $blogTitle = $x->title."";
 $blogSubTitle =  $x->subtitle."";
 $blogUrl = "";
 foreach ($x->link as $value)
 {
  $aux = $value->attributes();
  if($aux['rel']."" == 'alternate')
  {
  $blogUrl = $aux['href']."";
  }
 }

 // prepare for cacheing 

 //start output buffering
 ob_start();

 // here do entire presentation generation
 ?>
 <a href="<?php echo $entryUrl; ?>"><?php echo $entryName; ?></a> @ <a href="<?php echo $blogUrl; ?>" title="<?php echo $blogSubTitle; ?>"><?php echo $blogTitle; ?></a>

 <?php
 // save output buffer to $data
 $data = ob_get_contents();
 // clear output buffer and stop buffering
 // (ob_end_flush(); can be used to stop buffering and display buffer content)
 ob_end_clean();

 $handle = fopen($ident, 'wb');
 if(flock($handle, LOCK_EX))
 {
  $bytes = fwrite($handle, $data);
  flock($handle, LOCK_UN);
 }
 fclose($handle);
 return $data;
}
?>

Druga część jest odpowiedzialna za sam proces wyboru, czy chcemy użyć dane zapisane na dysku, lub czy mimo wszystko pobieramy je od nowa:
<?php
$cacheTime = 86400;
$ident = "myFeed.cache";
if(!is_file($ident))
{
 echo prepare($ident);
}
else
{
 if(filemtime($ident) + $cacheTime > time())
 {
  // show content of file
  echo file_get_contents($ident);
 }
 else
 {
  echo prepare($ident);
 }
}
?>
Oczywiście kod ten można w znacznej mierze zmodyfikować i sprawić, aby cały proces obsługi naszego cache wyglądał tak np:
<?php$cache = new Chors_Cache($identifier, $time);
if($cache->check())
{
 return $cache->fetchRender();
}
else
{
 $cache->startOutputCaching();
 // generate cached content begin
 ...
 // generate cached content end
 return $cache->endOutputCachingClean();
}?> 

Słowo na koniec

Jak widać obsługa kanałów syndykacji nie jest czymś trudnym a jedynym problemem na jaki możemy natrafić to konieczność stworzenia parsera wyłuskującego te dane, które nas interesują. Warto jeszcze wspomnieć, że o ile używanie AJAXa nie obciąża naszego serwera, to generuje zbyteczny ruch na serwerze udostępniającym kanał treści i z tego względu będąc "miłym sąsiadem" należałoby mimo wszystko zaprzęgnąć swój serwer do obsługi tych danych.

* Słowem wyjaśnienia: RSS i Atom o ile bywają synonimem udostępniania zawartości witryny to w rzeczywistości są tego udostępniania standardami, a nie samą nazwą procesu.

środa, 10 października 2012

Dźwięki w aplikacji na Windows Phone?

Jeśli chcemy mieć dźwięk przy zwykłej aplikacji, która nie jest pisana w XNA (czyli wg marketu nie jest grą) możemy go zdobyć na kilka sposobów... ale i tak koniec końców najlepiej użyć frameworku XNA :D

No, może nie wprost :] Ale o tym za chwilę.
Generalnie zapewne pierwszym sposobem, z którego większość by skorzystała jest natywna kontrolka "MediaElement", która fakt faktem udostępnia możliwość odtwarzania filmów i dźwięków niejako z marszu. Ot wrzucamy do XAML, dodajemy źródło naszej "zawartości medialnej" i mamy słyszalną zawartość. Czyli w skrócie wystarczy zrobić tak:
<MediaElement Height="100" Width="100" Name="media" Source="sound.mp3" AutoPlay="True" />
Prawda, że pięknie i prosto? No właśnie, nie do końca. Bo o ile jest to bardzo łatwe, to nadaje się de facto jedynie do odtwarzania zawartości na aktualnej stronie - wszak kontrolka jest sztywno przypisana do Page i po zmianie aktywnej strony po prostu przestanie wytwarzać jakiekolwiek dźwięki (co można obchodzić zmiennymi globalnymi, ale takie podejście jest po prostu marnowaniem zasobów telefonu). Z drugiej strony w przypadku wrzucenia apki używającej MediaElement do samo wyzwalającego się materiału (np. po pacnięciu w guzik) na market możemy spotkać się z odrzuceniem podczas certyfikacji, które to wynika z guidelines i domyślnego działania tej kontrolki:
  • When the user is already playing music on the phone when the application is launched, the application must not pause, resume, or stop the active music in the phone
    If the application plays its own background music or adjusts background music volume, it must ask the user for consent to stop playing/adjust the background music (e.g. message dialog or settings menu). This prompt must occur each time the application launches, unless there is an opt-in setting provided to the user and the user has used this setting to opt-in.
Czyli generalnie MediaElement jest super jeśli chcemy odtwarzać muzykę tła na pojedynczej stronie (lub wyświetlić filmik). Oczywiście nie możemy wtedy zapomnieć, że jeśli użytkownik słucha jakiejś muzyki to musimy się go zapytać czy wyraża zgodę na puszczenie muzyki z naszej aplikacji oraz zapewnić konfigurację dla naszego muzycznego tła:
  • If an application plays background music, the application must provide the user with configurable settings for both the background music, and the background music volume.
 Pewnym rozsądnym rozwiązaniem jest użycie BackgroundAudioPlayer, który pozwala nie tylko na puszczanie muzyki w tle wewnątrz całej aplikacji, ale również dzięki agentom wewnątrz innych aplikacji naszego telefonu (czyli możemy stworzyć odtwarzacz muzyki). Klasa ta jest bardzo przydatna, bo nie dość, że pozwala nam na wspomniane odtwarzanie tła, to również umożliwia przewijanie piosenek i wybór miejsca, od którego chcemy utwór odtworzyć (pod tym względem jest to nawet lepsze rozwiązanie od MediaPlayera z XNA). Rozwiązanie to ma jednak tą małą wadę, że plik z dźwiękiem albo musi być dostarczony z IsolatedStorage albo z zewnętrznego adresu URI (poradnik od MS dotyczący BackgroundAudioPlayer).

Jednak jeśli chcemy mieć dźwięki efektów, czyli np. odgłos gdaczącej kury po kliknięciu na guzik, to już tak MediaElement jak i BackgroundAudioPlayer się nie nadaje (bo m.in. wstrzymuje obecnie odgrywany dźwięk). Natomiast do tego celu nadaje się coś innego - klasa SoundEffect pochodząca z XNA :D
            using (var stream = TitleContainer.OpenStream("sound.wav"))
            {
                var effect = SoundEffect.FromStream(stream);
                FrameworkDispatcher.Update();
                effect.Play();
            }
Aż chciałoby się wykorzystać SoundEffect do odtwarzania muzyki w tle (zwłaszcza, że po stworzeniu instancji efektu możemy bez trudu odtwarzanie muzyki zapętlić)... niestety guideline jest bardzo stanowczy pod tym względem:
  • The SoundEffect class must not be used to play a continuous background music track in an application.
Ach Ci źli ludzie z Microsoftu! Na szczęście XNA daje nam dużo, dużo więcej możliwości, z których chyba najprzyjemniejszą jest połączenie klas Song i MediaPlayer:
            Song song = Song.FromUri("Nasze audio", new Uri("sound.mp3", UriKind.Relative));
            if (MediaPlayer.GameHasControl)
            {
                MediaPlayer.Play(song);
            }
Klasa ta jak widać nie tylko z gracją rozwiązuje problem nakładania się naszego muzycznego tła na muzykę odtwarzaną przez użytkownika dzięki polu GameHasControl i pozwala na odtwarzanie pojedynczych piosenek (Song), ale również umożliwia tworzenie ich całych kolekcji (SongCollection) oraz łatwe nawigowanie pomiędzy nimi. Tak więc myśląc o dźwięku w aplikacjach na Windows Phone bazujących na silverlight i tak prędzej czy później wykorzystamy biblioteki XNA, bo po prostu dają one ogromną swobodę i wbrew pozorom są bardzo łatwe w wykorzystaniu.