PicZoom: A Photo Viewer Created in OpenGL
Table of Contents
IntroductionThis article discuss implementation details of BackgroundWhen I started studying OpenGL, I created a simple application to load an image from file and do simple rotation, zoom with it. OpenGL provides APIs to do some simple image operations such as zoom, pan, rotate. Picasa PhotoViewer is a simple and superb image browser tool. In this article, I am just imitating some functionalities of Picasa Photo Viewer with OpenGL and MFC. I’m sure performance of this application is not satisfying with Picasa, but it’s an attempt to do something with OpenGL and MFC. Any comments and improvement suggestions are welcome. Screenshot of PicZoom [With some image operations]. Screenshot of PicZoom in dialog mode. Using the CodeInitially, I did almost all functionalities of When starting The screenshot of PicZoom with semi-transparent desktop as background. How to Capture Screenshot of DesktopJust call bool ScreenCapture::TakeScreenShot()
{
// Get Desktop window.
HWND hDesktop = ::GetDesktopWindow();
// Get temporary Dc for getting data from Desktop.
CDC dc;
HDC hdc = ::GetWindowDC(hDesktop);
dc.Attach(hdc);
CDC MemoryDC;
MemoryDC.CreateCompatibleDC(&dc);
CBitmap BmpObj;
BmpObj.CreateCompatibleBitmap(&dc, sz.cx, sz.cy);
CBitmap * oldbm = MemoryDC.SelectObject(&BmpObj);
// Get data from Desktop to Bitmap.
MemoryDC.BitBlt(0, 0, sz.cx, sz.cy, &dc, 0, 0, SRCCOPY);
// Read RGB data of Desktop window Dc.
int nStatus = ::GetDIBits( MemoryDC.m_hDC, (HBITMAP)BmpObj.m_hObject, 0, sz.cy,
m_pBuffer, &stBitmapInfo,DIB_RGB_COLORS );
} The above figure shows the split up of main classes used in Initially, Functionalities and Implementation of 4 components.All these classes [ 1. BackgroundThis class is responsible to create one texture with desktop image. The RGB buffer with screenshot of desktop is obtained with // Set Pixel Transfer to make semitransparent effect of Desktop.
glPixelTransferf( GL_RED_SCALE, 0.75 );
glPixelTransferf( GL_GREEN_SCALE, 0.75 );
glPixelTransferf( GL_BLUE_SCALE, 0.75 );
// Create Desktop Texture.
if( !m_pImage->Create( m_nWidth, m_nHeight, pbyScreenCapuredData ))
{
AfxMessageBox(L"Texture Creation failed.");
return false;
}
glPixelTransferf( GL_RED_SCALE, 1.0 );
glPixelTransferf( GL_GREEN_SCALE, 1.0 );
glPixelTransferf( GL_BLUE_SCALE, 1.0 ); The Draw() handler just displaying this texture to the e ntire screen. When switching from Desktop mode to Dialog mode, the Background instance is deleted from the WindowList and the background drawing is avoided.bool BackGround::Draw()
{
if( !GLWindowBase::m_bVisible )
{
return true;
}
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
// Here draws the background image.
m_pImage->Enable();
m_pVertexBuffer->DrawVertexBuffer( GL_POLYGON );
return true;
} 2. ImageAreaThe display and all operations (
2. 1. ZoomZoom functionality is simply achieved with OpenGL scale feature. Whenever mouse wheel received in Dialog, Dialog will send it to all components, and therefore bool ImageArea::OnMouseWheel( const int nX_I, const int nY_i,
const UINT nFlags, const short zDelta )
{
float fZoomValue = float( zDelta ) / WHEEL_DELTA;
if( 0 == m_fZoomOffset )
{
::SetTimer( m_hParentWindow, TIMER_ZOOM, 5, 0 );
}
// Find out zoom factor based on width of and height of image.
if( m_nImageWidth > m_nWidth || m_nImageHeight > m_nHeight )
{
float fImageToDesktopRatioX = (float)m_nWidth / m_nImageWidth;
float fImageToDesktopRatioY = (float)m_nHeight / m_nImageHeight;
float fImageToDesktopRatio =
min( fImageToDesktopRatioY, fImageToDesktopRatioX );
fZoomValue = fZoomValue * fImageToDesktopRatio;
}
m_fZoomOffset += ( fZoomValue / 100 );
// Apply Zoom factor 15 times. then first single scroll make 15% zoom.
m_ZoomTimer.SetMaxElapseTime( 15 );
return true;
} Inside 2. 2. Display of Zoom factorWhenever Zoom Factor is changing,
bool FontEngine::Create( HDC hDeviceContext_i )
{
VERIFY(m_pFont->CreateFont(
15, // nHeight
7, // nWidth
0, // nEscapement
0, // nOrientation
FW_BOLD, // nWeight
FALSE, // bItalic
FALSE, // bUnderline
0, // cStrikeOut
ANSI_CHARSET, // nCharSet
OUT_DEFAULT_PRECIS, // nOutPrecision
CLIP_DEFAULT_PRECIS, // nClipPrecision
ANTIALIASED_QUALITY, // nQuality
DEFAULT_PITCH, // nPitchAndFamily
L"Arial")); // lpszFacename
HGDIOBJ hOldFont = ::SelectObject(hDeviceContext_i, m_pFont->m_hObject);
if( !wglUseFontBitmaps( hDeviceContext_i, 0, 256 * 2, 1000 ))
{
return false;
}
::SelectObject( hDeviceContext_i, hOldFont );
return true;
} Screenshot of Zoom Text display. Zoom Text displayed at center of dialog, with current zoom factor. This text is displayed in a semi transparent way. Hide and Show are very smooth, which is implemented by blending feature of 2. 3. Translation
// Y value is -ve, only because opengl Y coordinate is
//increasing from bottom to top,
// But Y direction mouse movement received in Dialog is decreasing from bottom to top.
glTranslatef( m_fXTranslation, -m_fYTranslation, 0.0 ); 2. 4. RotationRotation is simply implemented with const float ROTATION_ANGLES[4] =
{ 0.0,// No rotation
270.0, // First Clockwise
180.0, // Second Clockwise
90.0 // 3rd clockwise
}; Inside /// Drawing of Image.
bool ImageArea::Draw()
{
// ....................
// Apply rotation
glRotatef( ROTATION_ANGLES[m_nRotate], 0, 0, 1 );
m_pVertexBuffer->DrawVertexBuffer( GL_QUADS );
// ...........................
return true;
} 2. 5. Vertex buffer logicHere we can discuss the vertex buffer logic. The below picture shows the 4 corners of image and its corresponding vertex coordinate. Rotation is applied to this vertex buffer and creates a rotated image display. Image is displayed to screen using // Vertex buffer creation logic in ImageArea.
bool ImageArea::SetupWindow()
{
// Setup Vertex buffer and UnProject.
int nHalfOfImageY = m_nImageHeight / 2;
int nHalfOfImageX = m_nImageWidth / 2;
/*
0--3
| |
1--2
*/
m_pVertexBuffer->SetAt( 0, -nHalfOfImageX ,
nHalfOfImageY, 0.0f, 0.0f,1.0f); // Left Top corner
m_pVertexBuffer->SetAt( 1, -nHalfOfImageX ,
-nHalfOfImageY, 0.0f, 0.0f,0.0f), // Left Bottom
m_pVertexBuffer->SetAt( 2, nHalfOfImageX,
-nHalfOfImageY, 0.0f, 1.0f,0.0f); // Right bottom
m_pVertexBuffer->SetAt( 3, nHalfOfImageX,
nHalfOfImageY, 0.0f, 1.0f,1.0f); // Right top
/// ...................
} 3. BottomWindowsThis class is responsible for drawing and message handling of buttons displayed at the bottom of PicZoom. There are 9 buttons displayed at bottom of PicZoom, which will help to explore different image files in the current folder, zoom, and rotation of image. Screenshot of BottomWindows.
/*
This class handles all operations related to a Button.
The drawing and mouse message handling is handled in this class.
The resource ID of bitmap is provided to this class, and ID of Message
to send to parentWindow( PicZoomDlg) is also provide to this class.
Whenever user press the button, this class will send message to PicZoomDlg.
*/
class GLButton : public GLWindowBase
{
public:
GLButton( HWND hParentWindow_i );
virtual ~GLButton();
virtual void SetRegion( const int nLeft_i, const int nTop_i,
const int nWidth_i, const int nHeight_i );
virtual void SetImage( const int nResourceID_i );
virtual bool SetupButton();
virtual void SetLButtonMessage( const int nMessageToParent_i );
virtual bool OnMouseMove( const int nX_i, const int nY_i, const int nFlags_i );
virtual bool OnLButtonDown
( const int nX_i, const int nY_i, const int nFlags_i );
virtual bool OnLButtonUp
( const int nX_i, const int nY_i, const int nFlags_i );
virtual bool IsWithinRegion( const int nX_i, const int nY_i );
void SetTransparency( const float fTransparency_i );
virtual bool Draw();
}; The initialization of a The transparent behaviour of each button is achieved by the alpha blending technique. The bitmaps are created in RGBA format (one 8 bit channel for alpha component). // The texture and vertex buffer for rendering are setup in this function.
bool GLButton::SetupButton()
{
// Create Vertex buffer.
m_pVertexBuffer = new GLVertexBuffer;
if( 0 == m_pVertexBuffer )
{
return false;
}
// Create Vertex buffer for rendering Quad image.
m_pVertexBuffer->CreateQuadVertexBuffer();
UpdateVertexBuffer();
// Setup Texture from Bitmap resource ID.
m_pTexture = new GLTexture;
int nWidth = 0;
int nHeight = 0;
BYTE* pbyARGBImage = 0;
BMPLoader LoadImage;
// Load Alpha enabled texture.
LoadImage.LoadBMP( m_nResourceID, nWidth, nHeight, pbyARGBImage, true );
// Create RGBA format texture.
m_pTexture->Create( nWidth, nHeight, pbyARGBImage, GL_RGBA, GL_RGBA8 );
return (GL_NO_ERROR == glGetError());
} 3. 1. Mouse Over Effect of ButtonThe mouse over effect of buttons is implemented in a tricky way. When displaying, the blending feature of texel (texture color) with current colour ( bool GLButton::Draw()
{
if( m_bMouseOver )
{
// After drawing, pixelstore biasing is changed.
//glColor4f( 1.0, 1.0, 1.0, 1.0 );
glColor4f( m_fTransparency, m_fTransparency,
m_fTransparency, m_fTransparency );
}
else
{
// After drawing, pixelstore biasing is changed.
//glColor4f( 0.75, 0.75, 0.75, 1.0 );
glColor4f( 0.75 * m_fTransparency, 0.75 *
m_fTransparency, 0.75 * m_fTransparency, 0.75 * m_fTransparency );
}
m_pVertexBuffer->DrawVertexBuffer( GL_QUADS );
return true;
} 3. 2. Displaying functionality of current buttonThe above screenshot displays the behavior of description text display. void GLText::Draw(const int nX_i, const int nY_i)
{
if( m_StringTimerHide.IsEnabled())
{
// Hide old string. Here fColorComponent will decrease
// after each frame.
int nRemTime = m_StringTimerHide.GetRemainingTime();
float fColorComponent = ( nRemTime / 20.0 );
glEnable( GL_BLEND );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glColor4f( 1.0, 1.0, 1.0, fColorComponent );
// Drawing text to screen.
m_pFontEngine->DrawText( nX_i, nY_i, m_csDisplayString.GetBuffer( 0 ) );
glDisable( GL_BLEND );
glColor4f( 1.0, 1.0, 1.0, 1.0 );
m_StringTimerHide.ElapseTime();
}
else
{
m_csDisplayString = m_csDisplayStringNew;
// Show New string. Here fColorComponent will
// increase during each frame, then reach the maximum value.
int nRemTime = 20 - m_StringTimerShow.GetRemainingTime();
float fColorComponent = ( nRemTime / 20.0 );
glEnable( GL_BLEND );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glColor4f( 1.0, 1.0, 1.0, fColorComponent );
// Drawing text to screen.
m_pFontEngine->DrawText( nX_i, nY_i, m_csDisplayString.GetBuffer( 0 ) );
glDisable( GL_BLEND );
glColor4f( 1.0, 1.0, 1.0, 1.0 );
m_StringTimerShow.ElapseTime();
}
} 4. CloseButton
bool CloseButton::IsWithinRegion( const int nX_i, const int nY_i )
{
if( GLButton::IsWithinRegion( nX_i, nY_i ))
{
// Here check the bottom left corner of circle.
// If mouse move is not within the semi-circle, then return false.
int nXDiff = nX_i - GLWindowBase::m_nWidth;
int nYDiff = nY_i;
int nRadius = sqrt( (float)nXDiff * nXDiff + (float)nYDiff * nYDiff );
if( nRadius < 45 )
{
return true;
}
}
return false;
} One workaround is also included in bool CloseButton::Draw()
{
GLButton::Draw();
// CloseBoundry draws the outline of circle in 50% transparency.
m_CloseBoundary.Draw();
return true;
} Slideshow FunctionalitySlideshow is also implemented with alpha blending functionality. Two textures are created with bitmap data of two image files. Transition from first image to second is created by blending first and texture texels. When starting slide show, the window size is changed to full desktop size, then hides all other windows ( void SlideShow::Display()
{
// glColor3f is used to make small amount of texture display.
// This color factor is multiplied with texel color and get a shading effect.
glColor4f( fColorFactor, fColorFactor, fColorFactor, fColorFactor );
m_pTexture1->Enable();
// Apply zoom1.
glScalef( m_fZoom1, m_fZoom1, 0.0 );
m_pVertexBuffer1->DrawVertexBuffer( GL_QUADS );
glPopMatrix();
if( nRemTime < 100 )
{
glPushMatrix();
// When transparent display of second texture is required.
float fTex2Color = 1.0 - fColorFactor;
glColor4f( fTex2Color, fTex2Color, fTex2Color, fTex2Color );
m_pTexture2->Enable();
// Apply Zoom 2.
glScalef( m_fZoom2, m_fZoom2, 0.0 );
m_pVertexBuffer2->DrawVertexBuffer( GL_QUADS );
glPopMatrix();
}
} Drag and Drop of Image FilesPicZoom supports dragging of image files[*.bmp, *.jpg, *.gif, *.tga]. Drag and Drop is implemented with the help of article from jibesh[http://www./KB/dialog/JibDragDrop.aspx].
LRESULT CPicZoomDlg::OnDropFiles(WPARAM wParam,LPARAM lParam)
{
TCHAR szDroppedFile[1024];
memset( szDroppedFile, 0, sizeof( szDroppedFile ));
HDROP hDrop ;
int nFiles;
hDrop = (HDROP)wParam;
nFiles = DragQueryFile(hDrop, // Structure Identifier
0, // -1 to Drop more than one file
szDroppedFile,// Dropped File Name
1023 *2 ); // Max char
// Load new Image file.
LoadImage( szDroppedFile );
return 1;
} Classes Used in PicZoomMain Classes
Utility Classes
OpenGL Wrapper classes:
Installer
The following registry modification is required for creating a context menu in Windows Explorer. Create a new key “
The registry modification is identified by trial and error method. I selected a new program(PicZoom.exe) as default application for opening a BMP file. Then I searched registry and found out the registry location to create an application in open with list. I don’t know any other method to do the same. The registry entries are created for open with list in bmp, jpg, png, and tga files. // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.jpg
// HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.bmp
// HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.png
// HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.tga
Points of InterestWhen retrieving the width and height of a character with Projection MatrixSince there is no 3D related operation, here we can use orthographic projection. Orthographic projection area is same as the window coordinate, and simply overcame -1 to +1 mapping method of perspective projection. CPU UsageInitially I prepared a timer, which will display image in 60 frames per second. This causes high CPU usage of Support for NonPowerOfTwo TexturesBy default, opengl texture dimension should satisfy power of 2. The width and height of texture should be a power of 2. This can be avoided if your machine supports Initially, bool GLTexture::Create(int nWidth, int nHeight, void *pbyData,
int nFormat_i, int nInternalFormat_i)
{
// ...............
// Retrieve Non power of two support and create texture based on it.
bool bNonPowerTwo = (GLExtension::GetInstance()).m_bNonPowerOfTwo;;
if( bNonPowerTwo )
{
glTexImage2D( GL_TEXTURE_2D, 0, nInternalFormat_i, nWidth, nHeight, 0,
nFormat_i, GL_UNSIGNED_BYTE, pbyData );
}
else
{
// if non-power of two is not supported, need to create nearest n^2 texture.
int nNewWidth = PicZoomUtil::GetNearestPowerOf2( nWidth );
int nNewHeight = PicZoomUtil::GetNearestPowerOf2( nHeight );
int nChannelCount = ( GL_RGB8 == nInternalFormat_i ) ? 3 : 4;
int nSize = nNewWidth * nNewHeight * nChannelCount;
if( 0 != ( nNewWidth * nChannelCount ) % 4 )
{
nSize = nNewHeight * ceil( ( nNewWidth * nChannelCount ) / 4.0f ) * 4.0f;
}
BYTE* pbyDataNew = new BYTE[nSize];
memset( pbyDataNew, 0, nSize );
// Set black data
glTexImage2D( GL_TEXTURE_2D, 0, nInternalFormat_i, nNewWidth, nNewHeight, 0,
nFormat_i, GL_UNSIGNED_BYTE, pbyDataNew );
// Update the required area with input data.
glTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, nWidth, nHeight, nFormat_i,
GL_UNSIGNED_BYTE, pbyData );
delete[] pbyDataNew;
}
} LimitationsSince all images are created as texture, I checked the reason of texture creation failure, in a machine without graphics card. Since some OpenGL applications with multi-texturing are perfectly running in those machines. I changed width and height of these textures to higher values [Modified multi-texturing application to display bitmap of size 1024]. Then I got white rectangle display, instead of proper texture image. I hope this issue is caused by lack of graphic memory to prepare big sized textures. History
|
|