在上一篇文章里,我们讲解了为滤镜添加术语资源,从而使我们的滤镜可以被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 ;
==================================================
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结构)。由于这是一个有难度的地方,因此我特别把这个函数代码放在此处展示,代码如下:
//定义描述缩略图数据的结构(在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 的一种标准用法:
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程序开发中的一些技术细节。
更多怎样编写一个Photoshop滤镜 -- 在对话框上增加缩略图 相关文章请关注PHP中文网!