Пользователь
0,0
рейтинг
31 января 2011 в 22:37

Обрабатываем картинки средствами 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

Подумать


В качестве упражнения предлагается скрипт, который может действительно пригодиться: сделать так, чтобы лого на фотке добавлялось в той же цветовой гамме, что и фотка – например, синее или жёлтое для сине-жёлтой фотки: это сделает лого не портящим общий цвет и настроение фотки. Лого не должно сливаться с цветом, т.е. не быть синим на синем. Кроме того, будет классно, если лого не будет на поверхности вроде травы, его можно попробовать перенести в другой угол или перекрасить.
@Antelle
карма
140,2
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое

Комментарии (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
            о блин не знал… видимо что-то там в шопе на гуй завязано очень сильно… или им просто лень это место переделывать.
            • +2
              на индуском гую они нас всех вертели
    • 0
      Я вам больше скажу — а Illustrator (как минимум версия CS3) вообще останавливает пакетную обработку, если окно не активно (т.е. переключитесь на другое). В результате когда я обрабатывал 400 с лишним фото приходилось выходить погулять на полчаса. Каждый раз.
  • 0
    В небезызвестном FastStone Image Viewer есть такая вещь, как «пакетное преобразование». В нем через гуй можно поменять размеры (в т.ч. по длинной стороне), обрезать, повернуть (в т.ч. автоматически по EXIF), поменять глубину цвета, изменить яркость/контраст/гамму/насыщенность/резкость, занегативить/сепировать, поменять dpi, добавить текст (с использованием переменных) и/или логотип к любому количеству изображений. В том числе все это можно проделать и с RAW.
  • +2
    «на The GIMP – там есть свой встроенный язык (наподобие lisp-а) и Python»
    В GIMP для скриптования используется Scheme. Это не «наподобие lisp» это один из современных развивающихся диалектов Лиспа.
    • 0
      Спасибо, поправил
  • 0
    Как я догадываюсь, с InDesign это тоже прокатит?
    Спасибо за статью. Ненавижу рутину, даешь автоматизацию!
    • 0
      с InDesign тоже прокатит
    • 0
      с InDesign дико рулит управление через COM. у меня так макет газеты на 120 страниц выгонялся.
      • 0
        А я как-то так еженедельники заверстал. Универсальная связка — сначала по страницам генерит блоки «месяц», «год», «дни недели», «календарная сетка» и т.п., а потом стилями графики, абзацев и знаков их на странице позиционируем. Жаль на практике так и не применил…
  • 0
    В большинстве случаев для не очень требовательно фотошопщика хватает Actions + пакетная обрабока,
    для себя Script использую пока только для определения вертикальное изображение или горизонтальное (пример скрипта тут habrahabr.ru/qa/2799/#answer_11537 )
  • 0
    Градиенты, насколько я понял, тоже только через ScriptListener.
  • 0
    О надо же, оказывается есть уже топик про скрипты в ФШ, да ещё и от человека, который смыслит в программировании :) А я хотел свой глупый написать

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