在上一篇文章里,我们讲解了为滤镜添加术语资源,从而使我们的滤镜可以被PS的scripting system感知和描述,这样即友好支持了PS的“动作”面板。在这一篇文章中,我们将对此前的DEMO进行进一步的细化,例如在参数对话框上增加实时预览的小缩略图等。对话框的引入主要是给用户一个机会和接口,设置或调节滤镜使用的图像处理算法。通常作为UI的友好性,在对话框上应该提供预览图,这样可以直观的把参数对结果产生的影响反馈给用户,指导他们调整参数。而不是要用户必须反复执行滤镜命令才能看到效果然后去调节参数。

此前我觉得“添加缩略图”这样的功能应该不是很困难,但当我尝试这样去做,我很快发现它的难度远远超过了以往我写的文章中的讲解。因为当我们尝试使用PS提供的回调函数去显示缩略图时,我们必须对PS提供的接口细节完全清楚,包括影响缩放的参数设置,数据分布,扫描行等细节。不能够有一分一毫的差错,否则我们就可能看到不正常的显示,甚至会不小心的使内存越界。

在引入缩略图之前,我先对滤镜算法做一些有趣的改进,进行了一点增强,使它更加实用化。

(1)引入“像素随机抖动”参数和算法。

             此前我们设置像素时,输入和输出的位置本来是完全一致的,即 Dest(i, j) = f (Src(i, j))。

现在我们考虑对上式进行一点改动,把源像素进行随机抖动,即Dest(i, j) = f (Src(i+dx, j+dy))。

我们设置抖动距离为distance(像素)参数,这样我们取源像素时,在以当前像素为中心向外围扩展distance的正方形内随机选取某个点作为源像素。从而使他们在结果图中具有一种“溶解”或“腐蚀”效果。如下图所示:

因此我们在滤镜参数中增加了distance参数,表示上图中的随机抖动距离。这样当我们设置位于 (i,j)位置的像素时,我们取的源像素坐标是:

x = i + rand()%(2*distance+1) - distance;

y = j + rand()%(2*distance+1) - distance;

在实际处理时,我们还需要考虑上述结果x,y可能会超出有效数据边界,因此需要把x,y限定在 filterRect 内部。

而由于我们采用的是贴片(Tile)处理方法,因此我们的调整需要一点技巧性。我们将改动我们向PS请求的inRect,即每次贴片时,outRect依然保持和此前一致,而把 inRect 尝试向四周扩张distance像素距离,这样可以保证我们每次贴片时都能拿到有效数据(当贴片位于filterRect内部时),除非贴片位于filterRect的边缘。

必须注意的是,由于 inRect 比 outRect 要“大一圈”,所以这时候两个Rect的像素已经不是大小一致完全重叠关系了,而是有一定偏移的!在代码中我们必须考虑两个矩形之间的偏移关系。这里可以参考我的源代码,就不详细讲解该处理方法了。

(2)为对话框增加“缩略图”(Proxy)。

我们增加了一个参数,然后在对话框左侧空出一个较小区域用于显示缩略图,为了方便,我在缩略图位置放置了一个隐藏的STATIC 控件(Proxy Banner),它的主要用处是使我可以在运行时获取到 “缩略图”的边界(客户区坐标)。修改后的对话框如下图所示:

(2.1) displayPixles 回调函数 和 PSPixelMap 结构;

             在显示缩略图时,我们使用的是 gFilterRecord 中的 displayPixels 回调函数,这个函数的原型如下(函数指针的typedef):

typedef MACPASCAL OSErr (*DisplayPixelsProc) (

const PSPixelMap *source,
                    const VRect *srcRect,
                    int32 dstRow,
                    int32 dstCol,
                    void *platformContext);

第一个参数是一个 PSPixelMap 结构体的指针用以描述一块像素数据区,它相当于BitBlt中的源图,srcRect参数描述的是源图矩形;

dstRow 和 dstCol 描述的是目标区域的“目标行”,“目标列”坐标,请注意其逻辑意义与我们通常使用的参数的区别。这里dstRow相当于 destY, dstCol就相当于 destX 参数,即(dstCol, dstRow) 是在目标区域中的起始坐标,这一点需要注意。

最后一个参数 platformContext 在 windows系统中也就是 HDC 。

其中第一个参数我们还需要再做简单介绍,即 PSPixelMap 的定义和 PS 对该数据区的分布要求。PSPixelMap的定义如下:

typedef  struct  PSPixelMap
{
    int32 version;
    VRect bounds;
    int32 imageMode;
    int32 rowBytes;
    int32 colBytes;
    int32 planeBytes;
     void   * baseAddr;
    
     // ---------------------------------------------------------------------------
     //  Fields new in version 1:
     // ---------------------------------------------------------------------------    
    PSPixelMask  * mat;
    PSPixelMask  * masks;
    
     //  Use to set the phase of the checkerboard:
    int32 maskPhaseRow;
    int32 maskPhaseCol;

// ---------------------------------------------------------------------------
     //  Fields new in version 2:
     // ---------------------------------------------------------------------------    
    PSPixelOverlay  * pixelOverlays;
    unsigned32 colorManagementOptions;

} PSPixelMap;

◆ version: 结构体版本。

对于PS CS版本来说,它要求我们把它设置为1。PS的未来版本可能会扩展它并提升此版本号。

            ◆ bounds:像素数据所占据的矩形。

            ◆ imageMode:数据区的图像模式。

它支持以下模式: grayscale, RGB, CMYK, Lab。

            ◆ rowBytes: 相邻行之间的字节数距离。

相当于扫描行宽度(重要),以字节为单位,必须设置正确。

            ◆ colBytes: 相邻列像素数据的字节数距离。

由于数据是“集中分布”的,所以这个属性的值主要取决于像素的色深度。对于每个通道每个像素使用一个字节的普通24位深度的图像来说,这个距离为1byte。

            ◆ planeBytes: 相邻通道的字节数距离。

由于数据是“集中分布”的,所以这个属性通常是 rowBytes * 图像高度。

            ◆ baseAddr:数据区起始地址。

我曾在此前的文章中讲过,PS提供给我们的 inData 和 outData 是通道交叉分布的(interleave)。而这里PSPixelMap中的数据的分布要求则不同,它要求数据是通道集中分布的。

例如对于RGB图像来说, 当我们请求所有通道时,inData/outData中的数据分布是:

R | G | B | R | G | B | R | G | B | ..... (interleave)

而对于 PSPixelMap 中的数据来说,则要求按如下分布:

R | R | R | .... G | G | G | ... B | B | B .... (集中分布)

也就是说对inData 和 outData, 所有通道数据交叉分布,同一个通道(plane)的数据在数据区中是跳跃式存在的。

而对于 PSPixelMap 来说,同一个通道(plane)的数据是集中在一起的,先列出所有第一个通道数据,再列出所有第二个通道数据,等等。

同时我们进行像素定位和预测缓冲区大小时,和普通的Bitmap像素定位一样,必须使用滤镜参数中的 inRowBytes 属性(相当于扫描行宽度)进行在“行”间定位,而不能假设或自行计算“行宽度”。

【注意】我们必须清楚数据分布的细节,这样才能正确定位到指定位置的像素。

(2.2)控制缩放:FilterRecord中的 inputRate 和 inputPadding 属性;

由于PS处理的图像大小是多种多样的,因此我们显示缩略图时必然面对的一个问题是缩放问题。大多数情况下,由于缩略图只是定性展示,对数据精确性要求可以有所降低,并且考虑到性能因素,因此一般缩略图的尺寸可以设置的较小,当原图(filterRect)比缩略图(bannerRect)大时,我们希望图像缩小显示在缩略图上。而当原图比缩略图小时,我们就采用实际原图大小(即缩放因子=1)即可。因此现在我们需要了解如何把原图缩小到缩略图大小。在GDI中我们知道我们可以使用 StretchBlt 函数来完成缩放的。而在这里我们获取源图数据是从PS传递给我们的inData得到的,当我们希望得到缩小的原图时,我们即通过设置 FilterRecord 参数中的 inputRate (采样率)属性来完成缩小。

(2.2.1)Fixed inputRate;

==================================================

【关于 Fixed 类型】

Fixed在PS中被定义为 long 类型:

typedef long Fixed;

但是Fixed在PS中的实际意义是一个(定点)小数。所谓Fixed是相对 float(浮点小数)来说的。在float中小数点的位置是不固定的,因此称为浮动点。而 Fixed 则把一个小数拆解为整数部分和小数部分,分别存储到一个高16位和低16位。即其含义是 "16.16"。

例如假设有一个小数是3.00f;则相应的Fixed数是 0x00030000;

从浮点数转换成 Fixed 类型的方法是:

    

double  _factor;

Fixed    _fixed = ( Fixed ) ( _factor * 0x10000 ) ;

    从 Fixed 类型转换成浮点类型的方法是 ( 备注:1 / 0x10000 = 0.0000152587890625 ) :

    Fixed   _fixed;

    double  _factor = _fixed * 0.0000152587890625 ;

==================================================

    【补充】

根据我对 Photoshop CS 的观察,当设置 inputRate 时,PS 会把这个值进行了取整处理。也就是说,如果 inputRate < 1.0, 则 inputRate = 1.0; 相当于 Photoshop 对插件设置的 inputRate 做了如下处理:

int nInputRate = (pFilterRecord->inputRate + 0x8000) >> 16;
if(nInputRate < 1) nInputRate = 1;

--hoodlum1980, on 2017-6-22。

==================================================

inputRate表示采样率,在逻辑意义上是一个小数,我们通过设置它的值来实现得到的inData是对原图的缩放结果。在默认情况下,PS设置的inputRate就是1,也就没有任何缩放。我们在获取缩略图数据时,需要计算缩放因子(factor),然后把inputRate设置为factor(注意数据类型的转换方法)。

这样设置后,我们得到的 inData 的实际图像坐标将是 inRect * inputRate,即

(inRect.left * inputRate, inRect.top* inputRate, 

                        inRect.right * inputRate, inRect.bottom * inputRate)

例如,当 inputRate 为 2.0 时,则每两个像素采样一个点,则 inRect, inData 的关系如下图所示:

在上面这幅图中,展示了在缩放时应该如何设置inRect,请注意为了获取我们需要的矩形,我们需要把我们希望的inRect(粉红色矩形区域)的坐标除以 inputRate(图中假设inputRate = 2),才是需要提交给PS的 inRect(上图中的蓝色矩形)。然后我们使用advanceProc回调函数,即可得到 inData 为上图中右侧的图像数据,可见它的尺寸在两个方向上都缩小了一半。

【注意1】在处理完毕缩略图并关闭对话框时,必须把inputRate恢复为 0x00010000 (1.0)。否则它将会继续影响后续的实际处理中的 inData!使处理结果产生意料外的结果。

【注意2】滤镜参数必须考虑图像缩放所带来的影响。和缩放有关的参数也要相应的映射到缩略图尺寸上(例如本例中的随机抖动距离要同比例缩小)。和缩放无关的参数(例如本例中的不透明度百分比,填充色)可不考虑缩放影响。

(2.2.2)int16 inputPadding;

当PS提供 inData 时,它可以被补齐。可以指定补齐的像素的值(0~255),也可以设置为以下选项(它们被定义为负数,以和用户设置像素值区别):

plugInWantsEdgeReplication: 复制边缘像素。

plugInDoesNotWantPadding:随机值(不确定的值)。

plugInWantsErrorOnBoundsException:(默认值)请求边界外数据时标记一个错误。

当请求区域超出边界,PS将使用上述选项设置 inData 的数据。

(2.2.3)显示缩略图。

为了显示缩略图,我们需要请求PS为我们分配缓冲区。我们首先需要预测我们的缓冲区的大小,并在Prepare调用时通知 PS 我们的需求。

考虑到当用户在对话框上进行参数调整时,我们应该实时的更新缩略图显示,以反馈当前参数效果。所以我们需要两份缩略图数据,一份是缩略图的原始数据,它作为算法的输入,在创建对话框时获取到源图数据,然后在整个对话框生命期间保持数据不会改变。另一份是我们用于处理 WM_PAINT 消息时使用的绘制数据,即可以实时改变的缩略图实际显示数据。

因此我们评估缩略图的尺寸,然后使用以下估计值:

bufferSize = 缩略图最大宽度 * 缩略图最大高度 * 通道数 * 2;

在 Prepare 调用期间,我们把这个值(bufferSize)设置到 FilterRecord 的 bufferSpace 和 maxSpace 属性中,这表示我们(PlugIn)和PS(Host)进行内存需求“协商”,使 PS 了解到我们预期的内存开销,然后尝试准备足够内存以供我们后续的申请。

真正显示对话框是在 start 调用中,我们在对话框的初始化消息时准备请PS为我们申请缓冲区。基本方式如下:

// 获取 buffer 回调函数集指针
BufferProcs  * bufferProcs  =  gFilterRecord -> bufferProcs;

// 请PS为我们申请内存
bufferProcs -> allocateProc(bufferSize,  & m_ProxyData.bufferId0);

// 请PS为我们锁定内存(禁止内存整理)
// [ 1 ]函数返回被锁定的内存起始地址。
// [ 2 ]第二个参数是BOOL moveHigth,对windows平台将被忽略。
m_ProxyData.data0  =  bufferProcs -> lockProc(m_ProxyData.bufferId0, TRUE);

// =============================
//   这里是处理和更新缓冲区的期间
// =============================

// 使用结束后,释放和解锁缓冲区。
// 解锁
gFilterRecord -> bufferProcs -> unlockProc(m_ProxyData.bufferId0);
// 释放内存
gFilterRecord -> bufferProcs -> freeProc(m_ProxyData.bufferId0);

我们使用 lockProc 锁定缓冲区这块内存,主要是防止操作系统在我们处理数据期间进行内存整理,从而破坏缓冲区资源。

【注意】这里加锁和解锁使用的是“引用计数”机制,即解锁次数 必须 匹配加锁次数才能使缓冲区真正得到解锁。

为了显示缩略图,并能够实时反馈用户的调节,我们准备了下面的四个函数(其中CreateProxyBuffer 和 UpdateProxy 难度最大):

● CreateProxyBuffer

计算缩略图实际大小和缩放因子,委托PS为我们申请缓冲区,同时也初始化了原始数据(即把inData拷贝到PsPixelMap中),在处理 WM_INITDIALOG 时调用。

● UpdateProxy

当用户在对话框上修改了某个参数时(WM_COMMAND)被调用,用于更新缩略图显示数据,并刷新缩略图显示。会引起对 PaintProxy 函数的间接调用。

● PaintProxy

绘制缩略图,通过 displayPixels 回调函数完成,在处理 WM_PAINT 消息时调用。

● DeleteProxyBuffer

释放我们申请的缓冲区,在对话框退出前(WM_DESTROY)调用。

现在总结一下上面四个函数的调用时机,使我们对这四个函数的分工具有一个明确的认识,如下表:

窗口消息

事件

被调用的函数

说明

WM_INITDIALOG

创建对话框

CreateProxyBuffer

申请缩略图缓冲区并初始化

WM_COMMAND

修改参数值

UpdateProxy

更新缩略图,将间接调用PainProxy

WM_PAINT

窗口绘制

PaintProxy

绘制缩略图

WM_DESTROY

退出对话框

DeleteProxyBuffer

释放缩略图缓冲区

【注意】把 inData 拷贝到 PSPixelMap, 是一个难度很大,并且特别需要注意的地方。两块数据的通道数据的分布不同,因此像素定位方式也完全不同。并且涉及到缓冲区大小的计算和申请。 复制缓冲区时是使用指针进行访问的,而这非常容易因为引发错误(将导致PS进程崩溃)。

在CreateProxyBuffer中,我们的主要任务是分配缓冲区,然后把源图数据(inData)相应的拷贝到我们的缓冲区(绘制时设置给PSPixelMap结构)。由于这是一个有难度的地方,因此我特别把这个函数代码放在此处展示,代码如下:

Code_CreateProxyBuffer
//定义描述缩略图数据的结构(在CommonDefine.h中定义)
typedef struct _PROXYDATA
{
    int        left;//缩略图左上角客户区坐标
    int        top;
    int        width;//缩略图实际尺寸(像素)
    int        height;
    int        rowbytes; //扫描行宽度(bytes)
    int        planebytes; //通道间的距离(bytes)
    float    factor;        //原图和缩略图之间的缩放因子
    Ptr        data0;    //缩放后的原始数据块(即inData的一份拷贝),通过设置inputRate。
    Ptr        data1;    //缩放后的显示数据块(用于即时性更新缩略图)
    BufferID bufferId0; //data0的bufferId
    BufferID bufferId1; //data1的bufferId
} PROXYDATA;

//用于缩略图缓冲区数据的参数
PROXYDATA m_ProxyData;

//申请缩略图内存,并申请缩略图数据
void CreateProxyBuffer()
{
    int filterWidth = gFilterRecord->filterRect.right - gFilterRecord->filterRect.left;
    int filterHeight = gFilterRecord->filterRect.bottom - gFilterRecord->filterRect.top;

int bannerWidth = m_RectBanner.right - m_RectBanner.left;
    int bannerHeight = m_RectBanner.bottom - m_RectBanner.top;

float f1 = (float)filterWidth / bannerWidth;
    float f2 = (float)filterHeight / bannerWidth;

m_ProxyData.factor = max(f1, f2);

//如果原图比缩略图小
    if(m_ProxyData.factor < 1.0f)
    {
        m_ProxyData.factor = 1.0f;
        m_ProxyData.width = filterWidth;
        m_ProxyData.height = filterHeight;
    }
    else
    {
        //原图比缩略图大,则计算缩略图的实际尺寸
        //把factor去除小数部分!因为我们不知道怎么把小数部分转换到Fixed的LOWORD。
        m_ProxyData.factor = (int)(m_ProxyData.factor + 1.0f);
        m_ProxyData.width = (int)(filterWidth / m_ProxyData.factor);
        m_ProxyData.height = (int)(filterHeight / m_ProxyData.factor);
    }

//设置缩略图左上角坐标(居中显示)
    m_ProxyData.left = m_RectBanner.left + (bannerWidth - m_ProxyData.width)/2;
    m_ProxyData.top = m_RectBanner.top + (bannerHeight - m_ProxyData.height)/2;

//想PS请求原始数据,用于填充data0
    gFilterRecord->inRect.left = (int)(gFilterRecord->filterRect.left / m_ProxyData.factor);
    gFilterRecord->inRect.top = (int)(gFilterRecord->filterRect.top / m_ProxyData.factor);
    gFilterRecord->inRect.right = (int)(gFilterRecord->filterRect.right / m_ProxyData.factor);
    gFilterRecord->inRect.bottom = (int)(gFilterRecord->filterRect.bottom / m_ProxyData.factor);

//通知 P S我们希望的补充数据(未知区域的填充数据)
    gFilterRecord->inputPadding = 255; //plugInWantsEdgeReplication;

//通知 PS 输入采样率
    //PS中,Fixed数字是用DWORD表示小数,HIWORDF表示整数部分,LOWORD表示小数部分。即 "ffff.ffff"
    WORD hiword = (WORD)(m_ProxyData.factor);
    gFilterRecord->inputRate = (hiword << 16);

//现在我们请求第一个通道的数据,以从PS那里获取一些必须的信息
    gFilterRecord->inLoPlane = 0;
    gFilterRecord->inHiPlane = 0;
    //请求PS为我们更新InData
    gFilterRecord->advanceState();

//现在我们委托PS申请缓存空间,为了简单,我们假设内存充裕,不会失败
    int inHeight = gFilterRecord->inRect.bottom - gFilterRecord->inRect.top;
    //扫描行宽度 * inRect高度 * 通道数
    int bufferSize = gFilterRecord->inRowBytes * inHeight * gFilterRecord->planes;

//获取 buffer 回调函数集指针
    BufferProcs *bufferProcs = gFilterRecord->bufferProcs;

//请PS为我们申请内存
    bufferProcs->allocateProc(bufferSize, &m_ProxyData.bufferId0);
    bufferProcs->allocateProc(bufferSize, &m_ProxyData.bufferId1);

//请PS为我们锁定内存(禁止内存整理)
    //[ 1 ]函数返回被锁定的内存起始地址。
    //[ 2 ]第二个参数是BOOL moveHigth,对windows平台将被忽略。
    m_ProxyData.data0 = bufferProcs->lockProc(m_ProxyData.bufferId0, TRUE);
    m_ProxyData.data1 = bufferProcs->lockProc(m_ProxyData.bufferId1, TRUE);

//注意提供给displayPixels函数的数据不是interleave分布,而是一个通道0,通道1,通道2(集中分布)!
    //也就是 R R R  R | G G G  G | B B B  B |
    //现在我们把得到的通道interleave分布的数据转换为通道集中分布
    uint8* p0=(uint8*)m_ProxyData.data0;

//我们复制第一个通道的数据到data0起始处
    m_ProxyData.planebytes = gFilterRecord->inRowBytes * inHeight;
    memcpy(p0,(void*)gFilterRecord->inData, m_ProxyData.planebytes);

//复制其他通道
    for(int i=1; i<gFilterRecord->planes; i++)
    {
        gFilterRecord->inLoPlane = i;
        gFilterRecord->inHiPlane = i;
        //请求PS为我们更新InData
        gFilterRecord->advanceState();
        
        memcpy(p0 + i * m_ProxyData.planebytes,(void*)gFilterRecord->inData, m_ProxyData.planebytes);
    }
    
    //设置扫描行宽度
    m_ProxyData.rowbytes = gFilterRecord->inRowBytes;
}

在上面的函数(CreateProxyBuffer)中,我们首先按照下面的方法计算出缩略图的缩放因子:

factor = ceiling (max(原图宽度 / 缩略图宽度, 原图高度 / 缩略图高度));

然后我们计算了缩略图的起始点坐标(m_ProxyData.left, m_ProxyData.top)和采用上述缩放因子后的缩略图实际尺寸(m_ProxyData.width, m_ProxyData.height)。请注意,我们把 factor 向上取整(ceiling),这会使缩略图的实际尺寸是小于等于其 BANNER 尺寸的。通过设置左上角坐标,我们使缩略图的位置在 BANNER 矩形中居中。

然后我们委托 PS 为我们分配两块同样大小的缓冲区 data0 和 data1(一个原图数据拷贝,一个是用于即时显示)并锁定它们。我们使用了PS提供的 advanceState 回调去请求原图数据,我在此前的文章中已经介绍过这个最重要的回调函数之一,它的作用是请求 Photoshop 立即更新滤镜参数(FilterRecord)结构中的相关数据,包括inData,outData等等。请注意在上面的代码中,我们是逐个通道进行复制的,即我们每次请求PS为我们发送一个通道的数据,然后我们把这批数据一次性的完全拷贝到缓冲区(使用memcpy),这样就完成了通道数据的“集中分布”。其中每个通道字节数(planeBytes)计算方法如下:

每个通道字节数(planeBytes) =  单一通道的扫描行宽度(inRowBytes) * 缩略图的图像高度(inRect高度);

            

我们把缩略图数据的信息并保存在m_ProxyData参数中。在 PaintProxy 中,我们只需要把这些信息再设置并提交给 displayProxy 回调函数即可。显示缩略图(PaintProxy,UpdateProxy)的主要逻辑和代码原理,限于篇幅这里不详细讲述,可参考附件中的源代码。最后我们可以看下滤镜的对话框运行效果如下:

当在上面的滤镜对话框中使用鼠标拖动或者键盘改变文本框数值时,左侧缩略图将会实时更新以反应当前的参数效果。在参数设置对话框中,我模拟了一个Photoshop中常见的UI特性,当你把鼠标悬停在数值文本框的左侧标签上时,光标变为一个拖动箭头的形状,这时按下鼠标,左右拖动,可以看到相应文本框的数据发生变化(这和操作滑杆控件非常类似)。在上面这个对话框中,你能够看到我如何模拟了PS的这种UI效果(在Photoshop看似朴素的外表下,隐藏着非常多让人惊叹的 UI 效果,而这只是它们中的其中一个,向强大的Photoshop致敬!)。

(3)增加一个我们自己定义的“关于对话框”。

在此前为了简单起见,在“关于”中我仅仅弹出了一个MessageBox。我们可以自定义一个关于对话框,同样这里我吸取了 PS 的关于对话框的建议和风格,即没有标题栏,没有任何按钮,对话框初始位置在其父窗口的中等偏上(上1/3)处。用户按Escape,回车键 或用鼠标点击任何位置即退出对话框。我的滤镜的关于对话框如下(在PS中点击菜单:帮助 -> 关于增效工具 -> FillRed Filter... ):

这是一个普通的对话框,但我主要想介绍是当鼠标移动到我的博客的网址上时,光标变成(IDC_HAND)手形,点击即可使用默认浏览器打开网址。它是用过使用PS的回调函数集中的相应函数来完成的。因此这里我将示范 PS callback suites 的一种标准用法:

Code
char url[256];

//函数集指针
PSGetFileListSuite4 *suite;

//获取GetFileList 回调函数集(callback suite)的指针
SPErr err = sSPBasic->AcquireSuite(
                 kPSGetFileListSuite,    //suite name
                 kPSGetFileListSuiteVersion4, //suite version
                 (const void**)&suite        //suite pointer
                 );

if(err) return TRUE;

//获取网址
GetDlgItemText(hDlg, IDC_STATIC_MYBLOG, url, sizeof(url));

//用默认浏览器打开网址
suite->BrowseUrl(url);

//释放suite
sSPBasic->ReleaseSuite(kPSGetFileListSuite, kPSGetFileListSuiteVersion4);

在上面的代码中我们可以看到, PS CALLBACK Suites的用法 和 COM 组件的 QueryInterface 的使用方法是完全类似的:先声明想获取的回调函数集(callback Suite,一个含有一组PS内部的函数指针的struct)的一个指针,然后把该指针的地址传递给 BasicSuite 的 AcquireSuite 函数,成功以后我们就可以通过该指针去调用PS提供给插件的相应回调函数。

(4)总结。

到目前为止,我们已经完整的讲解了有关制作一个Photoshop滤镜的主要技术环节,从(1)创建项目,到(2)添加UI资源,再到(3)使Photoshop Scripting System知道我们的滤镜,并支持“动作”面板的对话框选项,以及本篇重点讲述的添加在对话框上的缩略图。涵盖了制作 Photoshop 滤镜插件的流程和重要知识,而Photoshop插件开发的技术细节以及插件种类仍然是非常繁复众多的,有待进一步的研究。

我们开发Photoshop插件的一个主要原因是,PS是图形处理领域的重要软件,为第三方开放了插件扩展的接口。作为第三方开发者我们可以根据自己的需求,遵照PS的约定去以插件形式扩展PS。在PS的重要用户基础上,扩展和研究将会更有实际意义。

制作滤镜的基本技术已经介绍完成,剩下的其他工作将主要是对图像处理算法的寻求和发掘。

本例是以使用基于Platform SDK的Windows程序开发为基础的,但重点在于讲解PS插件开发,因此没有详细讲解Windows程序开发中的一些技术细节。

(5)最后是源代码(增量更新)的下载链接:

http://files.cnblogs.com/hoodlum1980/FillRed.rar

(6)我的相关文章:

《怎样编写一个Photoshop滤镜(1)》

《怎样编写一个Photoshop滤镜(2)》

《怎样编写一个Photoshop滤镜(3) -- Scriping PlugIn》

【备注】有网友反应范例中的编译结果在PS CS2下无法加载,我发现编译好的滤镜文件仅有60KB左右(这样小感觉不太正常),而正常应该有至少有几百KB大小才对,可能是因为某些依赖未能静态链接。因此这就会导致这个DLL在我的机器环境上能够正常加载,但换个环境可能就会导致缺少依赖而无法加载。

我把项目中的ATL选项设置为“静态链接到ATL”后,编译结果就成了几百KB。使用我同事的机器环境(安装了PS CS2版本)作为测试,这时果然能够正常被PS CS2加载到了。

怎样编写一个Photoshop滤镜(4) -- 在对话框上增加缩略图相关推荐

  1. 怎样编写一个Photoshop滤镜(1)

    在很久前我曾经写过一篇文章简要讲述了 Photoshop 的滤镜开发的基本概念,并描述了滤镜和 PS之间的协作关系,也提供了一个雨滴效果滤镜的 Demo.但是缺少源代码.而且我们将要产生疑问,我们如何 ...

  2. 怎样编写一个Photoshop滤镜(3)-- Scripting Plug-ins

    在第一篇文章中我们建立了一个没有UI的基本滤镜框架,并且引入PIPL资源使之能被PS加载到菜单.在第二篇文章中我们又引入了滤镜参数和相应的对话框资源,并且讲解了对话框在滤镜调用流程中的显示时机.这一篇 ...

  3. 编写一个程序,输入梯形的上底,下底和高,输出梯形的面积

    package HomeWork; import java.util.Scanner; public class Test17 { public static void main(String[] a ...

  4. 已知,1英里等于1.6公里,编写一个程序,将从键盘上输入的英里数转换为公里数,结果保留两位小数。...

    代码: 1 import java.util.*; 2 public class Main { 3 public static void main (String [] args) { 4 Scann ...

  5. 通讯录c语言总体程序框图,用c语言编写一个通讯录,并对他进行增加,查询,删除,修改,显示记录等操作,要写出源代码并画出流程图...

    满意答案 这个是我编的,用数组和链表两种功能实现的通讯录 基本能满足你的要求!! 代码如下: #include "stdlib.h" #define NEW (struct nod ...

  6. 海龟编辑器怎么运行html,使用海龟编辑器编写一个GUI简单对话框

    海龟编辑器是一款专为青少年提供的编程学习软件,海龟编辑器可以帮助青少年快速的理解和记忆PAthon这门语言,了解GUI的世界,该软件集合了多项库功能,第一类,识别功能,该功能可以识别图像.人脸.声音等 ...

  7. Photoshop滤镜开发简介(2)--Photoshop回调函数

    在上一篇文章中,我们介绍了开发Photoshop滤镜插件最基本的一些概念和基础.Ps为了满足插件的应用需求,同时也给插件提供了大量的回调函数(或服务).例如,滤镜可以在一次调用后,保存最近一次用户设置 ...

  8. Photoshop滤镜打造精致水晶美女头像(转)

    Photoshop滤镜打造精致水晶美女头像(转)[@more@] 当你静下心来仔细品味水晶艺术的时候,会发现它们别有一番韵味.下面我们就用Photshop来打造一个水晶美女. 水晶给人的印象就是晶莹. ...

  9. 职称计算机作用,2010年职称计算机:Photoshop滤镜作用

    stylize(风格化)可以产生不同风格的印象派艺术效果.有些滤镜可以强调图象的轮廓:用彩色线条勾画出彩色图象边缘,用白色线条勾画出灰度图象边缘. Find Edge(查找边缘) 可以强调图象的轮廓, ...

最新文章

  1. Cash Shuffle初次测试成功,BCH隐私研究逐步推进
  2. 构建nodejs环境
  3. log4j 打印线程号配置_log4j配置参数
  4. [9]UITableView表视图1
  5. .NET 6 新特性 Parallel ForEachAsync
  6. java 三大特性理解_java 三大特性--封装、继承和多态理解
  7. linux安装与登录
  8. windows下载mysql太慢
  9. 白话布隆过滤器(BloomFilter)
  10. 微信公众号不限次数发送消息
  11. 何为民间IP,小游戏竟然还可以这样做?脑洞大开
  12. JAVA程序设计题——英雄对战游戏,定义一个描述战斗单位的英雄(Hero)类,此类必须包含以下成员变量:名称(name),生命值(life),技能1攻击力(damage1),防御力(defence)
  13. 云服务器搭建个人站点-之-站点搭建
  14. 关于ES2020语法2345加速浏览器不兼容问题
  15. C++获取网卡名称和IP地址
  16. GD32汽车诊断KWP 协议/ ISO-14230测试
  17. AndroidStudio - - - 点击头像更换头像_菜单选择_相机拍照与相册获取
  18. Shiro 权限绕过漏洞分析(CVE-2020-1957)
  19. VS code 如何使用HTML Boilerplate插件
  20. 领导不喜欢这几种下属

热门文章

  1. python个人简历爬取_python 爬取免费简历模板网站的示例
  2. python jupyter notebook_jupyter notebook 可以做哪些事情?
  3. vSAN 报警处理:虚拟机的放置和可用性状态不可访问
  4. linux创建用户指定用户目录,linux创建用户并指定用户的默认目录 bash-4.2$
  5. 917:Knight Moves
  6. 计算机季度函数,季度怎么分月份(根据月份函数生成季度)
  7. switch高级用法
  8. 数据运营系列(三):熵权法如何确定指标权重构建评价体系
  9. 成为抖音带货达人,一定要知道这三点!
  10. 百度网盘 分享链接批量转存方法【2020-10】