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