Attempting to render web browser control using IViewObject :: Draw () in HDC crashes with IE8 but with IE11

I have an MFC dialog box in which I added a WebBrowser control (which encapsulates the Internet Explorer engine.)

The purpose of the following code is to make the contents of the specified web browser control into a device context , which I can subsequently use for printing:

 //MFC code, error checks are omitted for brevity //'m_browser' = is a web browser control of type `CExplorer1` IDispatch* pHtmlDoc = m_browser.get_Document(); CComPtr<IHTMLDocument2> pHtmlDocument2; pHtmlDoc->QueryInterface(IID_IHTMLDocument2, (void**)&pHtmlDocument2)); //Get IViewObject2 for the entire document that we will use to render into a DC CComPtr<IViewObject2> pViewObject; pHtmlDocument2->QueryInterface(IID_IViewObject2, (void **)&pViewObject)); CComPtr<IHTMLElement> pBody; pHtmlDocument2->get_body(&pBody)); CComPtr<IHTMLElement2> pBody2; pBody->QueryInterface(IID_IHTMLElement2, (void **)&pBody2)); //Get default printer DC CPrintDialog pd(TRUE, PD_ALLPAGES | PD_USEDEVMODECOPIES | PD_NOPAGENUMS | PD_HIDEPRINTTOFILE | PD_NOSELECTION); pd.m_pd.Flags |= PD_RETURNDC | PD_RETURNDEFAULT; pd.DoModal(); //corrected later HDC hPrintDC = pd.CreatePrinterDC(); //Calc bitmap width based on printer DC specs //Note that this width will be larger than //the width of the WebControl window itself due to //printer much higher DPI setting... int n_bitmapWidth = ::GetDeviceCaps(hPrintDC, HORZRES); //Use entire printable area //Get full size of the document long n_scrollWidth; long n_scrollHeight; pBody2->get_scrollWidth(&n_scrollWidth); pBody2->get_scrollHeight(&n_scrollHeight); //Calc proportional size of the bitmap in the DC to render int nWidth = n_bitmapWidth; int nHeight = n_bitmapWidth * n_scrollHeight / n_scrollWidth; //Create memory DC to render into HDC hDc = ::GetDC(hWnd); HDC hCompDc = ::CreateCompatibleDC(hDC); //I'm using a raw DIB section here as I'll need to access //its bitmap bits directly later in my code... BITMAPINFOHEADER infoHeader = {0}; infoHeader.biSize = sizeof(infoHeader); infoHeader.biWidth = nWidth; infoHeader.biHeight = -nHeight; infoHeader.biPlanes = 1; infoHeader.biBitCount = 24; infoHeader.biCompression = BI_RGB; BITMAPINFO info; info.bmiHeader = infoHeader; //Create a bitmap as DIB section of size `nWidth` by `nHeight` pixels BYTE* pMemory = 0; HBITMAP hBitmap = ::CreateDIBSection(hDc, &info, DIB_RGB_COLORS, (void**)&pMemory, 0, 0); HBITMAP hOldBmp = (HBITMAP)::SelectObject(hCompDc, hBitmap); RECT rcAll = {0, 0, nWidth, nHeight}; ::FillRect(hCompDc, &rcAll, (HBRUSH)::GetStockObject(WHITE_BRUSH)); RECTL rectPrnt = {0, 0, nWidth, nHeight}; //Do the upscaling & render -- note that IE8 fails to render it here!!!! pViewObject->Draw(DVASPECT_CONTENT, //DVASPECT_DOCPRINT -1, NULL, NULL, NULL, hCompDc, &rectPrnt, NULL, NULL, 0)); ::SelectObject(hCompDc, hOldBmp); //Now the bitmap in `hCompDc` contains the resulting pixels //For debugging purposes, save it as .bmp file BITMAPFILEHEADER fileHeader = {0}; fileHeader.bfType = 0x4d42; fileHeader.bfSize = 0; fileHeader.bfReserved1 = 0; fileHeader.bfReserved2 = 0; fileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER); CFile file( L"path-to\\test.bmp", CFile::modeCreate | CFile::modeReadWrite | CFile::shareDenyNone); file.Write((char*)&fileHeader, sizeof(fileHeader)); file.Write((char*)&infoHeader, sizeof(infoHeader)); int bytes = (((24 * nWidth + 31) & (~31)) / 8) * nHeight; file.Write(pMemory, bytes); //Clean up ::DeleteObject(hBitmap); ::DeleteDC(hCompDc); ::ReleaseDC(hWnd, hDc); ::DeleteDC(hPrintDC); 

This code works fine if I have the latest IE11 installed on my development machine. But if, for example, someone has IE8 installed on their Windows 7, the method enter image description here

and here is what happens with IE8 :

enter image description here

Does anyone know what I'm doing wrong here that IE8 doesn't like?

EDIT1:. A few more digging into IViewObject::Draw using WinDbg , and then found the source code for it. Here is CServer :: Draw () , which is IViewObject::Draw , and then CDoc :: Draw () , which is called inside CServer::Draw() .

+5
source share
2 answers

Firstly, thanks for the interesting question. Although it is not so practical - today many people use IE8 - it was not so difficult. I will describe what the problem is and provide a simplified but working solution that you can improve.


Before moving on to the IE8 solution, a few points:

  • A solution with a window size with a scroll size is unstable if you can run into large documents. If you don't know what to expect, you also need to reorganize the solution for later researchers to avoid using the resize window to scroll.

  • Why transfer a giant bitmap? Metaphiles, etc. Could fit better. Any sufficiently large page with this resolution is going to explode the memory on a PC with the naive creation of DIB. The Google page in the presented sample displays a 100 MB raster file, while the emf from which rasterization is performed takes less than 1 MB.

  • While I do not know the exact requirements and limitations of your project, I am 99% sure that drawing in a giant DIB is not the best solution. Even EMF, although better, is not the best either. If you need to, for example, add a signature and then print, there are better ways to handle this. This, of course, is just a note, not related to the question itself.


IE8 rendering problem

There is an error in IE8 renderer. Draw () will be cropped in the pixel dimensions of the actual display area (the visible rectangle that you see is the original display area in the scale of the rendering context).

So, if you have a scaled target, the size of which is larger than the actual one, then when scaling it will be cropped to the size in scaled pixels anyway (therefore, it has much less content than the original rectangle).

If this is not a clip for someone in genuine IE8, then the system has the remnants of a later IE, or there is another setting without scratches, a system update, or the like.

Workarounds

Good news can be circumvented; bad news workarounds are a bit unpleasant.

First, you can still get along with IViewObject. But since there is arbitrary scaling, and the available source rectangle is very small, this solution has some difficulties, which I think are SO's answer. Therefore, I would not plunge into this path.

Instead, we can run another, deprecated API: IHTMLElementRender . This allows you to display the page using DrawToDC in any context. Unfortunately, this is not as simple as it might seem, and goes beyond the simple context of the device.

Firstly, there is a similar clipping error. It is easier to handle because clipping occurs at large values ​​outside the screen size. Secondly, when using device context transformations, it either will not work or it will ruin the displayed html, so you cannot actually rely on scale or translate. Both problems require relatively non-trivial processing and complicate each other.

Decision

I will describe and provide sample code for a suboptimal but working on most simple pages. In general, an ideal and more effective solution can be achieved, but, again, this goes beyond the answer. Obviously, this is only IE8, so you need to check the browser version and run different handlers for IE8 compared to IE9 or higher, but you can take some ideas to improve the display of other browsers.

There are two interlocking workarounds here.

Upscaling

First, how do we scale vector content to printer quality if we cannot convert it? The workaround here is to render in a dc compatible printer environment. What will happen is that the content will be displayed in the DPI of the printer. Please note that it will not exactly match the width of the printer; it will scale to the printer DPI / screenDPI.

Later, when rasterized, we zoom out to fit the width of the printer. First, we visualize EMF, so there is a loss of quality (which will not happen on the printer itself). If you need higher quality (I doubt it), there are two possibilities: change the rendering solution for the target width (this is not trivial) or work with the resulting emf instead of a bitmap, and also allow the printer to make a reduced size.

Another remark is that at the moment you are only using the width of the printer, but there may be non-printable fields that you need to request from the printer and the account for them. Therefore, the printer can be scaled by the printer even if you provide a bitmap in the exact size of the printer. But then again, I doubt that this inconsistency of the resolution will be relevant to your project.

Clipping

The second to overcome is clipping. To overcome this limitation, we process the content in small pieces so that they are not clipped by the renderer. After rendering the fragment, we change the scroll position of the document and output the next fragment to the corresponding position in the target DC. This can be optimized to use large chunks, for example. the closest DPI to 1024 (using window resizing), but I haven't implemented this (it's just speed optimization). If you do not do this optimization, make sure that the minimum browser window size is not too small.

Please note: performing this scroll on an arbitrary fractional scale will be approximate and not so easy to implement in the general case. But with a regular printer and screen, we can perform whole block steps in DPI multiplications, for example. if the screen is 96 DPI and the printer is 600DPI, we take steps in the same multiple of 96 and 600 in each context, and everything is much simpler. However, the rest of the top or bottom after processing all the whole pieces will not be multiplied by DPI, so we can not scroll it as easily.

In general, we can approximate the scroll position in the printer space and hope that there will be no discrepancy between the final pieces. Instead, I add an absolutely positioned div with the block size at the bottom right of the page.

Please note that this may interfere with some pages and change the layout (perhaps not in the case of simple reports). If this is a problem, you need to add handling of the remainder after the loops instead of adding an element. The simplest solution in this case, as before, is to fill in the div, but not with the full size of the chunk, but just to make the width of the contents of the multiple DPI screen.

An even simpler idea, as I understood later, is simply to resize the window to the nearest multiple DPI and accept the size of this window as the block size. You can try this instead of a div, this will simplify the code and fix pages that might interfere with the nested div.

Code

This is just a sample.

  • No error handling. You need to add checks for each COM and API call, etc.

  • No code style, just fast and dirty.

  • Not sure if all purchased resources will be released as needed, complete your checks.

  • You must disable the page borders in the browser control in order for this pattern to work (if you need borders around the browser, just visualize them separately, the built-in ones are not consistent anyway). In IE8 this is not so trivial, but there are many answers here or on the Internet. In any case, I will include this patch in the draft sample solution. You can visualize with borders and exclude them, but this will be an unnecessary complication for a problem that has a simple solution.

  • The complete draft decision can be found on this link , here I have posted only the corresponding code.

The code below displays the page and saves to c: \ temp \ test.emf + c: \ temp \ test.bmp

 void convertEmfToBitmap(const RECT& fitRect, HDC hTargetDC, HENHMETAFILE hMetafile, LPCTSTR fileName); CComPtr<IHTMLDOMNode> appendPadElement(IHTMLDocument2* pDoc, IHTMLElement* pBody, long left, long top, long width, long height); void removeElement(IHTMLElement* pParent, IHTMLDOMNode* pChild); void CMFCApplication1Dlg::OnBnClickedButton2() { COleVariant varNull; COleVariant varUrl = L"http://www.google.com/search?q=ie+8+must+die"; m_browser.Navigate2(varUrl, varNull, varNull, varNull, varNull); } void CMFCApplication1Dlg::OnBnClickedButton1() { //get html interfaces IDispatch* pHtmlDoc = m_browser.get_Document(); CComPtr<IHTMLDocument2> pHtmlDocument2; pHtmlDoc->QueryInterface(IID_IHTMLDocument2, (void**)&pHtmlDocument2); CComPtr<IHTMLElement> pBody; pHtmlDocument2->get_body(&pBody); CComPtr<IHTMLElement2> pBody2; pBody->QueryInterface(IID_IHTMLElement2, (void**)&pBody2); CComPtr<IHTMLBodyElement> pBodyElement; pBody->QueryInterface(IID_IHTMLBodyElement, (void**)&pBodyElement); CComPtr<IHTMLElement> pHtml; pBody->get_parentElement(&pHtml); CComPtr<IHTMLElement2> pHtml2; pHtml->QueryInterface(IID_IHTMLElement2, (void**)&pHtml2); CComPtr<IHTMLStyle> pHtmlStyle; pHtml->get_style(&pHtmlStyle); CComPtr<IHTMLStyle> pBodyStyle; pBody->get_style(&pBodyStyle); //get screen info HDC hWndDc = ::GetDC(m_hWnd); const int wndLogPx = GetDeviceCaps(hWndDc, LOGPIXELSX); const int wndLogPy = GetDeviceCaps(hWndDc, LOGPIXELSY); //keep current values SIZE keptBrowserSize = { m_browser.get_Width(), m_browser.get_Height() }; SIZE keptScrollPos; //set reasonable viewport size //m_browser.put_Width(docSize.cx); //m_browser.put_Height(docSize.cy*2); pHtml2->get_scrollLeft(&keptScrollPos.cx); pHtml2->get_scrollTop(&keptScrollPos.cy); COleVariant keptOverflow; pBodyStyle->get_overflow(&keptOverflow.bstrVal); //setup style and hide scroll bars pHtmlStyle->put_border(L"0px;"); pHtmlStyle->put_overflow(L"hidden"); pBodyStyle->put_border(L"0px;"); pBodyStyle->put_overflow(L"hidden"); //get document size and visible area in screen pixels SIZE docSize; pBody2->get_scrollWidth(&docSize.cx); pBody2->get_scrollHeight(&docSize.cy); RECT clientRect = { 0 }; pHtml2->get_clientWidth(&clientRect.right); pHtml2->get_clientHeight(&clientRect.bottom); //derive chunk size const SIZE clientChunkSize = { clientRect.right - clientRect.right % wndLogPx, clientRect.bottom - clientRect.bottom % wndLogPy }; //pad with absolutely positioned element to have enough scroll area for all chunks //alternatively, browser can be resized to chunk multiplies (simplest), to DPI multiplies (more work). //This pad also can be made smaller, to modulus DPI, but then need more work in the loops below CComPtr<IHTMLDOMNode> pPadNode = appendPadElement(pHtmlDocument2, pBody, docSize.cx, docSize.cy, clientChunkSize.cx, clientChunkSize.cy); //get printer info CPrintDialog pd(TRUE, PD_ALLPAGES | PD_USEDEVMODECOPIES | PD_NOPAGENUMS | PD_HIDEPRINTTOFILE | PD_NOSELECTION); pd.m_pd.Flags |= PD_RETURNDC | PD_RETURNDEFAULT; pd.DoModal(); HDC hPrintDC = pd.CreatePrinterDC(); const int printLogPx = GetDeviceCaps(hPrintDC, LOGPIXELSX); const int printLogPy = GetDeviceCaps(hPrintDC, LOGPIXELSY); const int printHorRes = ::GetDeviceCaps(hPrintDC, HORZRES); const SIZE printChunkSize = { printLogPx * clientChunkSize.cx / wndLogPx, printLogPy * clientChunkSize.cy / wndLogPy }; //browser total unscaled print area in printer pixel space const RECT printRectPx = { 0, 0, docSize.cx* printLogPx / wndLogPx, docSize.cy*printLogPy / wndLogPy }; //unscaled target EMF size in 0.01 mm with printer resolution const RECT outRect001Mm = { 0, 0, 2540 * docSize.cx / wndLogPx, 2540 * docSize.cy / wndLogPy }; HDC hMetaDC = CreateEnhMetaFile(hPrintDC, L"c:\\temp\\test.emf", &outRect001Mm, NULL); ::FillRect(hMetaDC, &printRectPx, (HBRUSH)::GetStockObject(BLACK_BRUSH)); //unscaled chunk EMF size in pixels with printer resolution const RECT chunkRectPx = { 0, 0, printChunkSize.cx, printChunkSize.cy }; //unscaled chunk EMF size in 0.01 mm with printer resolution const RECT chunkRect001Mm = { 0, 0, 2540 * clientChunkSize.cx / wndLogPx, 2540 * clientChunkSize.cy / wndLogPy }; //////// //render page content to metafile by small chunks //get renderer interface CComPtr<IHTMLElementRender> pRender; pHtml->QueryInterface(IID_IHTMLElementRender, (void**)&pRender); COleVariant printName = L"EMF"; pRender->SetDocumentPrinter(printName.bstrVal, hMetaDC); //current positions and target area RECT chunkDestRectPx = { 0, 0, printChunkSize.cx, printChunkSize.cy }; POINT clientPos = { 0, 0 }; POINT printPos = { 0, 0 }; //loop over chunks left to right top to bottom until scroll area is completely covered const SIZE lastScroll = { docSize.cx, docSize.cy}; while (clientPos.y < lastScroll.cy) { while (clientPos.x < lastScroll.cx) { //update horizontal scroll position and set target area pHtml2->put_scrollLeft(clientPos.x); chunkDestRectPx.left = printPos.x; chunkDestRectPx.right = printPos.x + printChunkSize.cx; //render to new emf, can be optimized to avoid recreation HDC hChunkDC = CreateEnhMetaFile(hPrintDC, NULL, &chunkRect001Mm, NULL); ::FillRect(hChunkDC, &chunkRectPx, (HBRUSH)::GetStockObject(WHITE_BRUSH)); pRender->DrawToDC(hChunkDC); HENHMETAFILE hChunkMetafile = CloseEnhMetaFile(hChunkDC); //copy chunk to the main metafile PlayEnhMetaFile(hMetaDC, hChunkMetafile, &chunkDestRectPx); DeleteEnhMetaFile(hChunkMetafile); //update horizontal positions clientPos.x += clientChunkSize.cx; printPos.x += printChunkSize.cx; } //reset horizontal positions clientPos.x = 0; printPos.x = 0; //update vertical positions clientPos.y += clientChunkSize.cy; printPos.y += printChunkSize.cy; pHtml2->put_scrollTop(clientPos.y); chunkDestRectPx.top = printPos.y; chunkDestRectPx.bottom = printPos.y + printChunkSize.cy; } //restore changed values on browser //if for large pages on slow PC you get content scrolling during rendering and it is a problem, //you can either hide the browser and show "working" or place on top first chunk content pBodyStyle->put_overflow(keptOverflow.bstrVal); pHtml2->put_scrollLeft(keptScrollPos.cx); pHtml2->put_scrollTop(keptScrollPos.cy); m_browser.put_Width(keptBrowserSize.cx); m_browser.put_Height(keptBrowserSize.cy); removeElement(pBody, pPadNode); //draw to bitmap and close metafile HENHMETAFILE hMetafile = CloseEnhMetaFile(hMetaDC); RECT fitRect = { 0, 0, printHorRes, docSize.cy * printHorRes / docSize.cx }; convertEmfToBitmap(fitRect, hWndDc, hMetafile, L"c:\\temp\\test.bmp"); DeleteEnhMetaFile(hMetafile); //cleanup - probably more here ::ReleaseDC(m_hWnd, hWndDc); ::DeleteDC(hPrintDC); //{ // std::stringstream ss; // ss << "====" << docSize.cx << "x" << docSize.cy << " -> " << fitRect.right << "x" << fitRect.bottom << "" << "\n"; // OutputDebugStringA(ss.str().c_str()); //} } /////////////// ////some util void convertEmfToBitmap(const RECT& fitRect, HDC hTargetDC, HENHMETAFILE hMetafile, LPCTSTR fileName) { //Create memory DC to render into HDC hCompDc = ::CreateCompatibleDC(hTargetDC); //NOTE this BITMAPINFOHEADER infoHeader = { 0 }; infoHeader.biSize = sizeof(infoHeader); infoHeader.biWidth = fitRect.right; infoHeader.biHeight = -fitRect.bottom; infoHeader.biPlanes = 1; infoHeader.biBitCount = 24; infoHeader.biCompression = BI_RGB; BITMAPINFO info; info.bmiHeader = infoHeader; //create bitmap BYTE* pMemory = 0; HBITMAP hBitmap = ::CreateDIBSection(hCompDc, &info, DIB_RGB_COLORS, (void**)&pMemory, 0, 0); HBITMAP hOldBmp = (HBITMAP)::SelectObject(hCompDc, hBitmap); PlayEnhMetaFile(hCompDc, hMetafile, &fitRect); BITMAPFILEHEADER fileHeader = { 0 }; fileHeader.bfType = 0x4d42; fileHeader.bfSize = 0; fileHeader.bfReserved1 = 0; fileHeader.bfReserved2 = 0; fileHeader.bfOffBits = sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFOHEADER); CFile file( fileName, CFile::modeCreate | CFile::modeReadWrite | CFile::shareDenyNone); file.Write((char*)&fileHeader, sizeof(fileHeader)); file.Write((char*)&infoHeader, sizeof(infoHeader)); int bytes = (((24 * infoHeader.biWidth + 31) & (~31)) / 8) * abs(infoHeader.biHeight); file.Write(pMemory, bytes); ::SelectObject(hCompDc, hOldBmp); //Clean up if (hBitmap) ::DeleteObject(hBitmap); ::DeleteDC(hCompDc); } CComPtr<IHTMLDOMNode> appendPadElement(IHTMLDocument2* pDoc, IHTMLElement* pBody, long left, long top, long width, long height) { CComPtr<IHTMLElement> pPadElement; pDoc->createElement(L"DIV", &pPadElement); CComPtr<IHTMLStyle> pPadStyle; pPadElement->get_style(&pPadStyle); CComPtr<IHTMLStyle2> pPadStyle2; pPadStyle->QueryInterface(IID_IHTMLStyle2, (void**)&pPadStyle2); pPadStyle2->put_position(L"absolute"); CComVariant value = width; pPadStyle->put_width(value); value = height; pPadStyle->put_height(value); pPadStyle->put_posLeft((float)left); pPadStyle->put_posTop((float)top); CComPtr<IHTMLDOMNode> pPadNode; pPadElement->QueryInterface(IID_IHTMLDOMNode, (void**)&pPadNode); CComPtr<IHTMLDOMNode> pBodyNode; pBody->QueryInterface(IID_IHTMLDOMNode, (void **)&pBodyNode); pBodyNode->appendChild(pPadNode, NULL); return pPadNode; } void removeElement(IHTMLElement* pParent, IHTMLDOMNode* pChild) { CComPtr<IHTMLDOMNode> pNode; pParent->QueryInterface(IID_IHTMLDOMNode, (void **)&pNode); pNode->removeChild(pChild, NULL); } 

Page Output Example (4958x7656)

Sample output

+5
source

I took your code and ran it in IE11 when the WebBrowser smaller than the page size. He displayed a portion of the page equal to the size of the control. Not sure why you are saying that IE8 and IE11 are different.

It seems that the general approach to taking screenshots on the page allows you to adjust the size of the WebBrowser before taking a screenshot, for example:

 const long oldH = m_browser.get_Height(); const long oldW = m_browser.get_Width(); m_browser.put_Height(n_scrollHeight); m_browser.put_Width(n_scrollWidth); //Do the upscaling & render -- note that IE8 fails to render it here!!!! pViewObject->Draw(DVASPECT_CONTENT, //DVASPECT_DOCPRINT -1, NULL, NULL, NULL, hCompDc, &rectPrnt, NULL, NULL, 0); m_browser.put_Height(oldH); m_browser.put_Width(oldW); 

This works well, even on large pages such as the one you are currently reading (I took a screenshot of 1920x8477). It works on both IE11 and IE8 virtual machines.

It has the side effect of resetting scrollbars, but this can be solved, for example, using an invisible copy of WebBrowser for screenshots.

PS: You could do a better job by providing sample code that can be compiled, at least;)

+1
source

Source: https://habr.com/ru/post/1261777/


All Articles