argius note

プログラミング関連

Apache POIでExcelの図形の(拡張?)書式設定を変更する

Apache POIを使ってExcelの図形(シェイプ)を作って 図形の書式設定を変えることは、一部の書式は変更することはできますが、それ以外の書式設定を変えるにはどうすれば良いのか、ちょっと調べてみました。

今回の結論はリフレクションを使ったものになります。正規の方法では無いと思いますので、ご利用の際にはご注意ください。

追記(2015-06-06): XSSFバージョン(Excel2007以降のファイルフォーマット、*.xlsxの形式)を最後に追記しました。こちらはリフレクション無しです。


はじめに

とあるお題にて、「Apache POIで直線の矢印を設定したい」というものがありました。
図形の線の色、線のスタイル、線の幅、塗りつぶし色を設定するには、HSSFShapeメソッドが用意されているので、それを使って変更することができます。
しかし、HSSFShapeメソッドで提供されている図形の書式設定は、Excelの図形の書式設定のごく一部だけです。


私はPOIを使ったことがありましたが、ほとんどが読み取りの処理で、図形の処理はまったくやったことがありませんでした。
今回はちょっと興味が湧いたので、少しだけ調べてみました。


実行環境



普通の使い方で設定可能な書式を設定してみる

まず最初に、新しいブックを作って図形を追加し、保存するプログラムを示します。
今回は、Excel97-2003ブックで作成します。

// import java.io.*;
// import java.nio.file.*;
// import org.apache.poi.hssf.usermodel.*;

Path path = Paths.get("shape.xls");
try (OutputStream os = Files.newOutputStream(path); HSSFWorkbook book = new HSSFWorkbook()) {
    HSSFSheet sheet = book.createSheet();
    HSSFPatriarch patriarch = sheet.createDrawingPatriarch();
    // 図形1: 線1
    HSSFSimpleShape line1 = patriarch.createSimpleShape(patriarch.createAnchor(100, 100, 0, 0, 0, 0, 3, 10));
    line1.setShapeType(HSSFSimpleShape.OBJECT_TYPE_LINE);
    line1.setLineWidth(205 * 127); // 線の幅 2.05pt
    line1.setLineStyle(HSSFShape.LINESTYLE_LONGDASHGEL); // 長破線
    line1.setLineStyleColor(255, 0, 0); // 線の色 赤
    // 図形2: 四角形1
    HSSFSimpleShape rect1 = patriarch.createSimpleShape(patriarch.createAnchor(100, 100, 0, 0, 4, 0, 8, 10));
    rect1.setShapeType(HSSFSimpleShape.OBJECT_TYPE_RECTANGLE);
    rect1.setFillColor(0, 0, 255); // 塗りつぶし 青
    rect1.setLineWidth(542 * 127); // 線の幅 5.42pt
    book.write(os);
} catch (IOException e) {
    throw new UncheckedIOException(e);
}


Excelで見てみると、こんな感じになります。

f:id:argius:20150525224536p:plain
図1:線と四角形を作ってそれぞれスタイルを変更

前述のとおり、「ふつー」に設定できる書式設定はごく一部です。
ほかの書式設定はどうすれば良いのでしょうか。


EscherPropertyの設定について

Excelに限らず、MS Officeの図形の書式はMicrosoft Office Drawing formatに基づいています。POIの中では、org.apache.poi.ddfパッケージにこのフォーマットを扱うAPIがそろっています。

ググったりソースコードをちょっと調べた感じでは、このパッケージにあるEscherProperty (POI API Documentation)クラスを使うとできそうです。

ところが、HSSFShapeにはsetPropertyValue(EscherProperty)メソッドがあるものの、protectedなので直接呼び出すことができません。

HSSFShapeHSSFSimpleShapeを継承したクラスを作ったり、HSSFPatriarchクラスはfinalなのでクローンを作ってみるのを検討したりしましたが、パッケージプライベートのメソッドが障碍になって上手くいきませんでした。


リフレクションでEscherPropertyを設定する

リフレクションでEscherPropertyを設定してみます。
下記コードは、直線の終点を矢印にするプロパティーを設定する例です。

  • 直線の終点を矢印にする
// import java.lang.reflect.*; // 追加

EscherProperty lineEndArrowHeadIs1 = new EscherSimpleProperty(EscherProperties.LINESTYLE__LINEENDARROWHEAD, 1);

try {
    Method m = HSSFShape.class.getDeclaredMethod("setPropertyValue", EscherProperty.class);
    m.setAccessible(true);
    m.invoke(line1, lineEndArrowHeadIs1);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
    throw new RuntimeException(e);
}


毎回これを書くのは煩雑なので、関数型インターフェイスを使ってユーティリティー化してみましょう。

// import java.io.*;
// import java.lang.reflect.*;
// import java.nio.file.*;
// import java.util.function.*;
// import org.apache.poi.ddf.*;
// import org.apache.poi.hssf.usermodel.*;

Method m;
try {
    m = HSSFShape.class.getDeclaredMethod("setPropertyValue", EscherProperty.class);
    m.setAccessible(true);
} catch (NoSuchMethodException | SecurityException e) {
    throw new RuntimeException(e);
}
BiConsumer<HSSFShape, EscherProperty> funcSetProp = (shape, prop) -> {
    try {
        m.invoke(shape, prop);
    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
        throw new RuntimeException(e);
    }
};

Path path = Paths.get("shape.xls");
try (OutputStream os = Files.newOutputStream(path); HSSFWorkbook book = new HSSFWorkbook()) {
    HSSFSheet sheet = book.createSheet();
    HSSFPatriarch patriarch = sheet.createDrawingPatriarch();

    // 図形1: 線1
    HSSFSimpleShape line1 = patriarch.createSimpleShape(patriarch.createAnchor(100, 100, 0, 0, 0, 0, 3, 10));
    line1.setShapeType(HSSFSimpleShape.OBJECT_TYPE_LINE);
    line1.setLineWidth(205 * 127); // 線の幅 2.05pt
    line1.setLineStyle(HSSFShape.LINESTYLE_LONGDASHGEL); // 長破線
    line1.setLineStyleColor(255, 0, 0); // 線の色 赤
    // 終点 鋭い矢印 サイズ7
    funcSetProp.accept(line1, new EscherSimpleProperty(EscherProperties.LINESTYLE__LINEENDARROWHEAD, 1));
    funcSetProp.accept(line1, new EscherSimpleProperty(EscherProperties.LINESTYLE__LINEENDARROWWIDTH, 2));
    funcSetProp.accept(line1, new EscherSimpleProperty(EscherProperties.LINESTYLE__LINEENDARROWLENGTH, 0));

    // 図形2: 四角形1
    HSSFSimpleShape rect1 = patriarch.createSimpleShape(patriarch.createAnchor(100, 100, 0, 0, 4, 0, 8, 10));
    rect1.setShapeType(HSSFSimpleShape.OBJECT_TYPE_RECTANGLE);
    rect1.setFillColor(0, 0, 255); // 塗りつぶし 青
    rect1.setLineWidth(542 * 127); // 線の幅 5.42pt
    // 影
    funcSetProp.accept(rect1, new EscherSimpleProperty(EscherProperties.SHADOWSTYLE__SHADOWOBSURED, 196610));
    funcSetProp.accept(rect1, new EscherSimpleProperty(EscherProperties.SHADOWSTYLE__OFFSETX, 233487));
    funcSetProp.accept(rect1, new EscherSimpleProperty(EscherProperties.SHADOWSTYLE__OFFSETY, 233487));

    book.write(os);

} catch (IOException e) {
    throw new UncheckedIOException(e);
}
f:id:argius:20150525224539p:plain
図2:EscherPropertyで書式設定を追加したもの

矢印と影が設定できました。


リバースエンジニアリング

EscherPropertyを図形に設定する方法は分かりましたが、EscherPropertyに設定する値が分かりません。LINESTYLE__LINEENDARROWHEADのようなプロパティーは当てずっぽうでもできましたが、全部のプロパティーを調べるのは大変です。

そこで、既にExcel上で設定された図形の書式設定を読み取って出力するプログラムも作ってみました。こちらもリフレクション+関数型インターフェイスを使っています。

  • Excelの図形の書式設定を出力
// import java.io.*;
// import java.lang.reflect.*;
// import java.nio.file.*;
// import java.util.function.*;
// import org.apache.poi.ddf.*;
// import org.apache.poi.hssf.usermodel.*;

// 図形名
String[] shapeTypeNames = { "NotPrimitive", "Rectangle", "RoundRectangle", "Ellipse", "Diamond",
        "IsocelesTriangle", "RightTriangle", "Parallelogram", "Trapezoid", "Hexagon", "Octagon", "Plus",
        "Star", "Arrow", "ThickArrow", "HomePlate", "Cube", "Balloon", "Seal", "Arc", "Line", "Plaque", "Can",
        "Donut", "TextSimple", "TextOctagon", "TextHexagon", "TextCurve", "TextWave", "TextRing",
        "TextOnCurve", "TextOnRing", "StraightConnector1", "BentConnector2", "BentConnector3",
        "BentConnector4", "BentConnector5", "CurvedConnector2", "CurvedConnector3", "CurvedConnector4",
        "CurvedConnector5", "Callout1", "Callout2", "Callout3", "AccentCallout1", "AccentCallout2",
        "AccentCallout3", "BorderCallout1", "BorderCallout2", "BorderCallout3", "AccentBorderCallout1",
        "AccentBorderCallout2", "AccentBorderCallout3", "Ribbon", "Ribbon2", "Chevron", "Pentagon",
        "NoSmoking", "Star8", "Star16", "Star32", "WedgeRectCallout", "WedgeRRectCallout",
        "WedgeEllipseCallout", "Wave", "FoldedCorner", "LeftArrow", "DownArrow", "UpArrow", "LeftRightArrow",
        "UpDownArrow", "IrregularSeal1", "IrregularSeal2", "LightningBolt", "Heart", "PictureFrame",
        "QuadArrow", "LeftArrowCallout", "RightArrowCallout", "UpArrowCallout", "DownArrowCallout",
        "LeftRightArrowCallout", "UpDownArrowCallout", "QuadArrowCallout", "Bevel", "LeftBracket",
        "RightBracket", "LeftBrace", "RightBrace", "LeftUpArrow", "BentUpArrow", "BentArrow", "Star24",
        "StripedRightArrow", "NotchedRightArrow", "BlockArc", "SmileyFace", "VerticalScroll",
        "HorizontalScroll", "CircularArrow", "NotchedCircularArrow", "UturnArrow", "CurvedRightArrow",
        "CurvedLeftArrow", "CurvedUpArrow", "CurvedDownArrow", "CloudCallout", "EllipseRibbon",
        "EllipseRibbon2", "FlowChartProcess", "FlowChartDecision", "FlowChartInputOutput",
        "FlowChartPredefinedProcess", "FlowChartInternalStorage", "FlowChartDocument",
        "FlowChartMultidocument", "FlowChartTerminator", "FlowChartPreparation", "FlowChartManualInput",
        "FlowChartManualOperation", "FlowChartConnector", "FlowChartPunchedCard", "FlowChartPunchedTape",
        "FlowChartSummingJunction", "FlowChartOr", "FlowChartCollate", "FlowChartSort", "FlowChartExtract",
        "FlowChartMerge", "FlowChartOfflineStorage", "FlowChartOnlineStorage", "FlowChartMagneticTape",
        "FlowChartMagneticDisk", "FlowChartMagneticDrum", "FlowChartDisplay", "FlowChartDelay",
        "TextPlainText", "TextStop", "TextTriangle", "TextTriangleInverted", "TextChevron",
        "TextChevronInverted", "TextRingInside", "TextRingOutside", "TextArchUpCurve", "TextArchDownCurve",
        "TextCircleCurve", "TextButtonCurve", "TextArchUpPour", "TextArchDownPour", "TextCirclePour",
        "TextButtonPour", "TextCurveUp", "TextCurveDown", "TextCascadeUp", "TextCascadeDown", "TextWave1",
        "TextWave2", "TextWave3", "TextWave4", "TextInflate", "TextDeflate", "TextInflateBottom",
        "TextDeflateBottom", "TextInflateTop", "TextDeflateTop", "TextDeflateInflate",
        "TextDeflateInflateDeflate", "TextFadeRight", "TextFadeLeft", "TextFadeUp", "TextFadeDown",
        "TextSlantUp", "TextSlantDown", "TextCanUp", "TextCanDown", "FlowChartAlternateProcess",
        "FlowChartOffpageConnector", "Callout90", "AccentCallout90", "BorderCallout90",
        "AccentBorderCallout90", "LeftRightUpArrow", "Sun", "Moon", "BracketPair", "BracePair", "Star4",
        "DoubleWave", "ActionButtonBlank", "ActionButtonHome", "ActionButtonHelp", "ActionButtonInformation",
        "ActionButtonForwardNext", "ActionButtonBackPrevious", "ActionButtonEnd", "ActionButtonBeginning",
        "ActionButtonReturn", "ActionButtonDocument", "ActionButtonSound", "ActionButtonMovie", "HostControl",
        "TextBox", };

// HSSFShape#getOptRecordアクセッサー
Method m;
try {
    m = HSSFShape.class.getDeclaredMethod("getOptRecord");
    m.setAccessible(true);
} catch (NoSuchMethodException | SecurityException e) {
    throw new RuntimeException(e);
}
Function<HSSFShape, EscherOptRecord> funcGetOptRecord = (shape) -> {
    try {
        return (EscherOptRecord)m.invoke(shape);
    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
        throw new RuntimeException(e);
    }
};

// メイン部分
Path path = Paths.get("shape.xls");
try (InputStream is = Files.newInputStream(path); HSSFWorkbook book = new HSSFWorkbook(is)) {
    HSSFSheet sheet = book.getSheetAt(0);
    System.out.printf("sheet name = [%s]%n", sheet.getSheetName());
    HSSFPatriarch patriarch = sheet.createDrawingPatriarch();
    for (HSSFShape shape : patriarch.getChildren()) {
        HSSFSimpleShape ss = (HSSFSimpleShape)shape;
        System.out.println("* shape type = " + shapeTypeNames[ss.getShapeType()]);
        EscherOptRecord r = funcGetOptRecord.apply(shape);
        for (EscherProperty prop : r.getEscherProperties()) {
            System.out.println(">>> " + prop);
        }
    }
} catch (IOException e) {
    throw new UncheckedIOException(e);
}

  • 結果
sheet name = [Sheet0]
* shape type = Line
>>> propNum: 324, RAW: 0x0144, propName: geometry.shapepath, complex: false, blipId: false, value: 4 (0x00000004)
>>> propNum: 385, RAW: 0x0181, propName: fill.fillcolor, complex: false, blipId: false, value: 134217737 (0x08000009)
>>> propNum: 447, RAW: 0x01BF, propName: fill.nofillhittest, complex: false, blipId: false, value: 65536 (0x00010000)
>>> propNum: 448, RAW: 0x01C0, propName: linestyle.color, complex: false, blipId: false, value: 255 (0x000000FF)
>>> propNum: 459, RAW: 0x01CB, propName: linestyle.linewidth, complex: false, blipId: false, value: 26035 (0x000065B3)
>>> propNum: 462, RAW: 0x01CE, propName: linestyle.linedashing, complex: false, blipId: false, value: 7 (0x00000007)
>>> propNum: 465, RAW: 0x01D1, propName: linestyle.lineendarrowhead, complex: false, blipId: false, value: 1 (0x00000001)
>>> propNum: 468, RAW: 0x01D4, propName: linestyle.lineendarrowwidth, complex: false, blipId: false, value: 2 (0x00000002)
>>> propNum: 469, RAW: 0x01D5, propName: linestyle.lineendarrowlength, complex: false, blipId: false, value: 0 (0x00000000)
>>> propNum: 471, RAW: 0x01D7, propName: linestyle.lineendcapstyle, complex: false, blipId: false, value: 0 (0x00000000)
>>> propNum: 511, RAW: 0x01FF, propName: linestyle.nolinedrawdash, complex: false, blipId: false, value: 524296 (0x00080008)
>>> propNum: 959, RAW: 0x03BF, propName: groupshape.print, complex: false, blipId: false, value: 524288 (0x00080000)
* shape type = Rectangle
>>> propNum: 324, RAW: 0x0144, propName: geometry.shapepath, complex: false, blipId: false, value: 4 (0x00000004)
>>> propNum: 385, RAW: 0x0181, propName: fill.fillcolor, complex: false, blipId: false, value: 16711680 (0x00FF0000)
>>> propNum: 447, RAW: 0x01BF, propName: fill.nofillhittest, complex: false, blipId: false, value: 65536 (0x00010000)
>>> propNum: 448, RAW: 0x01C0, propName: linestyle.color, complex: false, blipId: false, value: 134217792 (0x08000040)
>>> propNum: 459, RAW: 0x01CB, propName: linestyle.linewidth, complex: false, blipId: false, value: 68834 (0x00010CE2)
>>> propNum: 462, RAW: 0x01CE, propName: linestyle.linedashing, complex: false, blipId: false, value: 0 (0x00000000)
>>> propNum: 511, RAW: 0x01FF, propName: linestyle.nolinedrawdash, complex: false, blipId: false, value: 524296 (0x00080008)
>>> propNum: 517, RAW: 0x0205, propName: shadowstyle.offsetx, complex: false, blipId: false, value: 233487 (0x0003900F)
>>> propNum: 518, RAW: 0x0206, propName: shadowstyle.offsety, complex: false, blipId: false, value: 233487 (0x0003900F)
>>> propNum: 575, RAW: 0x023F, propName: shadowstyle.shadowobsured, complex: false, blipId: false, value: 196610 (0x00030002)
>>> propNum: 959, RAW: 0x03BF, propName: groupshape.print, complex: false, blipId: false, value: 524288 (0x00080000)

この出力結果を元に、EscherPropertyの値を決めていけばOKです。


おわりに

設定することはできましたが、冒頭でも述べているとおり、これが正規の方法ではありません。

もっと良い方法がありましたら、是非お教えください。


追記(2015-06-06): XSSFバージョン

XSSFは、Excel2007以降の*.xlsxファイルとして保存されるOpen XML Formatsに対応したAPIです。
メインの部分はHSSFと大体同じですが、図形の拡張属性の変更はEscherPropertyではなく、CTXXXPropertiesXXXの部分は種類によって変わる)を使って設定します。また、こちらのプロパティーは、リフレクション無しで扱えます。ただし階層が少し複雑です。

なお、XSSFなどのOpen XML FormatsのAPIが含まれるpoi-ooxmlが必要です。

  • XSSFで直線の矢印と影付き四角形を作る
// import java.io.*;
// import java.nio.file.*;
// import org.apache.poi.ss.usermodel.*;
// import org.apache.poi.xssf.usermodel.*;
// import org.openxmlformats.schemas.drawingml.x2006.main.*;

Path path = Paths.get("shape.xlsx");
try (OutputStream os = Files.newOutputStream(path); XSSFWorkbook book = new XSSFWorkbook()) {
    XSSFSheet sheet = book.createSheet();
    XSSFDrawing patriarch = sheet.createDrawingPatriarch();

    // 図形1: 線1
    XSSFSimpleShape line1 = patriarch.createSimpleShape(patriarch.createAnchor(100, 100, 0, 0, 0, 0, 3, 10));
    line1.setShapeType(ShapeTypes.LINE);
    line1.setLineWidth(2.05); // 線の幅 2.05pt
    line1.setLineStyle(3); // 長破線
    line1.setLineStyleColor(255, 0, 0); // 線の色 赤
    // 終点 鋭い矢印 サイズ7(幅3,長さ1)
    CTShapeProperties line1prop = line1.getCTShape().getSpPr();
    CTLineProperties line1lp = line1prop.getLn();
    CTLineEndProperties line1lep = line1lp.addNewTailEnd();
    line1lep.setType(STLineEndType.Enum.forString("arrow"));
    line1lep.setW(STLineEndWidth.Enum.forInt(3));
    line1lep.setLen(STLineEndLength.Enum.forInt(1));

    // 図形2: 四角形1
    XSSFSimpleShape rect1 = patriarch.createSimpleShape(patriarch.createAnchor(100, 100, 0, 0, 4, 0, 8, 10));
    rect1.setShapeType(ShapeTypes.RECT);
    rect1.setFillColor(0, 0, 255); // 塗りつぶし 青
    rect1.setLineWidth(5.42); // 線の幅 5.42pt
    rect1.setLineStyleColor(0, 0, 0); // 線の色 黒(デフォルトでない)
    // 影   ⇒やり方分からなかった...

    book.write(os);

} catch (IOException e) {
    throw new UncheckedIOException(e);
}

影の付け方は分かりませんでした。
それ以外は、結果はHSSFと完全ではないですがだいたい同じになります。


XSSFのリバースエンジニアリングは、7-Zipアーカイバ―などでXLSXファイルを開いて、その中のxl\drawings\drawing1.xml辺りを見てみると何か分かるかもしれません。ということでプログラムは作っていません。