How to copy image without using the clipboard?

6

Question: I have the below code to capture an image from a webcam.

My problem is this part:

SendMessage(hCaptureWnd, WM_CAP_COPY, 0, 0);                    // copy it to the clipboard

What it does is copy the image from the window to the clipboard, and then create a byte array out of it.

It works - as long as you don't use the clipboard while the program is running.
The problem is, this doesn't even work for myself, as I sometimes copy something while Visual Studio takes ages to start debug the web application, and then it crashes.

So here my question:
How can I get the image without using the clipboard ? Or more specifically, how to transform hCaptureWnd to System.Drawing.Image ?


-- Edit:
I missed to say "without creating a file, i want a byte array".
It's a web application, so the user the application runs under shouldn't have write access to the file system (writing to a file only for temporary testing) ...
-- End Edit:


/// <summary>
/// Captures a frame from the webcam and returns the byte array associated
/// with the captured image
/// </summary>
/// <param name="connectDelay">number of milliseconds to wait between connect 
/// and capture - necessary for some cameras that take a while to 'warm up'</param>
/// <returns>byte array representing a bitmp or null (if error or no webcam)</returns>
private static byte[] InternalCaptureToByteArray(int connectDelay = 500)
{
    Clipboard.Clear();                                              // clear the clipboard
    int hCaptureWnd = capCreateCaptureWindowA("ccWebCam", 0, 0, 0,  // create the hidden capture window
        350, 350, 0, 0);
    SendMessage(hCaptureWnd, WM_CAP_CONNECT, 0, 0);                 // send the connect message to it
    Thread.Sleep(connectDelay);                                     // sleep the specified time
    SendMessage(hCaptureWnd, WM_CAP_GET_FRAME, 0, 0);               // capture the frame
    SendMessage(hCaptureWnd, WM_CAP_COPY, 0, 0);                    // copy it to the clipboard
    SendMessage(hCaptureWnd, WM_CAP_DISCONNECT, 0, 0);              // disconnect from the camera
    Bitmap bitmap = (Bitmap)Clipboard.GetDataObject().GetData(DataFormats.Bitmap);  // copy into bitmap

    if (bitmap == null)
        return null;

    using (MemoryStream stream = new MemoryStream())
    {
        bitmap.Save(stream, ImageFormat.Bmp);    // get bitmap bytes
        return stream.ToArray();
    } // End Using stream

} // End Function InternalCaptureToByteArray

Note (http://msdn.microsoft.com/en-us/library/windows/desktop/dd756879(v=vs.85).aspx):

HWND VFWAPI capCreateCaptureWindow(
  LPCTSTR lpszWindowName,
  DWORD dwStyle,
  int x,
  int y,
  int nWidth,
  int nHeight,
  HWND hWnd,
  int nID
);


#define VFWAPI  WINAPI 

typedef HANDLE HWND;
typedef PVOID HANDLE;
typedef void *PVOID;

Full code for reference

using System;
using System.IO;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
using System.Drawing.Imaging;
using System.Collections.Generic;
using System.Runtime.InteropServices;


// http://www.creativecodedesign.com/node/66
// http://www.barebonescoder.com/2012/01/finding-your-web-cam-with-c-directshow-net/
// http://www.codeproject.com/Articles/15219/WebCam-Fast-Image-Capture-Service-using-WIA
// http://www.c-sharpcorner.com/uploadfile/yougerthen/integrate-the-web-webcam-functionality-using-C-Sharp-net-and-com-part-viii/
// http://forums.asp.net/t/1410057.aspx


namespace cc.Utility
{


    // bool isCaptured = ccWebCam.CaptureSTA("capture.jpg"); // Access to path C:\Program Files (x86)\Common Files\Microsoft Shared\DevServer\10.0\capture.jpg" denied.
    // byte[] captureBytes = ccWebCam.CaptureSTA();

    /// <summary>
    /// Timur Kovalev (http://www.creativecodedesign.com):
    /// This class provides a method of capturing a webcam image via avicap32.dll api.
    /// </summary>    
    public static class ccWebCam
    {
        #region *** PInvoke Stuff - methods to interact with capture window ***

        [DllImport("user32", EntryPoint = "SendMessage")]
        private static extern int SendMessage(int hWnd, uint Msg, int wParam, int lParam);

        [DllImport("avicap32.dll", EntryPoint = "capCreateCaptureWindowA")]
        private static extern int capCreateCaptureWindowA(string lpszWindowName, int dwStyle, 
            int X, int Y, int nWidth, int nHeight, int hwndParent, int nID);  


        private const int WM_CAP_CONNECT = 1034;
        private const int WM_CAP_DISCONNECT = 1035;
        private const int WM_CAP_COPY = 1054;
        private const int WM_CAP_GET_FRAME = 1084;


        #endregion


        private static object objWebCamThreadLock = new object();


        //CaptureToFile(@"D:\Stefan.Steiger\Documents\Visual Studio 2010\Projects\Post_Ipag\image3.jpg"):
        public static bool Capture(string filePath, int connectDelay = 500)
        {
            lock (objWebCamThreadLock)
            {
                return cc.Utility.ccWebCam.InternalCaptureAsFileInThread(filePath, connectDelay);
            }
        } // End Treadsafe Function Capture


        public static byte[] Capture(int connectDelay = 500)
        {
            lock (objWebCamThreadLock)
            {
                return InternalCaptureToByteArrayInThread(connectDelay);
            }
        } // End Treadsafe Function Capture


        /// <summary>
        /// Captures a frame from the webcam and returns the byte array associated
        /// with the captured image. The image is also stored in a file
        /// </summary>
        /// <param name="filePath">path the file wher ethe image will be saved</param>
        /// <param name="connectDelay">number of milliseconds to wait between connect 
        /// and capture - necessary for some cameras that take a while to 'warm up'</param>
        /// <returns>true on success, false on failure</returns>
        private static bool InternalCaptureAsFileInThread(string filePath, int connectDelay = 500)
        {
            bool success = false;
            Thread catureThread = new Thread(() =>
            {
                success = InternalCaptureAsFile(filePath, connectDelay);
            });
            catureThread.SetApartmentState(ApartmentState.STA);
            catureThread.Start();
            catureThread.Join();
            return success;
        } // End Function InternalCaptureAsFileInThread


        /// <summary>
        /// Captures a frame from the webcam and returns the byte array associated
        /// with the captured image. The image is also stored in a file
        /// </summary>
        /// <param name="filePath">path the file wher ethe image will be saved</param>
        /// <param name="connectDelay">number of milliseconds to wait between connect 
        /// and capture - necessary for some cameras that take a while to 'warm up'</param>
        /// <returns>true on success, false on failure</returns>
        private static bool InternalCaptureAsFile(string filePath, int connectDelay = 500)
        {
            byte[] capture = ccWebCam.InternalCaptureToByteArray(connectDelay);
            if (capture != null)
            {
                // Access to path C:\Program Files (x86)\Common Files\Microsoft Shared\DevServer\10.0\image1.jpg" denied.
                File.WriteAllBytes(filePath, capture);
                return true;
            }
            return false;
        } // End Function InternalCaptureAsFile


        /// <summary>
        /// Captures a frame from the webcam and returns the byte array associated
        /// with the captured image. Runs in a newly-created STA thread which is 
        /// required for this method of capture
        /// </summary>
        /// <param name="connectDelay">number of milliseconds to wait between connect 
        /// and capture - necessary for some cameras that take a while to 'warm up'</param>
        /// <returns>byte array representing a bitmp or null (if error or no webcam)</returns>
        private static byte[] InternalCaptureToByteArrayInThread(int connectDelay = 500)
        {
            byte[] bytes = null;
            Thread catureThread = new Thread(() =>
            {
                bytes = InternalCaptureToByteArray(connectDelay);
            });
            catureThread.SetApartmentState(ApartmentState.STA);
            catureThread.Start();
            catureThread.Join();
            return bytes;
        } // End Function InternalCaptureToByteArrayInThread


        /// <summary>
        /// Captures a frame from the webcam and returns the byte array associated
        /// with the captured image
        /// </summary>
        /// <param name="connectDelay">number of milliseconds to wait between connect 
        /// and capture - necessary for some cameras that take a while to 'warm up'</param>
        /// <returns>byte array representing a bitmp or null (if error or no webcam)</returns>
        private static byte[] InternalCaptureToByteArray(int connectDelay = 500)
        {
            Clipboard.Clear();                                              // clear the clipboard
            int hCaptureWnd = capCreateCaptureWindowA("ccWebCam", 0, 0, 0,  // create the hidden capture window
                350, 350, 0, 0);
            SendMessage(hCaptureWnd, WM_CAP_CONNECT, 0, 0);                 // send the connect message to it
            Thread.Sleep(connectDelay);                                     // sleep the specified time
            SendMessage(hCaptureWnd, WM_CAP_GET_FRAME, 0, 0);               // capture the frame
            SendMessage(hCaptureWnd, WM_CAP_COPY, 0, 0);                    // copy it to the clipboard
            SendMessage(hCaptureWnd, WM_CAP_DISCONNECT, 0, 0);              // disconnect from the camera
            Bitmap bitmap = (Bitmap)Clipboard.GetDataObject().GetData(DataFormats.Bitmap);  // copy into bitmap

            if (bitmap == null)
                return null;

            using (MemoryStream stream = new MemoryStream())
            {
                bitmap.Save(stream, ImageFormat.Bmp);    // get bitmap bytes
                return stream.ToArray();
            } // End Using stream

        } // End Function InternalCaptureToByteArray


    }


}

I tried like this, but it only gets a black image...

    [DllImport("user32.dll")]
    static extern IntPtr GetWindowDC(IntPtr hWnd);

    [DllImport("gdi32.dll", SetLastError = true)]
    static extern IntPtr CreateCompatibleDC(IntPtr hdc);

    enum TernaryRasterOperations : uint
    {
        /// <summary>dest = source</summary>
        SRCCOPY = 0x00CC0020,
        /// <summary>dest = source OR dest</summary>
        SRCPAINT = 0x00EE0086,
        /// <summary>dest = source AND dest</summary>
        SRCAND = 0x008800C6,
        /// <summary>dest = source XOR dest</summary>
        SRCINVERT = 0x00660046,
        /// <summary>dest = source AND (NOT dest)</summary>
        SRCERASE = 0x00440328,
        /// <summary>dest = (NOT source)</summary>
        NOTSRCCOPY = 0x00330008,
        /// <summary>dest = (NOT src) AND (NOT dest)</summary>
        NOTSRCERASE = 0x001100A6,
        /// <summary>dest = (source AND pattern)</summary>
        MERGECOPY = 0x00C000CA,
        /// <summary>dest = (NOT source) OR dest</summary>
        MERGEPAINT = 0x00BB0226,
        /// <summary>dest = pattern</summary>
        PATCOPY = 0x00F00021,
        /// <summary>dest = DPSnoo</summary>
        PATPAINT = 0x00FB0A09,
        /// <summary>dest = pattern XOR dest</summary>
        PATINVERT = 0x005A0049,
        /// <summary>dest = (NOT dest)</summary>
        DSTINVERT = 0x00550009,
        /// <summary>dest = BLACK</summary>
        BLACKNESS = 0x00000042,
        /// <summary>dest = WHITE</summary>
        WHITENESS = 0x00FF0062,
        /// <summary>
        /// Capture window as seen on screen.  This includes layered windows 
        /// such as WPF windows with AllowsTransparency="true"
        /// </summary>
        CAPTUREBLT = 0x40000000
    }

    [DllImport("gdi32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool BitBlt(IntPtr hdc, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, TernaryRasterOperations dwRop);

    [DllImport("gdi32.dll")]
    static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);

    [DllImport("gdi32.dll", ExactSpelling = true, PreserveSig = true, SetLastError = true)]
    static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);

    [DllImport("gdi32.dll")]
    static extern bool DeleteDC(IntPtr hdc);

    [DllImport("user32.dll")]
    static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC);

    [DllImport("gdi32.dll")]
    static extern bool DeleteObject(IntPtr hObject);


    public static void ScreenshotWindow(IntPtr windowHandle)
    {
        Rect Rect = new Rect();

        GetWindowRect(windowHandle, ref Rect);
        int width = Rect.Right - Rect.Left;
        int height = Rect.Bottom - Rect.Top;

        IntPtr windowDeviceContext = GetWindowDC(windowHandle);
        IntPtr destDeviceContext = CreateCompatibleDC(windowDeviceContext);
        IntPtr bitmapHandle = CreateCompatibleBitmap(windowDeviceContext, width, height);
        IntPtr oldObject = SelectObject(destDeviceContext, bitmapHandle);

        BitBlt(destDeviceContext, 0, 0, width, height, windowDeviceContext, 0, 0, TernaryRasterOperations.CAPTUREBLT | TernaryRasterOperations.SRCCOPY);
        SelectObject(destDeviceContext, oldObject);

        DeleteDC(destDeviceContext);
        ReleaseDC(windowHandle, destDeviceContext);


        Image screenshot = Image.FromHbitmap(bitmapHandle);
        DeleteObject(bitmapHandle);

        screenshot.Save("d:\\temp\\mywebcamimage.png", System.Drawing.Imaging.ImageFormat.Png);

        /*
        // TODO - Remove above save when it works
        using (MemoryStream stream = new MemoryStream())
        {
            screenshot.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
            return stream.ToArray();
        }
        */
    }

And then this after SendMessage(hCaptureWnd, WM_CAP_GET_FRAME, 0, 0);

ScreenshotWindow(new IntPtr(hCaptureWnd));
c#
winapi
video
webcam
vfw
asked on Stack Overflow Apr 24, 2013 by Stefan Steiger • edited Apr 24, 2013 by (unknown user)

3 Answers

6

There is no such thing as WM_CAP_GET_FRAME. The correct name of the message is WM_CAP_GRAB_FRAME and it's described on MSDN.

What it does is:

The WM_CAP_GRAB_FRAME message retrieves and displays a single frame from the capture driver. After capture, overlay and preview are disabled. You can send this message explicitly or by using the capGrabFrame macro.

To obtain the actual data you need to use frame callback as described further on MSDN. The callback gets you the picture bytes, which you can write to file or use for whatever processing without need to transfer through clipboard.

...is the callback function used with streaming capture to optionally process a frame of captured video. The name capVideoStreamCallback is a placeholder for the application-supplied function name.

[And you have there a] ... Pointer to a VIDEOHDR structure containing information about the captured frame.

Again, this API is unlucky choice for video capture. Too old, too limited.

answered on Stack Overflow Apr 24, 2013 by Roman R. • edited Jun 20, 2020 by Community
3

You have to send a different message, specifically WM_CAP_FILE_SAVEDIB, to save data in a file on disk. You then'll be able to load it in a Bitmap object for further processing (I'm not aware of any builtin cam-to-byte[] functionality).

[DllImport("user32", EntryPoint = "SendMessage")]
private static extern int SendMessage(
    int hWnd, uint Msg, int wParam, string strFileName);

private const int WM_USER = 0x0400;
private const int WM_CAP_START = WM_USER;
private const int WM_CAP_FILE_SAVEDIB = WM_CAP_START + 25;

//before
SendMessage(hCaptureWnd, WM_CAP_COPY, 0, 0);

//after
string tempFile = Server.MapPath("~/App_Data/tempCap.bmp");
SendMessage(hCaptureWnd, WM_CAP_FILE_SAVEDIB, 0, tempFile); //create tempfile
Bitmap bitmap = new Bitmap(tempFile); //read tempfile
using (MemoryStream stream = new MemoryStream())
{
    bitmap.Save(stream, ImageFormat.Bmp);
    return stream.ToArray();
}
answered on Stack Overflow Apr 24, 2013 by Alex • edited Apr 24, 2013 by Alex
1

Building on Roman R.'s answer:

The finer point of morality is that you need to register the callback frame, and then call grabframe, and that you cannot directly cast C-style char[] to byte[], and that you get raw bitmap data - not a bitmap, and that the image size is 640x480, irrespective of what is set in capCreateCaptureWindowA, and that lpData needs to be a IntPtr, not a UIntPtr, because Marshal.Copy has no overload for UIntPtr, and that using WriteBitmapFile, it's possible to write raw bitmap data into a bitmap WITHOUT using unsafe code or mapping the bitmap file headers, and that whoever wrote Marshal.Copy made it possible to copy a negative value, because length is int, not uint...

Additionally, there is a need to rotate the image 180 oldDegrees for whatever reason...
Also, I changed the WM constants to their correct names.

    SendMessage(hCaptureWnd, WM_CAP_SET_CALLBACK_FRAME, 0, capVideoStreamCallback);
    SendMessage(hCaptureWnd, WM_CAP_GRAB_FRAME, 0, 0);               // capture the frame

With these additional things

    // http://msdn.microsoft.com/en-us/library/windows/desktop/dd757688(v=vs.85).aspx
    [StructLayout(LayoutKind.Sequential)]
    private struct VIDEOHDR 
    {
        // http://msdn.microsoft.com/en-us/library/windows/desktop/aa383751(v=vs.85).aspx


        // typedef unsigned char BYTE;
        // typedef BYTE far *LPBYTE;
        // unsigned char* lpData


        //public byte[] lpData; // LPBYTE    lpData; // Aaargh, invalid cast, not a .NET byte array...
        public IntPtr lpData; // LPBYTE    lpData;
        public UInt32 dwBufferLength; // DWORD     dwBufferLength;
        public UInt32 dwBytesUsed; // DWORD     dwBytesUsed;
        public UInt32 dwTimeCaptured; // DWORD     dwTimeCaptured;


        // typedef ULONG_PTR DWORD_PTR;
        // #if defined(_WIN64)  
        //   typedef unsigned __int64 ULONG_PTR;
        // #else
        //   typedef unsigned long ULONG_PTR;
        // #endif
        public IntPtr dwUser; // DWORD_PTR dwUser; 
        public UInt32 dwFlags; // DWORD     dwFlags;

        [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.ByValArray, SizeConst = 4)]
        public System.UIntPtr[] dwReserved; // DWORD_PTR dwReserved[4];

        // Does not make a difference
        //public System.UIntPtr[] dwReserved = new System.UIntPtr[4]; // DWORD_PTR dwReserved[4];
    }




    private delegate System.IntPtr capVideoStreamCallback_t(System.UIntPtr hWnd, ref VIDEOHDR lpVHdr);
    [DllImport("user32", EntryPoint = "SendMessage")]
    private static extern int SendMessage(int hWnd, uint Msg, int wParam, capVideoStreamCallback_t routine);




    // http://eris.liralab.it/yarpdoc/vfw__extra__from__wine_8h.html
    private const int WM_USER = 0x0400; // 1024
    private const int WM_CAP_START = WM_USER;
    private const int WM_CAP_DRIVER_CONNECT = WM_CAP_START + 10;
    private const int WM_CAP_DRIVER_DISCONNECT = WM_CAP_START + 11;

    private const int WM_CAP_FILE_SAVEDIB = WM_CAP_START + 25;
    private const int WM_CAP_SET_CALLBACK_FRAME = WM_CAP_START + 5;
    private const int WM_CAP_GRAB_FRAME = WM_CAP_START + 60;
    private const int WM_CAP_EDIT_COPY = WM_CAP_START + 30;




    // http://lists.ximian.com/pipermail/mono-devel-list/2011-March/037272.html

    private static byte[] baSplendidIsolation;


    private static System.IntPtr capVideoStreamCallback(System.UIntPtr hWnd, ref VIDEOHDR lpVHdr)
    {
        //System.Windows.Forms.MessageBox.Show("hello");
        //System.Windows.Forms.MessageBox.Show(lpVHdr.dwBufferLength.ToString() + " " + lpVHdr.dwBytesUsed.ToString());
        byte[] _imageTemp = new byte[lpVHdr.dwBufferLength];
        Marshal.Copy(lpVHdr.lpData, _imageTemp, 0, (int) lpVHdr.dwBufferLength);
        //System.IO.File.WriteAllBytes(@"d:\temp\mycbfile.bmp", _imageTemp); // AAaaarg, it's raw bitmap data...

        // http://stackoverflow.com/questions/742236/how-to-create-a-bmp-file-from-byte-in-c-sharp
        // http://stackoverflow.com/questions/2654480/writing-bmp-image-in-pure-c-c-without-other-libraries

        // Tsssss... 350 x 350 was the expected setting, but never mind... 
        // fortunately alex told me about WM_CAP_FILE_SAVEDIB, so I could compare to the direct output
        int width = 640;
        int height = 480;
        int stride = width*3;

        baSplendidIsolation = null;
        baSplendidIsolation = WriteBitmapFile(@"d:\temp\mycbfilecc.bmp", width, height, _imageTemp);

        /*
        unsafe
        {
            fixed (byte* ptr = _imageTemp)
            {
                using (Bitmap image = new Bitmap(width, height, stride, PixelFormat.Format24bppRgb, new IntPtr(ptr)))
                {
                    image.Save(@"d:\temp\mycbfile2.bmp");
                }
            }
        }
        */

        //var hdr = (Elf32_Phdr)Marshal.PtrToStructure(ptr, typeof(Elf32_Phdr));
        return System.IntPtr.Zero;
    }


    private static byte[] WriteBitmapFile(string filename, int width, int height, byte[] imageData)
    {
        using (var stream = new MemoryStream(imageData))
        using (var bmp = new Bitmap(width, height, PixelFormat.Format24bppRgb))
        {
            BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0,bmp.Width, bmp.Height)
                                                ,ImageLockMode.WriteOnly
                                                ,bmp.PixelFormat
            );

            Marshal.Copy(imageData, 0, bmpData.Scan0, imageData.Length);

            bmp.UnlockBits(bmpData);


            if (bmp == null)
                return null;

            bmp.RotateFlip(RotateFlipType.Rotate180FlipNone);
            bmp.Save(filename); // For testing only

            using (MemoryStream ms = new MemoryStream())
            {
                bmp.Save(ms, ImageFormat.Png);    // get bitmap bytes
                return ms.ToArray();
            } // End Using stream

        }

    } // End Function WriteBitmapFile


    /// <summary>
    /// Captures a frame from the webcam and returns the byte array associated
    /// with the captured image
    /// </summary>
    /// <param name="connectDelay">number of milliseconds to wait between connect 
    /// and capture - necessary for some cameras that take a while to 'warm up'</param>
    /// <returns>byte array representing a bitmp or null (if error or no webcam)</returns>
    private static byte[] InternalCaptureToByteArray(int connectDelay = 500)
    {
        Clipboard.Clear(); 
        int hCaptureWnd = capCreateCaptureWindowA("ccWebCam", 0, 0, 0,
            350, 350, 0, 0); // create the hidden capture window
        SendMessage(hCaptureWnd, WM_CAP_DRIVER_CONNECT, 0, 0); // send the connect message to it
        //SendMessage(hCaptureWnd, WM_CAP_DRIVER_CONNECT, i, 0); // i device number retval != 0 --> valid device_id

        Thread.Sleep(connectDelay);                                     // sleep the specified time
        SendMessage(hCaptureWnd, WM_CAP_SET_CALLBACK_FRAME, 0, capVideoStreamCallback);
        SendMessage(hCaptureWnd, WM_CAP_GRAB_FRAME, 0, 0);               // capture the frame

        //SendMessage(hCaptureWnd, WM_CAP_FILE_SAVEDIB, 0, "d:\\temp\\testmywebcamimage.bmp");
        //ScreenshotWindow(new IntPtr(hCaptureWnd));

        //SendMessage(hCaptureWnd, WM_CAP_EDIT_COPY, 0, 0); // copy it to the clipboard


        // using (Graphics g2 = Graphics.FromHwnd(new IntPtr(hCaptureWnd)))

        SendMessage(hCaptureWnd, WM_CAP_DRIVER_DISCONNECT, 0, 0);              // disconnect from the camera

        return baSplendidIsolation;

        /*
        Bitmap bitmap = (Bitmap)Clipboard.GetDataObject().GetData(DataFormats.Bitmap);  // copy into bitmap

        if (bitmap == null)
            return null;

        using (MemoryStream stream = new MemoryStream())
        {
            bitmap.Save(stream, ImageFormat.Bmp);    // get bitmap bytes
            return stream.ToArray();
        } // End Using stream
        */
    } // End Function InternalCaptureToByteArray
answered on Stack Overflow Apr 25, 2013 by Stefan Steiger • edited Oct 7, 2016 by Stefan Steiger

User contributions licensed under CC BY-SA 3.0