Pull to refresh

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

Reading time6 min
Views1.4K
Продолжение вот этого поста, автором которого является viklequick.

Шаг пятый, или Применяем стиль «пьяный мастер за работой»
Теперь, раз у нас уже есть примитивы, давайте применим цвета, альфа-канал и стили у линий.

private void applyStroke(Shape shape) {
  Stroke stroke = getStroke();
  Transform t = getTransform();
  if(stroke!=null) {
    if(stroke instanceof BasicStroke) {
      BasicStroke currStroke = (BasicStroke)stroke;
      float[] ff = currStroke.getDashArray();
      if(ff!=null) setdash(ff.length, ff, currStroke.getDashPhase());
      float width = (float)(currStroke.getLineWidth() * Math.max(t.getScaleX(), t.getScaleY()));
      if(width < 0.7f) width = 0.7f;
      setlinewidth(width);
    }
    else   {
      setlinewidth(1.0f);
      Shape strokedShape = stroke.createStrokedShape(shape);
      fill(strokedShape);
    }
  }
  else 
    setlinewidth(1.0f);
}

imageКод в общем ничего сложного не содержит, всего лишь разбор BasicStroke. Если же это нечто другое — то это представимо в виде Shape и может быть так и отрисовано.
Далее мы получаем уровень альфа-канала для последующих shapes, и передаем его в InDesign — код тоже достаточно прост.

private void applyAlpha() {
  float alpha = 1.0f;
  Composite comp = getComposite();
  if(comp!=null && comp instanceof AlphaComposite) {
    AlphaComposite alcomp = (AlphaComposite)comp;
    alpha = alcomp.getAlpha();
  }
  setopacity(alpha, true);
} 

А вот с custom paints and composites ситуация значительно грустнее. Приходится их растеризовать самому.

private boolean applyStyles(Shape shape) {
  Paint paint = getPaint();
  if(paint instanceof Color) {
    Color cc = (Color)paint;
    setrgbcolor(cc.getRed() / 255.0f, cc.getGreen() / 255.0f, cc.getBlue() / 255.0f);
    if(cc.getAlpha() != 0 && cc.getAlpha() != 255) setopacity(cc.getAlpha() / 255.0f, true);
    else applyAlpha();
  }
  else if(paint!=null && shape!=null) {
    applyAlpha();
    drawGradientAsBackground(shape, paint);
    return false;
  }

  Composite comp = getComposite();
  if(comp!=null && !(comp instanceof AlphaComposite) && shape!=null) {
    drawCustomComposite(shape, comp);
    return false;
  }
  return true;
} 

Ситуация с цветом достаточно очевидна (разве что у цвета может быть явно задан альфа-канал), а вот все остальное требует пояснений. Обратите внимание, функция возвращает true/false. True означает что никаких трюков применено не было, и текущий shape надо отрисовать через fill(). А вот false означает что shape уже растеризован в виде картинки и его рисовать поверх картинки не надо.
Сам градиент пришлось отрисовать вот таким образом:

private void drawGradientAsBackground(Shape shape, Paint paint) {
  GraphicsConfiguration conf = getDeviceConfiguration();
  PaintContext pctx = paint.createContext(
    conf.getColorModel(), conf.getBounds(), shape.getBounds2D(),
    getTransform(), getRenderingHints());

  Rectangle r = getTransform().createTransformedShape(shape).getBounds();
  Raster raster = pctx.getRaster(r.x, r.y, r.width, r.height);
  ColorModel cmodel = pctx.getColorModel();
  BufferedImage bfi = new BufferedImage(cmodel, raster.createCompatibleWritableRaster(),
    cmodel.isAlphaPremultiplied(), null);
  bfi.setData(raster);
  
  int[] argbBuf = getBuf(bfi, r.width, r.height);
  if(argbBuf != null) {
    applyClip(shape);
    drawImage(argbBuf, r.x, r.y, r.width, r.height, new AffineTransform());
    restoreClip();
  }
  pctx.dispose();
} 

В этой функции градиент рисуется на offscreen image, и отсечением картинки является сам shape. Аналогичным образом мы отрисовываем и нестандартные Composite:

private void drawCustomComposite(Shape shape, Composite comp) {
  Rectangle r = getTransform().createTransformedShape(shape).getBounds();
  BufferedImage bfi = new BufferedImage(r.width, r.height, BufferedImage.TYPE_INT_ARGB);
  Graphics2D g2 = (Graphics2D)bfi.getGraphics();
  g2.setTransform(getTransform());
  g2.setComposite(comp);
  g2.fill(shape);
  g2.dispose();
  
  int[] argbBuf = getBuf(bfi, r.width, r.height);
  if(argbBuf != null) {
    applyClip(shape);
    drawImage(argbBuf, r.x, r.y, r.width, r.height, new AffineTransform());
    restoreClip();
  }
} 

Так как мы отрисовываем уже трансформированный shape, то в drawImage мы задаем пустой AffineTransform.

Шаг шестой, или Добиваем супостата
imageНаконец-то мы дошли до самого интересного. Если для остальных случаев native реализация – это по сути однострочный код, то в случае с картинками это не так. Для начала приведу код для отрисовки картинки:

public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer)  {
  int[] argbBuf = getBuf(img, width, height);
  if(argbBuf == null) return false;
    
  applyClip(getClip());
  applyAlpha();
  drawImage(argbBuf, x, y, width, height, getTransform());
  restoreClip();
  return true;
} 

Код очень прост — задает отсечение, альфа-канал, получает битовую матрицу — и передает это все в InDesign. Получение битов очевидно:

private int[] getBuf(Image img, int width, int height) {
  int[] argbBuf = new int[width*height];
  PixelGrabber pg = new PixelGrabber(img, 0, 0, width, height, argbBuf, 0, width);
  try {
    if(!pg.grabPixels()) return null;
  }
  catch (InterruptedException e) { return null; }
  if((pg.getStatus() & ImageObserver.ABORT) != 0) return null;
  return argbBuf;
} 

А вот передача в InDesign обладает некоторыми особенностями. Рассмотрим их.

private void drawImage(int[] argbBuf, int x, int y, int width, int height, AffineTransform transform) {
  double[] transMatr = new double[6];
  transform.getMatrix(transMatr);
  this.image(argbBuf, x, y, width, height, transMatr);
} 

Легко заметить, это единственная функция, которая матрицу трансформаций передает в InDesign, а не отрабатывает самостоятельно. Можно было бы пойти другим путем, и трансформационную матрицу всегда передавать Индизайну, а не работать с transformed shapes. Но, к моему вящему сожалению, неумение рисовать градиенты средствами Индизайна привело вот к такому решению.
А теперь вернемся в С++ и рассмотрим нюансы со стороны плагина. Кода тут много, поэтому приведу только наиболее интересные выдержки, убрав тривиальности и обработку ошибок.
Начало очевидно:

JNIEXPORT void JNICALL Java_..._image
  (JNIEnv *env, jobject obj, 
  jintArray imageARGBBuff, jint x, jint y, jint width, jint height,
  jdoubleArray transformMatrix) {

  K2::scoped_array<uint8> maskBuffer(new uint8[imageARGBBuff.length]);
  K2::scoped_array<uint8> pictBuffer(new uint8[3*imageARGBBuff.length]);

// заполняем буферы по нашему ARGB

А вот дальше начинаются пляски с бубном.
Дело вот в чем — если у нас blending color space = RGB, то Индизайн хочет картинку в CMYK. А иначе — падает через раз, нанося богатырю страшную душевную рану, да так, что оный богатырь даже кушать не может. И наоборот — если blending color space = CMYK, то картинка должна быть в RGB. Найдено это было опытным путем, и почему все происходит именно так — затрудняюсь объяснить. Поэтому нам надо узнать, а какой же color space у нас сейчас выставлен:

Utils<IXPUtils> xpUtils;
InterfacePtr<IXPManager> xpManager(xpUtils->QueryXPManager(db));
InterfacePtr<IDocument> doc(db, db->GetRootUID(), UseDefaultIID());
AGMColorSpace* blendingSpace = xpManager->GetDocumentBlendingSpace();

и сконвертировать при необходимости:

uint8* outBuff = nil;
int16 compCnt = 3, space = kRGBColorSpace;

if(IsBeingFlattened(env, obj)) {
  TransformRGB2CMYKifNeeded(doc, blendingSpace, pictBuffer.get(), &outBuff, imageARGBBuff.length);
  compCnt = 4; space = kCMYKColorSpace;
  pictBuffer.reset(outBuff);
} 

Вот теперь мы готовы задать и картинку, и альфа-маску к ней:

AGMImageRecord imgR;
memset(&imgR, 0, sizeof(AGMImageRecord));

imgR.bounds.xMin = x;
imgR.bounds.yMin = y;
imgR.bounds.xMax= x + width;
imgR.bounds.yMax= y + height;
imgR.byteWidth  = compCnt * width;
imgR.colorSpace  = space;
imgR.bitsPerPixel = compCnt * 8;
imgR.baseAddr = (void*)pictBuffer.get();

AGMImageRecord maskR;
memset(&maskR, 0, sizeof(AGMImageRecord));
maskR.bounds.xMin = x;
maskR.bounds.yMin = y;
maskR.bounds.xMax = x + width;
maskR.bounds.yMax = y + height;
maskR.byteWidth   = width;
maskR.colorSpace  = kGrayColorSpace;
maskR.bitsPerPixel  = 8;
maskR.baseAddr   = (void*)maskBuffer.get();

Осталось только перегнать матрицу трансформации из формата Java в формат InDesign:

PMMatrix transform(nativeTransformData[0], nativeTransformData[1],
nativeTransformData[2],
    	nativeTransformData[3], 
nativeTransformData[4], 
nativeTransformData[5]); 

Последний штрих — надо донести до Индизайна знание о маске альфа-канала:

AGMPaint* alphaServer = xpUt->CreateImagePaintServer(&maskR, &transform, 0, nil);
PMRect bounds(x, y, width + x, height + y);


port->SetAlphaServer(alphaServer, kTrue, stub);


port->starttransparencygroup(bounds, blendingSpace, kFalse, kFalse); 

И можно наконец нарисовать картинку. We did it!

port->image(&imgR, transform, drawFlags); 

Код очистки альфа-серверов и прочего не привожу, так как он тривиален.

Часть последняя, или Выносим сокровища поверженного монстра
imageВот таким образом удалось-таки добру молодцу побороть змея трехглавого, а автору — скрестить ежа с ужом нарисовать один из наиболее слабодокументированных видов плагинов в Adobe InDesign SDK. К сожалению, остались места для улучшения, например те же градиенты, но итоговый результат оказался вполне удовлетворительным.
Вот и сказочке конец, а кто слушал — молодец!
Tags:
Hubs:
+2
Comments2

Articles