日本語対応版SDAPSのDockerイメージを作ってみた

# はじめに

SDAPSはマークシート専用紙を必要としない、一般的なコピー用紙とドキュメントスキャナを使用してアンケートの質問票作成、集計を行なうOMR(光学式マーク読取)ソフトウェアです。

厳密な精度が求められる入学試験のような用途には不適切ですが、塗り潰す必要がない(これはcheckmode=fillで塗り潰しに変更可能)、専用用紙を準備する必要がない、などのメリットがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78296/53cf423d-9cd5-d602-6c43-b56ed082dc0c.png)

オリジナルのSDAPSは日本語をサポートしていないため、日本語テキストを含む質問票を作成し、レポートを出力することができるように改造したsdapsコマンドを実行するDockerイメージを作成してみました。

作成したDockerイメージはDockerHubにて、Dockerfileや変更部分はGitHubにて公開しています。

# 成果物

変更部分などは下記のGithub, DockerHubにて公開しています。

* [【GitHub】YasuhiroABE/docker-sdaps-ja](https://github.com/YasuhiroABE/docker-sdaps-ja)
* [【DockerHub】yasuhiroabe/sdaps-ja](https://hub.docker.com/repository/docker/yasuhiroabe/sdaps-ja)
* [【GitHub】日本語の質問票を作成するためサンプルプロジェクト](https://github.com/YasuhiroABE/sdaps-ja-example)

最後のコード一式をdockerコマンドが利用できる環境(※)に展開すれば、質問票の作成を除けば、簡単なコマンド入力で作業を進めることができます。

# 参考資料

この記事を書く際に参考にしたURLをリストにしています。

* https://sdaps.org/
* http://fabiorehm.com/blog/2014/09/11/running-gui-apps-with-docker/
* https://qiita.com/YasuhiroABE/items/75639f4b95731171b894

# 開発環境

開発環境をUbuntu 22.04に変更したため、コンテナも合わせて更新しました。

* ホストOS
  * Ubuntu 22.04.3 on VMware Workstation 17 Pro
* Docker
  * docker-ce (20.10.23, rootless-mode, buildx)
* SDAPS
  *  [1.9.13 (ppa:benjamin-sipsolutions/sdaps)](https://launchpad.net/~benjamin-sipsolutions/+archive/ubuntu/sdaps?field.series_filter=jammy)

jammyではstable版と違いがなさそうなので特に意識せずにunstable版のPPAを利用していますが、将来変更するかもしれません。

## テスト環境

以下の環境で、質問票の作成・TIFFイメージの読取・修正ができることをテストしています。

> 【2022/03/11】Apple Silicon環境でのテストは限定的です。

* Linux Distribution
  * Xubuntu Desktop 22.04.3 amd64版
* Windows11 (22H2) amd64bit版
  * WSL2 + Docker Desktop for Windows (24.0.6)
  * Windows Terminal
* 【非対応】macOS (M1, Apple Silicon)、Raspberry Pi等ARM系CPU
  * PPAパッケージがarm64に対応していないためコンテナの構築ができません。必要な場合は docker.io/yasuhiroabe/sdaps-ja:ub2004-5 を利用してください。

質問票の印刷、読み取りには以下のような機材を使用しています。

* レーザープリンター (OKI B841)
  * 一般的なPPC用紙 (白色普通紙)
  * 印刷品位 - 高精細 (1200x1200)
* ドキュメントスキャナ (Brother ADS-3600W)
  * 設定 - TIFFマルチページ・300dpi・モノクロ・長辺とじ

### コピー(複写)機の利用について

いまのところ質問票を準備するために複写機は積極的に使用していませんが、利用できている事例はあります。

機種によってはPDF出力がJPEG画像を変換したのかと思われるほどに、マルチページTIFF出力と比較して明らかに劣化している場合があったので事前の動作確認は必須です。

# 使い方

コマンド実行時には、Dockerのvolume機能を利用して、回答票のTeXファイルとプロジェクト・ディレクトリを配置する作業用ディレクトリを作成し、必ずコンテナ内の/projにマウントするようにしてください。カレント・ワーキングディレクトリも/projになります。

* Dockerコンテン内の作業用ディレクトリ: /proj

## 作業の流れ

おおまかには次のような順序で利用しています。

1. 作業用ディレクトリを作成する (e.g. mkdir proj/)
2. 質問票のTeXファイルを作成 (e.g. proj/example.tex)
3. setupコマンドを実行し、プロジェクト・ディレクトリを作成
4. 記入済みの質問票をスキャンし、TIFFファイルを作業用ディレクトリに配置 (e.g. proj/01.tiff, proj/02.tiff,...)
5. addコマンドを繰り返し実行し、TIFFファイルを登録
6. recognizeコマンドを実行し、データを抽出
7. report_texコマンドでreportファイルを作成 (e.g. proj/work/report_1.pdf)
8. csvコマンドで生データを抽出 (e.g. proj/work/data_1.csv)

## 準備作業

使用するDockerイメージはコマンド実行前にpullしておきます。

```bash:pullの実行例
$ docker pull docker.io/yasuhiroabe/sdaps-ja:ub2204-20231004
```

### 作業用ディレクトリについて

参考までに最終的なproj/ディレクトリの様子を掲載します。

```bash:treeコマンドの出力結果
$ tree proj/
proj
├── 01.tiff
├── example.tex
└── work
    ├── 1.tif
    ├── data_1.csv
    ├── info
    ├── info~
    ├── log
    ├── questionnaire.aux
    ├── questionnaire.log
    ├── questionnaire.out
    ├── questionnaire.pdf
    ├── questionnaire.sdaps
    ├── questionnaire.tex
    ├── report_1.pdf
    ├── report_2.pdf
    ├── sdapsarray.sty
    ├── sdapsbase.sty
    ├── sdapsclassic.cls
    ├── sdapscode128.tex
    ├── sdapslayout.sty
    ├── sdaps.opt
    ├── sdapspdf.sty
    ├── sdapsreport.cls
    ├── survey.sqlite
    ├── translator-sdaps-dictionary-Brazilian.dict
    ├── translator-sdaps-dictionary-Dutch.dict
    ├── translator-sdaps-dictionary-English.dict
    ├── translator-sdaps-dictionary-Finnish.dict
    ├── translator-sdaps-dictionary-French.dict
    ├── translator-sdaps-dictionary-German.dict
    ├── translator-sdaps-dictionary-Italian.dict
    ├── translator-sdaps-dictionary-Norsk.dict
    ├── translator-sdaps-dictionary-Polish.dict
    ├── translator-sdaps-dictionary-Portuguese.dict
    ├── translator-sdaps-dictionary-Romanian.dict
    ├── translator-sdaps-dictionary-Sinhala.dict
    ├── translator-sdaps-dictionary-Spanish.dict
    ├── translator-sdaps-dictionary-Swedish.dict
    └── translator-sdaps-dictionary-Ukrainian.dict

1 directory, 39 files
```

## 日本語質問票のサンプルの準備

テストのために使用したファイルを掲載していますが、オリジナルからコメントやコンテンツの一部を削除しています。

```tex:example.texを元に日本語を埋め込んだ例
\documentclass[
  english,
  a4paper, 
  checkmode=check,
  pagemark,
  stamp]{sdapsclassic}
\usepackage[utf8]{inputenc}
% For demonstration purposes
\usepackage{multicol}

\usepackage{xltxtra}
\setmainfont{IPAPMincho}
\setsansfont{IPAPGothic}
\setmonofont{IPAGothic}
\XeTeXlinebreaklocale "ja"

\author{作者}
\title{タイトル}

\begin{document}
  \begin{questionnaire}
    \begin{info}
      \texttt{info}環境を利用することで、任意のテキストを追加することができます。
    \end{info}

    \addinfo{Date}{10.03.2013}

    \section{5段階評価}
    \singlemark{どのくらいの頻度でSDAPSを利用していますか?}{ほとんどない}{毎日}

  \end{questionnaire}
\end{document}
```

あらかじめ定義されている定型文の一部についても置換が可能です。

```text:translator-sdaps-dictionary-English.dictの例
\ProvidesDictionary{translator-sdaps-dictionary}{English}

\providetranslation{infotext}{記入方法/Instructions to fill out the form.}
\providetranslation{standard-deviation}{Standard-Deviation}
\providetranslation{info-cross}{マーク例/Check}
\providetranslation{info-correct}{修正例/Uncheck to correct}
\providetranslation{answers}{Answers}
\providetranslation{questionnaireid}{Questionnaire-ID:}
\providetranslation{surveyid}{Survey-ID:}
\providetranslation{draft}{draft}
\providetranslation{info-mark}{For questions with a range (1-5) choose the answer the mark that fits best.}
\providetranslation{info-select}{満足度などについては、5段階で 1つにマークしてください。}
\providetranslation{mean}{Mean}
```

example.texとtranslator-sdaps-dictionary-English.dictを、作業用ディレクトリ(dev.proj1)に配置した様子は次のようになります。

```bash:
$ tree proj/
proj/
├── example.tex
└── translator-sdaps-dictionary-English.dict

0 directories, 2 files
```

### 【補足】オリジナルの translator-sdaps-dictionary-English.dict の入手方法

Ubuntuなどでパッケージからインストールできる場合は、/usr/share/sdaps/tex/ からコピーすれば良いのですが、それができない場合にDockerイメージから抽出する方法をメモしておきます。

"docker cp"はイメージからのファイル抽出には使えないので、少々面倒ですが、次のような方法でオリジナルのファイルを抽出することができます。

```bash:コンテナ内部のファイルをローカルファイルシステムにコピーする
## sdapsコマンドを実行する("-rm"オプションは付けない)
$ docker run yasuhiroabe/sdaps-ja:ub2204-20231004

## コンテナIDを確認する
$ docker ps -a |grep sdaps-ja
a9e5dcb2a026        yasuhiroabe/sdaps-ja:ub2204-20231004   "/run.sh --help"         17 seconds ago      Exited (0) 16 s
econds ago                       eloquent_haibt

## コンテナIDを利用して必要なファイルをコピーする
$ docker cp a9e5dcb2a026:/usr/share/sdaps/tex/translator-sdaps-dictionary-English.dict my-translator.dict
```

## setupコマンド

回答票のTeXファイルを作成したら、sdapsコマンドにプロジェクト・ディレクトリを作成させます。事前にプロジェクト名でディレクトリを作成するとエラーになります。

```bash:setup実行例
$ docker run --rm \
        -v `pwd`/proj:/proj \
        --name sdaps-ja \
        yasuhiroabe/sdaps-ja:ub2204-20231004 \
        setup \
        --add translator-sdaps-dictionary-English.dict \
        work/ example.tex
```

```text:コマンド実行時のメッセージ
This is XeTeX, Version 3.14159265-2.6-0.999991 (TeX Live 2019/Debian) (preloaded format=xelatex)
 restricted \write18 enabled.
entering extended mode
This is XeTeX, Version 3.14159265-2.6-0.999991 (TeX Live 2019/Debian) (preloaded format=xelatex)
 restricted \write18 enabled.
entering extended mode
...
Running xelatex now multiple times to generate the questionnaire.
Running xelatex now multiple imes to generate the questionnaire.
タイトル
Author: 作者
Date: 10.03.2013
```

指定したプロジェクト・ディレクトリ(dev.proj1/work/)の中に、questionnaire.pdf が配置されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78296/6b669434-e0c3-9e17-451e-9e8a1284fc58.png)

## addコマンド

質問票をスキャンし、TIFFマルチページ形式のファイル(この例では01.tif)をプロジェクト・ディレクトリに配置してから、addを実行します。

```bash:add実行例
$ docker run --rm \
        -v `pwd`/proj:/proj \
        yasuhiroabe/sdaps-ja:ub2204-20231003 add work/ 01.tiff
```

```text:コマンド実行時のメッセージ
Processing 01.tiff
Done
```

使用している機材では1ファイルに25ページ分の回答しか格納できないので、これを越える場合は複数回、addコマンドを実行します。

## recognizeコマンド

回答シートの登録が終った時点で、その登録したシートを解析するフェーズです。

```bash:recognize
$ docker run --rm \
        -v `pwd`/dev.proj1:/proj \
        yasuhiroabe/sdaps-ja:ub2204-20231003 \
        recognize work/
```

```text:コマンド実行時のメッセージ
Connection Error (Failed to connect to socket /var/run/dbus/system_bus_socket: No such file or directory)
Connection Null
...
Warning: 1.tif, 1: Matrix not recognized.
Warning: No page number for page 1.tif, 1 exists.
Warning: Got a wrong survey ID (1.tif, 1)! It is None, but should be 2449560734.
Warning: 1.tif, 3: Matrix not recognized.
Warning: No page number for page 1.tif, 3 exists.
Warning: Got a wrong survey ID (1.tif, 3)! It is None, but should be 2449560734.
4 sheets
Processed 4 of 4 sheets, took 1.852894 seconds
‘‘‘

## report texコマンド

解析した結果を元にレポートPDFファイルを抽出するために使用します。

```bash:report_tex実行例
$ docker run --rm \
    -v `pwd`/proj:/proj \
    yasuhiroabe/sdaps-ja:ub2204-20231003 \
    report tex work/ 
```

```text:コマンド実行時のメッセージ
Unable to init server: Could not connect: Connection refused
Unable to init server: Could not connect: Connection refused
This is XeTeX, Version 3.14159265-2.6-0.999991 (TeX Live 2019/Debian) (preloaded format=xelatex)
 restricted \write18 enabled.
entering extended mode
...
Running xelatex now multiple times to generate the report.
```

処理が終るとプロジェクト・ディレクトリ(dev.proj1/work/)の中に、report_*.pdfファイルが生成されています。
report_texコマンドのオプションに、*--create-tex*オプションがあり、TeXファイルを抽出する機能がありますが、これは/tmpに途中経過を出力するため、もし利用する場合には、適当なディレクトリを/tmpにマウントしてください。この例はGNUmakefile内の、proj2-reporttexタスクに記載しています。

report_1.pdfには次のように結果が出力されます。"Answers:", "Mean:", "Standard-Deviation:"などを日本語化したい場合には、translator-sdaps-dictionary-English.dict を変更してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78296/e5a8bbe4-ff95-a35e-a295-bd33eca04d78.png)

## report reportlabコマンド

Pythonのreportlabライブラリを利用して直接出力を得ることも可能です。

LaTeXと比較すると出力されるレポートの品質が少し異なります。

```bash:report_tex実行例
$ docker run --rm \
    -v `pwd`/proj:/proj \
    yasuhiroabe/sdaps-ja:ub2204-20231003 \
    report reportlab work/ 
```

## csvコマンド

生データをCSV形式で抽出したい場合に利用します。レポートの内容が正しいか検証するために使用します。
Excelで偏差などを計算すると誤差の処理方法の違いから、微妙に違う値が表示されるなどするかもしれません。

```bash:csv実行例
$ docker run --rm \
   -v `pwd`/proj:/proj \
   yasuhiroabe/sdaps-ja:ub2204-20231003 \
   export csv work/ 
```

生成されたCSVファイルは、プロジェクト・ディレクトリの data_*.csv に出力されています。

```text:dev.proj1/work/data_2.csvの内容
questionnaire_id,global_id,empty,valid,recognized,review,verified,1_review,1_1_review,1_1
None,None,0,1,1,,0,,,3
None,None,1,0,1,,0,,,-1
None,None,0,1,1,,0,,,4
None,None,1,0,1,,0,,,-1
```

主な利用例は以上です。

# 作業中に発生した問題

最後に作業中に遭遇した問題についてメモを残しておきます。興味のある方はどうぞ。

## sdaps reportを実行するとエラーが発生する

コンテナの中から実行した時だけ、"Unable to init server: Could not connect: Connection refused"というメッセージが表示されます。

```bash:エラー発生時のコマンドライン
## あらかじめ dev.proj2/ には、質問票のTeXファイルと回答をsdaps addした20200310_survey/ディレクトリを配置しています
$ docker run --rm -v `pwd`/dev.proj2:/proj \
        --name sdaps-ja \
        sdaps-ja report tex 20200310_survey/
```

エラーのタイミングが不明だったので、起動スクリプトを作成し、straceで処理の様子を観察しました。

```bash:デバッグ用run.sh
#!/bin/bash

cd "${WORKING_DIR}"
strace sdaps "$@"
```

そこでメッセージが出力される

```text:
connect(6, {sa_family=AF_UNIX, sun_path=@"/home/sdaps/.cache/broadway1.socket"}, 38) = -1 ECONNREFUSED (Connection refused)
```

どうやら裏で画像イメージを処理するかなにかでロードしているライブラリが初期化処理で、Xサーバーに接続しようとして失敗しているように見えます。
sdapsがちゃんと実行できるローカル環境でもDISPLAY環境変数を稼動していない方に向けると失敗します。

```bash:エラーの再現
$ env DISPLAY="" sdaps report 20200310_survey
------------------------------------------------------------------------------
- SDAPS -- report
------------------------------------------------------------------------------
Unable to init server: Could not connect: Connection refused
Unable to init server: Could not connect: Connection refused
```

このため、DISPLAY環境変数をdocker runの際に--envオプションで追加することにしました。
Ubuntu 20.04のX serverはunix domain socketのみを開いているので(6000番ポートは閉じている)、/tmp/.X11-unixをmountしています。この辺りは、lsofの出力と参考情報にあるリンク先を参考にしています。

それでもDBus周りのエラーは残っていますが、とりあえずスルーすることにします。

```bash:次に発生したDBus周りのエラー
$ docker run --rm \
        -e DISPLAY=:0.0 \
        -v /tmp/.X11-unix:/tmp/.X11-unix \
        -v `pwd`/dev.proj2:/proj \
        --name sdaps-ja \
                sdaps-ja \
        report tex 20200310_survey/  

(sdaps:1): dbind-WARNING **: 13:41:40.375: Couldn't connect to accessibility bus: Failed to connect to socket 
/tmp/dbus-5MKrJQOOU3: Connection refused
```

なおX serverに接続しなくても、エラーのまま処理は正常に終了しているので、実際の利用時にはこれらの変数は設定していません。