Как стать автором
Обновить

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

Время на прочтение 11 мин
Количество просмотров 41K
Часто нам бывает надо сделать что-то с пачкой картинок. Есть несколько способов добиться этого:
  • используя 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

Подумать


В качестве упражнения предлагается скрипт, который может действительно пригодиться: сделать так, чтобы лого на фотке добавлялось в той же цветовой гамме, что и фотка – например, синее или жёлтое для сине-жёлтой фотки: это сделает лого не портящим общий цвет и настроение фотки. Лого не должно сливаться с цветом, т.е. не быть синим на синем. Кроме того, будет классно, если лого не будет на поверхности вроде травы, его можно попробовать перенести в другой угол или перекрасить.
Теги:
Хабы:
+26
Комментарии 36
Комментарии Комментарии 36

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн