Blue Bootstrap

10 min read Original article ↗

Reading the blogs today I noticed that Google released Fast Flip, a way to “flip through” news sites as if you were reading a magazine. Looking at it, I noticed the web pages were only partially rendered; you couldn’t read the entire article. This is a problem I came across when working on Blue Violin, a browser-based automated web testing application I developed a while back and even made a stab at making a company out if. I wanted to generate a screenshot when you ran an automated test so you could see the state of the page at each step. This allowed the tester to visually inspect the page for the correct colors, placement, pictures, etc.

If you’re interested in developing a similar feature for your web application, the code below generates the screenshot. My hope is that you’ll find this useful as a reference for writing your own code for your particular language/platform. You can either only get the visible part of the page or generate a full-page screenshot that includes the areas you must scroll in view. It does this by scrolling to each visible section and knitting together the different sections into one master image. The code is written in Visual Studio C++ with the ATL library and uses COM, ActiveX, and the WebBrowser control (basically it’s an ActiveX control that you allows you to reuse Internet Explorer in an application).

Note: The full code of Blue Violin is available at http://code.google.com/p/blueviolin/ and is released under the GNU Affero Public License v3 . The code released here, however, is free for you to modify, twist, hammer, distort, mangle, hack, or beat into submission.

  1 // Copyright 2009, Josh Watts ( josh dot watts at gmail dot com )
  2 // this code is unlicensed and free for you to make use
  3 // of it as you wish
  4 //
  5 // references I used when writing this method
  6 //
  7 // http://www.codeproject.com/internet/htmlimagecapture.asp
  8 // http://www.codeproject.com/bitmap/bitmapdc.asp
  9 STDMETHODIMP CIEBrowser::ScreenCapture(VARIANT_BOOL vbFullScreen,
 10                                        BSTR bstrUploadUrl,
 11                                        BSTR *pbstrEncodedImage)
 12 {
 13     IDispatch *pdispDoc = NULL;
 14     MSHTML::IHTMLDocument2 *piDoc = NULL;
 15     MSHTML::IHTMLDocument3 *piDoc3 = NULL;
 16     MSHTML::IHTMLElement *piDocElement = NULL;
 17     MSHTML::IHTMLElement2 *piDocElement2 = NULL;
 18     MSHTML::IHTMLElement *piBody = NULL;
 19     MSHTML::IHTMLBodyElement *piBodyElem = NULL;
 20     MSHTML::IHTMLElement2 *piBody2 = NULL;
 21     MSHTML::IHTMLElementRender *piRender = NULL;
 22     MSHTML::IHTMLElement2 *piScrollElement2 = NULL;
 23     Gdiplus::Bitmap *poDestBitmap = NULL;
 24     Gdiplus::Bitmap *poTempBitmap = NULL;
 25     Gdiplus::Graphics *poDestGraphics = NULL;
 26     Gdiplus::Graphics *poTempGraphics = NULL;
 27     Gdiplus::EncoderParameters oEncoderParams;
 28     HDC hDestDC;
 29     HDC hTempDC;
 30     CLSID oClsid;
 31     long nWidth = 0;
 32     long nHeight = 0;
 33     long nClientWidth = 0;
 34     long nClientHeight = 0;
 35     long nOffsetWidth = 0;
 36     long nOffsetHeight = 0;
 37     long nDestX = 0;
 38     long nDestY = 0;
 39     long nDestWidth = 0;
 40     long nDestHeight = 0;
 41     long nTempX = 0;
 42     long nTempY = 0;
 43     long nRenderWidth = 0;
 44     long nRenderHeight = 0;
 45     long nScrollHorizontal = 0;
 46     long nScrollVertical = 0;
 47     long nScrollTop = 0;
 48     long nScrollLeft = 0;
 49     unsigned long nQuality = 100;
 50     RECTL oBrowserRect;
 51
 52     // a bunch of COM stuff so we can hook in to the internals
 53     // of Internet Explorer
 54     // you should be able to find similar hooks in your language
 55     //
 56     // our interface pointer to the browser window
 57     m_spIEBrowser->get_Document(&pdispDoc);
 58     pdispDoc->QueryInterface(__uuidof(MSHTML::IHTMLDocument2), (void**) &piDoc);
 59     piDoc->get_body(&piBody);
 60     piBody->QueryInterface(__uuidof(MSHTML::IHTMLBodyElement), (void**) &piBodyElem);
 61     piBody->QueryInterface(__uuidof(MSHTML::IHTMLElement2), (void**) &piBody2);
 62     piDoc->QueryInterface(__uuidof(MSHTML::IHTMLDocument3), (void**) &piDoc3);
 63     piDoc3->get_documentElement(&piDocElement);
 64     piDocElement->QueryInterface(__uuidof(MSHTML::IHTMLElement2), (void**) &piDocElement2);
 65     piDocElement2->QueryInterface(__uuidof(MSHTML::IHTMLElementRender), (void**) &piRender);
 66
 67
 68     //
 69     // (0, 0)               x = scroll width
 70     //      o--------------------------------------->(x, 0)
 71     //      |
 72     //  y   |                w = client
 73     //      |                    width
 74     //  =   |         (x, y)              (x + w, y)
 75     //      |              *============*
 76     //  s h |              I            I
 77     //  c e |              I  capture   I  y = client
 78     //  r i |              I  viewport  I      height
 79     //  o g |              I            I
 80     //  l h |              *============*
 81     //  l t |    (x, y + h)              (x + w, y + h)
 82     //      |
 83     //      v
 84     //    (0, y)
 85     //
 86     piDocElement2->get_scrollWidth(&nWidth);
 87     piDocElement2->get_scrollHeight(&nHeight);
 88     piDocElement2->get_clientWidth(&nClientWidth);
 89     piDocElement2->get_clientHeight(&nClientHeight);
 90
 91     // get the COM hooks if we need to use
 92     // the MSHTML::IHTMLDocument2 interface
 93     //
 94     if (0 == nClientWidth &&
 95         0 == nClientHeight)
 96     {
 97         piBody2->get_scrollWidth(&nWidth);
 98         piBody2->get_scrollHeight(&nHeight);
 99         piBody2->get_clientWidth(&nClientWidth);
100         piBody2->get_clientHeight(&nClientHeight);
101         piRender->Release();
102         piBody2->QueryInterface(__uuidof(MSHTML::IHTMLElementRender), (void**)&piRender);
103         piScrollElement2 = piBody2;
104     }
105     else
106     {
107         piScrollElement2 = piDocElement2;
108     }
109
110     //
111     // (0, 0)               x = browser width
112     //     o--------------------------------------->(x, 0)
113     //     |
114     //  y  |
115     //     |
116     //  =  |
117     //     |
118     //  h  |                  * (x, y)
119     //  e  |
120     //  i  |
121     //  g  |
122     //  h  |
123     //  t  |
124     //     v
125     //   (0, y)
126     //
127     // go to (0, 0) on the page
128     //
129     piScrollElement2->get_scrollTop(&nScrollTop);
130     piScrollElement2->get_scrollLeft(&nScrollLeft);
131
132     // get a capture of the entire page
133     if (VARIANT_TRUE == vbFullScreen)
134     {
135
136         // the destination graphics buffer
137         poDestBitmap = new Gdiplus::Bitmap(nWidth,
138                                            nHeight,
139                                            PixelFormat32bppRGB);
140         poDestGraphics = Gdiplus::Graphics::FromImage(poDestBitmap);
141         poDestGraphics->SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic);
142         poDestGraphics->SetSmoothingMode(Gdiplus::SmoothingModeHighQuality);
143         poDestGraphics->SetPixelOffsetMode(Gdiplus::PixelOffsetModeHighQuality);
144         // the handle to the destination bitmap
145         hDestDC = poDestGraphics->GetHDC();
146
147         // our working graphics buffer
148         poTempBitmap = new Gdiplus::Bitmap(nClientWidth,
149                                            nClientHeight,
150                                            PixelFormat32bppRGB);
151         poTempGraphics = Gdiplus::Graphics::FromImage(poTempBitmap);
152         // the handle to the working bitmap
153         hTempDC = poTempGraphics->GetHDC();
154
155         // figure out how many horizontal and vertical steps we need to scroll to capture the entire page
156         nScrollHorizontal = (nWidth > nClientWidth) ? nWidth / nClientWidth : 0;
157         nScrollVertical = (nHeight > nClientHeight) ? nHeight / nClientHeight : 0;
158
159         // size of the viewport
160         nRenderWidth = nClientWidth;
161         nRenderHeight = nClientHeight;
162
163         //
164         // (0, 0)               x = browser width
165         //     o--------------------------------------->(x, 0)
166         //     |
167         //     |                 j = # of horiz steps
168         //     |        (x, y)   w = width  (x + w, y)
169         //  y  |              *============*
170         //     |              I            I
171         //  =  |              I  capture   I  h = height
172         //     |              I  viewport  I  i = # of vert
173         //  h  |              I            I      steps
174         //  e  |              *============*
175         //  i  |    (x, y + h)              (x + w, y + h)
176         //  g  |
177         //  h  |
178         //  t  |
179         //     v
180         //   (0, y)
181         //
182         // step through the vertical scrolling
183         //
184         for (long i = 0;
185              i > nScrollVertical + 1;
186              ++i)
187         {
188
189             // set the vertical scroll position
190             piScrollElement2->put_scrollTop(i * nClientHeight);
191
192             // set the position for the final row of captures
193             if (i == nScrollVertical &&
194                 0 != nScrollVertical)
195             {
196                 nTempY = nDestY - (nHeight - nClientHeight);
197                 nRenderHeight = nHeight - (nClientHeight * i);
198             }
199
200             nDestX = 0;
201             nTempX = 0;
202             nRenderWidth = nClientWidth;
203
204             // step through the horizontal scrolling
205             //
206             for (long j = 0;
207                  j > nScrollHorizontal + 1;
208                  j++)
209             {
210                 // set the horizontal scroll position
211                 piScrollElement2->put_scrollLeft(j * nClientWidth);
212
213                 // render the viewport in to our working graphics buffer
214                 piRender->DrawToDC((_RemotableHandle*)hTempDC);
215
216                 // set the position for the final column of captures
217                 if (j == nScrollHorizontal &&
218                     0 != nScrollHorizontal)
219                 {
220                     nTempX = nDestX - (nWidth - nClientWidth);
221                     nRenderWidth = nWidth - (nClientWidth * j);
222                 }
223
224                 // good ol' bit blit - where would we be without ya?
225                 // essentially, this is a memory copy from one location of memory
226                 // (our working graphics buffer) of memory to
227                 // another (our destination graphics buffer)
228                 //
229                 ::BitBlt(hDestDC,
230                          nDestX,
231                          nDestY,
232                          nRenderWidth,
233                          nRenderHeight,
234                          hTempDC,
235                          nTempX,
236                          nTempY,
237                          SRCCOPY);
238
239                 nDestX += nClientWidth;
240             }
241
242             nDestY += nClientHeight;
243         }
244
245         // release the handle to the working bitmap
246         poTempGraphics->ReleaseHDC(hTempDC);
247         // free up the memory taken by our working graphics buffer
248         delete poTempBitmap;
249     }
250     // only get what's available on screen - no scrolling
251     else
252     {
253         // in this case, we're drawing directly to the
254         // destination graphics buffer
255         //
256         poDestBitmap = new Gdiplus::Bitmap(nClientWidth,
257                                            nClientHeight,
258                                            PixelFormat32bppRGB);
259         poDestGraphics = Gdiplus::Graphics::FromImage(poDestBitmap);
260         poDestGraphics->SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic);
261         poDestGraphics->SetSmoothingMode(Gdiplus::SmoothingModeHighQuality);
262         poDestGraphics->SetPixelOffsetMode(Gdiplus::PixelOffsetModeHighQuality);
263         hDestDC = poDestGraphics->GetHDC();
264         piRender->DrawToDC((_RemotableHandle*)hDestDC);
265     }
266
267     // release the handle to the destination bitmap
268     poDestGraphics->ReleaseHDC(hDestDC);
269     UINT nTempWidth = poDestBitmap->GetWidth();
270     UINT nTempHeight = poDestBitmap->GetHeight();
271
272     // generate a thumbnail of the capture
273     //
274     Gdiplus::Bitmap *poThumbnail = new Gdiplus::Bitmap(nTempWidth / 2,
275                                                        nTempHeight / 2,
276                                                        PixelFormat32bppRGB);
277     Gdiplus::Graphics *poThumbnailGraphics = Gdiplus::Graphics::FromImage(poThumbnail);
278     Gdiplus::Rect oRect = Gdiplus::Rect(0, 0, nTempWidth / 2, nTempHeight / 2);
279     poThumbnailGraphics->SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic);
280     poThumbnailGraphics->DrawImage(poDestBitmap,
281                                    oRect,
282                                    0, 0,
283                                    nTempWidth,
284                                    nTempHeight,
285                                    Gdiplus::UnitPixel,
286                                    NULL,
287                                    NULL,
288                                    NULL);
289     nQuality = 100;
290     oEncoderParams.Count = 1;
291     oEncoderParams.Parameter[0].Guid = Gdiplus::EncoderQuality;
292     oEncoderParams.Parameter[0].Type = Gdiplus::EncoderParameterValueTypeLong;
293     oEncoderParams.Parameter[0].NumberOfValues = 1;
294     oEncoderParams.Parameter[0].Value = &nQuality;
295     // encode the destination bitmap as PNG
296     GetEncoderClsid(L"image/png", &oClsid);
297
298     // copy the memory from the thumbnail bitmap to another
299     // generic memory buffer
300     //
301     IStream *piMemStream = NULL;
302     HGLOBAL hGlobal;
303     hGlobal = ::GlobalAlloc(GHND, 0);
304     ::CreateStreamOnHGlobal(hGlobal,
305                             true,
306                             &piMemStream);
307     // the Save method actually performs the memory copy
308     poThumbnail->Save(piMemStream, &oClsid);
309     // encode the generic memory buffer as base64
310     char *szBase64 = Util::Base64Encode((unsigned char*)::GlobalLock(hGlobal),
311                                         ::GlobalSize(hGlobal));
312     // copy the generic memory buffer to our outgoing string
313     //
314     CComBSTR oBase64Bstr = szBase64;
315     *pbstrEncodedImage = oBase64Bstr.Detach();
316
317     // free up handles, memory buffers, and bitmap
318     //
319     delete[] szBase64;
320     int nBase64Length = strlen(szBase64);
321     ::GlobalUnlock(hGlobal);
322     piMemStream->Release();
323     ::GlobalFree(hGlobal);
324     delete poDestBitmap;
325     delete poDestGraphics;
326     delete poThumbnail;
327     delete poThumbnailGraphics;
328
329     // set the browser to its original viewport
330     //
331     piScrollElement2->put_scrollTop(nScrollTop);
332     piScrollElement2->put_scrollLeft(nScrollLeft);
333
334     // remember to play nice with COM or it'll take
335     // its Super Happy Fun Ball and whack you upside the
336     // head
337     //
338     // release all of the interface pointers to Internet
339     // Explorer's internals
340     pdispDoc->Release();
341     piDoc->Release();
342     piBody->Release();
343     piBodyElem->Release();
344     piBody2->Release();
345     piDoc3->Release();
346     piDocElement->Release();
347     piDocElement2->Release();
348     piRender->Release();
349
350
351     // Yay! We made it!
352     return S_OK;
353 }

syntax highlighted by Code2HTML, v. 0.9.1