Обрабатываем картинки средствами Photoshop и ExtendScript Toolkit

    Часто нам бывает надо сделать что-то с пачкой картинок. Есть несколько способов добиться этого:
    • используя ImageMagick – очень удобная консольная утилита, много чего умеющая
    • на The GIMP – там есть Scheme (диалект lisp-а) и Python
    • штатными средствами: PHP+gd / Powershell+System.Drawing / Python + PIL
    • в photoshop-е на JScript, VBScript или AppleScript
    Плюсы минусы последнего способа рассмотрим под катом. В качестве бонуса посмотрим на недокументированное API Photoshop-а.

    Нам понадобится

    • Adobe Photoshop CS5 (можно CS4)
    • Adobe ExtendScript Toolkit (входит в дистрибутив Photoshop)
    • Знание JScript
    • Несколько фоток

    Теория


    У Photoshop-а есть COM API, в котором покрыты многие из фотошоповских функций. Его, разумеется, можно использовать из JS- или VBS-скриптов. Adobe любезно предоставила разработчикам свою IDE, с автокопмлитом и брейкпоинтами. Поддерживаемые языки в ней JScript, VBScript (Win) и AppleScript (Mac). Я остановился на JScript, потому как большинству будет лучше всего понятен именно он.

    IDE


    Её зовут ExtendScript Toolkit. Вот она:ExtendScript Toolkit
    Что в ней меня поразило:
    • по умолчанию установлен не моноширинный шрифт, а какая-то дрянь. Тут же пофиксил
    • нет watch-ей. За это казнить надо. Их роль выполняет data browser и javascript console
    • по привычке нажал ctrl-D (копирует строчку в Решарпере) – и о чудо, оно работает!
    • там есть профайлинг
    • хелп есть, по контенту сносный, но до уровня msdn недотягивает.
    Скрпиты можно сохранить в формате jsx, при его открытии увидите вопрос: «запустить скрипт или редактировать?».
    Приятно, что jsx можно компилировать (File → Export as binary), при этом будет создан файл с расширением jsxbin. Контент его будет примерно таким:
    @JSXBIN@ES@2.0@MyBbyBnAIMVbyBn0AHJWn
    Удобно, особенно если надо написать скрипт для фотошопа под заказ и не хочется давать исходники. Насчёт возможности его декомпиляции я детально не разбирался, но думаю, что переменные он меняет и кое-какую оптимизацию всё-таки делает.
    Итак, IDE на первый взгляд неудобная, но поработав в ней минут 30, привыкаешь.

    Скриптовый язык


    Начинается он с фразы
    #target photoshop
    Это обычный javascript с библиотеками Adobe.
    Есть средства для работы с файловой системой, поддержка сокетов, reflection, XML. Класс Object есть.
    Для подключения к Photoshop-у существует глобальный объект app, ActiveXObject делать не надо. Активный документ в нём – app.activeDocumet. Функция alert показывает сообщение в Photoshop-е.
    При падении ошибки ничего не происходит, скрипт молча прекращает выполнение, как будто и не было его вовсе.
    Понравилось, как измерения (px, pt, cm, mm) конвертируются друг в друга:
    app.activeDocument.width.as("px");
    Т.к. ExtendScript кроссплатформенный, пути к файлам представляются как /d/Temp/…

    Живой пример


    Задача: в папке есть 100 файлов. Надо внедрить в каждый из них лого, которое есть в PSD-файле.
    Пример лого:
    logo example
    А вот и скрипт:
    #target photoshop
    app.bringToFront(); // запускаем Photoshop. Если он уже запущен, подключимся именно к нему, не к новому инстансу.
    var Constants = { /* определим кое-какие константы */ }
    ProcessDir(Constants.InputDir, Constants.OutputDir);
    function ProcessDir(dir, outDir) {
      var folder = Folder(dir); // Adobe-овский объект
      var files = folder.getFiles(Constants.FileMask); // Внимание, две маски через запятую (*.jpg,*.png) уже не работают.
      var outFolder = Folder(outDir);
      if (!outFolder.exists) {
        if (!outFolder.create()) {
          alert("Cannot create output folder");
          return; // может и не получиться
        }
      }
      var totalFiles = 0;
      for (var fileNum in files) {
         var outFile = GetOutputFileName(files[fileNum], outFolder.fullName); // куда писать результат
         AddLogoToFile(files[fileNum], outFile); // собственно, сама обработка
         totalFiles++;
      }
      alert(totalFiles + " files processed"); // увидит юзер в Photoshop-е в конце обработки
    }
    function AddLogoToFile(file, outputFile) {
      var photoFile = File(file); // Так открываются файлы, строчку open не понимает
      var logoFile = File(Constants.AddLogo.LogoPath);

      app.open(logoFile); // открываем лого
      app.activeDocument.artLayers["Text"].copy(); // ArtLayers – слои в файле. Этот слой назывался "Text"
      var logoWidth = app.activeDocument.width.as("px");
      var logoHeight = app.activeDocument.height.as("px");
      app.activeDocument.close();

      app.open(photoFile); // открываем фотку

      var width = app.activeDocument.width.as("px");
      var height = app.activeDocument.height.as("px");

      var logoLayer = app.activeDocument.artLayers.add(); // добавляем на фотку новый слой, куда поместим лого
      logoLayer.name = "Logo"; // название нового слоя

      app.activeDocument.paste(); // вставляем лого из clipboard

      var shape = [ // Photoshop вставляет всё в середину; выделяем лого, чтобы перенести его
        [(width - logoWidth) / 2, (height - logoHeight) / 2],
        [(width - logoWidth) / 2, (height + logoHeight) / 2],
        [(width + logoWidth) / 2, (height + logoHeight) / 2],
        [(width + logoWidth) / 2, (height - logoHeight) / 2]
      ];
      app.activeDocument.selection.select(shape);

      app.activeDocument.selection.translate( // переносим selection вправо вниз
        new UnitValue((width - logoWidth)/ 2, "px"),
        new UnitValue((height - logoHeight) / 2, "px"));

      var minImageDimension = Math.min(width, height); // масштабируем лого, чтобы оно было в 5 раз меньше минимального размера фотки
      var logoScaleMultiplier = minImageDimension / 5 / logoWidth * 100;
      app.activeDocument.selection.resize(logoScaleMultiplier, logoScaleMultiplier, AnchorPosition.BOTTOMRIGHT); // обратите внимание на последний аргумент

      app.activeDocument.selection.deselect();

      app.activeDocument.artLayers["Logo"].opacity = 75; // делаем слой полупрозрачным
      app.activeDocument.artLayers["Logo"].blendMode = BlendMode.LUMINOSITY; // устанавливаем режим смешивания, чтобы выглядело симпатичнее
      // а вот тут бы установить blending options! Об этом читайте дальше.
      SaveFile(outputFile); // сохранит и закроет файл
    }

    function SaveFile(outputFile) {
      var isPng = /png$/i.test(outputFile);
      var saveOptions;
      if (isPng) {
        saveOptions = new PNGSaveOptions();
      } else {
        saveOptions = new JPEGSaveOptions(); /* неинтересный код про качество картинки */
      }
      app.activeDocument.saveAs(File(outputFile), saveOptions, true, Extension.LOWERCASE) 
      app.activeDocument.close(SaveOptions.DONOTSAVECHANGES); // закрываем документ
    }
    Скрипт готов. Осталось сделать лого в формате PSD – такое, чтобы внутри был слой Text, на котором и должно быть размещено лого.
    Пример того, что получится:
    фото с лого
    Полностью скрипт вылолжил на pastebin.

    О грустном


    Самое вкусное, что есть в Photoshop-е – blending options! А их-то в API как раз и нет. Есть copyLayerStyle, но она работает некорректно даже из GUI (вы можете это проверить, поиграв с параметрами drop shadow). Поэтому лого, конечно, мы вставить можем, но результат будет не сильно превосходить тот же ImageMagick.
    UPD: есть два способа быстро и легко применить стили из скрипта:
    • записав Action с этими настройками и выполнив его (спасибо за подсказку serge2)
    • сохранить стиль в preset-ах (используя кнопку «New Style» в диалоге «Blending Options»)

    Немного о недокументированном API


    Почитав доки (вы можете найти их в %ProgramFiles%Adobe\Adobe Photoshop CS5\Scripting\Documents\), мы узнаём, что оказывается, Photoshop умеет записывать действия пользователя. Для этого надо:
    1. Скопировать файл «ScriptListener.8li» из %ProgramFiles%Adobe\Adobe Photoshop CS5\Scripting\Utilities\ в %ProgramFiles%Adobe\Adobe Photoshop CS5\Plug-ins\Automate\
    2. (пере)запустить Photoshop
    3. Сделать то действие, о котором хочется узнать
    4. Найти на рабочем столе файлы ScriptListener.jsx и ScriptListener.vbs
    5. Не забыть удалить ScriptListener.8li (он тормозит работу Photoshop)
    В надежде заполучить код того, что мы ждали, открываем с рабочего стола ScriptListener.jsx. И тут нас ждёт сюрприз: в файле вот такой неюзабельный трэш:
    var idsetd = charIDToTypeID( "setd" );
      var desc15 = new ActionDescriptor();
      var idnull = charIDToTypeID( "null" );
        var ref6 = new ActionReference();
        var idPrpr = charIDToTypeID( "Prpr" );
        var idLefx = charIDToTypeID( "Lefx" );
        ref6.putProperty( idPrpr, idLefx );
        var idLyr = charIDToTypeID( "Lyr " );
        var idOrdn = charIDToTypeID( "Ordn" );
        var idTrgt = charIDToTypeID( "Trgt" );
        ref6.putEnumerated( idLyr, idOrdn, idTrgt );
      desc15.putReference( idnull, ref6 );
      var idT = charIDToTypeID( "T  " );
        var desc16 = new ActionDescriptor();
        var idScl = charIDToTypeID( "Scl " );
        var idPrc = charIDToTypeID( "#Prc" );
        desc16.putUnitDouble( idScl, idPrc, 100.000000 );
        var idDrSh = charIDToTypeID( "DrSh" );
          var desc17 = new ActionDescriptor();
          var idenab = charIDToTypeID( "enab" );
          desc17.putBoolean( idenab, true );
          var idMd = charIDToTypeID( "Md " );
          var idBlnM = charIDToTypeID( "BlnM" );
          var idMltp = charIDToTypeID( "Mltp" );
          desc17.putEnumerated( idMd, idBlnM, idMltp );
          var idClr = charIDToTypeID( "Clr " );
            var desc18 = new ActionDescriptor();
            var idRd = charIDToTypeID( "Rd " );
            desc18.putDouble( idRd, 0.000000 );
            var idGrn = charIDToTypeID( "Grn " );
            desc18.putDouble( idGrn, 0.000000 );
            var idBl = charIDToTypeID( "Bl " );
            desc18.putDouble( idBl, 0.000000 );
          var idRGBC = charIDToTypeID( "RGBC" );
          desc17.putObject( idClr, idRGBC, desc18 );
          var idOpct = charIDToTypeID( "Opct" );
          var idPrc = charIDToTypeID( "#Prc" );
          desc17.putUnitDouble( idOpct, idPrc, 75.000000 );
          var iduglg = charIDToTypeID( "uglg" );
          desc17.putBoolean( iduglg, true );
          var idlagl = charIDToTypeID( "lagl" );
          var idAng = charIDToTypeID( "#Ang" );
          desc17.putUnitDouble( idlagl, idAng, 120.000000 );
          var idDstn = charIDToTypeID( "Dstn" );
          var idPxl = charIDToTypeID( "#Pxl" );
          desc17.putUnitDouble( idDstn, idPxl, 5.000000 );
          var idCkmt = charIDToTypeID( "Ckmt" );
          var idPxl = charIDToTypeID( "#Pxl" );
          desc17.putUnitDouble( idCkmt, idPxl, 0.000000 );
          var idblur = charIDToTypeID( "blur" );
          var idPxl = charIDToTypeID( "#Pxl" );
          desc17.putUnitDouble( idblur, idPxl, 5.000000 );
          var idNose = charIDToTypeID( "Nose" );
          var idPrc = charIDToTypeID( "#Prc" );
          desc17.putUnitDouble( idNose, idPrc, 0.000000 );
          var idAntA = charIDToTypeID( "AntA" );
          desc17.putBoolean( idAntA, false );
          var idTrnS = charIDToTypeID( "TrnS" );
            var desc19 = new ActionDescriptor();
            var idNm = charIDToTypeID( "Nm " );
            desc19.putString( idNm, "Linear" );
          var idShpC = charIDToTypeID( "ShpC" );
          desc17.putObject( idTrnS, idShpC, desc19 );
          var idlayerConceals = stringIDToTypeID( "layerConceals" );
          desc17.putBoolean( idlayerConceals, true );
        var idDrSh = charIDToTypeID( "DrSh" );
        desc16.putObject( idDrSh, idDrSh, desc17 );
      var idLefx = charIDToTypeID( "Lefx" );
      desc15.putObject( idT, idLefx, desc16 );
    executeAction( idsetd, desc15, DialogModes.NO );
    Как вы думаете, что делает этот код? Он добавляет тень (Drop Shadow) к слою, это видно по название «DrSh». Я подозреваю, что внутри Photoshop-а прямо так и называются контролы в GUI.
    Но, выполнив этот код, обнаружим, что он работает.
    Можно найти, что executeAction может как показать диалог пользователю, так и сделать свою работу молча (это определяет последний параметр). Сами ID-шники нигде не описаны, о них (как и о том, что будет с ними в CS6) мы можем только гадать.
    Тем не менее, фича логгирования действий довольно интересная, если очень надо, можно по-быстрому накидать скриптик для себя.

    Ещё скрипты


    Заодно я написал вот такие функции:
    • ресайзинг картинок до определённого размера (ширина не больше X, высота не больше Y)
    • добавление рамок к картинкам – таких же, как в этом топике
    Если вам интересно, вы можете посмотреть их в том же скрипте на pastebin.

    Интересные факты

    • в API есть поддержка RAW. После того, как вы обработали RAW-файлы в Photoshop-е, сохранив в них настройки, вы можете быстро сконвертировать их в JPEG
    • в отличие от blending options, фильтры представлены в API довольно хорошо, для каждого из них есть функция
    • код в jsx-файлах можно вешать на события в Photoshop: например, при открытии файла добавлять в него новый слой, и так далее
    • API есть и для Illustrator, и для Bridge
    • из API есть доступ к гистограмме и к каналам

    Выводы


    API вкусное, очень вкусное. Но отсутствие поддержки blending options сильно удручает; если они нужны – будьте готовы к тому, что придётся возиться со страшным кодом. Если всё, что вам надо (что как раз и надо в большинстве случаев от пакетной обработки) – обвести картинку рамочкой, думаю, ImageMagick в этом случае будет быстрее и намного удобнее.

    + / -

    plus фильтры, гистограммы
    plus RAW
    plus color profiles, как в Photoshop-е
    plus javascript – удобный, понятный почти всем язык
    plus документация с примерами
    minus отсутствие blending options
    minus для работы нужен Photoshop /* внезапно */
    minus работает довольно медленно

    Почитать


    Adobe Photoshop Scripting – официальный ресурс
    Scripting Photoshop – небольшой, но полезный тьюториал по скрпитингу в Photoshop
    PS-Scripts – форум о скриптах для Photoshop

    Подумать


    В качестве упражнения предлагается скрипт, который может действительно пригодиться: сделать так, чтобы лого на фотке добавлялось в той же цветовой гамме, что и фотка – например, синее или жёлтое для сине-жёлтой фотки: это сделает лого не портящим общий цвет и настроение фотки. Лого не должно сливаться с цветом, т.е. не быть синим на синем. Кроме того, будет классно, если лого не будет на поверхности вроде травы, его можно попробовать перенести в другой угол или перекрасить.
    Поделиться публикацией
    Никаких подозрительных скриптов, только релевантные баннеры. Не релевантные? Пиши на: adv@tmtm.ru с темой «Полундра»

    Зачем оно вам?
    Реклама
    Комментарии 36
    • +3
      Blending options можно записать в Action и запустить его из скрипта :) Пользуйтесь и не грустите больше!
      • 0
        За идею с Action спасибо, я как-то не подумал. Есть ещё способ сохранить настройки стиля в preset-ы и заюзать их. Обновил топик.
      • –1
        Спасибо. Очень полезно, и возможно, пригодится, поэтому в избранное.
        • +3
          Кстати, для наложения логотипа можно было использовать чистый Action через Batch. Скрипты полезны если нужно создавать динамические переменные, например меняющуюся нумерацию, или брать имена людей из массива и писать на пригласительных карточках, и т. п.

          Если что интересует по фотошопу и скриптам, спрашивайте…
          • 0
            можно, ну я, как говорится, just for fun, чтобы поучиться скриптам =)
            • 0
              через Action не всегда получается сделать. Например загнать логотип в правый нижний угол и применить это к пачке смешанных (горизонтальных и вертикальных) кадров.
              • 0
                Думаю я смогу это сделать. Если хотите практическую реализацию — с вас бутылка пива :)
                • 0
                  так у меня то есть, но решение ооочень уж нестандартное, с переварачиванием картинки и написанием текста вверногами :)
                  • 0
                    А если логотип — картинка? :)

                    Не нужно так извращаться, вот вам более приличный вариант.
                    1. Открываем логотип, выделяем его и копируем в клипборд CTRL+C
                    2. Открываем картинку и начинаем запись Экшена, вставляем логотип CTRL+V
                    3. Выделяем оба слоя в палитре Layers и используем Align из панели вверху, соответственно выбираем вправо и вниз (смотря в какой угол нам нужно)

                    Вот и все :)
                    Тут только одна сложность, логотип прислонится своими крайними точками к краю изображения, поэтому придется либо записать еще одно действие отступа, либо добавить в логотип пиксель с минимальной плотностью чтобы он был почти незаметным, но обозначал нижнюю и правую границы.
                    • 0
                      о! спасибо за мысль, про выравнивание слоями я не подумал… не пользуюсь этой функцией в повседневном фотошопаньи
              • 0
                Можно я спрошу?
                Как-то решил для заказываемых фоток сделать скрипт, который бы вставлял дату съемки в определенное место в изображение (JPEG). В моем случае Actions не помогали. Я справился со всем, кроме одного — я так и не понял, какая из дат хранит информацию о дате СЪЕМКИ. Даже при копировании файлов разными способами (Проводник, TotalCom и др.) дата создания и дата изменения вели себя по-разному. В результате при вставке скриптом нередко дата была неверной, более «новой». Как же узнать дату съемки?
                P.S. К сожалению, прямо сейчас исходный код предоставиьт не могу — где-то зарыт в архивах, поищу.
                • 0
                  Дата съёмки хранится в EXIF (это метаинформация в самом jpeg-файле; вместе с датой там есть параметры экспонирования и ещё куча чего), пример чтения EXIF-свойств, в том числе, даты съёмки, есть тут
                  • 0
                    Выше уже ответили про EXIF
                  • 0
                    Привет из будущего! У меня вопрос. Как сохранять переменные между скриптами? Сейчас использую текстовый файл, но есть подозрение, что это небыстрый вариант
                  • +4
                    • +3
                      imagemagick имеет наследника — graphicsmagick, его активно flickr использует, к примеру.

                      С его помощью, такая тривиальная задача как внедрение лого в 100 файлов будет занимать 1 строку (3, если считать for i in * do; и done;)
                      • 0
                        graphicsmagick — это не наследник, а форк. И по пакетной обработке идентичен imagemagick.
                      • 0
                        PIL для Python ещё забыли.
                        • 0
                          Это тоже одно из штатных средств; у каждого языка такое есть. Добавил, чтоб тоже было.
                        • 0
                          никогда не мог понять почему в Фотошопе экшоны сделаны так, что мы видим весь процес происходящего…
                          Неужели у них представление от логики не отделено?
                          • 0
                            Думаю, причина ещё в том, что фотошоп часто любит вопросы задавать. Например, если вы будете вставлять картинку с одни цветовым профиль в картинку с другим, он спросит пользователя об этом. Но да, согласен: вариант без GUI тоже был бы интересным — для серверов, например (да ещё много где пригодился бы). Может кто знает, как это можно сделать?
                            • 0
                              а разве дроплеты это не экшены без GUI?
                              • 0
                                Дроплеты тоже в гуе работают, по крайней мере в CS4 точно так было. Проверю дома в CS5, возможно, добавили фичу «негуёвых» дроплетов
                                • 0
                                  о блин не знал… видимо что-то там в шопе на гуй завязано очень сильно… или им просто лень это место переделывать.
                          • 0
                            Я вам больше скажу — а Illustrator (как минимум версия CS3) вообще останавливает пакетную обработку, если окно не активно (т.е. переключитесь на другое). В результате когда я обрабатывал 400 с лишним фото приходилось выходить погулять на полчаса. Каждый раз.
                          • 0
                            В небезызвестном FastStone Image Viewer есть такая вещь, как «пакетное преобразование». В нем через гуй можно поменять размеры (в т.ч. по длинной стороне), обрезать, повернуть (в т.ч. автоматически по EXIF), поменять глубину цвета, изменить яркость/контраст/гамму/насыщенность/резкость, занегативить/сепировать, поменять dpi, добавить текст (с использованием переменных) и/или логотип к любому количеству изображений. В том числе все это можно проделать и с RAW.
                            • +2
                              «на The GIMP – там есть свой встроенный язык (наподобие lisp-а) и Python»
                              В GIMP для скриптования используется Scheme. Это не «наподобие lisp» это один из современных развивающихся диалектов Лиспа.
                            • 0
                              Как я догадываюсь, с InDesign это тоже прокатит?
                              Спасибо за статью. Ненавижу рутину, даешь автоматизацию!
                              • 0
                                с InDesign тоже прокатит
                                • 0
                                  с InDesign дико рулит управление через COM. у меня так макет газеты на 120 страниц выгонялся.
                                  • 0
                                    А я как-то так еженедельники заверстал. Универсальная связка — сначала по страницам генерит блоки «месяц», «год», «дни недели», «календарная сетка» и т.п., а потом стилями графики, абзацев и знаков их на странице позиционируем. Жаль на практике так и не применил…
                                • 0
                                  В большинстве случаев для не очень требовательно фотошопщика хватает Actions + пакетная обрабока,
                                  для себя Script использую пока только для определения вертикальное изображение или горизонтальное (пример скрипта тут habrahabr.ru/qa/2799/#answer_11537 )
                                  • 0
                                    Градиенты, насколько я понял, тоже только через ScriptListener.
                                    • 0
                                      О надо же, оказывается есть уже топик про скрипты в ФШ, да ещё и от человека, который смыслит в программировании :) А я хотел свой глупый написать

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.