Greetings to all habrazhiteli!
First, a small lyrical digression. This post was not written by me, but for
now not filled with upd: already
enveloped viklequick , and, in my opinion, deserves your attention with all the consequences. So…
Zachin
Once, in an icy winter season, the
author did well
to the good fellow to fight with vector images in EPS format. And he decided to keep up with the progress and embed graphs and diagrams in the Adobe InDesign documents in the form of SVG. And then a great disappointment befell him, because Adobe Systems prefers its own Adobe Flash, and in Adobe InDesign, SVG support is not at its root. However, the well-done young man has accumulated a notable experience in creating plug-ins for InDesign, and he decided to use his powerful hero and take the three-headed hydra. The bogatyr said - the bogatyr did, namely, the fence.
About the details of this struggle and will lead our tale.
')
TaleStep One, or Study the Anatomy of the Hydra Triceps
To begin with, it is necessary to briefly mention how InDesign works with pictures.
From the point of view of InDesign, images are a special type of PageItem that can draw and print itself. And this very PageItem can be put on the page, or as inline in a frame, or in a table cell, and all this must be able to handle. And still, drawing happens three types - fast (the crossed out rectangle is drawn), optimum (raster proxy image), and full (slow). It is also necessary to support all this.
Thus, we see the Import Plug-in, Page Item and several auxiliary classes, like this:
Class
{
kSVGItem,
kDisplayListPageItemBoss,
{
IID_ISHAPE, kSVGShapeImpl,
IID_IINKRESOURCES, kAllProcessInkResourcesImpl,
IID_IFLATTENERUSAGE, kSVGItemFlattenerUsageImpl,
IID_IVISITORHELPER, kEPSItemVisitorHelperImpl,
IID_ISCRIPT, kSVGItemScriptImpl,
}
},
so:
Class
{
kSVGPlaceProviderBoss,
kInvalidClass,
{
IID_IK2SERVICEPROVIDER, kImportServiceImpl,
IID_IIMPORTPROVIDER, kSVGPlaceProviderImpl,
IID_IIMPORTPREVIEW, kSVGImportPreviewImpl,
}
},
And, of course, there is also a standard header on the subject of startup / shutdown, which makes no sense to bring.
Let's see what it is and why.
• IID_ISHAPE is the implementation of the actual page item, which is responsible for rendering.
• IID_IINKRESOURCES - we process links to external files (Ink).
• IID_IFLATTENERUSAGE - we process the rasterization of the alpha channel in PDF. Actually, this is one line, obviously requiring the inclusion of a flattener, and no more.
• IID_IVISITORHELPER - we leave it standard, borrowing from EPS.
• IID_ISCRIPT - we provide support for our element through scripting. This is also a very simple part, which only correctly defines the type of object.
• IID_IK2SERVICEPROVIDER and IID_IIMPORTPROVIDER - we add support for the Place command.
• IID_IIMPORTPREVIEW - and provide a preview in the file-specific dialog (Windows specific).
The most interesting is actually IID_ISHAPE, this is the heart of our plugin. He is required to draw himself directly on IGraphicsPort, and also to obtain a raster proxy image. Looking ahead, I want to note - there were enough surprises.
Step Two, or Choose a Bogatyr Sword
Express googling, as well as viewing sample images from OpenClipArt, made me think about Apache Batik. I will say straight away that I had to make some efforts to repair the Batik itself, but in the end they turned out to be insignificant. Basically boiled down to "guess the version of SVG content." However, the task “how to draw a batik image on IGraphicsPort” appeared, and we will focus on it as the most difficult one. And we will move from boring C ++ matters to a fun Java creative and vice versa.
However, the disgraceful minstrels will tell us - it’s you, the hero, wandered into the wrong tale, and you’ll end up with five meters of barbed wire, as always happens when crossing a hedgehog with a snake. Experience, the son of difficult mistakes, teaches us - in this case, it turns out something more useful.
Step Three, or Getting to a DuelAs mentioned above, Java should be taught to work with indizayn threads. Actually, the task is obvious and is solved through 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
...
The implementation is built through the native function, we give them a list. They are self-evident and require no special comments. There is only one interesting point, here it is:
/**
* '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();
And the list itself:
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();
The last two functions were added to cache elements directly in Java, in order not to build again DOM models for each drawing. You just need to track changes in data and quickly re-read them when you change.
And the final touch is the actual implementation of the native methods.
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
};
};
We have already reached SVG, but now the task is to get and draw a picture. Actually, by drawing a vector image in BufferedImage, we get our proxy image. The standard example of batik clearly shows us how it was obtained, so to save space I allow myself not to bring this footcloth here. It is also obvious that PixelGrabber is used to get an array of bytes in RGB from a BufferedImage. We transfer the resulting array to the AGMImageRecord in the same way as discussed in the second part of the article.
It is interesting that drawing on IGraphicsPort instead of BufferedImage differs in exactly one line, instead of the standard offscreen Graphics2D, we need to substitute our implementation. This is what we will do.
Step Four, or Cunningly connect to the sword an electric current, so that sparks flyThe amount of code for literate inheritance Graphics2D is large, boring, but obvious, and it can easily be peeped from the same Apache Batik. Therefore, we will focus on the most interesting parts, but a whole pack of getters and setters a la
Paint getPaint(){ return paint; }
Paint getPaint(){ return paint; }
we will lower.
I note only that, in general, you can safely insert plugs of this type:
public void setXORMode(Color c1){ throw new RuntimeException("setXORMode: N/A"); }
and such:
public void fillRect(int x, int y, int w, int h) { fill(new Rectangle(x, y, w, h)); }
As a result of such simplifications and the elimination of duplicates, we have so many real functions:
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);
You can go further by using StrokeTextPainter, and this set will be reduced to three functions, which is good.
Now let's get to the other side, and see what we need from 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();
Experienced look can be seen a clear lack of drawing tools such as gradients. Alas, the Adobe documentation for this part of the API is so scanty that I never managed to use the available functions, I had to shaman, as will be discussed below.
As you can see, we need a little bit. However, a new task arises: we need to somehow combine the whole thing now. Let's start with a simple - with geometric shapes.
The most difficult thing here is to deploy the shape from Java to a set of native functions. To do this, we use the standard PathIterator, and at the same time, we do not forget about transformations, because Batik uses the AffineTransform very actively:
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();
}
}
Now we can easily draw primitives:

public void draw(Shape s) {
PathIterator pi = s.getPathIterator(getTransform());
applyClip(getClip());
applyStroke(s);
applyStyles();
applyPath(pi, PATH_STROKE);
restoreClip();
}
For now, we will not focus on all kinds of apply *, they are discussed further. As a matter of fact, fill () looks exactly the same, except for some difficulties with gradients. As you might guess, applyClip () is also implemented in the same way:
private void applyClip(Shape xclip) {
gsave();
if(xclip != null) {
PathIterator pi = xclip.getPathIterator(getTransform());
applyPath(pi, PATH_CLIP);
}
}
The real code is a bit more complicated, you have to take into account the peculiarities of the flattener implementation in IGraphicsPort, and if necessary, turn the bypass in order to bypass the path counterclockwise. And, as mentioned earlier, Batik perfectly knows how to text a vector into a set of glyphs, so you should not worry about working with fonts. So the text inevitably becomes a set of geometric shapes.
To be continued…