mirror of
https://github.com/ShareX/ShareX.git
synced 2024-08-17 19:21:55 +12:00
366 lines
No EOL
18 KiB
C#
366 lines
No EOL
18 KiB
C#
#region License Information (GPL v3)
|
|
|
|
/*
|
|
ShareX - A program that allows you to take screenshots and share any file type
|
|
Copyright (c) 2007-2020 ShareX Team
|
|
|
|
This program is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU General Public License
|
|
as published by the Free Software Foundation; either version 2
|
|
of the License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
Optionally you can also view the license at <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#endregion License Information (GPL v3)
|
|
|
|
using System;
|
|
using System.Drawing;
|
|
using System.Drawing.Imaging;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.Windows.Forms;
|
|
|
|
namespace ShareX.HelpersLib
|
|
{
|
|
internal class ClipboardHelpersEx
|
|
{
|
|
// Credits: https://stackoverflow.com/a/46424800/264877
|
|
|
|
/// <summary>
|
|
/// Copies the given image to the clipboard as PNG, DIB and standard Bitmap format.
|
|
/// </summary>
|
|
/// <param name="image">Image to put on the clipboard.</param>
|
|
/// <param name="imageNoTr">Optional specifically nontransparent version of the image to put on the clipboard.</param>
|
|
/// <param name="data">Clipboard data object to put the image into. Might already contain other stuff. Leave null to create a new one.</param>
|
|
public static void SetClipboardImage(Bitmap image, Bitmap imageNoTr, DataObject data)
|
|
{
|
|
Clipboard.Clear();
|
|
if (data == null)
|
|
data = new DataObject();
|
|
if (imageNoTr == null)
|
|
imageNoTr = image;
|
|
using (MemoryStream pngMemStream = new MemoryStream())
|
|
using (MemoryStream dibMemStream = new MemoryStream())
|
|
{
|
|
// As standard bitmap, without transparency support
|
|
data.SetData(DataFormats.Bitmap, true, imageNoTr);
|
|
// As PNG. Gimp will prefer this over the other two.
|
|
image.Save(pngMemStream, ImageFormat.Png);
|
|
data.SetData("PNG", false, pngMemStream);
|
|
// As DIB. This is (wrongly) accepted as ARGB by many applications.
|
|
byte[] dibData = ConvertToDib(image);
|
|
dibMemStream.Write(dibData, 0, dibData.Length);
|
|
data.SetData(DataFormats.Dib, false, dibMemStream);
|
|
// The 'copy=true' argument means the MemoryStreams can be safely disposed after the operation.
|
|
Clipboard.SetDataObject(data, true);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts the image to Device Independent Bitmap format of type BITFIELDS.
|
|
/// This is (wrongly) accepted by many applications as containing transparency,
|
|
/// so I'm abusing it for that.
|
|
/// </summary>
|
|
/// <param name="image">Image to convert to DIB</param>
|
|
/// <returns>The image converted to DIB, in bytes.</returns>
|
|
public static byte[] ConvertToDib(Image image)
|
|
{
|
|
byte[] bm32bData;
|
|
int width = image.Width;
|
|
int height = image.Height;
|
|
// Ensure image is 32bppARGB by painting it on a new 32bppARGB image.
|
|
using (Bitmap bm32b = new Bitmap(image.Width, image.Height, PixelFormat.Format32bppPArgb))
|
|
{
|
|
using (Graphics gr = Graphics.FromImage(bm32b))
|
|
gr.DrawImage(image, new Rectangle(0, 0, bm32b.Width, bm32b.Height));
|
|
// Bitmap format has its lines reversed.
|
|
bm32b.RotateFlip(RotateFlipType.Rotate180FlipX);
|
|
int stride;
|
|
bm32bData = GetImageData(bm32b, out stride);
|
|
}
|
|
// BITMAPINFOHEADER struct for DIB.
|
|
int hdrSize = 0x28;
|
|
byte[] fullImage = new byte[hdrSize + 12 + bm32bData.Length];
|
|
//Int32 biSize;
|
|
WriteIntToByteArray(fullImage, 0x00, 4, true, (uint)hdrSize);
|
|
//Int32 biWidth;
|
|
WriteIntToByteArray(fullImage, 0x04, 4, true, (uint)width);
|
|
//Int32 biHeight;
|
|
WriteIntToByteArray(fullImage, 0x08, 4, true, (uint)height);
|
|
//Int16 biPlanes;
|
|
WriteIntToByteArray(fullImage, 0x0C, 2, true, 1);
|
|
//Int16 biBitCount;
|
|
WriteIntToByteArray(fullImage, 0x0E, 2, true, 32);
|
|
//BITMAPCOMPRESSION biCompression = BITMAPCOMPRESSION.BITFIELDS;
|
|
WriteIntToByteArray(fullImage, 0x10, 4, true, 3);
|
|
//Int32 biSizeImage;
|
|
WriteIntToByteArray(fullImage, 0x14, 4, true, (uint)bm32bData.Length);
|
|
// These are all 0. Since .net clears new arrays, don't bother writing them.
|
|
//Int32 biXPelsPerMeter = 0;
|
|
//Int32 biYPelsPerMeter = 0;
|
|
//Int32 biClrUsed = 0;
|
|
//Int32 biClrImportant = 0;
|
|
|
|
// The aforementioned "BITFIELDS": colour masks applied to the Int32 pixel value to get the R, G and B values.
|
|
WriteIntToByteArray(fullImage, hdrSize + 0, 4, true, 0x00FF0000);
|
|
WriteIntToByteArray(fullImage, hdrSize + 4, 4, true, 0x0000FF00);
|
|
WriteIntToByteArray(fullImage, hdrSize + 8, 4, true, 0x000000FF);
|
|
Array.Copy(bm32bData, 0, fullImage, hdrSize + 12, bm32bData.Length);
|
|
return fullImage;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves an image from the given clipboard data object, in the order PNG, DIB, Bitmap, Image object.
|
|
/// </summary>
|
|
/// <param name="retrievedData">The clipboard data.</param>
|
|
/// <returns>The extracted image, or null if no supported image type was found.</returns>
|
|
public static Bitmap GetClipboardImage(DataObject retrievedData)
|
|
{
|
|
Bitmap clipboardimage = null;
|
|
// Order: try PNG, move on to try 32-bit ARGB DIB, then try the normal Bitmap and Image types.
|
|
if (retrievedData.GetDataPresent("PNG"))
|
|
{
|
|
MemoryStream png_stream = retrievedData.GetData("PNG") as MemoryStream;
|
|
if (png_stream != null)
|
|
using (Bitmap bm = new Bitmap(png_stream))
|
|
clipboardimage = CloneImage(bm);
|
|
}
|
|
if (clipboardimage == null && retrievedData.GetDataPresent(DataFormats.Dib))
|
|
{
|
|
MemoryStream dib = retrievedData.GetData(DataFormats.Dib) as MemoryStream;
|
|
if (dib != null)
|
|
clipboardimage = ImageFromClipboardDib(dib.ToArray());
|
|
}
|
|
if (clipboardimage == null && retrievedData.GetDataPresent(DataFormats.Bitmap))
|
|
clipboardimage = new Bitmap(retrievedData.GetData(DataFormats.Bitmap) as Image);
|
|
if (clipboardimage == null && retrievedData.GetDataPresent(typeof(Image)))
|
|
clipboardimage = new Bitmap(retrievedData.GetData(typeof(Image)) as Image);
|
|
return clipboardimage;
|
|
}
|
|
|
|
public static Bitmap ImageFromClipboardDib(byte[] dibBytes)
|
|
{
|
|
if (dibBytes == null || dibBytes.Length < 4)
|
|
return null;
|
|
try
|
|
{
|
|
int headerSize = (int)ReadIntFromByteArray(dibBytes, 0, 4, true);
|
|
// Only supporting 40-byte DIB from clipboard
|
|
if (headerSize != 40)
|
|
return null;
|
|
byte[] header = new byte[40];
|
|
Array.Copy(dibBytes, header, 40);
|
|
int imageIndex = headerSize;
|
|
int width = (int)ReadIntFromByteArray(header, 0x04, 4, true);
|
|
int height = (int)ReadIntFromByteArray(header, 0x08, 4, true);
|
|
short planes = (short)ReadIntFromByteArray(header, 0x0C, 2, true);
|
|
short bitCount = (short)ReadIntFromByteArray(header, 0x0E, 2, true);
|
|
//Compression: 0 = RGB; 3 = BITFIELDS.
|
|
int compression = (int)ReadIntFromByteArray(header, 0x10, 4, true);
|
|
// Not dealing with non-standard formats.
|
|
if (planes != 1 || (compression != 0 && compression != 3))
|
|
return null;
|
|
PixelFormat fmt;
|
|
switch (bitCount)
|
|
{
|
|
case 32:
|
|
fmt = PixelFormat.Format32bppRgb;
|
|
break;
|
|
case 24:
|
|
fmt = PixelFormat.Format24bppRgb;
|
|
break;
|
|
case 16:
|
|
fmt = PixelFormat.Format16bppRgb555;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
if (compression == 3)
|
|
imageIndex += 12;
|
|
if (dibBytes.Length < imageIndex)
|
|
return null;
|
|
byte[] image = new byte[dibBytes.Length - imageIndex];
|
|
Array.Copy(dibBytes, imageIndex, image, 0, image.Length);
|
|
// Classic stride: fit within blocks of 4 bytes.
|
|
int stride = (((((bitCount * width) + 7) / 8) + 3) / 4) * 4;
|
|
if (compression == 3)
|
|
{
|
|
uint redMask = ReadIntFromByteArray(dibBytes, headerSize + 0, 4, true);
|
|
uint greenMask = ReadIntFromByteArray(dibBytes, headerSize + 4, 4, true);
|
|
uint blueMask = ReadIntFromByteArray(dibBytes, headerSize + 8, 4, true);
|
|
// Fix for the undocumented use of 32bppARGB disguised as BITFIELDS. Despite lacking an alpha bit field,
|
|
// the alpha bytes are still filled in, without any header indication of alpha usage.
|
|
// Pure 32-bit RGB: check if a switch to ARGB can be made by checking for non-zero alpha.
|
|
// Admitted, this may give a mess if the alpha bits simply aren't cleared, but why the hell wouldn't it use 24bpp then?
|
|
if (bitCount == 32 && redMask == 0xFF0000 && greenMask == 0x00FF00 && blueMask == 0x0000FF)
|
|
{
|
|
// Stride is always a multiple of 4; no need to take it into account for 32bpp.
|
|
for (int pix = 3; pix < image.Length; pix += 4)
|
|
{
|
|
// 0 can mean transparent, but can also mean the alpha isn't filled in, so only check for non-zero alpha,
|
|
// which would indicate there is actual data in the alpha bytes.
|
|
if (image[pix] == 0)
|
|
continue;
|
|
fmt = PixelFormat.Format32bppPArgb;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
// Could be supported with a system that parses the colour masks,
|
|
// but I don't think the clipboard ever uses these anyway.
|
|
return null;
|
|
}
|
|
Bitmap bitmap = BuildImage(image, width, height, stride, fmt, null, null);
|
|
// This is bmp; reverse image lines.
|
|
bitmap.RotateFlip(RotateFlipType.Rotate180FlipX);
|
|
return bitmap;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static void WriteIntToByteArray(byte[] data, int startIndex, int bytes, bool littleEndian, uint value)
|
|
{
|
|
int lastByte = bytes - 1;
|
|
if (data.Length < startIndex + bytes)
|
|
throw new ArgumentOutOfRangeException("startIndex", "Data array is too small to write a " + bytes + "-byte value at offset " + startIndex + ".");
|
|
for (int index = 0; index < bytes; index++)
|
|
{
|
|
int offs = startIndex + (littleEndian ? index : lastByte - index);
|
|
data[offs] = (byte)(value >> (8 * index) & 0xFF);
|
|
}
|
|
}
|
|
|
|
public static uint ReadIntFromByteArray(byte[] data, int startIndex, int bytes, bool littleEndian)
|
|
{
|
|
int lastByte = bytes - 1;
|
|
if (data.Length < startIndex + bytes)
|
|
throw new ArgumentOutOfRangeException("startIndex", "Data array is too small to read a " + bytes + "-byte value at offset " + startIndex + ".");
|
|
uint value = 0;
|
|
for (int index = 0; index < bytes; index++)
|
|
{
|
|
int offs = startIndex + (littleEndian ? index : lastByte - index);
|
|
value += (uint)(data[offs] << (8 * index));
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the raw bytes from an image.
|
|
/// </summary>
|
|
/// <param name="sourceImage">The image to get the bytes from.</param>
|
|
/// <param name="stride">Stride of the retrieved image data.</param>
|
|
/// <returns>The raw bytes of the image</returns>
|
|
public static byte[] GetImageData(Bitmap sourceImage, out int stride)
|
|
{
|
|
BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, sourceImage.Width, sourceImage.Height), ImageLockMode.ReadOnly, sourceImage.PixelFormat);
|
|
stride = sourceData.Stride;
|
|
byte[] data = new byte[stride * sourceImage.Height];
|
|
Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
|
|
sourceImage.UnlockBits(sourceData);
|
|
return data;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a bitmap based on data, width, height, stride and pixel format.
|
|
/// </summary>
|
|
/// <param name="sourceData">Byte array of raw source data</param>
|
|
/// <param name="width">Width of the image</param>
|
|
/// <param name="height">Height of the image</param>
|
|
/// <param name="stride">Scanline length inside the data</param>
|
|
/// <param name="pixelFormat">Pixel format</param>
|
|
/// <param name="palette">Color palette</param>
|
|
/// <param name="defaultColor">Default color to fill in on the palette if the given colors don't fully fill it.</param>
|
|
/// <returns>The new image</returns>
|
|
public static Bitmap BuildImage(byte[] sourceData, int width, int height, int stride, PixelFormat pixelFormat, Color[] palette, Color? defaultColor)
|
|
{
|
|
Bitmap newImage = new Bitmap(width, height, pixelFormat);
|
|
BitmapData targetData = newImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, newImage.PixelFormat);
|
|
int newDataWidth = ((Image.GetPixelFormatSize(pixelFormat) * width) + 7) / 8;
|
|
// Compensate for possible negative stride on BMP format.
|
|
bool isFlipped = stride < 0;
|
|
stride = Math.Abs(stride);
|
|
// Cache these to avoid unnecessary getter calls.
|
|
int targetStride = targetData.Stride;
|
|
long scan0 = targetData.Scan0.ToInt64();
|
|
for (int y = 0; y < height; y++)
|
|
Marshal.Copy(sourceData, y * stride, new IntPtr(scan0 + y * targetStride), newDataWidth);
|
|
newImage.UnlockBits(targetData);
|
|
// Fix negative stride on BMP format.
|
|
if (isFlipped)
|
|
newImage.RotateFlip(RotateFlipType.Rotate180FlipX);
|
|
// For indexed images, set the palette.
|
|
if ((pixelFormat & PixelFormat.Indexed) != 0 && palette != null)
|
|
{
|
|
ColorPalette pal = newImage.Palette;
|
|
for (int i = 0; i < pal.Entries.Length; i++)
|
|
{
|
|
if (i < palette.Length)
|
|
pal.Entries[i] = palette[i];
|
|
else if (defaultColor.HasValue)
|
|
pal.Entries[i] = defaultColor.Value;
|
|
else
|
|
break;
|
|
}
|
|
newImage.Palette = pal;
|
|
}
|
|
return newImage;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clones an image object to free it from any backing resources.
|
|
/// Code taken from http://stackoverflow.com/a/3661892/ with some extra fixes.
|
|
/// </summary>
|
|
/// <param name="sourceImage">The image to clone</param>
|
|
/// <returns>The cloned image</returns>
|
|
public static Bitmap CloneImage(Bitmap sourceImage)
|
|
{
|
|
Rectangle rect = new Rectangle(0, 0, sourceImage.Width, sourceImage.Height);
|
|
Bitmap targetImage = new Bitmap(rect.Width, rect.Height, sourceImage.PixelFormat);
|
|
targetImage.SetResolution(sourceImage.HorizontalResolution, sourceImage.VerticalResolution);
|
|
BitmapData sourceData = sourceImage.LockBits(rect, ImageLockMode.ReadOnly, sourceImage.PixelFormat);
|
|
BitmapData targetData = targetImage.LockBits(rect, ImageLockMode.WriteOnly, targetImage.PixelFormat);
|
|
int actualDataWidth = ((Image.GetPixelFormatSize(sourceImage.PixelFormat) * rect.Width) + 7) / 8;
|
|
int h = sourceImage.Height;
|
|
int origStride = sourceData.Stride;
|
|
bool isFlipped = origStride < 0;
|
|
origStride = Math.Abs(origStride); // Fix for negative stride in BMP format.
|
|
int targetStride = targetData.Stride;
|
|
byte[] imageData = new byte[actualDataWidth];
|
|
IntPtr sourcePos = sourceData.Scan0;
|
|
IntPtr destPos = targetData.Scan0;
|
|
// Copy line by line, skipping by stride but copying actual data width
|
|
for (int y = 0; y < h; y++)
|
|
{
|
|
Marshal.Copy(sourcePos, imageData, 0, actualDataWidth);
|
|
Marshal.Copy(imageData, 0, destPos, actualDataWidth);
|
|
sourcePos = new IntPtr(sourcePos.ToInt64() + origStride);
|
|
destPos = new IntPtr(destPos.ToInt64() + targetStride);
|
|
}
|
|
targetImage.UnlockBits(targetData);
|
|
sourceImage.UnlockBits(sourceData);
|
|
// Fix for negative stride on BMP format.
|
|
if (isFlipped)
|
|
targetImage.RotateFlip(RotateFlipType.Rotate180FlipX);
|
|
// For indexed images, restore the palette. This is not linking to a referenced
|
|
// object in the original image; the getter of Palette creates a new object when called.
|
|
if ((sourceImage.PixelFormat & PixelFormat.Indexed) != 0)
|
|
targetImage.Palette = sourceImage.Palette;
|
|
// Restore DPI settings
|
|
targetImage.SetResolution(sourceImage.HorizontalResolution, sourceImage.VerticalResolution);
|
|
return targetImage;
|
|
}
|
|
}
|
|
} |