Saturday, December 29, 2007

C#: Class for generating image thumbnails using Windows Shell

Update July 19, 2011: Replaced all instances of deprecated SHGetMalloc (from shell32.dll) with CoTaskMemFree (from ole32.dll) for better compatibility with Windows 7.

Question is, why not use GDI+? Answer is, it is tooo slow when you are working with quite a few files. GDI+ is full proof though, as windows shell would return the cached thumbnails...

By the way, I must mention that I will not take complete credit of this piece of gem! Kudos to whoever wrote this from scratch, but I do not mind taking credit for publishing it in my blog ;-)
So here it is...

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
 
namespace KodeSharp.SharedClasses
{
 public class ShellThumbnail : IDisposable
 {
  #region ShellFolder Enumerations
  [Flags]
  private enum ESTRRET : int
  {
   STRRET_WSTR = 0x0000,
   STRRET_OFFSET = 0x0001,
   STRRET_CSTR = 0x0002
  }
  [Flags]
  private enum ESHCONTF : int
  {
   SHCONTF_FOLDERS = 32,
   SHCONTF_NONFOLDERS = 64,
   SHCONTF_INCLUDEHIDDEN = 128
  }
 
  [Flags]
  private enum ESHGDN : int
  {
   SHGDN_NORMAL = 0,
   SHGDN_INFOLDER = 1,
   SHGDN_FORADDRESSBAR = 16384,
   SHGDN_FORPARSING = 32768
  }
  [Flags]
  private enum ESFGAO : int
  {
   SFGAO_CANCOPY = 1,
   SFGAO_CANMOVE = 2,
   SFGAO_CANLINK = 4,
   SFGAO_CANRENAME = 16,
   SFGAO_CANDELETE = 32,
   SFGAO_HASPROPSHEET = 64,
   SFGAO_DROPTARGET = 256,
   SFGAO_CAPABILITYMASK = 375,
   SFGAO_LINK = 65536,
   SFGAO_SHARE = 131072,
   SFGAO_READONLY = 262144,
   SFGAO_GHOSTED = 524288,
   SFGAO_DISPLAYATTRMASK = 983040,
   SFGAO_FILESYSANCESTOR = 268435456,
   SFGAO_FOLDER = 536870912,
   SFGAO_FILESYSTEM = 1073741824,
   SFGAO_HASSUBFOLDER = -2147483648,
   SFGAO_CONTENTSMASK = -2147483648,
   SFGAO_VALIDATE = 16777216,
   SFGAO_REMOVABLE = 33554432,
   SFGAO_COMPRESSED = 67108864
  }
  #endregion
 
  #region IExtractImage Enumerations
  private enum EIEIFLAG
  {
   IEIFLAG_ASYNC = 0x0001,
   IEIFLAG_CACHE = 0x0002,
   IEIFLAG_ASPECT = 0x0004,
   IEIFLAG_OFFLINE = 0x0008,
   IEIFLAG_GLEAM = 0x0010,
   IEIFLAG_SCREEN = 0x0020,
   IEIFLAG_ORIGSIZE = 0x0040,
   IEIFLAG_NOSTAMP = 0x0080,
   IEIFLAG_NOBORDER = 0x0100,
   IEIFLAG_QUALITY = 0x0200
  }
  #endregion
 
  #region ShellFolder Structures
  [StructLayoutAttribute(LayoutKind.Sequential, Pack = 4, Size = 0, CharSet = CharSet.Auto)]
  private struct STRRET_CSTR
  {
   public ESTRRET uType;
   [MarshalAs(System.Runtime.InteropServices.UnmanagedType.ByValArray, SizeConst = 520)]
   public byte[] cStr;
  }
 
  [StructLayout(LayoutKind.Explicit, CharSet = CharSet.Auto)]
  private struct STRRET_ANY
  {
   [FieldOffset(0)]
   public ESTRRET uType;
   [FieldOffset(4)]
   public IntPtr pOLEString;
  }
 
  [StructLayoutAttribute(LayoutKind.Sequential)]
  private struct SIZE
  {
   public int cx;
   public int cy;
  }
  #endregion
 
  #region Com Interop for IUnknown
  [ComImportGuid("00000000-0000-0000-C000-000000000046")]
  [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  private interface IUnknown
  {
   [PreserveSig]
   IntPtr QueryInterface(ref Guid riid, out IntPtr pVoid);
 
   [PreserveSig]
   IntPtr AddRef();
 
   [PreserveSig]
   IntPtr Release();
  }
  #endregion
 
  #region COM Interop for IEnumIDList
  [ComImportAttribute()]
  [GuidAttribute("000214F2-0000-0000-C000-000000000046")]
  [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
  private interface IEnumIDList
  {
   [PreserveSig]
   int Next(
    int celt,
    ref IntPtr rgelt,
    out int pceltFetched);
 
   void Skip(
    int celt);
 
   void Reset();
 
   void Clone(
    ref IEnumIDList ppenum);
  };
  #endregion
 
  #region COM Interop for IShellFolder
  [ComImportAttribute()]
  [GuidAttribute("000214E6-0000-0000-C000-000000000046")]
  [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
  private interface IShellFolder
  {
   void ParseDisplayName(
    IntPtr hwndOwner,
    IntPtr pbcReserved,
    [MarshalAs(UnmanagedType.LPWStr)] string lpszDisplayName,
    out int pchEaten,
    out IntPtr ppidl,
    out int pdwAttributes
    );
 
   void EnumObjects(
    IntPtr hwndOwner,
    [MarshalAs(UnmanagedType.U4)] ESHCONTF grfFlags,
    ref IEnumIDList ppenumIDList
    );
 
   void BindToObject(
    IntPtr pidl,
    IntPtr pbcReserved,
    ref Guid riid,
    ref IShellFolder ppvOut
    );
 
   void BindToStorage(
    IntPtr pidl,
    IntPtr pbcReserved,
    ref Guid riid,
    IntPtr ppvObj
    );
 
   [PreserveSig]
   int CompareIDs(
    IntPtr lParam,
    IntPtr pidl1,
    IntPtr pidl2
    );
 
   void CreateViewObject(
    IntPtr hwndOwner,
    ref Guid riid,
    IntPtr ppvOut
    );
 
   void GetAttributesOf(
    int cidl,
    IntPtr apidl,
    [MarshalAs(UnmanagedType.U4)] ref ESFGAO rgfInOut
    );
 
   void GetUIObjectOf(
    IntPtr hwndOwner,
    int cidl,
    ref IntPtr apidl,
    ref Guid riid,
    out int prgfInOut,
    ref IUnknown ppvOut
    );
 
   void GetDisplayNameOf(
    IntPtr pidl,
    [MarshalAs(UnmanagedType.U4)] ESHGDN uFlags,
    ref STRRET_CSTR lpName
    );
 
   void SetNameOf(
    IntPtr hwndOwner,
    IntPtr pidl,
    [MarshalAs(UnmanagedType.LPWStr)] string lpszName,
    [MarshalAs(UnmanagedType.U4)] ESHCONTF uFlags,
    ref IntPtr ppidlOut
    );
 
  };
 
  #endregion
 
  #region COM Interop for IExtractImage
  [ComImportAttribute()]
  [GuidAttribute("BB2E617C-0920-11d1-9A0B-00C04FC2D6C1")]
  [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
  private interface IExtractImage
  {
   void GetLocation(
    [Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszPathBuffer,
    int cch,
    ref int pdwPriority,
    ref SIZE prgSize,
    int dwRecClrDepth,
    ref int pdwFlags
    );
 
   void Extract(
    out IntPtr phBmpThumbnail
    );
  }
  #endregion
 
  #region UnmanagedMethods for IShellFolder
  private class UnmanagedMethods
  {
   [DllImport("ole32", CharSet = CharSet.Auto)]
   internal extern static void CoTaskMemFree(IntPtr ptr);
 
   [DllImport("shell32", CharSet = CharSet.Auto)]
   internal extern static int SHGetDesktopFolder(out IShellFolder ppshf);
 
   [DllImport("shell32", CharSet = CharSet.Auto)]
   internal extern static int SHGetPathFromIDList(IntPtr pidl, StringBuilder pszPath);
 
   [DllImport("gdi32", CharSet = CharSet.Auto)]
   internal extern static int DeleteObject(IntPtr hObject);
  }
  #endregion
 
  #region Member Variables
  private bool disposed = false;
  private System.Drawing.Bitmap thumbNail = null;
  #endregion
 
  #region Implementation
  public System.Drawing.Bitmap ThumbNail
  {
   get
   {
    return thumbNail;
   }
  }
 
  public System.Drawing.Bitmap GetThumbnail(string file, int width, int height)
  {
   if ((!File.Exists(file)) && (!Directory.Exists(file)))
   {
    throw new FileNotFoundException(
     String.Format("The file '{0}' does not exist", file),
     file);
   }
 
   if (thumbNail != null)
   {
    thumbNail.Dispose();
    thumbNail = null;
   }
 
   IShellFolder folder = null;
   try
   {
    folder = GetDesktopFolder;
   }
   catch (Exception ex)
   {
    throw ex;
   }
 
   if (folder != null)
   {
    IntPtr pidlMain = IntPtr.Zero;
    try
    {
     int cParsed = 0;
     int pdwAttrib = 0;
     string filePath = Path.GetDirectoryName(file);
     pidlMain = IntPtr.Zero;
     folder.ParseDisplayName(
      IntPtr.Zero,
      IntPtr.Zero,
      filePath,
      out cParsed,
      out pidlMain,
      out pdwAttrib);
    }
    catch (Exception ex)
    {
     Marshal.ReleaseComObject(folder);
     throw ex;
    }
 
    if (pidlMain != IntPtr.Zero)
    {
     // IShellFolder:
     Guid iidShellFolder = new Guid("000214E6-0000-0000-C000-000000000046");
     IShellFolder item = null;
 
     try
     {
      folder.BindToObject(pidlMain, IntPtr.Zero, ref
       iidShellFolder, ref item);
     }
     catch (Exception ex)
     {
      Marshal.ReleaseComObject(folder);
      UnmanagedMethods.CoTaskMemFree(pidlMain);
      throw ex;
     }
 
     if (item != null)
     {
      IEnumIDList idEnum = null;
      try
      {
       item.EnumObjects(
        IntPtr.Zero,
        (ESHCONTF.SHCONTF_FOLDERS |
        ESHCONTF.SHCONTF_NONFOLDERS),
        ref idEnum);
      }
      catch (Exception ex)
      {
       Marshal.ReleaseComObject(folder);
       UnmanagedMethods.CoTaskMemFree(pidlMain);
       throw ex;
      }
 
      if (idEnum != null)
      {
       int hRes = 0;
       IntPtr pidl = IntPtr.Zero;
       int fetched = 0;
       bool complete = false;
       while (!complete)
       {
        hRes = idEnum.Next(1, ref pidl, out fetched);
        if (hRes != 0)
        {
         pidl = IntPtr.Zero;
         complete = true;
        }
        else
        {
         if (GetThumbnail(file, pidl, item, width, height))
         {
          complete = true;
         }
        }
        if (pidl != IntPtr.Zero)
        {
         UnmanagedMethods.CoTaskMemFree(pidl);
        }
       }
 
       Marshal.ReleaseComObject(idEnum);
      }
 
 
      Marshal.ReleaseComObject(item);
     }
 
     UnmanagedMethods.CoTaskMemFree(pidlMain);
    }
 
    Marshal.ReleaseComObject(folder);
   }
   return thumbNail;
  }
 
  private bool GetThumbnail(string file, IntPtr pidl, IShellFolder item, int width, int height)
  {
   IntPtr hBmp = IntPtr.Zero;
   IExtractImage extractImage = null;
 
   try
   {
    string pidlPath = PathFromPidl(pidl);
    if (Path.GetFileName(pidlPath).ToUpper().Equals(Path.GetFileName(file).ToUpper()))
    {
     IUnknown iunk = null;
     int prgf = 0;
     Guid iidExtractImage = new Guid("BB2E617C-0920-11d1-9A0B-00C04FC2D6C1");
     item.GetUIObjectOf(IntPtr.Zero, 1, ref pidl, ref iidExtractImage, out prgf, ref iunk);
     extractImage = (IExtractImage)iunk;
 
     if (extractImage != null)
     {
      SIZE sz = new SIZE();
      sz.cx = width;
      sz.cy = height;
      StringBuilder location = new StringBuilder(260, 260);
      int priority = 0;
      int requestedColourDepth = 32;
      EIEIFLAG flags = EIEIFLAG.IEIFLAG_ASPECT | EIEIFLAG.IEIFLAG_SCREEN;
      int uFlags = (int)flags;
 
      extractImage.GetLocation(location, location.Capacity, ref priority, ref sz, requestedColourDepth, ref uFlags);
 
      extractImage.Extract(out hBmp);
      if (hBmp != IntPtr.Zero)
      {
       thumbNail = System.Drawing.Bitmap.FromHbitmap(hBmp);
      }
 
      Marshal.ReleaseComObject(extractImage);
      extractImage = null;
     }
     return true;
    }
    else
    {
     return false;
    }
   }
   catch (Exception ex)
   {
    if (hBmp != IntPtr.Zero)
    {
     UnmanagedMethods.DeleteObject(hBmp);
    }
    if (extractImage != null)
    {
     Marshal.ReleaseComObject(extractImage);
    }
    throw ex;
   }
  }
 
  private string PathFromPidl(IntPtr pidl)
  {
   StringBuilder path = new StringBuilder(260, 260);
   int result = UnmanagedMethods.SHGetPathFromIDList(pidl, path);
   if (result == 0)
   {
    return string.Empty;
   }
   else
   {
    return path.ToString();
   }
  }
 
  private IShellFolder GetDesktopFolder
  {
   get
   {
    IShellFolder ppshf;
    int r = UnmanagedMethods.SHGetDesktopFolder(out ppshf);
    return ppshf;
   }
  }
  #endregion
 
  #region Constructor, Destructor, Dispose
  public ShellThumbnail()
  {
  }
 
  public void Dispose()
  {
   if (!disposed)
   {
    if (thumbNail != null)
    {
     thumbNail.Dispose();
    }
    disposed = true;
   }
  }
 
  ~ShellThumbnail()
  {
   Dispose();
  }
  #endregion
 }
}
 

23 comments:

  1. Hi

    I am running this class - callingit from an asp.net app - and i get the error

    The data necessary to complete this operation is not yet available. (Exception from HRESULT: 0x8000000A)

    Any ideas?

    TIA

    Tony

    ReplyDelete
  2. Tony, thanks for reading my posts. I think this is one of those mysterious errors for which I have seen numerous posts online, but no one (including Microsoft) really came up with a full proof solution except having it automatically resolved by restarting the application, or Visual Studio, or IIS. Depending on your specific situation, try applying those suggestions that you get from a google search with that error message (I know this is nothing groundbreaking that I am saying).

    I am sorry I could not specifically answer your question. But I will definitely give you a piece of advice, that is, this class is really meant for doing quick and dirty job of reusing the shell for getting the thumbnails in a client type installation, and is in no way meant for an enterprise grade tool for generating thumbnails. Also since it is COM based, depending on the version of operating system in use, it may or may not work fully in future. So I will suggest if you have the horsepower in your machine, you are probably better off using the .Net assemblies, although I know they are much slower compared to this.

    I hope this makes sense. As always, I welcome any constructive criticism. Also please post here if you found a solution to this problem.

    Thanks,
    Kaushik

    ReplyDelete
  3. I have some experience with server-side thumbnail generation. Our's is currently classic asp/vb6 and I am working on a c# port (to be used in a Windows service, we do not want it to run inside a web app anymore). Googling for equivalent C# code, I found your blog, it looks to be exactly what I need :)

    In an attempt to help Tony, I know of two issues with similar code:

    - Thumbnail extraction doesn't work on windows 2000 server. This is an issue with BindToObject for the IExtractImage interface. I am not sure if the same issue may apply in this C# implementation, but our workaround in VB6 was to retry with BindToStorage if BindToObject fails for that interface:
    On Error Resume Next 'Do not log BindToObject error
    oFolder.BindToObject pIdL, 0, lpIID_IExtractImage, oExtract
    ' BindToObject always fails on Windows 2000 for lpIID_IExtractImage,
    ' but BindToStorage works and appears to have the same result.
    If oExtract Is Nothing Then
    oFolder.BindToStorage pIdL, 0, lpIID_IExtractImage, oExtract
    End If
    On Error GoTo errHandler ' Resume normal error handling

    Our VB code is based on this excellent sample code:
    http://www.vbfrance.com/codes/EXTRACTION-MINIATURES-FICHIERS_39648.aspx
    (unfortunately, site is in French, but the code was the best implementation I found for VB6)


    - Problem with pdf thumbnails. I am afraid you'll have to tinker around with dcomcnfg if that is your problem. We found that it helps to install Acrobat 7 instead of Acrobat 8. In any case, it goes like this:
    * Start > Run > dcomcnfg
    * go to Computers > My computer > DComCnfg
    * Confirm registry changes if prompted
    * tinker around with "Adobe Acrobat 7.0 Document", AcroPdf and PdfShellInfo classes, increase access on those. The user running your aspx site needs local launch and local activation rights on those objects.

    Still, we feel the the IExtract interface of Adobe Acrobat is really unstable and one of my design goals in the C# port is to replace it by our own pdf thumbnail generator if the source document is pdf.

    ReplyDelete
  4. Ok, I got the same error as Tony and it finally jogged my memory (I did see it before while implementing the VB6 stuff). The solution is simple: you may ignore warnings raised by GetLocation inside getThumbnail(). Like this:

    try
    {
    extractImage.GetLocation(location, location.Capacity, ref priority, ref sz, requestedColourDepth, ref uFlags);
    }
    catch{}

    Again, the error only occurs when we try to extract a pdf thumbnail (Adobe's implementation is *really* bad and yes I want to name and shame them) and it is not really an error at all - seems that they just bodged their internal error handling.

    I also rewrote the code to get the file pidl, to make it more similar to my vb6 code, more compact than your implementation. I replaced the block inside if (item != null) by this:
    if (item != null)
    {
    string sFileName = Path.GetFileName(file);
    IntPtr pidlFile = IntPtr.Zero;
    try
    {
    int cParsed = 0;
    int pdwAttrib = 0;
    item.ParseDisplayName(IntPtr.Zero, IntPtr.Zero, sFileName, out cParsed, out pidlFile, out pdwAttrib);
    if (pidlFile != IntPtr.Zero)
    {
    getThumbnail(file, pidlFile, item);
    Allocator.Free(pidlFile);
    }
    }
    catch (Exception ex)
    {
    Marshal.ReleaseComObject(folder);
    Allocator.Free(pidlMain);
    throw ex;
    }
    Marshal.ReleaseComObject(item);
    }

    ReplyDelete
  5. Berend,
    I truly appreciate your taking time to post your valuable comments.

    Tony,
    If you are still in touch with the thread - while you got this error, could you please let us know which type of file it came from, PDF? Image type? Something else? And of course, did you get to any conclusion on how to resolve this? Thank in advance.

    ReplyDelete
  6. KC,

    Berend is absolutely right! I encountered the same error with PDF thumbnails on Vista and ignoring the GetLocation error generated the thumbnail just fine.

    I was looking for a solution for PDF thumbnails for long time and luckily came across your site.

    Thanks to Berend and Kaushik!

    ReplyDelete
  7. G'day,

    I was using this code fine on a Windows XP C# application, however when trying to run it on Vista 64bit it fails.. I assume shell32 is no longer shell32... any ideas on how to get it to work in 64bit vista?

    ReplyDelete
  8. Rod,
    I just upgraded my OS to Vista 64bit over the thanksgiving weekend. I will test this out and let you know. However, I think Shell32 still exists and it should work, but I will let you know precisely. In the mean time, if you already figured something out, please let us know. Thanks, KC

    ReplyDelete
  9. I have your code in my asp net web site, but I got this error:
    The system cannot find the file specified. (Exception from HRESULT: 0x80070002)

    This error happening when execute next line and the file is a pdf, with other extensions (jpg, gif) there is not problem:
    item.GetUIObjectOf(IntPtr.Zero, 1, ref pidl, ref iidExtractImage, out prgf, ref iunk);

    When run in debug mode, works fine, no errors.

    Can you help me?

    Thanks in advance.
    Christ DM

    ReplyDelete
  10. Christ DM,
    In order for this to work, Adobe Reader (or even better if Acrobat) has to be installed on the server as Windows Shell will use appropriate handlers for the mime type to generate the thumbnails. Please verify the same as your first step of troubleshooting.
    Let me know what you find out.
    Thanks,
    KC

    ReplyDelete
  11. Dear KC!

    thank you for posting this class!

    I am running it on a Windows Server 2003 Standard Edition with SP2. I am trying to create thumbnails out of a PowerPoint slide, and getting the following error:
    At extractImage.Extract(out hBmp); //(line ca. 578)
    Exception is "Invalid clipboard format (Exception from HRESULT: 0x8004006A (DV_E_CLIPFORMAT))". Do you have some idea as to what the error might be?

    Best regards!

    Nasser

    ReplyDelete
  12. Nasser,
    Like for Christ DM, the first question I would ask is - is PowerPoint installed on the server? Because shell does use the mime type's default handler to create thumbnails. By the way, I am not going to the details of the fact that installing MS Office on a Win 2003 server is not supported and actually license violation from Microsoft's perspective, just so you know :-))

    ReplyDelete
  13. As I promised a while back, I upgraded my laptop to 64 bit vista ultimate. It took a while to get all the drivers and complete the install but I tested the class above on 64 bit recently, and it works without any issues! Just wanted to let you know.

    ReplyDelete
  14. Dear KC, im a IT student from Ukraine, can i use your file thumbnails class in my diploma project, with link to your blog in about?
    Good luck in any case!

    ReplyDelete
  15. Excelent post, worked perfectly in Win7 with berend changes.

    ReplyDelete
  16. Under XP worked okay... in Vista 64-bit it appeared to be working; but I'm getting "Value does not fall within the expected range." on line:

    folder.ParseDisplayName(
    IntPtr.Zero,
    IntPtr.Zero,
    filePath,
    out cParsed,
    out pidlMain,
    out pdwAttrib);

    but only when filePath is something like... "E:\\[D]\\My Collection"

    These paths are generated on picasa when it you archive images to CD/DVD media. The folders are valid. This error did not occur under XP. All other folders are working and producing thumbnails. Thank you for the posting and your help.

    Craig

    ReplyDelete
  17. Hi Kaushik and all,
    I am HaiTa. I installed this class and it helped me for creating a thumbnail of a video clip (*.wmv) in my local computer. But I have problem when copy my app to remoting server: it worked with file *.bmp but not *.wmv.
    Any idea? please help me.

    ReplyDelete
  18. Original Article "Thumbnail Extraction Using the Shell" at http://www.vbaccelerator.com/home/net/code/libraries/Shell_Projects/Thumbnail_Extraction/article.asp

    ReplyDelete
  19. KC, it works fine on 32bit but not on 64bit OS, off course for regular image files it is not an issue but for specific format which have custom handlers registered to them like CAD files it fails.

    ReplyDelete
  20. I get an InvalidCastException thrown from

    extractImage.Extract(out hBmp);

    Has anyone else encountered this? I'm running the code on a 32 bit Vista installation against Word 2007 and Excel 2007 documents.

    ReplyDelete
  21. Below error Iam getting when execute the above code.,

    The data necessary to complete this operation is not yet available. (Exception from HRESULT: 0x8000000A)

    ReplyDelete
  22. Thanks you is very useful :) i used to extend FileInfo ;)

    ReplyDelete
  23. Hi, thank you so much for this code. I have no idea how it works but I managed to use it in my C# application. I change the code just a bit to get high quality thumbnails (EIEIFLAG flags = EIEIFLAG.IEIFLAG_QUALITY), but I have problem with releasing memory. When I run the code with let say 100 pictures, I can see in Task Manager how the memory grows, but it is not released.
    Do you have any idea where is the problem?
    I am calling GetThumbnail like this:
    public static void CreateThumbnail(string origPath, string newPath) {
    int size = 400;
    string dir = Path.GetDirectoryName(newPath);
    if (dir == null) return;
    Directory.CreateDirectory(dir);
    try {
    KodeSharp.SharedClasses.ShellThumbnail st = new KodeSharp.SharedClasses.ShellThumbnail();
    st.GetThumbnail(origPath, size, size).Save(newPath, System.Drawing.Imaging.ImageFormat.Jpeg);
    st.Dispose();
    } catch (Exception) {
    //file can have 0 size
    }
    }

    ReplyDelete

Please use your common sense before making a comment, and I truly appreciate your constructive criticisms.