Monday, January 03, 2005

 

Capture an HTML document as an image

实现滚屏抓取多屏网页的讨论

起因是朋友说我写的抓屏软件不能抓IE多屏页面,然后便到处找资料,不过看来看去,都没什么可用的。不过查到了车东兄blog的“用HyperSnap实现滚屏抓取多屏网页”
http://www.chedong.com/blog/archives/000008.html
其中大致分析了一下HyperSnap的实现方式:

“1. 滚屏内容抓取:在HyperSnap的Active Window(ctrl+shift+w)抓取模式下只要启用自动滚屏(Auto Scroll)选项,在抓取的时候按住左键,过一会儿,当前屏幕就会PageDown的一页一页往下翻,一直翻到底后就将多屏幕网页内容抓取成了一个图片。
2. 存成GIF格式:GIF是文件最小而又能比较好还原原有效果的图片格式。”

自动滚屏的代码大家都写过吧。
[code]
script language"javascript">
var currentpos,timer;
function initialize()
{
timer=setInterval("scrollwindow()",10);
}
function sc(){
clearInterval(timer);
}
function scrollwindow()
{
currentpos=document.body.scrollTop;
window.scroll(0,++currentpos); //只是纵向滚动
if (currentpos != document.body.scrollTop)
sc();
}
document.onmousedown=sc
document.ondblclick=initialize
/script>
[/code]

我们通过COM得到了document对象也可以这么办,或者更直接的方法,直接call javasript,参见Eugene Khodakovsky 一篇很好玩的文章《JavaScript Calls from C++》,http://www.codeguru.com/Cpp/I-N/ieprogram/article.php/c4399

至于窗口有多宽,可以从COM得到的spBrowser->GetWidth()函数和spBrowser->GetHeight()函数得到。这里要限制一下最大的可截图片大小,HyperSnap好像是弹出对话框设置截图大小。否则太大的图没办法保存。对了IE好像也不是完全的虚模式显示。有谁看了源码,给讲讲好吗?

当然,我们还要小心翼翼的用前几天说到的FindConnectionPoint,Advise关注着IE的消息,尤其是窗口大小的变化,是否关闭窗口等等。没有用过SnagIt什么的,不知道正在截图时关掉窗口是什么结果,改变大小又是什么效果,反正我就break掉,然后弹出对话框警告,^_^。

然后就是截屏啦,主要叫出主程序慢慢一屏一屏的截,然后拼合起来。用主程序较好,否则突然关了IE怎么办。
1. retrieve HWND of the active window
2.Get IHTMLDocument2* from HWND, refer to http://support.microsoft.com/default.aspx?scid=kb;EN-US;q249232
3.Get IWebBrowser2 from IHTMLDocument2 (参见http://support.microsoft.com/default.aspx?scid=kb;EN-US;q249232 )

IWebBrowser2* pweb;
IHTMLDocument2* pdoc2;//suppose this is a valid pointer
pdoc2->QueryService(IID_IWebBrowserApp,IID_IWebBrowser2, (void**)&pweb);

拼合算法又要优化,不过这里就不赘述啦。最后是存图。

据说"extended capture" on WinXP有类似的作用,可惜没用过。这也是我为什么再前面的blog里面说想看shell/scrnsave/的代码。

听说有的软件还有Excel,Word什么的截图功能,我想也是用COM打入Office的band,然后用VBS动作吧?不甚清楚,估计应该如此。

对于任意窗口我们当然也可以发WM_PAGEUP,WM_PAGEDOWN让他滚屏,不过可截窗口大小怎么定不太明白,用API注入?呵呵,不多胡坎啦。

今天看到sam1111在blogcn也有自己的页面,还有我这里的link,呵呵,第三个。

补充:
程序终于有时间写完了,呵呵,真的很简单。不过下面的资料很有用(当然也是自己快把HTML给忘了的缘故)
http://www.quirksmode.org/viewport/compatibility.html
还有系统工具条是mHandle=FindWindow('Shell_TrayWnd',')
4/5/2004修改

补充:
刚刚发现,最方便的方法是这样的
Programmatically scrolling WebBrowser control from Visual C/C++
By Valters Vingolds
http://www.codeproject.com/miscctrl/scrollbrowser.asp

//
// All this code does is what
// "m_browser.Document.Body.ScrollTop = 100;"
// does in VB. Gotta love COM in C++.
//

// let's say m_browser is the WebBrowser's member variable.

HRESULT hr;

// get the document dispatch from browser
IDispatch *pDisp = m_browser.GetDocument();
ASSERT( pDisp ); //if NULL, we failed

// get document interface
IHTMLDocument2 *pDocument = NULL;
hr = pDisp->QueryInterface( IID_IHTMLDocument2, (void**)&pDocument );
ASSERT( SUCCEEDED( hr ) );
ASSERT( pDocument );

//
// this is the trick!
// take the body element from document...
//
IHTMLElement *pBody = NULL;
hr = pDocument->get_body( &pBody );
ASSERT( SUCCEEDED( hr ) );
ASSERT( pBody );

// from body we can get element2 interface,
// which allows us to do scrolling
IHTMLElement2 *pElement = NULL;
hr = pBody->QueryInterface(IID_IHTMLElement2,(void**)&pElement);
ASSERT(SUCCEEDED(hr));
ASSERT( pElement );

// now we are ready to scroll
// scroll down to 100th pixel from top
pElement->put_scrollTop( 100 );

// try to get the whole page size - but the returned number
// is not allways correct. especially with pages that use dynamic html
// tricks...
long scroll_height;
pElement->get_scrollHeight( &s );

// we can use this workaround!
long real_scroll_height;
pElement->put_scrollTop( 20000000 ); // ask to scroll really far down...
pElement->get_scrollTop( &real_scroll_height );
real_scroll_height += window_height; // will return the scroll height
// for the first visible pixel, to get whole html page size must
// add the window's height... (to obtain window_height is
// left as an exercise for the reader)


// print to debug output
TRACE( "real scroll height: %ld, get_scrollHeight: %ld\n",
real_scroll_height, scroll_height );

哈哈,继续改写程序吧

Rob Manderson, Jubjub 新鲜出炉的方法
http://www.codeproject.com/internet/htmlimagecapture.asp
核心代码如下

[code]
BOOL CCreateHTMLImage::CreateImage(
IHTMLDocument2 *pDoc,
LPCTSTR szDestFilename,
CSize srcSize,
CSize outputSize)
{
USES_CONVERSION;
ASSERT(szDestFilename);
ASSERT(AfxIsValidString(szDestFilename));
ASSERT(pDoc);

// Get our interfaces before we create anything else
IHTMLElement *pElement = (IHTMLElement *) NULL;
IHTMLElementRender *pRender = (IHTMLElementRender *) NULL;

// Let's be paranoid...
if (pDoc == (IHTMLElement *) NULL
return FALSE;

pDoc->get_body(&pElement);

if (pElement == (IHTMLElement *) NULL)
return FALSE;

pElement->QueryInterface(IID_IHTMLElementRender, (void **) &pRender);

if (pRender == (IHTMLElementRender *) NULL)
return FALSE;

CFileSpec fsDest(szDestFilename);
CBitmapDC destDC(srcSize.cx, srcSize.cy);

pRender->DrawToDC(destDC);

CBitmap *pBM = destDC.Close();

Bitmap *gdiBMP = Bitmap::FromHBITMAP(HBITMAP(pBM->GetSafeHandle()), NULL);
Image *gdiThumb = gdiBMP->GetimageImage(outputSize.cx, outputSize.cy);

gdiThumb->Save(T2W(fsDest.GetFullSpec()), &m_encoderClsid);
delete gdiBMP;
delete gdiThumb;
delete pBM;
return TRUE;
}
[/code]

可惜还是不能滚屏。

另外,怎样找到当前是拿一个具体的Element呢?IHTMLElement2的clientHeight和clientLeft都可以用,然后反向Mapping,重载IE的鼠标事件即可。简单吧!我的程序还没有正式更新,现在立刻改进吧。

IE 抓图的其它方法如下

shifty_mc 在 http://www.codeproject.com/internet/htmlimagecapture.asp#xx784442xx
提供了用PRINT消息抓图的方法。
PRINT消息抓图的具体分析可以参见4/14-15/2004的blog中转载的袁峰大侠的精彩文章,不过这里shifty_mc 的方法是C#下的port

public void GrabWindow()
{
IntPtr hWnd = this.Handle;
IntPtr hBmp;
Bitmap MyBitmap=null;

IntPtr hDCMem = CreateCompatibleDC((IntPtr)null);
Rectangle rect = new Rectangle(0,0,783,583); //783 and 583 to avoid scrollbars
IntPtr hDC = GetWindowDC(hWnd);
hBmp = CreateCompatibleBitmap(hDC, rect.Width, rect.Height);
ReleaseDC(hWnd, hDC);
IntPtr hOld = Win32.SelectObject(hDCMem, hBmp);
SendMessage(hWnd, WM_PRINT, hDCMem, new IntPtr(PRF_CLIENT PRF_CHILDREN));
SelectObject(hDCMem, hOld);
DeleteObject(hDCMem);
MyBitmap = Bitmap.FromHbitmap(hBmp);

MyBitmap.Save("test.bmp");
}

I was a bit wrong with my previous statement that scrollbars are eliminated - must have been half asleep, but to achieve the same effect I just reduce the size of the captured area by 17px (The browser is set up to be 800 by 600) so maybe it's not that great a way after all.
Pretty much all the methods came from dllimports (you have to use the namespace System.Runtime.InteropServices)
eg [DllImport("User32.dll")]
public static extern int SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
and the constants (eg PRF_CLIENT) defined as their hexadecimal equivalents.

The rest is just a matter of calling this method when a DocumentComplete event returns the url Navigate2 is given. I also use the event Application.Idle to ensure it's ready, so it's not that nice code and a bit hacky maybe, but hope it's helpful.

有趣,但是还得自己滚屏

另外,还有martyCZ 提供的OLE方法
http://www.codeproject.com/internet/htmlimagecapture.asp#xx783713xx


this is code fragment from my application, hope it can be useful for someone:

//
// using these variables of specified types:
//
// CWebBrowser www;
// HDC dc
//
// please init "dc" and "www", www does not need to be visible
//

IDispatch* doc = NULL;
if (doc = www.GetDocument()) {
IViewObject* obj = NULL;
doc->QueryInterface(IID_IViewObject, (void**)&obj);

if (obj) {
CRect rect;

// init rect in some way
www.GetWindowRect(rect);
www.ScreenToClient(rect);

HRESULT os = OleDraw(obj, DVASPECT_CONTENT, dc, rect);

if (os == S_OK) TRACE("Object was successfully drawn.");
if (os == OLE_E_BLANK) TRACE("No data to draw from.");
if (os == E_ABORT) TRACE("The draw operation was aborted.");
if (os == VIEW_E_DRAW) TRACE("An error occurred in drawing.");
if (os == OLE_E_INVALIDRECT) TRACE("The rectangle is invalid.");
if (os == DV_E_NOIVIEWOBJECT) TRACE("The object doesn't support the IViewObject interface.");

obj->Release();
}

doc->Release();
}


这个适合用在CWebBrowser控件的抓图上,但也还得自己滚屏

12/30/2004补充
By 鸟食轩
From http://www.cnblogs.com/birdshome/archive/2005/01/04/86507.html
http://www.cnblogs.com/birdshome/archive/2005/01/06/87667.html
http://www.cnblogs.com/birdshome/archive/2005/01/10/88454.html
http://www.cnblogs.com/birdshome/archive/2005/01/11/89753.html

如果试图单独抓取每个IHTMLElement,则必须要取得HTML元素在页面中的绝对位置(就是相对于浏览器的左上角坐标(0,0))。

这个位置的获取其实并不难,由于每个Html元素提供了一组和位置相关的属性,他们是: offsetLeft、offsetTop和offsetParent,还有两个带offset叫offsetWidth和offsetHeight,不过这俩和我们要说的取Html元素绝对位置没有太大关系。

属性offsetLeft和offsetTop就是Html元素相对于自己的offsetParent元素的位置,所以我们一般使用如下代码获得指定元素的绝对位置:

function GetAbsoluteLocation(element)
{
if ( arguments.length != 1 element == null )
{
return null;
}
var offsetTop = element.offsetTop;
var offsetLeft = element.offsetLeft;
var offsetWidth = element.offsetWidth;
var offsetHeight = element.offsetHeight;
while( element = element.offsetParent )
{
offsetTop += element.offsetTop;
offsetLeft += element.offsetLeft;
}
return { absoluteTop: offsetTop, absoluteLeft: offsetLeft,
offsetWidth: offsetWidth, offsetHeight: offsetHeight };
}
这个代码及其类似代码其实是比较常见,可是使用它来计算复杂的页面布局却会有问题,有什么问题呢?这需要了解了Web页中的HTML元素的排版布局规则后,才能很好的理解。

我们知道每个Web页面都是由一大堆的HTML元素组成的,我们把每对element>.../element>这样的结构称为box,在Web页面的排版布局中,浏览器把这样的box作为排版的元素,并且把box分为了inline level和block level两种类型。

当然这个box内是可以容纳很多其它的HTML标签的,但是不管它的内部有多少的TAGs,box都被我们看成是一个排版元素,比如:div style="border: solid 1px blue"> abc /div>和table> tr> td> span> i> abc /i> /span> /td> /tr> /table>就可以被看成是两个排版元素div和table(当然div和table的内部还可以继续细分子的box)。

在browser的默认排版策略(没有任何的CSS修饰)中,box的inline和block分别指的是:

Inline Level:元素按从左向右排列,就像我们输入文字一样,一行容纳不下了自动分行继续显示。比如text、a>、img>、span>等都属于inline element(除了30个block level的TAGs,和几个none的TAGs,其它大多数的标签都是inline level的;

Block Level:相对于它的parentElement构成的box来说,它的排版始终会独自占一行,就是在block level的元素后必然会新起一行。比如form>、hr>、div>、table>、p>等30个TAGs都属于block element。

在大多数的情况下,虽然我们完全可以在inline level的元素中嵌套block level的元素,可是这样会对它们的显示效果带来一些混淆和不确定。比如div>正常情况下是单独占一行的,可是我们却可以使用一个inline level的元素span>把div>包裹起来,这时这个span>div>.../span>构成的box排版属性仍是inline的(e.g.

span&div

)。可是如果我们用span>把table>包裹起来,这时这个span>table>.../span>构成的box的排版属性却成了block的了(e.g. span&table
)。

其实inline和block直观的表现就是,比如a>link1 /a>text1 a>link2 /a>text2 ...这样的HTML在browser里是可以显示一行上(preview:link1 text1 link2 text2 ...),而table> tr> td> text1 /td> /tr> /table> table> tr> td> text2 /td> /tr> /table> ...是不能显示在一行上的(preview: text1
text2
)。

以上是browser对于box的默认的排版策略,而我们可以通过css来改变这些默认的策略。使用css中提供的position(配合top,left)、float和clear三个属性就可以实现用户定制Web页中元素的排版布局策略。

我们简述了browser是以怎样的策略来排版布局的,但很多时候默认的排版却不能完全满足我们的需要,所以我们还需要靠自己来定制Web页中HTML元素的排版布局策略。
我们可以使用这些下css属性来定制页面的显示效果,它们是:clear、float、clip、overflow(又可分别分为overflow-x和overflow-y)、display和visibility。不过属性clipoverflowvisibility不是我们关心的重点,因为它们虽然影响页面的最终显示的效果,可是它们却不能影响browser里元素的布局规则。 我们可以查看msdn看看clearfloatdisplay的详细含义,简单说一下呢。这三个css属性都是影响我们在'规则'一文中说道的HTML元素的inline-level和block-level问题的。其中clear和float是相对的两个属性,clear:none是默认值,允许元素两边都有inline-level的box存在;clear:left,不允许元素左边有inline-level的box存在;clear:right,不允许元素右边有inline-level的box存在;clear:both,不允许有inline-level的box存在于一行。float:none是默认值,元素不漂浮;float:left,元素漂浮于对象布局流的右边,float:right,元素飘浮于对象布局流的左边。
属性display看起来比较麻烦,因为它有很多的取值,可是实际上我们可以简单的把display属性看成是用来定义box的level方式的,到底是inline的还是block的。比如我们知道div默认是block-level的,我们可以使用div style="display:inline">.../div>,它就变成inline-level的了,同时它也就遵循inline-level的排版布局策略了。display属性着重的是描述元素的render方式,所以当我们使用display:none时,元素将完全的消失掉,就和html代码中没有这个元素的显示效果一样(当然元素仍然在DHTML树中,可以使用脚本取到)。顺便插一句,display:none和visibility:hidden的区别,元素如果设置了属性后者,虽然也是不会再显示出来了,可是该元素的物理位置却是被browser保留了的,页面中将会显示一个和元素bounds一样的空白区域。
布局说完了,再来说一下元素的定位问题,定位是由属性:position、top(还有left、right、bottom,下面简称为TLRB)和z-index来控制的。其中TLRB四个属性是依赖于position的取值而起作用的,position取值为static、absolute和relative。如果postion取static,TLRB将不会起任何的定位作用;position取absolute,TLRB将把其所在的viewport(下面有解释)的左上角作为top和left的(0,0)起点,由此来定位元素;position取relative,TLRB将把元素本来布局流中的位置的左上角坐标作为top和left的(0,0)起点,并由此来定位元素。比如代码:

div id="div1" style="border: solid 1px blue; width: 200; height: 200; position: absolute;
top: 50; left: 50">
div id="div2" style="border: solid 1px green; width: 100; height: 100; position: absolute;
top: 25; left: 25">
/div>
/div>
将显示为:

上面说到的viewport是什么呢?在这个示例中,对于容器元素div来说,div1圈起来的蓝色区域就是div2的viewport,所以div2的position虽然是absolute,但是它的top&left(25,25)却不是相对于上图中的(0,0)。所以在viewport中定位元素时,要仔细却别于position为relative时的情况,虽然代码:

div id="div1" style="border: solid 1px blue; width: 200; height: 200; position: absolute;
top: 50; left: 50">
div id="div2" style="border: solid 1px green; width: 100; height: 100; position: relative;
top: 25; left: 25">
/div>
/div>
的显示效果和上图相同,但是元素的定位原理却是不同的。

当我们的元素所处的viewport不是body>的时候,其定位是从自己的viewport的(0,0)开始计算的。所以为了避免出错我们需要找到被计算绝对位置的元素的viewport,然后把它的offsetTop和offsetLeft累计到其viewport的(0,0)处为止。

当我们需要计算的元素的offsetPerent满足这个条件:elmt.style.position == 'absolute' || elmt.style.position == 'relative' || ( elmt.style.overflow != 'visible' && elmt.style.overflow != '' ),它将是一个viewport,也就是说会影响绝对定位的计算,我们应该在此停止offset的累加。

更新过的方法叫GetAbsoluteLocationEx,代码附后:

function GetAbsoluteLocationEx(element)
{
if ( arguments.length != 1 || element == null )
{
return null;
}
var elmt = element;
var offsetTop = elmt.offsetTop;
var offsetLeft = elmt.offsetLeft;
var offsetWidth = elmt.offsetWidth;
var offsetHeight = elmt.offsetHeight;
while( elmt = elmt.offsetParent )
{
// add this judge
if ( elmt.style.position == 'absolute' || elmt.style.position == 'relative'
|| ( elmt.style.overflow != 'visible' && elmt.style.overflow != '' ) )
{
break;
}
offsetTop += elmt.offsetTop;
offsetLeft += elmt.offsetLeft;
}
return { absoluteTop: offsetTop, absoluteLeft: offsetLeft,
offsetWidth: offsetWidth, offsetHeight: offsetHeight };
}
如果offsetParent没有设置position和overflow属性,GetAbsoluteLocation和GetAbsoluteLocationEx的计算结果完全相同的,也就是说GetAbsoluteLocationEx向下兼容GetAbsoluteLocation。

注:所有示例都只针对IE6.0sp1。

6/20/2005补充
Image Capture Whole Web Page using C#
By Douglas M. Weems

Capture whole web pages as a single image using C#.

http://www.codeproject.com/cs/media/IECapture.asp

(note: just as what I did above, but in C#)

A different way to capture the whole page Keith Iveson 15:20 28 Jun '05

Hi Douglas,

Nice article.

I wrote an add-on for an application called Desktop Sidebar[^] to do the exact same thing. The source code can be found here[^]

The basics of the page capture were much as you described - with the exception of the capture and joining of the parts of the web page that were off-screen. For this I used Ole32.OleDraw which allowed me to capture everything in one go - which obviously made things much simpler. You are welcome to look at the code and incorporate it into yours if you wish.

Regards,

Keith

补充 5/30/2005
精确地计算Web页面中滚动条的宽度

http://birdshome.cnblogs.com/archive/2005/07/02/184928.html

原来我一直以为Web页面中的滚动条宽度是不能精确确定的,因为用户自己可以在桌面属性中设置系统滚动条的宽度为任意整数。再加之上次在MyMsn的代码里看见M$程序员的注释,更加让我认为滚动条的宽度是不能精确计算地。但事实是怎么样的呢?

实际上对于HTML里面的容器元素,它们的长、宽之间存在这样的运算关系:
width = border-left-width + clientWidth + border-right-width;
height = border-top-width + clientHeight + border-bottom-width;

但是当容器内出现滚动条后,这个长、宽运算关系将变为:
width = border-left-width + clientWidth + scrollbar-width + border-right-width;
height = border-top-width + clientHeight + scrollbar-width + border-bottom-width;

下面是一个DIV的示例:style="margin: 25px; padding: 25px; width: 200px; height: 200px; border: solid 25px blue; background-color: yellow; overflow: scroll;"。

DIV: clientWidth: 130
DIV: offsetWidth: 200

上面示例中的scrollbar-width为:offsetWidth - borderLeftWidth - borderRightWidth - clientWidth = 200px - 25px - 25px - 130px = 20px。



<< Home

This page is powered by Blogger. Isn't yours?