🙆‍♂️

RustでImageMagickを使用して画像フォーマットを変換する(PDF→PNG)方法

2023/12/22に公開

こんにちは。
PharmaXの共同創業者の上野(@ueeeeniki)です。
この記事はPharmaXアドベントカレンダーの22日目の記事です。

https://qiita.com/advent-calendar/2023/pharma-x

PharmaXでは、一部プロダクトがRustで実装されています。Rustで画像フォーマットを変換したいという要件で困ったので、調査、対応した知見を共有したいと思います。
この記事では、ImageMagickを使って、Rustで画像フォーマットを変換する方法について解説します。
具体的には、Rustから外部コマンドを実行して、LinuxにインストールされているImageMagickで画像フォーマットを変換します。
ここでは例として、PDF→PNGの変換を取り上げますが、他の画像フォーマットの変換も同様の方法で行うことが可能です。

この記事で学べることは下記のとおりです。

  • 画像操作ソフトウェアのImageMagickを用いて画像フォーマットを変換する方法
  • Rustで外部コマンドを実行する方法
  • 画像ファイルを圧縮する方法

Rustの画像操作についてお困りの方の参考になると嬉しいです!
ImageMagickをRustではない他の言語から使用したい方にも参考になる内容だと思います。

はじめに

課題と解決方法

Rustは非常に強力な言語ですが、標準ライブラリが決して大きくはないので、あらゆる処理にサードパーティライブラリを使用する必要があります。ですが、新しい言語ということもあって、crate(ライブラリ)のエコシステムはまだ未成熟です。個人的な感覚ですが、特にPDFや画像などのファイル操作系のcrateで困ることが多いように感じます。
今回はPDFをPNGに変換したいのですが、Rustにはデファクトスタンダードとなるようなcrateがありません。世の中には私と同じようなことを考えている方もいて、下記には調査結果がまとまっています。

https://github.com/zentered/rust-pdf-to-png-service

This is the result of a long, painful and embarrassing journey to resolve a very simple task: get pngs and webp files from a PDF. Here are a few alternatives we tried: ……(省略)……
(これは、PDFからpngとwebpファイルを取得するという非常に単純なタスクを解決するための、長く、苦しい戸惑いの旅の顛末です。私たちが試したいくつかの選択肢は以下の通りです)

ここでは、ImageMagickを使って、Rustから呼び出して画像操作を行う方法を紹介したいと思います。

ImageMagickとは

ImageMagickは、画像の操作や表示をするためのソフトウェアです。PDF、JPEG、PNG、GIFなど様々な画像ファイルフォーマットに対応しており、画像の変換・編集などに必要な多数の機能を備えています。非常に歴史も古く、今でもCLIから画像を操作する場合の有力な選択肢の一つです。一般的なアプリケーションで行いたいような画像操作は、ほとんど可能だと考えても間違いではないと思います。

注意点

ImageMagickは古くから使われてきたソフトウェアですが、繰り返し脆弱性が発見されています。
もちろんメジャーなソフトウェアなので、脆弱性が発見され次第対応されますが、下記の記事のように懸念を示す会社もあるようです。
https://blog.cybozu.io/entry/2018/08/21/080000

下記の記事にあるように、ImageMagickは、読み書き出来る画像ファイルの形式が膨大で、対応しているOSも幅広いので、脆弱性は発生しやすいのです。ImageMagickのようなカバー範囲の広いソフトウェアでは当然のことで、これ自体は珍しいことではありません。この記事にもあるようにユーザーから受け取った任意の画像ファイル形式をすべて処理するようなことをしなければある程度は問題を限定できると思います。
https://qiita.com/yoya/items/2076c1f5137d4041e3aa

Rustで外部コマンドを呼び出す方法

今回はRustのCrateを使うのではなく、マシン上にインストールされたImageMagickを使用するので、コマンドを実行する必要があります。
Rustで外部コマンドを実行する方法は簡単で、下記のようにstd::process::Commandを使用することができます。

use std::process::Command;

// コマンドの結果を待機
let output = Command::new("ls")
    .args(["-l", "-a"])
    .output()
    .expect("failed");

// コマンドの実行結果を確認
if output.status.success() {
    println!("成功: {}", String::from_utf8_lossy(&output.stdout));
} else {
    eprintln!("エラー: {}", String::from_utf8_lossy(&output.stderr));
}

出力結果をoutputに入れることで、出力結果をその後の処理に使用する事が可能です。
また、引数は、下記のように一つ一つ指定することも可能です。

let output = Command::new("ls")
    .arg("-l")
    .arg("-a")
    .output()
    .expect("failed");

RustでImageMagickを呼び出して画像フォーマットの変換を行う方法

では、RustからImageMagickを呼び出す方法を解説していきましょう。

ImageMagickのインストール

この記事では、Linux(Debian)上にImageMagickをインストールすることします。私は、Dockerを使っているので、Dockerのイメージ上にImageMagickをImageMagickをインストールすることを想定しています。

Dockerfile
# ImageMagickのインストール
RUN apt-get update && \
    apt-get install -y imagemagick ghostscript && \
    rm -rf /var/lib/apt/lists/*

PDFファイルを処理するためには、ImageMagickはGhostscriptに依存しています。そのため、Ghostscriptも同時にインストールします。

PDFの変換を可能にするための設定方法

ImageMagickはデフォルトでは、セキュリティポリシーによってPDFの操作ができない設定になっています。この設定を変更するには、ImageMagickのポリシー設定ファイルpolicy.xmlを編集する必要があります。
policy.xmlは、/etc/ImageMagick-6、/etc/ImageMagick、または/usr/local/etc/ImageMagick-7あたりにあるようです。

policy.xmlを見つけたら、PDFの読み書きが可能になるように

policy.xml
<policy domain="coder" rights="none" pattern="PDF" />

という行を

policy.xml
<policy domain="coder" rights="read|write" pattern="PDF" />

に変更します。
Dockerfile上で行うなら、

Dockerfile
RUN sed -i '/<policy domain="coder" rights="none" pattern="PDF" \/>/c\<policy domain="coder" rights="read|write" pattern="PDF" \/>' /etc/ImageMagick/policy.xml

のように記述する必要があります。

ImageMagickを実行

ここまでで準備ができたので、ImageMagickの実行方法を解説します。

magick convert -density {DPI} {変換前のファイル名} {変換前のファイル名}

これをRustで実装すると、以下のようになります。

main.rs
use std::process::Command;
use std::io::Error;

fn main() -> Result<(), Error> {
    // 変換したいPDFファイルのパス
    let pdf_path = "input.pdf";
    // 出力されるPNGファイルのパス
    let png_path = "output.png";
    // 変換後の画像のDPIを指定
    let density = "600"

    // ImageMagickの`magick convert`コマンドを使用してPDFをPNGに変換
    // -density オプションを使用
    let output = Command::new("magick")
        .arg("convert")
        .arg("-density")
        .arg(density)
        .arg(pdf_path)
        .arg(png_path)
        .output()?;

    // コマンドの実行結果を確認
    if output.status.success() {
        println!("変換成功");
    } else {
        eprintln!("変換エラー: {}", String::from_utf8_lossy(&output.stderr));
    }

    Ok(())
}

-density オプションを使用することで、変換後の画像のDPIを指定することができます。
一方、ImageMagickは変換中にメモリやディスクスペースの制限に達するとエラーを出しまうので、densityを大きすぎる値に設定することは推奨されません。

元の画像がサイズが大きいときの対応(おまけ)

元の画像サイズが大き過ぎる場合には、当然、変換処理の負荷は高くなってしまいます。結果、メモリエラーが出る可能性も大きくなりますし、アプリケーションの要求する時間内に処理が終わらない可能性もあります。
このような場合には、ImageMagickと同時にインストールしたGhostscriptを使用し、下記のようにして元のPDFファイルを圧縮することが有効です。

gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/ebook -dNOPAUSE -dQUIET -dBATCH -sOutputFile=output.pdf input.pdf

ここで、input.pdfは圧縮するファイル、output.pdfは圧縮後のファイルです。-dPDFSETTINGSオプションは圧縮レベルを指定し、以下の値を取ることができます:

  • /screen:低品質、小さなサイズ
  • /ebook:中品質、適度なサイズ
  • /printerまたは/prepress:高品質、大きなサイズ

当然、高い圧縮率を指定し過ぎると、画質が低下しすぎてしまって使い物にならなくなる可能性もあるので注意が必要です。
また、この方法はPDFの内容によって効果が異なります。テキスト主体のドキュメントは良い結果が得られやすいですが、画像が多いドキュメントは品質の低下が顕著になる場合があるようです。

これをRustで実行すると下記のようになります。

use std::fs;
use std::process::Command;
use std::io::Error;

fn compress_pdf(pdf_path: &str, compressed_pdf_path: &str) -> Result<(), Error> {
    let output = Command::new("gs")
        .arg("-sDEVICE=pdfwrite")
        .arg("-dCompatibilityLevel=1.4")
        .arg("-dPDFSETTINGS=/ebook")
        .arg("-dNOPAUSE")
        .arg("-dQUIET")
        .arg("-dBATCH")
        .arg("-sOutputFile=".to_owned() + compressed_pdf_path)
        .arg(pdf_path)
        .output()?;

    if output.status.success() {
        Ok(())
    } else {
        eprintln!("PDF圧縮エラー: {}",String::from_utf8_lossy(&output.stderr));
        return Err(Error::from(std::io::ErrorKind::Other));
    }
}

適切なサイズに圧縮したPDFファイルをImageMagickでPNGに変換すれば、無事に変換が成功するでしょう。

最後に

今回は、Rustで画像フォーマットを変換する方法について紹介してきました。Rustで画像操作を行いたい方の参考になる記事になったのではないでしょうか。
また、画像操作ではなくとも、Rustで外部コマンドを実行したいケースはあると思います。Dockerなどのイメージ上で使用可能なコマンドをRustから呼び出したい場合には、今回紹介した方法で実行することが可能です。
この記事をお読みいただいた皆さまのRustライフが快適になることを願っています!

PhramaXでは、積極的な発信が文化となっていて、下記のような勉強会も行います。ご興味のある方は是非ご参加ください!

https://yojo.connpass.com/event/305679/

PharmaXテックブログ

Discussion