Сказ о том, как добрый молодец борол змея трехглавого, или Как встроить графики в формате SVG в документы Adobe InDesign — часть первая
Приветствую всех хабражителей!
Сначала небольшое лирическое отступление. Этот пост был написан не мной, а пока еще незахабренным upd: уже захабренным viklequick, и, на мой взгляд, заслуживает вашего внимания со всеми вытекающими. Итак…
Однажды в студеную зимнюю пору надоело автору добру молодцу бороться с векторными картинками в формате EPS. И решил он идти в ногу с прогрессом и встраивать графики и диаграммы в документы Adobe InDesign в виде SVG. И тут постигло его разочарование великое, ибо фирма Adobe Systems предпочитает свой Adobe Flash, а в Adobe InDesign поддержка SVG отсутствует на корню. Однако ж у добра молодца накопился знатный опыт создания плагинов под InDesign и решил он применить свою силушку богатырскую и забороть гидру трехглавую. Богатырь сказал – богатырь сделал, а именно — заборол.
О деталях этой борьбы и поведем наш сказ.
Шаг первый, или Изучаем анатомию гидры трехглавой
Для начала, необходимо вкратце упомянуть о том, как InDesign работает с картинками.
C точки зрения InDesign картинки — это специальный вид PageItem, который умеет сам себя отрисовывать и печатать. А этот самый PageItem может быть положен на страницу, или как inline во фрейм, или в ячейку таблицы, и все это надо уметь обрабатывать. И еще, отрисовка бывает трех видов — быстрая (рисуется перечеркнутый прямоугольник), оптимальная (растровый proxy image), и полная (медленная). Необходимо все это также поддерживать.
Таким образом, у нас вырисовывается Import Plug-in, Page Item и несколько вспомогательных классов, вот так:
Class
<
kSVGPlaceProviderBoss,
kInvalidClass,
<
IID_IK2SERVICEPROVIDER, kImportServiceImpl,
IID_IIMPORTPROVIDER, kSVGPlaceProviderImpl,
IID_IIMPORTPREVIEW, kSVGImportPreviewImpl,
>
>,
И, разумеется, есть еще стандартная шапка на предмет startup / shutdown, которую нет смысла приводить.
Давайте разберемся, что это и зачем.
• IID_ISHAPE — это реализация собственно page item, ведающая отрисовкой.
• IID_IINKRESOURCES — обрабатываем ссылки на внешние файлы (Ink).
• IID_IFLATTENERUSAGE — обрабатываем растеризацию альфа-канала в PDF. Собственно, это одна строчка, явно требующая включения flattener, и не более того.
• IID_IVISITORHELPER — оставляем стандартным, позаимствовав у EPS.
• IID_ISCRIPT — обеспечиваем поддержку нашего элемента через скриптинг. Это тоже очень простая часть, всего лишь корректно задающая тип объекта.
• IID_IK2SERVICEPROVIDER и IID_IIMPORTPROVIDER — добавляем поддержку для команды Place.
• IID_IIMPORTPREVIEW — и обеспечиваем предпросмотр в диалоге выбора файлов (Windows specific).
Наиболее интересным является собственно IID_ISHAPE, это сердце нашего плагина. От него требуется умение рисовать самого себя непосредственно по IGraphicsPort, а также получение растрового proxy image. Забегая вперед, хочу отметить — сюрпризов было в достаточном количестве.
Шаг второй, или Выбираем меч богатырский
Экспресс-гугление, а также просмотр образцов картинок из OpenClipArt навел меня на мысль остановиться на Apache Batik. Сразу скажу, пришлось приложить некоторые усилия для починки самого Batik, но они в итоге оказались незначительны. В основном свелись к «угадай версию SVG по содержимому». Однако появилась задача «как отрисовать батиком картинку по IGraphicsPort», на ней мы и сосредоточимся, как на самой сложной. И будем перемещаться от скучных материй С++ к веселому креативу Java и обратно.
Однако ж менестрели позорные нам скажут – это ты, богатырь, не в ту сказку забрел, и получишь в итоге пять метров колючей проволоки, как всегда и бывает при скрещивании ежа с ужом. Опыт, сын ошибок трудных, нас учит – в данном случае получается нечто более полезное.
Шаг третий, или Приступаем к поединку
Как уже упоминалось выше, надо научить Java работать с потоками Индизайна. Собственно, задача очевидна и решается через JNI:
/**
* Class for reading from InDesign IPMStream
*/
public class PMInputStream extends InputStream
<
/**
* Creates PMInputStream object and attaches it to IPMStream* already
* opened by InDesign.
* @param iPMStreamPtr IPMStream* to attach to
* @throws IOException The stream has no ability to read.
*/
public PMInputStream(long iPMStreamPtr) throws IOException <
this.ownedStreamPtr = iPMStreamPtr;
this.retain();
>
@Override
public int read() throws IOException <
return readByte();
>
@Override
public int read(byte b[], int off, int len) throws IOException <
if (len == 0) return 0;
return readBytes(b, off, len);
>
@Override
public long skip(long n) throws IOException <
return performSeek(n, SeekFromCurrent);
>
public long seek(long numberOfBytes, int fromHere) throws IOException <
return performSeek(numberOfBytes, fromHere);
>
@Override
public int available() <
return availableBytes();
>
// native functions below
.
Реализация строится через native функции, приведем их список. Они самоочевидны и особых комментариев не требуют. Есть только один интересный момент, вот он:
/**
* ‘Cause IPMStream is a COM-like object we gotta call AddRef() for it
* if we wanna save pointer in our class.
*/
private native void retain();
/**
* Once we called AddRef() we shouldn’t forget calling corresponding
* Release(). Let’s do it!
*/
private native void release();
protected native int readByte();
protected native int readBytes(byte b[], int off, int len);
protected native long performSeek(long numberOfBytes, int fromHere);
protected native int availableBytes();
protected native String getFileName();
protected native long getLastModifiedTime();
Две последних функции были добавлены для кэширования элементов непосредственно в Java, чтобы не строить заново DOM-модели на каждую отрисовку. Надо всего лишь отслеживать изменения данных и оперативно перечитывать их при изменении.
И последний штрих — собственно реализация native методов.
class IPMStream; class PMInputStream
<
private:
IPMStream* fStream;
public:
PMInputStream(IPMStream* stream);
int read(unsigned char* buffer, int len)< return fStream->XferByte(buffer, len); >
XInt64 seek(XInt64 numberOfBytes, SeekFrom fromHere) <
return fStream->Seek((int32)numberOfBytes, fromHere);
>
public:
enum SeekFrom < SeekFromStart = kSeekFromStart,
SeekFromCurrent = kSeekFromCurrent,
SeekFromEnd = kSeekFromEnd
>;
>;
Мы уже добрались до SVG, но теперь появляется задача получить и нарисовать картинку. Собственно, нарисовав векторную картинку в BufferedImage, мы и получаем наш proxy image. Стандартный пример из батика нам наглядно показывает, каким образом он получен, поэтому для экономии места позволю себе эту портянку тут не приводить. Очевидно также использование PixelGrabber для получения массива байтов в RGB из BufferedImage. Полученный массив мы передаем в AGMImageRecord точно так же, как рассмотрено во второй части статьи.
Интересно, что отрисовка на IGraphicsPort вместо BufferedImage отличается ровно одной строчкой, вместо стандартного offscreen Graphics2D нам надо подставить свою реализацию. Этим мы и займемся.
Шаг четвертый, или Коварно подключаем к мечу электроток, чтобы искры летели
Объем кода для грамотного наследования Graphics2D велик, зануден, но очевиден, и его легко можно подсмотреть у того же Apache Batik. Поэтому мы сосредоточимся на наиболее интересных частях, а вот целую пачку геттеров и сеттеров а-ля Paint getPaint() < return paint; >опустим.
Отмечу лишь, что в общем можно смело вставлять заглушки такого вида:
public void fillRect(int x, int y, int w, int h)
В результате таких вот упрощений и изничтожения дубликатов, у нас реальных функций получается вот сколько:
public void draw(Shape s);
public void fill(Shape s);
public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer);
public void drawString(String str, float x, float y);
public void drawString(AttributedCharacterIterator iterator, float x, float y);
Можно пойти дальше, использовав StrokeTextPainter, и этот набор сократится до трех функций, что радует.
Теперь подойдем с другой стороны, и посмотрим что нам надо от IGraphicsPort:
private native void newpath();
private native void moveto(float x, float y);
private native void lineto(float x, float y);
private native void curveto(float x1, float y1, float x2, float y2, float x3, float y3);
private native void curvetov(float x2, float y2, float x3, float y3);
private native void closepath();
private native void gsave();
private native void grestore();
private native void setlinewidth(float width);
private native void setdash(int len, float[] dashArray, float offset);
private native void setopacity(float opacity, boolean bIsAlphaShape); // from 0 to 1.0
private native void setrgbcolor(float r, float g, float b);
private native void fill();
private native void eofill();
private native void stroke();
private native void image(int[] buffer, int x, int y, int width, int height, double[] transformMatrix);
private native void clip();
private native void eoclip();
Опытным взглядом можно заметить явное отсутствие таких средств отрисовки, как градиенты. Увы, документация на эту часть API у Adobe настолько скудна, что мне так и не удалось воспользоваться наличествующими функциями, пришлось шаманить, как будет рассмотрено ниже.
Как видно, надо нам немного. Однако возникает новая задача: нам надо как-то все это дело теперь совместить. Начнем с простого — с геометрических фигур.
Самое сложное здесь — это развернуть shape из Java в набор native функций. Для этого воспользуемся стандартным PathIterator, ну и при этом не забываем про трансформации, ибо Batik использует AffineTransform весьма активно:
private void applyPath(PathIterator pi, int kind) <
float[] coord = new float[6];
int retSeg;
newpath();
while(!pi.isDone()) <
retSeg = pi.currentSegment(coord);
switch (retSeg) <
case SEG_LINETO:
lineto(coord[0], coord[1]);
break;
case SEG_CUBICTO:
curveto(coord[0], coord[1], coord[2], coord[3], coord[4], coord[5]);
break;
case SEG_MOVETO:
moveto(coord[0], coord[1]);
break;
case SEG_QUADTO:
curvetov(coord[0], coord[1], coord[2], coord[3]);
break;
case SEG_CLOSE:
closepath();
break;
>
pi.next();
>
if(kind == PATH_FILL) <
if(pi.getWindingRule() == PathIterator.WIND_EVEN_ODD) eofill();
else fill();
>
else if(kind == PATH_STROKE) stroke();
else if(kind == PATH_CLIP) <
if(pi.getWindingRule() == PathIterator.WIND_EVEN_ODD) eoclip();
else clip();
>
>
Теперь мы можем легко рисовать примитивы:

public void draw(Shape s) <
PathIterator pi = s.getPathIterator(getTransform());
applyClip(getClip());
applyStroke(s);
applyStyles();
applyPath(pi, PATH_STROKE);
restoreClip();
>
Пока что не будем акцентироваться на всяческих apply*, они рассмотрены далее. Собственно говоря, fill() выглядит точно так же, если не считать некоторых трудностей с градиентами. Как нетрудно догадаться, applyClip() также реализуется аналогично:
private void applyClip(Shape xclip) <
gsave();
if(xclip != null) <
PathIterator pi = xclip.getPathIterator(getTransform());
applyPath(pi, PATH_CLIP);
>
>
Реальный код чуть сложнее, приходится учитывать особенности реализации flattener в IGraphicsPort, и по мере необходимости обход переворачивать, чтобы обход пути шел против часовой стрелки. И, как и упоминалось ранее, Batik прекрасно умеет текст векторизовать в набор глифов, поэтому о работе со шрифтами можно не беспокоиться. Так что текст у нас неизбежно становится набором геометрических фигур.
Автоматическая вставка картинок
Детально описываю процесс работы: нам одновременно нужно сверстать несколько каталогов в неделю. Для этого в офисе работает несколько дизайнеров (кроме других специалистов). Копирайтеры дают нам Excel файл (с названием товаррв и кодами) такого типа:
91056 Парфюмерия Женская парфюмерия Дезодорант женский "Love More " 75мл ТМ "Shirley May", арт.MM38530
Кроме этого нам дают файл где показана структура каталога (структура не повторяется ни в одном каталоге)
Я, дизайнер, должен сначала создать документ в индизайне заданного размера (размеры тоже всегда разные) и раскидать весь товар по всему каталогу на места которые указанные в структуре. Для этого я смотрю код товара, захожу в папку где "миллион фотографий", нахожу нужный товар и вручную затягиваю в в каталог (в индизайн). А хочу я не тратить время на поиски товаров.
Из иллюстратора в InD
объясню еще раз — есть странички в иллюстраторе — текст и прилинкованые картинки. Хочется — на страничках в индизайне получить точно такие же странички, то есть если картинки прилинкованы, то пусть будут прилинкованы (хотя это не главное — смогу вставить картинки в инд сам, но не хочется этого делать), если текст — то текст(хотя вроде в мануале прочитал, что так не получиться, все-равно будет в кривых). главное — что бы выглядело также как в иллюстраторе, и чтобы можно было потом двигать объекты с полосы на полосу. Если делать File>Place документ *.ai, то я могу двигать только страницы целиком, а мне надо элементы этих страниц. воот.
Задача — сделать все это с минимальным затратом сил. 144 полосы как никак ж)). где же эта хваленая совместимость иллюстратор — индизайн. я так на нее расчитываю ))
19. Создание изображения. Создание полупрозрачных изображений в InDesign.
Иногда использовать файлы PDF нe представляется возможным. Вам может понадобиться импортировать изображение из макета, чтобы разместить его на Web-странице или отправить по электронной почте. Когда вы в следующий раз решите создать изображение JPEG на основе изображения из макета, воспользуйтесь данным советом. Выделите объекты, которые необходимо поместить в файл JPEG. Затем выберите команду Export (Экспортировать) из меню File (Файл), укажите формат JPEG и щелкните на кнопке Save (Сохранить). В появившемся диалоговом окне Export JPEG (Экспорт в JPEG) вместо переключателя Page (Страница) выберите переключатель Selection (Выделение). Задав необходимое качество, щелкните на кнопке Export (Экспорт).
Иногда требуется разместить над фоном полупрозрачное изображение. Когда в следующий раз перед вами возникнет подобная задача, выделите изображение с помощью инструмента Selection (Выделение), после чего перетащите влево ползунок Opacity <Непрозрачность) в палитре Transparency (Прозрачность).
20. Удаление ненужных образцов цвета. Как присвоить названия образцам (и зачем это делать) в InDesign?
При необходимости отправить документ на печать на принтер или в типографию, вам следует убедиться в том, что документ максимально прост, что позволит избежать многих потенциальных проблем. Удалите все неиспользуемые образцы из палитры Swatches (Образцы), чтобы принтер “знал”, какие именно цвета используются в документе. В InDesign отсутствует специальная команда, поэтому вам придется удалять лишние цвета в два этапа. Выберите команду Select All Unused (Выделить все неиспользуемые) из меню палитры Swatches (Образцы). В результате будут выделены все неиспользуемые в документе образцы цвета. После этого щелкните на значке в виде мусорной корзины в нижней части палитры Swatches (Образцы), чтобы сразу удалить все выделенные образцы.

При создании нового образца цвета, по умолчанию задается название, в котором указываются значения голубого, пурпурного, желтого и черного, используемые при его получении (например, С=39, М=0, Y=87, К=0). Если только вы не являетесь опытным специалистом по допечатной подготовке, подобные названия вам вряд ли покажутся описательными. Для того чтобы задать для образца более подходящее название (например, “оливково-зеленый”), отобразите палитру Swatches (Образцы) и дважды щелкните на образце. В результате на экране появится диалоговое окно Swatch Options (Параметры образца). Текстовое поле Swatch Name (Имя образца) будет недоступно до тех пор, пока вы не сбросите флажок Name with Color Value (Название по значению цвета). После этого в текстовом поле Swatch Name (Имя образца) можно ввести более подходящее название. Это не только позволит вам быстрее выбирать необходимые оттенки, но и облегчит жизнь сотруднику типографии, так как он будет точно знать, какие цвета необходимо использовать (заказные цвета не создаются на основе основных цветов, как технологические).