饭祷爱

The quieter you are,the more you are able to hear

导航

powershell初探(六)

  这一章讲讲ps与.net对象的二三事,将用一个小的实例说明下。

  既然是.NET对象,那FRAMEWORK是必须的。我的机器上装的是v2.0。这次主要用到的是System.Drawing命名空间。

  那么.NET对象在哪?难不成可以直接使用?C#是要用using来引入命名空间的,ps引入.NET命名空间使用的是

 [reflection.assembly]::LoadWithPartialName("命名空间")

  其中assembly是在System.reflection这个命名空间下,ps会自动将System补上,所以直接用[reflection.assembly]就行了,而 "::" 是ps中使用对象(类或者结构体等)的静态成员的操作符(后面可以接静态方法,属性),还记得字面变量吗?其实说的就是[reflection.assembly]这种类型的变量,当时我觉得叫静态变量更好些。相似的还有[DateTime],这里也将System省略了。例如你可以用

[DateTime]::Now

来获取当前的时间。

  好了,问题来了,system.reflection这个命名空间又是怎么引入的?这有点像鸡生蛋的问题。其实有的命名空间当你打开ps的控制台的时候就已经在当前的领域中了。读起来有点绕是吧?其实说领域是因为你可以用System这个命名空间下的AppDomain的一个静态方法来查看当前领域中的程序集(assembly),其实把领域理解成当前的运行环境就好了。查看当前环境已引用的程序集的命令如下

[appdomain]::CurrentDomain.GetAssemblies()|%{$_.fullname}|Sort-Object

还记得命令管道吧,后面的两个命令管道“|%{$_.fullname}“和”|Sort-Object“是为了格式化输出到控制台的数据,其中%是foreach的别名,而foreach又是for-each的简写,sort-object是将管道内容排序,[appdomain]::CurrentDomain.GetAssemblies()就是获取当前以引入的程序集的命令了。在没有使用[reflection.assembly]::LoadWithPartialName("命名空间")引入其他程序集的情况下,我的输出如下图

其中我并没有找到system.reflection这个命名空间,命名空间应该是不相交的,所以他到底怎么来的,对我来说还是个迷╮(╯▽╰)╭

  开始说到了要用到System.Drawing这个命名空间,而默认是没有的,所以需要执行下面的语句

[Reflection.assembly]::LoadWithPartialName("System.Drawing")

  如果系统成功找到了该程序集,会有如下输出

  你可以让[Reflection.assembly]::LoadWithPartialName("System.Drawing")返回空值既

[void][Reflection.assembly]::LoadWithPartialName("System.Drawing")

  或者命令的输出传给一个空值既

[Reflection.assembly]::LoadWithPartialName("System.Drawing") >> $null

来取消输出。而"System.Drawing"里的System是不能省略的,好吧,其实这些不重要...

 

  今天要完成的小例子就是通过ps与.NET对象的交互来实现在控制台输出一张图片。先来科普下微软控制台的颜色系统。

  在视觉时代的今天微软的控制台只能同时存在16种颜色,分别是ConsoleColor这个枚举里的16个值

    Black             黑色。
    DarkBlue          藏蓝色。
    DarkGreen         深绿色。
    DarkCyan          深紫色(深蓝绿色)。
    DarkRed           深红色。
    DarkMagenta       深紫红色。
    DarkYellow        深黄色(赭色)。
    Gray              灰色。
    DarkGray          深灰色。
    Blue              蓝色。
    Green             绿色。
    Cyan              青色(蓝绿色)。
    Red               红色。
    Magenta           紫红色。
    Yellow            黄色。 

 

  这16种颜色你可以通过右键控制台的标题栏选择”默认值“或者“属性”来看见,如下图

  当然,控制台不是调色板,我们不能怪微软。不过其实控制台的默认颜色是可以改变的,只不过只能改成另外的16种而已,而他们的名字还是那16个枚举值。这就有点说不过去了吧。比如你可以把console的颜色设置成如下这样。

  此时上述的16种颜色就全部变成了灰色系,也就是说consoleColor枚举值的名字没有改变,不过对应的颜色变了。例如[ConsoleColor]::Yellow对应的将是非常浅的灰色。下面说说console的字体大小,这将决定最后生成图像的像素多少。因为我将一个字符代替一个像素,所以console可以输出的字符数越多,就可以生成像素越高的图像。

  console默认的字符大小是8X16的点阵字体,也就是说一个字符的宽是8个像素,高是16个像素,我的显示器是19寸(1440x900)的,也就是说在当前的console下只能生成最多像素值为90X55的位图,基本就等于看马赛克了。这里因为像素是一个带颜色的正方形,所以实际要用两个字符才能替代一个像素。

  那我设置不就行了?当然可以,不过不幸的是在console的UI里最小的字符是设置成新宋体,而且大小是3X7的,效果应该类似于薄码,而且无法用两个字符刚好组成一个像素,因为无法构成完美的正方形,所以这样设置的话最后出来的图像会有一定的拉长的效果。如果希望设置在以后的窗体生效的话,点确定后请选择“修改该窗口启动的快捷方式”,如下图

  那怎么整,就这么做吗?其实console的额外设置就在注册表里,我一直觉得注册表其中的一个作用就是配置文件。在“运行”输入regedit之后,果然找到“HKEY_CURRENT_USER\Console%SystemRoot%_system32_WindowsPowerShell_v1.0_powershell.exe”(这个键的存在应该是我配置过powershell的原因,要是没有这个键你也可以手动加一个,缺什么补什么)我的配置如下图

  我设置的FontSize的数据对应着新宋体的1号字,字体大小为1X2,貌似这个合适了。console的设置就此完成,我将这个键的注册表导出得到如下的配置文本

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Console\%SystemRoot%_system32_WindowsPowerShell_v1.0_powershell.exe]
"ScreenColors"=dword:0000000f
"ColorTable02"=dword:00202020
"ColorTable03"=dword:00303030
"ColorTable04"=dword:00404040
"ColorTable05"=dword:00505050
"ColorTable06"=dword:00606060
"ColorTable07"=dword:00707070
"ColorTable09"=dword:00909090
"ColorTable10"=dword:00a0a0a0
"ColorTable11"=dword:00b0b0b0
"ColorTable12"=dword:00c0c0c0
"ColorTable13"=dword:00d0d0d0
"ColorTable14"=dword:00e0e0e0
"ColorTable15"=dword:00f0f0f0
"ScreenBufferSize"=dword:01a605a0
"WindowSize"=dword:01a605a0
"FontSize"=dword:00010000
"FontWeight"=dword:00000190
"FontFamily"=dword:00000036
"FaceName"="新宋体"

   颜色我也已经设置成了灰色系,闲手动设置麻烦也可以吧上面的代码粘到txt中改下名运行应该就ok了。好吧,下面开始进入正题。

------------------------------------------------------------------我是正题的分割--------------------------------------------------------------------------------------

  前面的程序集已经导入了,那如何在powershell中创建一个.NET呢?就是用到new-object这个命令了,既然是和位图交互,那先创建个位图的对象

[Drawing.Image]$image=New-Object drawing.bitmap($sPath)

   这里使用的是bitmap构造函数的一个重载,$sPath是位图的路径

但有的位图可是很大的,绝对大过现在我的console能呈现的像素大小(720X450)所以要先判断下位图的大小,与console的窗口比较下。console的相关属性可以用[Console]这个静态类来调用。放缩位图的大小可以使用image类的GetThumbnailImagez这个实例方法。代码如下

    #[Console]::LargestWindowHeight获取console的最大高度为实际的像素高度,因为[Console]的高度和宽度值不是横屏或纵屏的像素,而是实际可以容纳的字符个数,所以高度就不用/2
    [int]$pixHeight=[Math]::Floor([Console]::LargestWindowHeight)
    #[Console]::LargestWindowHeight获取console的最大宽度/2为实际的像素宽度
    [int]$pixWidth=[Math]::Floor([Console]::LargestWindowWidth/2)
    if(($image.Width -gt [int]$pixWidth) -or ($image.Height -gt [int]$pixHeight))
    {
        #放缩比例为高度和宽度对比实际比例较大的一个
        $rate=[Math]::Max($image.Width/$pixWidth,$image.Height/[int]$pixHeight)
        #放缩
        $image=$image.GetThumbnailImage([Math]::Floor(($image.Width)/$rate),[Math]::Floor(($image.Height)/$rate),$null,0)
    }

  图片处理完毕,可以直接用image的getpixel这个实例方法获取每一个点的像素的RGB分量值,然后使用Grey=R*0.7+B*0.2+G*0.1这个灰度公式(此公式来自百度)将像素逐一灰化,公式的系数可以自己定。由于灰色系的R,G,B值都是一样的,所以只要得出一个灰度就可以确定这个像素点灰化后的颜色值。最后再用灰度和[consoleColor]比较就可以确定与该像素点颜色最近似的灰色系颜色。代码如下

  

    #得到原像素点的灰度
    $greyDegree=($imColor.R)*0.7+($imColor.G)*0.2+($imColor.B)*0.1
    #这里的seed=16,因为我为了方便,将灰色的进阶值设为了16(256色/console可以呈现的颜色数16),及灰色按(0,0,0);(16,16,16);(32,32,32)这样递增
    #得到初始的偏移值用于比较
    $offset=[Math]::Abs($greyDegree-0*$global:seed)
    #$global:siColor是一个全局数组,里面保存了16种控制台颜色的枚举(名字一样,颜色却不同了),设置一个初始的颜色,对应初始偏移值
    $fbColor=($global:siColor[0])
    for($i=0;$i -lt ($global:siColor.Length);$i++)
    {
        #遍历全局数组,求出一个临时偏移值
        $tempOffset=[Math]::Abs($greyDegree-$i*$seed)
        if($tempOffset -lt $offset)
        {
            #如果临时偏移值小于初始偏移值,说明当前的偏移值更加近似原像素灰度,交换~继续比较直到找出最近似的
            $offset=$tempOffset
            $fbColor=($global:siColor[$i])
        }
    }

  接下来就是呈现的问题了,这个问题我纠结了很久,开始我是用write-host这个命令配合-foregroupcolor,-backgroupcolor,-nonewline这几个参数配合一个一个以对应的近似灰色值作为前景和背景色输出一个字符来作为像素,不过这样一个像素一个像素输出确实比较恶心,后来又想先把所有的像素值保存在缓存中,最后一次输出,可是当像素值比较多的时候要等很长的时间。最后还是决折中,一行一行输出。

  先说说呈现时使用到的命名空间System.Management.Automation.Host,这是powershell默认会导入的一个命名空间,所以不用重新加载。

  可以使用(get-host).UI.RAWUI这个命令来得到此命名空间中的一个对象,这个对象也就是当前的powershell的host的UI,这里也就是console。得到了这个RAWUI对象之后可以使用SetBufferContents($pPos,$rRectscreen)这个重载方法将一个保存着缓存单元的矩阵数组以console的某一个点作为这个矩阵数组的左上角(也就是矩阵数组第一个元素)将该矩阵数组输出到屏幕上。这一段我自己都把自己绕糊涂了。

  简单点说,SetBufferContents($pPos,$rRectscreen)这个重载方法有两个参数,一个是二维坐标$pPos(这是System.Management.Automation.Host下的一个结构以),对应着console中的某一个点,其中(0,0)表示的坐标是console的左上角。而$rRectscreen保存着一个矩阵数组(不是二维数组,二维数组用array[index][index]获取元素,而矩阵数组使用array[a,b]其中a,b表示的应该是向量,具体的区别比如在内存中的存储方式什么的我真不知道,求解惑。总之二者不是一回事)。这个矩阵数组的每一个元素都是一个buffercell对象,这个对象保存着一个字符,这个字符有前景色和背景色等属性,具体细节请MSDN。所以我们的目标是将一个二维图像的每一个像素进行灰化处理后保存在一个矩阵数组中,当一行像素保存完成后,用SetBufferContents将这行像素输出到屏幕的指定位置。

  其他的比较简单,而要在ps中得到一个矩阵数组却有点麻烦,至今我们找到直接在ps里创建一个矩阵数组的方法,开始我是用RAWUI的GetBufferContents($Rect)这个方法返回的一个矩阵数组,然后再操作这个矩阵数组。$Rect是System.Management.Automation.Host下的另一个结构体。不过这样实在让我不爽,取到了屏幕上的一个矩阵数组里的值结果直接把他丢弃了,要的是容器而已。有点买椟还珠的意思。所以这个方法我就不推荐了,实现详情请google

  经过一番探索之后,我终于通过反射的机制实现了我想要的,具体的代码(包括如何呈现如下)

#获取System.Management.Automation.Host.BufferCell[,]类型
$bufferRect=[Type]::GetType("System.Management.Automation.Host.BufferCell[,]")
#获取Int类型
$paramType=[Type]::GetType("System.Int32")
#初始化一个参数数组,用于作为GetConstructor的参数,来获取一个"System.Management.Automation.Host.BufferCell[,]"的需要两个Int类型作为参数的构造函数
$paramTypes=@($paramType,$paramType)
#获取"System.Management.Automation.Host.BufferCell[,]"的其中一个构造函数,这个构造函数接受两个Int型作为参数
$ctor =$bufferRect.GetConstructor($paramTypes)
#初始化将传给构造函数的参数数组
$param=@(1,(($iImage.Width)*2))
#通过构造函数实例化一个矩阵数组(BufferCell[,])
$rRectscreen=$ctor.Invoke($param) 

#创建一个BufferCell对象,将被存在矩阵数组(BufferCell[,])中
$bcCell=New-Object "System.Management.Automation.Host.BufferCell"
#设置bufferCell的字符属性,可以是任何英文字符
$bcCell.Character="R"

#把这个BufferCell的前景色和背景色设置为同一颜色,作为一个像素的组成部分
$bcCell.ForegroundColor=$fbColor
$bcCell.BackgroundColor=$fbColor
#将这个BufferCell装入矩阵数组(BufferCell[,])中,因为console字体的关系所以两个BufferCell将组成一个像素
$rRectscreen[0,($j*2)]=$bcCell  
$rRectscreen[0,(($j*2)+1)]=$bcCell

  上面贴的程序是核心部分,最后将所有程序组合在一起是这个样子滴

function GetImage
{
    param([String]$p)
    [Drawing.Image]$image=New-Object drawing.bitmap($sPath)
    #[Console]::LargestWindowHeight获取console的最大高度/2为实际的像素高度
    [int]$pixHeight=[Math]::Floor([Console]::LargestWindowHeight/2)
    #[Console]::LargestWindowHeight获取console的最大宽度/2为实际的像素宽度
    [int]$pixWidth=[Math]::Floor([Console]::LargestWindowWidth/2)
    if(($image.Width -gt [int]$pixWidth) -or ($image.Height -gt [int]$pixHeight))
    {
        #放缩比例为高度和宽度对比实际比例较大的一个
        $rate=[Math]::Max($image.Width/$pixWidth,$image.Height/[int]$pixHeight)
        #放缩
        $image=$image.GetThumbnailImage([Math]::Floor(($image.Width)/$rate),[Math]::Floor(($image.Height)/$rate),$null,0) 
    }
    return $image
}
function GetColor
{
    param([Int]$R,[Int]$G,[Int]$B)
    #得到原像素点的灰度
    $greyDegree=($imColor.R)*0.7+($imColor.G)*0.2+($imColor.B)*0.1
    #这里的seed=16,因为我为了方便,将灰色的进阶值设为了16(256色/console可以呈现的颜色数16),及灰色按(0,0,0);(16,16,16);(32,32,32)这样递增
    #得到初始的偏移值用于比较
    $offset=[Math]::Abs($greyDegree-0*$global:seed)
    #$global:siColor是一个全局数组,里面保存了16种控制台颜色的枚举(名字一样,颜色却不同了),设置一个初始的颜色,对应初始偏移值
    $fbColor=($global:siColor[0])
    for($i=0;$i -lt ($global:siColor.Length);$i++)
    {
        #遍历全局数组,求出一个临时偏移值
        $tempOffset=[Math]::Abs($greyDegree-$i*$seed)
        if($tempOffset -lt $offset)
        {
            #如果临时偏移值小于初始偏移值,说明当前的偏移值更加近似原像素灰度,交换~继续比较直到找出最近似的
            $offset=$tempOffset
            $fbColor=($global:siColor[$i])
        }
    }
    return $fbColor
}
$sPath="C:\Documents and Settings\Administrator\桌面\fd2.jpg"
if(!(Test-Path $sPath))
{
    Write-Host "path not found"
    exit
}
$global:seed=16
$global:siColor=@("Black","DarkBlue","DarkGreen","DarkCyan","DarkRed","DarkMagenta","DarkYellow","Gray","DarkGray","Blue","Green","Cyan","Red","Magenta","Yellow","White")
[void][reflection.assembly]::LoadWithPartialName("System.Drawing") 
[Console]::WindowHeight=[Console]::LargestWindowHeight
[Console]::WindowWidth=[Console]::LargestWindowWidth
$uConsoleHostRawUI=(Get-Host).UI.RawUI


$iImage=GetImage -p $sPath

#获取System.Management.Automation.Host.BufferCell[,]类型
$bufferRect=[Type]::GetType("System.Management.Automation.Host.BufferCell[,]")
#获取Int类型
$paramType=[Type]::GetType("System.Int32")
#初始化一个参数数组,用于作为GetConstructor的参数,来获取一个"System.Management.Automation.Host.BufferCell[,]"的需要两个Int类型作为参数的构造函数
$paramTypes=@($paramType,$paramType)
#获取"System.Management.Automation.Host.BufferCell[,]"的其中一个构造函数,这个构造函数接受两个Int型作为参数
$ctor =$bufferRect.GetConstructor($paramTypes)
#初始化将传给构造函数的参数数组
$param=@(1,(($iImage.Width)*2))
#通过构造函数实例化一个矩阵数组(BufferCell[,])
$rRectscreen=$ctor.Invoke($param) 

#创建一个BufferCell对象,将被存在矩阵数组(BufferCell[,])中
$bcCell=New-Object "System.Management.Automation.Host.BufferCell"
#设置bufferCell的字符属性,可以是任何英文字符
$bcCell.Character="R"
$pPos=$uConsoleHostRawUI.WindowPosition
for($i=0;$i -lt $iImage.Height;$i++)
{
    for($j=0;$j -lt $iImage.Width;$j++)
    {
        [drawing.color]$imColor=$iImage.GetPixel($j,$i)
        $fbColor=GetColor -R $imColor.R -G $imColor.G -B $imColor.B
        #把这个BufferCell的前景色和背景色设置为同一颜色,作为一个像素的组成部分
        $bcCell.ForegroundColor=$fbColor
        $bcCell.BackgroundColor=$fbColor
        #将这个BufferCell装入矩阵数组(BufferCell[,])中,因为console字体的关系所以两个BufferCell将组成一个像素
        $rRectscreen[0,($j*2)]=$bcCell  
        $rRectscreen[0,(($j*2)+1)]=$bcCell  
    }
    $pPos.x=0
    $pPos.y=$i
    $uConsoleHostRawUI.SetBufferContents($pPos,$rRectscreen)
}

Read-host

  还有以下几点需要说明  

  (1)最后用一个Read-Host来等待用户输入,以免程序运行完成之后直接退出。

  (2)$sPath设置成你要输出在控制台的文件路径即可

  (3)实例化矩阵数据的程序没有放在一个函数里是因为ps神奇的返回值方式(将返回值装在一个object[]中)而又无法再将返回值还原成矩阵数据(其他的类型String什么的却可以)

    (4)算法和代码结构都比较挫,能理解ps和.NET对象的交互就好。

  (5)将源文件保存为.PS1的然后-右键-使用powershell运行(因为我们只在注册表里更改了powershell的console配置,而且让你在1x2的字体下敲命令我估计也不太现实)

  最后是效果图,先是永远的饭岛爱老师

  然后是经过放缩后的walle

  其他的.NET类的使用大同小异。中秋快乐~

 

 

 

  

posted on 2012-09-23 23:22  饭祷爱  阅读(2787)  评论(4编辑  收藏  举报