Programmer » Pictures For DVD

Pictures For DVD

This free Windows utility is one small step in preparing pictures for writing onto DVD. You drop one of your picture folder onto this application and it generates a simpler, smaller copy of the folder called "for-dvd":

If you had pictures in lots of subfolders, this utility flattens them all into just a single folder with all the pictures in order. This can help you visualize and sort them all together. It's also needed for making a Kodak PictureCD, where you're not allowed subfolders.

If you had pictures with long filenames, this utility renames them all into 8.3 format suitable for PictureCD.

If you had excessively large pictures (e.g. 10 megapixel photos from your digital camera) this utility shrinks them to a size appropriate for DVD so as to make them easier to work with.

If you had subfolders, this utility creates a "title page" for each of those subfolders. This way you can watch a slideshow of your photos and still know what the directory-names were.

The source code shows how to recursively scan a directory, how to respond to WM_DROP, how to use GDI+ to load and resize a jpeg.


Which software to use?

Windows Photo Story 3 -- free from Microsoft. You give this wizard a collection of up to 300 pictures and it makes a slideshow out of them. It generated nicer, more aesthetic slideshows than any other software I found. (Note: a bug in the software stops you from using mp3s as your soundtrack to the slideshow. You have to use WMA instead. See Audio Conversion for how to do this.)

Sony DVD Architect -- $50 from Sony. This software is for designing the DVD - making its menu page, assembling the video and audio that will go on it, burning the final thing to disk. This software stands head and shoulders above all the other free and shareware utilities out there.

http://www.videohelp.com/ -- A very helpful website. Although it gives too much weight to bad free and shareware software. I waded through a lot of rubbish before I found Photo Story 3 and DVD Architect.


Source code

// PICTURES-FOR-DVD -- assembles photos into order for putting onto a slideshow DVD
// (c) 2004-2006 Lucian Wischik, lu@wischik.com
// Anyone can use this code anyway they wish.
//
// The application has a main window and a multiline edit window which totally fills it (used for logs)
// The user drops a directory into the main window. We recursively scan this directory, using
// a recursive function "scan". For each image, we use GDI+ to make a PAL-DVD-sized image of it.
// And for each directory we make a "chapter-title". Everything is triggered by the WM_DROP
// message: this is where we invoke "scan"
//


#include <windows.h>
#include <gdiplus.h>
#include <shlobj.h>
#include <stdio.h>
#include <list>
#include <string>
#include <fstream>
using namespace std;
using namespace Gdiplus;


HINSTANCE hInstance;
HWND hwnd,hed;          // Main window (initialized in WinMain), and edit control (initialized in WM_CREATE)
bool processing=false;  // 'True' during processing. (during processing, we use PeekMessages periodically)
bool abort_p=false;     // if user clicks WM_CLOSE during processing, we just set this flag rather than closing
int dvdwidth=720, dvdheight=576; // size of the PAL screen, in pixels
CLSID encoderClsid; EncoderParameters encoderParameters; ULONG quality=80; // used for GDI+ jpeg stuff (initalized in WinMain, used when saving jpegs)
wstring StringInt(int i) {wchar_t c[64]; wsprintf(c,L"%i",i); return wstring(c);}
wstring StringInt3(int i) {wchar_t c[64]; wsprintf(c,L"%03i",i); return wstring(c);}

wstring GetLastErrorString()
{ LPVOID lpMsgBuf; FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,NULL,
    GetLastError(),MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &lpMsgBuf, 0, NULL);
  wstring s((wchar_t*)lpMsgBuf); LocalFree( lpMsgBuf ); return s;
}


// print -- prints a log in the edit window "hed"
void print(const wstring msg)
{ wstring t=msg+L"\r\n";
  unsigned int len = GetWindowTextLength(hed);
  if (len>16000) SetWindowText(hed,L"");
  int i=GetWindowTextLength(hed);
  SendMessage(hed,EM_SETSEL,(WPARAM)i,(LPARAM)i);
  SendMessage(hed,EM_REPLACESEL,FALSE,(LPARAM)t.c_str());
}

// during processing, we call PumpMessages periodically to ensure responsiveness
void PumpMessages()
{ MSG msg; for (;;)
  { BOOL res=PeekMessage(&msg,0,0,0,PM_REMOVE);
    if (!res||msg.message==WM_QUIT) return;
    TranslateMessage(&msg); DispatchMessage(&msg);
  }
}



// scan -- this function recursively scans a directory looking for images.
// When it finds an image it generates a thumbnail of at least 80x80
// and adds it to the TAccumulator. The function calls PumpMessages periodically
// to allow for user responsiveness, and if ever the global flag "abort_p" is
// set then it aborts.
void scan(const wstring dir, const wstring photodir, int *folderi, int *filei)
{ wstring s = dir+L"\\*";
  WIN32_FIND_DATA fdat;
  // 1. look for files
  HANDLE hfind = FindFirstFile(s.c_str(),&fdat);
  for (BOOL gotmore=(hfind!=INVALID_HANDLE_VALUE); gotmore && !abort_p; gotmore=FindNextFile(hfind,&fdat))
  { if (fdat.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue;
    // get the extension
    const wchar_t *c=fdat.cFileName, *ext=0;
    while (*c!=0) {if (*c=='.' || *c=='\\' || *c=='/' || *c==':') ext=c; c++;}
    if (ext==0 || *ext==0 || *ext!='.') ext=L"";
    bool isimg = (_wcsicmp(ext,L".jpg")==0) | (_wcsicmp(ext,L".jpeg")==0) | (_wcsicmp(ext,L".bmp")==0)
                |(_wcsicmp(ext,L".tif")==0) | (_wcsicmp(ext,L".tiff")==0) | (_wcsicmp(ext,L".gif")==0);
    if (!isimg) continue;
    wstring sfn = dir+L"\\"+fdat.cFileName;
    wstring dfn = photodir+L"\\"+StringInt3(*folderi)+StringInt3(*filei)+L".jpg"; *filei = *filei+1;
    // save the file as a thumbnail
    Image *image=new Image(sfn.c_str());
    double w=image->GetWidth(), h=image->GetHeight();
    double scalew=((double)dvdwidth)*2.0/w, scaleh=((double)dvdheight)*2.0/h;
    double scale=scalew; if (scaleh>scale) scale=scaleh;
    int sw=(int)(w*scale+0.5), sh=(int)(h*scale+0.5);
    Image *i2=0, *simage=image;
    if (scale<1.0)
    { i2 = new Bitmap(sw,sh,PixelFormat32bppRGB);
      Graphics *g = Graphics::FromImage(i2);
      RectF drc(0,0,(float)sw,(float)sh);
      g->DrawImage(image,drc,0,0,(float)w,(float)h,UnitPixel);
      delete g;
      simage=i2;
    }
    simage->Save(dfn.c_str(), &encoderClsid, &encoderParameters);
    if (i2!=0) delete i2;
    delete image;
    print(sfn);
    //
    PumpMessages();
  }
  if (hfind!=INVALID_HANDLE_VALUE) FindClose(hfind);
  // 2. look for directories
  hfind = FindFirstFile(s.c_str(),&fdat);
  for (BOOL gotmore=(hfind!=INVALID_HANDLE_VALUE); gotmore && !abort_p; gotmore=FindNextFile(hfind,&fdat))
  { if (!(fdat.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) continue;
    if (fdat.cFileName==wstring(L".") || fdat.cFileName==wstring(L"..") || fdat.cFileName==wstring(L"for-dvd")) continue;
    wstring s = fdat.cFileName;
    //
    float w=(float)dvdwidth, h=(float)dvdheight;
    Bitmap bmp(dvdwidth,dvdheight,PixelFormat32bppRGB);
    Graphics graphics(&bmp);
    SolidBrush blackBrush(Color(0,0,0));
    SolidBrush whiteBrush(Color(255,255,255));
    FontFamily fontFamily(L"Arial");
    Font font(&fontFamily,36,FontStyleRegular,UnitPixel);
    RectF bounds(0.2f*w, 0.2f*h, 0.6f*w, 0.6f*h);
    StringFormat format;
    format.SetAlignment(StringAlignmentCenter);
    format.SetLineAlignment(StringAlignmentCenter);
    graphics.FillRectangle(&blackBrush,0,0,dvdwidth,dvdheight);
    graphics.DrawString(s.c_str(),-1,&font,bounds,&format,&whiteBrush);

    *folderi = *folderi + 1;
    wstring dfn = photodir+L"\\"+StringInt3(*folderi)+L"000_.jpg";
    bmp.Save(dfn.c_str(), &encoderClsid, &encoderParameters);
    *filei=1;
    scan(dir+L"\\"+fdat.cFileName, photodir, folderi, filei);
  }
  if (hfind!=INVALID_HANDLE_VALUE) FindClose(hfind);
}


LRESULT CALLBACK PlainWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{ switch (msg)
  { case WM_CREATE:
    { hed = CreateWindow(L"EDIT",L"",WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL|ES_LEFT|ES_MULTILINE,
                    0,0,100,100,hwnd,NULL,hInstance,0);
      print(L"Drop a picture-folder here...");
      print(L"I will make a 'for-dvd' folder inside it,");
      print(L"of its contents.");
      DragAcceptFiles(hwnd,TRUE);
    } break;
    case WM_SIZE:
    { RECT rc; GetClientRect(hwnd,&rc);
      MoveWindow(hed,0,0,rc.right,rc.bottom,TRUE);
    } break;
    case WM_PAINT:
    { PAINTSTRUCT ps; BeginPaint(hwnd,&ps);
      EndPaint(hwnd,&ps);
    } break;
    case WM_DROPFILES:
    { HDROP hdrop = (HDROP)wParam;
      int num = DragQueryFile(hdrop,0xFFFFFFFF,NULL,0);
      if (num>1)
      { DragFinish(hdrop);
        MessageBox(hwnd,L"Please drag just a single folder",L"Catalogue",MB_OK);
        break;
      }
      int len = DragQueryFile(hdrop,0,0,0);
      wchar_t *c=new wchar_t[len+1]; DragQueryFile(hdrop,0,c,len+1);
      wstring dir(c); delete[] c; print(dir);
      abort_p=false;
      processing=true;
      //
      // delete the for-dvd folder if it was there already
      wstring photodir = dir+L"\\for-dvd";
      if (GetFileAttributes(photodir.c_str())==INVALID_FILE_ATTRIBUTES) CreateDirectory(photodir.c_str(),0);
      wstring s=photodir+L"\\*";
      WIN32_FIND_DATA fdat; HANDLE hfind = FindFirstFile(s.c_str(),&fdat);
      for (BOOL gotmore=(hfind!=INVALID_HANDLE_VALUE); gotmore; gotmore=FindNextFile(hfind,&fdat))
      { if (fdat.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue;
        wstring fn = photodir+L"\\"+fdat.cFileName;
        DeleteFile(fn.c_str());
      }
      if (hfind!=INVALID_HANDLE_VALUE) FindClose(hfind);
      //
      int folderi=1, filei=1;
      scan(dir,photodir,&folderi,&filei);
      //
      processing=false;
      DragFinish(hdrop);
      print(L"done.");
      print(L"");
      print(L"Drop more picture-folders here if you want...");
      if (abort_p) PostMessage(hwnd,WM_CLOSE,0,0);
    } break;
    case WM_CLOSE:
    { if (processing) {abort_p=true; return 0;}
      else {DestroyWindow(hwnd); return 0;}
    } 
    case WM_DESTROY:
    { DragAcceptFiles(hwnd,FALSE);
      PostQuitMessage(0);
    } break;
  }
  return DefWindowProc(hwnd, msg, wParam, lParam);
}





int WINAPI WinMain(HINSTANCE h,HINSTANCE,LPSTR,int)
{ hInstance=h;
  abort_p=false;
  processing=false;
  //
  GdiplusStartupInput gdiplusStartupInput; ULONG_PTR gdiplusToken;
  GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
  // get clsid of the jpeg-encoder
  bool foundencoder=false; UINT num=0, size=0; GetImageEncodersSize(&num, &size); 
  if (size!=0)
  { ImageCodecInfo* pImageCodecInfo = (ImageCodecInfo*)(malloc(size));
    GetImageEncoders(num, size, pImageCodecInfo);
    for (unsigned int j=0; j<num; j++)
    { if (wcscmp(pImageCodecInfo[j].MimeType,L"image/jpeg")==0)
      { encoderClsid=pImageCodecInfo[j].Clsid; foundencoder=true;
      }    
    }
    free(pImageCodecInfo);
  }
  if (!foundencoder) {MessageBox(NULL,L"Failed to find GDI+ jpeg",L"Error",MB_OK); GdiplusShutdown(gdiplusToken); return 0;}
  // and configure the jpeg-encoder
  encoderParameters.Count = 1;
  encoderParameters.Parameter[0].Guid = EncoderQuality;
  encoderParameters.Parameter[0].Type = EncoderParameterValueTypeLong;
  encoderParameters.Parameter[0].NumberOfValues = 1;
  encoderParameters.Parameter[0].Value = &quality;
  //
  WNDCLASSEX wcex; ZeroMemory(&wcex,sizeof(wcex)); wcex.cbSize = sizeof(WNDCLASSEX);
  BOOL res=GetClassInfoEx(hInstance,L"PlainClass",&wcex);
  if (!res)
  { wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = (WNDPROC)PlainWndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = NULL;
    wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wcex.lpszMenuName	= NULL;
    wcex.lpszClassName = L"PlainClass";
    wcex.hIconSm = NULL;
    ATOM res=RegisterClassEx(&wcex);
    if (res==0) {MessageBox(NULL,L"Failed to register class",L"Error",MB_OK); GdiplusShutdown(gdiplusToken); return 0;}
  }
  //
  hwnd = CreateWindowEx(0,L"PlainClass", L"Pictures-for-DVD", WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, CW_USEDEFAULT, 400, 400, NULL, NULL, hInstance, NULL);
  if (hwnd==NULL) {MessageBox(NULL,L"Failed to create window",L"Error",MB_OK); GdiplusShutdown(gdiplusToken); return 0;}
  ShowWindow(hwnd,SW_SHOW);
  //
  MSG msg;
  while (GetMessage(&msg, NULL, 0, 0))
  { TranslateMessage(&msg);
	  DispatchMessage(&msg);
  }
  //
  GdiplusShutdown(gdiplusToken);
  return (int)msg.wParam;
}
add comment  edit commentPlease add comments here