Этот пост — продолжение первой части, где был представлен скрипт Expand Clipping Mask и детально описано, что и как он делает, а также попутно рассмотрены основные принципы создания подобных программ в целом. В этой части я продолжу рассказ о том, как добавить в программу новый функционал, чтобы из "заготовки" получить на выходе "готовое изделие". Здесь не обойтись без более глубокого погружения в предметную область, что является одним из необходимых условий создания полноценного продукта. Итак, начинаем погружение!


В качестве контура маски в Adobe Illustrator могут быть использованы следующие графические примитивы: простой контур (Path), составной контур (Compound Path), составная форма (Compound Shape) и текстовые объекты (Point Text и Text on the Path). На данный момент скрипт работает только с простыми контурами, что видно из приведенного ниже кода, где PathItem — это обращение к элементу Path.


      var clipGroup = sel[0].pageItems.length;
      for (var i = 0; i < clipGroup; i++) {
        if (sel[0].pageItems[i].typename == 'PathItem' &&
            sel[0].pageItems[i].clipping == true) {
          clipPath = sel[0].pageItems[i];
          break;
        };
      };

Перед этим мы объявили переменную clipPath, но не присваили ей значение.


      var clipPath;

Это значит, что ее значение пока не определено, т.е. оно undefined. Если сейчас выделить маску, контуром которой будет, скажем, Compound Path и запустить скрипт, то программа выдаст ошибку на последней строчке функциональной части скрипта,


      clipPath.remove();

так как условие в цикле не будет выполнено, переменная clipPath так и останется undefined, а применить метод remove() к чему-то неопределенному невозможно. Чтобы не допустить такой ситуации, мы сделаем следующее — присвоим clipPath значение null, которое, в отличии от undefined, уже что-то более определенное, что можно хотя-бы проверить.


Давайте подумаем, как определить, что какой-то Compound Path является контуром нашей маски? Когда я говорю "давайте подумаем", это значит, что я предлагаю заглянуть в документацию и найти нужное нам свойство. По аналогии с PathItem ищем свойство clipping. Оказывается, что у объекта CompoundPathItem нет такого свойства, зато есть свойство pathItems, через которое можно добраться до простых контуров PathItem, у которых свойство clipping имеется.

Теперь можно превратить наши размышления/поиски в код. Первым делом проверяем, что clipPath в предыдущей итерации не был определен, а дальше копируем уже написанный блок кода и вносим в него небольшие изменения.


      if (clipPath == null) {
        var clipGroup = sel[0].pageItems.length;
        for (var i = 0; i < clipGroup; i++) {
          if (sel[0].pageItems[i].typename == 'CompoundPathItem' &&
              sel[0].pageItems[i].pathItems[0].clipping == true) {
            clipPath = sel[0].pageItems[i];
            break;
          };
        };
      };

Собственно, изменения коснутся только одной строки. Как мы здесь видим, 'PathItem' поменялся на 'CompoundPathItem', а также добавилась новая конструкция 'pathItems[0]', с помощью который мы обращаемся к элементу составного контура.


if (sel[0].pageItems[i].typename == 'CompoundPathItem' && sel[0].pageItems[i].pathItems[0].clipping == true) {

Ниже приведен функциональный блок кода, созданный на данный момент.


      var clipGroup = sel[0].pageItems.length;
        for (var i = 0; i < clipGroup; i++) {
          if (sel[0].pageItems[i].typename == 'PathItem' &&
              sel[0].pageItems[i].clipping == true) {
            clipPath = sel[0].pageItems[i];
            break;
          };
        };
      if (clipPath == null) {
        for (var i = 0; i < clipGroup; i++) {
          if (sel[0].pageItems[i].typename == 'CompoundPathItem' &&
              sel[0].pageItems[i].pathItems[0].clipping == true) {
            clipPath = sel[0].pageItems[i];
            break;
          };
        };
      };

Следующий на очереди "пациент" — это Compound Shape (составная форма). Вот здесь становиться совсем интересно! В документации мы вообще не находим такого объекта. Что делать? Для начала давайте определим к какому классу объектов он относится. Чтобы выяснить это, напишем небольшой вспомогательный код, который потом выкинем. Как было сказано в первой части, мы не касаемся вопроса используемых инструментов для написания/отладки кода. Поэтому предположим, что это будет отдельный файл, которой потом просто отправится в корзину. Код будет следующим:


var obj = app.activeDocument.selection[0];
alert(obj.typename);

В первой строке мы создаем ссылку на выделенный объект, во второй — выводим сообщение о том, каким типом он является. Выделяем в Adobe Illustrator контур маски, т.е. тот самый объект Compound Shape и запускаем скрипт. В окне сообщения мы видим, что Compound Shape представляет из себя PluginItem. Избавляемся от вспомогательного кода, снова возвращаемся к документации, но не находим у PluginItem ни свойства clipping, ни pathItems. Вообще ничего, что помогло бы нам однозначно указать, что этот объект является контуром маски. Из скрипта даже нельзя определить, что это за plugin. Какой-то внешний модуль и всё тут!


Вот ведь засада! — восклицаете в сердцах вы. А мозг лихорадочно работает, в надежде решить нерешаемую задачу. И тогда, перебрав все возможные и невозможные варианты, вы в отчаянии нажимаете Del и удаляете ненавистный Compound Shape. И тут краем глаза на палитре Layers вы замечаете, что после этого действия, контейнер маски, который был Clip Group, стал просто Group. Что бы это могло значит? А то, что свойство clipped объекта-маски из true стало false. Вот оно, решение, которое может сработать! Конечно это, по-большому счету, hack, но какая разница, если он поможет определить искомый контур.

Алгоритм определения контура маски, представленный объектом Compound Shape, тогда будет следующим: перебираем в цикле все объекты маски и, когда обнаруживаем PluginItem, то удаляем его и проверяем изменилось ли свойство clipped у контейнера маски. Если оно стало false, значит это и есть наш контур. Единственное, чтобы этот hack сработал, надо после удаления объекта обновить DOM Illustrator, что можно сделать методом app.redraw(). Потом еще надо не забыть вернуть удаленный объект, что делается методом app.undo().


Ниже приведен код для контура Compound Shape:


      if (clipPath == null) {
        for (var i = 0; i < clipGroup; i++) {
          if (sel[0].pageItems[i].typename == 'PluginItem') {
            sel[0].pageItems[i].remove();
            app.redraw();
            if (sel[0].clipped == false) {
              app.undo();
              clipPath = sel[0].pageItems[i];
              break;
            }
            else {
              app.undo();
            }
          };
        };
      };

Теперь из всех возможных вариантов типа объектов, которые могут быть контуром маски, остался только текст (или TextFrameItem, в терминологии illustrator scripting references). Снова обращаемся к документации и опять не находим там свойства clipping. Но на этот раз мы уже не переживаем так сильно по этому поводу и спокойно выясняем, что у TextFrameItem есть свойство kind, которое определяет тип текстового объекта (TextType). Выясняем, что таких типов может быть три: AREATEXT, POINTTEXT и PATHTEXT. Первый тип нам не интересен, так как его нельзя использовать в качестве контура маски, а другие два еще как интересны. Остается только найти hack, который поможет нам определить теперь уже не контур, а текстовый объект, являющийся контуром маски. И этим hack-ом станет команда, Convert To Area Type, которая конвертирует POINTTEXT в AREATEXT. Как и в случае с Compound Shape, при этом происходит неявное изменение свойства clipped.


Соответственно, код для TextFrameItem типа POINTTEXT будет следующим:


      if (clipPath == null) {
        for (var i = 0; i < clipGroup; i++) {
          if (sel[0].pageItems[i].typename == 'TextFrame' &&
              sel[0].pageItems[i].kind == 'TextType.POINTTEXT') {
            sel[0].pageItems[i].convertPointObjectToAreaObject();
            app.redraw();
            if (sel[0].clipped == false) {
              app.undo();
              clipPath = sel[0].pageItems[i];
              break;
            }
            else {
              app.undo();
            }
          };
        };
      };

Остался только TextFrameItem типа PATHTEXT. К сожалению, при конвертировании PATHTEXT в AREATEXT, свойство clipped не изменяется. Но так как это последний из возможных претендентов на звание "контур маски", то можно использовать именно такое его поведение. То есть проверяем, что после выполнения команды Convert To Area Type свойство clipped осталось true. Ниже приведен код для TextFrameItem типа PATHTEXT.


      if (clipPath == null) {
        for (var i = 0; i < clipGroup; i++) {
          if (sel[0].pageItems[i].typename == 'TextFrame' &&
              sel[0].pageItems[i].kind == 'TextType.PATHTEXT') {
            sel[0].pageItems[i].convertPointObjectToAreaObject();
            app.redraw();
            if (sel[0].clipped == true) {
             clipPath = sel[0].pageItems[i];
             break;
            }
            else {
              app.undo();
            }
          };
        };
      };

Таким образом, если собрать вместе последовательно написанные части кода, включая блок проверок, то мы получим такой код, выполнение которого, как было заявлено еще в первой части поста, будет реализовывать действие новой команды Expand Clipping Mask в Adobe Illustrator.


#target illustrator
if (app.documents.length > 0) {
  var doc = app.activeDocument;
  var sel = doc.selection;
  var clipPath = null;
  if (sel.length > 0) {
    if (sel[0].typename == 'GroupItem' && sel[0].clipped == true) {
      var clipGroup = sel[0].pageItems.length;
        for (var i = 0; i < clipGroup; i++) {
          if (sel[0].pageItems[i].typename == 'PathItem' &&
              sel[0].pageItems[i].clipping == true) {
            clipPath = sel[0].pageItems[i];
            break;
          };
        };
      if (clipPath == null) {
        for (var i = 0; i < clipGroup; i++) {
          if (sel[0].pageItems[i].typename == 'CompoundPathItem' &&
              sel[0].pageItems[i].pathItems[0].clipping == true) {
            clipPath = sel[0].pageItems[i];
            break;
          };
        };
      };
      if (clipPath == null) {
        for (var i = 0; i < clipGroup; i++) {
          if (sel[0].pageItems[i].typename == 'PluginItem') {
            sel[0].pageItems[i].remove();
            app.redraw();
            if (sel[0].clipped == false) {
              app.undo();
              clipPath = sel[0].pageItems[i];
              break;
            }
            else {
              app.undo();
            }
          };
        };
      };
      if (clipPath == null) {
        for (var i = 0; i < clipGroup; i++) {
          if (sel[0].pageItems[i].typename == 'TextFrame' &&
              sel[0].pageItems[i].kind == 'TextType.POINTTEXT') {
            sel[0].pageItems[i].convertPointObjectToAreaObject();
            app.redraw();
            if (sel[0].clipped == false) {
              app.undo();
              clipPath = sel[0].pageItems[i];
              break;
            }
            else {
              app.undo();
            }
          };
        };
      };
      if (clipPath == null) {
        for (var i = 0; i < clipGroup; i++) {
          if (sel[0].pageItems[i].typename == 'TextFrame' &&
              sel[0].pageItems[i].kind == 'TextType.PATHTEXT') {
            sel[0].pageItems[i].convertPointObjectToAreaObject();
            app.redraw();
            if (sel[0].clipped == true) {
              clipPath = sel[0].pageItems[i];
              break;
            }
            else {
              app.undo();
            }
          };
        };
      };
      app.executeMenuCommand('releaseMask');
      clipPath.remove();
    }
    else {
      alert ('Выделение не является объектом-маской!');
    };
  }
  else {
    alert ('Нет выделенных объектов!');
  };
}
else {
  alert ('Нет открытых документов!');
};

Здесь можно поставить точку. Нет, лучше точку с запятой.


Надеюсь, этими постами я помог вам стать немного ближе к своей цели — начать программировать в Adobe Illustrator. Спасибо за внимание!

Комментарии (2)


  1. olegkrasnov
    23.05.2019 13:02

    Сейчас мы имеем пять циклов, по циклу на каждую проверку. Не лучше ли запихнуть все проверки в один цикл?

    В следующих статьях было бы неплохо упомянуть про ExtendScript Debugger, необходимый в отладке инструмент, пришедший на смену тормозному ESTK.

    Спасибо за полезный материал.


    1. romaya Автор
      23.05.2019 18:31

      Сейчас мы имеем пять циклов, по циклу на каждую проверку. Не лучше ли запихнуть все проверки в один цикл?

      Можно, но тогда код будет менее читабельный, имхо.

      В следующих статьях было бы неплохо упомянуть про ExtendScript Debugger, необходимый в отладке инструмент, пришедший на смену тормозному ESTK.

      Неплохо было бы, но, к сожалению, я не работаю в Visual Studio Code. Мои инструменты — Atom и ESTK.