Предисловие
Довольно часто слышу мнение, что ассемблер мертвый язык, что он никому не нужен, за исключением особых ситуаций и т.д. и т.п. Хочу надеяться, что эта статья будет неплохим гвоздем в гроб этих мненийеще одним гвоздем в крышку гроба этих мнений, т.к. в этом рассмотренном ниже случае его применение более чем оправдано. Как говорил Остап Бендер: «Слухи о моей смерти явно преувеличены». Тоже самое может сказать и за ассемблер, который периодически закапывают не уставая все кому не лень со времени его появления. И все-таки он жив. Жив и активно развивается. Убедитесь сами!
Пишем фильтр
Являясь большим поклонником как замечательного продукта Adobe Photoshop, так и не меньшим (если не большим) поклонником ассемблера, мне подумалось - почему бы не усилить эффект и соединить два в одном - так, собственно, и родилась идея написания плагина-дополнения для Photoshop, и непременно на ассемблере. Порывшись в интернете и скачав Photoshop 6.0 SDK, я обнаружил практически полное отсутствие информации об этом, а платить Adobe за участие в их форумах не хотелось. Но кое-какую информацию найти все же удалось - совершенно случайно на сайте WhizKid-а я обнаружил пример фильтра для Photoshop, но написан он был достаточно громоздко с использованием синтаксиса nasm. Пришлось заняться рассмотрением примеров и углубиться в чтение документации, содержащимися в SDK, благо - все оказалась достаточно информативным.
Что удалось выяснить. Photoshop изначально создавался как продукт для Apple Macintosh, соответственно первые плагины и интерфейс взаимодействия программы-хоста (Photoshop) и плагина создавался, учитывая специфику яблочных компьютеров. Отголоски этого при написании плагина в среде Windows состоят в специфическом формате ресурсов PiPL. Но об этом формате немного позже. Еще одним наследием Apple является то, что данные и указатели для Photoshop формируются в формате big endian (в отличие от little endian у процессоров Intel), но я пока не замечал этого влияния, так что об этом пока достаточно просто упомянуть (с этим столкнутся те, кто будет писать плагины импорта и экспорта).
2.1 Ресурсы
Формат ресурсов для фильтров Photoshop отличается от формата ресурсов программ для Windows. Остановимся на его основных элементах подробнее.
| Kind { Filter } | - тип плагина, возможные варианты - "Filter", "Parser", "ImageFormat",
"Extension", "Acquire", "Export". Соответственно у нас тип плагина - фильтр. |
| Name { "masquer" } | - имя в меню Plugins, здесь только фантазия разработчика. |
| Category { "masquer test" } | - имя в субменю, здесь будет masquer->masquer test. |
| Version { (1 << 16) | 0 } | - версия плагина, здесь 1.0. |
| CodeWin32X86 { "PluginMain" } | - точка входа в наш плагин. |
| SupportedModes | - режимы изображений, которые мы будем поддерживать, у нас
будет так: noBitmap, doesSupportGrayScale, noIndexedColor, noRGBColor, noCMYKColor, noHSLColor, noHSBColor, noMultichannel, noDuotone, noLABColor |
| EnableInfo | - строка, при выполнении условий которой, наш плагин будет
находиться в положении Enable. Например, "in (PSHOP_ImageMode, GrayScaleMode)"Функция in возвращает истинное значение только тогда, когда первый параметр соответствует хотя бы одному из следующих. В данном случае мы имеем истинное значение, если изображение находится в режиме градаций серого. |
| FilterCaseInfo | - массив из 7 элементов, каждый из которых состоит из четырех
байт. Первые два значения определяют действия хоста для пре- и постпроцессинга.
Т.е. определив в нашем случае inStraightData, outStraightData мы говорим
хосту (Adobe Photoshop) что мы хотим получить наши данные немаскированными,
и отдадим их с тем же условием, что хост не будет их демаскировать (dematte).
Оставшиеся элементы составляют комбинацию флагов:
|
Полный список функций и параметров можно найти в файле Plug-in Resource Guide.pdf.
Теперь, когда у нас готов файл с ресурсами (расширение *.r), для пишущих под Windows необходимо привести этот формат к стандартному. Для этого с Photoshop 6.0 SDK идет утилита Cnvtpipl.exe. В качестве параметра нужно просто указать наш файл с ресурсом и все - у нас уже есть файл с "нормальными" ресурсами (*.rc).
2.2 Структура FilterRecord и написание фильтра
Собственно теперь мы можем приступать к непосредственному написанию кода нашего
плагина. Но перед этим рассмотрим одну очень важную для нас структуру. А именно
речь пойдет о структуре FilterRecord. Всю структуру я рассматривать не буду,
она слишком большая, да и нет в этом смысла, я начну, а те кому это нужно будет,
уже сами смогут разобрать остальные поля. Итак:
| serialNumber | dd 0 | В предыдущих версиях здесь был серийный номер Photoshop-а и плагин мог применять его для copy-protection. Не знаю как в 6, а в 7 версии здесь 0 |
| abortProc | dd 0 | Адрес процедуры TestAbort. Не интересно |
| progressProc | dd 0 | Адрес процедуры UpdateProgress. Для создания ProgressBar. Обойдемся без него. |
| parameters | dd 0 | Параметры, задаваемые пользователем. Для нас неинтересно. В начале содержит 0 |
| imageSize | pPOINT<> | Размеры изображения. Структура из двух word-ов. Отсюда вытекает, что изображение не может быть больше, чем 65535х65535 пикселей |
| planes | dw 0 | количество каналов, например для CMYK - 4, для RGB - 3, мы упростим себе задачу и выберем GrayScale - 1. |
| filterRect | pRECT<> | Общий фильтруемый прямоугольник. Даже если мы фильтруем неровное выделение, все равно нам будет передан граничащий прямоугольник, внутри которого располагается выделение. Структура из 4 word-ов: top, left, bottom и right соответственно. |
| background | RGBColor<> | Цвет подложки (background) |
| foreground | RGBColor<> | Цвет переднего плана (foreground) |
| dw 0 | Для выравнивания внутри структуры | |
| maxSpace | dd 0 | Максимально возможный объем данных... |
| bufferSpace | dd 0 | ...и буфера |
| inRect | pRECT<> | Прямоугольник, который будет передан фильтру в текущей итерации. Для уменьшения расхода памяти рекомендуется условно разбить изображение на прямоугольники типа 128х128, или 256х256 и "кусками" обрабатывать изображение. Мы этим пренебрежем сейчас и закажем все изображение. |
| inLoPlane | dw 0 | Первый запрашиваемый канал... |
| inHiPlane | dw 0 | ...и последний |
| outRect | pRECT<> | Прямоугольник для результирующего изображения. У нас inRect и outRect будут совпадать |
| outLoPlane | dw 0 | так же, как и для inRect |
| outHiPlane | dw 0 | |
| inData | dd 0 | адрес, где находится исходное изображение для обработки |
| inRowBytes | dd 0 | смещение между строками изображения. Дело в том, что если ширина изображения не кратна 32, а адрес каждой новой строки должен быть кратен 32. Соответственно в таких случаях остаток до границы строки заполняется 0 и не учитывается при выводе результата. |
| outData | dd 0 | адрес, где мы можем разместить результат |
| outRowBytes | dd 0 | то же смещение |
Ну, пятую часть структуры я разобрал, нам этого вполне достаточно. Теперь мы практически во всеоружии и можем писать код.
Основной экспортируемой нашим плагином функцией является PluginMain. Экспортируемой, потому что наш плагин не что иное, как библиотека dll, имеющая расширение 8bf. В качестве входящих параметров мы имеем selector, адрес filterRecord, data, result. Начнем с конца, как с наименее важных параметров:
align 4 DoAbout proc invoke MessageBox, 0, ADDR szText, ADDR szText, MB_OK ret DoAbout endp
Параметром номер 1 является filterSelectorParameters, где по идее мы должны обработать передаваемые нам параметры и отразить диалог, мы упростим все до одного ret.
align 4 DoParameters proc ret DoParameters endp
2 - filterSelectorPrepare - здесь мы должны посчитать и выделить память, а можно и нули занести, тогда Photoshop сам это за нас сделает. Так и решим.
align 4 DoPrepare proc xor eax, eax mov fr.bufferSpace, eax mov fr.maxSpace, eax ret DoPrepare endp
3 - filterSelectorStart - здесь мы уже обрабатываем изображение
align 4 DoStart proc uses edi esi mov edx, dword ptr fr assume edx:PTR FilterRecord ; edx указывает на FilterRecord movzx eax, [edx].imageSize.h ; получаем размеры изображения movzx ebx, [edx].imageSize.v mov word ptr [edx].inRect.top, 0 ; укажем хосту что именно мы хотим обрабатывать mov word ptr [edx].inRect.left, 0 mov word ptr [edx].inRect.bottom, bx mov word ptr [edx].inRect.right, ax mov word ptr [edx].outRect.top, 0 ; здесь inRect = outRect mov word ptr [edx].outRect.left, 0 mov word ptr [edx].outRect.bottom, bx mov word ptr [edx].outRect.right, ax mov eax, [edx].advanceState ; пускай хост обновит параметры, call eax ; хотя нам в данном случае это и не нужно. Это необходимо при ; обработке изображения "кусками" mov edx, dword ptr fr assume edx:PTR FilterRecord mov esi, dword ptr [edx].outData ; так как у нас inRect = outRect,
; то esi указывает на данные для обработки mov edi, esi ; и edi тоже movzx eax, [edx].imageSize.h ; рассчитаем количество итераций цикла обработки mov ebx, eax shr eax, 4 test ebx, 0Fh jz @@_1 inc eax @@_1: shl eax, 4 movzx ebx, [edx].imageSize.v mul ebx mov ecx, eax shr ecx, 4 ; наконец-то рассчитали ; дальше идет основной цикл вычислений. ; Чтобы особо не выдумывать я сдвигаю каждое значение яркости на 1 бит влево используя MMX. ; Но об этом в другой раз. @@next: movq mm0, [esi] movq mm1, [esi+8] movq mm2, mm0 movq mm3, mm1 psllw mm0, 9 psrlw mm2, 8 psllw mm2, 9 psrlw mm2, 8 psllw mm1, 9 psrlw mm3, 8 psllw mm3, 9 psrlw mm3, 8 por mm0, mm2 por mm1, mm3 movq [esi], mm0 movq [esi+8], mm1 add esi, 16 dec ecx jz @@fin jmp @@next ;end of main @@fin: emms ; вдруг кому-то еще с сопроцессором работать нужно mov edx, dword ptr fr assume edx:PTR FilterRecord mov dword ptr [edx].inRect.top, 0 ; скажем хосту - все готово mov dword ptr [edx].inRect.bottom, 0 mov dword ptr [edx].outRect.top, 0 mov dword ptr [edx].outRect.bottom, 0 mov dword ptr [edx].maskRect.top, 0 mov dword ptr [edx].maskRect.bottom, 0 ret DoStart endp
4 - filterSelectorContinue - так как мы за раз уже обработали изображение на третьем шаге, то здесь нам делать нечего, хотя правильно было бы именно сюда вынести обработку.
align 4 DoContinue proc ret DoContinue endp
5 - filterSelectorFinish - финиш - он и есть финиш - очистка ресурсов, а так как никаких ресурсов мы не занимали, то и чистить нам нечего.
align 4 DoFinish proc ret DoFinish endp
Теперь посмотрим, как все это вызвать, чтобы работало. Собственно, процедура PluginMain
.data szText db "masquer's Photoshop Plugin",0 procs dd DoAbout, DoParameters, DoPrepare, DoStart, DoContinue, DoFinish
;LUT (LookUp Table) align 4 fr FilterRecord <> outRect pRECT <> imageSize pPOINT <> .code ... align 4 PluginMain proc uses ebp edi ecx, selector:DWORD,
filterRecord:DWORD, data:DWORD, result:DWORD mov ecx, result ; нулевой результат нам не нужен jcxz @@exit mov eax, filterRecord lea eax, [eax] mov dword ptr [fr], eax ; настроим указатели mov eax, selector cmp eax, 5 ja @@exit call [procs+eax*4] ; вызовет процедуру в соответствии с селектором @@exit: mov esp, ebp ; подчистим стек pop ebp retn PluginMain endp ...
Ну, и, как и у любой нормальной dll, пропишем DllEntry
align 4 DllEntry proc hInstDLL:DWORD, reason:DWORD, unused:DWORD mov eax, 1 ret DllEntry Endp
Для отладки я использовал незаменимый во всех случаях жизни SoftIce, а точнее я цеплялся к MessageBoxA, смотрел хелп, ну а дальше уже дело техники. Напоследок приведу только содержимое .def файла
LIBRARY test.8bf EXPORTS PluginMain
Да, чуть не забыл. Для того чтобы все это заработало, достаточно скопировать полученный test.8bf в папку к плагинам [Здесь путь к каталогу Photoshop]\Plug-Ins\test.8bf.
Заключение
Как говорится - вуаля! Заготовка полностью написана, теперь только знание математики и ее применение к обработке изображений сдерживает наш творческий порыв. Но к следующему разу я что-нибудь обязательно придумаю, про MMX, например, попробуем рассказать, что это такое и где его можно применить.
[C] masquer