Ubuntu Weekly Recipe

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

この記事を読むのに必要な時間:およそ 17 分

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⁠。

※3
実際はもっと効率の良い変換方法があるのかもしれませんが,ここではベタな方法を使っています。

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

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

著者プロフィール

柴田充也(しばたみつや)

Ubuntu Japanese Team Member株式会社 創夢所属。数年前にLaunchpad上でStellariumの翻訳をしたことがきっかけで,Ubuntuの翻訳にも関わるようになりました。