Ubuntu Weekly Recipe

第437回LibreOfficeはreOfficeのライブラリではありません LibreOffice Onlineを支える技術

皆さんはLibreOfficeというソフトウェアをご存知でしょうか。名前の最初に「Lib」がついているので何かのライブラリかと思いがちですが、実はこれMicrosoft Officeのようなオフィススイートなんです。今回はこのLibreOfficeをライブラリとして使用します。

LibreOfficeとはなんぞや

「ご存知でしょうか」も何も、この連載の読者であればLibreOfficeを知らない方はいませんよね。デスクトップ版のUbuntuであれば最初からインストールされていますし、UnityのLauncherにも登録されていますので、アイコンを見たことがないという方もほとんどいないはずです。

少しだけ説明すると、オフィススイートであるLibreOfficeはフリーでオープンなソフトウェアであり、非常に活発な開発コミュニティを持っているソフトウェアでもあります。およそ半年ごとにメジャーバージョンをリリースしており、8月には新しい5.2がリリースされたところです。ちなみにUbuntuで5.2を使いたい場合は第434回を参照してください。

機能としてはワープロ、表計算、プレゼンテーション、作図、データベース、数式といった一般的なオフィスソフトウェア機能一式を揃えています。ISOやJISの標準規格であるOpenDocument Format(拡張子.odt、.ods、.odpなど)をサポートするのみならず、Microsoft Officeで使われているフォーマット(拡張子.docx、.xlsx、.pptxなど)のファイルも読み書きできるのです。クロスプラットフォームに対応しているため、OSの垣根を超えて共通のスタンドアローンなオフィスソフトを使いたいとなると、LibreOfficeがもっとも有力な候補になるでしょう。また海外のみならず日本の地方公共団体でも採用事例が増えていることからもわかるように、UIや機能ともに日本語話者の利用に耐えうる品質を持っています。

LibreOfficeの詳しい話については、本連載のみならずgihyo.jpの各記事でも何度もとりあげていますので、そちらを参照してください。

LibreOfficeKit

さて今回はこのLibreOfficeを「ライブラリ」として使うことにします。オフィススイートであってもソフトウェアである以上、普段は人力で行っている各種操作を機械に任せたいと考えるのが人間の性です。LibreOfficeももちろん例外ではなく、⁠マクロ」「拡張機能」という形で各種処理を自動化できる仕組みが備わっています。とはいえこれらはあくまでLibreOfficeの中から、オフィスファイルを操作するツールです。どうせ自動化するのであれば、GUIを立ち上げることなくオフィスファイルを編集できると何かと便利です。

そこで今回は、オフィスファイルの操作ツールとしてLibreOfficeKitを使用します。LibreOfficeKitは、LibreOffice本体のライブラリ群を用いてLibreOfficeの各種機能にアクセスするためのC/C++向けインターフェースです。正確にはLibreOfficeKitは、LibreOffice本体のプログラムをdlopen()して使用するヘッダーファイルのみで構成されます。このヘッダーファイルをインクルードしたバイナリを作ることで、LibreOffice本体を経由したファイルの操作やレンダリングができるというわけです[1]⁠。まさに「LibreOfficeをライブラリとして使う」という状態ですね[2]⁠。

LibreOfficeKitは、オフィスファイルをブラウザから閲覧・編集できるLibreOffice Online新春特別企画の記事や、Android版のLibreOffice Viewerなどで使われています。つまりクライアント側でLibreOfficeのGUIを動かすことが難しい環境向けに、サーバー側でLibreOffice本体がレンダリングを行い、クライアント側ではそれを受け取ってクライアントの状況に即したレイアウトで表示させようという仕組みです。

ちなみにUbuntuの日本のコミュニティで活躍している有志が集まって執筆したうぶんちゅ!まがじん ざっぱ〜ん♪ Vol.5では、本連載の印刷関連の記事やLibreOfficeのイベントレポートでもおなじみのおがさわらさんが、LibreOffice Onlineを構築する手順やその内容について解説してくれています。興味のある方はぜひお買い求めください。

サンプルコードのサンプル

LibreOfficeKitのもっとも参考になるサンプルコードはLibreOffice Onlineのソースコードです。loolwsd(LibreOffice Online WebSocket Daemon)側でLibreOfficeKitを用いてオフィスファイルを操作・レンダリングした結果を、loleaflet側でJavaScript製のマップレンダリングライブラリであるLeafletとNode.jsによって描画するという仕組みになっています。LibreOfficeKitの使い方は、loolwsdのコードを見ればおおよそわかるでしょう。

シンプルな例ではlloconvがあります。これはunoconvのLibreOfficeKit版とも言えるツールです。基本的な初期化と使い方をてっとりばやく把握できます。

レンダリング機能も利用した例であれば、LibreOfficeのソースコードに同梱されているgtktiledviewerも参考になるかもしれません。ちなみにこれを動かす場合はLibreOfficeだけでなく「libreoffice-gtk3」パッケージも必要になります。

プログラムを作ってみよう

ここからは具体的なソースコードの例を交えて、LibreOfficeKitを使ったプログラムを作ってみます。作るのは以下の4つのプログラムです。

  • ファイルフォーマットを変換するプログラム2ページに掲載)
  • ImpressのすべてのページをPNGとして出力するプログラム3ページに掲載)
  • Calcでキーボード操作をエミュレーションするプログラム4ページに掲載)
  • スクリーンショットをCalcに貼り付けるプログラム5ページに掲載)

なお、動作確認環境はUbuntu 16.04 LTSとLibreOffice 5.1との組み合わせです。特に5.2の場合はいくつか挙動が変わっている可能性があるので、注意してください。また、いずれのプログラムもライセンスをCC0とします。また、ソースコードはGitHubにもアップロードしてあります

必要なパッケージのインストール

まずプログラムをビルドするためのパッケージをインストールします。

$ sudo apt install build-essential libreofficekit-dev

これだけです。⁠/usr/include/LibreOfficeKit」以下に必要なヘッダーファイルがインストールされます。ちなみにサーバー環境で使用したい場合は、別途LibreOffice本体もインストールしておいてください。

ファイルフォーマットを変換するプログラム

まずは指定したオフィスドキュメントを別のフォーマットに変換するプログラムを作ってみましょう。

lok_convert.cpp
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <LibreOfficeKit/LibreOfficeKit.hxx>

namespace lok_tools {
    const std::string LO_PATH = "/usr/lib/libreoffice/program";
}

void usage(const char *name)
{
    std::cerr << "Usage: " << name;
    std::cerr << " [-p path-of-libreoffice] FROM TO" << std::endl;
}

int main(int argc, char **argv)
{
    int opt;
    std::string lo_path = lok_tools::LO_PATH;
    while ((opt = getopt(argc, argv, "p:")) != -1) {
        switch (opt) {
        case 'p':
            lo_path = std::string(optarg);
            break;
        default:
            usage(argv[0]);
            exit(EXIT_FAILURE);
        }
    }

    if (argc - optind < 2) {
        usage(argv[0]);
        exit(EXIT_FAILURE);
    }
    const char *from_file = argv[optind++];
    const char *to_file = argv[optind];


    try {
        lok::Office *lo = lok::lok_cpp_init(lo_path.c_str());
        if (!lo) {
            std::cerr << "Error: Failed to initialise LibreOfficeKit";
            std::cerr << std::endl;
            exit(EXIT_FAILURE);
        }

        lok::Document *doc = lo->documentLoad(from_file);
        if (!doc) {
            std::cerr << "Error: Failed to load document: ";
            std::cerr << lo->getError() << std::endl;
            delete lo;
            exit(EXIT_FAILURE);
        }

        if (!doc->saveAs(to_file)) {
            std::cerr << "Error: Failed to save document: ";
            std::cerr << lo->getError() << std::endl;
            delete doc;
            delete lo;
            exit(EXIT_FAILURE);
        }

        delete doc;
        delete lo;
    } catch (const std::exception & e) {
        std::cerr << "Error: " << e.what() << std::endl;
        exit(EXIT_FAILURE);
    }

    return 0;
}

次のようにビルドします。

$ g++ lok_convert.cpp -Wall -Werror -ldl -o lok_convert

libdlを使うため、-ldlオプションが必要です。また使い方は次のとおりです。

$ ./lok_convert 変換元ファイル 変換先ファイル

LibreOfficeKitにはlok::Officeクラスとlok::Documentクラスが存在します。lok::OfficeクラスはLibreOffice本体の準備に使うクラスで、lok::Documentは読み込んだドキュメント本体のクラスです。

lok::Officeのインスタンスはlok::lok_cpp_init()を用いて作成します。このときの第一引数はLibreOfficeのプログラムがインストールされたパスであり、第二引数はユーザープロファイルのパスです。第二引数は省略できます。第一引数の「LibreOfficeのプログラム」は、Ubuntuの場合/usr/lib/libreoffice/programになります。このディレクトリ以下にLibreOfficeの本体となる各種共有ライブラリがインストールされているのです。LibreOfficeKitでは、初期化時にこのライブラリをdlopen()で開いた上で、その中のシンボルを呼び出していくことになります。本プログラムでは、プログラムのパスが異なる時のことを考慮して、-pオプションでパスを指定できるようにしてあります。

LibreOfficeのファイルを開くのは、lok::OfficeクラスのdocumentLoad()メソッドです。第一引数にファイルのパスを、第二引数にフィルタリングオプションを指定します。第二引数は省略可能です。

lok::DocumentクラスのsaveAs()はいわゆる「名前を付けて保存」です。これを使えば、開いたファイルを別形式で保存できるというわけです。

lok::lok_cpp_init()documentLoad()newで割り当てた結果を返すため、呼び出した側で不要になった段階でdeleteを呼んでいます。C++を使うならスマートポインタでもいいでしょう。ただしnewしたポインタをうまく処理する必要があります。ちなみに、LibreOffice Onlineの方はstd::shared_ptr化していますが、こちらはLibreOfficeKit.hxxの中でstd::make_sharedを呼ぶように改変しているようです。

ちなみに単純にコマンドから別名で保存したいだけであれば、libreofficeコマンドそのものを使えます。

$ libreoffice --headless --convert-to 変換先の拡張子 変換元ファイル

--headlessオプションは起動ロゴやUIを表示せず、ユーザーの操作なしにLibreOfficeを起動するオプションです。--convert-toで変換先の拡張子を指定します。--infilterオプションを使えば、入力データのフィルタリングもできます。オフィスファイルをPDFに変換したいという用途であれば、このコマンドを使うと良いでしょう。ちなみに、unoconvパッケージのunoconvコマンドも、同様の機能を持っています。

ImpressのすべてのページをPNGとして出力するプログラム

LibreOfficeKitは、タイル画像にレンダリングする機能を持っています。つまりオフィスファイルを画面上に出力する内容を、縦横複数枚のファイルに分割して表示する機能です。タイル型になっているので、ユーザーの操作によって画面の状態が変わった時に、該当する領域のタイルのみ更新すれば良いことになります。そのため、更新が発生したタイルを通知するコールバック機能も存在します。

ここで紹介するプログラムはレンダリング機能のみを使います。プレゼンテーションツールであるImpressの、すべてのスライドをそれぞれPNGに変換するプログラムです。指定したファイルを1ページごとに1枚の画像にレンダリングして、ビットマップからPNG画像に変換します。PNG画像への変換は、libpngのC++ラッパーであるpng++を使用します。

lok_split.cpp
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <vector>
#include <fstream>
#include <png++/png.hpp>
#define LOK_USE_UNSTABLE_API
#include <LibreOfficeKit/LibreOfficeKit.hxx>
#include <LibreOfficeKit/LibreOfficeKitEnums.h>

namespace lok_tools {
    const std::string LO_PATH = "/usr/lib/libreoffice/program";
}

void usage(const char *name)
{
    std::cerr << "Usage: " << name;
    std::cerr << " [-p path-of-libreoffice] [-d dpi] ImpressFile BaseName";
    std::cerr << std::endl;
}

int splitImpress(lok::Document *doc, std::string base, int dpi)
{
    if (!doc || base.empty() || dpi == 0) return -1;

    if (doc->getDocumentType() != LOK_DOCTYPE_PRESENTATION) {
        std::cerr << "Error: Is not Impress file (type = ";
        std::cerr << doc->getDocumentType() << ")" << std::endl;
        return -1;
    }

    std::string extname = ".png";
    for (int i = 0; i < doc->getParts(); ++i) {
        long pageWidth, pageHeight;

        doc->setPart(i);
        doc->getDocumentSize(&pageWidth, &pageHeight);

        /* 1 Twips = 1/1440 inch */
        int canvasWidth = pageWidth * dpi / 1440;
        int canvasHeight = pageHeight * dpi / 1440;
        std::vector<unsigned char> pixmap(canvasWidth * canvasHeight * 4);
        doc->paintTile(pixmap.data(), canvasWidth, canvasHeight, 0, 0,
                       pageWidth, pageHeight);

        png::image <png::rgba_pixel> image(canvasWidth, canvasHeight);
        for (int x = 0; x < canvasWidth; ++x) {
            for (int y = 0; y < canvasHeight; ++y) {
                image[y][x].red     = pixmap[(canvasWidth * y + x) * 4];
                image[y][x].green   = pixmap[(canvasWidth * y + x) * 4 + 1];
                image[y][x].blue    = pixmap[(canvasWidth * y + x) * 4 + 2];
                image[y][x].alpha   = pixmap[(canvasWidth * y + x) * 4 + 3];
            }
        }
        std::string filename = base + std::to_string(i) + extname;
        image.write(filename);
    }

    return 0;
}

int main(int argc, char **argv)
{
    int opt;
    std::string lo_path = lok_tools::LO_PATH;
    int dpi = 96;
    while ((opt = getopt(argc, argv, "p:d:")) != -1) {
        switch (opt) {
        case 'p':
            lo_path = std::string(optarg);
            break;
        case 'd':
            dpi = atoi(optarg);
            break;
        default:
            usage(argv[0]);
            exit(EXIT_FAILURE);
        }
    }

    if (argc - optind < 2) {
        usage(argv[0]);
        exit(EXIT_FAILURE);
    }
    const char *from_file = argv[optind++];
    const char *base_name = argv[optind];


    try {
        lok::Office *lo = lok::lok_cpp_init(lo_path.c_str());
        if (!lo) {
            std::cerr << "Error: Failed to initialise LibreOfficeKit";
            std::cerr << std::endl;
            exit(EXIT_FAILURE);
        }

        lok::Document *doc = lo->documentLoad(from_file);
        if (!doc) {
            std::cerr << "Error: Failed to load document: ";
            std::cerr << lo->getError() << std::endl;
            delete lo;
            exit(EXIT_FAILURE);
        }

        if (splitImpress(doc, base_name, dpi)) {
            std::cerr << "Error: Failed to split document" << std::endl;
            delete doc;
            delete lo;
            exit(EXIT_FAILURE);
        }

        delete doc;
        delete lo;
    } catch (const std::exception & e) {
        std::cerr << "Error: " << e.what() << std::endl;
        exit(EXIT_FAILURE);
    }

    return 0;
}

ビルドする前にpng++のヘッダーファイルをインストールしてください。また、ビルドにはpng++のオプションを指定します。

$ sudo apt install libpng++-dev
$ g++ lok_split.cpp -Wall -Werror -std=c++11 \
  `pkg-config --cflags --libs libpng` -ldl -o lok_split

std::to_string()を使いたかったので-std=c++11を指定しています。LibreOfficeKitで必要というわけではありません。コマンドの使い方は次のとおりです。

$ ./lok_split Impressのファイル名 変換先の名前

「変換先の名前(ページ番号).png」という画像が生成されます。また-dオプションを用いてDPIも指定できます。未指定の場合は96DPIです。

実はLibreOfficeKitにおいて、レンダリングやファイル編集系のAPIは「Unstable API」という扱いになっています。つまり将来的に内容が変わる可能性があるというわけです。これらのAPIを使いたい場合は、LibreOfficeKitのヘッダーファイルをインクルードする前に#define LOK_USE_UNSTABLE_APIを指定する必要があります。

lok::DocumentクラスのgetDocumentType()では、開いたファイルの種類を取得できます。今回はImpressのみに限定しています。ファイルタイプの定義については、LibreOfficeKitEnums.hを参照してください。同クラスのgetParts()ではImpressのスライド数、getPart()では現在のスライド番号を取得でき、setPart()ではスライドを移動できます。ちなみにCalcの場合は、スライド番号ではなくシート番号の意味になります。

getDocumentSize()を使うとページのサイズを取得できます。このときの単位はTwipです。1Twipは1/20ポイント(1/1440インチ)となります。ピクセル数に変換するには、さらにDPIを指定する必要があります。

paintTile()メソッドを用いると、ページの指定した領域を指定したピクセル数の画像に変換してくれます。

inline void paintTile(unsigned char* pBuffer,   /* 出力バッファのポインタ */
                      const int nCanvasWidth,   /* pBufferの横方向のピクセルサイズ */
                      const int nCanvasHeight,  /* pBufferの縦方向のピクセルサイズ */
                      const int nTilePosX,      /* ページの出力する領域のX座標をTWIPで */
                      const int nTilePosY,      /* ページの出力する領域のY座標をTWIPで */
                      const int nTileWidth,     /* ページの出力する領域の幅をTWIPで */
                      const int nTileHeight)    /* ページの出力する領域の高さをTWIPで */

今回はページ全体を出力したかったので、nTilePosX/nTilePosYは0で、nTileWidth/nTileHeightgetDocumentSize()で取得した値をそのまま使います。またpaintTile()で出力されるフォーマットは現在のところRGBAのみとなります。一応getTileMode()でフォーマットの種別を判定できます。

あとはpng::imageのインスタンスに1ピクセルずつコピーしてPNG画像にして保存しています[3]⁠。

ちなみにlibreofficeコマンドの--convert-toでPNG画像で変換した場合、先頭スライドのみ変換されるようです。今回のコマンドのように複数のページを複数のファイルに変換したい場合は、一度PDFに変換した上で、poppler-utilsのpdftoppmコマンドを使うと良いでしょう。

$ libreoffice --headless --convert-to pdf foo.odp
$ pdftoppm -png foo.pdf slide

Calcでキーボード操作をエミュレーションするプログラム

ここまでは元ファイルを変更しないプログラムでしたが、今度は元ファイルを変更することにします。つまり変更後に「保存」が必要になるわけです。また、LibreOfficeKitはWebブラウザーなど別のプログラムがUI部分を担当するので、UIのキーボード・マウスイベントを受け取る口が必要です。そこで今回のプログラムは、指定したセルまで移動するようキーボード入力した上で、指定した文字列をそのセルに貼り付け、保存して終了します。

lok_keyevent.cpp
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <mutex>
#include <condition_variable>
#define LOK_USE_UNSTABLE_API
#include <LibreOfficeKit/LibreOfficeKit.hxx>
#include <LibreOfficeKit/LibreOfficeKitEnums.h>

class LokTools {
public:
    static const std::string LO_PATH;

    LokTools(std::string path) :
        isUnoCompleted(false),
        _lo(NULL),
        _doc(NULL) {
        _lo = lok::lok_cpp_init(path.c_str());
        if (!_lo)
            throw;
    };

    ~LokTools() {
        if (_doc) delete _doc;
        if (_lo) delete _lo;
    };

    int open(std::string file) {
        _doc = _lo->documentLoad(file.c_str());
        if (!_doc) {
            std::cerr << "Error: Failed to load document: ";
            std::cerr << _lo->getError() << std::endl;
            return -1;
        }

        _doc->registerCallback(docCallback, this);
        return 0;
    };

    static void docCallback(int type, const char* payload, void* data) {
        LokTools* self = reinterpret_cast<LokTools*>(data);

        switch (type) {
        case LOK_CALLBACK_UNO_COMMAND_RESULT:
            {
                std::unique_lock<std::mutex> lock(self->unoMtx);
                self->isUnoCompleted = true;
            }
            self->unoCv.notify_one();
            break;
        default:
            ; /* do nothing */
        }
    };

    void postUnoCommand(const char *cmd, const char *args = NULL,
                        bool notify = false) {
        isUnoCompleted = false;
        _doc->postUnoCommand(cmd, args, notify);

        if (notify) {
            std::unique_lock<std::mutex> lock(unoMtx);
            unoCv.wait(lock, [this]{ return isUnoCompleted;});
        }
    };

    int inputKey(char col, char row, std::string label) {
        if (!_doc || label.empty()) return -1;

        if (_doc->getDocumentType() != LOK_DOCTYPE_SPREADSHEET) {
            std::cerr << "Error: Is not Calc file (type = ";
            std::cerr << _doc->getDocumentType() << ")" << std::endl;
            return -1;
        }

        /* GoTo the celll */
        for (int i = 0; i < col - 'A'; ++i) {
            _doc->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, 1027);
            _doc->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, 1027);
        }
        for (int i = 0; i < row - '1'; ++i) {
            _doc->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, 1024);
            _doc->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, 1024);
        }

        /* Paste label */
        if (!_doc->paste("text/plain;charset=utf-8",
                         label.c_str(), label.size())) {
            std::cerr << "Error: Failed to paste: " << label << std::endl;
        }

        /* GoTo A1 */
        gotoCell("A", "1");

        return 0;
    };

    void gotoCell(const std::string &col, const std::string &row) {
        std::string command = ".uno:GoToCell";
        std::string arguments = "{"
            "\"ToPoint\":{"
                "\"type\":\"string\","
                "\"value\":\"$" + col + "$" + row + "\""
            "}}";
        postUnoCommand(command.c_str(), arguments.c_str(), true);
    };

    int save() {
        postUnoCommand(".uno:Save", NULL, true);
        return 0;
    };

    std::mutex unoMtx;
    std::condition_variable unoCv;
    bool isUnoCompleted;

private:
    lok::Office *_lo;
    lok::Document *_doc;
};
const std::string LokTools::LO_PATH = "/usr/lib/libreoffice/program";

void usage(const char *name)
{
    std::cerr << "Usage: " << name;
    std::cerr << " [-p path-of-libreoffice] [-r row] [-c column]";
    std::cerr << " CalcFile Label" << std::endl;
}

int main(int argc, char **argv)
{
    int opt;
    std::string lo_path = LokTools::LO_PATH;
    char column = 'A';
    char row = '1';
    while ((opt = getopt(argc, argv, "p:c:r:")) != -1) {
        switch (opt) {
        case 'p':
            lo_path = std::string(optarg);
            break;
        case 'c':
            if (isalpha(*optarg))
                column = toupper(*optarg);
            break;
        case 'r':
            if (isdigit(*optarg))
                row = *optarg;
            break;
        default:
            usage(argv[0]);
            exit(EXIT_FAILURE);
        }
    }

    if (argc - optind < 2) {
        usage(argv[0]);
        exit(EXIT_FAILURE);
    }
    const char *calc_file = argv[optind++];
    const char *label = argv[optind];


    try {
        LokTools lok(lo_path);

        if (lok.open(calc_file)) {
            std::cerr << "Error: Failed to open" << std::endl;
            exit(EXIT_FAILURE);
        }

        if (lok.inputKey(column, row, label)) {
            std::cerr << "Error: Failed to input keys" << std::endl;
            exit(EXIT_FAILURE);
        }
        if (lok.save()) {
            std::cerr << "Error: Failed to save document" << std::endl;
            exit(EXIT_FAILURE);
        }

    } catch (const std::exception & e) {
        std::cerr << "Error: " << e.what() << std::endl;
        exit(EXIT_FAILURE);
    }

    return 0;
}

ビルド方法と使い方は次のとおりです。以下の例だと「F5」のセルに「日本語ラベル」を記述します。ちなみにサンプルの都合上、-cオプションと-rオプションともに、1文字しか受け付けないようにしています。きちんと作りなおせば、もっと広範囲のセルを指定が可能です。

$ g++ lok_keyevent.cpp -Wall -Werror -std=c++11 -ldl -o lok_keyevent
$ ./lok_keyevent -c F -r 5 sample.ods "日本語ラベル"

コマンドには表計算ソフトウェアであるCalcのファイルを渡してください。A1セルにカーソルがある空のファイルを想定しています。LibreOffice Calcで新規作成して保存すれば、そのようなファイルを生成できます。libreofficeコマンドを使ってヘッドレスに作成する方法はわかりませんでした。

コードは前と異なり、LokToolsクラスを作成しその中に各種メソッドを追加しています。しかしながら初期化やファイルを開いている部分でやろうとしていることは同じです。

キーボード入力部分は、inputKey()メソッドが該当します。

/* GoTo the celll */
for (int i = 0; i < col - 'A'; ++i) {
    _doc->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, 1027);
    _doc->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, 1027);
}
for (int i = 0; i < row - '1'; ++i) {
    _doc->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, 1024);
    _doc->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, 1024);
}

lok::DocumentクラスのpostKeyEvent()を使ってキーボードイベントをLibreOffice本体に伝えます。第一引数はイベントの種類、第二引数がキーイベントよって入力される文字のUnicodeで、第三引数はキーイベントの値です。今回はカーソルキーなので第二引数は0になっています。第三引数の値の具体的な値については、LibreOfficeのIDLファイルAPIドキュメントを参照してください。ここでは1027が右カーソルキーで、1024が下カーソルキーです。つまり列の数だけ右に移動したあとに、行の数だけ下に移動しているわけです。

文字列も究極的にはこのpostKeyEvent()を用いて入力可能です。ただ単純に指定した文字列をセルを書き込むだけであれば、paste()メソッドを使えます。paste()メソッドは第一引数にMIMEを、第二引数に貼り付けるデータを、第三引数にそのサイズを指定します。MIMEを指定できることからもわかるように、別に文字列に限らず、LibreOfficeで貼り付けられるデータであればなんでもかまいません。今回のコマンドでは、指定した文字列をそのまま移動したセルの上に貼り付けています。

最後にgotoCell()を呼び出して、カーソルの位置をA1に戻しています。このgotoCell()では、カーソルキーとは異なりlok::DocumentクラスのpostUnoCommand()を使ってカーソルの移動を行っています。このpostUnoCommand()は、ドキュメントの操作を行えるUNOインターフェースのコマンドを送るメソッドです。第一引数にコマンド名を、第二引数にコマンドのオプションをJSON形式で渡します。

UNO APIはマクロでも使われているAPIです。つまり、UNOコマンドを使えば、大抵の操作はできることになります。もし現時点でLibreOfficeKitを使うとなると、このpostUnoCommand()経由でさまざまな操作を行うことになるでしょう[4]⁠。サンプルコードではpostUnoCommand()自体がいくつかの場所にわかれています。以下はそれらのうち説明に必要な部分をまとめた擬似的なコードです。

std::string command = ".uno:GoToCell";
std::string arguments = "{"
    "\"ToPoint\":{"
        "\"type\":\"string\","
        "\"value\":\"$" + col + "$" + row + "\""
    "}}";
_doc->postUnoCommand(command.c_str(), arguments.c_str(), true);

std::unique_lock<std::mutex> lock(unoMtx);
unoCv.wait(lock, [this]{ return isUnoCompleted;});

.uno:GoToCellがUNO APIのコマンドです。GoToCellはToPointプロパティで、移動先のセル名を指定します。F5セルだったら$F$5となります。postUnoCommand()は第三引数で、コマンド完了の通知を行うかどうかのフラグを設定出来ます。

完了時に呼び出されるのはlok::DocumentクラスのregisterCallback()で登録したコールバック関数です。このコールバックはLibreOfficeKitEnums.hのLibreOfficeKitCallbackTypeにリストアップされているイベント時に呼び出され、コマンド完了時以外にもタイル領域の更新時や検索完了時にも呼び出されます。そこで本プログラムではstd::condition_variableを用いて、postUnoCommand()呼出し後にコールバックが呼ばれるまで待つようにしています[5]⁠。

ファイルを保存する場合も同様に.uno:Saveを用いて保存しています。保存の場合は、引数を与える必要は特にありません。ただしコマンドの終了イベントをちゃんと待たずにプログラムを終了すると、当然のことながら保存されませんので注意してください。

スクリーンショットをCalcに貼り付けるプログラム

Microsoft OfficeにはExcelというとても強力な事務書類作成ツールが存在します。グリッドに沿った文字列の配置、強力で柔軟な罫線ツール、簡単に配置可能な入力フォームといった便利な機能のおかげで、日々の業務における、事務書類(の雛形)作成の生産性を「向上」させていることでしょう。

LibreOfficeにもExcelに似たUIのツールとしてCalcが存在しますが、こちらは表計算ソフトという位置づけです。しかしながらExcelでできることはたいていCalcでもできます。Excelの主な用途に、各シートへのスクリーンショットの貼り付けが存在するということを、小鳥の噂で聞いたことがあります。そこで最後のサンプルとして、一定時間ごとにスクリーンショットを撮影し、新しいシートにそれを貼り付けるコマンドを作成してみました。

lok_screenshot.cpp
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <fstream>
#include <mutex>
#include <condition_variable>
#include <thread>
#define LOK_USE_UNSTABLE_API
#include <LibreOfficeKit/LibreOfficeKit.hxx>
#include <LibreOfficeKit/LibreOfficeKitEnums.h>

class LokTools {
public:
    static const std::string LO_PATH;

    LokTools(std::string path) :
        isUnoCompleted(false),
        _lo(NULL),
        _doc(NULL) {
        _lo = lok::lok_cpp_init(path.c_str());
        if (!_lo)
            throw;
    };

    ~LokTools() {
        if (_doc) delete _doc;
        if (_lo) delete _lo;
    };

    int open(std::string file) {
        _doc = _lo->documentLoad(file.c_str());
        if (!_doc) {
            std::cerr << "Error: Failed to load document: ";
            std::cerr << _lo->getError() << std::endl;
            return -1;
        }

        _doc->registerCallback(docCallback, this);
        return 0;
    };

    static void docCallback(int type, const char* payload, void* data) {
        LokTools* self = reinterpret_cast<LokTools*>(data);

        switch (type) {
        case LOK_CALLBACK_UNO_COMMAND_RESULT:
            {
                std::unique_lock<std::mutex> lock(self->unoMtx);
                self->isUnoCompleted = true;
            }
            self->unoCv.notify_one();
            break;
        default:
            ; /* do nothing */
        }
    };

    void postUnoCommand(const char *cmd, const char *args = NULL,
                        bool notify = false) {
        isUnoCompleted = false;
        _doc->postUnoCommand(cmd, args, notify);

        if (notify) {
            std::unique_lock<std::mutex> lock(unoMtx);
            unoCv.wait(lock, [this]{ return isUnoCompleted;});
        }
    };

    int takeScreenshot(char col, char row, int sheet) {
        if (!_doc) return -1;

        if (_doc->getDocumentType() != LOK_DOCTYPE_SPREADSHEET) {
            std::cerr << "Error: Is not Calc file (type = ";
            std::cerr << _doc->getDocumentType() << ")" << std::endl;
            return -1;
        }

        if (sheet >= _doc->getParts()) {
            std::string sheetName = "Sheet" + std::to_string(sheet+1);
            std::string sheetCmd = ".uno:Add";
            std::string sheetArg = "{"
                "\"Name\":{"
                    "\"type\":\"string\","
                    "\"value\":\"" + sheetName + "\""
                "}}";
            postUnoCommand(sheetCmd.c_str(), sheetArg.c_str(), true);
        }
        _doc->setPart(sheet);

        gotoCell(std::string(1, col), std::string(1, row));

        /* Take screenshot */
        std::string fileName = "/tmp/lok_tools_screen.png";
        std::string cmdName = "import -window root " + fileName;
        if (system(cmdName.c_str())) {
            std::cerr << "Error: Failed to take screenshot" << std::endl;
        }

        insertGraphic(fileName);
        gotoCell("A", "1");

        return 0;
    };

    void gotoCell(const std::string &col, const std::string &row) {
        std::string command = ".uno:GoToCell";
        std::string arguments = "{"
            "\"ToPoint\":{"
                "\"type\":\"string\","
                "\"value\":\"$" + col + "$" + row + "\""
            "}}";
        postUnoCommand(command.c_str(), arguments.c_str(), true);
    };

    void insertGraphic(const std::string &file) {
        std::string scheme = "file://";
        std::string command = ".uno:InsertGraphic";
        std::string argument = "{"
            "\"FileName\":{"
                "\"type\":\"string\","
                "\"value\":\"" + scheme + file + "\""
            "}}";
        postUnoCommand(command.c_str(), argument.c_str(), true);
    };

    int save() {
        postUnoCommand(".uno:Save", NULL, true);
        return 0;
    };

    std::mutex unoMtx;
    std::condition_variable unoCv;
    bool isUnoCompleted;

private:
    lok::Office *_lo;
    lok::Document *_doc;
};
const std::string LokTools::LO_PATH = "/usr/lib/libreoffice/program";

void usage(const char *name)
{
    std::cerr << "Usage: " << name;
    std::cerr << " [-p path-of-libreoffice] [-c column] [-r row]";
    std::cerr << " [-s sheets] [-i interval] CalcFile" << std::endl;
}

int main(int argc, char **argv)
{
    int opt;
    std::string lo_path = LokTools::LO_PATH;
    char column = 'A';
    char row = '1';
    int sheets = 1;
    int interval = 5;
    while ((opt = getopt(argc, argv, "p:c:r:s:i:")) != -1) {
        switch (opt) {
        case 'p':
            lo_path = std::string(optarg);
            break;
        case 'c':
            if (isalpha(*optarg))
                column = toupper(*optarg);
            break;
        case 'r':
            if (isdigit(*optarg))
                row = *optarg;
            break;
        case 's':
            sheets = atoi(optarg);
            break;
        case 'i':
            interval = atoi(optarg);
            break;
        default:
            usage(argv[0]);
            exit(EXIT_FAILURE);
        }
    }

    if (argc - optind < 1) {
        usage(argv[0]);
        exit(EXIT_FAILURE);
    }
    const char *calc_file = argv[optind++];


    try {
        LokTools lok(lo_path);

        if (lok.open(calc_file)) {
            std::cerr << "Error: Failed to open" << std::endl;
            exit(EXIT_FAILURE);
        }

        for (int i = 0; i < sheets; ++i) {
            std::cout << "Take screenshot for Sheet" << i+1 << std::endl;

            if (lok.takeScreenshot(column, row, i)) {
                std::cerr << "Error: Failed to take screenshot" << std::endl;
                exit(EXIT_FAILURE);
            }

            std::this_thread::sleep_for(std::chrono::seconds(interval));
        }

        if (lok.save()) {
            std::cerr << "Error: Failed to save document" << std::endl;
            exit(EXIT_FAILURE);
        }

    } catch (const std::exception & e) {
        std::cerr << "Error: " << e.what() << std::endl;
        exit(EXIT_FAILURE);
    }

    return 0;
}

ビルド方法は次のとおりです。

$ g++ lok_screenshot.cpp -Wall -Werror -std=c++11 -ldl -o lok_screenshot
$ ./lok_screenshot -c F -r 5 -s 10 -i 5 sample.ods

-c-rはスクリーンショットを貼り付ける列と行の指定です。-sは撮影するスクリーンショットの枚数で、-iは撮影間隔です。上記コマンドだと「5秒ごとにスクリーンショットを撮影し、F5セルに貼り付けることを10回繰り返す」ということになります。

Impressの時にも説明したように、シートの数はgetParts()で取得できます。シートを増やしたい場合は.uno:Addコマンドを使用します。

スクリーンショットの撮影はImageMagickのimportコマンドを使うことにしました。デスクトップ版のUbuntuなら最初から入っているはずです。importコマンドのオプションを変更すれば、特定のウィンドウや特定の領域のみ作成することも可能です。

画像ファイルをセルに貼り付けるコマンドは.uno:InsertGraphicです。今回はファイル名以外の引数は設定していませんが、フィルタをかけることなども可能なようです。paste()でもできるとは思いますが、ファイルを開く部分もあわせてLibreOfficeに任せられるので、こちらの方が簡単です。貼り付けたあとに-iオプションで指定した分、スリープします。

作業の割には、一つ前のプログラムに比べて特に複雑なことはしていません。たとえばImpressのスライドを出力した時のように、PNG++と組み合わせれば、画像を埋め込む代わりに、各ピクセルをセルにして「表」にしてしまうことも可能です。むしろそのほうが、すべてのピクセルの統計情報を「計算」できるので、より表計算らしい使い方と言えるでしょう。なに、そんなことをする意味は、あとから考えればいいのです。

LibreOfficeの可能性は無限大

このようにLibreOfficeには外部からの操作ができるような仕組みが備わっています。LibreOfficeKitでなくても、python3-unoパッケージをインストールすればPythonからUNO API経由でもっと簡単に操作できるでしょう。日々同じようなフォーマットのオフィスドキュメントの入力に疲れている方は、LibreOfficeを使って「Software Defined Office Document」な世界に足を踏み入れてはいかがでしょうか。

ちなみにUbuntu Phone向けアプリであるubuntu-doc-viewerは、プラグインとしてLibreOfficeファイルの閲覧にも対応しています。これはLibreOfficeKitを用いてタイル画像に描画しつつ、Qt5/QML用のLibreOffice描画プラグインを作ってその画像データを画面に表示させているというものです。

また最近のUbuntu Phoneはフル機能のLibreOfficeを起動することも可能です。スマートフォンぐらいの画面サイズだと厳しいですが、SlimPort/HDMI経由やMiracast経由での外部ディスプレイ出力にも対応しているので、速度的な点に目を瞑ればPCがなくてもLibreOfficeファイルを直接編集できます。

おすすめ記事

記事・ニュース一覧