Pull to refresh

Сказ о том, как добрый молодец борол змея трехглавого, или Как встроить графики в формате SVG в документы Adobe InDesign — часть первая

Reading time 7 min
Views 2.9K
Приветствую всех хабражителей!
Сначала небольшое лирическое отступление. Этот пост был написан не мной, а пока еще незахабренным upd: уже захабренным viklequick, и, на мой взгляд, заслуживает вашего внимания со всеми вытекающими. Итак…

Зачин

image Однажды в студеную зимнюю пору надоело автору добру молодцу бороться с векторными картинками в формате EPS. И решил он идти в ногу с прогрессом и встраивать графики и диаграммы в документы Adobe InDesign в виде SVG. И тут постигло его разочарование великое, ибо фирма Adobe Systems предпочитает свой Adobe Flash, а в Adobe InDesign поддержка SVG отсутствует на корню. Однако ж у добра молодца накопился знатный опыт создания плагинов под InDesign и решил он применить свою силушку богатырскую и забороть гидру трехглавую. Богатырь сказал – богатырь сделал, а именно — заборол.
О деталях этой борьбы и поведем наш сказ.

Сказ

Шаг первый, или Изучаем анатомию гидры трехглавой

imageДля начала, необходимо вкратце упомянуть о том, как InDesign работает с картинками.
C точки зрения InDesign картинки — это специальный вид PageItem, который умеет сам себя отрисовывать и печатать. А этот самый PageItem может быть положен на страницу, или как inline во фрейм, или в ячейку таблицы, и все это надо уметь обрабатывать. И еще, отрисовка бывает трех видов — быстрая (рисуется перечеркнутый прямоугольник), оптимальная (растровый proxy image), и полная (медленная). Необходимо все это также поддерживать.
Таким образом, у нас вырисовывается Import Plug-in, Page Item и несколько вспомогательных классов, вот так:

Class
{
  kSVGItem,
  kDisplayListPageItemBoss,
  {
    IID_ISHAPE, kSVGShapeImpl,
    IID_IINKRESOURCES, kAllProcessInkResourcesImpl,
    IID_IFLATTENERUSAGE, kSVGItemFlattenerUsageImpl,
    IID_IVISITORHELPER, kEPSItemVisitorHelperImpl,
    IID_ISCRIPT, kSVGItemScriptImpl,
  }
},


и так:

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. Забегая вперед, хочу отметить — сюрпризов было в достаточном количестве.

Шаг второй, или Выбираем меч богатырский

imageЭкспресс-гугление, а также просмотр образцов картинок из OpenClipArt навел меня на мысль остановиться на Apache Batik. Сразу скажу, пришлось приложить некоторые усилия для починки самого Batik, но они в итоге оказались незначительны. В основном свелись к «угадай версию SVG по содержимому». Однако появилась задача «как отрисовать батиком картинку по IGraphicsPort», на ней мы и сосредоточимся, как на самой сложной. И будем перемещаться от скучных материй С++ к веселому креативу Java и обратно.
Однако ж менестрели позорные нам скажут – это ты, богатырь, не в ту сказку забрел, и получишь в итоге пять метров колючей проволоки, как всегда и бывает при скрещивании ежа с ужом. Опыт, сын ошибок трудных, нас учит – в данном случае получается нечто более полезное.

Шаг третий, или Приступаем к поединку
Как уже упоминалось выше, надо научить Java работать с потоками Индизайна. Собственно, задача очевидна и решается через JNI:
image
/**
* 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);
~PMInputStream() { close(); }

void close();

int read() {
unsigned char result;
return read(&result, 1) == 1? result: -1;
}

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 setXORMode(Color c1){ throw new RuntimeException("setXORMode: N/A"); }

и такого:

public void fillRect(int x, int y, int w, int h) { fill(new Rectangle(x, y, w, 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();
}
}


Теперь мы можем легко рисовать примитивы:

image

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 прекрасно умеет текст векторизовать в набор глифов, поэтому о работе со шрифтами можно не беспокоиться. Так что текст у нас неизбежно становится набором геометрических фигур.

Продолжение следует…
Tags:
Hubs:
+17
Comments 2
Comments Comments 2

Articles