皆さんは
LibreOfficeとはなんぞや
「ご存知でしょうか」
少しだけ説明すると、
機能としてはワープロ、
LibreOfficeの詳しい話については、
LibreOfficeKit
さて今回はこのLibreOfficeを
そこで今回は、dlopen()
して使用するヘッダーファイルのみで構成されます。このヘッダーファイルをインクルードしたバイナリを作ることで、
LibreOfficeKitは、
ちなみにUbuntuの日本のコミュニティで活躍している有志が集まって執筆した
サンプルコードのサンプル
LibreOfficeKitのもっとも参考になるサンプルコードはLibreOffice Onlineのソースコードです。loolwsd
シンプルな例ではlloconvがあります。これはunoconvのLibreOfficeKit版とも言えるツールです。基本的な初期化と使い方をてっとりばやく把握できます。
レンダリング機能も利用した例であれば、
プログラムを作ってみよう
ここからは具体的なソースコードの例を交えて、
- ファイルフォーマットを変換するプログラム
(2ページに掲載) - ImpressのすべてのページをPNGとして出力するプログラム
(3ページに掲載) - Calcでキーボード操作をエミュレーションするプログラム
(4ページに掲載) - スクリーンショットをCalcに貼り付けるプログラム
(5ページに掲載)
なお、
必要なパッケージのインストール
まずプログラムをビルドするためのパッケージをインストールします。
$ sudo apt install build-essential libreofficekit-dev
これだけです。
ファイルフォーマットを変換するプログラム
まずは指定したオフィスドキュメントを別のフォーマットに変換するプログラムを作ってみましょう。
#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
」lok::Document
」
「lok::Office
」lok::lok_
を用いて作成します。このときの第一引数はLibreOfficeのプログラムがインストールされたパスであり、/usr/
」dlopen()
で開いた上で、-p
」
LibreOfficeのファイルを開くのは、lok::Office
」documentLoad()
メソッドです。第一引数にファイルのパスを、
「lok::Document
」saveAs()
はいわゆる
lok::lok_
もdocumentLoad()
もnew
で割り当てた結果を返すため、delete
を呼んでいます。C++を使うならスマートポインタでもいいでしょう。ただしnew
したポインタをうまく処理する必要があります。ちなみに、std::shared_
化していますが、std::make_
を呼ぶように改変しているようです。
ちなみに単純にコマンドから別名で保存したいだけであれば、
$ libreoffice --headless --convert-to 変換先の拡張子 変換元ファイル
「--headless
」--convert-to
」--infilter
」unoconv
コマンドも、
ImpressのすべてのページをPNGとして出力するプログラム
LibreOfficeKitは、
ここで紹介するプログラムはレンダリング機能のみを使います。プレゼンテーションツールであるImpressの、
#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++のヘッダーファイルをインストールしてください。また、
$ 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_
を使いたかったので-std=c++11
を指定しています。LibreOfficeKitで必要というわけではありません。コマンドの使い方は次のとおりです。
$ ./lok_split Impressのファイル名 変換先の名前
「変換先の名前-d
」
実はLibreOfficeKitにおいて、#define LOK_
」
「lok::Document
」getDocumentType()
では、getParts()
ではImpressのスライド数、getPart()
では現在のスライド番号を取得でき、setPart()
ではスライドを移動できます。ちなみにCalcの場合は、
getDocumentSize()
を使うとページのサイズを取得できます。このときの単位は
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/
は0で、nTileWidth/
はgetDocumentSize()
で取得した値をそのまま使います。またpaintTile()
で出力されるフォーマットは現在のところRGBAのみとなります。一応getTileMode()
でフォーマットの種別を判定できます。
あとはpng::image
のインスタンスに1ピクセルずつコピーしてPNG画像にして保存しています
ちなみにlibreoffice
コマンドの--convert-to
」pdftoppm
コマンドを使うと良いでしょう。
$ libreoffice --headless --convert-to pdf foo.odp
$ pdftoppm -png foo.pdf slide
Calcでキーボード操作をエミュレーションするプログラム
ここまでは元ファイルを変更しないプログラムでしたが、
#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;
}
ビルド方法と使い方は次のとおりです。以下の例だと-c
」-r
」
$ 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本体に伝えます。第一引数はイベントの種類、
文字列も究極的にはこのpostKeyEvent()
を用いて入力可能です。ただ単純に指定した文字列をセルを書き込むだけであれば、paste()
メソッドを使えます。paste()
メソッドは第一引数にMIMEを、
最後にgotoCell()
を呼び出して、gotoCell()
では、lok::Document
」postUnoCommand()
を使ってカーソルの移動を行っています。このpostUnoCommand()
」
UNO APIはマクロでも使われているAPIです。つまり、postUnoCommand()
経由でさまざまな操作を行うことになるでしょう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
」ToPoint
プロパティで、$F$5
」postUnoCommand()
は第三引数で、
完了時に呼び出されるのはlok::Document
」registerCallback()
で登録したコールバック関数です。このコールバックはLibreOfficeKitEnums.LibreOfficeKitCallbackType
にリストアップされているイベント時に呼び出され、std::condition_
を用いて、postUnoCommand()
呼出し後にコールバックが呼ばれるまで待つようにしています
ファイルを保存する場合も同様に.uno:Save
」
スクリーンショットをCalcに貼り付けるプログラム
Microsoft OfficeにはExcelというとても強力な事務書類作成ツールが存在します。グリッドに沿った文字列の配置、
LibreOfficeにもExcelに似たUIのツールとしてCalcが存在しますが、
#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
」
Impressの時にも説明したように、getParts()
で取得できます。シートを増やしたい場合は.uno:Add
」
スクリーンショットの撮影はImageMagickのimport
コマンドを使うことにしました。デスクトップ版のUbuntuなら最初から入っているはずです。import
コマンドのオプションを変更すれば、
画像ファイルをセルに貼り付けるコマンドは.uno:InsertGraphic
」paste()
でもできるとは思いますが、-i
」
作業の割には、
LibreOfficeの可能性は無限大
このようにLibreOfficeには外部からの操作ができるような仕組みが備わっています。LibreOfficeKitでなくても、
ちなみにUbuntu Phone向けアプリである
また最近のUbuntu Phoneはフル機能のLibreOfficeを起動することも可能です。スマートフォンぐらいの画面サイズだと厳しいですが、