WPFのClipboardはイマイチな感じで、画像を取得するにはGetImage()を使うんだけど、普通の画像が完全透明になってしまうことがある
透明にならないように取得するには
blogs.yahoo.co.jp
こちらで紹介されているように
Streamに一時保存して読み直すか、問答無用でピクセルフォーマットBgr32に変換する方法が良さそう
透明になってしまう状態というか画像
これはクリップボードにある画像のbpp(色深度)が関係していて、32bpp未満の画像だと透明になってしまう…きがする、逆に言うと32bppの画像なら問題ない
e-words.jp
アプリによってクリップボードにコピーするときの挙動が違って、どんな画像でも32bppに変換するアプリと、そうじゃないアプリがある
32bppに変換してコピーするアプリは
変換しないアプリは
Microsoft Edgeで表示されている画像をコピーして、GetImage()して表示してみる
この前の記事をMicrosoft Edgeで表示して、画像をコピーしているところ、これを
BitmapSource source = Clipboard.GetImage();
で取得した画像を表示してみる
表示はされているけど完全透明なので何も見えない
32bppに変換してコピーするアプリからなら
問題なく取得できるので表示される、こうなってほしい
BitmapSourceクラスのCopyPixels()でピクセルの色の値を見てみる
private byte[] GetPixels(BitmapSource source)
{
int w = source.PixelWidth;
int h = source.PixelHeight;
int stride = w * 4;
byte[] pixels = new byte[h * stride];
source.CopyPixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
return pixels;
}
GetImageで取得したBitmapSourceをこれに渡す
32bppに変換するアプリからだと
いいね、ピクセルフォーマットはBgra32なのでアルファの値は[3]、[7]、[11]…のところを見ると、すべて255なので問題なく表示される
32bppに変換しないアプリからだと
あかん、全て0になっているので、この画像は完全透明
Streamに一時保存してから読み込む方法
private BitmapSource GetClipboardBitmapEncDec()
{
BitmapSource source = Clipboard.GetImage();
if (source == null)
{
return null;
}
var encoder = new BmpBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(source));
using (var stream = new System.IO.MemoryStream())
{
encoder.Save(stream);
stream.Seek(0, System.IO.SeekOrigin.Begin);
var decoder = new BmpBitmapDecoder(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
source = decoder.Frames[0];
}
return source;
}
これを使って表示すると
表示された、ピクセルフォーマットがBgr32になっているねえ、それはいいんだけどdpiが中途半端な値になるのが気になる
ピクセルフォーマットをBgr32へ変換する方法
private BitmapSource GetClipboarBitmapBgr32()
{
BitmapSource source = Clipboard.GetImage();
if (source == null) return null;
return new FormatConvertedBitmap(source, PixelFormats.Bgr32, null, 0);
}
これを使うと
表示されるし、dpiも96なので、この方法が良さそう
クリップボードにある画像のbppを取得したいけど
透明や半透明ピクセルがある画像、つまりアルファの値が255以外の画像もBgr32へ変換してしまうと半透明部分が不透明で表示になってしまうので、そういう画像の場合は変換しないでそのままで取得したい、ってことはクリップボードにある画像のbppが32未満か、そうでないのかを判定する必要がある、でも、GetImage()で取得した時点でピクセルフォーマットはbpp32のBgra32になってしまうので、別の方法で取得する必要がある
クリップボードにあるデータのフォーマット形式?
なんかいっぱいあるみたいで、これは
Clipboard.GetDataObject().GetFormats();
ってすると取得できるので一覧を表示する
var formats = Clipboard.GetDataObject().GetFormats();
string name = "";
foreach (var item in formats)
{
name += Environment.NewLine + item;
}
MessageBox.Show(name);
これで見てみると
PrintScreenキーを押したときのクリップボードの中
上の3つは普通のBitmapだなあってわかる、4つ目のDeviceIndependentBitmapってのは見慣れない形式だけどBitmapって付いてるから画像っぽい、5つめはさっぱりわからん
Google Chromeに表示された画像をコピー
画像形式もあるけどその他に、ウェブブラウザだからかHTMLってのがある
Microsoft Edgeで表示されている画像をコピーしたとき
随分違うねえ、画像っぽいのはDeviceIndependentBitmapだけ
メモ帳の文字列をコピーしたときのなか
Localeの以外は文字列関係だねえ、画像はない
エクセルのセルをコピーしたときの中
なんかいっぱいある
クリップボードからのデータ取得は、このフォーマット形式の名前でもできるみたいで
GetData()にフォーマット形式を文字列で指定する
クリップボードの中にBitmap形式のデータがあるときなら
var source = Clipboard.GetDataObject().GetData("Bitmap") as BitmapSource;
これで取得できる
BitmapSourceなら
var source = Clipboard.GetDataObject().GetData("System.Windows.Media.Imaging.BitmapSource") as BitmapSource;
でも、これで取得できるデータは今までのGetImage()で得られるものと全く同じものみたい
そこで、さっきの見慣れないDeviceIndependentBitmap、Bitmapって付いているから画像なのかと思ったけど、これをGetDataで取得すると、型はMemoryStreamだった
もし画像ならBitmapFrameのCreateでStreamから画像を作れるからって、試したけどこれはエラー
なんとかBitmapSourceに変換できなかと、"DeviceIndependentBitmap bitmapsource"とかでググっていたら
thomaslevesque.com
をグーグル翻訳して読んでたら、WPFのGetImageはBitmapのファイルヘッダっていう部分を削除して取得してしまうみたい
Bitmapのファイルヘッダをググって
ja.wikipedia.org
ここと
algorithm.joho.info
ファイルヘッダはファイルサイズとかフォーマット形式を入れるところみたい
Bitmapはファイルヘッダが先頭にあって、次に情報ヘッダ、次にピクセルの色のデータって並んでいて、情報ヘッダにbppの値が入っているのがわかった
WPFのGetImageは情報ヘッダは取得しているみたいなので、これの確認方法がわかればいい
dobon.net
ここ!ここ見たらMemoryStreamのToArray()でStreamをbyte型の配列に変換している、こういうのがあってこういうことができるんだなあ
できたのがこれ
<summary>
</summary>
<returns></returns>
private BitmapSource GetClipboardBitmapDIB()
{
var data = Clipboard.GetDataObject();
if (data == null) return null;
var ms = data.GetData("DeviceIndependentBitmap") as System.IO.MemoryStream;
if (ms == null) return null;
byte[] dib = ms.ToArray();
if (dib[14] < 32)
{
return new FormatConvertedBitmap(Clipboard.GetImage(), PixelFormats.Bgr32, null, 0);
}
else
{
return Clipboard.GetImage();
}
}
GetData("DeviceIndependentBitmap")で得たStreamをToArrayでbyte型の配列にする
配列の15番目の値がbppの値なので32未満なら、ピクセルフォーマットを変換、それ以外はそのままGetImage
これで透明にならないし、半透明部分がある画像も不透明にならずに取得できる
Microsoft Edgeでコピーした画像の取得
透明にならずに取得できた、ピクセルフォーマットは変換されてBgr32
半透明や透明部分がある画像の取得
この画像をPixtack紫陽花でコピーして
右下の半透明グラデーションの画像をコピーして
貼り付け
半透明なまま取得できた、ピクセルフォーマットもBgra32のまま
半透明画像をBgr32にすると
半透明が失われてしまった画像になる
bppの値があるインデックス番号は
Bitmapのファイルヘッダはindex0(先頭)から14まで、情報ヘッダは15からで28にBitmapのbppの値がある、WPFはファイルヘッダを取得しないのでbppの値があるindexは、28-14=14、index14
32bppに変換するアプリからの画像
index14の値は32
32bppに変換しないアプリからの画像の場合
24
1bppの画像
ファイルのプロパティ
ビットの深さ(bpp)は1
これをJTrimで開いて
コピーしたものを取得すると
bppは4になっていた、32未満なのでBgr32へ変換する、もし変換しないでそのままだと
これも透明になってしまった
ここまででできたなあと思っていたけど、エクセルのセルをコピーしたものは32bppなのに透明になってしまう!
グラフも中途半端に透明になる
本当は
こうなってほしい
- GetImage
- Streamに一時保存でBmpBitmapEncoderとDecoderを使う
- 問答無用でBgr32へ変換
- GetData("DeviceIndependentBitmap")で取得したStreamからbppを確認、32未満ならBgr32へ変換、それ以外ならそのままGetImage
1番が発端
2番はdpiが変な値になるアルファの値は0のままだけど255のような扱いになるしアルファが255固定
3番はアルファの値は0のままだけど255のような扱いになる
4番は1~3番の問題は解決、普通の画像ならこれでいい、あとはエクセルをなんとかしたい、エクセルの問題は1~3番も同じ
エクセル判定できた
private bool IsExcel()
{
string[] formats = Clipboard.GetDataObject().GetFormats();
foreach (var item in formats)
{
if(item == "EnhancedMetafile")
{
return true;
}
}
return false;
}
フォーマット名にEnhancedMetafileがあったらエクセルと判定することにしているけど、エクセル以外でもEnhancedMetafileを使うアプリはありそうだから誤判定もあり得る
これをさっきのbpp判定と組み合わせて
private BitmapSource GetClipboardBitmapDIBExcel()
{
var data = Clipboard.GetDataObject();
if (data == null) return null;
var ms = data.GetData("DeviceIndependentBitmap") as System.IO.MemoryStream;
if (ms == null) return null;
byte[] dib = ms.ToArray();
if (dib[14] < 32 || IsExcel())
{
return new FormatConvertedBitmap(Clipboard.GetImage(), PixelFormats.Bgr32, null, 0);
}
else
{
return Clipboard.GetImage();
}
}
こう、
if (dib[14] < 32 || IsExcel())
でbpp32未満かエクセルからのコピーだと判断したら、Bgr32へ変換することにした
これでエクセルのグラフも
できた
セルも
できた!これだけできれば十分
wpf_test2/20191111_クリップボードからの画像が透明 at master · gogowaten/wpf_test2
github.com
<Window x:Class="_20191111_クリップボードからの画像が透明.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:_20191111_クリップボードからの画像が透明"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="600">
<Grid>
<StackPanel Background="Gray">
<Button Content="クリップボードのデータ形式一覧表示" Click="ButtonViewFormats_Click"/>
<Button Content="GetImage()" Click="ButtonGetImage_Click"/>
<Button Content="GetData(Bitmap)" Click="ButtonGetDataBitmap_Click"/>
<Button Content="GetData(BitmapSource)" Click="ButtonGetDataBitmapSource_Click"/>
<Button Content="Streamに一時保存方式、BmpBitmapのエンコーダーとデコーダーを使う" Click="ButtonEncDec_Click"/>
<Button Content="Bgr32へ変換" Click="ButtonBgr32_Click"/>
<Button Content="GetData(DeviceIndependentBitmap)のbppで判定、32未満ならBgr32へ変換" Click="ButtonDeviceIndependentBitmap_Click"/>
<Button Content="GetData(DeviceIndependentBitmap)のbppとエクセル?で判定、32未満ならBgr32へ変換" Click="Button_Click"/>
<TextBlock Name="MyTextBlock" Text="クリップボードに画像をコピーした状態で上の各ボタンを押す"/>
<ScrollViewer UseLayoutRounding="True">
<Image Name="MyImage" Stretch="None"/>
</ScrollViewer>
</StackPanel>
</Grid>
</Window>
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace _20191111_クリップボードからの画像が透明
{
<summary>
</summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void ButtonViewFormats_Click(object sender, RoutedEventArgs e)
{
var formats = Clipboard.GetDataObject().GetFormats();
string name = "";
foreach (var item in formats)
{
name += Environment.NewLine + item;
}
MessageBox.Show(name);
}
private void ButtonGetImage_Click(object sender, RoutedEventArgs e)
{
BitmapSource source = Clipboard.GetImage();
SetImageSource(source);
}
private void ButtonGetDataBitmap_Click(object sender, RoutedEventArgs e)
{
BitmapSource source = Clipboard.GetData("Bitmap") as BitmapSource;
SetImageSource(source);
}
private void ButtonGetDataBitmapSource_Click(object sender, RoutedEventArgs e)
{
var source = Clipboard.GetDataObject().GetData("System.Windows.Media.Imaging.BitmapSource") as BitmapSource;
SetImageSource(source);
}
private void ButtonEncDec_Click(object sender, RoutedEventArgs e)
{
SetImageSource(GetClipboardBitmapEncDec());
}
private BitmapSource GetClipboardBitmapEncDec()
{
BitmapSource source = Clipboard.GetImage();
if (source == null)
{
return null;
}
var encoder = new BmpBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(source));
using (var stream = new System.IO.MemoryStream())
{
encoder.Save(stream);
stream.Seek(0, System.IO.SeekOrigin.Begin);
var decoder = new BmpBitmapDecoder(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
source = decoder.Frames[0];
}
return source;
}
private void ButtonBgr32_Click(object sender, RoutedEventArgs e)
{
SetImageSource(GetClipboarBitmapBgr32());
}
private BitmapSource GetClipboarBitmapBgr32()
{
BitmapSource source = Clipboard.GetImage();
if (source == null) return null;
return new FormatConvertedBitmap(source, PixelFormats.Bgr32, null, 0);
}
private void ButtonDeviceIndependentBitmap_Click(object sender, RoutedEventArgs e)
{
SetImageSource(GetClipboardBitmapDIB());
}
<summary>
</summary>
<returns></returns>
private BitmapSource GetClipboardBitmapDIB()
{
var data = Clipboard.GetDataObject();
if (data == null) return null;
var ms = data.GetData("DeviceIndependentBitmap") as System.IO.MemoryStream;
if (ms == null) return null;
byte[] dib = ms.ToArray();
if (dib[14] < 32)
{
return new FormatConvertedBitmap(Clipboard.GetImage(), PixelFormats.Bgr32, null, 0);
}
else
{
return Clipboard.GetImage();
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
SetImageSource(GetClipboardBitmapDIBExcel());
}
private BitmapSource GetClipboardBitmapDIBExcel()
{
var data = Clipboard.GetDataObject();
if (data == null) return null;
var ms = data.GetData("DeviceIndependentBitmap") as System.IO.MemoryStream;
if (ms == null) return null;
byte[] dib = ms.ToArray();
if (dib[14] < 32 || IsExcel())
{
return new FormatConvertedBitmap(Clipboard.GetImage(), PixelFormats.Bgr32, null, 0);
}
else
{
return Clipboard.GetImage();
}
}
private bool IsExcel()
{
string[] formats = Clipboard.GetDataObject().GetFormats();
foreach (var item in formats)
{
if (item == "EnhancedMetafile")
{
return true;
}
}
return false;
}
private void SetImageSource(BitmapSource source)
{
if (source != null)
{
MyImage.Source = source;
MyTextBlock.Text = "PixelFormats = " + source.Format.ToString() + Environment.NewLine +
"DpiX = " + source.DpiX;
}
else
{
MyImage.Source = null;
MyTextBlock.Text = "null";
}
}
private byte[] GetPixels(BitmapSource source)
{
int w = source.PixelWidth;
int h = source.PixelHeight;
int stride = w * 4;
byte[] pixels = new byte[h * stride];
source.CopyPixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
return pixels;
}
<summary>
</summary>
<param name="source"></param>
<returns></returns>
private bool IsTransparent(BitmapSource source)
{
var pixels = GetPixels(source);
if (pixels[3] != 0)
{
return false;
}
ulong alpha = 0;
for (int i = 3; i < pixels.Length; i += 4)
{
alpha += pixels[i];
}
if (alpha == 0)
{
return true;
}
else
{
return false;
}
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
SetImageSource(ClipboardGetImageFix());
}
private BitmapSource ClipboardGetImageFix()
{
var data = Clipboard.GetDataObject();
if (data == null) return null;
BitmapSource source = Clipboard.GetImage();
if (source == null) return null;
if (IsTransparent(source))
{
return new FormatConvertedBitmap(source, PixelFormats.Bgr32, null, 0);
}
else
{
return source;
}
}
}
}
2019/11/13追記ここから
ピクセルフォーマットをBgra32からBgr32へ変換しても、アルファの値は変化していなくて0のままだった、Bgr32はアルファの値を無視して255扱いにする感じかなあ、てっきり255で上書きされると思っていた、その勘違い箇所を書き直した
2019/11/13追記ここまで
関連記事
2年後、スクショアプリで使った
gogowaten.hatenablog.com
1年後
gogowaten.hatenablog.com
5ヶ月後
結局すべてのピクセルのAlphaを判定することにした
11日前
gogowaten.hatenablog.com