Сказ о том, как добрый молодец борол змея трехглавого, или Как встроить графики в формате SVG в документы Adobe InDesign — часть вторая
Продолжение вот этого поста, автором которого является 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); }
Далее мы получаем уровень альфа-канала для последующих 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.
Шаг шестой, или Добиваем супостата
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);
Код очистки альфа-серверов и прочего не привожу, так как он тривиален.
Часть последняя, или Выносим сокровища поверженного монстра
Вот и сказочке конец, а кто слушал — молодец!