WebBrowser control...again about web site thumbnails/screenshots generated from the server

by Patrice 5/23/2008 5:10:46 PM

So, how did I start this easy project? Opened the browser, Googled for it. What did I get as results? It's mid 2008 and I got (personal statistics here) :

  1. 80% of samples, code snippet, suggestions using WebBrowser.DrawToBitmap
  2. 18% of IHTMLElementRender::DrawToDC
  3. 1% of Print screen
  4. 1% of IViewObject::Draw

Obvioulsy, the WebBrowser is just a wrapper around IE. After all these years, solutions on Internet should be trustable? no?...nope.

The first solution, at first, seems very easy: navigate to your page (WebBrowser.Navigate), once you get the DocumentCompleted event, call DrawToBitmap. But that works only with simple web sites. For the others, we get what people on forums call: the white page.... empty image. So that means that it would mostly never work for today's web sites. (I've found one solution on the web that was generating the images correctly but it uses so much code to solve the problem (refresh and reload) and it was implemented in a WinForm).

The second solution, is a bit more complex but easy to implement. Same as the first solution, it all starts with a call to WebBrowser.Navigate and then wait for the DocumentCompleted event. There, I said to myself: "that is it!". I started to test my solution (keep in mind that at this point, I'm using a WinForm project for test purposes). It was all good until I arrived on finance.yahoo.com... Dooh, just some parts of the page were copied to my file... test, change code, test, modify here and there, test... nothing always the same %#^$% result. I needed something else.

The third worked but I was getting the browser frame included in the image... so abandoned this one quickly.

Finally, the last one, is the answer to the second solution's problem. What I've found, is that IHTMLElementRender does not deal with opacity (transparency). It doesn't draw it and that's why my image was missing some parts. I've found the solution from an old post from Nathan Moinvaziri. So, I implemented his easy solution that is to use IViewObject instead of IHTMLElementRender... and it worked with all the sites I was browsing to... wooohoo!

...but I'm not done yet. It now runs in a WinForm application and I don't want this WinForm to pop up on my server... obviously. So, I said to myself, I just have to create a simple class library, reference it from my web project and that's it!... hmmm, not exactly. STA... this is at least documented everywhere... the WebBrowser control needs to run within a Single-Threaded Apartment... long time I haven't heard about this term, hehehe.

Alright, I just have to do it this way...  and it worked! Even for finance.yahoo.com! .... then I tried it with finance.yahoo.com/q?s=IBM  ... #%^*!@ ... "Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))".... wtf is going on??? So, I'm getting exception on WebBrowser.ReadyState that I used in a "while" loop waiting for "WebBrowser.ReadyState == WebBrowserReadyState.Complete"... most of the code samples found for WebBrowser/STA do this trick... since I'm a dumass monkey, I did the same... alright, must be another way... I brought back my DocumentCompleted event but it's not obvious to deal with this since it has to be run within one call. I mixed the "while" loop with "Application.DoEvents()" to wait for the DocumentCompleted event be called and processed and then set a flag to "true" to exit the loop... here is the result:

public class ScreenCapturerSTA
{
    private string m_url;
    private byte[] m_byteImg;
    private bool m_done = false;
    private int m_tnWidth = 100;
    private int m_tnHeight = 100;

    /// <returns>Base64 string format</returns>
    public string CaptureBase64(string url)
    {
        return Convert.ToBase64String(Capture(url));
    }
    public byte[] Capture(string url)
    {
        return Capture(url, 60000/*1 min*/);
    }
    public byte[] Capture(string url, int timeout)
    {
        m_url = url;
        Thread t = new Thread(new ThreadStart(Capture));
        t.Name = "screenCapturer";
        t.SetApartmentState(ApartmentState.STA);
        t.Start();
        t.Join(timeout);
        return m_byteImg;
    }
    private void Capture()
    {
        m_byteImg = null;
        using (WebBrowser webBrowser = new WebBrowser())
        {
            webBrowser.DocumentCompleted += new WebBrowserDocumentCompletedEventHandler(webBrowser_DocumentCompleted);
            webBrowser.Height = 1000;
            webBrowser.Width = 1000;
            webBrowser.ScriptErrorsSuppressed = true;
            webBrowser.ScrollBarsEnabled = false;
            webBrowser.Navigate(m_url);
            while (!m_done)
            {
                Application.DoEvents();
            }
        }
    }

    void webBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
    {
        if (e.Url == new Uri(m_url))//skip everything else
        {
            WebBrowser webBrowser = sender as WebBrowser;

            try
            {
                int width = webBrowser.ClientRectangle.Width;
                int height = webBrowser.ClientRectangle.Height;

                IHTMLDocument2 doc2 = (IHTMLDocument2)webBrowser.Document.DomDocument;
                using (Bitmap image = new Bitmap(width, height))
                {
                    using (Graphics grfx = Graphics.FromImage(image))
                    {
                        grfx.Clear(Color.White);

                        _RECTL bounds;
                        bounds.left = 0;
                        bounds.top = 0;
                        bounds.right = width;
                        bounds.bottom = height;

                        IntPtr grfxHdc = grfx.GetHdc();
                        IViewObject viewObject = doc2 as IViewObject;

                        viewObject.Draw(1, -1, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero,
                            (IntPtr)grfxHdc, ref bounds, IntPtr.Zero, IntPtr.Zero, 0);

                        grfx.ReleaseHdc(grfxHdc);
                    }

                    using (Image thumbnailImage = image.GetThumbnailImage(m_tnWidth,
                        m_tnHeight, new Image.GetThumbnailImageAbort(ThumbnailCallback), IntPtr.Zero))
                    {
                        using (MemoryStream memstream = new MemoryStream())
                        {
                            thumbnailImage.Save(memstream, ImageFormat.Png);
                            m_byteImg = memstream.ToArray();
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
                using (Bitmap image = new Bitmap(m_tnWidth, m_tnHeight))
                {
                    using (Graphics grfx = Graphics.FromImage(image))
                    {
                        grfx.Clear(Color.White);

                        StringFormat format = new StringFormat();
                        format.LineAlignment = StringAlignment.Center;
                        format.Alignment = StringAlignment.Center;

                        using (SolidBrush brush = new SolidBrush(Color.FromArgb(61, 61, 61)))
                        {
                            grfx.DrawString("No image generated", new Font("arial", 6, 
                                FontStyle.Bold, GraphicsUnit.Point), brush, 
                                new Rectangle(0, 0, m_tnWidth, m_tnHeight), format);
                        }
                    }
                    using (MemoryStream memstream = new MemoryStream())
                    {
                        image.Save(memstream, ImageFormat.Png);
                        m_byteImg = memstream.ToArray();
                    }
                }
            }
            finally 
            {
                m_done = true;
            }
        }
    }

    public bool ThumbnailCallback()
    {
        return true;
    }

}
 
note: this hasn't been tested on a production environment yet, be careful!

 

Hopefully, it will serve fellow developers!

Missing ToTitleCase from TextInfo (Silverlight)

by Patrice 5/4/2008 12:35:00 AM

The other day I was converting some "utility" classes to be used within my Silverlight project and I realized (well, the compiler told me) that the ToTitleCase method was missing from System.Globalization.TextInfo. So I created one that ended up being more useful then the original one (well, for my own usage).

The default utility of ToTitleCase is obviously to change the first letter to uppercase and the rest to lowercase (mine can take a complete sentence too). Then a friend of mine gave me a case where it wouldn't work. Take for example "Merriam-Webster"; the W after the dash will be lower case. That's why I added a parameter to my ToTitleCase method, a character list, that are used as "uppercase after this character". The default value is naturally " " (space).

While I was there, I used the new Method Extensions feature.  So it can be used like:

"this sentence to titlecase".ToTitleCase(); 

 

   1:  public static  class UtilsClient
   2:  {
   3:      public static string ToTitleCase(this string value)
   4:      {
   5:          return ToTitleCase(value, new List<char> { ' ' });
   6:      }
   7:   
   8:      public static string ToTitleCase(this string value, List<char> separators)
   9:      {
  10:          string result = "";
  11:          bool nextUpper = true; //first letter always upper case
  12:   
  13:          value = value.ToLower();//initialize all to lower case
  14:   
  15:          for (int charIndex = 0; charIndex < value.Length; charIndex++)
  16:          {
  17:              string nextChar = value[charIndex].ToString();
  18:              if (nextUpper)
  19:              {
  20:                  nextChar = nextChar.ToUpper();
  21:              }
  22:   
  23:              result += nextChar;
  24:   
  25:              if (separators.Any(c => c.Equals(value[charIndex])))//put next char to upper case
  26:                  nextUpper = true;
  27:              else
  28:                  nextUpper = false;
  29:   
  30:          }
  31:   
  32:          return result;
  33:      }
  34:  }

Silverlight Project Structures : Design vs. Business Logic ( or UI vs. UX )

by Patrice 5/2/2008 11:07:00 PM

Since I've been experimenting with Silverlight V2.0 Beta1 (business logic in V1.0 was mostly JavaScript code, so mostly the same structure as regular web development), I've just played around by testing snippets of code in "test" projects. Now that I try to build more serious projects, I struggle on how structuring my project. I know, it might sounds stupid but Silverlight is not  Winform nor ASP.NET...or a little of both.

As much as people like to talk about the separation between design and business logic, I think it's a personal thing (as in "you", "your team" or "your company"). When the language/platform allows the developer to write "design" part in the business logic side and some business logic in the design side, it always ends up with "spaghetti" code. I remember classic ASP when we were mixing the client side JavaScript and the server side JavaScript code between HTML tags (supposedly the design part)... and when ASP.NET arrived it was supposed to be "the" answer for "spaghetti" code... I think I've seen worst in ASP.NET then ASP... design part done by spitting HTML strings from server controls. Even if it's the core of ASP.NET (hide the HTML spitting from the developers), there are a better ways of doing this (but anyway that's old technology, LOL).

With Silverlight, there is no "server side" code... but there is still a layer separation: user interface (UI) vs. user experience (UX).  So, we are faced again with the same interrogations... for example, should I write my "animation" in directly in XAML or create it dynamically from C#?  I'll find my way through experimentations...

I read Jose Fajardo blog frequently and two weeks ago he wrote about his preferred way of structuring his Silverlight projects. I really like it. I'll "borrow" his idea and start my new project with this structure. We'll see how much I'll keep the original form.

Powered by BlogEngine.NET 1.3.1.0

LFDX Software Inc.

About the author

P. Lafond Patrice Lafond
(and yes, it's Mr!)
Software Engineer
French Canadian expat in Bermuda for over 7 years.
flag QC  flag BDA

E-mail me Send mail

Calendar

<<  January 2009  >>
MoTuWeThFrSaSu
2930311234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar

Pages

    Recent comments

    Authors

    Disclaimer

    The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

    © Copyright 2009

    Sign in